<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://bfmhno3.github.io/</id>
  <title type="text">Better Mistakes</title>
  <subtitle type="text">Trial.Error.Iteration.</subtitle>
  <updated>2026-10-19T15:00:00.000Z</updated>
  <author><name>bfmhno3</name></author>
  <link rel="alternate" href="https://bfmhno3.github.io/"/>
  <link rel="self" href="https://bfmhno3.github.io/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-16-export-and-tooling/</id>
      <title type="text">导出、工具链与 CI</title>
      <published>2026-10-19T15:00:00.000Z</published>
      <updated>2026-10-19T15:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-16-export-and-tooling/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 16 篇：我用 Typst 0.15.1 在 tutorial-v1 的干净克隆里跑通 106 个 ok 的 make ci（实测 12.2 秒），用 --pdf-standard a-2b,ua-1、HTML、bundle、typst eval、--timings、--deps 导出并检查了 lab/ch16，用 tinymist 0.15.2 的 LSP 验证了编辑器端的诊断、悬停和格式化，补上模板、测试和 GitHub Actions 工作流，最后列出“Typst 特性 → 第几篇”的索引。</summary>
      <content type="html"><![CDATA[<p>在干净克隆里，<code>nix develop --command make ci</code> 在我的机器上用了 12.2 秒：检查了所有 <code>.typ</code> 的格式，编译了 5 份示例文档、16 章的实验文件和各章 <code>extra.sh</code> 里的额外命令、6 个测试文件，一共打印 106 行 <code>ok</code>，0 行 <code>FAIL</code>。这一篇讲的就是这 12 秒里发生的事，以及我怎么确认“它绿了”这件事是可信的：PDF 的元数据和标准、PNG/SVG/HTML/bundle 几种输出、<code>typst eval</code>、<code>--timings</code>、<code>--deps</code>、测试、格式化、编辑器端的 tinymist、模板、CI。最后把整个系列对一遍。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时，仓库里本篇涉及的文件都已经存在，且都是<strong>最终版</strong>：<code>lab/ch16/</code>（含 <code>extra.sh</code>）、<code>tests/</code> 下的 6 个文件、<code>packages/local/xuan/0.1.0/template/main.typ</code>、<code>.github/workflows/ci.yml</code>、<code>README.md</code>、<code>Makefile</code> 里的 <code>test</code>/<code>fmt</code>/<code>fmt-check</code>/<code>install</code>/<code>ci</code> 几个目标。<code>lib.typ</code>、<code>src/base.typ</code>、<code>src/note.typ</code> 在前面的篇章里已经加完，这一篇没有再改它们。<code>make ch16</code> 和 <code>make ci</code> 通过。
前置：<a href="/posts/typst-by-xuan-15-wasm-plugin/">第 15 篇</a>（Rust 插件和术语表；这一篇的测试和 CI 都要用到它产出的 <code>.wasm</code>）。如果你一路跟着写，到这里 <code>git diff tutorial-v1 --stat</code> 应该什么也不打印，文末有这个检查。</p></div></div>
<section><h2>先看结果：干净克隆里的 make ci<a href="#先看结果干净克隆里的-make-ci"><span>#</span></a></h2><p>我先把标签克隆到一个全新的目录里，再跑一遍：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>--branch</span><span> </span><span>tutorial-v1</span><span> </span><span>https://github.com/bfmhno3/typst-lab.git</span><span> </span><span>/tmp/lab16</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>/tmp/lab16</span></div></div><div><div><div>3</div></div><div><span>( </span><span>time</span><span> nix develop --command make ci ) &gt; </span><span>/tmp/ci.log</span><span> 2&gt;&amp;1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（我克隆的是本机仓库的同一个标签，读者用上面的 GitHub 地址。<code>@preview</code> 缓存在我这台机器上是热的，第 14 篇的包不需要重新下载，后面 CI 一节会谈冷缓存。）</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ grep -c '^ok ' /tmp/ci.log</span></div></div><div><div><div>2</div></div><div><span>106</span></div></div><div><div><div>3</div></div><div><span>$ grep -c FAIL /tmp/ci.log</span></div></div><div><div><div>4</div></div><div><span>0</span></div></div><div><div><div>5</div></div><div><span>$ grep '^real' /tmp/ci.log</span></div></div><div><div><div>6</div></div><div><span>real  0m12.222s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>/tmp/ci.log</code> 里本篇的那一章是这样的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>./scripts/chapter.sh ch16</span></div></div><div><div><div>2</div></div><div><span>ok   lab/ch16/eval.typ</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch16/metadata.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch16/pdf-standards.typ</span></div></div><div><div><div>5</div></div><div><span>ok   pdf-standards a-2b,ua-1</span></div></div><div><div><div>6</div></div><div><span>ok   html</span></div></div><div><div><div>7</div></div><div><span>ok   bundle</span></div></div><div><div><div>8</div></div><div><span>ok   eval items = 3</span></div></div><div><div><div>9</div></div><div><span>ok   eval problems = 4</span></div></div><div><div><div>10</div></div><div><span>ok   timings (18842 events)</span></div></div><div><div><div>11</div></div><div><span>ok   deps</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>12.2 秒花在哪里？<code>Makefile</code> 里决定顺序的是这几行（缩进是制表符）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ci</span><span><span>:</span><span> fmt-check all</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>all</span><span><span>:</span><span> examples </span></span><span>$(</span><span>CHAPTERS</span><span>)</span><span> test</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用 <code>hyperfine</code> 单独量了其中几块：</p>

<table><thead><tr><th>步骤</th><th>命令</th><th>耗时</th></tr></thead><tbody><tr><td>格式检查</td><td><code>hyperfine -w 2 -N "make fmt-check"</code></td><td>22.7 ms ± 1.1 ms</td></tr><tr><td>5 份示例</td><td><code>hyperfine -w 1 -r 5 "make examples"</code></td><td>1.352 s ± 0.015 s</td></tr><tr><td>6 个测试</td><td><code>hyperfine -w 1 -r 5 "make test"</code></td><td>434.5 ms ± 4.2 ms</td></tr><tr><td>16 章（含一次 <code>examples</code>）</td><td><code>make ch01 ch02 ... ch16</code>，单次</td><td>11.44 s</td></tr></tbody></table><p>剩下的零头是 <code>nix develop</code> 进入环境本身（<code>nix develop --command true</code> 0.23 s）。结论很直白：测试只占 0.43 s，时间几乎都在编译各章的实验文件。这个比例是我愿意把测试写得多一点的原因，后面“测试”一节会说怎么写。</p></section>
<section><h2>document：PDF 元数据从哪来<a href="#documentpdf-元数据从哪来"><span>#</span></a></h2><p>PDF 的标题、作者、关键词不是排版出来的，是 <code>document</code> 这个“设置规则”写进文件信息字典和 XMP 里的。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch16/metadata.typ" target="_blank"><code>lab/ch16/metadata.typ</code></a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `document` sets the PDF metadata; the template fills it from `note`.</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>document</span><span>(</span></div></div><div><div><div>3</div></div><div><span>  </span><span>title</span><span>: </span><span>"元数据演示"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>author</span><span>: (</span><span>"bfmhno3"</span><span>, </span><span>"Zhang San"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>  </span><span>keywords</span><span>: (</span><span>"Typst"</span><span>, </span><span>"PDF"</span><span>),</span></div></div><div><div><div>6</div></div><div><span>  </span><span>date</span><span>: </span><span>datetime</span><span>(</span><span>year</span><span>: </span><span>2026</span><span>, </span><span>month</span><span>: </span><span>10</span><span>, </span><span>day</span><span>: </span><span>19</span><span>),</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>=</span><span> 元数据</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>用 </span><span>`pdfinfo`</span><span> 或 </span><span>`exiftool`</span><span> 查看这份 PDF 的标题、作者和关键词。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdfinfo out/ch16/metadata.pdf</code>（这台机器没有 <code>exiftool</code>，我用的是 poppler 的 <code>pdfinfo</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Title:           元数据演示</span></div></div><div><div><div>2</div></div><div><span>Keywords:        Typst, PDF</span></div></div><div><div><div>3</div></div><div><span>Author:          bfmhno3, Zhang San</span></div></div><div><div><div>4</div></div><div><span>Creator:         Typst 0.15.1</span></div></div><div><div><div>5</div></div><div><span>CreationDate:    Sun Oct 18 17:00:00 2026 PDT</span></div></div><div><div><div>6</div></div><div><span>ModDate:         Sun Oct 18 17:00:00 2026 PDT</span></div></div><div><div><div>7</div></div><div><span>Custom Metadata: no</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>作者的数组被 <code>pdfinfo</code> 用逗号连起来了。时间看起来差了一天，是因为 <code>datetime(year:, month:, day:)</code> 不带时分秒，取当天 00&lt;00&gt;，而这台机器的时区让 <code>pdfinfo</code> 把它换算成了 PDT。换成 UTC 就对了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ TZ=UTC pdfinfo out/ch16/metadata.pdf | grep CreationDate</span></div></div><div><div><div>2</div></div><div><span>CreationDate:    Mon Oct 19 00:00:00 2026 UTC</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不写 <code>date</code> 会怎样？<code>examples/notes</code> 就没写，它的创建时间是编译那一刻（<code>Mon Oct 19 09:12:56 2026 PDT</code>，是我跑 <code>make ci</code> 的时间；前一次编译读到的是 <code>08:54:07</code>），每次编译都不同。要让 PDF 的元数据可复现，就要显式写 <code>date</code>。<code>flake.nix</code> 的 <code>shellHook</code> 里有一行 <code>unset SOURCE_DATE_EPOCH</code>，也是为了让 <code>datetime.today()</code> 在 nix 里取到真实日期（否则它会返回 1980-01-01）。</p><p><code>xuan</code> 的 <code>note</code> 在第 4 篇就已经替你做了这件事，<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/note.typ" target="_blank"><code>src/note.typ</code></a> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>set document(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>title: title,</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>author: ensure-array(author),</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>keywords: keywords,</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（这是该文件第 20 到 24 行的原样引用，函数头和其余部分在第 4、10 篇已经展示。）<code>pdfinfo out/examples/notes.pdf</code> 因此直接给出 <code>Title: 线性代数笔记：最小二乘与 SVD</code>、<code>Keywords: 最小二乘, SVD, 伪逆</code>、<code>Author: bfmhno3</code>。模板的价值之一就在这里：作者只写一次 <code>title:</code>，页面上的标题块、页眉和 PDF 元数据三处同时对上。</p></section>
<section><h2>—pdf-standard：同时满足 PDF/A 和 PDF/UA<a href="#pdf-standard同时满足-pdfa-和-pdfua"><span>#</span></a></h2><p><a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch16/pdf-standards.typ" target="_blank"><code>lab/ch16/pdf-standards.typ</code></a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Compiled twice: plain, and with `--pdf-standard a-2b,ua-1` (see extra.sh).</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>document</span><span>(</span><span>title</span><span>: </span><span>"PDF/A 与 PDF/UA"</span><span>, </span><span>author</span><span>: </span><span>"bfmhno3"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>=</span><span> 带标签的 PDF</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>标准要求字体全部嵌入、文档有标题和语言，PDF/UA 还要求图片有替代文字。</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>10</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>3cm</span><span>, </span><span>height</span><span>: </span><span>1cm</span><span>, </span><span>fill</span><span>: </span><span>aqua</span><span>),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>alt</span><span>: </span><span>"一个浅蓝色矩形"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>caption</span><span>: [图片必须有 alt。],</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个文件编译了两次：<code>chapter.sh</code> 的通用循环出一份不带标准的 PDF，<code>extra.sh</code> 再带 <code>--pdf-standard a-2b,ua-1</code> 出一份。0.15 起这个选项可以接逗号分隔的多个标准，一个归档标准（PDF/A）和一个无障碍标准（PDF/UA）可以叠在一起。我从 XMP 元数据里把标准声明抠出来对比：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdfinfo -meta out/ch16/pdf-standards.pdf | grep -Eo '&lt;pdfaid:[a-z]*&gt;[^&lt;]*|&lt;pdfuaid:[a-z]*&gt;[^&lt;]*|&lt;pdf:PDFVersion&gt;[^&lt;]*'</span></div></div><div><div><div>2</div></div><div><span>&lt;pdf:PDFVersion&gt;1.7</span></div></div><div><div><div>3</div></div><div><span>$ pdfinfo -meta out/ch16/pdf-standards-a2b-ua1.pdf | grep -Eo '&lt;pdfaid:[a-z]*&gt;[^&lt;]*|&lt;pdfuaid:[a-z]*&gt;[^&lt;]*|&lt;pdf:PDFVersion&gt;[^&lt;]*'</span></div></div><div><div><div>4</div></div><div><span>&lt;pdfaid:part&gt;2</span></div></div><div><div><div>5</div></div><div><span>&lt;pdfaid:conformance&gt;B</span></div></div><div><div><div>6</div></div><div><span>&lt;pdfuaid:part&gt;1</span></div></div><div><div><div>7</div></div><div><span>&lt;pdf:PDFVersion&gt;1.7</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>普通的那份没有任何标准声明；带标准的那份声明了 PDF/A-2b 和 PDF/UA-1。文件大小从 25566 字节涨到 29557 字节，我猜多出来的主要是 XMP 里那批 PDF/A 扩展 schema（<code>pdfinfo -meta</code> 的输出里能看到 <code>pdfaExtension:schemas</code>），没有逐字节核对。<code>pdffonts</code> 显示四个字体子集（两个 Noto Serif CJK、两个 Libertinus Serif）全部 <code>emb yes</code>。</p><p>至于 <code>Tagged: yes</code>：两份 PDF 都是。<code>typst compile --help</code> 对 <code>--no-pdf-tags</code> 的说明是，即使不要求 PDF/UA，Typst 默认也会写出带标签的 PDF 作为无障碍基线。所以带标准的那份并不是“多了标签”，而是“多了检查”。检查不过会直接报错，我写了两个会失败的小文件（不在仓库里，在 <code>/tmp</code> 里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>document</span><span>(</span><span>title</span><span>: </span><span>"t"</span><span>, </span><span>author</span><span>: </span><span>"a"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>(</span><span>width</span><span>: </span><span>3cm</span><span>, </span><span>height</span><span>: </span><span>1cm</span><span>, </span><span>fill</span><span>: </span><span>aqua</span><span>), </span><span>caption</span><span>: [x])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --pdf-standard a-2b,ua-1 noalt.typ noalt.pdf</span></div></div><div><div><div>2</div></div><div><span>error: PDF/UA-1 error: missing alt text</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ ../p16/noalt.typ:2:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>2 │ #figure(rect(width: 3cm, height: 1cm, fill: aqua), caption: [x])</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= hint: make sure your images and equations have alt text</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>另一个文件只有一行 <code>= hi</code>，用 <code>--pdf-standard ua-1</code> 编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: PDF/UA-1 error: missing document title</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>= hint: set the title with `set document(title: [...])`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个标准的组合也有硬约束。PDF/A 只能选一个，A-4 要求 PDF 2.0 而 UA-1 只到 1.7：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --pdf-standard a-1b,a-2b ...</span></div></div><div><div><div>2</div></div><div><span>error: choose at most one PDF/A standard</span></div></div><div><div><div>3</div></div><div><span>$ typst compile --pdf-standard a-4,ua-1 ...</span></div></div><div><div><div>4</div></div><div><span>error: PDF/A-4 and PDF/UA-1 are mutually incompatible because they do not have any overlapping PDF versions</span></div></div><div><div><div>5</div></div><div><span>hint: PDF/A-4 requires version PDF 2.0</span></div></div><div><div><div>6</div></div><div><span>hint: PDF/UA-1 requires a version between PDF 1.4 and PDF 1.7</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我试了 <code>a-3b,ua-1</code>，通过；<code>xuan</code> 的几份模板我没有逐个拿去过这个检查，所以不声称它们都满足 PDF/UA（<code>note</code> 里的图要带 <code>alt</code>，这需要写文档的人负责）。</p></section>
<section><h2>PNG 与 SVG：—ppi 和页码模板<a href="#png-与-svgppi-和页码模板"><span>#</span></a></h2><p>位图和矢量图的输出用同一组选项：<code>--format png|svg</code>，<code>--pages</code> 选页，<code>--ppi</code> 定位图的分辨率（默认 144），输出路径里的 <code>{p}</code> 和 <code>{0p}</code> 是页码模板。我拿 17 页的 <code>examples/slides</code> 来试：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>.</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>--pages</span><span> </span><span>1-3</span><span> </span><span>--format</span><span> </span><span>png</span><span> </span><span>--ppi</span><span> </span><span>72</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>examples/slides/main.typ</span><span> </span><span>"/tmp/p16/png/s-{0p}.png"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ls -l /tmp/p16/png</span></div></div><div><div><div>2</div></div><div><span>-rw-r--r-- 1 bfmhno3 users 14794 Oct 19 08:55 s-01.png</span></div></div><div><div><div>3</div></div><div><span>-rw-r--r-- 1 bfmhno3 users 11569 Oct 19 08:55 s-02.png</span></div></div><div><div><div>4</div></div><div><span>-rw-r--r-- 1 bfmhno3 users 27361 Oct 19 08:55 s-03.png</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条命令（<code>--pages 1-3</code>，72 ppi）的挂钟时间是 0.137 s。注意文件名是 <code>01</code> 而不是 <code>1</code>：我只导出了前 3 页，文件名却补了零，我猜是 <code>{0p}</code> 按文档的总页数（17 页，宽度 2）补零，没有去核对源码。<code>{p}</code> 则不补零。同一份文档导出 SVG：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile ... --format svg examples/slides/main.typ "/tmp/p16/png/v-{p}.svg"</span></div></div><div><div><div>2</div></div><div><span>$ ls /tmp/p16/png | grep svg | head -20 | tr "\n" " "</span></div></div><div><div><div>3</div></div><div><span>v-10.svg v-11.svg v-12.svg v-13.svg v-14.svg v-15.svg v-16.svg v-17.svg v-1.svg v-2.svg v-3.svg v-4.svg v-5.svg v-6.svg v-7.svg v-8.svg v-9.svg</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>17 个文件，<code>v-1.svg</code> 到 <code>v-17.svg</code>。想按字典序排好就用 <code>{0p}</code>，这是它存在的全部理由。如果输出多页却不写模板，Typst 直接拒绝：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: cannot export multiple images without a page number template ({p}, {0p}) in the output path</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>--ppi</code> 控制像素尺寸，不改变页面的物理尺寸。<code>examples/slides</code> 的第 1 页是 16&lt;9&gt; 的演示页：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ file s-01.png s144.png s288.png      # 分别是 --ppi 72、144、288</span></div></div><div><div><div>2</div></div><div><span>s-01.png: PNG image data, 842 x 474, ...</span></div></div><div><div><div>3</div></div><div><span>s144.png: PNG image data, 1684 x 947, ...</span></div></div><div><div><div>4</div></div><div><span>s288.png: PNG image data, 3368 x 1894, ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>像素数正好随 ppi 线性变化。博客里嵌图我一直用 144，是在清晰度和体积之间的折中，没有更深的道理。</p></section>
<section><h2>HTML 与 MathML<a href="#html-与-mathml"><span>#</span></a></h2><p>HTML 导出在 0.15.1 里仍然要用 <code>--features html</code> 打开，编译器会明说它在开发中。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch16/html/site.typ" target="_blank"><code>lab/ch16/html/site.typ</code></a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Compiled with `--features html --format html` (see ../extra.sh).</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>document</span><span>(</span><span>title</span><span>: </span><span>"HTML 导出"</span><span>, </span><span>author</span><span>: </span><span>"bfmhno3"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>=</span><span> HTML 导出</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>段落、*强调*与行内公式 </span><span>$</span><span>x^2 + y^2 = z^2</span><span>$</span><span> 会变成语义化的标签，公式导出为 MathML。</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>- 列表</span></div></div><div><div><div>9</div></div><div><span>- 也一样</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#html</span><span>.</span><span>elem</span><span>(</span><span>"div"</span><span>, </span><span>attrs</span><span>: (</span><span>class</span><span>: </span><span>"note"</span><span>))[自定义元素：</span><span>`html.elem`</span><span>。]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译后的 <code>out/ch16/site.html</code> 一共 2109 字节，其中一大半是 <code>&lt;head&gt;</code> 里内联的 MathML 样式表。我只取 <code>&lt;body&gt;</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ grep -o '&lt;body&gt;.*' out/ch16/site.html</span></div></div><div><div><div>2</div></div><div><span>&lt;body&gt;&lt;h2&gt;HTML 导出&lt;/h2&gt;&lt;p&gt;段落、&lt;strong&gt;强调&lt;/strong&gt;与行内公式 &lt;math&gt;&lt;msup&gt;&lt;mi&gt;𝑥&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;msup&gt;&lt;mi&gt;𝑦&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;msup&gt;&lt;mi&gt;𝑧&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;/math&gt; 会变成语义化的标签，公式导出为 MathML。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;列表&lt;/li&gt;&lt;li&gt;也一样&lt;/li&gt;&lt;/ul&gt;&lt;div class="note"&gt;自定义元素：&lt;code&gt;html.elem&lt;/code&gt;。&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>几个值得看的点：</p><ul>
<li><code>= HTML 导出</code> 变成了 <code>&lt;h2&gt;</code>，不是 <code>&lt;h1&gt;</code>。我猜 Typst 把一级标题降了一级，是为了留出 <code>&lt;h1&gt;</code> 给页面标题，但没有查文档证实；<code>&lt;title&gt;</code> 里确实是 <code>HTML 导出</code>。</li>
<li>公式是 <code>&lt;math&gt;</code> 加 <code>&lt;msup&gt;</code>、<code>&lt;mi&gt;</code>、<code>&lt;mo&gt;</code> 这套 MathML，不是图片也不是 SVG。字母 <code>x</code> 写成了 <code>𝑥</code>（U+1D465，Unicode 的数学斜体字母），不是普通的 <code>x</code>。</li>
<li><code>html.elem("div", attrs: (class: "note"))[...]</code> 是通往原始 HTML 的后门：标签名和属性自己指定。</li>
<li>编译时 stderr 有一段警告：<code>warning: html export is under active development and incomplete</code>，附 <code>hint: its behaviour may change at any time</code> 和一个 issue 链接。不加 <code>--features html</code> 则报错 <code>error: html export is only available when </code>—features html<code> is passed</code>。环境变量 <code>TYPST_FEATURES=html</code> 与命令行开关效果相同：我用环境变量编译出的文件和上面那份 <code>cmp</code> 比较，逐字节相同。</li>
</ul><p>还有一个细节。上面的 <code>&lt;html lang="en"&gt;</code>：这个实验文件没有 <code>#set text(lang: "zh")</code>，所以尽管内容是中文，<code>lang</code> 仍是默认的 <code>en</code>。块公式（<code>display="block"</code>）我在 <code>/tmp</code> 里单独试了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>document</span><span>(</span><span>title</span><span>: </span><span>"t"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 标题</span></div></div><div><div><div>4</div></div><div><span>块公式：</span></div></div><div><div><div>5</div></div><div><span>$</span><span> A = U </span><span>Sigma</span><span> V^T </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;html lang="zh"&gt;</span></div></div><div><div><div>2</div></div><div><span>&lt;body&gt;&lt;h2&gt;标题&lt;/h2&gt;&lt;p&gt;块公式：&lt;/p&gt;&lt;math display="block"&gt;&lt;mi&gt;𝐴&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mi&gt;𝑈&lt;/mi&gt;&lt;mi mathvariant="normal"&gt;Σ&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;𝑉&lt;/mi&gt;&lt;mi&gt;𝑇&lt;/mi&gt;&lt;/msup&gt;&lt;/math&gt;&lt;/body&gt;&lt;/html&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>lang</code> 传到了 <code>&lt;html lang="zh"&gt;</code>，块公式带 <code>display="block"</code>，希腊字母 <code>Σ</code> 是 <code>mathvariant="normal"</code>（直立），拉丁字母 <code>A</code>、<code>U</code>、<code>V</code>、<code>T</code> 是 Unicode 数学斜体字母。</p></section>
<section><h2>bundle：一次编译出一个站点<a href="#bundle一次编译出一个站点"><span>#</span></a></h2><p>bundle 是 0.15 的实验性导出：一份源文件里用 <code>document(...)</code> 声明多个输出文档，用 <code>asset(...)</code> 声明原样输出的文件，整个编译产出一个目录。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch16/bundle/site.typ" target="_blank"><code>lab/ch16/bundle/site.typ</code></a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Compiled with `--features bundle,html --format bundle` (see ../extra.sh).</span></div></div><div><div><div>2</div></div><div><span>#document</span><span>(</span><span>"index.html"</span><span>, </span><span>title</span><span>: [首页])[</span></div></div><div><div><div>3</div></div><div><span>  </span><span>=</span><span> 首页</span></div></div><div><div><div>4</div></div><div><span>  </span><span>#link</span><span>(</span><span>&lt;blog&gt;</span><span>)[进入博客] 或下载 </span><span>#link</span><span>(</span><span>&lt;blog-pdf&gt;</span><span>)[PDF 版]。</span></div></div><div><div><div>5</div></div><div><span>]</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>post</span><span><span> </span><span>= </span><span>[</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>==</span><span> 博客</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>同一份内容同时输出为网页和 PDF。</span></div></div><div><div><div>10</div></div><div><span>]</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#document</span><span>(</span><span>"blog.html"</span><span>, </span><span>title</span><span>: [博客])[</span><span>#post</span><span>] </span><span>&lt;blog&gt;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#document</span><span>(</span><span>"blog.pdf"</span><span>, </span><span>title</span><span>: [博客])[</span><span>#post</span><span>] </span><span>&lt;blog-pdf&gt;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>#asset</span><span>(</span><span>"hello.txt"</span><span>, </span><span>bytes</span><span>(</span><span>"hello from bundle"</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意命令行要同时打开 <code>bundle</code> 和 <code>html</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>.</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>--features</span><span> </span><span>bundle,html</span><span> </span><span>--format</span><span> </span><span>bundle</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>lab/ch16/bundle/site.typ</span><span> </span><span>out/ch16/bundle</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只开 <code>bundle</code> 会失败，因为里面有 <code>.html</code> 文档：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: html export is only available when the `html` feature is enabled</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch16/bundle/site.typ:2:1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译成功后输出路径是一个目录：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ls -l out/ch16/bundle</span></div></div><div><div><div>2</div></div><div><span>-rw-r--r-- 1 bfmhno3 users   243 Oct 19 09:13 blog.html</span></div></div><div><div><div>3</div></div><div><span>-rw-r--r-- 1 bfmhno3 users 14093 Oct 19 09:13 blog.pdf</span></div></div><div><div><div>4</div></div><div><span>-rw-r--r-- 1 bfmhno3 users    17 Oct 19 09:13 hello.txt</span></div></div><div><div><div>5</div></div><div><span>-rw-r--r-- 1 bfmhno3 users   277 Oct 19 09:13 index.html</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出格式由文件名后缀决定：<code>blog.html</code> 是网页，<code>blog.pdf</code> 是一份真的 PDF（<code>pdfinfo</code> 读出 <code>Title: 博客</code>、<code>Pages: 1</code>）。最妙的是交叉引用，<code>index.html</code> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;p&gt;&lt;a href="blog.html"&gt;进入博客&lt;/a&gt; 或下载 &lt;a href="blog.pdf"&gt;PDF 版&lt;/a&gt;。&lt;/p&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>#link(&lt;blog&gt;)</code> 指向的是另一个文档的标签，在 HTML 里变成了相对路径 <code>blog.html</code>。同一份 <code>post</code> 内容被同时放进网页和 PDF，这也是我以后想用它的场景：同一个笔记，既能在网上读又能打印。至于 <code>==</code> 在 <code>blog.html</code> 里是 <code>&lt;h3&gt;</code>：它在 <code>document</code> 里是二级标题，降一级之后就是 <code>h3</code>。</p><p>stderr 里有 <code>warning: bundle export is experimental</code> 和 <code>hint: do not rely on this feature for production use cases</code>。我听从它：这一章只验证它能编译，没有拿它生产任何东西。</p></section>
<section><h2>typst eval：不生成文档，只问一个问题<a href="#typst-eval不生成文档只问一个问题"><span>#</span></a></h2><p><code>typst eval &lt;表达式&gt; --in &lt;文件&gt;</code>（取代已弃用的 <code>typst query</code>）在那份文档的上下文里求值一个表达式，把结果序列化成 JSON 或 YAML 打印。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch16/eval.typ" target="_blank"><code>lab/ch16/eval.typ</code></a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A small target for `typst eval`: three labelled items and two headings.</span></div></div><div><div><div>2</div></div><div><span>=</span><span> 标题一</span></div></div><div><div><div>3</div></div><div><span>==</span><span> 标题二</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#metadata</span><span>(</span><span>"甲"</span><span>) </span><span>&lt;item&gt;</span></div></div><div><div><div>6</div></div><div><span>#metadata</span><span>(</span><span>"乙"</span><span>) </span><span>&lt;item&gt;</span></div></div><div><div><div>7</div></div><div><span>#metadata</span><span>(</span><span>"丙"</span><span>) </span><span>&lt;item&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval --root . "query(&lt;item&gt;).len()" --in lab/ch16/eval.typ</span></div></div><div><div><div>2</div></div><div><span>3</span></div></div><div><div><div>3</div></div><div><span>$ typst eval --root . "query(&lt;item&gt;).map(m =&gt; m.value)" --in lab/ch16/eval.typ</span></div></div><div><div><div>4</div></div><div><span>["甲","乙","丙"]</span></div></div><div><div><div>5</div></div><div><span>$ typst eval --root . --format yaml "query(&lt;item&gt;).map(m =&gt; m.value)" --in lab/ch16/eval.typ</span></div></div><div><div><div>6</div></div><div><span>- 甲</span></div></div><div><div><div>7</div></div><div><span>- 乙</span></div></div><div><div><div>8</div></div><div><span>- 丙</span></div></div><div><div><div>9</div></div><div><span>$ typst eval --root . --pretty "query(heading).map(h =&gt; h.level)" --in lab/ch16/eval.typ</span></div></div><div><div><div>10</div></div><div><span>[</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>1,</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>2</span></div></div><div><div><div>13</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第 12 篇里我用 <code>query</code> 在文档内部数过 <code>&lt;xuan-problem&gt;</code>，现在它跑在文档外面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval --root . --in examples/homework/main.typ "query(&lt;xuan-problem&gt;).len()"</span></div></div><div><div><div>2</div></div><div><span>4</span></div></div><div><div><div>3</div></div><div><span>$ typst eval --root . --in examples/homework/main.typ "query(&lt;xuan-problem&gt;).map(m =&gt; m.value)"</span></div></div><div><div><div>4</div></div><div><span>[25,25,20,30]</span></div></div><div><div><div>5</div></div><div><span>$ typst eval --root . --in examples/homework/main.typ "query(&lt;xuan-problem&gt;).map(m =&gt; m.value).sum()"</span></div></div><div><div><div>6</div></div><div><span>100</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>作业的总分 100 不用打开 PDF 就能读出来，<code>make ch16</code> 把第一条的结果存进 <code>out/ch16/eval.txt</code>（内容是 <code>4</code>），这就是仓库里的“集成测试”：模板能不能把题目的分值正确地暴露给 <code>query</code>。不带 <code>--in</code> 时它就是一个计算器：<code>typst eval "range(1, 6).map(x =&gt; x * x).sum()"</code> 打印 <code>55</code>。</p><p>有一条边界我是撞出来的。<code>--in</code> 带来的是那份文档的<strong>内省数据</strong>（<code>query</code> 能查到东西），不是它的绑定：我在 <code>eval.typ</code> 上调用 <code>plain-text</code>，得到</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unknown variable: plain-text</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ &lt;input-expression&gt;:1:34</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ query(heading).map(h =&gt; (h.level, plain-text(h.body)))</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│                                   ^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我没有去查 <code>typst eval</code> 对表达式作用域的规定，只记录这次的结果：<code>eval.typ</code> 里没有 <code>import xuan</code>，表达式没有看到 <code>plain-text</code>。内容值被序列化时也不是“文本”：我把 <code>h.body</code> 换成 <code>repr(h.body)</code>，得到 <code>[[1,"[标题一]"],[2,"[标题二]"]]</code>，标题的内容被写成了字符串 <code>"[标题一]"</code>。要做更多处理就在表达式里自己拼，或者像第 7 篇那样把数据放进 <code>metadata</code>。<code>--target html|bundle</code> 也能选（<code>typst eval --help</code> 里列了 <code>paged</code>、<code>html</code>、<code>bundle</code> 三个值），我没有用。</p></section>
<section><h2>—timings 与 —deps：看编译器在忙什么<a href="#timings-与-deps看编译器在忙什么"><span>#</span></a></h2><p><code>--timings</code> 写出一个 JSON 数组，每个元素有 <code>name</code>、<code>cat</code>、<code>ph</code>、<code>ts</code>、<code>pid</code>、<code>tid</code>、<code>args</code> 这几个字段，是 Chrome 追踪事件的形状（<code>typst compile --help</code> 说这个 JSON 可以载入 <a href="https://ui.perfetto.dev" target="_blank">https://ui.perfetto.dev</a> 之类的追踪工具，该选项仍是实验性的；我没有试过），<code>extra.sh</code> 里的那一行是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>.</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>--timings</span><span> </span><span>out/ch16/timings.json</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>examples/notes/main.typ</span><span> </span><span>out/ch16/notes-timed.pdf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这份 4 页的笔记一共写出 18842 个事件，开头两个是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ jq length out/ch16/timings.json</span></div></div><div><div><div>2</div></div><div><span>18842</span></div></div><div><div><div>3</div></div><div><span>$ jq -c '.[0:2]' out/ch16/timings.json</span></div></div><div><div><div>4</div></div><div><span>[{"name":"compile once","cat":"typst","ph":"B","ts":0.0,"pid":1,"tid":1,"args":null},{"name":"compile","cat":"typst","ph":"B","ts":1.112,"pid":1,"tid":1,"args":null}]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>ph</code> 是 <code>B</code>（开始）或 <code>E</code>（结束），<code>ts</code> 是时间戳。我写了一个 jq 程序（<code>timing.jq</code>，在 <code>/tmp</code> 里，不在仓库里），把每个名字的最早开始到最晚结束之间的跨度算出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>map(select(.name | test("^(compile|iter \\(.\\))$")))</span></div></div><div><div><div>2</div></div><div><span>| group_by(.name)[]</span></div></div><div><div><div>3</div></div><div><span>| "\(.[0].name)\t\(((map(select(.ph == "E").ts) | max) - (map(select(.ph == "B").ts) | min)) / 1000 * 10 | round / 10) ms"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ jq -r -f timing.jq out/ch16/timings.json</span></div></div><div><div><div>2</div></div><div><span>compile  207.5 ms</span></div></div><div><div><div>3</div></div><div><span>iter (1)  102.8 ms</span></div></div><div><div><div>4</div></div><div><span>iter (2)  60.5 ms</span></div></div><div><div><div>5</div></div><div><span>iter (3)  10.3 ms</span></div></div><div><div><div>6</div></div><div><span>iter (4)  2.2 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里我把 <code>ts</code> 当作微秒（除以 1000 得到毫秒）。依据是 <code>compile</code> 的跨度 207.5 ms 小于我用 <code>hyperfine -w 2 -r 10 'typst compile --root . --ignore-system-fonts examples/notes/main.typ out.pdf'</code> 量到的整条命令 248.5 ms ± 1.9 ms（进程启动、发现字体和导出也在里面），数量级吻合，但文档里没有写，所以只是合理的猜测。数字本身是确定的：笔记的布局迭代了 4 轮，第一轮 102.8 ms，随后 60.5、10.3、2.2 ms。这正是第 7 篇说的“多遍编译”：每一轮用上一轮的 <code>counter</code>、<code>state</code>、<code>query</code> 结果重新排版，直到结果不再变化。我猜第四轮就是“确认已稳定”的那一遍，所以它最便宜（事件名里确实有 <code>check stabilized</code>）。事件名里还有 <code>call plugin</code>：整份文档调用了一次 Rust 插件（第 15 篇术语表的排序键），也出现在时间线上。开始事件（<code>ph</code> 为 <code>B</code>）数得最多的是 <code>func call</code>（2157 个）、<code>handle text</code>（1837）、<code>realize</code>（837）和 <code>math glyph layout</code>（708）。</p><p>四次运行里 <code>compile</code> 的跨度是 207.5、209.6、224.2 和 242 ms（分别在不同的克隆和时间），所以 207.5 ms 不是一个稳定值，只是这一次的读数。</p><p><code>--deps</code> 写出这次编译读取了哪些文件，格式由 <code>--deps-format</code> 选 <code>json</code>（默认）、<code>zero</code>（以 NUL 分隔，能表达任意路径）或 <code>make</code>（输出 Makefile 的依赖行）。<code>extra.sh</code> 对第 12 篇的模块实验这样用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat out/ch16/deps.json</span></div></div><div><div><div>2</div></div><div><span>{"inputs":["lab/ch12/modules/b.typ","lab/ch12/modules/a.typ","lab/ch12/modules/main.typ"],"outputs":["out/ch16/deps-demo.pdf"]}</span></div></div><div><div><div>3</div></div><div><span>$ typst compile --root . --ignore-system-fonts --deps - --deps-format make lab/ch12/modules/main.typ /tmp/p16/f/d.pdf</span></div></div><div><div><div>4</div></div><div><span>/tmp/p16/f/d.pdf: lab/ch12/modules/b.typ lab/ch12/modules/a.typ lab/ch12/modules/main.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make</code> 格式正好是一条 Makefile 规则，按 Makefile 的语法可以用 <code>-include</code> 引入它来让 <code>make</code> 跟踪依赖（我没有试过）。笔记示例的依赖要多得多：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ jq ".inputs | length" deps2.json</span></div></div><div><div><div>2</div></div><div><span>23</span></div></div><div><div><div>3</div></div><div><span>$ jq -r ".inputs[]" deps2.json | grep -vE "src/|lib.typ"</span></div></div><div><div><div>4</div></div><div><span>examples/notes/glossary.yaml</span></div></div><div><div><div>5</div></div><div><span>examples/notes/main.typ</span></div></div><div><div><div>6</div></div><div><span>examples/notes/refs.bib</span></div></div><div><div><div>7</div></div><div><span>packages/local/xuan/0.1.0/typst.toml</span></div></div><div><div><div>8</div></div><div><span>packages/local/xuan/0.1.0/plugin/xuan_pinyin.wasm</span></div></div><div><div><div>9</div></div><div><span>examples/notes/figs/projection.svg</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>23 个文件，除了 <code>src/</code> 下的 xuan 源码，就是笔记的 <code>.typ</code>、YAML 术语表、<code>.bib</code>、SVG 图、包清单，以及那个 <code>.wasm</code>。这个清单是“改了哪些文件需要重编”的准确答案，比我手写的依赖规则可靠。</p></section>
<section><h2>测试：assert 就是断言框架<a href="#测试assert-就是断言框架"><span>#</span></a></h2><p>Typst 没有测试框架，也不需要：<code>assert</code>、<code>assert.eq</code> 在编译期求值，失败就是编译错误，<code>typst compile</code> 退出码非零。所以 <code>tests/</code> 下每个文件就是一份普通的 <code>.typ</code>，对模块的行为写断言，<code>make test</code> 把它们逐个编译成一次性的 PDF：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>@mkdir -p out/tests</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>@for f in tests/*.typ; do \</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>echo "typst compile </span><span>$$</span><span>f"; \</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>typst compile </span><span>$(</span><span>TFLAGS</span><span>)</span><span> </span><span>$$</span><span>f out/tests/</span><span>$$</span><span>(basename </span><span>$$</span><span>f .typ).pdf || exit 1; \</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>六个文件，共 109 行，一个模块一个文件。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/tests/units.typ" target="_blank"><code>tests/units.typ</code></a> 是最直白的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>thin</span><span><span> </span><span>= </span></span><span>"</span><span>\u{2009}</span><span>"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// Groups of three from the decimal point outwards, from 5 digits up.</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"1234567.891"</span><span>), </span><span>"1"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"234"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"567.891"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"1234"</span><span>), </span><span>"1234"</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"12345"</span><span>), </span><span>"12"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"345"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"0.123456"</span><span>), </span><span>"0."</span><span><span> </span><span>+</span><span> </span></span><span>"123"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"456"</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span><span>(</span><span>-</span></span><span>1234567</span><span>), </span><span>"</span><span>\u{2212}</span><span>1"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"234"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"567"</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// Quantities: upright unit, thin space after the number, `^n` superscripts.</span></div></div><div><div><div>13</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>qty</span><span>(</span><span>9.81</span><span>, </span><span>"m/s^2"</span><span>)), </span><span>"9.81"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"m/s2"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span></div></div><div><div><div>15</div></div><div><span>  </span><span>plain-text</span><span>(</span><span>qty-range</span><span>(</span><span>0</span><span>, </span><span>50</span><span>, </span><span>"mm"</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)),</span></div></div><div><div><div>16</div></div><div><span>  </span><span>"0～50"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"mm"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div><div><div><div>18</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>qty-range</span><span>(</span><span>0</span><span>, </span><span>50</span><span>, </span><span>"mm"</span><span>, </span><span>lang</span><span>: </span><span>"en"</span><span>)), </span><span>"0–50"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"mm"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>num</code> 返回字符串，<code>qty</code> 返回 <code>box</code>，所以测试 <code>qty</code> 要先用第 3 篇的 <code>plain-text</code> 把内容摊平成字符串。这就是我当初把 <code>plain-text</code> 放进 <code>util.typ</code> 的真正原因：它是测试的基础设施。</p><p>依赖“编译之后才知道”的行为就要上 <code>context</code>。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/tests/theorem.typ" target="_blank"><code>tests/theorem.typ</code></a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>=</span><span> 第一章</span></div></div><div><div><div>5</div></div><div><span>#theorem</span><span>[甲] </span><span>&lt;t1&gt;</span></div></div><div><div><div>6</div></div><div><span>#lemma</span><span>[乙] </span><span>&lt;t2&gt;</span></div></div><div><div><div>7</div></div><div><span>#proof</span><span>[略。]</span></div></div><div><div><div>8</div></div><div><span>#example</span><span>[丙] </span><span>&lt;t3&gt;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>=</span><span> 第二章</span></div></div><div><div><div>11</div></div><div><span>#theorem</span><span>[丁] </span><span>&lt;t4&gt;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>14</div></div><div><span>  </span><span>// One counter is shared by all environments and restarts at each chapter.</span></div></div><div><div><div>15</div></div><div><span>  </span><span>let</span><span> </span><span>n</span><span>(</span><span>l</span><span><span>) </span><span>= </span></span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>"xuan-theorem"</span><span>)).</span><span>at</span><span>(</span><span>l</span><span>).</span><span>first</span><span>()</span></div></div><div><div><div>16</div></div><div><span>  </span><span>assert</span><span>.</span><span>eq</span><span>((</span><span>n</span><span>(</span><span>&lt;t1&gt;</span><span>), </span><span>n</span><span>(</span><span>&lt;t2&gt;</span><span>), </span><span>n</span><span>(</span><span>&lt;t3&gt;</span><span>), </span><span>n</span><span>(</span><span>&lt;t4&gt;</span><span>)), (</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>1</span><span>))</span></div></div><div><div><div>17</div></div><div><span>  </span><span>// `proof` is unnumbered.</span></div></div><div><div><div>18</div></div><div><span>  </span><span>assert</span><span>.</span><span>eq</span><span>(</span><span>query</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>"xuan-theorem"</span><span>)).</span><span>len</span><span>(), </span><span>4</span><span>)</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>文档本身像往常一样排版，最后的 <code>context</code> 块在整个文档排完之后读取计数器在四个标签处的值：定理、引理、例题共用一个计数器，第二章重新从 1 数。<code>proof</code> 不编号，所以 <code>query</code> 只数到 4 个。其余四个文件的主题：<code>util.typ</code> 测 <code>plain-text</code>、<code>ensure-array</code>、<code>pick</code>；<code>homework.typ</code> 测 3 道有分值的题和 1 道无分值的题的总分是 35；<code>slides.typ</code> 测 3 步的页面会产出 5 页（标题页加 3 步加 1 页普通页）；<code>glossary.typ</code> 测码点顺序与拼音顺序不同，它用到仓库里<strong>已提交的</strong> <code>.wasm</code>。</p><p>测试能不能抓到错误？我临时在 <code>tests/</code> 里放了一个故意写错的文件（跑完删除，没有提交）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"1234"</span><span>), </span><span>"1 234"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts tests/bad-units.typ /tmp/p16/f/x.pdf</span></div></div><div><div><div>2</div></div><div><span>error: equality assertion failed: value "1234" was not equal to "1 234"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ tests/bad-units.typ:3:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>3 │ #assert.eq(num("1234"), "1 234")</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div><span>$ echo $?</span></div></div><div><div><div>8</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>失败信息带着两边的值和位置，和任何测试框架的输出一样有用。局限也明显：没有测试名、没有汇总、第一个失败就中止（<code>|| exit 1</code>）。对 6 个文件、0.43 s 的套件，这些都不算问题。“预期会失败”的行为不放在 <code>tests/</code> 里，由 <code>*.fail.typ</code> 的约定负责：<code>scripts/chapter.sh</code> 要求它们必须失败，退出码 0 反而算错。</p></section>
<section><h2>typstyle：格式化也是一道门<a href="#typstyle格式化也是一道门"><span>#</span></a></h2><p>仓库里每个 Typst 片段（包括这个系列里贴出来的）都过了 <code>typstyle -l 80</code>。<code>make fmt-check</code> 的文件列表来自一条 <code>find</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>TYP_FILES</span><span><span> </span><span>=</span><span> </span></span><span>$(</span><span>shell</span><span> find . -name '</span><span>*</span><span>.typ' -not -path './plugin/</span><span>*</span><span>' -not -path './out/</span><span>*</span><span>' -not -path './.direnv/</span><span>*</span><span>' -not -name '</span><span>*</span><span>.fail.typ')</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fmt-check</span><span>:</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>typstyle --check -l 80 </span><span>$(</span><span>TYP_FILES</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>排除了 <code>*.fail.typ</code>：它们本来就有语法错误，我实测 <code>typstyle --check</code> 对 <code>lab/ch01/unclosed.fail.typ</code> 打印 <code>warn: Failed to parse ... The source is erroneous.</code> 并退出 0，也就是它不会让检查失败，只是多一行噪音。我拿一个故意写乱的文件（<code>/tmp</code> 里，不在仓库里）试了 typstyle 的几种用法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span>   </span><span>greet</span><span>( </span><span>name</span><span>,</span><span>greeting</span><span>:</span><span>"hi"</span><span><span> )</span><span>=</span><span>{</span></span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[</span><span>#greeting</span><span>, </span><span>#name</span><span>!]</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>:(</span><span>"Libertinus Serif"</span><span>,</span><span>"Noto Serif CJK SC"</span><span>),</span><span>size</span><span>:</span><span>11pt</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#greet</span><span>( </span><span>"Typst"</span><span> )</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typstyle --check -l 80 messy.typ</span></div></div><div><div><div>2</div></div><div><span>Would reformat: messy.typ</span></div></div><div><div><div>3</div></div><div><span>exit=1</span></div></div><div><div><div>4</div></div><div><span>$ typstyle --diff -l 80 messy.typ</span></div></div><div><div><div>5</div></div><div><span>--- messy.typ</span></div></div><div><div><div>6</div></div><div><span>+++ messy.typ</span></div></div><div><div><div>7</div></div><div><span>@@ -1,5 +1,5 @@</span></div></div><div><div><div>8</div></div><div><span>-#let   greet( name,greeting:"hi" )={</span></div></div><div><div><div>9</div></div><div><span>- [#greeting, #name!]</span></div></div><div><div><div>10</div></div><div><span>+#let greet(name, greeting: "hi") = {</span></div></div><div><div><div>11</div></div><div><span>+  [#greeting, #name!]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>-#set text(font:("Libertinus Serif","Noto Serif CJK SC"),size:11pt)</span></div></div><div><div><div>14</div></div><div><span>-#greet( "Typst" )</span></div></div><div><div><div>15</div></div><div><span>+#set text(font: ("Libertinus Serif", "Noto Serif CJK SC"), size: 11pt)</span></div></div><div><div><div>16</div></div><div><span>+#greet("Typst")</span></div></div><div><div><div>17</div></div><div><span>exit=1</span></div></div><div><div><div>18</div></div><div><span>$ typstyle -i -l 80 messy2.typ     # 对一份拷贝原地改写</span></div></div><div><div><div>19</div></div><div><span>exit=0</span></div></div><div><div><div>20</div></div><div><span>$ typstyle --check -l 80 messy2.typ</span></div></div><div><div><div>21</div></div><div><span>exit=0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>--check</code> 只判断、不动手，不通过退出码为 1；<code>--diff</code> 打印补丁，退出码同样是 1；<code>-i</code> 原地改写，之后 <code>--check</code> 通过。把这个乱文件放进 <code>lab/ch01/</code> 再跑 <code>make fmt-check</code>，输出 <code>Would reformat: lab/ch01/messy-tmp.typ</code> 和 <code>make: *** [Makefile:45: fmt-check] Error 1</code>（make 自己退出 2），说明 <code>ci</code> 在格式不对时会在编译之前停下。（我放进去又删掉了，没有提交。）</p><p>为什么 Makefile 里每次都写 <code>-l 80</code>？因为这是 <code>typstyle</code> 的默认值，写出来是给读者看的：这个系列的代码块宽度限制和博客的 80 列规则一致，不是为了覆盖默认。</p></section>
<section><h2>tinymist：我验证了什么，没验证什么<a href="#tinymist我验证了什么没验证什么"><span>#</span></a></h2><p>tinymist 是 Typst 的语言服务器。先交代清楚：<strong>这一篇里我没有在任何编辑器里交互式地用过它。</strong> 这台机器上装了 Helix（<code>hx --version</code> 为 <code>helix 25.07.1</code>），我用 Helix 的体检命令验证接线，再用一个自己写的 LSP 客户端脚本直接驱动 <code>tinymist lsp</code>，核对它对诊断、悬停、格式化请求的回应。实时预览（<code>tinymist preview</code> 或编辑器里的预览面板）、保存即导出、其他编辑器（VS Code、Neovim）的配置，我都没有测试，下面不对它们做任何声明。</p><p>先是接线。<code>hx --health typst</code> 在开发 shell 外面和里面的结果不同（每行开头有一个叉或勾的符号，我略去了，叉表示没找到，勾表示找到了）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ hx --health typst            # 不在 nix develop 里</span></div></div><div><div><div>2</div></div><div><span>Configured language servers:</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>tinymist: 'tinymist' not found in $PATH</span></div></div><div><div><div>4</div></div><div><span>$ nix develop --command hx --health typst</span></div></div><div><div><div>5</div></div><div><span>Configured language servers:</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>tinymist: /nix/store/mpdcjcaxcdy8wr9r4j3prmrs3yixs5aj-tinymist-0.15.2/bin/tinymist</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>Tree-sitter parser</code>、<code>Highlight queries</code> 两项在两种情况下都是勾，我只贴了语言服务器那一行。）这就是为什么我把 <code>tinymist</code> 放进 <code>flake.nix</code> 的 <code>packages</code> 里：版本和 Typst 一起锁定，在 <code>nix develop</code> 里启动的 Helix 就能找到它。</p><p>然后是协议。脚本不在仓库里，核心只有几行 bash：用 <code>coproc</code> 启动 <code>tinymist lsp</code>，按 LSP 的 <code>Content-Length</code> 帧格式收发 JSON：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 节选自 /tmp/p16/lsp.sh（不在仓库里）</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>LC_ALL</span><span>=</span><span>C</span><span>   </span><span># ${#1} 要数字节，不是字符</span></div></div><div><div><div>3</div></div><div><span>coproc TM { </span><span>tinymist</span><span> </span><span>lsp</span><span> 2&gt;</span><span>/tmp/p16/tinymist.err</span><span>; }</span></div></div><div><div><div>4</div></div><div><span>send</span><span>() { </span><span>printf</span><span> </span><span>'Content-Length: %d\r\n\r\n%s'</span><span> </span><span>"</span><span>${</span><span>#</span><span><span>1</span><span>}</span></span><span>"</span><span> </span><span>"</span><span>$1</span><span>"</span><span> &gt;&amp;</span><span>"</span><span><span>${</span><span>TM</span></span><span>[1]</span><span>}</span><span>"</span><span>; }</span></div></div><div><div><div>5</div></div><div><span>send</span><span> </span><span>"$(</span><span>jq</span><span> </span><span>-cn</span><span> </span><span>--arg</span><span> u "file://</span><span>$ROOT</span><span>/</span><span>$1</span><span>" </span><span>--rawfile</span><span> t "</span><span>$1</span><span>" </span><span>\</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>'{jsonrpc:"2.0",method:"textDocument/didOpen",params:{textDocument:{uri:$u,languageId:"typst",version:1,text:$t}}}')"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>initialize</code> 的回应里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{"server":{"name":"tinymist","version":"0.15.2"},"formatting":true,"hover":true}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>接着对几份文件发 <code>didOpen</code>，收集它们的诊断。对 <code>lab/ch01/unclosed.fail.typ</code>（第 1 篇的预期失败文件）和一个我写的坏文件 <code>broken-tmp.typ</code>（三行：<code>#let x = 1</code>、<code>#y</code>、<code>#x.foo()</code>，放在克隆里验证完就删了）：</p><div><figure><figcaption><span>lab/ch01/unclosed.fail.typ</span></figcaption><pre><code><div><div><div>1</div></div><div><span>[{"line":3,"severity":1,"message":"unclosed delimiter"}]</span></div></div><div><div><div>2</div></div><div><span>## diagnostics: broken-tmp.typ</span></div></div><div><div><div>3</div></div><div><span>[{"line":2,"severity":1,"message":"unknown variable: y"}]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>severity: 1</code> 是 LSP 里的 Error。第二个文件三行里 tinymist 只返回了一条诊断，也就是第 2 行的未知变量。对 <code>lab/ch04/first-note/main.typ</code> 和 <code>examples/notes/main.typ</code>，它没有发布任何诊断，说明从语言服务器的角度，<code>#import "@local/xuan:0.1.0": *</code> 能解析。</p><p>这个“能”是有代价的。我刚开始做这一章时，<code>typst.toml</code> 里写着 <code>compiler = "0.15.1"</code>，同样的两份文件得到：</p><div><figure><figcaption><span>lab/ch04/first-note/main.typ</span></figcaption><pre><code><div><div><div>1</div></div><div><span>[{"line":1,"severity":1,"message":"package requires Typst 0.15.1 or newer (current version is 0.15.0)"}]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>原因在 <code>tinymist --version</code> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ tinymist --version | grep -E 'tinymist|Typst Version'</span></div></div><div><div><div>2</div></div><div><span>tinymist</span></div></div><div><div><div>3</div></div><div><span>Typst Version:       0.15.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>nixpkgs 里的 tinymist 0.15.2 内嵌的是 Typst 0.15.0 的编译器，比命令行的 0.15.1 低一个补丁版本。包的 <code>compiler</code> 字段是下限，写 0.15.1 的包在编辑器里被拒绝，编译却在命令行里正常。我把它改成 <code>"0.15.0"</code>（第 4 篇里就是这个值，那里说了原因），在克隆里来回切了两次，每次都用同一个脚本复现：写 0.15.1 报错，写 0.15.0 无诊断，<code>make all</code> 在两种取值下都是 106 个 <code>ok</code>。所以仓库现在的值是 0.15.0，它是“命令行和编辑器都能用”的最大值。</p><p>悬停也核对了：对 <code>lab/ch04/first-note/main.typ</code> 里 <code>note.with</code> 的 <code>note</code>（0 起算的第 2 行第 9 列）发 <code>textDocument/hover</code>，返回的是 <code>xuan</code> 里 <code>note</code> 函数的签名，Markdown 代码块的语言标记是 <code>typc</code>（Typst 的代码模式）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let note(</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>body: any,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>abstract: content | none = none,</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>author: array | type = (),</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>bibliography: none = none,</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（我把它截到前几行，实际回应更长。）这就是 <code>note</code> 的参数列表，从包里的定义直接读出来，开头那句 “Notes and reports.” 来自 <code>note.typ</code> 里的 <code>///</code> 文档注释。</p><p>最后是格式化。我用一个 87 列的单行文件（<code>/tmp</code> 里）试，看 tinymist 的格式化和命令行的 <code>typstyle -l 80</code> 是否一致：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>sum-of-squares</span><span>(</span><span>values</span><span><span>) </span><span>= </span></span><span>values</span><span>.</span><span>map</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> </span><span>v</span><span><span> </span><span>*</span><span> </span></span><span>v</span><span>).</span><span>fold</span><span>(</span><span>0</span><span>, (</span><span>acc</span><span>, </span><span>x</span><span>) </span><span>=&gt;</span><span> </span><span>acc</span><span><span> </span><span>+</span><span> </span></span><span>x</span><span><span>) </span><span>+</span><span> </span></span><span>100</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用默认配置向 tinymist 发 <code>textDocument/formatting</code>，返回的编辑列表是空的 <code>[]</code>，也就是它认为不用改。在 <code>initialize</code> 的 <code>initializationOptions</code> 里加 <code>{formatterPrintWidth: 80}</code> 后，同一请求返回：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[{"range":[0,30,0,87],"newText":"(\n  values.map(v =&gt; v * v).fold(0, (acc, x) =&gt; acc + x) + 100\n)"}]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个编辑和 <code>typstyle -l 80</code> 对同一个文件的输出完全一致：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>sum-of-squares</span><span>(</span><span>values</span><span><span>) </span><span>= </span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>values</span><span>.</span><span>map</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> </span><span>v</span><span><span> </span><span>*</span><span> </span></span><span>v</span><span>).</span><span>fold</span><span>(</span><span>0</span><span>, (</span><span>acc</span><span>, </span><span>x</span><span>) </span><span>=&gt;</span><span> </span><span>acc</span><span><span> </span><span>+</span><span> </span></span><span>x</span><span><span>) </span><span>+</span><span> </span></span><span>100</span></div></div><div><div><div>3</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以如果你想让编辑器里的保存格式化和 <code>make fmt-check</code> 不打架，要把 tinymist 的 <code>formatterPrintWidth</code> 设成 80。具体写在哪个编辑器的哪个配置文件里，我没有实测，不写。</p></section>
<section><h2>模板与安装：typst init 和 make install<a href="#模板与安装typst-init-和-make-install"><span>#</span></a></h2><p>第 4 篇的 <code>typst.toml</code> 里有一节 <code>[template]</code>，指向 <code>template/main.typ</code>，这一篇才把它建出来。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/template/main.typ" target="_blank"><code>packages/local/xuan/0.1.0/template/main.typ</code></a>，11 行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>note</span><span>.</span><span>with</span><span>(</span></div></div><div><div><div>4</div></div><div><span>  </span><span>title</span><span>: </span><span>"标题"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>author</span><span>: </span><span>"作者"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>abstract</span><span>: [一两句话说明这份文档写什么。],</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>=</span><span> 开始</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>在这里写正文，公式写成 </span><span>$</span><span>a^2 + b^2 = c^2</span><span>$</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst init @local/xuan:0.1.0 /tmp/xxx</span></div></div><div><div><div>2</div></div><div><span>Successfully created new project from @local/xuan:0.1.0 ...</span></div></div><div><div><div>3</div></div><div><span>To start writing, run:</span></div></div><div><div><div>4</div></div><div><span>&gt; cd /tmp/xxx</span></div></div><div><div><div>5</div></div><div><span>&gt; typst watch main.typ</span></div></div><div><div><div>6</div></div><div><span>$ ls /tmp/xxx</span></div></div><div><div><div>7</div></div><div><span>main.typ</span></div></div><div><div><div>8</div></div><div><span>$ typst compile --root /tmp/xxx --ignore-system-fonts /tmp/xxx/main.typ /tmp/xxx/main.pdf</span></div></div><div><div><div>9</div></div><div><span>$ pdfinfo /tmp/xxx/main.pdf | grep -E "Title|Author|Pages"</span></div></div><div><div><div>10</div></div><div><span>Title:           标题</span></div></div><div><div><div>11</div></div><div><span>Author:          作者</span></div></div><div><div><div>12</div></div><div><span>Pages:           1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（成功提示的第一行末尾有一个庆祝用的 emoji，我没有贴。）<code>typst init</code> 只复制 <code>template/</code> 目录里的内容，所以展开出来的项目里只有 <code>main.typ</code>，它仍然写着 <code>@local/xuan:0.1.0</code>，也就是说 <code>xuan</code> 本身必须在 Typst 找得到的地方。这条命令在我这里成功，是因为 <code>nix develop</code> 设了 <code>TYPST_PACKAGE_PATH=$PWD/packages</code>。</p><p>开发 shell 之外怎么办？<code>make install</code> 就一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>install</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>mkdir -p ~/.local/share/typst/packages/local &amp;&amp; ln -sfn "</span><span>$(</span><span>CURDIR</span><span>)</span><span>/packages/local/xuan" ~/.local/share/typst/packages/local/xuan</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它在用户级的包目录里建一个符号链接，指向仓库里的包。我用一个临时的 <code>HOME</code>（<code>/tmp/p16/home</code>）验证，不碰真实的家目录：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ HOME=/tmp/p16/home make install</span></div></div><div><div><div>2</div></div><div><span>$ ls -l /tmp/p16/home/.local/share/typst/packages/local/</span></div></div><div><div><div>3</div></div><div><span>lrwxrwxrwx 1 bfmhno3 users 30 Oct 19 09:15 xuan -&gt; /tmp/lab16/packages/local/xuan</span></div></div><div><div><div>4</div></div><div><span>$ env -u TYPST_PACKAGE_PATH HOME=/tmp/p16/home typst compile --ignore-system-fonts main.typ main.pdf</span></div></div><div><div><div>5</div></div><div><span>$ echo $?</span></div></div><div><div><div>6</div></div><div><span>0</span></div></div><div><div><div>7</div></div><div><span>$ env -u TYPST_PACKAGE_PATH HOME=/tmp/p16/empty typst compile --ignore-system-fonts main.typ x.pdf</span></div></div><div><div><div>8</div></div><div><span>error: package not found (searched for @local/xuan:0.1.0)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>┌─ ../p16/proj/main.typ:1:8</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span>1 │ #import "@local/xuan:0.1.0": *</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>取消 <code>TYPST_PACKAGE_PATH</code>、只靠安装的链接，编译通过；换一个没装过的 <code>HOME</code>，就是第 4 篇讲过的 <code>package not found</code>。用符号链接而不是复制，是因为你改仓库里的 <code>src/*.typ</code>，所有用 <code>@local/xuan</code> 的文档立刻看到改动，不用重新安装。代价是链接指向仓库路径，仓库搬家就断。</p></section>
<section><h2>CI：同一条命令再跑一遍<a href="#ci同一条命令再跑一遍"><span>#</span></a></h2><p><a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/.github/workflows/ci.yml" target="_blank"><code>.github/workflows/ci.yml</code></a>，25 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>name</span><span>: </span><span>ci</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>on</span><span>:</span></div></div><div><div><div>4</div></div><div><span>  </span><span>push</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>branches</span><span>: [</span><span>main</span><span>]</span></div></div><div><div><div>6</div></div><div><span>  </span><span>pull_request</span><span>:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>branches</span><span>: [</span><span>main</span><span>]</span></div></div><div><div><div>8</div></div><div><span>  </span><span>workflow_dispatch</span><span>:</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>jobs</span><span>:</span></div></div><div><div><div>11</div></div><div><span>  </span><span>build</span><span>:</span></div></div><div><div><div>12</div></div><div><span>    </span><span>runs-on</span><span>: </span><span>ubuntu-latest</span></div></div><div><div><div>13</div></div><div><span>    </span><span>steps</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v7</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>cachix/install-nix-action@v31</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>nix develop --command make ci</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>  </span><span>plugin</span><span>:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>runs-on</span><span>: </span><span>ubuntu-latest</span></div></div><div><div><div>20</div></div><div><span>    </span><span>steps</span><span>:</span></div></div><div><div><div>21</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v7</span></div></div><div><div><div>22</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>cachix/install-nix-action@v31</span></div></div><div><div><div>23</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>&gt;-</span></div></div><div><div><div>24</div></div><div><span><span>          </span></span><span>nix develop --command bash -c</span></div></div><div><div><div>25</div></div><div><span><span>          </span></span><span>'cd plugin &amp;&amp; cargo fmt --check &amp;&amp; cargo clippy --release --target wasm32-unknown-unknown -- -D warnings'</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>整份工作流的设计原则就一条：<strong>CI 里跑的必须是你本地跑的那条命令。</strong> <code>build</code> 任务的 <code>run</code> 就是 <code>nix develop --command make ci</code>，也就是本篇开头那 12.2 秒；<code>nix develop</code> 读 <code>flake.lock</code>，而 <code>flake.lock</code> 把 nixpkgs 锁在 <code>56c02bc00adcf003215cc4bd996d6efaf4cff188</code>，和这个博客仓库的 <code>flake.lock</code> 锁的是同一个提交（我用 <code>jq -r '.nodes.nixpkgs.locked.rev'</code> 对比了两份锁文件）。所以 CI 里的 Typst 是 0.15.1、typstyle 是 0.15.0、字体是同样的 Noto CJK，不会出现“我这里是绿的，CI 里是红的”。Actions 的版本我用 <code>gh api</code> 查了今天的最新发布：<code>actions/checkout</code> 是 <code>v7.0.1</code>，<code>cachix/install-nix-action</code> 是 <code>v31.11.2</code>，工作流里写的是浮动的主版本标签 <code>@v7</code> 和 <code>@v31</code>。</p><p>要打个比方的话，CI 每次都给你换一张干净的桌子，重做一遍实验：桌上昨天留下的东西（缓存、你装过但没写进依赖的工具、忘了提交的文件）都不算数。我在 <code>/tmp</code> 里重新克隆，是同一个意思，只是把桌子留在本地。</p><p><code>plugin</code> 任务只检查 Rust 源码的格式和 lint：<code>cargo fmt --check</code> 和 <code>cargo clippy --release --target wasm32-unknown-unknown -- -D warnings</code>。我在本地跑了它们，格式检查无输出（<code>fmt-ok</code>），clippy 的最后三行是 <code>Checking pinyin v0.11.0</code>、<code>Checking xuan-pinyin v0.1.0</code> 和 <code>Finished</code>，用时 0.67 s。注意它<strong>没有</strong>把 Rust 重新编译成 <code>.wasm</code> 放进包里。</p><p>为什么测试用提交进仓库的 <code>.wasm</code>，而不在 CI 里临时编译？我有三个理由，其中第一个是实测的：</p><ol>
<li>编译是可复现的。我在克隆里对已提交的 <code>.wasm</code> 跑 <code>sha256sum</code>，再跑 <code>make plugin</code>（依赖已缓存的机器上用时 3.4 s）重新编译、覆盖，再跑一次：两次都是 <code>280cfb1184870d980cb321dc9fd27048c263440a8ecea10b31b23e329da90d07</code>，<code>git status</code> 干净，字节相同。既然重编出来一样，提交的那份就不是“不知道怎么来的二进制”。</li>
<li>包要能被 <code>typst init</code> 和 <code>make install</code> 的使用者直接用，他们不需要 Rust 工具链。<code>.wasm</code> 是包的一部分，跟 <code>lib.typ</code> 一样要提交。</li>
<li><code>build</code> 任务的职责是验证包的行为，不是验证 Rust 的构建；把后者拆到 <code>plugin</code> 任务里，哪个坏了一眼就知道。</li>
</ol><p>冷缓存的代价也量过一次。第 14 篇的 <code>examples/ecosystem/main.typ</code> 要下载 <code>@preview</code> 包，CI 的每次运行都从空缓存开始。我用一个空的 <code>--package-cache-path</code> 量了：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>.</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>--package-cache-path</span><span> </span><span>/tmp/p16/cache</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>examples/ecosystem/main.typ</span><span> </span><span>/tmp/p16/eco.pdf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一次（冷）25.07 s，第二次（热）0.49 s，缓存目录里留下 <code>cetz</code>、<code>elembic</code>、<code>lilaq</code>、<code>oxifmt</code>、<code>tiptoe</code>、<code>zero</code> 六个包（Touying 用在 <code>touying.typ</code> 里，这条命令没有编译它）。冷启动的耗时取决于当时的网络，第 14 篇量到的是 11.1 s，这次是 25 s，我不会把任何一个当成常数。如果 CI 因此变慢，可以用 <code>actions/cache</code> 缓存这个目录，我没有配置它，12 秒级的 CI 目前不需要。</p><p>我没有在这里贴 GitHub 上 Actions 的运行结果，因为我能在本机验证的是工作流所执行的那条命令，而不是 GitHub 的调度。仓库的 Actions 页上有它的真实状态。</p></section>
<section><h2>索引：Typst 特性 → 第几篇<a href="#索引typst-特性--第几篇"><span>#</span></a></h2><p>这张表按系列开头定下的覆盖清单一行一行列，每一行的每个条目按下面的对应关系出现在那一篇里。</p>

<table><thead><tr><th>Typst 特性</th><th>条目</th><th>篇</th></tr></thead><tbody><tr><td>CLI 基础</td><td><code>compile</code>、<code>watch</code>、<code>fonts</code>、<code>--root</code>、<code>--font-path</code>、<code>--ignore-system-fonts</code>、内嵌字体</td><td><a href="/posts/typst-by-xuan-01-first-document/">01</a>、<a href="/posts/typst-by-xuan-05-fonts-and-cjk/">05</a></td></tr><tr><td>标记语法</td><td>段落、<code>*strong*</code>、<code>_emph_</code>、行内与块 <code>raw</code>、自动链接、标题 <code>=</code>、列表 <code>-</code>、编号列表 <code>+</code>、术语列表 <code>/ a: b</code>、标签 <code>&lt;a&gt;</code>、引用 <code>@a</code>、<code>\</code> 换行、<code>~</code>、<code>--</code>/<code>---</code>、智能引号、注释、转义、<code>\u{...}</code></td><td><a href="/posts/typst-by-xuan-01-first-document/">01</a></td></tr><tr><td>模式</td><td>标记、代码、数学三种模式，<code>#</code> 嵌入规则，<code>#(...)</code></td><td><a href="/posts/typst-by-xuan-01-first-document/">01</a>、<a href="/posts/typst-by-xuan-02-code-mode/">02</a></td></tr><tr><td>值与类型</td><td><code>none</code>、<code>auto</code>、<code>bool</code>、<code>int</code>（<code>int.min</code>/<code>int.max</code>）、<code>float</code>、<code>decimal</code>、<code>str</code>（字节、字素簇与码点）、长度单位、<code>ratio</code>、<code>relative</code>、<code>fraction</code>、<code>angle</code>、<code>version</code>、<code>type</code>、<code>regex</code>、<code>label</code>、<code>symbol</code>/<code>sym</code></td><td><a href="/posts/typst-by-xuan-02-code-mode/">02</a></td></tr><tr><td>控制与绑定</td><td><code>let</code>、遮蔽、解构、<code>if</code>/<code>for</code>/<code>while</code>/<code>break</code>/<code>continue</code>、代码块与内容块的拼接、运算符、<code>range(inclusive:)</code></td><td><a href="/posts/typst-by-xuan-02-code-mode/">02</a></td></tr><tr><td>集合</td><td>数组与字典的方法：<code>map</code>、<code>filter</code>、<code>fold</code>、<code>zip</code>、<code>enumerate</code>、<code>sorted(key:)</code>、<code>dedup</code>、<code>join</code>、<code>at(default:)</code>，0.15 的 <code>dictionary.map</code>/<code>filter</code></td><td><a href="/posts/typst-by-xuan-02-code-mode/">02</a></td></tr><tr><td>诊断</td><td><code>assert</code>、<code>assert.eq</code>、<code>panic</code>、<code>repr</code>、<code>type</code></td><td><a href="/posts/typst-by-xuan-02-code-mode/">02</a>，测试里的用法见本篇</td></tr><tr><td>函数</td><td>命名与默认参数、<code>..args</code>、<code>arguments</code>（<code>pos</code>、<code>named</code>、字段访问）、展开、闭包、递归、<code>.with</code>、纯度与捕获的不可变性</td><td><a href="/posts/typst-by-xuan-03-functions-and-content/">03</a></td></tr><tr><td>内容模型</td><td>元素函数、<code>func</code>、<code>fields</code>、<code>has</code>、<code>at</code>、序列、<code>plain-text</code></td><td><a href="/posts/typst-by-xuan-03-functions-and-content/">03</a></td></tr><tr><td>样式</td><td><code>set</code>、<code>set ... if</code>、作用域、折叠、<code>show</code> 的 set 与变换形式、选择器（元素、<code>where</code>、字符串、正则、标签、<code>or</code>/<code>and</code>/<code>before</code>/<code>after</code>、<code>within</code>）、<code>show: template.with(...)</code></td><td><a href="/posts/typst-by-xuan-04-set-and-show/">04</a></td></tr><tr><td>编号</td><td><code>numbering</code> 的字符串与函数形式，中文编号（<code>"一"</code>、<code>"壹"</code>）</td><td><a href="/posts/typst-by-xuan-04-set-and-show/">04</a>、<a href="/posts/typst-by-xuan-05-fonts-and-cjk/">05</a></td></tr><tr><td>文本</td><td><code>text</code> 的参数（字体列表、<code>covers</code>、<code>lang</code>/<code>region</code>、字号、字重、字形、<code>tracking</code>、<code>spacing</code>、<code>features</code>、<code>number-type</code>、<code>variations</code>、<code>cjk-latin-spacing</code>、<code>fallback</code>）、<code>smartquote</code>、<code>linebreak</code>、<code>lorem</code>、<code>highlight</code>、<code>underline</code>、<code>strike</code>、<code>overline</code>、<code>sub</code>、<code>super</code>、<code>upper</code>、<code>lower</code>、<code>smallcaps</code></td><td><a href="/posts/typst-by-xuan-05-fonts-and-cjk/">05</a>、<a href="/posts/typst-by-xuan-09-figures-tables-code/">09</a></td></tr><tr><td>段落</td><td><code>par</code> 的 <code>justify</code>、<code>leading</code>、<code>spacing</code>、<code>first-line-indent</code>（折叠）、<code>hanging-indent</code>、<code>linebreaks</code></td><td><a href="/posts/typst-by-xuan-05-fonts-and-cjk/">05</a></td></tr><tr><td>页面与布局</td><td><code>page</code>（纸张、边距、装订、分栏、页眉页脚、页码、填充、背景/前景、出血）、<code>pagebreak</code>、<code>block</code>、<code>box</code>、<code>align</code>、<code>pad</code>、<code>h</code>/<code>v</code>（弱间距、<code>fr</code>）、<code>stack</code>、<code>grid</code>、<code>columns</code>/<code>colbreak</code>、<code>place</code>（浮动、<code>scope</code>）、<code>move</code>/<code>rotate</code>/<code>scale</code>/<code>skew</code>、<code>repeat</code>、<code>hide</code>、<code>layout</code>、<code>measure</code></td><td><a href="/posts/typst-by-xuan-06-page-and-layout/">06</a></td></tr><tr><td>内省</td><td><code>context</code>、<code>here</code>、<code>locate</code>、<code>location</code>、<code>counter</code>（内置、自定义、<code>display(at:)</code>）、<code>state</code>、<code>query</code>、<code>metadata</code>、收敛</td><td><a href="/posts/typst-by-xuan-07-context-counters-state/">07</a></td></tr><tr><td>数学</td><td>行内与块公式、标识符、数学中的文本、符号与修饰、附标、<code>frac</code>、<code>sqrt</code>/<code>root</code>、<code>vec</code>、<code>mat</code>、<code>cases</code>、<code>lr</code>/<code>mid</code>/<code>abs</code>/<code>norm</code>/<code>floor</code>/<code>ceil</code>、<code>&amp;</code> 对齐、<code>op</code>、<code>limits</code>/<code>scripts</code>、字体变体（<code>bold</code>、<code>upright</code>、<code>cal</code>、<code>bb</code>、<code>frak</code>、<code>sans</code>、<code>mono</code>）、<code>cancel</code>、<code>underbrace</code>/<code>overbrace</code>、撇号、<code>class</code>、数学中的代码、<code>math.equation</code> 的编号与补充语</td><td><a href="/posts/typst-by-xuan-08-math/">08</a></td></tr><tr><td>模型元素</td><td><code>figure</code>（种类、标题、放置）、<code>image</code>、<code>table</code>（表头、表尾、<code>cell</code>、<code>hline</code>、<code>vline</code>、<code>stroke</code>/<code>fill</code>/<code>align</code> 的函数形式）、<code>grid.cell</code>、<code>ref</code> 的补充语、<code>link</code>、<code>footnote</code>、<code>outline</code>（目标）、<code>quote</code>、<code>divider</code>、对 <code>emph</code>/<code>strong</code> 的 show 规则</td><td><a href="/posts/typst-by-xuan-09-figures-tables-code/">09</a></td></tr><tr><td>代码显示</td><td><code>raw</code>（<code>lang</code>、<code>block</code>、<code>theme</code>、<code>tab-size</code>、<code>raw.line</code>、<code>syntaxes</code>）</td><td><a href="/posts/typst-by-xuan-09-figures-tables-code/">09</a></td></tr><tr><td>参考文献</td><td><code>bibliography</code>（<code>.bib</code>、Hayagriva YAML、样式、CSL 文件、<code>full</code>、<code>title</code>）、<code>cite</code> 的形式与补充语、0.15 的 <code>target</code>/<code>group</code></td><td><a href="/posts/typst-by-xuan-10-bibliography/">10</a></td></tr><tr><td>数据加载</td><td><code>yaml</code>、<code>toml</code>、<code>json</code>、<code>csv</code>、<code>xml</code>、<code>cbor</code>、<code>read</code>、字节、<code>sys.inputs</code>、<code>eval</code>（标记模式）、<code>datetime</code>、<code>duration</code></td><td><a href="/posts/typst-by-xuan-11-data-driven-cv/">11</a></td></tr><tr><td>模块</td><td><code>import</code> 的各种形式、<code>include</code>、根相对路径、0.15 的 <code>path</code> 类型、循环导入错误</td><td><a href="/posts/typst-by-xuan-12-homework/">12</a></td></tr><tr><td>可视化</td><td><code>rect</code>/<code>square</code>/<code>circle</code>/<code>ellipse</code>/<code>line</code>/<code>polygon</code>/<code>curve</code>、<code>color</code> 的色彩空间与方法、<code>color.map</code>、<code>gradient</code>（线性、径向、锥形）、<code>tiling</code>、<code>stroke</code> 字典</td><td><a href="/posts/typst-by-xuan-13-slides-and-drawing/">13</a></td></tr><tr><td>包</td><td><code>@preview</code> 导入、缓存与 <code>--package-cache-path</code>、<code>--package-path</code>、<code>typst.toml</code> 的字段、<code>typst init</code>、<code>@local</code></td><td><a href="/posts/typst-by-xuan-14-packages-ecosystem/">14</a>、本篇（<code>typst init</code>、<code>make install</code>）</td></tr><tr><td>插件</td><td><code>plugin</code>、字节协议、确定性、错误、Rust 的 <code>wasm-minimal-protocol</code></td><td><a href="/posts/typst-by-xuan-15-wasm-plugin/">15</a></td></tr><tr><td>导出与工具链</td><td>PDF 元数据（<code>document</code>）、<code>--pdf-standard</code>（多个）、PNG/SVG（<code>--ppi</code>、<code>{p}</code>）、HTML（<code>--features html</code>、MathML）、bundle（实验性）、<code>typst eval</code>、<code>--timings</code>、<code>--deps</code>、tinymist、typstyle、测试、CI</td><td>本篇</td></tr></tbody></table><p>这张表我是对着覆盖清单抄的，抄完后我 <code>grep</code> 了第 1 到 15 篇里的一批关键词（<code>bleed</code>、<code>measure</code>、<code>display(at</code>、<code>conic</code>、<code>package-cache-path</code> 等）确认它们都出现过，但没有逐行逐条重编译；每一行是否真讲到，以各篇里那条命令的输出为准。</p></section>
<section><h2>验证你手里的树<a href="#验证你手里的树"><span>#</span></a></h2><p>这个系列只有一个“权威版本”，就是 <code>tutorial-v1</code> 标签。如果你用 <code>git clone --branch tutorial-v1</code> 拿的仓库，一路上只是跟着改文件，那么到这里，你的工作树应该和标签完全一致。检查它只需要一条命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>tutorial-v1</span><span> </span><span>--stat</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它把<strong>工作树</strong>和标签比较，没有任何改动时什么也不打印。我在 <code>/tmp/lab16</code>（标签的全新克隆，<code>git describe --tags --exact-match HEAD</code> 输出 <code>tutorial-v1</code>）里验证：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ git status --short</span></div></div><div><div><div>2</div></div><div><span>$ git diff tutorial-v1 --stat</span></div></div><div><div><div>3</div></div><div><span>$ echo $?</span></div></div><div><div><div>4</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两条命令都没有输出。再往 <code>util.typ</code> 里追加一行注释：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ echo "// scratch" &gt;&gt; packages/local/xuan/0.1.0/src/util.typ</span></div></div><div><div><div>2</div></div><div><span>$ git diff tutorial-v1 --stat</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>packages/local/xuan/0.1.0/src/util.typ | 1 +</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>1 file changed, 1 insertion(+)</span></div></div><div><div><div>5</div></div><div><span>$ git checkout -q packages/local/xuan/0.1.0/src/util.typ</span></div></div><div><div><div>6</div></div><div><span>$ git diff tutorial-v1 --stat</span></div></div><div><div><div>7</div></div><div><span>$ echo $?</span></div></div><div><div><div>8</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>改动被报告出来，还原之后又没有输出了。有两点要小心：<code>git diff</code> 看不到<strong>未跟踪</strong>的新文件（你自己新建的文件要看 <code>git status --short</code>），退出码也不会因为有差异而变成非零，它只是打印；要拿它做脚本判断，用 <code>git diff --quiet tutorial-v1</code>。</p><p>走到这一步你手里有的东西：一个 70 行的 Makefile 驱动的、16 章实验的小型文档系统，一个能 <code>typst init</code> 的本地包，四类文档（笔记、作业、幻灯片、简历）的 Typst 模板，一个 Rust 写的 235242 字节的插件，6 个测试，和一个只做一件事的 CI。Typst 的导出方向里，HTML 和 bundle 在 0.15.1 里都还挂着“实验性”的警告，这两个我没有用来生产任何东西，只放进 <code>extra.sh</code> 当“金丝雀”：以后 nixpkgs 升级 Typst，<code>make ch16</code> 会第一个告诉我它们的行为有没有变。<code>typst compile --help</code> 里 <code>--features</code> 的取值还列了一个 <code>a11y-extras</code>，我没有试，下一次想让 <code>note</code> 的图和公式过 PDF/UA 的时候我会从它开始。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-15-wasm-plugin/</id>
      <title type="text">用 Rust 写一个 WebAssembly 插件：按拼音排序术语表</title>
      <published>2026-10-19T14:00:00.000Z</published>
      <updated>2026-10-19T14:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-15-wasm-plugin/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 15 篇：我用 Rust 1.97.1 写了一个 29 行的 WebAssembly 插件，在 Typst 0.15.1 里按拼音给术语表排序；量出 235242 字节的 .wasm、冷编译 3.74 秒，在 /tmp 里用计数器插件验证了 Typst 会缓存同参数的调用，并复现了 plugin 的五种报错。</summary>
      <content type="html"><![CDATA[<p>笔记末尾想要一张术语表，按拼音排序。Typst 的 <code>array.sorted()</code> 不认拼音，只认码点：把 <code>("卡尔曼", "奇异值", "矩阵", "最小二乘")</code> 排一下，<code>最小二乘</code> 会跑到 <code>矩阵</code> 前面，因为 <code>最</code> 是 U+6700，<code>矩</code> 是 U+77E9。语言本身没有办法查汉字的读音，所以这一篇我用 Rust 写了一个 29 行的 WebAssembly 插件，让 Typst 通过 <code>plugin</code> 调用它，再把它包成 <code>xuan</code> 的 <code>term</code> 和 <code>glossary</code>。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有这些新文件，都是<strong>最终版</strong>，文中完整展示：<code>plugin/Cargo.toml</code>、<code>plugin/src/lib.rs</code>、<code>src/glossary.typ</code>；还有 <code>plugin/Cargo.lock</code>（依赖锁，文中只引用）和已经提交进仓库的 <code>packages/local/xuan/0.1.0/plugin/xuan_pinyin.wasm</code>。<code>lib.typ</code> 仍是<strong>中间版本</strong>：这一篇只多一行 <code>#import "src/glossary.typ": glossary, term</code>。<code>lab/ch15/</code> 下的 6 个文件（含两个预期失败的）全部通过 <code>make ch15</code>，<code>make plugin</code> 能从源码重建 <code>.wasm</code>。
前置：<a href="/posts/typst-by-xuan-07-context-counters-state/">第 7 篇</a>（<code>context</code>、<code>query</code> 和 <code>metadata</code>，术语表整个建立在它们之上）。Rust 本身不在这一篇讲，需要入门的话看 <a href="/posts/rust-by-pebble-01-first-crate/">Rust 入门：第一个 crate</a>。</p></div></div>
<section><h2>为什么码点顺序不是拼音顺序<a href="#为什么码点顺序不是拼音顺序"><span>#</span></a></h2><p>先把问题钉死。<code>lab/ch15/unicode-sort.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch15/unicode-sort.typ" target="_blank">源文件</a>）把同样四个词用两种方式排序，一起打印出来。它还依赖本篇后面才写的 <code>pinyin-keys</code>，先看结果：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Codepoint order vs pinyin order for the same four terms.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"../../packages/local/xuan/0.1.0/src/glossary.typ"</span><span>: </span><span>pinyin-keys</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>serif</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>words</span><span><span> </span><span>= </span><span>(</span></span><span>"卡尔曼"</span><span>, </span><span>"奇异值"</span><span>, </span><span>"矩阵"</span><span>, </span><span>"最小二乘"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>by-codepoint</span><span><span> </span><span>= </span></span><span>words</span><span>.</span><span>sorted</span><span>()</span></div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>keys</span><span><span> </span><span>= </span></span><span>pinyin-keys</span><span>(</span><span>words</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#let</span><span> </span><span>by-pinyin</span><span><span> </span><span>= </span></span><span>words</span><span>.</span><span>zip</span><span>(</span><span>keys</span><span>).</span><span>sorted</span><span>(</span><span>key</span><span>: </span><span>p</span><span> </span><span>=&gt;</span><span> </span><span>p</span><span>.</span><span>at</span><span>(</span><span>1</span><span>)).</span><span>map</span><span>(</span><span>p</span><span> </span><span>=&gt;</span><span> </span><span>p</span><span>.</span><span>at</span><span>(</span><span>0</span><span>))</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#table</span><span>(</span></div></div><div><div><div>12</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>[codepoint], [pinyin],</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>..</span><span>by-codepoint</span><span>.</span><span>zip</span><span>(</span><span>by-pinyin</span><span>).</span><span>flatten</span><span>(),</span></div></div><div><div><div>15</div></div><div><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>#repr</span><span>(</span><span>by-codepoint</span><span>) </span><span>\</span></div></div><div><div><div>18</div></div><div><span>#repr</span><span>(</span><span>by-pinyin</span><span>) </span><span>\</span></div></div><div><div><div>19</div></div><div><span>#repr</span><span>(</span><span>keys</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>by-codepoint</span><span>, (</span><span>"卡尔曼"</span><span>, </span><span>"奇异值"</span><span>, </span><span>"最小二乘"</span><span>, </span><span>"矩阵"</span><span>))</span></div></div><div><div><div>22</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>by-pinyin</span><span>, (</span><span>"矩阵"</span><span>, </span><span>"卡尔曼"</span><span>, </span><span>"奇异值"</span><span>, </span><span>"最小二乘"</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pdftotext -layout out/ch15/unicode-sort.pdf -</code> 的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>codepoint pinyin</span></div></div><div><div><div>2</div></div><div><span>卡尔曼       矩阵</span></div></div><div><div><div>3</div></div><div><span>奇异值       卡尔曼</span></div></div><div><div><div>4</div></div><div><span>最小二乘 奇异值</span></div></div><div><div><div>5</div></div><div><span>矩阵        最小二乘</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>("卡尔曼", "奇异值", "最小二乘", "矩阵")</span></div></div><div><div><div>8</div></div><div><span>("矩阵", "卡尔曼", "奇异值", "最小二乘")</span></div></div><div><div><div>9</div></div><div><span>(</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>"ka er man ",</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>"qi yi zhi ",</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>"ju zhen ",</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>"zui xiao er cheng ",</span></div></div><div><div><div>14</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>码点顺序是 卡 &lt; 奇 &lt; 最 &lt; 矩，拼音顺序是 ju &lt; ka &lt; qi &lt; zui。四个词在两种顺序里的位置全部不同：<code>矩阵</code> 从最后一名变成第一名，另外三个各后移一位。CJK 统一表意文字区大致按部首和笔画排列，和读音没有关系，所以只要排序用的是字符串比较，这个问题就不可能靠调参数解决。最后两个 <code>assert.eq</code> 把这个事实固定下来：文件能编译，说明两种顺序就是上面印出来的样子。</p><p>排序键是第三个 <code>repr</code> 打印的东西：<code>矩阵</code> 变成 <code>"ju zhen "</code>，每个汉字换成无声调拼音再加一个空格。自己在 Typst 里维护这张字音表太费事（常用字就有几千个），而 Rust 的 <code>pinyin</code> crate 已经带着数据表。缺的只是把它接进 Typst 的办法。</p></section>
<section><h2>插件协议：字节进，字节出<a href="#插件协议字节进字节出"><span>#</span></a></h2><p>办法是 WebAssembly。<code>plugin("x.wasm")</code> 加载一个 <code>.wasm</code> 文件，返回一个 module，模块里每个导出函数变成一个 Typst 函数。把它想成一台只有一个投币口和一个出货口的自动售货机：你从投币口放进去几段字节，它从出货口吐出一段字节，机器内部怎么运转你看不见，机器也看不到外面的任何东西。</p><p>协议在 <a href="https://typst.app/docs/reference/foundations/plugin/" target="_blank"><code>plugin</code> 的文档</a>里有完整规定，我把和这个项目有关的几条挑出来：</p><ul>
<li>导出函数接受 <code>n</code> 个 32 位整数（每个参数的<strong>长度</strong>），返回一个 32 位整数。</li>
<li>函数先分配一块长度为各参数长度之和的缓冲区，调用宿主函数 <code>wasm_minimal_protocol_write_args_to_buffer(ptr)</code>，宿主把所有参数依次写进去。</li>
<li>干完活之后，调用宿主函数 <code>wasm_minimal_protocol_send_result_to_host(ptr, len)</code> 把结果交回去。</li>
<li>返回 <code>0</code> 表示成功；返回 <code>1</code> 表示失败，此时缓冲区里的内容被当作 UTF-8 错误信息。</li>
<li>模块只允许从 <code>typst_env</code> 导入上面那两个函数。没有文件、没有网络、没有打印：文档说插件“在隔离中运行”。</li>
</ul><p>我没打算只信文档，写完之后用 Bun 的 <code>WebAssembly.Module.imports</code> 和 <code>.exports</code> 读了一遍编译出来的 <code>.wasm</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>imports: typst_env.wasm_minimal_protocol_write_args_to_buffer (i32) -&gt; ()</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>typst_env.wasm_minimal_protocol_send_result_to_host (i32, i32) -&gt; ()</span></div></div><div><div><div>3</div></div><div><span>exports: memory (20 页)</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>pinyin_keys (i32) -&gt; i32</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（这个检查脚本放在 <code>/tmp/p15-wasm.js</code>，不在仓库里；原始输出是 JSON，我整理成了上面的样子。）导入恰好两个，导出一个函数加一块内存。<code>pinyin_keys</code> 接受一个 <code>i32</code>（唯一一个参数的长度），返回一个 <code>i32</code>（状态码）。这和协议对得上。</p><p>这也解释了为什么 Rust 要选 <code>wasm32-unknown-unknown</code> 目标：<code>unknown-unknown</code> 表示没有操作系统也没有 WASI，生成的模块天然不依赖 <code>fd_write</code> 之类的系统调用。很多编译器默认走 WASI，那样产出的模块里有 Typst 不提供的导入，要先用 <code>wasi-stub</code> 处理；这个项目不需要。</p><p>手写协议要自己管内存分配和两次宿主调用，很容易写错。<code>wasm-minimal-protocol</code> crate 用一个属性宏把这些都包掉了：<code>#[wasm_func]</code> 标注的函数只需要接受 <code>&amp;[u8]</code> 参数，返回 <code>Vec&lt;u8&gt;</code> 或 <code>Result&lt;Vec&lt;u8&gt;, E&gt;</code>（<code>E</code> 实现 <code>Display</code> 即可）。我读了它在本机 cargo 缓存里的宏源码确认展开结果：<code>Ok</code> 对应状态码 <code>0</code>，<code>Err(e)</code> 把 <code>e.to_string()</code> 作为消息送回宿主并返回 <code>1</code>。</p></section>
<section><h2>写 Rust 端<a href="#写-rust-端"><span>#</span></a></h2><p><code>plugin/Cargo.toml</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/plugin/Cargo.toml" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"xuan-pinyin"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>5</div></div><div><span>license</span><span> = </span><span>"MIT OR Apache-2.0"</span></div></div><div><div><div>6</div></div><div><span>description</span><span> = </span><span>"Typst plugin: pinyin sort keys for Chinese glossary terms"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>[</span><span>lib</span><span>]</span></div></div><div><div><div>9</div></div><div><span>crate-type</span><span> = [</span><span>"cdylib"</span><span>]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pinyin</span><span> = { </span><span>version</span><span> = </span><span>"0.11.0"</span><span>, </span><span>default-features</span><span> = </span><span>false</span><span>, </span><span>features</span><span> = [</span><span>"plain"</span><span>] }</span></div></div><div><div><div>13</div></div><div><span>wasm-minimal-protocol</span><span> = </span><span>"0.2.1"</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[</span><span>profile</span><span>.</span><span>release</span><span>]</span></div></div><div><div><div>16</div></div><div><span>opt-level</span><span> = </span><span>"s"</span></div></div><div><div><div>17</div></div><div><span>lto</span><span> = </span><span>true</span></div></div><div><div><div>18</div></div><div><span>strip</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三处是有意为之的。<code>crate-type = ["cdylib"]</code> 让 rustc 产出一个没有 Rust 元数据的 C 风格动态库，在 wasm 目标上就是一个可被宿主加载的 <code>.wasm</code>；默认的 <code>rlib</code> 是给别的 Rust 代码链接用的，Typst 用不了。<code>default-features = false, features = ["plain"]</code> 只打开无声调拼音：<code>pinyin</code> 的默认特性还包括声调、数字声调、多音字等几套数据（<code>Cargo.toml</code> 的 <code>[features]</code> 里列着）。我在 <code>/tmp/p15/full</code> 里把这一行改回 <code>pinyin = "0.11.0"</code>（不在仓库里），同样的 profile 下编出来是 495471 字节，是现在 235242 字节的 2.1 倍。<code>opt-level = "s"</code>、<code>lto</code>、<code>strip</code> 是针对体积的常规设置：我猜体积小对编译速度也有一点帮助，但我没有量过，选它们主要是为了仓库里提交的二进制别太大。</p><p><code>plugin/src/lib.rs</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/plugin/src/lib.rs" target="_blank">源文件</a>），完整的 29 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pinyin</span><span>::</span></span><span>ToPinyin</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>wasm_minimal_protocol</span><span>::*;</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>initiate_protocol!</span><span>();</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/// Input: UTF-8 lines. Output: one sort key per line.</span></div></div><div><div><div>7</div></div><div><span>/// Han characters become toneless pinyin followed by a space,</span></div></div><div><div><div>8</div></div><div><span>/// every other character is lowercased as-is.</span></div></div><div><div><div>9</div></div><div><span>#[wasm_func]</span></div></div><div><div><div>10</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>pinyin_keys</span><span>(</span><span>input</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; </span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt;, </span><span>String</span><span>&gt; {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span></span><span>str</span><span>::</span><span>from_utf8</span><span>(</span><span>input</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>.</span><span>map_err</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> </span></span><span>"xuan-pinyin: input is not valid UTF-8"</span><span>.</span><span>to_string</span><span>())?;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>keys</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span><span>&gt; </span><span>=</span><span> </span></span><span>text</span><span>.</span><span>split</span><span>(</span><span>'</span><span>\n</span><span>'</span><span>).</span><span>map</span><span>(</span><span>line_key</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Ok</span><span>(</span><span>keys</span><span>.</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>).</span><span>into_bytes</span><span>())</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>fn</span><span> </span><span>line_key</span><span>(</span><span>line</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>key</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>19</div></div><div><span>    </span><span>for</span><span> </span><span>ch</span><span> </span><span>in</span><span> </span><span>line</span><span>.</span><span>chars</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>match</span><span> </span><span>ch</span><span>.</span><span>to_pinyin</span><span>() {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>Some</span><span>(</span><span>p</span><span>) =&gt; {</span></div></div><div><div><div>22</div></div><div><span>                </span><span>key</span><span>.</span><span>push_str</span><span>(</span><span>p</span><span>.</span><span>plain</span><span>());</span></div></div><div><div><div>23</div></div><div><span>                </span><span>key</span><span>.</span><span>push</span><span>(</span><span>' '</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>            </span><span>None</span><span> =&gt; </span><span>key</span><span>.</span><span>extend</span><span>(</span><span>ch</span><span>.</span><span>to_lowercase</span><span>()),</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>    </span><span>key</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个设计选择值得说清楚。</p><p><strong>一次调用处理所有词。</strong> 输入是“用换行分隔的多行 UTF-8”，输出是同样行数的键。每次 <code>plugin</code> 函数调用都要过一遍参数序列化，而且是一次宿主到客体的边界穿越；术语表通常几十条，一次打包比循环调用便宜，Typst 端也只产生一条缓存记录。代价是词里不能含换行：<code>pinyin-keys</code> 把词用 <code>\n</code> 拼起来再按 <code>\n</code> 拆开，含换行的词会让键的行数和词的数量对不上。术语不会含换行，所以我没有做防护。</p><p><strong>每个汉字后面补一个空格。</strong> 为什么不直接拼成 <code>juzhen</code>？因为拼音音节边界会产生歧义。我在 <code>/tmp/p15/poly.typ</code> 里用真实插件算了 <code>("先", "西安", "西")</code> 的键：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"xian "</span></div></div><div><div><div>2</div></div><div><span>"xi an "</span></div></div><div><div><div>3</div></div><div><span>"xi "</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>有空格的时候 <code>西</code> &lt; <code>西安</code> &lt; <code>先</code>，这是字典里的顺序；没有空格 <code>西安</code> 和 <code>先</code> 的键都是 <code>xian</code>，会并列，由排序的稳定性（输入顺序）决定谁在前。空格比任何字母小，所以它把“音节结束”这个信息编进了字符串比较里。</p><p><strong>非汉字原样小写保留。</strong> <code>ch.to_pinyin()</code> 对非汉字返回 <code>None</code>，走 <code>ch.to_lowercase()</code> 分支，<code>Abc矩阵</code> 的键是 <code>abcju zhen </code>（注意 <code>abc</code> 和 <code>ju</code> 之间没有空格：拉丁字母不加分隔，只有汉字加）。<code>tests/glossary.typ</code> 里有一条断言固定了这个行为。</p><p><strong>错误是值，不是 panic。</strong> <code>from_utf8</code> 失败就返回 <code>Err(String)</code>，宏把它翻译成状态码 <code>1</code> 加消息，Typst 端于是能原样显示这句话（后面有真实输出）。这个函数里唯一会失败的地方就是输入不是 UTF-8，所以我只需要处理这一条路径。</p><p><strong>局限：多音字只取第一个读音。</strong> 这个 crate 的 <code>to_pinyin</code> 对每个字只给一个读音。同样在 <code>poly.typ</code> 里我算了 <code>银行</code>，键是 <code>"yin xing "</code>，而在这个词里 <code>行</code> 读 <code>háng</code>。对于术语表，这种错误只影响极少数词的相对位置；如果要处理，应该用 <code>heteronym</code> 特性加上词库，那是另一个项目了。</p></section>
<section><h2>构建、格式和 lint<a href="#构建格式和-lint"><span>#</span></a></h2><p>仓库的 <code>Makefile</code> 里有一条 <code>plugin</code> 目标：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>plugin</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>cd plugin &amp;&amp; cargo build --release --target wasm32-unknown-unknown &amp;&amp; cp target/wasm32-unknown-unknown/release/xuan_pinyin.wasm ../packages/local/xuan/0.1.0/plugin/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>流程是在 <code>nix develop</code>（Rust 1.97.1，它自带 <code>wasm32-unknown-unknown</code> 的标准库，构建不需要 <code>rustup target add</code>，也没有用到 <code>wasm-ld</code> 或 <code>wasi-stub</code>）里：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>make</span><span> </span><span>plugin</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>plugin</span><span> &amp;&amp; </span><span>cargo</span><span> </span><span>build</span><span> </span><span>--release</span><span> </span><span>--target</span><span> </span><span>wasm32-unknown-unknown</span><span> &amp;&amp; </span><span>cp</span><span> </span><span>target/wasm32-unknown-unknown/release/xuan_pinyin.wasm</span><span> </span><span>../packages/local/xuan/0.1.0/plugin/</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Finished</span><span> </span><span>`</span><span>release</span><span>`</span><span> </span><span>profile</span><span> [optimized] target(</span><span>s</span><span>) in 0.23s</span></div></div><div><div><div>4</div></div><div><span>$</span><span> </span><span>stat</span><span> </span><span>-c</span><span> </span><span>%s</span><span> </span><span>packages/local/xuan/0.1.0/plugin/xuan_pinyin.wasm</span></div></div><div><div><div>5</div></div><div><span>235242</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是一次增量构建，<code>target/</code> 已经在了所以只花 0.23 秒。冷构建我在另一个目录里量：<code>CARGO_TARGET_DIR=/tmp/p15-target cargo build --release --target wasm32-unknown-unknown</code>（依赖已经在 cargo 缓存里，不含下载时间）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling proc-macro2 v1.0.107</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling quote v1.0.47</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Compiling unicode-ident v1.0.27</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>Compiling pinyin v0.11.0</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>Compiling venial v0.5.0</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>Compiling wasm-minimal-protocol v0.2.1</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>Compiling xuan-pinyin v0.1.0 (/home/bfmhno3/Projects/typst-lab/plugin)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>Finished `release` profile [optimized] target(s) in 3.74s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>冷构建 3.74 秒，产物 235242 字节，而且 <code>cmp</code> 把它和仓库里提交的那份比较，输出 <code>same</code>：<code>wasm32-unknown-unknown</code> 的构建在这台机器上是逐字节可复现的。这个事实有实际意义，因为 CI 的 <code>build</code> 任务<strong>不</strong>重新编译插件，直接用提交进仓库的 <code>.wasm</code>（这样 Typst 的测试不依赖 Rust 工具链），所以我需要知道它没有和源码脱节。235242 字节里大头是拼音表：我另外用一个只有协议、没有 <code>pinyin</code> 依赖的计数器 crate（后文会用到）编了一份，是 15377 字节，相差约 219 KB。</p><p>CI 的 <code>plugin</code> 任务只跑格式和 lint，我原样跑一遍（命令和 <code>.github/workflows/ci.yml</code> 里一致）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>cd</span><span> </span><span>plugin</span><span> &amp;&amp; </span><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--check</span><span> &amp;&amp; </span><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--release</span><span> </span><span>--target</span><span> </span><span>wasm32-unknown-unknown</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo fmt --check</code> 无输出（我的命令里加了 <code>&amp;&amp; echo fmt-ok</code> 确认退出码为 0），clippy 输出 <code>Finished</code> 一行，没有任何警告。<code>-D warnings</code> 把警告升级成错误，所以 CI 绿了就意味着一条警告都没有。</p></section>
<section><h2>在 Typst 里调用它<a href="#在-typst-里调用它"><span>#</span></a></h2><p>先手动调用一次，不经过任何包装。<code>lab/ch15/plugin-basics.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch15/plugin-basics.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Calling the plugin by hand: bytes in, bytes out.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>p</span><span><span> </span><span>= </span></span><span>plugin</span><span>(</span><span>"../../packages/local/xuan/0.1.0/plugin/xuan_pinyin.wasm"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>out</span><span><span> </span><span>= </span></span><span>p</span><span>.</span><span>pinyin_keys</span><span>(</span><span>bytes</span><span>(</span><span>"矩阵</span><span>\n</span><span>ABC"</span><span>))</span></div></div><div><div><div>5</div></div><div><span>#type</span><span>(</span><span>out</span><span>), </span><span>#out</span><span>.</span><span>len</span><span>() bytes: </span><span>#repr</span><span>(</span><span>str</span><span>(</span><span>out</span><span>))</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// Same input, same output: plugins are pure functions.</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>out</span><span>, </span><span>p</span><span>.</span><span>pinyin_keys</span><span>(</span><span>bytes</span><span>(</span><span>"矩阵</span><span>\n</span><span>ABC"</span><span>)))</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>// Errors come back as Typst errors, which `assert` cannot catch,</span></div></div><div><div><div>11</div></div><div><span>// so we only show the valid path here. See bad-bytes.fail.typ.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>bytes, 12 bytes: "ju zhen \nabc"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个点：返回值的类型是 <code>bytes</code>，要用 <code>str()</code> 才能当字符串用；<code>"ju zhen \nabc"</code> 是 8 + 1 + 3 = 12 字节；<code>bytes(...)</code> 和 <code>str(...)</code> 就是 Typst 一侧的全部“序列化”。</p><p>调用约定是严格的。我在 <code>/tmp/p15/</code> 里试了几种错误用法（都不在仓库里），都是 <code>typst compile --root /tmp/p15 &lt;file&gt;</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>p</span><span><span> </span><span>= </span></span><span>plugin</span><span>(</span><span>"counter.wasm"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#p</span><span>.</span><span>echo</span><span>(</span><span>"矩阵"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: expected bytes, found string</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ nostr.typ:2:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #p.echo("矩阵")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>参数必须是 <code>bytes</code>，不会自动转换。其余三种：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: module does not contain `nothing`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ nofn.typ:2:3</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #p.nothing(bytes("x"))</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│    ^^^^^^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>error: file not found (searched at /tmp/p15/missing.wasm)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>┌─ nofile.typ:1:16</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>10</div></div><div><span>1 │ #let p = plugin("missing.wasm")</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>│                 ^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第三种是把一个 <code>.typ</code> 文件当 <code>.wasm</code> 加载（<code>plugin("counter.typ")</code>），错误开头是 <code>error: failed to load WebAssembly module (magic header not detected: bad magic number ...</code>，后面跟着期望的 <code>0x0 0x61 0x73 0x6d</code>（即 <code>\0asm</code>）和实际读到的 <code>0x23 0x6c 0x65 0x74</code>（即 <code>#let</code>，源文件的前四个字节）。</p><p>这些都是编译期的使用错误。插件函数<strong>自己</strong>报错是另一回事，<code>lab/ch15/bad-bytes.fail.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch15/bad-bytes.fail.typ" target="_blank">源文件</a>）故意把两个不可能出现在 UTF-8 里的字节 <code>255, 254</code> 送进去：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Invalid UTF-8 makes the plugin return Err, which aborts compilation.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>p</span><span><span> </span><span>= </span></span><span>plugin</span><span>(</span><span>"../../packages/local/xuan/0.1.0/plugin/xuan_pinyin.wasm"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#p</span><span>.</span><span>pinyin_keys</span><span>(</span><span>bytes</span><span>((</span><span>255</span><span>, </span><span>254</span><span>)))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>out/ch15/bad-bytes.stderr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: plugin errored with: xuan-pinyin: input is not valid UTF-8</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch15/bad-bytes.fail.typ:3:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>3 │ #p.pinyin_keys(bytes((255, 254)))</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Rust 里写的那句 <code>"xuan-pinyin: input is not valid UTF-8"</code> 原样出现在 Typst 的错误信息里，前面只加了 <code>plugin errored with: </code>。这就是状态码 <code>1</code> 加错误消息的完整往返。这个错误是编译错误，不是值：<code>assert</code> 抓不住它，没有 <code>try</code> 之类的东西，所以 <code>plugin-basics.typ</code> 末尾的注释说只展示成功路径。（在插件包里把错误消息写成 <code>包名: 原因</code> 的形式，用户读到 Typst 的输出时才知道该找谁。）</p></section>
<section><h2>纯函数：Typst 会缓存同参数的调用<a href="#纯函数typst-会缓存同参数的调用"><span>#</span></a></h2><p>文档在 Purity 一节写得很直接：插件函数必须是纯函数，同样的参数必须总是返回同样的值，调用不能对后续调用产生可观察的副作用。原因是 Typst 函数本身是纯的，插件调用继承了这个要求。具体来说，同一个参数调用两次，Typst 可能缓存结果，只真正调用一次；Typst 还可能在多个线程里运行插件的多个实例，实例之间不共享状态。Typst 不检查你是否真的纯（为了效率），违反的后果文档称为不可预测、不可复现。</p><p>我不太喜欢只靠文档的措辞，所以写了一个故意不纯的插件来看它到底怎么不可预测。<code>/tmp/p15/counter/src/lib.rs</code>（不在仓库里，<code>cargo fmt --check</code> 通过）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>sync</span><span>::</span><span>atomic</span><span>::{</span></span><span>AtomicU32</span><span>, </span><span>Ordering</span><span>};</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>wasm_minimal_protocol</span><span>::*;</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>initiate_protocol!</span><span>();</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>static</span><span> </span><span>CALLS</span><span>: </span><span>AtomicU32</span><span><span> </span><span>=</span><span> </span></span><span>AtomicU32</span><span>::</span><span>new</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#[wasm_func]</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>bump</span><span>(</span><span>_input</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; </span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>CALLS</span><span>.</span><span>fetch_add</span><span>(</span><span>1</span><span><span>, </span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span>) + </span><span>1</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>n</span><span>.</span><span>to_string</span><span>().</span><span>into_bytes</span><span>()</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#[wasm_func]</span></div></div><div><div><div>15</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>echo</span><span>(</span><span>input</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; </span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>input</span><span>.</span><span>to_vec</span><span>()</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>bump</code> 每次调用把一个全局计数加一并返回。如果每次调用都真的执行，输入 <code>a, b, a, a</code> 应该得到 <code>1 2 3 4</code>。<code>/tmp/p15/counter.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>p</span><span><span> </span><span>= </span></span><span>plugin</span><span>(</span><span>"counter.wasm"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#for</span><span> </span><span>w</span><span> </span><span>in</span><span> (</span><span>"a"</span><span>, </span><span>"b"</span><span>, </span><span>"a"</span><span>, </span><span>"a"</span><span>) [</span></div></div><div><div><div>3</div></div><div><span>  </span><span>#str</span><span>(</span><span>p</span><span>.</span><span>bump</span><span>(</span><span>bytes</span><span>(</span><span>w</span><span>))) </span><span>#h</span><span>(</span><span>1em</span><span>)</span></div></div><div><div><div>4</div></div><div><span>]</span></div></div><div><div><div>5</div></div><div><span>\</span></div></div><div><div><div>6</div></div><div><span>#str</span><span>(</span><span>p</span><span>.</span><span>echo</span><span>(</span><span>bytes</span><span>(</span><span>"矩阵"</span><span>)))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdftotext -layout</code> 的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 2   1   1</span></div></div><div><div><div>2</div></div><div><span>矩阵</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>是 <code>1 2 1 1</code>。第一个 <code>a</code> 真的执行了，得 1；<code>b</code> 得 2；后面两个 <code>a</code> 的参数和第一次完全相同，Typst 直接返回缓存的 <code>1</code>，计数器没有被碰过。同一份源码，插入或删除一个调用，后面所有数字都会变，这就是“不可复现”。第二个小实验换成 <code>range(3)</code> 循环，每轮先调 <code>bump(bytes("a"))</code> 再调 <code>bump(bytes(str(i)))</code>，输出 <code>12 13 14</code>：<code>a</code> 永远是 1，而每个新参数让计数器前进一步，也印证了缓存的键是“函数加参数字节”。</p><p>如果插件确实需要状态（比如昂贵的初始化），官方提供 <code>plugin.transition(func, ..args)</code>：执行一次调用，返回一个<strong>新</strong> module，新 module 里的函数能看到这次调用的副作用，原 module 不受影响。<code>/tmp/p15/trans.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>p</span><span><span> </span><span>= </span></span><span>plugin</span><span>(</span><span>"counter.wasm"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>q</span><span><span> </span><span>= </span></span><span>plugin</span><span>.</span><span>transition</span><span>(</span><span>p</span><span>.</span><span>bump</span><span>, </span><span>bytes</span><span>(</span><span>"x"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>#str</span><span>(</span><span>p</span><span>.</span><span>bump</span><span>(</span><span>bytes</span><span>(</span><span>"a"</span><span>))) </span><span>#str</span><span>(</span><span>q</span><span>.</span><span>bump</span><span>(</span><span>bytes</span><span>(</span><span>"a"</span><span>))) </span><span>#str</span><span>(</span><span>q</span><span>.</span><span>bump</span><span>(</span><span>bytes</span><span>(</span><span>"b"</span><span>)))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出 <code>123</code>：<code>p</code> 是原模块，计数器从零开始，<code>p.bump("a")</code> 得 1；<code>q</code> 在 transition 里已经吃掉了一次调用，所以 <code>q.bump("a")</code> 得 2，<code>q.bump("b")</code> 得 3。两个模块各有各的状态。官方文档特别注明：由于内部 WebAssembly 实现的限制，transition 只保证反映插件内存的变化，不保证反映 WebAssembly 全局变量的变化。这个计数器能工作，是因为 Rust 的 <code>static</code> 放在线性内存里，不是 wasm 全局变量（这是我对编译结果的推断，没有反汇编确认；实验结果与之相符）。我在 xuan 里没有用 <code>transition</code>：拼音转换天然是纯函数。</p><p>对 <code>pinyin_keys</code> 来说，纯函数意味着：它没有全局状态，输出只取决于输入字节，所以缓存对它只有好处。</p></section>
<section><h2>xuan 的 glossary.typ<a href="#xuan-的-glossarytyp"><span>#</span></a></h2><p>插件有了，包装它才是 xuan 的工作。<code>src/glossary.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/glossary.typ" target="_blank">源文件</a>）完整的 47 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Glossary sorted by pinyin, using the Rust plugin in ../plugin.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"util.typ"</span><span>: </span><span>pick</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>pinyin</span><span><span> </span><span>= </span></span><span>plugin</span><span>(</span><span>"../plugin/xuan_pinyin.wasm"</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/// Sort keys for a list of strings, computed by the WebAssembly plugin.</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>pinyin-keys</span><span>(</span><span>words</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>str</span><span>(</span><span>pinyin</span><span>.</span><span>pinyin_keys</span><span>(</span><span>bytes</span><span>(</span><span>words</span><span>.</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>)))).</span><span>split</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>/// Look up the entries registered by `glossary(...)` anywhere in the document.</span></div></div><div><div><div>12</div></div><div><span>#let</span><span> </span><span>entries-of</span><span>(</span><span>key</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>13</div></div><div><span>  </span><span>let</span><span> </span><span>found</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>&lt;xuan-entries&gt;</span><span>)</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> </span><span>found</span><span>.</span><span>len</span><span><span>() </span><span>==</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>panic</span><span>(</span><span>"xuan: term("</span><span><span> </span><span>+</span><span> </span></span><span>repr</span><span>(</span><span>key</span><span><span>) </span><span>+</span><span> </span></span><span>") needs a glossary(...) call"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>  </span><span>let</span><span> </span><span>entries</span><span><span> </span><span>= </span></span><span>found</span><span>.</span><span>first</span><span>().</span><span>value</span></div></div><div><div><div>18</div></div><div><span>  </span><span>if</span><span> </span><span>key</span><span> </span><span>not</span><span> </span><span>in</span><span> </span><span>entries</span><span> {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>panic</span><span>(</span><span>"xuan: unknown glossary key: "</span><span><span> </span><span>+</span><span> </span></span><span>key</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>  </span><span>entries</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>/// Print the term and record its use for the glossary.</span></div></div><div><div><div>25</div></div><div><span>#let</span><span> </span><span>term</span><span>(</span><span>key</span><span><span>) </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>26</div></div><div><span>  </span><span>let</span><span> </span><span>entries</span><span><span> </span><span>= </span></span><span>entries-of</span><span>(</span><span>key</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>[</span><span>#entries</span><span>.</span><span>at</span><span>(</span><span>key</span><span>).</span><span>term</span><span>#metadata</span><span>(</span><span>key</span><span>)</span><span>&lt;xuan-term&gt;</span><span>]</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>/// List the terms used so far, ordered by pinyin.</span></div></div><div><div><div>31</div></div><div><span>#let</span><span> </span><span>glossary</span><span>(</span><span>entries</span><span>, </span><span>title</span><span>: </span><span>auto</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>[</span><span>#metadata</span><span>(</span><span>entries</span><span>) </span><span>&lt;xuan-entries&gt;</span><span>]</span></div></div><div><div><div>33</div></div><div><span>  </span><span>context</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>let</span><span> </span><span>used</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>&lt;xuan-term&gt;</span><span>).</span><span>map</span><span>(</span><span>m</span><span> </span><span>=&gt;</span><span> </span><span>m</span><span>.</span><span>value</span><span>).</span><span>dedup</span><span>()</span></div></div><div><div><div>35</div></div><div><span>    </span><span>let</span><span> </span><span>words</span><span><span> </span><span>= </span></span><span>used</span><span>.</span><span>map</span><span>(</span><span>k</span><span> </span><span>=&gt;</span><span> </span><span>entries</span><span>.</span><span>at</span><span>(</span><span>k</span><span>).</span><span>term</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>let</span><span> </span><span>keys</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>words</span><span>.</span><span>len</span><span><span>() </span><span>==</span><span> </span></span><span>0</span><span> { () } </span><span>else</span><span> { </span><span>pinyin-keys</span><span>(</span><span>words</span><span>) }</span></div></div><div><div><div>37</div></div><div><span>    </span><span>let</span><span> </span><span>rows</span><span><span> </span><span>= </span></span><span>used</span><span>.</span><span>zip</span><span>(</span><span>keys</span><span>).</span><span>sorted</span><span>(</span><span>key</span><span>: </span><span>pair</span><span> </span><span>=&gt;</span><span> </span><span>pair</span><span>.</span><span>at</span><span>(</span><span>1</span><span>))</span></div></div><div><div><div>38</div></div><div><span>    </span><span>let</span><span> </span><span>heading-text</span><span><span> </span><span>= </span></span><span>pick</span><span>(</span><span>title</span><span>, </span><span>if</span><span> </span><span>text</span><span>.</span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> { </span><span>"术语表"</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>39</div></div><div><span>      </span><span>"Glossary"</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>41</div></div><div><span>    </span><span>heading</span><span>(</span><span>heading-text</span><span>)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>for</span><span> (</span><span>k</span><span>, </span><span>_</span><span>) </span><span>in</span><span> </span><span>rows</span><span> {</span></div></div><div><div><div>43</div></div><div><span>      </span><span>let</span><span> </span><span>e</span><span><span> </span><span>= </span></span><span>entries</span><span>.</span><span>at</span><span>(</span><span>k</span><span>)</span></div></div><div><div><div>44</div></div><div><span>      </span><span>terms</span><span>.</span><span>item</span><span>(</span><span>strong</span><span>(</span><span>e</span><span>.</span><span>term</span><span>), </span><span>e</span><span>.</span><span>desc</span><span>)</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>从上往下读，每个选择都有原因。</p><p><strong>插件路径相对于这个文件。</strong> <code>plugin("../plugin/xuan_pinyin.wasm")</code> 里的路径和 <code>import</code> 一样是相对于当前源文件解析的（第 12 篇讲过），所以不管用户从哪里 <code>import "@local/xuan:0.1.0"</code>，路径都指向包目录里的 <code>plugin/</code>。<code>.wasm</code> 因此必须随包一起发布：这就是它被提交进 <code>packages/local/xuan/0.1.0/plugin/</code> 的原因。</p><p><strong><code>pinyin-keys</code> 是对插件的一层薄封装。</strong> 它做三件事：拼接、调用、拆分。<code>bytes</code> 和 <code>str</code> 的转换只出现在这一处，包的其余部分只和字符串数组打交道。把它单独导出（<code>lab</code> 和 <code>tests</code> 都直接从 <code>src/glossary.typ</code> 导入它）也方便测试。</p><p><strong><code>term</code> 需要知道词的写法，但调用点在 <code>glossary(...)</code> 之前。</strong> 用户这样写文章：先在正文里 <code>#term("lsq")</code>，最后才 <code>#glossary(yaml("glossary.yaml"))</code>。<code>term</code> 只拿到一个键，要渲染出“最小二乘”，必须在这一刻读到词典。解法是 <code>glossary()</code> 开头把整个字典用 <code>[#metadata(entries) &lt;xuan-entries&gt;]</code> 挂在文档里，<code>term</code> 再用 <code>query(&lt;xuan-entries&gt;)</code>（这就是第 7 篇讲的内省）把它查回来。这也是 <code>term</code> 的整个函数体包在 <code>context</code> 里的原因：<code>query</code> 只能在 context 里调用。</p><p><strong>两种错误都变成可读的 <code>panic</code>。</strong> 键不存在是用户最容易犯的错，所以 <code>entries-of</code> 先检查词典是否存在、再检查键是否在词典里，两处各给一条指名道姓的消息。<code>lab/ch15/bad-key.fail.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch15/bad-key.fail.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// An unknown key is a hard error with a readable message.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#glossary</span><span>((</span><span>a</span><span>: (</span><span>term</span><span>: </span><span>"矩阵"</span><span>, </span><span>desc</span><span>: </span><span>"数表"</span><span>)))</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#term</span><span>(</span><span>"matrix"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>out/ch15/bad-key.stderr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: panicked with: xuan: unknown glossary key: matrix</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>┌─ @local/xuan:0.1.0/src/glossary.typ:19:4</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>19 │     panic("xuan: unknown glossary key: " + key)</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>│     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>while calling `entries-of` at @local/xuan:0.1.0/src/glossary.typ:26:16</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>entries-of(key)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>另一种是根本没写 <code>glossary(...)</code>。我在 <code>/tmp/p15/nogloss.typ</code> 里只放了一行 <code>#term("a")</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: panicked with: xuan: term("a") needs a glossary(...) call</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>┌─ @local/xuan:0.1.0/src/glossary.typ:15:4</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>15 │     panic("xuan: term(" + repr(key) + ") needs a glossary(...) call")</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>│     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有这个检查的话，第二种情况会掉进 <code>found.first()</code>，得到一个“数组为空”的错误，和用户写的东西毫无关系。</p><p><strong><code>glossary</code> 只列用到过的词，并且去重。</strong> <code>query(&lt;xuan-term&gt;)</code> 按文档顺序返回所有用过的 <code>term</code> 标记，<code>.map(m =&gt; m.value).dedup()</code> 取出键并保持首次出现的顺序。<code>lab/ch15/glossary.typ</code> 里 <code>lsq</code> 用了两次，<code>unused</code> 一次也没用，表里只出现一次 <code>lsq</code>，没有 <code>unused</code>。</p><p><strong>排序是 <code>zip</code> 加 <code>sorted(key:)</code>。</strong> <code>keys</code> 和 <code>used</code> 等长，<code>used.zip(keys)</code> 得到 <code>(键, 拼音键)</code> 对，<code>sorted(key: pair =&gt; pair.at(1))</code> 按拼音键排。这里不能直接排 <code>used</code>（键是英文标识符），也不能排词（那就是码点顺序），必须把“排序依据”和“要排的东西”绑在一起。<code>words.len() == 0</code> 时跳过插件调用：没有用到任何词，就没有必要往边界那头送一个空缓冲区。</p><p><strong>标题用 <code>text.lang</code> 选语言。</strong> <code>heading-text</code> 通过 <code>pick</code>（第 3 篇的 <code>util.typ</code>）处理 <code>title: auto</code>，在 context 里读 <code>text.lang</code>，中文文档是“术语表”，其他语言是 <code>Glossary</code>。显式传 <code>title:</code> 则覆盖。</p><p><code>lab/ch15/glossary.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch15/glossary.typ" target="_blank">源文件</a>）里有 5 个词条，正文用了其中 3 个（<code>lsq</code> 两次）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// term() marks a use; glossary() lists the used terms in pinyin order.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>serif</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>entries</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>6</div></div><div><span>  </span><span>svd</span><span>: (</span><span>term</span><span>: </span><span>"奇异值分解"</span><span>, </span><span>desc</span><span>: [把矩阵写成 </span><span>$</span><span>U </span><span>Sigma</span><span> V^T</span><span>$</span><span>。]),</span></div></div><div><div><div>7</div></div><div><span>  </span><span>lsq</span><span>: (</span><span>term</span><span>: </span><span>"最小二乘"</span><span>, </span><span>desc</span><span>: </span><span>"使残差范数最小的拟合。"</span><span>),</span></div></div><div><div><div>8</div></div><div><span>  </span><span>rank</span><span>: (</span><span>term</span><span>: </span><span>"秩"</span><span>, </span><span>desc</span><span>: </span><span>"列空间的维数。"</span><span>),</span></div></div><div><div><div>9</div></div><div><span>  </span><span>kalman</span><span>: (</span><span>term</span><span>: </span><span>"卡尔曼滤波"</span><span>, </span><span>desc</span><span>: </span><span>"递推的线性最优估计。"</span><span>),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>unused</span><span>: (</span><span>term</span><span>: </span><span>"伪逆"</span><span>, </span><span>desc</span><span>: </span><span>"这一条没有被引用，不会出现。"</span><span>),</span></div></div><div><div><div>11</div></div><div><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>正文先提到 </span><span>#term</span><span>(</span><span>"lsq"</span><span>)，再提到 </span><span>#term</span><span>(</span><span>"svd"</span><span>) 和 </span><span>#term</span><span>(</span><span>"rank"</span><span>)，</span></div></div><div><div><div>14</div></div><div><span>最后又提到 </span><span>#term</span><span>(</span><span>"lsq"</span><span>)，去重后只列一次。</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>#glossary</span><span>(</span><span>entries</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pdftotext -layout out/ch15/glossary.pdf -</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>正文先提到 最小二乘，再提到 奇异值分解 和 秩， 最后又提到 最小二乘，去重后只列一次。</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>术语表</span></div></div><div><div><div>4</div></div><div><span>奇异值分解 把矩阵写成 𝑈 Σ𝑉 𝑇 。</span></div></div><div><div><div>5</div></div><div><span>秩 列空间的维数。</span></div></div><div><div><div>6</div></div><div><span>最小二乘 使残差范数最小的拟合。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>顺序 qi、zhi、zui。<code>desc</code> 可以是字符串也可以是 content，<code>奇异值分解</code> 的说明里有公式，也正常渲染。</p><p><code>lab/ch15/fig-glossary.typ</code> 用四个词渲染了一张图，这次四个词全部用到了：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch15-glossary.png" alt="正文一行“最小二乘、奇异值分解、矩阵、卡尔曼滤波”，下面是术语表，按顺序排列的是矩阵、卡尔曼滤波、奇异值分解、最小二乘，各条目带粗体术语与一句说明" /><figcaption>正文一行“最小二乘、奇异值分解、矩阵、卡尔曼滤波”，下面是术语表，按顺序排列的是矩阵、卡尔曼滤波、奇异值分解、最小二乘，各条目带粗体术语与一句说明</figcaption></figure><p></p><p>正文里 <code>最小二乘</code> 排在第一，术语表里它排在最后；<code>矩阵</code> 正文第三，表里第一。</p></section>
<section><h2>在笔记里用<a href="#在笔记里用"><span>#</span></a></h2><p><code>examples/notes/glossary.yaml</code> 有 8 个词条，yaml 的结构正好是 <code>glossary(entries)</code> 要的字典：键到 <code>term</code> 和 <code>desc</code>。<code>examples/notes/main.typ</code> 在正文里用了 5 个：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#term</span><span>(</span><span>"lsq"</span><span>)问题的本质是</span><span>#term</span><span>(</span><span>"proj"</span><span>)，</span><span>#term</span><span>(</span><span>"svd"</span><span>) 把投影、</span><span>#term</span><span>(</span><span>"rank"</span><span>)与</span><span>#term</span><span>(</span><span>"cond"</span><span>)放进同一个分解。进一步阅读可参考</span></div></div><div><div><div>2</div></div><div><span>国内教材 </span><span>@tongji2014</span><span> 中关于正交化与二次型的章节。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#glossary</span><span>(</span><span>yaml</span><span>(</span><span>"glossary.yaml"</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 <code>main.typ</code> 末尾的一段，不是整个文件。<code>make examples</code> 之后用 <code>pdftotext -layout out/examples/notes.pdf -</code> 看，术语表这一节（笔记共 4 页）依次是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>奇异值分解 把任意矩阵写成正交阵、对角阵、正交阵的乘积，对角元是奇异值。</span></div></div><div><div><div>2</div></div><div><span>条件数 最大与最小非零奇异值之比，衡量解对扰动的敏感程度。</span></div></div><div><div><div>3</div></div><div><span>正交投影 把向量映到子空间上最近的点，残差与子空间正交。</span></div></div><div><div><div>4</div></div><div><span>秩 列空间的维数，也等于非零奇异值的个数。</span></div></div><div><div><div>5</div></div><div><span>最小二乘 使残差平方和最小的近似解法，几何上是向列空间做正交投影。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（第一条在第 3 页末尾，其余四条被分页挤到第 4 页，我只摘了条目，没有抄页脚。）拼音顺序是 qi、tiao、zheng、zhi、zui。同样五个词按码点排序（我在 <code>/tmp/p15/cp.typ</code> 里直接 <code>.sorted()</code> 打印了一下）是：奇异值分解、最小二乘、条件数、正交投影、秩。变化最大的是 <code>最小二乘</code>：码点顺序排第二，拼音顺序排最后。<code>秩</code> 从第五变成第四，<code>条件数</code> 和 <code>正交投影</code> 各提前一位。五个词里只有 <code>奇异值分解</code> 在两种顺序里都排第一，其余四个全部换了位置。</p></section>
<section><h2>加到 lib.typ<a href="#加到-libtyp"><span>#</span></a></h2><p><code>packages/local/xuan/0.1.0/lib.typ</code> 增加这一篇的一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"src/glossary.typ"</span><span>: </span><span>glossary</span><span>, </span><span>term</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>lib.typ</code> 仍是中间版本，这一篇只多了上面那一行。验证用 <code>make ch15</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ok   lab/ch15/bad-bytes.fail.typ (failed as expected)</span></div></div><div><div><div>2</div></div><div><span>ok   lab/ch15/bad-key.fail.typ (failed as expected)</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch15/fig-glossary.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch15/glossary.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch15/plugin-basics.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch15/unicode-sort.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个 <code>make ch15</code> 在 0.44 秒内跑完（<code>time make ch15</code>，6 个文件，包括两个预期失败的）。插件的 235242 字节不是瓶颈。</p></section>
<section><h2>什么时候该写插件<a href="#什么时候该写插件"><span>#</span></a></h2><p>我这个插件做的事情，用一张手写的静态映射表也能做，只不过要自己维护数据。我觉得值得写插件的场景是：算法有成熟的库（排序、哈希、压缩、解析），而 Typst 语言本身又不是为这类计算设计的。反过来，只是想“多一点逻辑”的时候，Typst 自己的函数通常够用，而且不需要提交一个 235 KB 的二进制进仓库。</p><p>有两条边界要记住。第一，插件不能读文件、不能联网，输入全靠 Typst 端以字节传进去，所以如果要处理一个数据文件，先在 Typst 里 <code>read(..., encoding: none)</code> 把字节取出来（第 11 篇讲过）再交给插件。第二，<code>.wasm</code> 是二进制，评审者无法审阅它，所以我把 Rust 源码和 <code>Cargo.lock</code> 一并提交，CI 里跑 fmt 与 clippy，并且验证过它可以逐字节复现。</p><p>下一篇是最后一篇：怎么把这一切变成可以交付的东西，包括 PDF 标准、HTML 导出、<code>typst eval</code>、<code>--timings</code>、tests、CI 和模板，最后用一张“Typst 特性 → 第几篇”的索引表把整个系列对一遍。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-14-packages-ecosystem/</id>
      <title type="text">生态：Typst Universe 与第三方包</title>
      <published>2026-10-19T13:00:00.000Z</published>
      <updated>2026-10-19T13:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-14-packages-ecosystem/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 14 篇：我用 Typst 0.15.1 在空的 --package-cache-path 下编译 CeTZ 与 Lilaq 的图，量出首次下载约 11.1 秒、缓存命中后 51.6 毫秒，列出下载后缓存里的 8 个包，再把 examples/slides 的三页用 Touying 重写，数出 31 行对 28 行，最后把一个玩具包装成 @local 包并用 typst init 展开模板。</summary>
      <content type="html"><![CDATA[<p>前十三篇里，<code>xuan</code> 的每一行都是我自己写的，连幻灯片和作业都是。这一篇我要看看别人写的包：<code>import "@preview/cetz:0.5.2"</code> 这一行执行时，Typst 到底做了什么，下载了哪些文件，放在哪里，第二次为什么不再联网，以及“版本写死在导入字符串里”究竟保护了我什么。我顺手用 CeTZ 画了编译流水线图，用 Lilaq 画了一条阻尼振荡曲线，还把 <code>examples/slides</code> 里的三页用 Touying 重写了一遍，结果发现它并不比我自己的 <code>xuan</code> 更短。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇不新增 <code>xuan</code> 的源文件，<code>lib.typ</code> 仍是第 4 篇的<strong>中间版本</strong>，<code>src/base.typ</code>、<code>src/note.typ</code> 这一章没有改动（它们分别在第 9、10 篇之后才是最终版）。你会新得到 <code>lab/ch14/</code> 下的 4 个入口（<code>fig-cetz.typ</code>、<code>fig-lilaq.typ</code>、<code>import-preview.typ</code>、<code>manifest/main.typ</code>）、<code>lab/ch14/extra.sh</code>，以及 <code>examples/ecosystem/</code> 下的 <code>main.typ</code>、<code>touying.typ</code> 和 <code>data/damped.csv</code>。<code>make ch14</code> 全部通过；它需要联网（首次运行会下载 8 个包）。
前置：<a href="/posts/typst-by-xuan-13-slides-and-drawing/">第 13 篇</a>（幻灯片与绘图）。这一篇会把第 13 篇的 <code>examples/slides</code> 当作对照，把第 4 篇的“包目录布局”和 <code>--package-path</code> 补完，第 12 篇的 <code>import</code> 语法默认你已经会了。</p></div></div>
<section><h2>第一次导入：11.1 秒与 52 毫秒<a href="#第一次导入111-秒与-52-毫秒"><span>#</span></a></h2><p>先看一个最小的例子，<code>lab/ch14/import-preview.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch14/import-preview.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `@preview` imports download the package on first use, then hit the cache.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@preview/cetz:0.5.2"</span><span>: </span><span>canvas</span><span>, </span><span>draw</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#canvas</span><span>(</span><span>length</span><span>: </span><span>1cm</span><span>, {</span></div></div><div><div><div>5</div></div><div><span>  </span><span>draw</span><span>.</span><span>circle</span><span>((</span><span>0</span><span>, </span><span>0</span><span>), </span><span>radius</span><span>: </span><span>0.6</span><span>, </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#cfe0f5"</span><span>), </span><span>name</span><span>: </span><span>"a"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>  </span><span>draw</span><span>.</span><span>circle</span><span>((</span><span>3</span><span>, </span><span>0</span><span>), </span><span>radius</span><span>: </span><span>0.6</span><span>, </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#f5d9cf"</span><span>), </span><span>name</span><span>: </span><span>"b"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>  </span><span>draw</span><span>.</span><span>line</span><span>(</span><span>"a.east"</span><span>, </span><span>"b.west"</span><span>, </span><span>mark</span><span>: (</span><span>end</span><span>: </span><span>"&gt;"</span><span>))</span></div></div><div><div><div>8</div></div><div><span>})</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>// The version is part of the import: a new release never changes this file.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有任何 <code>pip install</code> 之类的前置步骤。我把缓存目录指到一个不存在的 <code>/tmp/pc14</code>，让这次编译成为真正的“第一次”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ rm -rf /tmp/pc14</span></div></div><div><div><div>2</div></div><div><span>$ time typst compile --root . --ignore-system-fonts --package-cache-path /tmp/pc14 lab/ch14/import-preview.typ /tmp/ip.pdf</span></div></div><div><div><div>3</div></div><div><span>downloading @preview/cetz:0.5.2</span></div></div><div><div><div>4</div></div><div><span>downloading @preview/oxifmt:1.0.0</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>real  0m8.398s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（终端里还有一条随下载刷新的进度条，我省略了。）注意有两行 <code>downloading</code>：我只导入了 CeTZ，但它自己又导入了 <code>oxifmt</code>，Typst 顺着依赖一并拉了下来。依赖不需要我声明，也没有锁文件，因为每个包的导入字符串里都已经写死了版本。</p><p>冷启动和热启动的差距要用 hyperfine 量。冷的那一组每次运行前删掉缓存目录，所以每次都要下载：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ hyperfine -N --warmup 0 --runs 5 --prepare "rm -rf /tmp/pc14" "typst compile --root . --ignore-system-fonts --package-cache-path /tmp/pc14 lab/ch14/import-preview.typ /tmp/ip.pdf"</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>Time (mean ± σ):     11.106 s ±  3.311 s    [User: 0.107 s, System: 0.024 s]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Range (min … max):    5.831 s … 14.515 s    5 runs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>热的那一组先编译一次填好缓存，再用 <code>--warmup 2</code> 预热，命令完全相同：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ hyperfine -N --warmup 2 "typst compile --root . --ignore-system-fonts --package-cache-path /tmp/pc14 lab/ch14/import-preview.typ /tmp/ip.pdf"</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>Time (mean ± σ):      51.6 ms ±   6.1 ms    [User: 40.0 ms, System: 13.7 ms]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Range (min … max):    46.6 ms …  70.5 ms    43 runs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>11.106 s 对 51.6 ms，约 215 倍。但这个比值不能当成 Typst 的性能数字：冷启动的 <code>User</code> 只有 0.107 s，剩下全是等网络，5.8 到 14.5 秒的跨度也是网络抖出来的（我的网络，你的会不一样）。这里真正要记住的结论只有一个：<strong>下载只发生一次，之后包就是磁盘上的一个目录</strong>。</p></section>
<section><h2>缓存里到底有什么<a href="#缓存里到底有什么"><span>#</span></a></h2><p>我用一个新的空目录 <code>/tmp/pc14b</code> 编译 <code>examples/ecosystem/main.typ</code>（CeTZ 加 Lilaq），再编译 <code>examples/ecosystem/touying.typ</code>，两条命令都带 <code>--package-cache-path /tmp/pc14b</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts --package-cache-path /tmp/pc14b examples/ecosystem/main.typ /tmp/eco.pdf</span></div></div><div><div><div>2</div></div><div><span>downloading @preview/cetz:0.5.2</span></div></div><div><div><div>3</div></div><div><span>downloading @preview/oxifmt:1.0.0</span></div></div><div><div><div>4</div></div><div><span>downloading @preview/lilaq:0.6.0</span></div></div><div><div><div>5</div></div><div><span>downloading @preview/elembic:1.1.1</span></div></div><div><div><div>6</div></div><div><span>downloading @preview/zero:0.6.1</span></div></div><div><div><div>7</div></div><div><span>downloading @preview/tiptoe:0.4.0</span></div></div><div><div><div>8</div></div><div><span>$ typst compile --root . --ignore-system-fonts --package-cache-path /tmp/pc14b examples/ecosystem/touying.typ /tmp/tou.pdf</span></div></div><div><div><div>9</div></div><div><span>downloading @preview/touying:0.8.0</span></div></div><div><div><div>10</div></div><div><span>downloading @preview/uniwarn:0.1.1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个“我写在源码里”的包（<code>cetz</code>、<code>lilaq</code>、<code>touying</code>），加上五个“它们自己引入的”，一共 8 个。缓存的结构是 <code>&lt;命名空间&gt;/&lt;名字&gt;/&lt;版本&gt;</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cd /tmp/pc14b &amp;&amp; ls -d preview/*/* &amp;&amp; du -sh . &amp;&amp; du -s preview/*/* | sort -rn &amp;&amp; find . -type f | wc -l</span></div></div><div><div><div>2</div></div><div><span>preview/cetz/0.5.2</span></div></div><div><div><div>3</div></div><div><span>preview/elembic/1.1.1</span></div></div><div><div><div>4</div></div><div><span>preview/lilaq/0.6.0</span></div></div><div><div><div>5</div></div><div><span>preview/oxifmt/1.0.0</span></div></div><div><div><div>6</div></div><div><span>preview/tiptoe/0.4.0</span></div></div><div><div><div>7</div></div><div><span>preview/touying/0.8.0</span></div></div><div><div><div>8</div></div><div><span>preview/uniwarn/0.1.1</span></div></div><div><div><div>9</div></div><div><span>preview/zero/0.6.1</span></div></div><div><div><div>10</div></div><div><span>3.5M  .</span></div></div><div><div><div>11</div></div><div><span>1360  preview/touying/0.8.0</span></div></div><div><div><div>12</div></div><div><span>796  preview/cetz/0.5.2</span></div></div><div><div><div>13</div></div><div><span>596  preview/lilaq/0.6.0</span></div></div><div><div><div>14</div></div><div><span>376  preview/elembic/1.1.1</span></div></div><div><div><div>15</div></div><div><span>128  preview/zero/0.6.1</span></div></div><div><div><div>16</div></div><div><span>100  preview/tiptoe/0.4.0</span></div></div><div><div><div>17</div></div><div><span>88  preview/oxifmt/1.0.0</span></div></div><div><div><div>18</div></div><div><span>28  preview/uniwarn/0.1.1</span></div></div><div><div><div>19</div></div><div><span>214</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>3.5 MiB，214 个文件，8 个包（<code>du -s</code> 的单位是 1 KiB 块）。<code>touying</code> 占的空间最大，1360 KiB，其次是 <code>cetz</code> 的 796 KiB。</p><p>再看一个包的里面。前面 <code>/tmp/pc14</code> 里的 <code>cetz/0.5.2</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/tmp/pc14/preview/cetz/0.5.2/cetz-core</span></div></div><div><div><div>2</div></div><div><span>/tmp/pc14/preview/cetz/0.5.2/cetz-core/cetz_core.wasm</span></div></div><div><div><div>3</div></div><div><span>/tmp/pc14/preview/cetz/0.5.2/gallery</span></div></div><div><div><div>4</div></div><div><span>/tmp/pc14/preview/cetz/0.5.2/LICENSE</span></div></div><div><div><div>5</div></div><div><span>/tmp/pc14/preview/cetz/0.5.2/README.md</span></div></div><div><div><div>6</div></div><div><span>/tmp/pc14/preview/cetz/0.5.2/src</span></div></div><div><div><div>7</div></div><div><span>/tmp/pc14/preview/cetz/0.5.2/typst.toml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个包就是一个普通文件夹：<code>typst.toml</code> 是清单，<code>src/</code> 是代码，外加许可证和说明。有意思的是 <code>cetz-core/cetz_core.wasm</code>：CeTZ 0.5.2 自带一个 WebAssembly 文件，也就是说我下一篇要自己写的东西（<code>plugin</code>），已经在你每天用的包里悄悄运行了。（我没有去读 CeTZ 用它做什么，别把这一句当成对它实现的判断。）</p><p>缓存放在哪里，由系统决定。我这台 Linux 上是 <code>~/.cache/typst/packages/</code>，<code>typst compile --help</code> 说 <code>--package-cache-path</code> 的默认值是“system-dependent location”，也可以用环境变量 <code>TYPST_PACKAGE_CACHE_PATH</code> 设置。上面所有实验我都用 <code>--package-cache-path</code> 指向 <code>/tmp</code>，原因很简单：这样我可以随时 <code>rm -rf</code> 而不碰你机器上真正的缓存。仓库的 CI 没有缓存这个目录（<code>.github/workflows/ci.yml</code> 里没有任何 cache 步骤），每次都从头下载，8 个包的体量下这是可以接受的。</p><p><code>make ch14</code> 的 <code>extra.sh</code> 还会把你默认缓存目录里的包列成 <code>out/ch14/cache.txt</code>；那份清单里除了这 8 个还有什么，因机器而异，与这一章无关。</p></section>
<section><h2>导入字符串的结构<a href="#导入字符串的结构"><span>#</span></a></h2><p><code>@preview/cetz:0.5.2</code> 由三段组成：<code>@命名空间/名字:版本</code>。三段缺一不可，我故意写错三次看 Typst 怎么说（以下三个文件都在 <code>/tmp/p14/</code> 里，不在仓库里，每个文件一行）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@preview/cetz"</span><span>: </span><span>canvas</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: package specification is missing version</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ nov2.typ:1:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #import "@preview/cetz": canvas</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有“默认取最新版”这回事。这是故意的：如果取最新版，同一份源文件今天和明年就可能编译出不同结果，一次包升级就足以让你一份旧文档的排版变掉。把版本写进导入字符串，源码本身就成了锁文件。（类比：这就像文献引用里必须写版次，“Strang《线性代数导论》”和“Strang《线性代数导论》第 6 版”指向的不是同一份文本。）</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@preview/cetz:9.9.9"</span><span>: </span><span>canvas</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: package found, but version 9.9.9 does not exist (latest is 0.5.2)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ nover.typ:1:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #import "@preview/cetz:9.9.9": canvas</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>包名对、版本错，Typst 会去 Universe 的索引里查，并顺便告诉你最新是多少。2026 年 10 月 19 日这一天索引里 CeTZ 的最新版就是 0.5.2，这也是这一章写死的版本。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@preview/nosuchpkg:0.1.0"</span><span>: </span><span>x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: package not found (searched for @preview/nosuchpkg:0.1.0)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ nopkg.typ:1:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #import "@preview/nosuchpkg:0.1.0": x</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>至于命名空间，Typst 里有两个有实际意义的：<code>@preview</code> 是 Universe 上的公开包（对应缓存目录和网络下载），<code>@local</code> 是你自己机器上的包（不联网，下一节讲）。我也试了一个自己编的命名空间 <code>@foo/toy:0.1.0</code>，得到的是同一句 <code>package not found (searched for @foo/toy:0.1.0)</code>，没有更多提示。</p></section>
<section><h2>清单 typst.toml：compiler 字段是怎么被检查的<a href="#清单-typsttomlcompiler-字段是怎么被检查的"><span>#</span></a></h2><p>每个包的根目录都有一份 <code>typst.toml</code>。先看我自己的 <code>xuan</code>（<code>packages/local/xuan/0.1.0/typst.toml</code>，全文）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"xuan"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>entrypoint</span><span> = </span><span>"lib.typ"</span></div></div><div><div><div>5</div></div><div><span>compiler</span><span> = </span><span>"0.15.0"</span></div></div><div><div><div>6</div></div><div><span>authors</span><span> = [</span><span>"bfmhno3"</span><span>]</span></div></div><div><div><div>7</div></div><div><span>license</span><span> = </span><span>"MIT OR Apache-2.0"</span></div></div><div><div><div>8</div></div><div><span>description</span><span> = </span><span>"Notes, homework, slides and CV templates for Chinese and English writing"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>[</span><span>template</span><span>]</span></div></div><div><div><div>11</div></div><div><span>path</span><span> = </span><span>"template"</span></div></div><div><div><div>12</div></div><div><span>entrypoint</span><span> = </span><span>"main.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>[package]</code> 里每个字段的含义：</p><ul>
<li><code>name</code>、<code>version</code>：包自己的名字和版本。我的所有包都按 <code>&lt;名字&gt;/&lt;版本&gt;/</code> 摆放目录，导入字符串里的名字、版本也是这样对应的。</li>
<li><code>entrypoint</code>：<code>#import "@local/xuan:0.1.0": *</code> 实际加载的文件，相对包根目录。CeTZ 是 <code>src/lib.typ</code>，Lilaq 是 <code>src/lilaq.typ</code>，Touying 是 <code>lib.typ</code>（我读了缓存里三个包的 <code>typst.toml</code>）。</li>
<li><code>compiler</code>：这个包要求的 Typst 最低版本。</li>
<li><code>authors</code>、<code>license</code>、<code>description</code>：元数据，说明作者、许可证和一句话介绍。</li>
</ul><p><code>compiler</code> 到底在什么时候检查？我用一个玩具包验证。<code>lab/ch14/manifest/</code> 是一个只有三个文件的包（<a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1/lab/ch14/manifest" target="_blank">目录</a>）。<code>typst.toml</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># The manifest of a toy package. Compare packages/local/xuan/0.1.0/typst.toml.</span></div></div><div><div><div>2</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>3</div></div><div><span>name</span><span> = </span><span>"toy"</span></div></div><div><div><div>4</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>5</div></div><div><span>entrypoint</span><span> = </span><span>"lib.typ"</span></div></div><div><div><div>6</div></div><div><span>authors</span><span> = [</span><span>"bfmhno3"</span><span>]</span></div></div><div><div><div>7</div></div><div><span>license</span><span> = </span><span>"MIT OR Apache-2.0"</span></div></div><div><div><div>8</div></div><div><span>description</span><span> = </span><span>"A toy package for the manifest demo"</span></div></div><div><div><div>9</div></div><div><span>compiler</span><span> = </span><span>"0.15.1"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>lib.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The entrypoint named in typst.toml. Everything public is a top-level let.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>greet</span><span>(</span><span>name</span><span><span>) </span><span>= </span><span>[你好，</span></span><span>#name</span><span>。]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>main.typ</code> 是包内部自己测试用的，它直接 <code>#import "lib.typ": greet</code>，所以这个目录单独就能被 <code>make ch14</code> 当作一份文档编译。要作为 <code>@local/toy:0.1.0</code> 使用，必须把目录放到 <code>&lt;包路径&gt;/local/toy/0.1.0/</code>。<code>lab/ch14/extra.sh</code> 做的正是这件事：复制到 <code>out/ch14/pkgs/local/toy/0.1.0</code>，删掉 <code>main.typ</code>，再写一份 <code>use-toy.typ</code> 导入它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts --package-path out/ch14/pkgs out/ch14/use-toy.typ out/ch14/use-toy.pdf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make ch14</code> 的输出里有一行 <code>ok   @local/toy:0.1.0 via --package-path out/ch14/pkgs</code>，说明导入成功。</p><p>然后我在 <code>/tmp/p14/</code> 里把那份清单的 <code>compiler</code> 改成 <code>"0.16.0"</code>（只改副本，不动仓库），再导入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: package requires Typst 0.16.0 or newer (current version is 0.15.1)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ toy.typ:1:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #import "@local/toy:0.1.0": greet</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以：<code>compiler</code> 是导入时检查的下限，不是编译到某个函数时才发现它不存在。这对你选包很有用：我用到的三个包要求的下限分别是 CeTZ 0.14.0、Lilaq 0.13.0、Touying 0.15.0（读自缓存里的 <code>typst.toml</code>），都不超过我的 0.15.1，所以全部能编译。Touying 的 0.15.0 是最紧的一个：按同样的机制，在 0.14 的编译器上导入它应该会在导入这一行报上面那样的错（我没有装 0.14 去验证）。</p><p><code>[template]</code> 小节让这个包可以被 <code>typst init</code> 展开成一个起始项目。我在 <code>/tmp/p14/pk3/local/toy/0.1.0/</code> 里给玩具包加了 <code>template/main.typ</code> 和这一节（仍然不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>template</span><span>]</span></div></div><div><div><div>2</div></div><div><span>path</span><span> = </span><span>"template"</span></div></div><div><div><div>3</div></div><div><span>entrypoint</span><span> = </span><span>"main.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst init --package-path pk3 @local/toy:0.1.0 ti2</span></div></div><div><div><div>2</div></div><div><span>$ ls ti2</span></div></div><div><div><div>3</div></div><div><span>main.typ</span></div></div><div><div><div>4</div></div><div><span>$ cat ti2/main.typ</span></div></div><div><div><div>5</div></div><div><span>#import "@local/toy:0.1.0": greet</span></div></div><div><div><div>6</div></div><div><span>#greet("模板")</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>typst init</code> 成功时还会打印几行提示语，我没有贴，那一行里有一个 emoji。）它把 <code>template/</code> 目录里的文件复制到新目录，模板文件里仍然写着 <code>@local/toy:0.1.0</code>，所以展开出来的项目不是一份自包含的拷贝，而是依赖这个包。Touying 没有 <code>[template]</code>，对它 <code>typst init</code> 得到的是 <code>error: package @preview/touying:0.8.0 is not a template</code>。<code>xuan</code> 的模板是第 16 篇的内容。</p></section>
<section><h2>@local 与 —package-path<a href="#local-与-package-path"><span>#</span></a></h2><p><code>@local</code> 包没有索引也不联网，Typst 只会去磁盘上的包目录里找。我先用 <code>--package-path</code> 的“找不到”报错确认这一点：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: package not found (searched for @local/toy:0.1.0)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ nolocal.typ:1:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #import "@local/toy:0.1.0": greet</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（那次编译没有设 <code>TYPST_PACKAGE_PATH</code>，也没有 <code>--package-path</code>，所以找不到。）</p><p>包目录的约定是 <code>&lt;包路径&gt;/&lt;命名空间&gt;/&lt;名字&gt;/&lt;版本&gt;/typst.toml</code>。有两种告诉 Typst 包路径的办法：</p><ul>
<li><code>--package-path &lt;DIR&gt;</code> 或环境变量 <code>TYPST_PACKAGE_PATH</code>。仓库的 <code>flake.nix</code> 在 <code>shellHook</code> 里导出了 <code>TYPST_PACKAGE_PATH="$PWD/packages"</code>，这就是 <code>xuan</code> 在 <code>nix develop</code> 里什么都不装就能用的原因。</li>
<li>默认位置。我用 <code>XDG_DATA_HOME=/tmp/p14/xdg</code> 和一个符号链接 <code>xdg/typst/packages/local/toy -&gt; /tmp/p14/pk/local/toy</code> 验证了默认位置在 <code>$XDG_DATA_HOME/typst/packages</code> 之下；命令里 <code>env -u TYPST_PACKAGE_PATH</code> 清掉了环境变量，编译输出的文字是 <code>你好，x。</code>。没有设 <code>XDG_DATA_HOME</code> 时，我猜是 <code>~/.local/share/typst/packages</code>（Linux 的惯例，我没有单独验证这一情形）。<code>make install</code> 做的就是这件事：把仓库里的 <code>packages/local/xuan</code> 链接到 <code>~/.local/share/typst/packages/local/xuan</code>，这样不进 <code>nix develop</code> 也能导入 <code>@local/xuan:0.1.0</code>。</li>
</ul><p>还有一个事实容易被忽略：<code>--package-path</code> 不限于 <code>@local</code>，任何命名空间都先找这里。我在 <code>/tmp/p14/pk2/preview/zzdemo/1.0.0/</code> 里写了一个 <code>@preview/zzdemo:1.0.0</code>（Universe 上并没有这个包），导入它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@preview/zzdemo:1.0.0"</span><span>: </span><span>hi</span></div></div><div><div><div>2</div></div><div><span>#hi</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不带 <code>--package-path</code> 时得到 <code>error: package not found (searched for @preview/zzdemo:1.0.0)</code>；带上 <code>--package-path pk2</code>，同一份文件编译通过，<code>pdftotext</code> 读出 <code>from package path</code>。</p><p>这个特性在离线环境里很好用：把需要的 <code>@preview</code> 包预先放进一个目录，提交进仓库或打进镜像，编译就不依赖网络。（我没有在断网环境里实测这一点，上面只证明了“包路径先于下载”这一半。）</p></section>
<section><h2>CeTZ 与 Lilaq：画图的两种思路<a href="#cetz-与-lilaq画图的两种思路"><span>#</span></a></h2><p>第 13 篇用内置的 <code>rect</code>、<code>line</code> 画过图，那是“按坐标摆形状”。CeTZ 是它的升级：有命名锚点，可以写 <code>a.east</code> 这样的位置。<code>lab/ch14/fig-cetz.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch14/fig-cetz.typ" target="_blank">源文件</a>，全文 28 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// CeTZ: the compile pipeline as boxes and arrows.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@preview/cetz:0.5.2"</span><span>: </span><span>canvas</span><span>, </span><span>draw</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: </span><span>fonts</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>sans</span><span>, </span><span>size</span><span>: </span><span>9pt</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>stage</span><span>(</span><span>x</span><span>, </span><span>name</span><span>, </span><span>label</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>9</div></div><div><span>  </span><span>draw</span><span>.</span><span>rect</span><span>(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(</span><span>x</span><span><span> </span><span>-</span><span> </span></span><span>1</span><span><span>, </span><span>-</span></span><span>0.6</span><span>),</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>(</span><span>x</span><span><span> </span><span>+</span><span> </span></span><span>1</span><span>, </span><span>0.6</span><span>),</span></div></div><div><div><div>12</div></div><div><span>    </span><span>name</span><span>: </span><span>name</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#cfe0f5"</span><span>),</span></div></div><div><div><div>14</div></div><div><span>    </span><span>stroke</span><span>: </span><span>0.8pt</span><span><span> </span><span>+</span><span> </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>),</span></div></div><div><div><div>15</div></div><div><span>    </span><span>radius</span><span>: </span><span>0.15</span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span>  </span><span>draw</span><span>.</span><span>content</span><span>(</span><span>name</span><span>, </span><span>label</span><span>)</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>#canvas</span><span>({</span></div></div><div><div><div>21</div></div><div><span>  </span><span>stage</span><span>(</span><span>0</span><span>, </span><span>"parse"</span><span>, [解析])</span></div></div><div><div><div>22</div></div><div><span>  </span><span>stage</span><span>(</span><span>3.2</span><span>, </span><span>"eval"</span><span>, [求值])</span></div></div><div><div><div>23</div></div><div><span>  </span><span>stage</span><span>(</span><span>6.4</span><span>, </span><span>"layout"</span><span>, [排版])</span></div></div><div><div><div>24</div></div><div><span>  </span><span>stage</span><span>(</span><span>9.6</span><span>, </span><span>"export"</span><span>, [导出])</span></div></div><div><div><div>25</div></div><div><span>  </span><span>for</span><span> (</span><span>a</span><span>, </span><span>b</span><span>) </span><span>in</span><span> ((</span><span>"parse"</span><span>, </span><span>"eval"</span><span>), (</span><span>"eval"</span><span>, </span><span>"layout"</span><span>), (</span><span>"layout"</span><span>, </span><span>"export"</span><span>)) {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>draw</span><span>.</span><span>line</span><span>(</span><span>a</span><span><span> </span><span>+</span><span> </span></span><span>".east"</span><span>, </span><span>b</span><span><span> </span><span>+</span><span> </span></span><span>".west"</span><span>, </span><span>mark</span><span>: (</span><span>end</span><span>: </span><span>"&gt;"</span><span>))</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch14-cetz.png" alt="CeTZ 画的编译流水线：四个浅蓝色圆角方框依次写着解析、求值、排版、导出，相邻方框之间有从左指向右的箭头" /><figcaption>CeTZ 画的编译流水线：四个浅蓝色圆角方框依次写着解析、求值、排版、导出，相邻方框之间有从左指向右的箭头</figcaption></figure><p></p><p>三点值得讲。第一，<code>canvas({ ... })</code> 的参数是一个<strong>代码块</strong>，里面不是 content 而是一串 <code>draw.*</code> 调用，每个调用返回的是“绘图指令”，由 <code>canvas</code> 统一执行；这和第 3 篇讲的“函数返回 content”不同，它是 CeTZ 自己的一层小语言。第二，<code>name: name</code> 给矩形起了名字，后面 <code>draw.content(name, label)</code> 把文字放在那个名字的中心，<code>draw.line(a + ".east", ...)</code> 用的是名字加锚点，不用算坐标，加一个阶段只需要多写一行 <code>stage(...)</code>。第三，<code>for (a, b) in (...)</code> 是第 2 篇的解构，直接用在了画图的循环上。</p><p><code>#import "@local/xuan:0.1.0": fonts</code> 是为了字体名不重复写：这里 <code>fonts.sans</code> 就是第 5 篇的 <code>Noto Sans CJK SC</code>。包和包之间可以随便混用，<code>@preview</code> 的 CeTZ 和 <code>@local</code> 的 <code>xuan</code> 在同一份文件里互不干扰。</p><p>Lilaq 的思路则是“数据在前”，<code>lab/ch14/fig-lilaq.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch14/fig-lilaq.typ" target="_blank">源文件</a>，全文 21 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Lilaq: plot the CSV that examples/ecosystem ships.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@preview/lilaq:0.6.0"</span><span> </span><span>as</span><span> </span><span>lq</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: </span><span>fonts</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>serif</span><span>, </span><span>size</span><span>: </span><span>9pt</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>rows</span><span><span> </span><span>= </span></span><span>csv</span><span>(</span><span>"/examples/ecosystem/data/damped.csv"</span><span>, </span><span>row-type</span><span>: </span><span>dictionary</span><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#lq</span><span>.</span><span>diagram</span><span>(</span></div></div><div><div><div>11</div></div><div><span>  </span><span>width</span><span>: </span><span>9cm</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>height</span><span>: </span><span>4.5cm</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>xlabel</span><span>: </span><span>$</span><span>t</span><span>$</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>ylabel</span><span>: </span><span>$</span><span>y</span><span>$</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>lq</span><span>.</span><span>plot</span><span>(</span></div></div><div><div><div>16</div></div><div><span>    </span><span>rows</span><span>.</span><span>map</span><span>(</span><span>r</span><span> </span><span>=&gt;</span><span> </span><span>float</span><span>(</span><span>r</span><span>.</span><span>t</span><span>)),</span></div></div><div><div><div>17</div></div><div><span>    </span><span>rows</span><span>.</span><span>map</span><span>(</span><span>r</span><span> </span><span>=&gt;</span><span> </span><span>float</span><span>(</span><span>r</span><span>.</span><span>y</span><span>)),</span></div></div><div><div><div>18</div></div><div><span>    </span><span>mark</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>stroke</span><span>: </span><span>1.2pt</span><span><span> </span><span>+</span><span> </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>),</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>21</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch14-lilaq.png" alt="Lilaq 画的曲线：横轴 t 从 0 到 10，纵轴 y 在 1 与负 0.6 之间振荡，振幅逐渐衰减的阻尼余弦曲线，带网格" /><figcaption>Lilaq 画的曲线：横轴 t 从 0 到 10，纵轴 y 在 1 与负 0.6 之间振荡，振幅逐渐衰减的阻尼余弦曲线，带网格</figcaption></figure><p></p><p>数据来自 <code>data/damped.csv</code>（201 行，<span><span>y=e−0.3tcos⁡2ty = e^{-0.3t}\cos 2t</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>0.3</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>2</span><span>t</span></span></span></span>，第 11 篇的 <code>csv(..., row-type: dictionary)</code> 加上第 2 篇的 <code>map</code>）。这里路径是 <code>"/examples/ecosystem/data/damped.csv"</code>，以 <code>/</code> 开头，相对 <code>--root .</code>，因为 <code>lab/ch14/</code> 离 <code>examples/</code> 隔了好几层；<code>examples/ecosystem/main.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/ecosystem/main.typ" target="_blank">源文件</a>，48 行，里面同时用 CeTZ 和 Lilaq 画了这两张图）则和数据同在一个目录树里，直接写相对路径 <code>data/damped.csv</code>。<code>import ... as lq</code> 与 <code>import ...: canvas, draw</code> 是第 12 篇讲的两种导入形式，这里一个包各用了一种：CeTZ 的名字只有两个，直接导入；Lilaq 的名字多且容易和别处撞，留在 <code>lq.</code> 前缀后面。</p><p>这份文档热缓存下的编译时间：<code>hyperfine -N --warmup 2 "typst compile --root . --ignore-system-fonts examples/ecosystem/main.typ /tmp/eco.pdf"</code>，543.3 ms ± 65.1 ms。比第一节里只导入 CeTZ 的 51.6 ms 慢了十倍，原因我没有拆开量，大概是 Lilaq 的包体量和求值开销，别当成结论。</p></section>
<section><h2>Touying：包不一定更短<a href="#touying包不一定更短"><span>#</span></a></h2><p>第 13 篇的 <code>slides</code> 是我自己写的，95 行。Touying 是 Universe 上成熟的幻灯片包。我想知道的是：用它重写同样的内容，我能省下多少？<code>examples/ecosystem/touying.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/ecosystem/touying.typ" target="_blank">源文件</a>，全文）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Three slides of examples/slides, rebuilt with Touying.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@preview/touying:0.8.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>themes</span><span>.</span><span>simple</span><span>: *</span></div></div><div><div><div>4</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: </span><span>fonts</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>sans</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#show</span><span>: </span><span>simple-theme</span><span>.</span><span>with</span><span>(</span><span>aspect-ratio</span><span>: </span><span>"16-9"</span><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>=</span><span> Typst 一小时入门</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>==</span><span> 三种模式</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>- 标记模式：默认，写正文</span></div></div><div><div><div>14</div></div><div><span>- 代码模式：</span><span>`#`</span><span> 进入，写逻辑</span></div></div><div><div><div>15</div></div><div><span>- 数学模式：</span><span>`$ ... $`</span><span>，写公式</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>==</span><span> 编译很快</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>编译一份 10 页文档：</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#pause</span></div></div><div><div><div>22</div></div><div><span>- 解析：把标记变成语法树</span></div></div><div><div><div>23</div></div><div><span>#pause</span></div></div><div><div><div>24</div></div><div><span>- 求值：运行代码，得到 content</span></div></div><div><div><div>25</div></div><div><span>#pause</span></div></div><div><div><div>26</div></div><div><span>- 排版：多遍迭代，直到页码和引用稳定</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>==</span><span> 一个公式</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>最小二乘的正规方程：</span></div></div><div><div><div>31</div></div><div><span>$</span><span> A^T A </span><span>hat</span><span>(x) = A^T b </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里的结构和 <code>xuan</code> 完全不同：幻灯片不是 <code>#slide(...)[...]</code> 调用，而是由<strong>标题级别</strong>决定的：<code>=</code> 是章节，<code>==</code> 是一页，<code>#pause</code> 让后面的内容在下一个子页出现。这一切由 <code>#show: simple-theme.with(aspect-ratio: "16-9")</code> 驱动，也就是第 4 篇的 <code>show: template.with(...)</code> 形状，只是这个“模板”是整个包的主题。我猜它是用 show 规则读取标题来切页的，但我没有去读 Touying 的实现。</p><p>行数我用 <code>extra.sh</code> 数了，结果写进 <code>out/ch14/linecount.txt</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>touying.typ        31 lines (3 slides)</span></div></div><div><div><div>2</div></div><div><span>examples/slides    28 lines (same 3 slides plus preamble)</span></div></div><div><div><div>3</div></div><div><span>examples/slides    99 lines (whole deck)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Touying 的 3 页是 31 行，我自己的 <code>xuan</code> 版本是 28 行：<strong>Touying 更长</strong>。第二行的 28 行由两部分组成：<code>examples/slides/main.typ</code> 到第一个 <code>=</code> 标题之前的 9 行前言（导入和 <code>slides.with(...)</code>），加上 <code>三种模式</code>、<code>编译很快</code>、<code>一个公式</code> 这三个 <code>#slide(...)</code> 块的 19 行（统计规则在 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch14/extra.sh" target="_blank"><code>lab/ch14/extra.sh</code></a> 里的一段 awk）。</p><p>这个对比有几处不公平，我都列出来：</p><ul>
<li>我的 <code>slides</code> 只做了我要的功能，而 Touying 的缓存目录里有 8 个主题文件（<code>themes/</code> 下的 aqua、article、default、dewdrop、metropolis、simple、stargazer、university），我只用了其中的 <code>simple</code>。比行数只比了一个维度。</li>
<li>Touying 用标题切页，省掉了 <code>#slide(title: "...")[ ]</code> 的括号，但 <code>#pause</code> 单独占了 3 行，我的 <code>steps: 3, k =&gt; [...]</code> 把同样的事放在了函数里。</li>
<li>页数不同：Touying 编出 7 页（<code>pdfinfo out/ch14/touying.pdf</code>）。我数出来的构成是：标题页 1，三种模式 1，编译很快 4（3 个 <code>#pause</code> 切出 4 个子页，第一个子页只有那句引导语），公式 1。页脚显示 <code>2/4</code>、<code>3/4</code>、<code>4/4</code>，也就是“第几张幻灯片”，同一张幻灯片的子页共用一个编号，而不是页码。</li>
<li>体量：缓存里 Touying 占 1360 KiB，我的 <code>src/slides.typ</code> 是 95 行。当然我的版本只在我自己的文档里工作。</li>
</ul><p>如果我只想要一份自己的幻灯片，自己写的 95 行够用；如果要换主题、做动画，Touying 把这些工作做完了。这个选择不是“包比自己写好”，而是“谁来维护这部分代码”。我猜多数日常场景下，自己写 100 行比读懂别人的 1360 KiB 更省事，但我没有做任何用户研究，这只是我的偏好。</p><p><code>touying.typ</code> 热缓存编译 197.1 ms ± 15.6 ms（<code>hyperfine -N --warmup 2 "typst compile --root . --ignore-system-fonts examples/ecosystem/touying.typ /tmp/tou.pdf"</code>）。</p></section>
<section><h2>版本，固定在哪里<a href="#版本固定在哪里"><span>#</span></a></h2><p>回头看这一章用到的所有版本：<code>cetz:0.5.2</code>、<code>lilaq:0.6.0</code>、<code>touying:0.8.0</code>，全部写在 <code>import</code> 字符串里，没有别的地方。要升级只改那一行；不改，源码就永远指向同一份包代码，缓存目录里的 <code>preview/cetz/0.5.2/</code> 也不会被覆盖（版本在路径里，新版本是另一个目录）。对一个会发表、会交作业、会隔几年再打开的文档，这是我想要的行为。</p><p>下一篇我写自己的包里最“重”的一块：用 Rust 编译一个 WebAssembly 文件，放进 <code>xuan</code> 的 <code>plugin/</code> 目录，让术语表按拼音排序。你刚才在 <code>cetz-core/cetz_core.wasm</code> 里见过它的同类。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-13-slides-and-drawing/</id>
      <title type="text">幻灯片与绘图</title>
      <published>2026-10-19T12:00:00.000Z</published>
      <updated>2026-10-19T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-13-slides-and-drawing/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 13 篇：我用 Typst 0.15.1 把 examples/slides 编成 17 页（13 个幻灯片编号，135.5 ms），逐页渲染检查了分步页、进度条页脚和绘图页，用 ch13 的 8 个 lab 文件跑通形状、颜色、渐变、tiling 与描边，并写出 xuan 的 slides.typ（95 行）。</summary>
      <content type="html"><![CDATA[<p>一份幻灯片，在 Typst 眼里就是一份页面比较特别的文档：页面是 16&lt;9&gt;，字号 20pt，每个“幻灯片”占一页，页脚里画一条进度条。<code>examples/slides</code> 这份《Typst 一小时入门》有 9 张内容幻灯片（两张是分步显示的），编出来是 17 页，<code>xuan</code> 里负责它的 <code>src/slides.typ</code> 只有 95 行。这一篇我先把这 17 页逐页渲染出来看，再把那 95 行拆开，最后补上它里面用到的绘图原语：<code>rect</code>、<code>circle</code>、<code>curve</code>、颜色、渐变、<code>tiling</code> 和描边。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有一个新文件 <code>src/slides.typ</code>（<strong>最终版</strong>，95 行，文中完整展示），并在 <code>lib.typ</code> 里加上一行 <code>#import "src/slides.typ": only, slide, slides, uncover</code>。<code>lib.typ</code> 仍是<strong>中间版本</strong>（后面的章节还要往里加行）。<code>examples/slides/main.typ</code> 和 <code>lab/ch13/</code> 下的 8 个文件全部通过 <code>make ch13</code>。
前置：<a href="/posts/typst-by-xuan-12-homework/">第 12 篇</a>（模块与 <code>include</code>）。<code>fonts.typ</code> 与 <code>page.typ</code>（<code>title-block</code>）在第 5、6 篇已经写好，这一篇的 <code>slides.typ</code> 直接复用它们。</p></div></div>
<section><h2>先看成品：17 页，13 个编号<a href="#先看成品17-页13-个编号"><span>#</span></a></h2><p>在 <code>~/Projects/typst-lab</code> 里，<code>nix develop</code> 之后 <code>make examples</code> 已经把 <code>out/examples/slides.pdf</code> 编好了。先数页数和页面尺寸：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pdfinfo</span><span> </span><span>out/examples/slides.pdf</span><span> | </span><span>grep</span><span> </span><span>-E</span><span> </span><span>'Pages|Page size'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Pages:           17</span></div></div><div><div><div>2</div></div><div><span>Page size:       841.89 x 473.563 pts</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>841.89 × 473.563 pt 是 <code>presentation-16-9</code> 这个纸张名对应的尺寸。再看页脚上印的数字，<code>pdftotext -layout</code> 把 17 页的文字抽出来，只留 <code>n / N</code> 的行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pdftotext</span><span> </span><span>-layout</span><span> </span><span>out/examples/slides.pdf</span><span> </span><span>-</span><span> | </span><span>grep</span><span> </span><span>-E</span><span> </span><span>'[0-9]+ / [0-9]+'</span><span> | </span><span>tr</span><span> </span><span>-s</span><span> </span><span>' '</span><span> | </span><span>tr</span><span> </span><span>'\n'</span><span> </span><span>';'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>1 / 13; 2 / 13; 3 / 13; 3 / 13; 3 / 13; 4 / 13; 5 / 13; 6 / 13; 7 / 13; 8 / 13; 9 / 13; 9 / 13; 10 / 13; 11 / 13; 12 / 13; 13 / 13;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>17 页只有 16 行，因为第 1 页是标题页，没有页脚。16 行里编号一共到 13，<code>3 / 13</code> 出现了三次，<code>9 / 13</code> 出现了两次：这就是“分步显示”在页脚上的样子，同一张幻灯片占几页，编号就重复几次。17 = 1 张标题页 + 4 张章节分隔页 + 12 页内容（其中“编译很快”占 3 页，“内置图形”占 2 页，另有 7 张单页幻灯片）；13 = 4 + 9，即 4 张分隔页加 9 张内容幻灯片。</p><p>这份文档编译要多久？<code>hyperfine</code> 的结果（<code>--warmup 2</code>，20 次运行）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>/tmp/p13</span></div></div><div><div><div>2</div></div><div><span>hyperfine</span><span> </span><span>-w</span><span> </span><span>2</span><span> </span><span>"typst compile --root . --ignore-system-fonts examples/slides/main.typ /tmp/p13/s.pdf"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Benchmark 1: typst compile --root . --ignore-system-fonts examples/slides/main.typ /tmp/p13/s.pdf</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>Time (mean ± σ):     135.5 ms ±   4.2 ms    [User: 229.0 ms, System: 86.5 ms]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Range (min … max):   127.0 ms … 143.5 ms    20 runs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>135.5 ms 编完 17 页，其中包括字体加载和页脚里要求 <code>counter.final()</code> 带来的多遍排版（这一机制见<a href="/posts/typst-by-xuan-07-context-counters-state/">第 7 篇</a>）。</p><p>我把每一页渲染成 PNG 来看（这里的命令把输出放在 <code>/tmp</code>，<code>{0p}</code> 是补零的页码模板）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>.</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>--format</span><span> </span><span>png</span><span> </span><span>--ppi</span><span> </span><span>72</span><span> </span><span>examples/slides/main.typ</span><span> </span><span>"/tmp/p13/slides-{0p}.png"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（路径里的 <code>/tmp/p13</code> 是我建的目录，先 <code>mkdir -p</code> 一下。）然后用 ImageMagick 7.1.2 的 <code>magick montage</code>（在 dev shell 之外运行，不是仓库的一部分）拼成一张总览：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-deck-sheet.png" alt="examples/slides 的 17 页总览：标题页、四张蓝色章节分隔页、三页逐步出现的“编译很快”、公式页、表格页、代码页、两页逐步出现的“内置图形”、渐变与描边页、小结与结束页" /><figcaption>examples/slides 的 17 页总览：标题页、四张蓝色章节分隔页、三页逐步出现的“编译很快”、公式页、表格页、代码页、两页逐步出现的“内置图形”、渐变与描边页、小结与结束页</figcaption></figure><p></p><p>源文件 <code>examples/slides/main.typ</code> 共 99 行，下面是它的前 26 行：用 <code>show: slides.with(...)</code> 套上模板，<code>=</code> 一级标题产生章节页，<code>#slide(...)[...]</code> 是内容页，第二张用了函数形式的 body 来分步（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/slides/main.typ" target="_blank">完整源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>slides</span><span>.</span><span>with</span><span>(</span></div></div><div><div><div>4</div></div><div><span>  </span><span>title</span><span>: </span><span>"Typst 一小时入门"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>subtitle</span><span>: </span><span>"标记、脚本与排版"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>author</span><span>: </span><span>"张三"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>date</span><span>: </span><span>"2026 年 10 月"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>=</span><span> 为什么是 Typst</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#slide</span><span>(</span><span>title</span><span>: </span><span>"三种模式"</span><span>)[</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>- 标记模式：默认，写正文</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>- 代码模式：</span><span>`#`</span><span> 进入，写逻辑</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>- 数学模式：</span><span>`$ ... $`</span><span>，写公式</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>整个文档是一棵函数调用树，</span><span>`set`</span><span> 与 </span><span>`show`</span><span> 改变它的样式。</span></div></div><div><div><div>18</div></div><div><span>]</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>#slide</span><span>(</span><span>title</span><span>: </span><span>"编译很快"</span><span>, </span><span>steps</span><span>: </span><span>3</span><span>, </span><span>k</span><span> </span><span>=&gt;</span><span> [</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>编译一份 10 页文档：</span></div></div><div><div><div>22</div></div><div><span>  </span><span>#v</span><span>(</span><span>0.5em</span><span>)</span></div></div><div><div><div>23</div></div><div><span>  </span><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>1</span><span>)[- 解析：把标记变成语法树]</span></div></div><div><div><div>24</div></div><div><span>  </span><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>2</span><span>)[- 求值：运行代码，得到 content]</span></div></div><div><div><div>25</div></div><div><span>  </span><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>3</span><span>)[- 排版：多遍迭代，直到页码和引用稳定]</span></div></div><div><div><div>26</div></div><div><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意标题页上的日期是写死的字符串 <code>"2026 年 10 月"</code>，这不是偷懒，下面“日期”一节解释原因。</p></section>
<section><h2>一张幻灯片就是一个 pagebreak 之后的内容<a href="#一张幻灯片就是一个-pagebreak-之后的内容"><span>#</span></a></h2><p>先把 95 行全文放在这里（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/slides.typ" target="_blank">源文件</a>），后面按功能拆开讲：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Slides: one page per slide, steps as extra pages, progress bar footer.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"fonts.typ"</span><span>: </span><span>fonts</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"page.typ"</span><span>: </span><span>title-block</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>/// Content shown from step `from` on; before that it is hidden, so the</span></div></div><div><div><div>6</div></div><div><span>/// layout does not jump. `hide` leaves list markers and enum numbers drawn,</span></div></div><div><div><div>7</div></div><div><span>/// so they are blanked inside the hidden copy.</span></div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>1</span><span>, </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>9</div></div><div><span>  </span><span>if</span><span> </span><span>k</span><span><span> </span><span>&gt;=</span><span> </span></span><span>from</span><span> { </span><span>body</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>hide</span><span>({</span></div></div><div><div><div>11</div></div><div><span>      </span><span>set</span><span> </span><span>list</span><span>(</span><span>marker</span><span>: [])</span></div></div><div><div><div>12</div></div><div><span>      </span><span>set</span><span> </span><span>enum</span><span>(</span><span>numbering</span><span>: </span><span>_</span><span> </span><span>=&gt;</span><span> [])</span></div></div><div><div><div>13</div></div><div><span>      </span><span>body</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>/// Content present only at the listed step(s).</span></div></div><div><div><div>19</div></div><div><span>#let</span><span> </span><span>only</span><span>(</span><span>k</span><span>, </span><span>at</span><span>: (</span><span>1</span><span>,), </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>20</div></div><div><span>  </span><span>let</span><span> </span><span>steps</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>type</span><span>(</span><span>at</span><span><span>) </span><span>==</span><span> </span></span><span>array</span><span> { </span><span>at</span><span> } </span><span>else</span><span> { (</span><span>at</span><span>,) }</span></div></div><div><div><div>21</div></div><div><span>  </span><span>if</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>steps</span><span> { </span><span>body</span><span> }</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>/// A slide. `body` is content, or a function `k =&gt; content` that is called</span></div></div><div><div><div>25</div></div><div><span>/// once per step and produces `steps` pages sharing one slide number.</span></div></div><div><div><div>26</div></div><div><span>#let</span><span> </span><span>slide</span><span>(</span><span>title</span><span>: </span><span>none</span><span>, </span><span>steps</span><span>: </span><span>1</span><span>, </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>27</div></div><div><span>  </span><span>let</span><span> </span><span>steps</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>type</span><span>(</span><span>body</span><span><span>) </span><span>==</span><span> </span></span><span>function</span><span> { </span><span>steps</span><span> } </span><span>else</span><span> { </span><span>1</span><span> }</span></div></div><div><div><div>28</div></div><div><span>  </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>steps</span><span><span> </span><span>+</span><span> </span></span><span>1</span><span>) {</span></div></div><div><div><div>29</div></div><div><span>    </span><span>pagebreak</span><span>(</span><span>weak</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>if</span><span> </span><span>k</span><span><span> </span><span>==</span><span> </span></span><span>1</span><span> { </span><span>counter</span><span>(</span><span>"xuan-slide"</span><span>).</span><span>step</span><span>() }</span></div></div><div><div><div>31</div></div><div><span>    </span><span>if</span><span> </span><span>title</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>32</div></div><div><span>      </span><span>block</span><span>(</span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.4em</span><span>, </span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>title</span><span>), </span><span>below</span><span>: </span><span>0.9em</span><span>)</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>    </span><span>if</span><span> </span><span>type</span><span>(</span><span>body</span><span><span>) </span><span>==</span><span> </span></span><span>function</span><span> { </span><span>body</span><span>(</span><span>k</span><span>) } </span><span>else</span><span> { </span><span>body</span><span> }</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>/// Footer: progress bar over `n / N`.</span></div></div><div><div><div>39</div></div><div><span>#let</span><span> </span><span>slide-footer</span><span><span>() </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>40</div></div><div><span>  </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"xuan-slide"</span><span>).</span><span>get</span><span>().</span><span>first</span><span>()</span></div></div><div><div><div>41</div></div><div><span>  </span><span>let</span><span> </span><span>total</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"xuan-slide"</span><span>).</span><span>final</span><span>().</span><span>first</span><span>()</span></div></div><div><div><div>42</div></div><div><span>  </span><span>let</span><span> </span><span>accent</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>43</div></div><div><span>  </span><span>stack</span><span>(</span></div></div><div><div><div>44</div></div><div><span>    </span><span>spacing</span><span>: </span><span>4pt</span><span>,</span></div></div><div><div><div>45</div></div><div><span>    </span><span>block</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>height</span><span>: </span><span>2pt</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>225</span><span>), </span><span>rect</span><span>(</span></div></div><div><div><div>46</div></div><div><span>      </span><span>width</span><span>: </span><span>100%</span><span><span> </span><span>*</span><span> </span></span><span>n</span><span><span> </span><span>/</span><span> </span></span><span>calc</span><span>.</span><span>max</span><span>(</span><span>total</span><span>, </span><span>1</span><span>),</span></div></div><div><div><div>47</div></div><div><span>      </span><span>height</span><span>: </span><span>2pt</span><span>,</span></div></div><div><div><div>48</div></div><div><span>      </span><span>fill</span><span>: </span><span>accent</span><span>,</span></div></div><div><div><div>49</div></div><div><span>      </span><span>stroke</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>)),</span></div></div><div><div><div>51</div></div><div><span>    </span><span>align</span><span>(</span><span>right</span><span>, </span><span>text</span><span>(</span><span>size</span><span>: </span><span>0.6em</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>110</span><span>))[</span><span>#n</span><span> / </span><span>#total</span><span>]),</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>53</div></div><div><span>}</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>/// The whole deck. Level-1 headings become section-divider slides.</span></div></div><div><div><div>56</div></div><div><span>#let</span><span> </span><span>slides</span><span>(</span></div></div><div><div><div>57</div></div><div><span>  </span><span>title</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>58</div></div><div><span>  </span><span>subtitle</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>59</div></div><div><span>  </span><span>author</span><span>: (),</span></div></div><div><div><div>60</div></div><div><span>  </span><span>date</span><span>: </span><span>auto</span><span>,</span></div></div><div><div><div>61</div></div><div><span>  </span><span>aspect</span><span>: </span><span>"16-9"</span><span>,</span></div></div><div><div><div>62</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>63</div></div><div><span>  </span><span>body</span><span>,</span></div></div><div><div><div>64</div></div><div><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>65</div></div><div><span>  </span><span>set</span><span> </span><span>document</span><span>(</span><span>title</span><span>: </span><span>if</span><span> </span><span>title</span><span><span> </span><span>==</span><span> </span></span><span>none</span><span> { </span><span>""</span><span> } </span><span>else</span><span> { </span><span>title</span><span> })</span></div></div><div><div><div>66</div></div><div><span>  </span><span>set</span><span> </span><span>page</span><span>(</span></div></div><div><div><div>67</div></div><div><span>    </span><span>paper</span><span>: </span><span>"presentation-"</span><span><span> </span><span>+</span><span> </span></span><span>aspect</span><span>,</span></div></div><div><div><div>68</div></div><div><span>    </span><span>margin</span><span>: (</span><span>x</span><span>: </span><span>2em</span><span>, </span><span>top</span><span>: </span><span>2.5em</span><span>, </span><span>bottom</span><span>: </span><span>2em</span><span>),</span></div></div><div><div><div>69</div></div><div><span>    </span><span>footer</span><span>: </span><span>slide-footer</span><span>(),</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>71</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>sans</span><span>, </span><span>size</span><span>: </span><span>20pt</span><span>, </span><span>lang</span><span>: </span><span>lang</span><span>)</span></div></div><div><div><div>72</div></div><div><span>  </span><span>set</span><span> </span><span>par</span><span>(</span><span>leading</span><span>: </span><span>0.7em</span><span>, </span><span>spacing</span><span>: </span><span>1em</span><span>)</span></div></div><div><div><div>73</div></div><div><span>  </span><span>show</span><span> </span><span>raw</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>mono</span><span>, </span><span>size</span><span>: </span><span>0.85em</span><span>)</span></div></div><div><div><div>74</div></div><div><span>  </span><span>show</span><span> </span><span>math</span><span>.</span><span>equation</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>math</span><span>)</span></div></div><div><div><div>75</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>76</div></div><div><span>    </span><span>pagebreak</span><span>(</span><span>weak</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>77</div></div><div><span>    </span><span>counter</span><span>(</span><span>"xuan-slide"</span><span>).</span><span>step</span><span>()</span></div></div><div><div><div>78</div></div><div><span>    </span><span>v</span><span>(</span><span>1fr</span><span>)</span></div></div><div><div><div>79</div></div><div><span>    </span><span>align</span><span>(</span><span>center</span><span>, </span><span>text</span><span>(</span></div></div><div><div><div>80</div></div><div><span>      </span><span>size</span><span>: </span><span>1.8em</span><span>,</span></div></div><div><div><div>81</div></div><div><span>      </span><span>weight</span><span>: </span><span>"bold"</span><span>,</span></div></div><div><div><div>82</div></div><div><span>      </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>),</span></div></div><div><div><div>83</div></div><div><span>      </span><span>it</span><span>.</span><span>body</span><span>,</span></div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>))</span></div></div><div><div><div>85</div></div><div><span>    </span><span>v</span><span>(</span><span>1fr</span><span>)</span></div></div><div><div><div>86</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>87</div></div><div><span>  </span><span>page</span><span>(</span><span>footer</span><span>: </span><span>none</span><span>, </span><span>align</span><span>(</span><span>horizon</span><span>, </span><span>title-block</span><span>(</span></div></div><div><div><div>88</div></div><div><span>    </span><span>title</span><span>: </span><span>title</span><span>,</span></div></div><div><div><div>89</div></div><div><span>    </span><span>subtitle</span><span>: </span><span>subtitle</span><span>,</span></div></div><div><div><div>90</div></div><div><span>    </span><span>author</span><span>: </span><span>author</span><span>,</span></div></div><div><div><div>91</div></div><div><span>    </span><span>date</span><span>: </span><span>date</span><span>,</span></div></div><div><div><div>92</div></div><div><span>    </span><span>lang</span><span>: </span><span>lang</span><span>,</span></div></div><div><div><div>93</div></div><div><span><span>  </span></span><span>)))</span></div></div><div><div><div>94</div></div><div><span>  </span><span>body</span></div></div><div><div><div>95</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它由四部分组成：<code>uncover</code>/<code>only</code>（逐步显示的两个小工具）、<code>slide</code>（一张幻灯片）、<code>slide-footer</code>（页脚）、<code>slides</code>（整份文档的模板）。从最外层的 <code>slides</code> 说起。</p><section><h3>模板函数 slides：页面、字体和两种特殊页<a href="#模板函数-slides页面字体和两种特殊页"><span>#</span></a></h3><p><code>slides</code> 的签名和第 4 篇的 <code>note</code> 是一个路数：命名参数加最后一个位置参数 <code>body</code>，在 <code>#show: slides.with(...)</code> 里被当作模板调用。它做了四件事。</p><p><strong>第一，<code>set page</code>。</strong> <code>paper: "presentation-16-9"</code> 是 <code>"presentation-" + aspect</code> 拼出来的，<code>aspect</code> 默认 <code>"16-9"</code>，传 <code>"4-3"</code> 就得到 4&lt;3&gt; 的页面。边距写成 <code>(x: 2em, top: 2.5em, bottom: 2em)</code>，用 <code>em</code> 而不是 <code>cm</code>，大概是想让页边距跟着字号走。我在 72 ppi 的渲染图里看左边距：幻灯片标题的左缘大约在 40 px 处，也就是 40 pt，等于 20pt 字号的 2em，和预期一致。<code>footer: slide-footer()</code> 把页脚挂到每一页上。</p><p><strong>第二，字体。</strong> <code>set text(font: fonts.sans, size: 20pt, lang: lang)</code> 用第 5 篇 <code>fonts.typ</code> 里的无衬线栈；幻灯片投影时无衬线更清晰，这是选择它而不是衬线栈的原因。<code>raw</code> 单独用等宽字体并缩到 0.85em（我觉得 20pt 的页面上代码块按默认大小会偏大），公式用 <code>fonts.math</code>。</p><p><strong>第三，章节页。</strong> <code>show heading.where(level: 1)</code> 把一级标题整个重写成一张分隔页：先 <code>pagebreak(weak: true)</code>，再 <code>counter("xuan-slide").step()</code>，然后 <code>v(1fr)</code>、居中的标题、<code>v(1fr)</code>。两个 <code>1fr</code> 平分页面剩余的高度，标题就落在垂直正中。它输出的是 <code>it.body</code> 而不是 <code>it</code>，标题完全由这条规则自己排版。章节页也算一个“幻灯片”，要占一个编号（总览图里的 <code>1 / 13</code> 就是第一张章节页）。</p><p><strong>第四，标题页。</strong> 最后一段 <code>page(footer: none, align(horizon, title-block(...)))</code>：<code>page</code> 作为函数调用，自成一页，并在这一页上覆盖了 <code>footer</code>，所以标题页没有页脚（这也是为什么上面 17 页只有 16 行 <code>n / N</code>）。里面直接复用第 6 篇写好的 <code>title-block</code>，<code>align(horizon, ...)</code> 让它垂直居中。</p><p>这四件事做完，用户在 <code>body</code> 里写的 <code>=</code> 和 <code>#slide</code> 就是全部输入。</p></section><section><h3>slide：一页，或者 N 页<a href="#slide一页或者-n-页"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>slide</span><span>(</span><span>title</span><span>: </span><span>none</span><span>, </span><span>steps</span><span>: </span><span>1</span><span>, </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>let</span><span> </span><span>steps</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>type</span><span>(</span><span>body</span><span><span>) </span><span>==</span><span> </span></span><span>function</span><span> { </span><span>steps</span><span> } </span><span>else</span><span> { </span><span>1</span><span> }</span></div></div><div><div><div>3</div></div><div><span>  </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>steps</span><span><span> </span><span>+</span><span> </span></span><span>1</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pagebreak</span><span>(</span><span>weak</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> </span><span>k</span><span><span> </span><span>==</span><span> </span></span><span>1</span><span> { </span><span>counter</span><span>(</span><span>"xuan-slide"</span><span>).</span><span>step</span><span>() }</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span> </span><span>title</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>7</div></div><div><span>      </span><span>block</span><span>(</span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.4em</span><span>, </span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>title</span><span>), </span><span>below</span><span>: </span><span>0.9em</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span> </span><span>type</span><span>(</span><span>body</span><span><span>) </span><span>==</span><span> </span></span><span>function</span><span> { </span><span>body</span><span>(</span><span>k</span><span>) } </span><span>else</span><span> { </span><span>body</span><span> }</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这段是这份文件的核心，值得逐行读。</p><ul>
<li><code>body</code> 有两种形态：普通 content，或者一个 <code>k =&gt; content</code> 的函数。<code>type(body) == function</code> 判断是哪一种。</li>
<li>第一行 <code>let steps = ...</code> 把参数 <code>steps</code> 遮蔽（第 2 篇讲过的 shadowing）成一个新值：body 是 content 时一律是 1，<code>steps: 3</code> 配一个静态 body 不会生成三页一模一样的页面。</li>
<li>循环里每个 <code>k</code> 生成一页。<code>pagebreak(weak: true)</code> 放在每页的<strong>开头</strong>，而不是结尾：函数调用没有“结束时”的钩子，把分页写在开头，最后一页后面就不会拖一个空页。<code>weak</code> 的意思是：如果当前页本来就是空的（刚刚被 <code>page(...)</code> 或者前一次分页开出来），这次分页不生效。17 页的结果印证了这一点：1 + 4 + 12 = 17，没有任何空页。</li>
<li><code>if k == 1 { counter("xuan-slide").step() }</code>：<strong>只有第一步</strong>让编号加一。后面的步骤与它共享编号，页脚读到的就是同一个数，这就是上面 <code>3 / 13</code> 重复三次的来源。</li>
<li>标题是 1.4em 粗体的一个 <code>block</code>，<code>below: 0.9em</code> 拉开和正文的距离。</li>
<li>最后一行把 <code>k</code> 传给函数形态的 body。</li>
</ul><p>分步显示在这里不是“在同一页上改动”，而是每一步重画一整页，像手绘动画的逐帧：每一帧都是完整的画。一张幻灯片因此就是 <code>steps</code> 次完整排版，代价是 body 要写成函数，好处是没有任何隐藏状态，任何一页单独拿出来都是自洽的。</p><p>用法就是前面摘录里的“编译很快”：把 <code>body</code> 写成 <code>k =&gt; [...]</code>，<code>steps: 3</code> 说明要三页。这个函数是最后一个位置参数，所以写在括号里面，前面的 <code>title:</code> 和 <code>steps:</code> 是命名参数。</p></section><section><h3>uncover 与 only：占位与不占位<a href="#uncover-与-only占位与不占位"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>1</span><span>, </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>if</span><span> </span><span>k</span><span><span> </span><span>&gt;=</span><span> </span></span><span>from</span><span> { </span><span>body</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>hide</span><span>({</span></div></div><div><div><div>4</div></div><div><span>      </span><span>set</span><span> </span><span>list</span><span>(</span><span>marker</span><span>: [])</span></div></div><div><div><div>5</div></div><div><span>      </span><span>set</span><span> </span><span>enum</span><span>(</span><span>numbering</span><span>: </span><span>_</span><span> </span><span>=&gt;</span><span> [])</span></div></div><div><div><div>6</div></div><div><span>      </span><span>body</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#let</span><span> </span><span>only</span><span>(</span><span>k</span><span>, </span><span>at</span><span>: (</span><span>1</span><span>,), </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>12</div></div><div><span>  </span><span>let</span><span> </span><span>steps</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>type</span><span>(</span><span>at</span><span><span>) </span><span>==</span><span> </span></span><span>array</span><span> { </span><span>at</span><span> } </span><span>else</span><span> { (</span><span>at</span><span>,) }</span></div></div><div><div><div>13</div></div><div><span>  </span><span>if</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>steps</span><span> { </span><span>body</span><span> }</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个函数回答同一个问题的两个答案：“这一步还没到的时候，这块内容的位置要不要留着？”</p><ul>
<li><code>uncover</code> 用 <code>hide</code>：内容被隐藏，但<strong>仍然占着位置</strong>。后面的内容不会因为它出现而被挤下去，页面布局在每一步之间保持不动。</li>
<li><code>only</code> 用“不输出”：内容只在列出的步骤里存在，其他步骤里完全不存在，不占位。<code>at</code> 可以是单个整数或者整数数组，函数开头用 <code>type(at) == array</code> 把单个数包成一元数组，再用 <code>k in steps</code> 判断。默认 <code>(1,)</code> 表示只在第一步出现，用来写“第一步的占位提示，下一步换掉”之类的内容。</li>
</ul><p><code>uncover</code> 里多出来的那两行 <code>set</code> 有一段来历。最朴素的写法是 <code>if k &gt;= from { body } else { hide(body) }</code>，xuan 的 <code>uncover</code> 一开始就是这样写的，渲染出来第 4 页是这样（这张图来自 <code>uncover</code> 修复之前的仓库状态）：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-deck-step1-before.png" alt="修复前的“编译很快”第一步：第一条有文字，后两条没有文字却留着圆点" /><figcaption>修复前的“编译很快”第一步：第一条有文字，后两条没有文字却留着圆点</figcaption></figure><p></p><p>被隐藏的条目只有文字消失了，圆点还在。我在 <code>/tmp</code> 里把它缩成最小复现（<code>k = 1</code>，只有 <code>/tmp</code>，不在仓库中）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>8cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>1</span><span>, </span><span>body</span><span><span>) </span><span>= </span></span><span>if</span><span> </span><span>k</span><span><span> </span><span>&gt;=</span><span> </span></span><span>from</span><span> { </span><span>body</span><span> } </span><span>else</span><span> { </span><span>hide</span><span>(</span><span>body</span><span>) }</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>k</span><span><span> </span><span>= </span></span><span>1</span></div></div><div><div><div>4</div></div><div><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>2</span><span>)[- 乙]</span></div></div><div><div><div>5</div></div><div><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>3</span><span>)[- 丙]</span></div></div><div><div><div>6</div></div><div><span>#line</span><span>(</span><span>length</span><span>: </span><span>100%</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>1</span><span>)[- 甲]</span></div></div><div><div><div>8</div></div><div><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>2</span><span>)[- 乙]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdftotext -layout</code> 的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>•</span></div></div><div><div><div>2</div></div><div><span>•</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>• 甲</span></div></div><div><div><div>6</div></div><div><span>•</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上半部分两个条目都被 <code>hide</code> 了，输出里却有两个 <code>•</code>；下半部分只有“甲”可见，“乙”也留下了圆点。（我另外试过单独一个 <code>#hide[- 隐藏的列表项]</code> 后面跟一段正文，它连圆点一起隐藏了，所以这个现象和列表相邻的情形有关，具体原因我没有深挖。）把圆点放在 <code>uncover</code> 外面的写法 <code>- #uncover(k, from: 2)[乙]</code> 我也试了，圆点同样会显示。</p><p>结论是：<code>hide</code> 在这里只保证文字不可见，列表的标记（和枚举的数字）仍会被画出来。xuan 的做法是在隐藏的那一份里，把标记本身换成空内容：<code>set list(marker: [])</code> 和 <code>set enum(numbering: _ =&gt; [])</code>。两条 <code>set</code> 写在 <code>hide({ ... })</code> 的代码块里，作用范围只到这个块，可见的那一份不受影响；<code>hide</code> 之后条目的行高和位置没变，所以布局仍然不跳。我用 xuan 里现在的 <code>uncover</code> 重跑同样的三项（<code>k = 1</code>，同样只在 <code>/tmp</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: </span><span>uncover</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>8cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>k</span><span><span> </span><span>= </span></span><span>1</span></div></div><div><div><div>4</div></div><div><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>1</span><span>)[- 甲]</span></div></div><div><div><div>5</div></div><div><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>2</span><span>)[- 乙]</span></div></div><div><div><div>6</div></div><div><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>3</span><span>)[- 丙]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>• 甲</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现在只有“甲”那一行。真实的第 4 页（<code>pdftotext</code> 的输出里只有一行条目）：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-deck-step1.png" alt="修复后的“编译很快”第一步：只有第一条条目可见，后两条完全看不见" /><figcaption>修复后的“编译很快”第一步：只有第一条条目可见，后两条完全看不见</figcaption></figure><p></p><p>第 6 页（<code>k = 3</code>）三条全部可见，第一条在两张图里的位置没有动：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-deck-step3.png" alt="“编译很快”第三步：三条条目全部可见" /><figcaption>“编译很快”第三步：三条条目全部可见</figcaption></figure><p></p><p><code>only</code> 在不涉及占位的场景里没有这个问题，例如“内置图形”那一页：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#slide</span><span>(</span><span>title</span><span>: </span><span>"内置图形"</span><span>, </span><span>steps</span><span>: </span><span>2</span><span>, </span><span>k</span><span> </span><span>=&gt;</span><span> [</span></div></div><div><div><div>2</div></div><div><span>  </span><span>#align</span><span>(</span><span>center</span><span>, </span><span>box</span><span>(</span><span>width</span><span>: </span><span>12em</span><span>, </span><span>height</span><span>: </span><span>6em</span><span>, {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>place</span><span>(</span><span>rect</span><span>(</span><span>width</span><span>: </span><span>5em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#cfe0f5"</span><span>), </span><span>radius</span><span>: </span><span>4pt</span><span>))</span></div></div><div><div><div>4</div></div><div><span>    </span><span>place</span><span>(</span><span>dx</span><span>: </span><span>6.5em</span><span>, </span><span>circle</span><span>(</span><span>radius</span><span>: </span><span>2em</span><span>, </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#f5d9cf"</span><span>)))</span></div></div><div><div><div>5</div></div><div><span>    </span><span>only</span><span>(</span><span>k</span><span>, </span><span>at</span><span>: </span><span>2</span><span>, </span><span>place</span><span>(</span><span>dx</span><span>: </span><span>4.8em</span><span>, </span><span>dy</span><span>: </span><span>2em</span><span>, </span><span>line</span><span>(</span></div></div><div><div><div>6</div></div><div><span>      </span><span>length</span><span>: </span><span>2em</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>stroke</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>),</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>)))</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}))</span></div></div><div><div><div>10</div></div><div><span>  </span><span>#uncover</span><span>(</span><span>k</span><span>, </span><span>from</span><span>: </span><span>2</span><span>)[矩形与圆之间多了一条线。]</span></div></div><div><div><div>11</div></div><div><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>box(width: 12em, height: 6em, { ... })</code> 先给出一块固定大小的画布，里面的矩形和圆都用 <code>place</code> 摆放，<code>place</code> 不占流内空间，所以 <code>only(k, at: 2, place(...))</code> 在第 2 步才出现的那条线，既不需要预留位置，也不会推动任何东西。第 12、13 页如下：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-deck-draw1.png" alt="“内置图形”第一步：一个蓝色矩形和一个浅红色圆，中间没有线" /><figcaption>“内置图形”第一步：一个蓝色矩形和一个浅红色圆，中间没有线</figcaption></figure><p></p><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-deck-draw2.png" alt="“内置图形”第二步：矩形与圆之间多了一条蓝色短线，下方出现说明文字" /><figcaption>“内置图形”第二步：矩形与圆之间多了一条蓝色短线，下方出现说明文字</figcaption></figure><p></p><p>第二步下方多出来的“矩形与圆之间多了一条线。”是 <code>uncover(k, from: 2)</code> 控制的：第一步里它是隐藏的，位置留着，所以图在两步之间没有跳动。</p></section><section><h3>页脚：进度条是怎么算出来的<a href="#页脚进度条是怎么算出来的"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>slide-footer</span><span><span>() </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"xuan-slide"</span><span>).</span><span>get</span><span>().</span><span>first</span><span>()</span></div></div><div><div><div>3</div></div><div><span>  </span><span>let</span><span> </span><span>total</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"xuan-slide"</span><span>).</span><span>final</span><span>().</span><span>first</span><span>()</span></div></div><div><div><div>4</div></div><div><span>  </span><span>let</span><span> </span><span>accent</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>stack</span><span>(</span></div></div><div><div><div>6</div></div><div><span>    </span><span>spacing</span><span>: </span><span>4pt</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>block</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>height</span><span>: </span><span>2pt</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>225</span><span>), </span><span>rect</span><span>(</span></div></div><div><div><div>8</div></div><div><span>      </span><span>width</span><span>: </span><span>100%</span><span><span> </span><span>*</span><span> </span></span><span>n</span><span><span> </span><span>/</span><span> </span></span><span>calc</span><span>.</span><span>max</span><span>(</span><span>total</span><span>, </span><span>1</span><span>),</span></div></div><div><div><div>9</div></div><div><span>      </span><span>height</span><span>: </span><span>2pt</span><span>,</span></div></div><div><div><div>10</div></div><div><span>      </span><span>fill</span><span>: </span><span>accent</span><span>,</span></div></div><div><div><div>11</div></div><div><span>      </span><span>stroke</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>)),</span></div></div><div><div><div>13</div></div><div><span>    </span><span>align</span><span>(</span><span>right</span><span>, </span><span>text</span><span>(</span><span>size</span><span>: </span><span>0.6em</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>110</span><span>))[</span><span>#n</span><span> / </span><span>#total</span><span>]),</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它用到了上一篇的全部家当：</p><ul>
<li>整个函数体包在 <code>context</code> 里，因为 <code>counter(...).get()</code> 和 <code>.final()</code> 都需要知道“当前在哪”。页脚在每一页上各被求值一次，每次的 <code>get()</code> 都是该页的编号。</li>
<li><code>counter("xuan-slide")</code> 是一个按字符串命名的自定义计数器，不绑定任何元素。<code>slide</code> 和章节页在各自开头 <code>step()</code> 它，页脚只读。</li>
<li><code>.get().first()</code> 是当前页的编号 <code>n</code>，<code>.final().first()</code> 是整份文档最后的值 <code>total</code>（这里是 13，不是页数 17：编号按“幻灯片”算，不按“页”算）。读 <code>.final()</code> 的代价是文档至少要排两遍，这也是 135.5 ms 里包含的东西。</li>
<li>进度条是两层：灰色轨道 <code>block(width: 100%, height: 2pt, fill: luma(225), ...)</code>，里面一条 <code>n / total</code> 比例的蓝色 <code>rect</code>。<code>rect</code> 的宽度写成 <code>100% * n / calc.max(total, 1)</code>，这是一个相对长度，相对于外层轨道 <code>block</code> 的宽度，所以不需要知道页面有多宽。<code>calc.max(total, 1)</code> 防止没有任何幻灯片编号时除以零。右下角是 <code>n / total</code> 的数字，0.6em 灰色。</li>
<li>我在 <code>/tmp</code> 里核对了这个比例（只在 <code>/tmp</code>，不在仓库里）：200pt 宽的页面，<code>n = 3</code>、<code>total = 13</code>，渲染到 72 ppi 后蓝色条的宽度是 46 px，与 200 × 3 / 13 = 46.15 相符。</li>
</ul><p>进度条的几何关系，我用一张手绘的假幻灯片（<code>lab/ch13/fig-slide.typ</code>）单独画了出来（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/fig-slide.typ" target="_blank">源文件</a>）：<code>rect</code> 当页面边框，<code>place(bottom + left, ...)</code> 把一条 33% 宽的蓝条放在左下角：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// One slide of examples/slides, rendered as a PNG figure.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>sans</span><span>, </span><span>size</span><span>: </span><span>14pt</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#rect</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>width</span><span>: </span><span>20em</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>height</span><span>: </span><span>11.25em</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>stroke</span><span>: </span><span>0.6pt</span><span><span> </span><span>+</span><span> </span></span><span>luma</span><span>(</span><span>160</span><span>),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>inset</span><span>: </span><span>1.2em</span><span>,</span></div></div><div><div><div>12</div></div><div><span>)[</span></div></div><div><div><div>13</div></div><div><span>  </span><span>#text</span><span>(</span><span>size</span><span>: </span><span>1.4em</span><span>, </span><span>weight</span><span>: </span><span>"bold"</span><span>)[三种模式]</span></div></div><div><div><div>14</div></div><div><span>  </span><span>#v</span><span>(</span><span>0.6em</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>- 标记模式：默认，写正文</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>- 代码模式：</span><span>`#`</span><span> 进入，写逻辑</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>- 数学模式：</span><span>`$ ... $`</span><span>，写公式</span></div></div><div><div><div>18</div></div><div><span>  </span><span>#place</span><span>(</span><span>bottom</span><span><span> </span><span>+</span><span> </span></span><span>left</span><span>, </span><span>dy</span><span>: </span><span>0.6em</span><span>, </span><span>rect</span><span>(</span></div></div><div><div><div>19</div></div><div><span>    </span><span>width</span><span>: </span><span>33%</span><span>,</span></div></div><div><div><div>20</div></div><div><span>    </span><span>height</span><span>: </span><span>2pt</span><span>,</span></div></div><div><div><div>21</div></div><div><span>    </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>),</span></div></div><div><div><div>22</div></div><div><span>    </span><span>stroke</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>))</span></div></div><div><div><div>24</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-slide.png" alt="用 rect 手绘的一页假幻灯片：粗体标题“三种模式”，三条要点，左下角一条蓝色进度条，宽度约为页面的三分之一" /><figcaption>用 rect 手绘的一页假幻灯片：粗体标题“三种模式”，三条要点，左下角一条蓝色进度条，宽度约为页面的三分之一</figcaption></figure><p></p></section><section><h3>日期：datetime.today() 与 SOURCE_DATE_EPOCH<a href="#日期datetimetoday-与-source_date_epoch"><span>#</span></a></h3><p><code>slides</code> 把 <code>date</code> 原样交给第 6 篇的 <code>title-block</code>。<code>title-block</code> 对 <code>date</code> 有三种处理：<code>auto</code> 取 <code>datetime.today()</code> 并按中文格式显示，<code>datetime</code> 值显示成 <code>2026-10-19</code> 的样式，字符串原样输出。<code>examples/slides</code> 传的是第三种。</p><p>原因在 dev shell。<code>nix develop</code> 的 stdenv 习惯把 <code>SOURCE_DATE_EPOCH</code> 设成 1980-01-01，这会让 <code>datetime.today()</code> 永远是 1980 年；<code>typst-lab</code> 的 <code>flake.nix</code> 在 <code>shellHook</code> 里把它 <code>unset</code> 了（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/flake.nix" target="_blank">源文件</a>里就是一行 <code>unset SOURCE_DATE_EPOCH</code>，为的是让 <code>datetime.today()</code> 回到真实的日期）。我在 <code>/tmp</code> 里写了两个文件验证（只在 <code>/tmp</code>，不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>slides</span><span>.</span><span>with</span><span>(</span><span>title</span><span>: </span><span>"日期"</span><span>, </span><span>date</span><span>: </span><span>auto</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>slides</span><span>.</span><span>with</span><span>(</span><span>title</span><span>: </span><span>"日期"</span><span>, </span><span>date</span><span>: </span><span>datetime</span><span>(</span></div></div><div><div><div>3</div></div><div><span>  </span><span>year</span><span>: </span><span>2026</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>month</span><span>: </span><span>10</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>day</span><span>: </span><span>19</span><span>,</span></div></div><div><div><div>6</div></div><div><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>/tmp</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>today.typ</span><span> </span><span>a.pdf</span></div></div><div><div><div>2</div></div><div><span>pdftotext</span><span> </span><span>a.pdf</span><span> </span><span>-</span><span> | </span><span>grep</span><span> </span><span>年</span></div></div><div><div><div>3</div></div><div><span>SOURCE_DATE_EPOCH</span><span>=</span><span>315532800</span><span> </span><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>/tmp</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>today.typ</span><span> </span><span>b.pdf</span></div></div><div><div><div>4</div></div><div><span>pdftotext</span><span> </span><span>b.pdf</span><span> </span><span>-</span><span> | </span><span>grep</span><span> </span><span>年</span></div></div><div><div><div>5</div></div><div><span>SOURCE_DATE_EPOCH</span><span>=</span><span>315532800</span><span> </span><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>/tmp</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>today-fixed.typ</span><span> </span><span>c.pdf</span></div></div><div><div><div>6</div></div><div><span>pdftotext</span><span> </span><span>c.pdf</span><span> </span><span>-</span><span> | </span><span>grep</span><span> </span><span>-E</span><span> </span><span>'[0-9]{4}'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>2026 年 10 月 19 日</span></div></div><div><div><div>2</div></div><div><span>1980 年 1 月 1 日</span></div></div><div><div><div>3</div></div><div><span>2026-10-19</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行是 dev shell 里 <code>date: auto</code> 的结果（<code>SOURCE_DATE_EPOCH</code> 未设置）。第二行我手动设 <code>SOURCE_DATE_EPOCH=315532800</code>（1980-01-01 00&lt;00&gt; UTC，也就是上面说的 nix 默认值）后，<code>date: auto</code> 变成了 1980 年；第三行里同样设置，但源码里传的是显式的 <code>datetime(...)</code>，仍然是 2026-10-19，不受环境变量影响。</p><p>所以结论是：<code>date: auto</code> 的幻灯片，其日期取决于编译时的环境，不是源文件的函数。想要可复现的输出（CI 里、别人机器上），就像 <code>examples/slides</code> 那样写死日期。</p></section></section>
<section><h2>绘图原语<a href="#绘图原语"><span>#</span></a></h2><p>幻灯片里的图形来自 Typst 内置的绘图函数，不需要任何包。<code>lab/ch13/</code> 的 6 个文件逐个过一遍：形状、曲线、颜色、渐变、tiling、描边。为了在文中放图，我用一个 <code>/tmp</code> 里的包装文件把每个 lab 文件 <code>include</code> 进一个 <code>width: auto, height: auto</code> 的页面，再渲染成 PNG（只在 <code>/tmp</code>，不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>"/home/bfmhno3/Projects/typst-lab/lab/ch13/"</span><span><span> </span><span>+</span><span> </span></span><span>sys</span><span>.</span><span>inputs</span><span>.</span><span>name</span><span><span> </span><span>+</span><span> </span></span><span>".typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>/</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>--input</span><span> </span><span>name=shapes</span><span> </span><span>--format</span><span> </span><span>png</span><span> </span><span>--ppi</span><span> </span><span>144</span><span> </span><span>crop.typ</span><span> </span><span>shapes.png</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make ch13</code> 本身的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make ch13</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch13</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch13/color.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch13/curve.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch13/fig-drawing.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch13/fig-slide.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch13/gradient.typ</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch13/shapes.typ</span></div></div><div><div><div>9</div></div><div><span>ok   lab/ch13/stroke.typ</span></div></div><div><div><div>10</div></div><div><span>ok   lab/ch13/tiling.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>形状：rect、square、circle、ellipse、line、polygon<a href="#形状rectsquarecircleellipselinepolygon"><span>#</span></a></h3><p><code>lab/ch13/shapes.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/shapes.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Built-in shapes: rect, square, circle, ellipse, line, polygon.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>sans</span><span>, </span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>blue</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>tint</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#cfe0f5"</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>columns</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>gutter</span><span>: </span><span>1.2em</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>4em</span><span>, </span><span>height</span><span>: </span><span>2.5em</span><span>, </span><span>fill</span><span>: </span><span>tint</span><span>, </span><span>stroke</span><span>: </span><span>blue</span><span>),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>// `radius` rounds corners; a dictionary sets each corner.</span></div></div><div><div><div>12</div></div><div><span>  </span><span>rect</span><span>(</span></div></div><div><div><div>13</div></div><div><span>    </span><span>width</span><span>: </span><span>4em</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>height</span><span>: </span><span>2.5em</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fill</span><span>: </span><span>tint</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>radius</span><span>: (</span><span>top-left</span><span>: </span><span>8pt</span><span>, </span><span>bottom-right</span><span>: </span><span>8pt</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>18</div></div><div><span>  </span><span>// `square` is a rect whose `size` fixes both sides.</span></div></div><div><div><div>19</div></div><div><span>  </span><span>square</span><span>(</span><span>size</span><span>: </span><span>2.5em</span><span>, </span><span>fill</span><span>: </span><span>tint</span><span>, </span><span>stroke</span><span>: </span><span>blue</span><span>),</span></div></div><div><div><div>20</div></div><div><span>  </span><span>circle</span><span>(</span><span>radius</span><span>: </span><span>1.2em</span><span>, </span><span>fill</span><span>: </span><span>tint</span><span>, </span><span>stroke</span><span>: </span><span>blue</span><span>),</span></div></div><div><div><div>21</div></div><div><span>  </span><span>ellipse</span><span>(</span><span>width</span><span>: </span><span>4em</span><span>, </span><span>height</span><span>: </span><span>2.5em</span><span>, </span><span>fill</span><span>: </span><span>tint</span><span>, </span><span>stroke</span><span>: </span><span>blue</span><span>),</span></div></div><div><div><div>22</div></div><div><span>  </span><span>// A shape can hold content; `inset` pads it.</span></div></div><div><div><div>23</div></div><div><span>  </span><span>rect</span><span>(</span><span>fill</span><span>: </span><span>tint</span><span>, </span><span>inset</span><span>: </span><span>8pt</span><span>)[文字],</span></div></div><div><div><div>24</div></div><div><span>  </span><span>line</span><span>(</span><span>length</span><span>: </span><span>4em</span><span>, </span><span>stroke</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>),</span></div></div><div><div><div>25</div></div><div><span>  </span><span>line</span><span>(</span><span>start</span><span>: (</span><span>0pt</span><span>, </span><span>0pt</span><span>), </span><span>end</span><span>: (</span><span>4em</span><span>, </span><span>2em</span><span>), </span><span>stroke</span><span>: </span><span>1pt</span><span>),</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>  </span><span>polygon</span><span>(</span></div></div><div><div><div>28</div></div><div><span>    </span><span>fill</span><span>: </span><span>tint</span><span>,</span></div></div><div><div><div>29</div></div><div><span>    </span><span>stroke</span><span>: </span><span>blue</span><span>,</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>(</span><span>0pt</span><span>, </span><span>2.5em</span><span>),</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>(</span><span>1.5em</span><span>, </span><span>0pt</span><span>),</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>(</span><span>3em</span><span>, </span><span>2.5em</span><span>),</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>34</div></div><div><span>)</span></div></div><div><div><div>35</div></div><div><span>// Shapes are ordinary content: they have a type and can be inspected.</span></div></div><div><div><div>36</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>repr</span><span>(</span><span>rect</span><span>().</span><span>func</span><span>()), </span><span>"rect"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-shapes.png" alt="六种形状：矩形、两个对角圆角的矩形、正方形、圆、椭圆、带文字的矩形，下一行是粗直线、斜线和三角形" /><figcaption>六种形状：矩形、两个对角圆角的矩形、正方形、圆、椭圆、带文字的矩形，下一行是粗直线、斜线和三角形</figcaption></figure><p></p><p>几点值得记：</p><ul>
<li>所有形状都有 <code>fill</code> 和 <code>stroke</code>。<code>stroke: blue</code> 只指定颜色，粗细用默认值。</li>
<li><code>rect</code> 的 <code>radius</code> 可以是一个长度，也可以是一个字典（<code>top-left</code>、<code>bottom-right</code> 等四个角分别设置）。图里第二个矩形的左上和右下是圆角。</li>
<li><code>square(size: ...)</code> 就是 <code>width</code> 和 <code>height</code> 相等的 <code>rect</code>；<code>circle(radius: ...)</code> 用半径，<code>ellipse</code> 给 <code>width</code> 和 <code>height</code>。</li>
<li>形状可以带内容：<code>rect(fill: tint, inset: 8pt)[文字]</code>，<code>inset</code> 是内容到边框的距离。不给尺寸时它收缩到内容的大小，这就是图里“文字”那个浅蓝色块。</li>
<li><code>line</code> 有两种写法：<code>line(length: 4em)</code> 画一条水平线，<code>line(start: ..., end: ...)</code> 给出两个端点。三角形用 <code>polygon</code> 给顶点列表。</li>
<li>形状是普通 content：文件最后一行 <code>#assert.eq(repr(rect().func()), "rect")</code> 说明 <code>rect()</code> 的 <code>func</code> 就是 <code>rect</code>，可以用第 3 篇的内省办法检查。</li>
</ul></section><section><h3>curve：由分段组成的路径<a href="#curve由分段组成的路径"><span>#</span></a></h3><p><code>curve</code> 与 <code>polygon</code> 的区别在于它的参数是一串<strong>路径分段</strong>，包括直线、二次与三次贝塞尔曲线，可以画出光滑的形状。<code>lab/ch13/curve.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/curve.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `curve` draws paths from components: move, line, quad, cubic, close.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>blue</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// A closed triangle.</span></div></div><div><div><div>5</div></div><div><span>#curve</span><span>(</span></div></div><div><div><div>6</div></div><div><span>  </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#cfe0f5"</span><span>),</span></div></div><div><div><div>7</div></div><div><span>  </span><span>stroke</span><span>: </span><span>blue</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>curve</span><span>.</span><span>move</span><span>((</span><span>0pt</span><span>, </span><span>40pt</span><span>)),</span></div></div><div><div><div>9</div></div><div><span>  </span><span>curve</span><span>.</span><span>line</span><span>((</span><span>30pt</span><span>, </span><span>0pt</span><span>)),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>curve</span><span>.</span><span>line</span><span>((</span><span>60pt</span><span>, </span><span>40pt</span><span>)),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>curve</span><span>.</span><span>close</span><span>(),</span></div></div><div><div><div>12</div></div><div><span>)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#v</span><span>(</span><span>1em</span><span>)</span></div></div><div><div><div>15</div></div><div><span>// A cubic Bezier: start, two control points, end.</span></div></div><div><div><div>16</div></div><div><span>#curve</span><span>(</span></div></div><div><div><div>17</div></div><div><span>  </span><span>stroke</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>,</span></div></div><div><div><div>18</div></div><div><span>  </span><span>curve</span><span>.</span><span>move</span><span>((</span><span>0pt</span><span>, </span><span>40pt</span><span>)),</span></div></div><div><div><div>19</div></div><div><span>  </span><span>curve</span><span>.</span><span>cubic</span><span>((</span><span>20pt</span><span><span>, </span><span>-</span></span><span>10pt</span><span>), (</span><span>60pt</span><span>, </span><span>90pt</span><span>), (</span><span>80pt</span><span>, </span><span>40pt</span><span>)),</span></div></div><div><div><div>20</div></div><div><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>#v</span><span>(</span><span>1em</span><span>)</span></div></div><div><div><div>23</div></div><div><span>// A quadratic Bezier; `relative: false` makes the control point absolute.</span></div></div><div><div><div>24</div></div><div><span>#curve</span><span>(</span></div></div><div><div><div>25</div></div><div><span>  </span><span>stroke</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>,</span></div></div><div><div><div>26</div></div><div><span>  </span><span>curve</span><span>.</span><span>move</span><span>((</span><span>0pt</span><span>, </span><span>30pt</span><span>)),</span></div></div><div><div><div>27</div></div><div><span>  </span><span>curve</span><span>.</span><span>quad</span><span>((</span><span>30pt</span><span><span>, </span><span>-</span></span><span>20pt</span><span>), (</span><span>60pt</span><span>, </span><span>30pt</span><span>), </span><span>relative</span><span>: </span><span>false</span><span>),</span></div></div><div><div><div>28</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-curve.png" alt="curve 的三个例子：闭合的浅蓝三角形，一条三次贝塞尔曲线（S 形），一条二次贝塞尔曲线（拱形）" /><figcaption>curve 的三个例子：闭合的浅蓝三角形，一条三次贝塞尔曲线（S 形），一条二次贝塞尔曲线（拱形）</figcaption></figure><p></p><p>分段都是 <code>curve</code> 下的子函数：<code>curve.move</code> 把笔移到起点，<code>curve.line</code> 画直线，<code>curve.quad</code> 给一个控制点和终点，<code>curve.cubic</code> 给两个控制点和终点，<code>curve.close</code> 把路径封闭回起点，图里的三角形用它。</p><p>最后一个例子传的是 <code>relative: false</code>，也就是控制点和终点都是绝对坐标（这是默认值）。我在 <code>/tmp</code> 里把 <code>relative</code> 分别设成 <code>false</code> 和 <code>true</code> 画在一起（左 <code>false</code>，右 <code>true</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>blue</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>4</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>gutter</span><span>: </span><span>2em</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>curve</span><span>(</span></div></div><div><div><div>7</div></div><div><span>    </span><span>stroke</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>curve</span><span>.</span><span>move</span><span>((</span><span>0pt</span><span>, </span><span>30pt</span><span>)),</span></div></div><div><div><div>9</div></div><div><span>    </span><span>curve</span><span>.</span><span>quad</span><span>((</span><span>30pt</span><span><span>, </span><span>-</span></span><span>20pt</span><span>), (</span><span>60pt</span><span>, </span><span>30pt</span><span>), </span><span>relative</span><span>: </span><span>false</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>curve</span><span>(</span></div></div><div><div><div>12</div></div><div><span>    </span><span>stroke</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>curve</span><span>.</span><span>move</span><span>((</span><span>0pt</span><span>, </span><span>30pt</span><span>)),</span></div></div><div><div><div>14</div></div><div><span>    </span><span>curve</span><span>.</span><span>quad</span><span>((</span><span>30pt</span><span><span>, </span><span>-</span></span><span>20pt</span><span>), (</span><span>60pt</span><span>, </span><span>30pt</span><span>), </span><span>relative</span><span>: </span><span>true</span><span>),</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>16</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-curve-relative.png" alt="两条二次曲线：左边是拱形（绝对坐标），右边的整体向右下偏移（相对坐标，控制点和终点都以上一个点为原点）" /><figcaption>两条二次曲线：左边是拱形（绝对坐标），右边的整体向右下偏移（相对坐标，控制点和终点都以上一个点为原点）</figcaption></figure><p></p><p><code>relative: true</code> 时点是相对于上一个点的偏移，所以同样的数字画出来完全是另一条线。所以 lab 文件里的注释写成“<code>relative: false</code> makes the control point absolute”，<code>false</code> 是默认值，写出来是为了让读的人看见这一点。</p></section><section><h3>颜色：色彩空间与方法<a href="#颜色色彩空间与方法"><span>#</span></a></h3><p><code>lab/ch13/color.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/color.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Color spaces, color methods and built-in maps.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>swatch</span><span>(</span><span>c</span><span><span>) </span><span>= </span></span><span>box</span><span>(</span><span>width</span><span>: </span><span>2.2em</span><span>, </span><span>height</span><span>: </span><span>1.2em</span><span>, </span><span>fill</span><span>: </span><span>c</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>row</span><span>(</span><span>name</span><span>, ..</span><span>cs</span><span><span>) </span><span>= </span><span>[</span></span><span>#name</span><span>: </span><span>#cs</span><span>.</span><span>pos</span><span>().</span><span>map</span><span>(</span><span>swatch</span><span>).</span><span>join</span><span>(</span><span>h</span><span>(</span><span>2pt</span><span>)) </span><span>\ </span><span>]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#row</span><span>(</span><span>"rgb"</span><span>, </span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>), </span><span>rgb</span><span>(</span><span>31</span><span>, </span><span>78</span><span>, </span><span>156</span><span>), </span><span>rgb</span><span>(</span><span>12%</span><span>, </span><span>30%</span><span>, </span><span>61%</span><span>))</span></div></div><div><div><div>6</div></div><div><span>#row</span><span>(</span><span>"luma"</span><span>, </span><span>luma</span><span>(</span><span>0</span><span>), </span><span>luma</span><span>(</span><span>128</span><span>), </span><span>luma</span><span>(</span><span>230</span><span>))</span></div></div><div><div><div>7</div></div><div><span>#row</span><span>(</span><span>"oklab"</span><span>, </span><span>oklab</span><span>(</span><span>70%</span><span>, </span><span>0.1</span><span>, </span><span>0.1</span><span>), </span><span>oklab</span><span>(</span><span>70%</span><span><span>, </span><span>-</span></span><span>0.1</span><span>, </span><span>0.1</span><span>))</span></div></div><div><div><div>8</div></div><div><span>#row</span><span>(</span><span>"oklch"</span><span>, </span><span>oklch</span><span>(</span><span>70%</span><span>, </span><span>0.15</span><span>, </span><span>30deg</span><span>), </span><span>oklch</span><span>(</span><span>70%</span><span>, </span><span>0.15</span><span>, </span><span>200deg</span><span>))</span></div></div><div><div><div>9</div></div><div><span>#row</span><span>(</span><span>"cmyk"</span><span>, </span><span>cmyk</span><span>(</span><span>100%</span><span>, </span><span>50%</span><span>, </span><span>0%</span><span>, </span><span>20%</span><span>))</span></div></div><div><div><div>10</div></div><div><span>#row</span><span>(</span><span>"hsl"</span><span>, </span><span>color</span><span>.</span><span>hsl</span><span>(</span><span>210deg</span><span>, </span><span>70%</span><span>, </span><span>40%</span><span>))</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// Methods return new colors; the original is unchanged.</span></div></div><div><div><div>13</div></div><div><span>#let</span><span> </span><span>base</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>#row</span><span>(</span></div></div><div><div><div>15</div></div><div><span>  </span><span>"methods"</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>base</span><span>.</span><span>lighten</span><span>(</span><span>40%</span><span>),</span></div></div><div><div><div>17</div></div><div><span>  </span><span>base</span><span>.</span><span>darken</span><span>(</span><span>30%</span><span>),</span></div></div><div><div><div>18</div></div><div><span>  </span><span>base</span><span>.</span><span>rotate</span><span>(</span><span>120deg</span><span>),</span></div></div><div><div><div>19</div></div><div><span>  </span><span>base</span><span>.</span><span>transparentize</span><span>(</span><span>60%</span><span>),</span></div></div><div><div><div>20</div></div><div><span>  </span><span>base</span><span>.</span><span>negate</span><span>(),</span></div></div><div><div><div>21</div></div><div><span>)</span></div></div><div><div><div>22</div></div><div><span>// `mix` blends in a chosen space; `components` reads the numbers back.</span></div></div><div><div><div>23</div></div><div><span>#row</span><span>(</span><span>"mix"</span><span>, </span><span>color</span><span>.</span><span>mix</span><span>(</span><span>red</span><span>, </span><span>blue</span><span>), </span><span>color</span><span>.</span><span>mix</span><span>(</span><span>red</span><span>, </span><span>blue</span><span>, </span><span>space</span><span>: </span><span>oklab</span><span>))</span></div></div><div><div><div>24</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>luma</span><span>(</span><span>50%</span><span>).</span><span>components</span><span>(</span><span>alpha</span><span>: </span><span>false</span><span>).</span><span>len</span><span>(), </span><span>1</span><span>)</span></div></div><div><div><div>25</div></div><div><span>// `color.map` holds ready-made color arrays (here 9 stops), also usable in gradients.</span></div></div><div><div><div>26</div></div><div><span>#row</span><span>(</span><span>"map"</span><span>, ..</span><span>color</span><span>.</span><span>map</span><span>.</span><span>viridis</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-colors.png" alt="九行色块：rgb 三块几乎相同的蓝色、luma 的黑灰浅灰、oklab 的橙与绿、oklch 的粉红与青、cmyk 的深蓝、hsl 的蓝、methods 的五种变换（浅蓝、深蓝、暗红、淡蓝、褐黄）、mix 的两块相同的紫色、map 的 viridis 九色" /><figcaption>九行色块：rgb 三块几乎相同的蓝色、luma 的黑灰浅灰、oklab 的橙与绿、oklch 的粉红与青、cmyk 的深蓝、hsl 的蓝、methods 的五种变换（浅蓝、深蓝、暗红、淡蓝、褐黄）、mix 的两块相同的紫色、map 的 viridis 九色</figcaption></figure><p></p><p>Typst 的颜色是带“色彩空间”的值：<code>rgb</code>、<code>luma</code>（灰度）、<code>oklab</code>、<code>oklch</code>、<code>cmyk</code> 都是顶层构造函数，lab 文件里的 hsl 写成了 <code>color.hsl</code>。同一个蓝色可以用不同方式表达，我用 <code>typst eval</code> 核对了第一行的三块：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'rgb("#1f4e9c").to-hex()'</span></div></div><div><div><div>2</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'repr(rgb(31, 78, 156) == rgb("#1f4e9c"))'</span></div></div><div><div><div>3</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'repr(rgb(12%, 30%, 61%).to-hex())'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"#1f4e9c"</span></div></div><div><div><div>2</div></div><div><span>"true"</span></div></div><div><div><div>3</div></div><div><span>"\"#1f4c9c\""</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>十六进制和 <code>rgb(31, 78, 156)</code> 是同一个颜色；百分比那一块是我凑的数，其实是 <code>#1f4c9c</code>，红绿蓝各差一两个单位，肉眼分不出来，所以图里三块“几乎相同”。</p><p>颜色的方法<strong>都返回新颜色</strong>，原来的不变。同一个基色 <code>#1f4e9c</code>，我用 <code>typst eval</code> 读出每个方法的结果：</p>

<table><thead><tr><th>表达式</th><th>结果</th></tr></thead><tbody><tr><td><code>base.lighten(40%)</code></td><td><code>#7995c4</code></td></tr><tr><td><code>base.darken(30%)</code></td><td><code>#16376d</code></td></tr><tr><td><code>base.rotate(120deg)</code></td><td><code>#8e2731</code></td></tr><tr><td><code>base.transparentize(60%)</code></td><td><code>#1f4e9c66</code>（最后两位 <code>66</code> 是 alpha）</td></tr><tr><td><code>base.negate()</code></td><td><code>#9e6900</code></td></tr><tr><td><code>base.components()</code></td><td><code>["12.16%", "30.59%", "61.18%", "100%"]</code></td></tr></tbody></table><p>这正好对上图里 methods 那一行的五块色。<code>rotate(120deg)</code> 是在色相环上转 120 度，所以蓝变成了暗红；<code>transparentize(60%)</code> 把透明度加到 60%，alpha 变成 40%，十六进制里就是 <code>66</code>。</p><p><code>color.mix</code> 的混合空间是个坑：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'color.mix(red, blue).to-hex()'</span></div></div><div><div><div>2</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'color.mix(red, blue, space: oklab).to-hex()'</span></div></div><div><div><div>3</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'color.mix(red, blue, space: rgb).to-hex()'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"#a37095"</span></div></div><div><div><div>2</div></div><div><span>"#a37095"</span></div></div><div><div><div>3</div></div><div><span>"#805b87"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>默认空间是 <code>oklab</code>（前两行相同），所以 lab 里 mix 那一行的两块是同一个紫色（图里确实一样）；在 <code>rgb</code> 里混合是 <code>#805b87</code>，更暗更灰，因为 sRGB 的数值空间对人眼来说不均匀。</p><p>最后一行 <code>color.map.viridis</code> 是一个内置的颜色数组，<code>.len()</code> 是 9；<code>color.map.turbo</code> 的长度是 256（我同样用 <code>typst eval</code> 核对了）。lab 里用 <code>..cs.pos()</code> 之类把它展开，在渐变里则直接 <code>..color.map.turbo</code> 展开成色标。</p></section><section><h3>渐变：linear、radial、conic<a href="#渐变linearradialconic"><span>#</span></a></h3><p><code>lab/ch13/gradient.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/gradient.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Three gradient kinds, all usable wherever a fill or stroke is.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>stops</span><span><span> </span><span>= </span><span>(</span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>), </span><span>rgb</span><span>(</span><span>"#e7a33e"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>box</span><span>(</span><span>g</span><span><span>) </span><span>= </span></span><span>rect</span><span>(</span><span>width</span><span>: </span><span>8em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>g</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>6</div></div><div><span>  </span><span>columns</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>gutter</span><span>: </span><span>1em</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>linear</span><span>(..</span><span>stops</span><span>)), </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>linear</span><span>(..</span><span>stops</span><span>, </span><span>angle</span><span>: </span><span>45deg</span><span>)),</span></div></div><div><div><div>9</div></div><div><span>  </span><span>// Explicit stops: (color, position).</span></div></div><div><div><div>10</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>linear</span><span>((</span><span>red</span><span>, </span><span>0%</span><span>), (</span><span>white</span><span>, </span><span>30%</span><span>), (</span><span>blue</span><span>, </span><span>100%</span><span>))),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>radial</span><span>(..</span><span>stops</span><span>)),</span></div></div><div><div><div>12</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>radial</span><span>(..</span><span>stops</span><span>, </span><span>center</span><span>: (</span><span>20%</span><span>, </span><span>30%</span><span>), </span><span>radius</span><span>: </span><span>80%</span><span>)),</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>conic</span><span>(..</span><span>stops</span><span>)),</span></div></div><div><div><div>15</div></div><div><span>  </span><span>// Interpolation space changes the midpoint: try oklab vs srgb.</span></div></div><div><div><div>16</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>linear</span><span>(</span><span>red</span><span>, </span><span>blue</span><span>, </span><span>space</span><span>: </span><span>oklab</span><span>)),</span></div></div><div><div><div>17</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>linear</span><span>(</span><span>red</span><span>, </span><span>blue</span><span>, </span><span>space</span><span>: </span><span>rgb</span><span>)),</span></div></div><div><div><div>18</div></div><div><span>  </span><span>// A built-in map plus `.sharp` for banding.</span></div></div><div><div><div>19</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>linear</span><span>(..</span><span>color</span><span>.</span><span>map</span><span>.</span><span>turbo</span><span>)),</span></div></div><div><div><div>20</div></div><div><span>  </span><span>box</span><span>(</span><span>gradient</span><span>.</span><span>linear</span><span>(..</span><span>stops</span><span>).</span><span>sharp</span><span>(</span><span>5</span><span>)),</span></div></div><div><div><div>21</div></div><div><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>#let</span><span> </span><span>g</span><span><span> </span><span>= </span></span><span>gradient</span><span>.</span><span>linear</span><span>(..</span><span>stops</span><span>)</span></div></div><div><div><div>24</div></div><div><span>// `sample` reads a color back at a ratio (in the gradient space, oklab).</span></div></div><div><div><div>25</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>rgb</span><span>(</span><span>g</span><span>.</span><span>sample</span><span>(</span><span>0%</span><span>)).</span><span>to-hex</span><span>(), </span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>26</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>g</span><span>.</span><span>stops</span><span>().</span><span>len</span><span>(), </span><span>2</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-gradients.png" alt="十个渐变色块：线性渐变（水平、45 度、红白蓝三色显式色标）、径向渐变（居中、偏心）、圆锥渐变、红到蓝在 oklab 与 sRGB 两个空间里的对比、turbo 彩虹、五段硬边色带" /><figcaption>十个渐变色块：线性渐变（水平、45 度、红白蓝三色显式色标）、径向渐变（居中、偏心）、圆锥渐变、红到蓝在 oklab 与 sRGB 两个空间里的对比、turbo 彩虹、五段硬边色带</figcaption></figure><p></p><p>渐变是一个值，可以出现在任何要颜色的地方（<code>fill</code>、<code>stroke</code>、文字的 <code>fill</code>）。三种：</p><ul>
<li><code>gradient.linear(..)</code>：沿一个方向变化，<code>angle:</code> 改方向。色标可以直接给颜色（均分），也可以给 <code>(颜色, 位置)</code> 对，如图里第三个红、白、蓝，白色被拉到 30% 处。</li>
<li><code>gradient.radial(..)</code>：从中心向外，<code>center</code> 和 <code>radius</code> 控制圆心与半径。图里第二行的第二块把圆心移到 <code>(20%, 30%)</code>。</li>
<li><code>gradient.conic(..)</code>：绕中心旋转的角度渐变，图里第二行第三块，两种颜色在分界线上有一条硬边。</li>
</ul><p><strong>插值空间</strong>决定渐变的中间色。默认是 <code>oklab</code>，我核对了一下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'gradient.linear(red, blue).space() == oklab'</span></div></div><div><div><div>2</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'gradient.linear(red, blue, space: oklab).sample(50%).to-hex()'</span></div></div><div><div><div>3</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'gradient.linear(red, blue, space: rgb).sample(50%).to-hex()'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>true</span></div></div><div><div><div>2</div></div><div><span>"#a37095"</span></div></div><div><div><div>3</div></div><div><span>"#805b87"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>和上面 <code>mix</code> 的两个结果一致：在 <code>oklab</code> 里红到蓝的中点是 <code>#a37095</code>，在 sRGB 里是 <code>#805b87</code>。图里第三行前两块就是这一对，左边（<code>oklab</code>）中间明显更亮。</p><p>另外两个值得一记的：<code>.sharp(5)</code> 把平滑渐变切成 5 段硬边色带，图里最后一块；<code>..color.map.turbo</code> 把 256 个色标一次展开成渐变，得到彩虹条。文件最后两条 <code>assert</code> 用 <code>sample</code> 和 <code>stops</code> 读回渐变：<code>rgb(g.sample(0%)).to-hex()</code> 等于起点色 <code>#1f4e9c</code>，<code>g.stops().len()</code> 是 2。<code>sample</code> 返回的颜色在渐变自己的空间（这里是 <code>oklab</code>）里，所以断言前先用 <code>rgb(...)</code> 转了一下。</p></section><section><h3>tiling：用一小块图案铺满<a href="#tiling用一小块图案铺满"><span>#</span></a></h3><p><code>tiling</code> 把一块小内容重复，当作 <code>fill</code> 用。<code>lab/ch13/tiling.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/tiling.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `tiling` repeats a cell to fill a shape (patterns).</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>blue</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// Diagonal hatching.</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>hatch</span><span><span> </span><span>= </span></span><span>tiling</span><span>(</span><span>size</span><span>: (</span><span>8pt</span><span>, </span><span>8pt</span><span>))[</span></div></div><div><div><div>6</div></div><div><span>  </span><span>#place</span><span>(</span><span>line</span><span>(</span><span>start</span><span>: (</span><span>0%</span><span>, </span><span>100%</span><span>), </span><span>end</span><span>: (</span><span>100%</span><span>, </span><span>0%</span><span>), </span><span>stroke</span><span>: </span><span>0.8pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>))</span></div></div><div><div><div>7</div></div><div><span>]</span></div></div><div><div><div>8</div></div><div><span>// Dots, with `spacing` between cells.</span></div></div><div><div><div>9</div></div><div><span>#let</span><span> </span><span>dots</span><span><span> </span><span>= </span></span><span>tiling</span><span>(</span><span>size</span><span>: (</span><span>10pt</span><span>, </span><span>10pt</span><span>), </span><span>spacing</span><span>: (</span><span>4pt</span><span>, </span><span>4pt</span><span>))[</span></div></div><div><div><div>10</div></div><div><span>  </span><span>#align</span><span>(</span><span>center</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>, </span><span>circle</span><span>(</span><span>radius</span><span>: </span><span>2pt</span><span>, </span><span>fill</span><span>: </span><span>blue</span><span>))</span></div></div><div><div><div>11</div></div><div><span>]</span></div></div><div><div><div>12</div></div><div><span>// Tilings take a relative frame: `parent` makes them align across shapes.</span></div></div><div><div><div>13</div></div><div><span>#let</span><span> </span><span>grid-cell</span><span><span> </span><span>= </span></span><span>tiling</span><span>(</span><span>size</span><span>: (</span><span>12pt</span><span>, </span><span>12pt</span><span>), </span><span>relative</span><span>: </span><span>"parent"</span><span>)[</span></div></div><div><div><div>14</div></div><div><span>  </span><span>#rect</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>height</span><span>: </span><span>100%</span><span>, </span><span>stroke</span><span>: </span><span>0.4pt</span><span><span> </span><span>+</span><span> </span></span><span>luma</span><span>(</span><span>150</span><span>))</span></div></div><div><div><div>15</div></div><div><span>]</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>18</div></div><div><span>  </span><span>columns</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>19</div></div><div><span>  </span><span>gutter</span><span>: </span><span>1em</span><span>,</span></div></div><div><div><div>20</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>6em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>hatch</span><span>, </span><span>stroke</span><span>: </span><span>blue</span><span>),</span></div></div><div><div><div>21</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>6em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>dots</span><span>),</span></div></div><div><div><div>22</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>6em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>grid-cell</span><span>),</span></div></div><div><div><div>23</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-tiling.png" alt="三个矩形：左边是蓝色斜线填充，中间是整齐的蓝色圆点，右边是灰色方格网" /><figcaption>三个矩形：左边是蓝色斜线填充，中间是整齐的蓝色圆点，右边是灰色方格网</figcaption></figure><p></p><p>左边是斜线填充，中间是圆点，右边的方格网用 <code>relative: "parent"</code> 让方格相对于父容器对齐。</p><p>圆点那一格用的是 <code>align(center + horizon, ...)</code>，而不是上面斜线用的 <code>place</code>。这不是风格问题：我在 <code>/tmp</code> 里把三种写法并排比较了（只在 <code>/tmp</code>，不在仓库里）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>blue</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>a</span><span><span> </span><span>= </span></span><span>tiling</span><span>(</span><span>size</span><span>: (</span><span>10pt</span><span>, </span><span>10pt</span><span>), </span><span>spacing</span><span>: (</span><span>4pt</span><span>, </span><span>4pt</span><span>))[</span></div></div><div><div><div>4</div></div><div><span>  </span><span>#place</span><span>(</span><span>center</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>, </span><span>circle</span><span>(</span><span>radius</span><span>: </span><span>2pt</span><span>, </span><span>fill</span><span>: </span><span>blue</span><span>))</span></div></div><div><div><div>5</div></div><div><span>]</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>b</span><span><span> </span><span>= </span></span><span>tiling</span><span>(</span><span>size</span><span>: (</span><span>10pt</span><span>, </span><span>10pt</span><span>), </span><span>spacing</span><span>: (</span><span>4pt</span><span>, </span><span>4pt</span><span>))[</span></div></div><div><div><div>7</div></div><div><span>  </span><span>#align</span><span>(</span><span>center</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>, </span><span>circle</span><span>(</span><span>radius</span><span>: </span><span>2pt</span><span>, </span><span>fill</span><span>: </span><span>blue</span><span>))</span></div></div><div><div><div>8</div></div><div><span>]</span></div></div><div><div><div>9</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span></span><span>tiling</span><span>(</span><span>size</span><span>: (</span><span>10pt</span><span>, </span><span>10pt</span><span>), </span><span>spacing</span><span>: (</span><span>4pt</span><span>, </span><span>4pt</span><span>))[</span></div></div><div><div><div>10</div></div><div><span>  </span><span>#place</span><span>(</span><span>dx</span><span>: </span><span>3pt</span><span>, </span><span>dy</span><span>: </span><span>3pt</span><span>, </span><span>circle</span><span>(</span><span>radius</span><span>: </span><span>2pt</span><span>, </span><span>fill</span><span>: </span><span>blue</span><span>))</span></div></div><div><div><div>11</div></div><div><span>]</span></div></div><div><div><div>12</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>13</div></div><div><span>  </span><span>columns</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>gutter</span><span>: </span><span>1em</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>6em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>a</span><span>),</span></div></div><div><div><div>16</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>6em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>b</span><span>),</span></div></div><div><div><div>17</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>6em</span><span>, </span><span>height</span><span>: </span><span>4em</span><span>, </span><span>fill</span><span>: </span><span>c</span><span>),</span></div></div><div><div><div>18</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-tiling-fix.png" alt="三种圆点图案：左边（place + center + horizon）每个点只剩左上角的四分之一，中间（align + center + horizon）和右边（place + dx/dy）都是完整的圆点" /><figcaption>三种圆点图案：左边（place + center + horizon）每个点只剩左上角的四分之一，中间（align + center + horizon）和右边（place + dx/dy）都是完整的圆点</figcaption></figure><p></p><p><code>a</code> 用 <code>place(center + horizon, ...)</code>，每个点只剩左上角；<code>b</code> 用 <code>align</code>，<code>c</code> 用 <code>place</code> 加显式偏移，都是完整的圆点。我的猜测是：<code>tiling</code> 的 body 不是在一个固定 <code>size</code> 的容器里排版的，<code>place</code> 的对齐目标是一个没有尺寸的区域，<code>center + horizon</code> 因此对准了左上角这个原点；而 <code>align</code> 参与了内容自身的排版，用的是 <code>size</code> 给出的单元。我没有去翻 Typst 的源码确认，只能说这是三种写法的实测差异。</p><p><code>tiling(size: ..., spacing: ..., relative: ...)</code> 的三个参数：<code>size</code> 是一个单元的大小，<code>spacing</code> 是单元之间的间距（中间那种圆点图案里的空白就来自它），<code>relative</code> 说明图案相对谁对齐，lab 里的方格网用了 <code>"parent"</code>，让图案相对于父容器对齐，多个形状上的图案就能连成一整块。</p></section><section><h3>描边：字典的写法<a href="#描边字典的写法"><span>#</span></a></h3><p><code>lab/ch13/stroke.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/stroke.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A stroke is paint + thickness + dash + cap + join, or a dictionary.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>blue</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>ln</span><span>(</span><span>s</span><span><span>) </span><span>= </span></span><span>line</span><span>(</span><span>length</span><span>: </span><span>8em</span><span>, </span><span>stroke</span><span>: </span><span>s</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#ln</span><span>(</span><span>1pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>) </span><span>// shorthand: length + color</span></div></div><div><div><div>6</div></div><div><span>#ln</span><span>((</span><span>paint</span><span>: </span><span>blue</span><span>, </span><span>thickness</span><span>: </span><span>3pt</span><span>, </span><span>cap</span><span>: </span><span>"round"</span><span>))</span></div></div><div><div><div>7</div></div><div><span>#ln</span><span>((</span><span>paint</span><span>: </span><span>blue</span><span>, </span><span>thickness</span><span>: </span><span>2pt</span><span>, </span><span>dash</span><span>: </span><span>"dashed"</span><span>))</span></div></div><div><div><div>8</div></div><div><span>#ln</span><span>((</span><span>paint</span><span>: </span><span>blue</span><span>, </span><span>thickness</span><span>: </span><span>2pt</span><span>, </span><span>dash</span><span>: </span><span>"dotted"</span><span>))</span></div></div><div><div><div>9</div></div><div><span>#ln</span><span>((</span><span>paint</span><span>: </span><span>blue</span><span>, </span><span>thickness</span><span>: </span><span>2pt</span><span>, </span><span>dash</span><span>: (</span><span>array</span><span>: (</span><span>6pt</span><span>, </span><span>3pt</span><span>, </span><span>1pt</span><span>, </span><span>3pt</span><span>))))</span></div></div><div><div><div>10</div></div><div><span>#ln</span><span>((</span><span>paint</span><span>: </span><span>gradient</span><span>.</span><span>linear</span><span>(</span><span>red</span><span>, </span><span>blue</span><span>), </span><span>thickness</span><span>: </span><span>4pt</span><span>))</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// Per-side strokes on a rect; `none` hides a side.</span></div></div><div><div><div>13</div></div><div><span>#rect</span><span>(</span></div></div><div><div><div>14</div></div><div><span>  </span><span>width</span><span>: </span><span>8em</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>height</span><span>: </span><span>3em</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>stroke</span><span>: (</span><span>top</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>, </span><span>bottom</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>, </span><span>rest</span><span>: </span><span>0.5pt</span><span><span> </span><span>+</span><span> </span></span><span>luma</span><span>(</span><span>150</span><span>)),</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div><div><div><div>18</div></div><div><span>// `join` matters on sharp corners.</span></div></div><div><div><div>19</div></div><div><span>#polygon</span><span>(</span></div></div><div><div><div>20</div></div><div><span>  </span><span>stroke</span><span>: (</span><span>paint</span><span>: </span><span>blue</span><span>, </span><span>thickness</span><span>: </span><span>6pt</span><span>, </span><span>join</span><span>: </span><span>"round"</span><span>),</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>(</span><span>0pt</span><span>, </span><span>30pt</span><span>),</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>(</span><span>30pt</span><span>, </span><span>0pt</span><span>),</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>(</span><span>60pt</span><span>, </span><span>30pt</span><span>),</span></div></div><div><div><div>24</div></div><div><span>)</span></div></div><div><div><div>25</div></div><div><span>// A stroke value is inspectable.</span></div></div><div><div><div>26</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>1pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>).</span><span>thickness</span><span>, </span><span>1pt</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-strokes.png" alt="八个描边示例：细蓝线、圆头粗线、虚线、点线、点划线、红到蓝渐变的粗线、上下蓝边其余灰边的矩形、圆角连接的三角形" /><figcaption>八个描边示例：细蓝线、圆头粗线、虚线、点线、点划线、红到蓝渐变的粗线、上下蓝边其余灰边的矩形、圆角连接的三角形</figcaption></figure><p></p><p>描边是 <code>paint</code>（颜色或渐变）、<code>thickness</code>、<code>dash</code>、<code>cap</code>、<code>join</code> 的组合。有两种写法：</p><ul>
<li>简写：<code>1pt + blue</code>，长度加颜色。<code>repr(1pt + rgb("#1f4e9c"))</code> 的输出是 <code>1pt + rgb("#1f4e9c")</code>。</li>
<li>字典：<code>(paint: blue, thickness: 3pt, cap: "round")</code>，可以写全部五个键。</li>
</ul><p><code>dash</code> 接受预设字符串或者自定义数组。我用 <code>typst eval</code> 把预设展开成了数组：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'repr(stroke((paint: red, thickness: 2pt, dash: "dashed")).dash)'</span></div></div><div><div><div>2</div></div><div><span>typst</span><span> </span><span>eval</span><span> </span><span>'repr(stroke((paint: red, thickness: 2pt, dash: "dotted")).dash)'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"(array: (3pt, 3pt), phase: 0pt)"</span></div></div><div><div><div>2</div></div><div><span>"(array: (\"dot\", 2pt), phase: 0pt)"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>"dashed"</code> 是“线 3pt、空 3pt”；<code>"dotted"</code> 是一个 <code>"dot"</code> 加 2pt 空。自定义的 <code>(array: (6pt, 3pt, 1pt, 3pt))</code> 是“线 6pt、空 3pt、线 1pt、空 3pt”循环，就是图里的点划线。<code>paint</code> 也可以是渐变（图里第六条红到蓝的粗线）。</p><p>图里倒数第二个矩形展示了<strong>逐边描边</strong>：<code>stroke: (top: 2pt + blue, bottom: 2pt + blue, rest: 0.5pt + luma(150))</code>，上下两条蓝色粗边，剩下的边（<code>rest</code>）是灰色细边。最后一个三角形用 <code>join: "round"</code> 让尖角变圆；文件末尾的 <code>assert.eq((1pt + blue).thickness, 1pt)</code> 说明描边值可以读回属性。</p></section><section><h3>一张小拼图<a href="#一张小拼图"><span>#</span></a></h3><p><code>lab/ch13/fig-drawing.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch13/fig-drawing.typ" target="_blank">源文件</a>）把以上的东西拼在一起，是 <code>make ch13</code> 直接输出的图（<code>out/figures/ch13-drawing.png</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A small gallery: shapes, a gradient, a tiling and a dashed stroke.</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>blue</span><span><span> </span><span>= </span></span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>hatch</span><span><span> </span><span>= </span></span><span>tiling</span><span>(</span><span>size</span><span>: (</span><span>8pt</span><span>, </span><span>8pt</span><span>))[</span></div></div><div><div><div>5</div></div><div><span>  </span><span>#place</span><span>(</span><span>line</span><span>(</span><span>start</span><span>: (</span><span>0%</span><span>, </span><span>100%</span><span>), </span><span>end</span><span>: (</span><span>100%</span><span>, </span><span>0%</span><span>), </span><span>stroke</span><span>: </span><span>0.8pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>))</span></div></div><div><div><div>6</div></div><div><span>]</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>9</div></div><div><span>  </span><span>columns</span><span>: </span><span>4</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>gutter</span><span>: </span><span>14pt</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>60pt</span><span>, </span><span>height</span><span>: </span><span>40pt</span><span>, </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#cfe0f5"</span><span>), </span><span>stroke</span><span>: </span><span>blue</span><span>),</span></div></div><div><div><div>12</div></div><div><span>  </span><span>circle</span><span>(</span><span>radius</span><span>: </span><span>20pt</span><span>, </span><span>fill</span><span>: </span><span>gradient</span><span>.</span><span>radial</span><span>(</span><span>white</span><span>, </span><span>blue</span><span>)),</span></div></div><div><div><div>13</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>60pt</span><span>, </span><span>height</span><span>: </span><span>40pt</span><span>, </span><span>fill</span><span>: </span><span>hatch</span><span>, </span><span>stroke</span><span>: </span><span>blue</span><span>),</span></div></div><div><div><div>14</div></div><div><span>  </span><span>polygon</span><span>(</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#f5d9cf"</span><span>),</span></div></div><div><div><div>16</div></div><div><span>    </span><span>stroke</span><span>: (</span><span>paint</span><span>: </span><span>blue</span><span>, </span><span>thickness</span><span>: </span><span>2pt</span><span>, </span><span>dash</span><span>: </span><span>"dashed"</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>(</span><span>0pt</span><span>, </span><span>40pt</span><span>),</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>(</span><span>30pt</span><span>, </span><span>0pt</span><span>),</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>(</span><span>60pt</span><span>, </span><span>40pt</span><span>),</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>21</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch13-drawing.png" alt="四个图形：浅蓝矩形、白到蓝的径向渐变圆、斜线填充的矩形、虚线描边的浅红三角形" /><figcaption>四个图形：浅蓝矩形、白到蓝的径向渐变圆、斜线填充的矩形、虚线描边的浅红三角形</figcaption></figure><p></p></section></section>
<section><h2>回到幻灯片：把图形放进 slide<a href="#回到幻灯片把图形放进-slide"><span>#</span></a></h2><p><code>examples/slides</code> 里有一页专门用了这篇的东西，“渐变与描边”（第 14 页）：左边 <code>gradient.linear</code> 填充的矩形，右边 <code>fill: none</code> 加虚线描边的矩形，放在一个两列的 <code>grid</code> 里（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/slides/main.typ" target="_blank">源文件</a> 的 70 到 86 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#slide</span><span>(</span><span>title</span><span>: </span><span>"渐变与描边"</span><span>)[</span></div></div><div><div><div>2</div></div><div><span>  </span><span>#grid</span><span>(</span></div></div><div><div><div>3</div></div><div><span>    </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>gutter</span><span>: </span><span>1em</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>rect</span><span>(</span></div></div><div><div><div>6</div></div><div><span>      </span><span>width</span><span>: </span><span>100%</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>height</span><span>: </span><span>4em</span><span>,</span></div></div><div><div><div>8</div></div><div><span>      </span><span>fill</span><span>: </span><span>gradient</span><span>.</span><span>linear</span><span>(</span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>), </span><span>rgb</span><span>(</span><span>"#e7a33e"</span><span>)),</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>),</span></div></div><div><div><div>10</div></div><div><span>    </span><span>rect</span><span>(</span></div></div><div><div><div>11</div></div><div><span>      </span><span>width</span><span>: </span><span>100%</span><span>,</span></div></div><div><div><div>12</div></div><div><span>      </span><span>height</span><span>: </span><span>4em</span><span>,</span></div></div><div><div><div>13</div></div><div><span>      </span><span>fill</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>14</div></div><div><span>      </span><span>stroke</span><span>: (</span><span>paint</span><span>: </span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>), </span><span>thickness</span><span>: </span><span>2pt</span><span>, </span><span>dash</span><span>: </span><span>"dashed"</span><span>),</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>),</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>渲染出来就是总览图里倒数第四页，左右各一个 <code>width: 100%</code> 的块，<code>gutter: 1em</code> 隔开。</p></section>
<section><h2>往下走<a href="#往下走"><span>#</span></a></h2><p><code>slides.typ</code> 的 95 行已经够做一份像样的课堂幻灯片，但缺的东西也很明显：没有备注页、没有页眉、没有主题切换、没有动画之外的过渡。这些都有现成的包。<a href="/posts/typst-by-xuan-14-packages-ecosystem/">第 14 篇</a>会用同一份内容的三张幻灯片，用 <code>touying</code> 重做一遍，并把代码量与 <code>examples/slides</code> 做对比；同一篇里还会用 <code>cetz</code> 画图、<code>lilaq</code> 画函数曲线，处理的正是这一篇里的“手写 <code>place</code> 和 <code>rect</code>”之外的需求。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-12-homework/</id>
      <title type="text">作业模板：模块、多文件与条件编译</title>
      <published>2026-10-19T11:00:00.000Z</published>
      <updated>2026-10-19T11:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-12-homework/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 12 篇：我用 Typst 0.15.1 把 examples/homework 的四道题拆成四个文件，用 include 拼成一份作业，再用 --input solutions=false 编出不带解答的 PDF（两份都是 1 页，解答各为 4 行与 0 行，总分都是 100），并逐个跑了 import 的各种写法、根相对路径、0.15 的 path 类型和循环导入的报错。</summary>
      <content type="html"><![CDATA[<p>作业是一种很特别的文档：同一份源文件要交出两个版本，学生拿到的只有题目，助教拿到的带解答；题目的分值加起来要等于总分，而总分没人愿意手算。这一篇我要做的，是让这些约束都变成编译器的事：四道题各放一个文件，用 <code>include</code> 拼起来，总分从题目自己带的分值里算出来，解答用一个命令行参数开关。顺着这条线，我把 Typst 的模块系统（<code>import</code>、<code>include</code>、路径、0.15 新增的 <code>path</code> 类型、循环导入）一次跑完。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>src/homework.typ</code>（<strong>最终版</strong>，88 行，全文都在下面）、<code>examples/homework/</code> 下的 <code>main.typ</code> 与 <code>problems/p1.typ</code> 到 <code>p4.typ</code>，以及 <code>lab/ch12/</code> 下的 4 个入口（<code>modules/main.typ</code>、<code>cycle/main.fail.typ</code>、<code>path-type/main.typ</code>、<code>fig-homework.typ</code>）。<code>lib.typ</code> 是<strong>中间版本</strong>，这一章只往里加了一行 <code>#import "src/homework.typ": homework, problem, solution</code>。<code>make ch12</code> 全部通过；<code>make examples</code> 会产出 <code>out/examples/homework.pdf</code> 和 <code>out/examples/homework-questions.pdf</code>。
前置：<a href="/posts/typst-by-xuan-11-data-driven-cv/">第 11 篇</a>（数据驱动的简历）。这一篇用到第 3 篇的函数与 <code>..args</code>、第 4 篇的 <code>show: f.with(...)</code> 模板形状，以及第 7 篇的 <code>context</code>、<code>state</code>、<code>query</code> 和 <code>metadata</code>，<a href="/posts/typst-by-xuan-07-context-counters-state/">第 7 篇</a>是读懂“总分怎么算出来”的前提。</p></div></div>
<section><h2>一份源码，两个 PDF<a href="#一份源码两个-pdf"><span>#</span></a></h2><p>先看产物。同一份 <code>examples/homework/main.typ</code>，编译两次，只差一个参数（命令就是 <code>make examples</code> 里的两行，我在仓库的 <code>nix develop</code> 里跑的）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts examples/homework/main.typ out/examples/homework.pdf</span></div></div><div><div><div>2</div></div><div><span>$ typst compile --root . --ignore-system-fonts --input solutions=false examples/homework/main.typ out/examples/homework-questions.pdf</span></div></div><div><div><div>3</div></div><div><span>$ pdfinfo out/examples/homework.pdf | grep Pages</span></div></div><div><div><div>4</div></div><div><span>Pages:           1</span></div></div><div><div><div>5</div></div><div><span>$ pdfinfo out/examples/homework-questions.pdf | grep Pages</span></div></div><div><div><div>6</div></div><div><span>Pages:           1</span></div></div><div><div><div>7</div></div><div><span>$ pdftotext out/examples/homework.pdf - | grep -c 解答</span></div></div><div><div><div>8</div></div><div><span>4</span></div></div><div><div><div>9</div></div><div><span>$ pdftotext out/examples/homework-questions.pdf - | grep -c 解答</span></div></div><div><div><div>10</div></div><div><span>0</span></div></div><div><div><div>11</div></div><div><span>$ pdftotext out/examples/homework.pdf - | grep 总分</span></div></div><div><div><div>12</div></div><div><span>总分：100</span></div></div><div><div><div>13</div></div><div><span>$ pdftotext out/examples/homework-questions.pdf - | grep 总分</span></div></div><div><div><div>14</div></div><div><span>总分：100</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两份都是 1 页；“解答”二字在带解答的版本里出现在 4 行（每题一次），在题目版里是 0 行；总分两份都是 100，也就是 25 + 25 + 20 + 30。后面这一点值得停一下：题目版里解答一个字都没有，总分却没有变，说明总分不依赖“解答显示与否”这个开关，它只依赖题目本身。</p><p>带解答的版本：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch12-homework.png" alt="带解答的作业第一页：顶部居中“线性代数”和“第 3 次作业”，下面一行姓名、学号、截止日期和“总分：100”，一条横线之后是第 1 题到第 4 题，每题标题后括号里是分值，题目下方有一块左侧灰色竖线的“解答”块，里面是矩阵、行列式和投影的推导" /><figcaption>带解答的作业第一页：顶部居中“线性代数”和“第 3 次作业”，下面一行姓名、学号、截止日期和“总分：100”，一条横线之后是第 1 题到第 4 题，每题标题后括号里是分值，题目下方有一块左侧灰色竖线的“解答”块，里面是矩阵、行列式和投影的推导</figcaption></figure><p></p><p>题目版（<code>--input solutions=false</code>，渲染命令是同一条加上 <code>--pages 1 --format png --ppi 144</code>，图片是 <code>/tmp</code> 里生成后复制进博客的，不是仓库里的 <code>out/figures</code>）：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch12-homework-questions.png" alt="不带解答的作业第一页：页眉与总分和上一张完全相同，四道题依次排列，题目下面没有灰色竖线块，页面下半部分是空白" /><figcaption>不带解答的作业第一页：页眉与总分和上一张完全相同，四道题依次排列，题目下面没有灰色竖线块，页面下半部分是空白</figcaption></figure><p></p><p>页眉一模一样，差别只有那四块灰色竖线。编译时间我也量了：<code>hyperfine -N --warmup 3 --runs 20</code> 分别跑上面两条 <code>typst compile</code> 命令，带解答 119.1 ms ± 6.4 ms，题目版 120.8 ms ± 6.5 ms，在误差之内没有区别。这不意外：<code>solution</code> 里的 <code>if</code> 只决定“要不要排版这个 block”，并不会让编译少读一个文件。</p></section>
<section><h2>四个文件，一个文档<a href="#四个文件一个文档"><span>#</span></a></h2><p>入口 <code>examples/homework/main.typ</code> 一共 12 行（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/homework/main.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Compile with --input solutions=false for the questions-only PDF.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>homework</span><span>.</span><span>with</span><span>(</span></div></div><div><div><div>5</div></div><div><span>  </span><span>course</span><span>: </span><span>"线性代数"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>title</span><span>: </span><span>"第 3 次作业"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>student</span><span>: </span><span>"张三"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>student-id</span><span>: </span><span>"20240001"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>due</span><span>: </span><span>"2026-11-02"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#include</span><span> </span><span>"problems/p1.typ"</span></div></div><div><div><div>13</div></div><div><span>#include</span><span> </span><span>"problems/p2.typ"</span></div></div><div><div><div>14</div></div><div><span>#include</span><span> </span><span>"problems/p3.typ"</span></div></div><div><div><div>15</div></div><div><span>#include</span><span> </span><span>"problems/p4.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>结构和第 4 篇的 <code>#show: note.with(...)</code> 一样：第一行 <code>import</code> 把 <code>homework</code>、<code>problem</code>、<code>solution</code> 这些名字拿进来，<code>show: homework.with(...)</code> 把版式规则套在后面的一切内容上，最后四行 <code>include</code> 把题目拉进来。每道题一个文件，<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/homework/problems/p1.typ" target="_blank"><code>p1.typ</code></a> 是这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#problem</span><span>(</span><span>points</span><span>: </span><span>25</span><span>, </span><span>title</span><span>: </span><span>"矩阵乘法"</span><span>)[</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>设 </span><span>$</span><span>A = </span><span>mat</span><span>(1, 2; 3, 4)</span><span>$</span><span>，</span><span>$</span><span>B = </span><span>mat</span><span>(0, 1; 1, 0)</span><span>$</span><span>。计算 </span><span>$</span><span>A B</span><span>$</span><span> 与 </span><span>$</span><span>B A</span><span>$</span><span>，</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>并说明矩阵乘法是否可交换。</span></div></div><div><div><div>6</div></div><div><span>]</span></div></div><div><div><div>7</div></div><div><span>#solution</span><span>[</span></div></div><div><div><div>8</div></div><div><span>  </span><span>$</span><span> A B = </span><span>mat</span><span>(2, 1; 4, 3), </span><span>quad</span><span> B A = </span><span>mat</span><span>(3, 4; 1, 2). </span><span>$</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>两者不相等，所以一般不可交换。</span></div></div><div><div><div>10</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/homework/problems/p2.typ" target="_blank"><code>p2.typ</code></a> 展示小问怎么写，用的是 <code>enum</code> 函数，编号样式由 <code>homework</code> 统一设成 <code>"(a)"</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#problem</span><span>(</span><span>points</span><span>: </span><span>25</span><span>, </span><span>title</span><span>: </span><span>"行列式"</span><span>)[</span></div></div><div><div><div>4</div></div><div><span>  </span><span>#enum</span><span>(</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>[求 </span><span>$</span><span>det</span><span> </span><span>mat</span><span>(1, 2; 3, 4)</span><span>$</span><span>。],</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>[若 </span><span>$</span><span>det</span><span> A = 3</span><span>$</span><span>，求 </span><span>$</span><span>det</span><span>(2 A)</span><span>$</span><span>（</span><span>$</span><span>A</span><span>$</span><span> 为 </span><span>$</span><span>3 </span><span>times</span><span> 3</span><span>$</span><span> 矩阵）。],</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>8</div></div><div><span>]</span></div></div><div><div><div>9</div></div><div><span>#solution</span><span>[</span></div></div><div><div><div>10</div></div><div><span>  </span><span>#enum</span><span>(</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>[</span><span>$</span><span>1 </span><span>dot</span><span> 4 - 2 </span><span>dot</span><span> 3 = -2</span><span>$</span><span>。],</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>[</span><span>$</span><span>det</span><span>(2 A) = 2^3 </span><span>det</span><span> A = 24</span><span>$</span><span>。],</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>14</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>p3.typ</code>、<code>p4.typ</code> 结构相同（分值 20 和 30），不贴了，在<a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1/examples/homework/problems" target="_blank">仓库</a>里。</p><p>注意每个题目文件<strong>自己</strong>又写了一遍 <code>#import "@local/xuan:0.1.0": *</code>。这不是啰嗦，是 <code>include</code> 的语义：它求值另一个文件，把得到的 <strong>content</strong> 贴到当前位置，不带回任何绑定，也不把当前文件的绑定借给对方。我做了个小实验确认（文件在 <code>/tmp/ch12x</code>，不在仓库里）。<code>scope.typ</code> 在自己里面定义了 <code>greet</code>，再 <code>include</code> 一个想用它的文件 <code>inc.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>greet</span><span>(</span><span>who</span><span><span>) </span><span>= </span><span>[hi </span></span><span>#who</span><span>]</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>"inc.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>inc.typ</code> 只有一行 <code>#greet("x")</code>，编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unknown variable: greet</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ inc.typ:1:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #greet("x")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^^^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>while including `inc.typ` at scope.typ:2:1</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>include "inc.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>被包含的文件是一个独立的求值单元，看不到外面的名字。这是个好设计：每个题目文件的依赖都写在自己头上，单独拿出来也能编译。我直接编译了 <code>p1.typ</code>（<code>typst compile --root . --ignore-system-fonts examples/homework/problems/p1.typ /tmp/p1.pdf</code>），退出码 0，但内容变了：<code>pdftotext</code> 的第一行是 <code>Problem 1：矩阵乘法 (25 pts)</code>。单独编译时没有 <code>homework</code> 的 show 规则，<code>text.lang</code> 是默认的英文，标题就走了英文分支。这也反过来印证了前面的说法：中文的 <code>第 1 题</code> 来自 <code>show: homework.with(...)</code> 套在被包含的内容上。</p><p>那 <code>homework.with(...)</code> 的版式规则怎么影响到被包含的内容？因为 <code>include</code> 的结果是 content，它被放在 <code>show: homework.with(...)</code> 之后的文档流里，show 规则和 set 规则作用的是文档流，而不是文件边界。证据就在 PDF 里：<code>solution</code> 在被包含的文件里调用，却能读到 <code>text.lang</code> 并排出“解答”，说明 <code>homework</code> 里 <code>xuan-cjk</code> 设的 <code>lang: "zh"</code> 已经落在了这些内容上。</p><p>借用一个比方，把这件事一次说清：<code>include</code> 像是把别人写好的几页纸直接订进你的本子，纸上写什么就是什么；<code>import</code> 像是向别人借工具箱，你拿到的是名字（函数、变量、颜色），自己想怎么用怎么用。作业的题目是前者，<code>homework</code> 和 <code>problem</code> 是后者。</p></section>
<section><h2>src/homework.typ：全文<a href="#srchomeworktyp全文"><span>#</span></a></h2><p>下面是 88 行的全文（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/homework.typ" target="_blank">源文件</a>）。它已经是最终版，我一段一段讲。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Homework with switchable solutions and an auto-computed total.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"cjk.typ"</span><span>: </span><span>xuan-cjk</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"fonts.typ"</span><span>: </span><span>fonts</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>solutions-on</span><span><span> </span><span>= </span></span><span>state</span><span>(</span><span>"xuan-solutions"</span><span>, </span><span>true</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>problem-counter</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"xuan-problem-n"</span><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>/// `points` is stored in a metadata element so the header can sum it.</span></div></div><div><div><div>10</div></div><div><span>#let</span><span> </span><span>problem</span><span>(</span><span>points</span><span>: </span><span>none</span><span>, </span><span>title</span><span>: </span><span>none</span><span>, </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>11</div></div><div><span>  </span><span>problem-counter</span><span>.</span><span>step</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>[</span><span>#metadata</span><span>(</span><span>points</span><span>) </span><span>&lt;xuan-problem&gt;</span><span>]</span></div></div><div><div><div>13</div></div><div><span>  </span><span>context</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>problem-counter</span><span>.</span><span>get</span><span>().</span><span>first</span><span>()</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>head</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>text</span><span>.</span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [第 </span><span>#n</span><span> 题] </span><span>else</span><span> [Problem </span><span>#n</span><span>]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> </span><span>title</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { </span><span>head</span><span><span> </span><span>+=</span><span> [：</span></span><span>#title</span><span>] }</span></div></div><div><div><div>17</div></div><div><span>    </span><span>if</span><span> </span><span>points</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>18</div></div><div><span>      </span><span>head</span><span><span> </span><span>+=</span><span> </span></span><span>if</span><span> </span><span>text</span><span>.</span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [（</span><span>#points</span><span> 分）] </span><span>else</span><span> [ (</span><span>#points</span><span> pts)]</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>    </span><span>v</span><span>(</span><span>0.8em</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>text</span><span>(</span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>head</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>parbreak</span><span>()</span></div></div><div><div><div>23</div></div><div><span>    </span><span>body</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>/// Shown only when the `xuan-solutions` state is true.</span></div></div><div><div><div>28</div></div><div><span>#let</span><span> </span><span>solution</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>29</div></div><div><span>  </span><span>if</span><span> </span><span>solutions-on</span><span>.</span><span>get</span><span>() {</span></div></div><div><div><div>30</div></div><div><span>    </span><span>let</span><span> </span><span>label</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>text</span><span>.</span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> { </span><span>"解答"</span><span> } </span><span>else</span><span> { </span><span>"Solution"</span><span> }</span></div></div><div><div><div>31</div></div><div><span>    </span><span>block</span><span>(</span></div></div><div><div><div>32</div></div><div><span>      </span><span>width</span><span>: </span><span>100%</span><span>,</span></div></div><div><div><div>33</div></div><div><span>      </span><span>inset</span><span>: (</span><span>left</span><span>: </span><span>0.8em</span><span>, </span><span>y</span><span>: </span><span>0.4em</span><span>),</span></div></div><div><div><div>34</div></div><div><span>      </span><span>stroke</span><span>: (</span><span>left</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>luma</span><span>(</span><span>150</span><span>)),</span></div></div><div><div><div>35</div></div><div><span>      </span><span>above</span><span>: </span><span>0.8em</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>)[</span></div></div><div><div><div>37</div></div><div><span>      </span><span>#text</span><span>(</span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>80</span><span>), </span><span>label</span><span>) </span><span>\</span></div></div><div><div><div>38</div></div><div><span>      </span><span>#body</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>#let</span><span> </span><span>homework</span><span>(</span></div></div><div><div><div>44</div></div><div><span>  </span><span>course</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>45</div></div><div><span>  </span><span>title</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>46</div></div><div><span>  </span><span>student</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>47</div></div><div><span>  </span><span>student-id</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>48</div></div><div><span>  </span><span>due</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>49</div></div><div><span>  </span><span>show-solutions</span><span>: </span><span>auto</span><span>,</span></div></div><div><div><div>50</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>51</div></div><div><span>  </span><span>body</span><span>,</span></div></div><div><div><div>52</div></div><div><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>53</div></div><div><span>  </span><span>let</span><span> </span><span>zh</span><span><span> </span><span>= </span></span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span></div></div><div><div><div>54</div></div><div><span>  </span><span>let</span><span> </span><span>on</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>show-solutions</span><span><span> </span><span>==</span><span> </span></span><span>auto</span><span> {</span></div></div><div><div><div>55</div></div><div><span>    </span><span>sys</span><span>.</span><span>inputs</span><span>.</span><span>at</span><span>(</span><span>"solutions"</span><span>, </span><span>default</span><span>: </span><span>"true"</span><span><span>) </span><span>==</span><span> </span></span><span>"true"</span></div></div><div><div><div>56</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> { </span><span>show-solutions</span><span> }</span></div></div><div><div><div>57</div></div><div><span>  </span><span>set</span><span> </span><span>document</span><span>(</span><span>title</span><span>: </span><span>if</span><span> </span><span>title</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { </span><span>title</span><span> } </span><span>else</span><span> { </span><span>""</span><span> })</span></div></div><div><div><div>58</div></div><div><span>  </span><span>set</span><span> </span><span>page</span><span>(</span><span>paper</span><span>: </span><span>"a4"</span><span>, </span><span>margin</span><span>: </span><span>2.5cm</span><span>)</span></div></div><div><div><div>59</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-cjk</span><span>.</span><span>with</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>)</span></div></div><div><div><div>60</div></div><div><span>  </span><span>solutions-on</span><span>.</span><span>update</span><span>(</span><span>on</span><span>)</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span>  </span><span>align</span><span>(</span><span>center</span><span>)[</span></div></div><div><div><div>63</div></div><div><span>    </span><span>#if</span><span> </span><span>course</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { </span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.1em</span><span>, </span><span>course</span><span>) }</span></div></div><div><div><div>64</div></div><div><span>    </span><span>#v</span><span><span>(</span><span>-</span></span><span>0.3em</span><span>)</span></div></div><div><div><div>65</div></div><div><span>    </span><span>#text</span><span>(</span><span>size</span><span>: </span><span>1.7em</span><span>, </span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>title</span><span>)</span></div></div><div><div><div>66</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>67</div></div><div><span>  </span><span>let</span><span> </span><span>info</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>68</div></div><div><span>    </span><span>if</span><span> </span><span>student</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { (</span><span>if</span><span> </span><span>zh</span><span> { </span><span>"姓名"</span><span> } </span><span>else</span><span> { </span><span>"Name"</span><span> }, </span><span>student</span><span>) },</span></div></div><div><div><div>69</div></div><div><span>    </span><span>if</span><span> </span><span>student-id</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { (</span><span>if</span><span> </span><span>zh</span><span> { </span><span>"学号"</span><span> } </span><span>else</span><span> { </span><span>"ID"</span><span> }, </span><span>student-id</span><span>) },</span></div></div><div><div><div>70</div></div><div><span>    </span><span>if</span><span> </span><span>due</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { (</span><span>if</span><span> </span><span>zh</span><span> { </span><span>"截止日期"</span><span> } </span><span>else</span><span> { </span><span>"Due"</span><span> }, </span><span>due</span><span>) },</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>).</span><span>filter</span><span>(</span><span>x</span><span> </span><span>=&gt;</span><span> </span><span>x</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span>)</span></div></div><div><div><div>72</div></div><div><span>  </span><span>grid</span><span>(</span></div></div><div><div><div>73</div></div><div><span>    </span><span>columns</span><span>: </span><span>info</span><span>.</span><span>len</span><span><span>() </span><span>+</span><span> </span></span><span>1</span><span>,</span></div></div><div><div><div>74</div></div><div><span>    </span><span>column-gutter</span><span>: </span><span>1.5em</span><span>,</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>..</span><span>info</span><span>.</span><span>map</span><span>(((</span><span>k</span><span>, </span><span>v</span><span>)) </span><span>=&gt;</span><span> [</span><span>#k</span><span>#if</span><span> </span><span>zh</span><span> [：] </span><span>else</span><span> [: ]</span><span>#v</span><span>]),</span></div></div><div><div><div>76</div></div><div><span>    </span><span>context</span><span> {</span></div></div><div><div><div>77</div></div><div><span>      </span><span>let</span><span> </span><span>total</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>&lt;xuan-problem&gt;</span><span>)</span></div></div><div><div><div>78</div></div><div><span><span>        </span></span><span>.</span><span>map</span><span>(</span><span>m</span><span> </span><span>=&gt;</span><span> </span><span>m</span><span>.</span><span>value</span><span>)</span></div></div><div><div><div>79</div></div><div><span><span>        </span></span><span>.</span><span>filter</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> </span><span>v</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span>)</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>.</span><span>sum</span><span>(</span><span>default</span><span>: </span><span>0</span><span>)</span></div></div><div><div><div>81</div></div><div><span>      </span><span>if</span><span> </span><span>zh</span><span> [总分：</span><span>#total</span><span>] </span><span>else</span><span> [Total: </span><span>#total</span><span>]</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>83</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>84</div></div><div><span>  </span><span>line</span><span>(</span><span>length</span><span>: </span><span>100%</span><span>, </span><span>stroke</span><span>: </span><span>0.5pt</span><span>)</span></div></div><div><div><div>85</div></div><div><span>  </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>86</div></div><div><span>  </span><span>set</span><span> </span><span>enum</span><span>(</span><span>numbering</span><span>: </span><span>"(a)"</span><span>)</span></div></div><div><div><div>87</div></div><div><span>  </span><span>body</span></div></div><div><div><div>88</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>两个文档级变量：<code>solutions-on</code> 和 <code>problem-counter</code>。</strong> 题目文件之间互不可见，它们之间要通信，只能走文档级的东西：<code>state("xuan-solutions", true)</code> 记录“要不要显示解答”，<code>counter("xuan-problem-n")</code> 记录“现在是第几题”。两者按<strong>字符串键</strong>全局共享，不管 <code>problem</code> 是在 <code>main.typ</code> 里调用还是在 <code>p3.typ</code> 里调用，用的都是同一份。这正是上一节那个限制的另一面：include 的文件看不到变量，所以跨文件共享的东西要放进 state 和 counter。</p><p><strong><code>problem</code>：编号、标题与分值。</strong> <code>problem-counter.step()</code> 让计数器加一，它本身不需要 context；<code>[#metadata(points) &lt;xuan-problem&gt;]</code> 在这里留下一个带标签的隐形元素，值就是分值，页眉稍后会 <code>query</code> 它。标题那一段包在 <code>context { ... }</code> 里，因为读 <code>problem-counter.get()</code> 和 <code>text.lang</code> 都要知道“我在文档的哪个位置”（第 7 篇的主题）；<code>text.lang == "zh"</code> 决定输出 <code>第 1 题（25 分）</code> 还是 <code>Problem 1 (25 pts)</code>。<code>head += [：#title]</code> 利用的是 content 可以用 <code>+</code> 拼接这个事实（第 3 篇）。<code>points: none</code> 是合法的：没有分值的题只是不显示括号，也不计入总分。</p><p><strong><code>solution</code>：整个内容被开关一刀切掉。</strong> <code>context { if solutions-on.get() { ... } }</code>：状态为假时，<code>context</code> 的值是 <code>none</code>，页面上什么都不剩，包括外面那圈 <code>block</code> 的缩进和灰线，所以题目版里不会有空白的竖线。左侧竖线用 <code>stroke: (left: 2pt + luma(150))</code>，只给一边描边。</p><p><strong><code>homework</code>：把参数变成版式。</strong> 函数体按顺序做五件事：</p><ol>
<li>解出 <code>on</code>。<code>show-solutions</code> 默认是 <code>auto</code>，此时读 <code>sys.inputs.at("solutions", default: "true") == "true"</code>；调用者也可以直接传 <code>true</code> 或 <code>false</code>，写死在源文件里，优先于命令行。</li>
<li>三条 set：<code>document(title:)</code> 写进 PDF 元数据，<code>page(paper: "a4", margin: 2.5cm)</code>，以及 <code>show: xuan-cjk.with(lang: lang)</code>，复用第 5 篇的中文排版。</li>
<li><code>solutions-on.update(on)</code>：把第 1 步算出的布尔值存进 state，从这一刻起，文档里所有后面的 <code>solution</code> 都读得到。</li>
<li>标题块和信息栏。<code>info</code> 是一个数组，每个元素是 <code>(标签, 值)</code>，没传的字段是 <code>none</code>，再 <code>.filter(x =&gt; x != none)</code> 去掉；<code>info.len() + 1</code> 就是网格的列数，多出来的那一列留给总分。<code>..info.map(((k, v)) =&gt; ...)</code> 里的 <code>((k, v)) =&gt;</code> 是闭包参数的解构（我在 <code>/tmp/ch12x/d3.typ</code> 里编译确认过：<code>((k, v)) =&gt; [#k=#v]</code> 接收 <code>(1, 2)</code> 得到 <code>1=2</code>，不在仓库里），<code>..</code> 把整个数组展开成网格的单元格。</li>
<li><code>set par(first-line-indent: 0pt)</code> 与 <code>set enum(numbering: "(a)")</code>：作业里的段落不需要首行缩进，小问用 <code>(a)</code>、<code>(b)</code>。这两条 set 写在页眉之后、<code>body</code> 之前，只影响题目内容，不影响上面的标题块。</li>
</ol></section>
<section><h2>总分：为什么需要多遍编译<a href="#总分为什么需要多遍编译"><span>#</span></a></h2><p>总分那一格是整个模板里最有意思的地方。下面是它的代码，摘自上面 <code>grid</code> 的最后一个参数（我去掉了缩进和结尾的逗号，并给 <code>context</code> 补了一个 <code>#</code>，这样它能独立成立；<code>zh</code> 是 <code>homework</code> 开头定义的变量）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>let</span><span> </span><span>total</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>&lt;xuan-problem&gt;</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>.</span><span>map</span><span>(</span><span>m</span><span> </span><span>=&gt;</span><span> </span><span>m</span><span>.</span><span>value</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>.</span><span>filter</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> </span><span>v</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>.</span><span>sum</span><span>(</span><span>default</span><span>: </span><span>0</span><span>)</span></div></div><div><div><div>6</div></div><div><span>  </span><span>if</span><span> </span><span>zh</span><span> [总分：</span><span>#total</span><span>] </span><span>else</span><span> [Total: </span><span>#total</span><span>]</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它在文档<strong>最前面</strong>，却要知道<strong>后面</strong>四道题各带多少分。按“从头到尾排一遍版”的直觉，这做不到：排到页眉时，题目还没出现。Typst 的做法在第 7 篇讲过：多编译几遍，第一遍时 <code>query</code> 先拿到一个不完整的（甚至空的）答案，排完版把所有 <code>metadata</code> 收集起来，第二遍用收集到的值重新排，直到答案和上一遍一致。总分正是这样收敛的：第一遍 <code>query(&lt;xuan-problem&gt;)</code> 看不到题目，总分是个占位值；第二遍看到四个值，得到 100；100 的排版没有改变任何 metadata，编译器确认稳定，停止。</p><p><code>default: 0</code> 看起来是为第一遍写的，但我实测发现不是：它只在<strong>最后一遍</strong>里没有任何 <code>problem</code> 调用时才起作用。空数组调 <code>.sum()</code> 会报错，我单独试了一行 <code>#().sum()</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: cannot calculate sum of empty array with no default</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ sum.typ:1:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #().sum()</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把 <code>default: 0</code> 从 <code>homework.typ</code> 里临时删掉（审阅时做的实验，已还原），<code>examples/homework</code> 照样编出了 <code>总分：100</code>：非最终遍里的错误会被丢弃，只有最终遍的结果算数。我用两个 <code>/tmp/ch12x</code> 里的小文件确认了这一点（不在仓库里）：<code>#context { query(&lt;p&gt;).map(m =&gt; m.value).sum() }</code> 后面跟两个带 <code>&lt;p&gt;</code> 的 <code>metadata</code>，编译输出 <code>100</code>；去掉那两个 <code>metadata</code>，编译报上面这条 <code>cannot calculate sum of empty array with no default</code>。所以 <code>default: 0</code> 保护的是“一道题都没有”的文档（比如只写了 <code>#show: homework.with(...)</code> 的空作业）。<code>.filter(v =&gt; v != none)</code> 的作用我也试了一下（<code>/tmp/ch12x/ns.typ</code>，不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#repr</span><span>((</span><span>1</span><span>, </span><span>none</span><span>).</span><span>sum</span><span>()) </span><span>\</span></div></div><div><div><div>2</div></div><div><span>#repr</span><span>((</span><span>none</span><span>, </span><span>none</span><span>).</span><span>sum</span><span>()) </span><span>\</span></div></div><div><div><div>3</div></div><div><span>#repr</span><span>((</span><span>none</span><span>, </span><span>none</span><span>).</span><span>filter</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> </span><span>v</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span>).</span><span>sum</span><span>(</span><span>default</span><span>: </span><span>0</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1</span></div></div><div><div><div>2</div></div><div><span>none</span></div></div><div><div><div>3</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>none</code> 对加法是单位元，所以混着没有分值的题时 <code>sum</code> 本来就不会报错；但如果所有题都没有分值，<code>sum</code> 返回的是 <code>none</code> 而不是 0，页眉会排出一个空的总分。先过滤再 <code>sum(default: 0)</code>，这种情况下得到的是 0。</p><p><code>query</code> 能看到后面的元素，这一点我单独验证了一遍（<code>/tmp/ch12x/conv.typ</code>，不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#context</span><span> [前面的 query：</span><span>#query</span><span>(</span><span>&lt;p&gt;</span><span>).</span><span>len</span><span>() 个]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>[</span><span>#metadata</span><span>(</span><span>25</span><span>) </span><span>&lt;p&gt;</span><span>]</span></div></div><div><div><div>4</div></div><div><span>[</span><span>#metadata</span><span>(</span><span>75</span><span>) </span><span>&lt;p&gt;</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>前面的 query：2 个</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行在两个 metadata 之前，却数到了 2 个。再数一数编译遍数：用第 7 篇的方法，<code>--timings</code> 加 <code>jq</code> 数 <code>iter</code> 事件的个数：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>.</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>--input</span><span> </span><span>solutions=</span><span>true</span><span> </span><span>--timings</span><span> </span><span>t.json</span><span> </span><span>examples/homework/main.typ</span><span> </span><span>out.pdf</span></div></div><div><div><div>2</div></div><div><span>jq</span><span> </span><span>'[.[]|select(.name|startswith("iter"))|select(.ph=="B")]|length'</span><span> </span><span>t.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出 <code>2</code>。<code>--input solutions=false</code> 同样是 <code>2</code>。两遍，和上面的推演一致：第一遍总分是 0，第二遍是 100，第三遍没有必要。</p></section>
<section><h2><code>--input</code>：命令行里的条件编译<a href="#--input命令行里的条件编译"><span>#</span></a></h2><p><code>typst compile --input solutions=false</code> 把键值对放进 <code>sys.inputs</code>，一个字典，在源码里用 <code>sys.inputs.at("solutions", default: "true")</code> 读。<code>default</code> 让不带参数的编译走“显示解答”的分支。</p><p>有一个细节容易踩：<code>--input</code> 的值<strong>永远是字符串</strong>。所以 <code>homework</code> 里比较的是 <code>== "true"</code>，而不是当作布尔值直接用。我多试了一种写法，把值写成 <code>maybe</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts --input solutions=maybe examples/homework/main.typ /tmp/o3.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext /tmp/o3.pdf - | grep -c 解答</span></div></div><div><div><div>3</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>"maybe" == "true"</code> 为假，解答被隐藏。这个规则是我在 <code>homework</code> 里写下的（只有字符串 <code>"true"</code> 才算开），不是 Typst 的内置行为；如果想更严格，可以对 <code>"true"</code> 和 <code>"false"</code> 之外的值直接 <code>panic</code>，这里我没有做。</p><p>这就是“条件编译”在 Typst 里的全部：没有 <code>#ifdef</code>，只有普通的 <code>if</code> 加上一个能从外面传进来的字典。同样的做法在第 11 篇的 <code>--input lang=en</code> 里出现过，这里换了个用途。</p></section>
<section><h2>模块：每一种写法都跑一遍<a href="#模块每一种写法都跑一遍"><span>#</span></a></h2><p><code>lab/ch12/modules/</code> 下有三个文件，演示 <code>import</code> 的所有形式。先是被导入的 <code>a.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch12/modules/a.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>name</span><span><span> </span><span>= </span></span><span>"a"</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>greet</span><span>(</span><span>who</span><span><span>) </span><span>= </span><span>[Hello </span></span><span>#who</span><span> from </span><span>#name</span><span>]</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>hidden-helper</span><span><span> </span><span>= </span></span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再是 <code>b.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch12/modules/b.typ" target="_blank">源文件</a>），它自己也导入了 <code>a.typ</code>，并且正文里有一个标题：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"a.typ"</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>name</span><span><span> </span><span>= </span></span><span>"b"</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>both</span><span><span>() </span><span>= </span><span>[</span></span><span>#a</span><span>.</span><span>name</span><span> and </span><span>#name</span><span>]</span></div></div><div><div><div>4</div></div><div><span>=</span><span> Section from b.typ</span></div></div><div><div><div>5</div></div><div><span>This file was included.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>入口 <code>main.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch12/modules/main.typ" target="_blank">源文件</a>）把所有写法排成一列：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// All import forms. Paths are relative to the importing file.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"a.typ"</span><span> </span><span>// module bound to `a`</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"a.typ"</span><span> </span><span>as</span><span> </span><span>alpha</span><span> </span><span>// renamed</span></div></div><div><div><div>4</div></div><div><span>#import</span><span> </span><span>"a.typ"</span><span>: </span><span>greet</span><span> </span><span>// one item</span></div></div><div><div><div>5</div></div><div><span>#import</span><span> </span><span>"a.typ"</span><span>: </span><span>greet</span><span> </span><span>as</span><span> </span><span>hi</span><span>, </span><span>name</span><span> </span><span>as</span><span> </span><span>a-name</span><span> </span><span>// items with aliases</span></div></div><div><div><div>6</div></div><div><span>#import</span><span> </span><span>"/lab/ch12/modules/b.typ"</span><span>: </span><span>both</span><span> </span><span>// root-relative (--root .)</span></div></div><div><div><div>7</div></div><div><span>#import</span><span> </span><span>"a.typ"</span><span>: * </span><span>// everything public: `name`, `greet`, ...</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>a</span><span>.</span><span>name</span><span>, </span><span>"a"</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>alpha</span><span>.</span><span>name</span><span>, </span><span>"a"</span><span>)</span></div></div><div><div><div>11</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>a-name</span><span>, </span><span>"a"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>name</span><span>, </span><span>"a"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>#greet</span><span>(</span><span>"you"</span><span>); </span><span>#hi</span><span>(</span><span>"me"</span><span>) </span><span>\</span></div></div><div><div><div>14</div></div><div><span>#both</span><span>() </span><span>\</span></div></div><div><div><div>15</div></div><div><span>// `include` pastes the other file's content, not its bindings.</span></div></div><div><div><div>16</div></div><div><span>#include</span><span> </span><span>"b.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐行读：</p><ol>
<li><code>#import "a.typ"</code>：不带冒号，整个模块绑成一个名字 <code>a</code>（取文件名去掉扩展名），之后写 <code>a.name</code>、<code>a.greet(...)</code>。</li>
<li><code>#import "a.typ" as alpha</code>：同样绑整个模块，但改名。</li>
<li><code>#import "a.typ": greet</code>：只取一个名字。</li>
<li><code>#import "a.typ": greet as hi, name as a-name</code>：取多个名字，每个都可以单独 <code>as</code> 改名。<code>name as a-name</code> 是为了不和后面 <code>*</code> 带进来的 <code>name</code> 打架，这里的用意是演示别名；<code>hi</code> 是 <code>greet</code> 的别名。</li>
<li><code>#import "/lab/ch12/modules/b.typ": both</code>：以 <code>/</code> 开头的路径是<strong>根相对</strong>的，根是 <code>--root</code> 指定的目录（<code>make</code> 里是 <code>--root .</code>，即仓库根）。</li>
<li><code>#import "a.typ": *</code>：把 <code>a.typ</code> 里的所有顶层名字都倒进当前作用域。</li>
</ol><p><code>assert.eq</code> 检查的是绑定，真正的输出来自后面三行。<code>make ch12</code> 编译后用 <code>pdftotext -layout</code> 看内容：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Hello you from a Hello me from a</span></div></div><div><div><div>2</div></div><div><span>a and b</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>Section from b.typ</span></div></div><div><div><div>5</div></div><div><span>This file was included.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行：<code>greet("you")</code> 和 <code>hi("me")</code> 是同一个函数；两处都是 “from a”，因为函数里的 <code>name</code> 是定义它的 <code>a.typ</code> 里的那个，不是调用处当前作用域里的 <code>name</code>（这是第 3 篇讲过的闭包：函数记住的是定义时的环境）。第二行：<code>both()</code> 来自 <code>b.typ</code>，它用的 <code>a.name</code> 与 <code>name</code>（<code>"b"</code>）都在 <code>b.typ</code> 里解析，所以是 <code>a and b</code>。第三、四行是 <code>#include "b.typ"</code> 的结果：<code>b.typ</code> 里的正文（那个标题和那句话）原样出现在这里，而 <code>b.typ</code> 里的 <code>let</code> 绑定一个也没有带进来。</p><section><h3>Typst 没有“私有”<a href="#typst-没有私有"><span>#</span></a></h3><p><code>a.typ</code> 里我故意留了一个 <code>hidden-helper</code>，名字听起来像内部用的。用 <code>*</code> 导入后直接用它（<code>/tmp/ch12x/priv.typ</code>，实验文件，不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"a.typ"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#hidden-helper</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有任何拦截。Typst 的模块里每个顶层 <code>let</code> 都是公开的，没有 <code>pub</code> 或下划线约定能把它藏起来。这就是为什么 <code>xuan</code> 的 <code>lib.typ</code> 只放显式的 <code>#import "src/x.typ": a, b, c</code> 列表（第 4 篇）：包的公开接口靠“入口文件里挑名字”来定义，而不是靠语言的可见性。</p></section><section><h3>根相对路径与 <code>--root</code><a href="#根相对路径与---root"><span>#</span></a></h3><p>第 5 行的 <code>/lab/ch12/modules/b.typ</code> 依赖 <code>--root</code>。不给它的话，Typst 的根默认是入口文件所在目录，<code>/</code> 指向那里。我在仓库根目录直接去掉 <code>--root .</code> 编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --ignore-system-fonts lab/ch12/modules/main.typ /tmp/m.pdf</span></div></div><div><div><div>2</div></div><div><span>error: file not found (searched at /home/bfmhno3/Projects/typst-lab/lab/ch12/modules/lab/ch12/modules/b.typ)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ lab/ch12/modules/main.typ:6:8</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>6 │ #import "/lab/ch12/modules/b.typ": both // root-relative (--root .)</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>报错里“searched at”后面那串路径，把 <code>/</code> 解析成了 <code>lab/ch12/modules/</code>，再拼上 <code>lab/ch12/modules/b.typ</code>。这就是根相对路径的含义：<code>/</code> 不是文件系统根，是 <code>--root</code>。仓库里每条 <code>typst compile</code> 都带 <code>--root .</code>，就是为了让这类以 <code>/</code> 开头的路径指向仓库根。</p><p><code>lab/ch12/fig-homework.typ</code> 也靠相对路径：它只有三行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The homework example's first page, with solutions.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>"../../examples/homework/main.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它是上面带解答 PDF 的出处：<code>make ch12</code> 把它渲染成 <code>out/figures/ch12-homework.png</code>，也就是本文第一张图。路径里的 <code>..</code> 从当前文件的目录往上走。但根是边界：我在 <code>/tmp/ch12x/sub/m.typ</code> 里写 <code>#import "../up.typ": v</code>，用 <code>--root /tmp/ch12x/sub</code> 编译，得到 <code>error: path `"../up.typ"` would escape the project root</code>。所以 <code>--root</code> 同时是路径的起点和围栏：源码够不到根之外的文件。</p></section></section>
<section><h2>0.15 的 <code>path</code> 类型<a href="#015-的-path-类型"><span>#</span></a></h2><p>模块的路径是字符串，字符串有个问题：它相对于<strong>谁</strong>？<code>import "a.typ"</code> 和 <code>include</code> 很清楚，相对于写这行代码的文件。但当一个函数接收路径、再用 <code>csv(p)</code> 去读文件时，就有了两个候选：调用者的目录，还是函数所在文件的目录？Typst 对字符串的回答是后者：读文件的地方说了算。0.15 新增的 <code>path</code> 类型提供了另一个选择：<code>path("...")</code> 在<strong>创建的地方</strong>就按那个文件解析，之后不管传到哪里都指向同一个文件。</p><p><code>lab/ch12/path-type/</code> 把两种行为放在一起（<a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1/lab/ch12/path-type" target="_blank">目录</a>）。数据有两份，文件名一样，内容不同：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>who,</span><span>score</span></div></div><div><div><div>2</div></div><div><span>caller,</span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 <code>path-type/data/scores.csv</code>；<code>path-type/lib/data/scores.csv</code> 的第二行是 <code>helper,2</code>。帮手模块 <code>lib/helper.typ</code> 只有一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A path value remembers the file that made it; a string does not.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>first-row</span><span>(</span><span>p</span><span><span>) </span><span>= </span></span><span>csv</span><span>(</span><span>p</span><span>).</span><span>at</span><span>(</span><span>1</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>入口 <code>main.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 0.15: path("...") is resolved against THIS file, wherever it is used.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"lib/helper.typ"</span><span>: </span><span>first-row</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// Resolves relative to main.typ: data/scores.csv here (caller).</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>from-path</span><span><span> </span><span>= </span></span><span>first-row</span><span>(</span><span>path</span><span>(</span><span>"data/scores.csv"</span><span>))</span></div></div><div><div><div>6</div></div><div><span>// A plain string resolves where it is read: lib/data/scores.csv (helper).</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>from-str</span><span><span> </span><span>= </span></span><span>first-row</span><span>(</span><span>"data/scores.csv"</span><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>from-path</span><span>.</span><span>first</span><span>(), </span><span>"caller"</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>from-str</span><span>.</span><span>first</span><span>(), </span><span>"helper"</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>- </span><span>`path("data/scores.csv")`</span><span> loaded the </span><span>#from-path</span><span>.</span><span>first</span><span>() file</span></div></div><div><div><div>13</div></div><div><span>- </span><span>`"data/scores.csv"`</span><span> loaded the </span><span>#from-str</span><span>.</span><span>first</span><span>() file</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两次调用传的是同样的字面量 <code>"data/scores.csv"</code>，区别只是有没有套 <code>path(...)</code>。<code>make ch12</code> 编完后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext out/ch12/path-type.pdf -</span></div></div><div><div><div>2</div></div><div><span>• path("data/scores.csv") loaded the caller file</span></div></div><div><div><div>3</div></div><div><span>• "data/scores.csv" loaded the helper file</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>path(...)</code> 版读到了 <code>caller</code>（<code>main.typ</code> 旁边的 <code>data/</code>），字符串版读到了 <code>helper</code>（<code>helper.typ</code> 旁边的 <code>lib/data/</code>），两条 <code>assert.eq</code> 也通过了。</p><p>那 <code>path</code> 值里到底存了什么？我在同一个目录里临时建了一个文件（<code>zz.typ</code>，编完就删了，不在仓库里），内容是一行 <code>#repr(path("data/scores.csv"))</code>，用 <code>--root .</code> 编译，输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>path("/lab/ch12/path-type/data/scores.csv")</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>创建的那一刻，相对路径就被换算成了根相对的绝对形式。这解释了“记住来源”：值里存的是已经解析好的路径，后面谁来读都不会再变。另一个小实验（<code>/tmp/ch12x/pt.typ</code>，根是 <code>/tmp/ch12x</code>，所以结果是根相对的 <code>/a/b.csv</code>）也印证了同一点：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#repr</span><span>(</span><span>path</span><span>(</span><span>"a/b.csv"</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>path("/a/b.csv")</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>什么时候用哪个？写一个“读用户给的文件”的库函数，用 <code>path</code> 型参数，调用者传 <code>path("data/x.csv")</code> 就不用操心库文件在哪；要是只收字符串，它就相对库文件解析，调用者多半会写错。homework 模板本身没有用到 <code>path</code>（它不读外部文件），这里单独拿出来，是因为“相对谁”这个问题一旦跨文件传参就绕不开。</p></section>
<section><h2>循环导入<a href="#循环导入"><span>#</span></a></h2><p>模块之间可以互相依赖，但不能绕成环。<code>lab/ch12/cycle/</code> 下，<code>x.typ</code> 导入 <code>y.typ</code>，<code>y.typ</code> 又导入 <code>x.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"y.typ"</span><span>: </span><span>y</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>x</span><span><span> </span><span>= </span></span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"x.typ"</span><span>: </span><span>x</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>y</span><span><span> </span><span>= </span></span><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>入口 <code>main.fail.typ</code>（文件名里的 <code>.fail</code> 是仓库约定，<code>make ch12</code> 期望它编译失败并保存 stderr）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// x.typ imports y.typ, which imports x.typ again.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"x.typ"</span><span>: </span><span>x</span></div></div><div><div><div>3</div></div><div><span>#x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>out/ch12/cycle.stderr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: cyclic import</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch12/cycle/y.typ:1:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #import "x.typ": x</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^^^^^^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>while importing `y.typ` at lab/ch12/cycle/x.typ:1:1</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>import "y.typ": y</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>while importing `x.typ` at lab/ch12/cycle/main.fail.typ:2:1</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>import "x.typ": x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>错误指在“闭合环”的那一行（<code>y.typ</code> 的第 1 行，它要导入的 <code>x.typ</code> 已经在求值栈里了），下面的 <code>while importing</code> 链是完整的调用栈：<code>main.fail.typ</code> 导入了 <code>x.typ</code>，<code>x.typ</code> 导入了 <code>y.typ</code>，<code>y.typ</code> 想再导入 <code>x.typ</code>。原因不难理解：<code>import</code> 要先把对方整个求值完才能拿到名字，两个文件互相等对方求值完，谁也没法先出结果。<code>include</code> 受同样的限制，因为它同样要先求值对方。</p><p>解法一般是把两边共同需要的东西抽到第三个文件里，让 <code>x</code> 和 <code>y</code> 都只单向依赖它。<code>xuan</code> 的 <code>src/</code> 就是这样分层的：<code>util.typ</code>、<code>fonts.typ</code> 和 <code>page.typ</code> 不导入任何兄弟模块，在最底层，<code>cjk.typ</code> 导入 <code>fonts.typ</code>，在上一层，<code>note.typ</code>、<code>homework.typ</code> 在最上层，没有一条回头的边。</p></section>
<section><h2>跑一遍<a href="#跑一遍"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/Projects/typst-lab</span></div></div><div><div><div>2</div></div><div><span>nix</span><span> </span><span>develop</span></div></div><div><div><div>3</div></div><div><span>make</span><span> </span><span>ch12</span></div></div><div><div><div>4</div></div><div><span>make</span><span> </span><span>examples</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make ch12</code> 的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>./scripts/chapter.sh ch12</span></div></div><div><div><div>2</div></div><div><span>ok   lab/ch12/fig-homework.typ</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch12/cycle/main.fail.typ (failed as expected)</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch12/modules/main.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch12/path-type/main.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make examples</code> 最后两个文件就是前面看到的 <code>out/examples/homework.pdf</code> 与 <code>out/examples/homework-questions.pdf</code>。你可以把 <code>examples/homework/problems/p3.typ</code> 里的 <code>points: 20</code> 改成 <code>points: 5</code>，再编译一次（我在 <code>/tmp/hw</code> 的副本上这样做过），<code>pdftotext</code> 里的总分是 <code>总分：85</code>，改的只是一个文件里的一个数字。</p><p>下一篇是幻灯片与绘图：同样是“一份源码，多个页面”，只不过页面不再是 A4 纸，而是一页一页的画面，<code>slide</code> 的 <code>steps</code> 会让一个函数发出多页，那里还会再用到 counter。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-11-data-driven-cv/</id>
      <title type="text">数据驱动：用一份 YAML 生成中英文简历</title>
      <published>2026-10-19T10:00:00.000Z</published>
      <updated>2026-10-19T10:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-11-data-driven-cv/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 11 篇：我用 Typst 0.15.1 把 examples/cv 里的一份 cv.yaml 通过 --input lang=en 渲染成中英文两版一页 A4 简历（hyperfine 实测 153.8 ms 与 153.5 ms），逐个跑了 yaml、toml、json、csv、xml、cbor、read、datetime 和 eval，并实测了 eval 能看到哪些名字、能读到哪些文件。</summary>
      <content type="html"><![CDATA[<p>简历是最适合拿来学“数据和版式分开”的文档：内容一年改两三次，版式一次定好就不动，而且常常要两种语言各来一份。前面的文章里，正文就是源码；这一篇换个路子：内容放进 <code>cv.yaml</code>，版式放进 <code>src/cv.typ</code> 里的一个函数，<code>typst compile</code> 加一个 <code>--input lang=en</code> 就换一种语言。顺着这条线，我把 Typst 所有的数据加载函数、命令行传参、<code>eval</code> 和 <code>datetime</code> 一次跑完。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>src/cv.typ</code>（<strong>最终版</strong>，113 行，全文都在下面）、<code>examples/cv/cv.yaml</code> 与 <code>examples/cv/main.typ</code>，以及 <code>lab/ch11/</code> 下的 5 个文件。<code>lib.typ</code> 是<strong>中间版本</strong>，这一章只往里加了一行 <code>#import "src/cv.typ": cv</code>。<code>make ch11</code> 全部通过；<code>make examples</code> 会产出 <code>out/examples/cv.pdf</code> 和 <code>out/examples/cv-en.pdf</code>。
前置：<a href="/posts/typst-by-xuan-10-bibliography/">第 10 篇</a>（参考文献）。这一篇用到第 2 篇的字典和 <code>for</code>、第 3 篇的闭包和 <code>..args</code>、第 5 篇的 <code>xuan-cjk</code>。</p></div></div>
<section><h2>一份数据，两个 PDF<a href="#一份数据两个-pdf"><span>#</span></a></h2><p>先看产物。<code>examples/cv/main.typ</code> 一共四行（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/cv/main.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Language comes from the command line: --input lang=en</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>lang</span><span><span> </span><span>= </span></span><span>sys</span><span>.</span><span>inputs</span><span>.</span><span>at</span><span>(</span><span>"lang"</span><span>, </span><span>default</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#cv</span><span>(</span><span>yaml</span><span>(</span><span>"cv.yaml"</span><span>), </span><span>lang</span><span>: </span><span>lang</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>数据在 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/examples/cv/cv.yaml" target="_blank"><code>cv.yaml</code></a>，共 54 行，人物是虚构的张三 / Zhang San。我只摘开头到第一条教育经历，完整文件在仓库里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Every human-readable field is a string or {zh:, en:}.</span></div></div><div><div><div>2</div></div><div><span># Strings are evaluated as Typst markup.</span></div></div><div><div><div>3</div></div><div><span>name</span><span>: { </span><span>zh</span><span>: </span><span>张三</span><span>, </span><span>en</span><span>: </span><span>Zhang San</span><span> }</span></div></div><div><div><div>4</div></div><div><span>email</span><span>: </span><span>zhangsan@example.com</span></div></div><div><div><div>5</div></div><div><span>phone</span><span>: </span><span>"+86 138 0000 0000"</span></div></div><div><div><div>6</div></div><div><span>github</span><span>: </span><span>github.com/example</span></div></div><div><div><div>7</div></div><div><span>site</span><span>: </span><span>example.com/zhangsan</span></div></div><div><div><div>8</div></div><div><span>education</span><span>:</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>- </span><span>school</span><span>: { </span><span>zh</span><span>: </span><span>示例大学</span><span>, </span><span>en</span><span>: </span><span>Example University</span><span> }</span></div></div><div><div><div>10</div></div><div><span>    </span><span>degree</span><span>: { </span><span>zh</span><span>: </span><span>计算机科学与技术 本科</span><span>, </span><span>en</span><span>: </span><span>"B.Eng. in Computer Science"</span><span> }</span></div></div><div><div><div>11</div></div><div><span>    </span><span>start</span><span>: </span><span>"2021-09"</span></div></div><div><div><div>12</div></div><div><span>    </span><span>end</span><span>: </span><span>"2025-06"</span></div></div><div><div><div>13</div></div><div><span>    </span><span>detail</span><span>:</span></div></div><div><div><div>14</div></div><div><span>      </span><span>zh</span><span>: </span><span>"GPA 3.8/4.0，专业排名前 5%，主修课程：线性代数、算法、操作系统。"</span></div></div><div><div><div>15</div></div><div><span>      </span><span>en</span><span>: </span><span>"GPA 3.8/4.0, top 5% of the program. Courses: linear algebra, algorithms, operating systems."</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>约定只有两条：每个给人看的字段要么是一个字符串，要么是 <code>{zh: ..., en: ...}</code>；字符串里可以夹 Typst 标记（比如 <code>*3 倍*</code>、<code>#link(...)[...]</code>、<code>$x_k$</code>）。两条约定对应后面 <code>tr</code> 和 <code>rich</code> 两个小函数。</p><p>用两种方式编译，再看页数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts examples/cv/main.typ out/examples/cv.pdf</span></div></div><div><div><div>2</div></div><div><span>$ typst compile --root . --ignore-system-fonts --input lang=en examples/cv/main.typ out/examples/cv-en.pdf</span></div></div><div><div><div>3</div></div><div><span>$ pdfinfo out/examples/cv.pdf | grep Pages</span></div></div><div><div><div>4</div></div><div><span>Pages:           1</span></div></div><div><div><div>5</div></div><div><span>$ pdfinfo out/examples/cv-en.pdf | grep Pages</span></div></div><div><div><div>6</div></div><div><span>Pages:           1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（这两条编译命令就是 <code>make examples</code> 里的两行，我是在仓库的 <code>nix develop</code> 里跑的。）渲染结果：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch11-cv.png" alt="中文简历：居中的大号姓名“张三”，一行联系方式，教育经历、实习经历、项目经历、技能四节，每节标题下有一条细线，条目右侧是 2021.09–2025.06 这样的日期，进行中的写“至今”" /><figcaption>中文简历：居中的大号姓名“张三”，一行联系方式，教育经历、实习经历、项目经历、技能四节，每节标题下有一条细线，条目右侧是 2021.09–2025.06 这样的日期，进行中的写“至今”</figcaption></figure><p></p><p></p><figure><img src="/assets/images/typst-by-xuan/ch11-cv-en.png" alt="英文简历：同样的版式，标题换成 Education、Experience、Projects、Skills，学校名加粗，职位斜体，进行中的日期写 Present，数学式 x_k = F x_(k-1) 照常排出" /><figcaption>英文简历：同样的版式，标题换成 Education、Experience、Projects、Skills，学校名加粗，职位斜体，进行中的日期写 Present，数学式 x_k = F x_(k-1) 照常排出</figcaption></figure><p></p><p>两张图版式完全一致，差别只来自 YAML 里 <code>zh</code> 和 <code>en</code> 两个键、四个节标题、以及 <code>至今</code> / <code>Present</code>。这像是一份价目表可以印成中文菜单也可以印成英文菜单：价格不用重抄，改一处，两本菜单一起变。</p></section>
<section><h2>加载器：每一种都返回普通值<a href="#加载器每一种都返回普通值"><span>#</span></a></h2><p>Typst 的数据加载函数有一个共同点：它们是<strong>编译期读文件</strong>，返回的就是第 2 篇里讲过的普通值（字典、数组、字符串、字节），没有什么“数据对象”。<code>lab/ch11/loading.typ</code> 把它们逐个过了一遍（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch11/loading.typ" target="_blank">源文件</a>），数据在 <code>lab/ch11/data/</code> 下。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Every loader returns plain values; paths are relative to this file.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>y</span><span><span> </span><span>= </span></span><span>yaml</span><span>(</span><span>"data/scores.yaml"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>y</span><span>.</span><span>students</span><span>.</span><span>first</span><span>().</span><span>name</span><span>, </span><span>"张三"</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>t</span><span><span> </span><span>= </span></span><span>toml</span><span>(</span><span>"data/cfg.toml"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>t</span><span>.</span><span>page</span><span>.</span><span>margin</span><span>, </span><span>2.5</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>j</span><span><span> </span><span>= </span></span><span>json</span><span>(</span><span>"data/obj.json"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>j</span><span>.</span><span>tags</span><span>, (</span><span>"typst"</span><span>, </span><span>"json"</span><span>))</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// Default: array of arrays, header row included.</span></div></div><div><div><div>12</div></div><div><span>#let</span><span> </span><span>rows</span><span><span> </span><span>= </span></span><span>csv</span><span>(</span><span>"data/scores.csv"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>rows</span><span>.</span><span>first</span><span>(), (</span><span>"name"</span><span>, </span><span>"score"</span><span>))</span></div></div><div><div><div>14</div></div><div><span>// `row-type: dictionary` keys every row by the header.</span></div></div><div><div><div>15</div></div><div><span>#let</span><span> </span><span>recs</span><span><span> </span><span>= </span></span><span>csv</span><span>(</span><span>"data/scores.csv"</span><span>, </span><span>row-type</span><span>: </span><span>dictionary</span><span>)</span></div></div><div><div><div>16</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>recs</span><span>.</span><span>at</span><span>(</span><span>1</span><span>).</span><span>score</span><span>, </span><span>"85"</span><span>)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>// xml: an array of nodes; elements are dictionaries.</span></div></div><div><div><div>19</div></div><div><span>#let</span><span> </span><span>x</span><span><span> </span><span>= </span></span><span>xml</span><span>(</span><span>"data/scores.xml"</span><span>).</span><span>first</span><span>()</span></div></div><div><div><div>20</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>x</span><span>.</span><span>tag</span><span>, </span><span>"course"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>x</span><span>.</span><span>attrs</span><span>.</span><span>code</span><span>, </span><span>"LA101"</span><span>)</span></div></div><div><div><div>22</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>x</span><span>.</span><span>children</span><span>.</span><span>first</span><span>().</span><span>children</span><span>, (</span><span>"张三"</span><span>,))</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span></span><span>cbor</span><span>(</span><span>"data/a.cbor"</span><span>)</span></div></div><div><div><div>25</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>c</span><span>, (</span><span>a</span><span>: </span><span>1</span><span>))</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>// read: text by default, bytes with encoding: none.</span></div></div><div><div><div>28</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>read</span><span>(</span><span>"data/hello.bin"</span><span>), </span><span>"hello"</span><span>)</span></div></div><div><div><div>29</div></div><div><span>#let</span><span> </span><span>b</span><span><span> </span><span>= </span></span><span>read</span><span>(</span><span>"data/hello.bin"</span><span>, </span><span>encoding</span><span>: </span><span>none</span><span>)</span></div></div><div><div><div>30</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>b</span><span>), </span><span>bytes</span><span>)</span></div></div><div><div><div>31</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>b</span><span>.</span><span>len</span><span>(), </span><span>5</span><span>)</span></div></div><div><div><div>32</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>array</span><span>(</span><span>b</span><span>).</span><span>first</span><span>(), </span><span>104</span><span>)</span></div></div><div><div><div>33</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>str</span><span>(</span><span>b</span><span>), </span><span>"hello"</span><span>)</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>=</span><span> Loaded data</span></div></div><div><div><div>36</div></div><div><span>- yaml: </span><span>#y</span><span>.</span><span>course</span><span>, </span><span>#y</span><span>.</span><span>students</span><span>.</span><span>len</span><span>() students</span></div></div><div><div><div>37</div></div><div><span>- csv: </span><span>#recs</span><span>.</span><span>map</span><span>(</span><span>r</span><span> </span><span>=&gt;</span><span> </span><span>r</span><span>.</span><span>name</span><span><span> </span><span>+</span><span> </span></span><span>"="</span><span><span> </span><span>+</span><span> </span></span><span>r</span><span>.</span><span>score</span><span>).</span><span>join</span><span>(</span><span>", "</span><span>)</span></div></div><div><div><div>38</div></div><div><span>- xml code: </span><span>#x</span><span>.</span><span>attrs</span><span>.</span><span>code</span></div></div><div><div><div>39</div></div><div><span>- bytes: </span><span>#b</span><span>.</span><span>len</span><span>() bytes, first byte </span><span>#array</span><span>(</span><span>b</span><span>).</span><span>first</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make ch11</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch11</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch11/datetime.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch11/eval-markup.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch11/fig-cv.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch11/inputs.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch11/loading.typ</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch11/inputs.typ (default and --input name=Typst)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所有 <code>assert.eq</code> 都过了，文件才能编译成功，所以这个文件本身就是一份可执行的说明。几个要点：</p><p><strong>yaml、toml、json 是同一种东西。</strong> 三者都把文档解析成字典和数组，数字就是 <code>int</code> 或 <code>float</code>，字符串就是 <code>str</code>。选哪个看谁来写文件：简历这种嵌套多、有列表的数据，YAML 最省字符；配置风格的键值用 TOML；要和别的程序共用就是 JSON。</p><p><strong>csv 默认丢掉类型。</strong> 所有单元格都是字符串，包括数字。我把 <code>scores.csv</code> 的三种读法用 <code>repr</code> 打了出来（<code>/tmp/p11/rp/t.typ</code>，不在仓库里，数据文件是从 <code>lab/ch11/data/</code> 拷过去的）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>("name", "score"),</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>("张三", "92"),</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>("李四", "85"),</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div><div><div><div>6</div></div><div><span>(</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(name: "张三", score: "92"),</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>(name: "李四", score: "85"),</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一段是默认的数组套数组，表头也是一行数据；第二段是 <code>row-type: dictionary</code>，第一行变成键。要做算术必须自己 <code>int(r.score)</code>，这也是为什么 <code>loading.typ</code> 里断言的是 <code>"85"</code> 而不是 <code>85</code>。</p><p><strong>xml 返回节点树。</strong> 同一个实验里 <code>xml("data/scores.xml")</code> 的 <code>repr</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>namespace: none,</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>tag: "course",</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>attrs: (code: "LA101"),</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>children: (</span></div></div><div><div><div>7</div></div><div><span><span>           </span></span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>              </span></span><span>namespace: none,</span></div></div><div><div><div>9</div></div><div><span><span>              </span></span><span>tag: "student",</span></div></div><div><div><div>10</div></div><div><span><span>              </span></span><span>attrs: (score: "92"),</span></div></div><div><div><div>11</div></div><div><span><span>              </span></span><span>children: ("张三",),</span></div></div><div><div><div>12</div></div><div><span><span>           </span></span><span>),</span></div></div><div><div><div>13</div></div><div><span><span>           </span></span><span>(</span></div></div><div><div><div>14</div></div><div><span><span>              </span></span><span>namespace: none,</span></div></div><div><div><div>15</div></div><div><span><span>              </span></span><span>tag: "student",</span></div></div><div><div><div>16</div></div><div><span><span>              </span></span><span>attrs: (score: "85"),</span></div></div><div><div><div>17</div></div><div><span><span>              </span></span><span>children: ("李四",),</span></div></div><div><div><div>18</div></div><div><span><span>           </span></span><span>),</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>),</span></div></div><div><div><div>21</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>顶层是数组（可以有多个根节点或文本节点），元素是带 <code>tag</code>、<code>attrs</code>、<code>children</code> 的字典，文本直接是字符串。所以 <code>xml(...).first()</code> 才是 <code>&lt;course&gt;</code> 元素。</p><p><strong>cbor 是二进制 JSON。</strong> <code>lab/ch11/data/a.cbor</code> 一共 4 个字节：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ od -An -tx1 lab/ch11/data/a.cbor</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>a1 61 61 01</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>a1</code> 是“含 1 个键值对的 map”，<code>61 61</code> 是长度 1 的字符串 <code>"a"</code>，<code>01</code> 是整数 1。<code>cbor("data/a.cbor")</code> 读出来就是 <code>(a: 1)</code>。</p><p><strong><code>read</code> 是所有加载器的底层。</strong> 默认把文件当 UTF-8 文本读成 <code>str</code>；加 <code>encoding: none</code> 就返回 <code>bytes</code>。<code>bytes</code> 能 <code>.len()</code>，能 <code>array(b)</code> 变成整数数组（<code>h</code> 是 104），也能 <code>str(b)</code> 解回字符串。第 15 篇的 WebAssembly 插件，输入输出全是 <code>bytes</code>。如果文件不是合法 UTF-8，不加 <code>encoding: none</code> 会直接报错，我拿两个字节 <code>\xff\xfe</code> 试了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: failed to convert to string (file is not valid UTF-8 in bin.dat:1:1)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ r.typ:1:6</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #read("bin.dat")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│       ^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>路径相对谁，能走多远<a href="#路径相对谁能走多远"><span>#</span></a></h3><p><code>loading.typ</code> 开头那句注释“paths are relative to this file”值得拆开。相对路径是相对<strong>调用加载函数的那个 <code>.typ</code> 文件</strong>，不是相对你运行 <code>typst</code> 的目录。<code>main.typ</code> 里写 <code>yaml("cv.yaml")</code> 能成功，是因为 <code>cv.yaml</code> 和 <code>main.typ</code> 在同一个目录；<code>lab/ch11/fig-cv.typ</code> 要借同一份数据，就得写 <code>yaml("../../examples/cv/cv.yaml")</code>。</p><p>以斜杠开头的路径（<code>/examples/cv/cv.yaml</code>）相对<strong>项目根</strong>，根由 <code>--root</code> 决定，默认是入口文件所在目录。这就是为什么仓库的所有命令都带 <code>--root .</code>：<code>fig-cv.typ</code> 在 <code>lab/ch11/</code> 下，要读到 <code>examples/</code>，根必须是仓库顶层。越出根目录的路径是被拒绝的，不是被忽略的，下面 <code>eval</code> 一节我会实测一次。</p></section><section><h3>YAML 解析出来的类型<a href="#yaml-解析出来的类型"><span>#</span></a></h3><p>写 <code>cv.yaml</code> 时我最担心的是 YAML 的隐式类型：<code>2021-09</code> 会不会变成日期，<code>no</code> 会不会变成 <code>false</code>。我试了一下（<code>/tmp/p11/y/</code>，不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>a</span><span>: </span><span>2021-09</span></div></div><div><div><div>2</div></div><div><span>b</span><span>: </span><span>2021-09-01</span></div></div><div><div><div>3</div></div><div><span>c</span><span>: </span><span>no</span></div></div><div><div><div>4</div></div><div><span>d</span><span>: </span><span>1.0</span></div></div><div><div><div>5</div></div><div><span>e</span><span>: </span><span>"2021-09"</span></div></div><div><div><div>6</div></div><div><span>f</span><span>: </span><span>present</span></div></div><div><div><div>7</div></div><div><span>g</span><span>: </span><span>0123</span></div></div><div><div><div>8</div></div><div><span>h</span><span>: </span><span>null</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>d</span><span><span> </span><span>= </span></span><span>yaml</span><span>(</span><span>"d.yaml"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#for</span><span> (</span><span>k</span><span>, </span><span>v</span><span>) </span><span>in</span><span> </span><span>d</span><span> [</span></div></div><div><div><div>3</div></div><div><span>  </span><span>#k</span><span>: </span><span>#repr</span><span>(</span><span>v</span><span>) (</span><span>#type</span><span>(</span><span>v</span><span>)) </span><span>\</span></div></div><div><div><div>4</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>a: "2021-09" (str)</span></div></div><div><div><div>2</div></div><div><span>b: "2021-09-01" (str)</span></div></div><div><div><div>3</div></div><div><span>c: "no" (str)</span></div></div><div><div><div>4</div></div><div><span>d: 1.0 (float)</span></div></div><div><div><div>5</div></div><div><span>e: "2021-09" (str)</span></div></div><div><div><div>6</div></div><div><span>f: "present" (str)</span></div></div><div><div><div>7</div></div><div><span>g: "0123" (str)</span></div></div><div><div><div>8</div></div><div><span>h: none (type(none))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 Typst 0.15.1 里，不带引号的日期、<code>no</code>、<code>0123</code> 都是字符串，<code>1.0</code> 是 float，<code>null</code> 是 <code>none</code>。所以 <code>cv.yaml</code> 里 <code>start: "2021-09"</code> 的引号对 Typst 来说不是必须的。我还是留着：这份文件如果换成别的 YAML 工具读，规则可能不同（这一点我没有验证，只是不想让数据对解析器敏感）。</p></section></section>
<section><h2>sys.inputs：从命令行到文档的唯一通道<a href="#sysinputs从命令行到文档的唯一通道"><span>#</span></a></h2><p><code>--input lang=en</code> 做的事极其朴素：命令行上每个 <code>key=value</code> 进入字典 <code>sys.inputs</code>，键和值都是<strong>字符串</strong>。<code>lab/ch11/inputs.typ</code> 把它缩到最小（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch11/inputs.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Pass values from the shell: typst compile --input name=Typst inputs.typ</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>name</span><span><span> </span><span>= </span></span><span>sys</span><span>.</span><span>inputs</span><span>.</span><span>at</span><span>(</span><span>"name"</span><span>, </span><span>default</span><span>: </span><span>"world"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>// Inputs are always strings.</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>name</span><span>), </span><span>str</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Hello, </span><span>#name</span><span>!</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#if</span><span> </span><span>name</span><span><span> </span><span>==</span><span> </span></span><span>"Typst"</span><span> [The flag was set.] </span><span>else</span><span> [Using the default.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>chapter 脚本的 <code>lab/ch11/extra.sh</code> 把它编译了两次，一次不带参数，一次带 <code>--input name=Typst</code>。我手工跑一遍并读出文字：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts --input name=Typst lab/ch11/inputs.typ /tmp/p11/i.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext /tmp/p11/i.pdf -</span></div></div><div><div><div>3</div></div><div><span>Hello, Typst!</span></div></div><div><div><div>4</div></div><div><span>The flag was set.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不带参数时走 <code>default: "world"</code>，输出 <code>Hello, world!</code> 和 <code>Using the default.</code>（<code>make ch11</code> 编译过这一份，我没有单独读它的文字）。</p><p>关于 <code>sys.inputs</code> 有三个细节：</p><ol>
<li><strong>一定用 <code>.at(key, default: ...)</code>。</strong> 直接 <code>sys.inputs.lang</code> 在没传参时会因为键不存在而报错，文档就编译不出来。带 <code>default</code> 的写法让“不传参数”成为合法状态，<code>make examples</code> 里中文版根本不带 <code>--input</code>。</li>
<li><strong>值是字符串。</strong> 想传数字要自己 <code>int(sys.inputs.n)</code>，想传布尔要写成 <code>sys.inputs.at("solutions") == "true"</code>。第 12 篇的作业模板 <code>--input solutions=false</code> 就是这么判断的。</li>
<li><strong>格式有要求。</strong> <code>--input lang</code> 没有等号，命令行解析器直接拒绝：</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: invalid value 'lang' for '--input &lt;key=value&gt;': input must be a key and a value separated by an equal sign</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>值里再出现等号没问题，<code>--input lang=a=b</code> 会得到键 <code>lang</code>、值 <code>a=b</code>（我跑了，<code>sys.inputs.lang</code> 输出 <code>a=b</code>）。</p><section><h3>给 lang 加一道检查<a href="#给-lang-加一道检查"><span>#</span></a></h3><p><code>main.typ</code> 把 <code>sys.inputs</code> 里的字符串原样交给 <code>cv(lang:)</code>，所以拼错的语言也会原样进来。<code>cv</code> 的第一行就是对它的检查：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>assert(lang in ("zh", "en"), message: "xuan: cv lang must be zh or en")</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（它是 <code>cv</code> 函数体里的第一行，完整上下文在下面的 <code>cv.typ</code> 全文里。）我试了 <code>--input lang=fr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts --input lang=fr examples/cv/main.typ /tmp/p11/fr.pdf</span></div></div><div><div><div>2</div></div><div><span>error: assertion failed: xuan: cv lang must be zh or en</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>┌─ @local/xuan:0.1.0/src/cv.typ:61:2</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>61 │   assert(lang in ("zh", "en"), message: "xuan: cv lang must be zh or en")</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>│   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>while calling `cv` at examples/cv/main.typ:5:1</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>cv(yaml("cv.yaml"), lang: lang)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>错误直接说出了原因，并且指向调用 <code>cv</code> 的那一行。（这一行是后来加的：加之前，同一条命令报的是 <code>cannot add dictionary and string</code>，指到 <code>set document</code> 那一行，因为 <code>tr</code> 在字典里找不到键 <code>fr</code> 时把整个 <code>{zh:, en:}</code> 字典原样返回，后面再拿它做字符串加法才炸。）</p></section></section>
<section><h2>eval：把字符串变成 content<a href="#eval把字符串变成-content"><span>#</span></a></h2><p><code>cv.yaml</code> 里有这样的字符串：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># examples/cv/cv.yaml, line 21 (education) and line 29 (experience)</span></div></div><div><div><div>2</div></div><div><span>      </span><span>zh</span><span>: </span><span>"研究方向：#emph[程序分析]与排版系统。"</span></div></div><div><div><div>3</div></div><div><span><span>      </span></span><span>- </span><span>zh</span><span>: </span><span>"用 Rust 重写日志解析服务，吞吐提升 *3 倍*。"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>YAML 解析器只会给你字符串，<code>#emph[...]</code> 和 <code>*3 倍*</code> 在 Typst 眼里是普通字符。要让它们生效，需要 <code>eval</code>：把字符串当 Typst 源码再编译一遍。<code>lab/ch11/eval-markup.typ</code> 展示了它的三种模式（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch11/eval-markup.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// eval turns a string into content (markup), a value (code) or math.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>src</span><span><span> </span><span>= </span></span><span>"*bold* and _emph_ with $x^2$"</span></div></div><div><div><div>3</div></div><div><span>#eval</span><span>(</span><span>src</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// mode: "code" returns values, not content.</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>eval</span><span>(</span><span>"1 + 2 * 3"</span><span>, </span><span>mode</span><span>: </span><span>"code"</span><span>), </span><span>7</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>eval</span><span>(</span><span>"(1, 2, 3).map(x =&gt; x * x)"</span><span>), (</span><span>1</span><span>, </span><span>4</span><span>, </span><span>9</span><span>))</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// mode: "math" parses math syntax.</span></div></div><div><div><div>10</div></div><div><span>#eval</span><span>(</span><span>"a^2 + b^2 = c^2"</span><span>, </span><span>mode</span><span>: </span><span>"math"</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// Extra names go in `scope`.</span></div></div><div><div><div>13</div></div><div><span>#eval</span><span>(</span><span>"[#who wrote this.]"</span><span>, </span><span>mode</span><span>: </span><span>"code"</span><span>, </span><span>scope</span><span>: (</span><span>who</span><span>: </span><span>"I"</span><span>))</span></div></div><div><div><div>14</div></div><div><span>#eval</span><span>(</span><span>"#who wrote this."</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>, </span><span>scope</span><span>: (</span><span>who</span><span>: </span><span>"You"</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>mode</code> 默认是 <code>"code"</code>，和你在 <code>#</code> 后面写的是同一种语法，返回的是<strong>值</strong>；<code>"markup"</code> 返回 content，和你在方括号里写的一样；<code>"math"</code> 是公式模式。<code>cv</code> 用的是 markup：简历里的每个字符串本质上是一小段正文，<code>#emph[...]</code> 要带 <code>#</code> 才是调用，<code>*...*</code> 直接就是加粗，这和你在 <code>.typ</code> 文件正文里写的规则一模一样。</p><section><h3>它能看到哪些名字<a href="#它能看到哪些名字"><span>#</span></a></h3><p>我最想弄清的是作用域：<code>eval</code> 里能引用我模板里的变量吗？能调用 <code>cv</code> 吗？下面是六个最小实验，放在 <code>/tmp/p11/sc/</code>（不在仓库里），每个文件一行 <code>eval</code>，用同一个脚本编译并打印第一行错误或输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>secret</span><span><span> </span><span>= </span></span><span>1</span></div></div><div><div><div>2</div></div><div><span>#eval</span><span>(</span><span>"#secret"</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: </span><span>cv</span></div></div><div><div><div>2</div></div><div><span>#eval</span><span>(</span><span>"#cv"</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#eval</span><span>(</span><span>"#emph[x] #calc.pow(2, 3) #sys.inputs.len()"</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>secret</span><span><span> </span><span>= </span></span><span>1</span></div></div><div><div><div>2</div></div><div><span>#eval</span><span>(</span><span>"#secret"</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>, </span><span>scope</span><span>: (</span><span>secret</span><span>: </span><span>secret</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#eval</span><span>(</span><span>"#read(</span><span>\"</span><span>s1.typ</span><span>\"</span><span>)"</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#eval</span><span>(</span><span>"#read(</span><span>\"</span><span>../../../../etc/passwd</span><span>\"</span><span>)"</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>脚本是这样的：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> </span><span>f</span><span> </span><span>in</span><span> </span><span>s1</span><span> </span><span>s2</span><span> </span><span>s3</span><span> </span><span>s4</span><span> </span><span>s5</span><span> </span><span>s6</span><span>; </span><span>do</span></div></div><div><div><div>2</div></div><div><span>  </span><span>echo</span><span> </span><span>"== </span><span>$f</span><span>"</span></div></div><div><div><div>3</div></div><div><span>  </span><span>if</span><span> </span><span>typst</span><span> </span><span>compile</span><span> </span><span>--root</span><span> </span><span>.</span><span> </span><span>--ignore-system-fonts</span><span> </span><span>$f</span><span>.typ</span><span> </span><span>$f</span><span>.pdf</span><span> 2&gt; </span><span>$f</span><span>.err</span><span>; </span><span>then</span></div></div><div><div><div>4</div></div><div><span>    </span><span>echo</span><span> </span><span>"ok: $(</span><span>pdftotext</span><span> </span><span>$f</span><span>.pdf - </span><span>|</span><span> </span><span>head</span><span> </span><span>-c</span><span> </span><span>60</span><span> </span><span>|</span><span> </span><span>tr</span><span> '\n' ' ')"</span></div></div><div><div><div>5</div></div><div><span>  </span><span>else</span></div></div><div><div><div>6</div></div><div><span>    </span><span>head</span><span> </span><span>-1</span><span> </span><span>$f</span><span>.err</span></div></div><div><div><div>7</div></div><div><span>  </span><span>fi</span></div></div><div><div><div>8</div></div><div><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>== s1</span></div></div><div><div><div>2</div></div><div><span>error: unknown variable: secret</span></div></div><div><div><div>3</div></div><div><span>== s2</span></div></div><div><div><div>4</div></div><div><span>error: unknown variable: cv</span></div></div><div><div><div>5</div></div><div><span>== s3</span></div></div><div><div><div>6</div></div><div><span>ok: x80</span></div></div><div><div><div>7</div></div><div><span>== s4</span></div></div><div><div><div>8</div></div><div><span>ok: 1</span></div></div><div><div><div>9</div></div><div><span>== s5</span></div></div><div><div><div>10</div></div><div><span>ok: #let secret = 1 #eval("#secret", mode: "markup")</span></div></div><div><div><div>11</div></div><div><span>== s6</span></div></div><div><div><div>12</div></div><div><span>error: path `"../../../../etc/passwd"` would escape the project root</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结论：</p><ul>
<li><strong><code>eval</code> 里是一张干净的作用域，只有标准库。</strong> 你文件里 <code>#let</code> 的变量（s1）和 <code>#import</code> 进来的名字（s2）都不可见；<code>emph</code>、<code>calc.pow</code>、<code>sys.inputs</code> 这些标准库的东西可见（s3，输出 <code>x</code>、<code>8</code>、<code>0</code>：没传 <code>--input</code>，<code>sys.inputs.len()</code> 是 0）。</li>
<li><strong>要让数据看到你的东西，用 <code>scope:</code> 显式递过去。</strong> s4 里 <code>secret</code> 通过 <code>scope: (secret: secret)</code> 才可见。这也解释了为什么 <code>cv.typ</code> 里的 <code>rich</code> 只用 <code>eval(v, mode: "markup")</code> 就够了：简历字符串里只会出现标准库函数（<code>emph</code>、<code>link</code>、数学式），不需要引用模板里的任何名字。</li>
<li><strong><code>eval</code> 里的文件读取仍然可用。</strong> s5 里 <code>read("s1.typ")</code> 读到了文件内容。路径相对<strong>调用 <code>eval</code> 的那个文件</strong>，这一点我也验证了：把 <code>eval</code> 放在 <code>sub/m.typ</code> 里的函数中，主文件调用它去读 <code>h.txt</code>，读到的是 <code>sub/h.txt</code>（内容 <code>in-sub</code>），不是根目录那份 <code>h.txt</code>（内容 <code>in-root</code>）。</li>
<li><strong>越出项目根的路径被拒绝。</strong> s6 明确报 <code>would escape the project root</code>，和 <code>--root</code> 的行为是同一套。</li>
</ul></section><section><h3>风险：别 eval 不是你写的字符串<a href="#风险别-eval-不是你写的字符串"><span>#</span></a></h3><p>由此产生的结论很直接：<code>eval</code> 的字符串等同于你文档里的源码，权限也等同。s5 说明它能读项目根下的任何文件，读出来的内容可以直接排进 PDF；<code>#import</code>、<code>#read</code>、<code>yaml(...)</code> 都是可用的。如果 YAML 来自别人（比如网页表单收集的简历数据，再由服务端调 <code>typst compile</code>），<strong>不要对它 <code>eval</code></strong>，要么把字段当纯文本直接 <code>[#field]</code>，要么限制成一个小的、自己写的标记子集。我实测的只有上面几条（读根内文件、不能越根）；至于它能不能被拿来拖死编译，我没有试。顺带一个实测的小保护：<code>eval("#while true {}", mode: "markup")</code> 会被编译器在静态阶段拒绝（<code>error: condition is always true</code>），但这只拦得住最明显的写法，不要把它当防线。</p><p>我在 <code>cv.typ</code> 里仍然用了 <code>eval</code>，是因为 <code>cv.yaml</code> 是我自己写的、放在自己的仓库里，信任边界和 <code>.typ</code> 文件一样。</p><p>还有一个很实际的体验：YAML 里的标记写错了，错误指向的是 <code>eval</code> 的那一行而不是 YAML 里的那一行。我把 <code>bullet: "提升 *3 倍。"</code>（星号没闭合）放进 <code>bad.yaml</code> 再 <code>eval(d.bullet, mode: "markup")</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unclosed delimiter</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ bad.typ:2:6</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #eval(d.bullet, mode: "markup")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│       ^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以 <code>cv.yaml</code> 变长以后，出错时要靠 <code>unclosed delimiter</code> 这类描述自己去 YAML 里找。不 <code>eval</code> 的话（只是 <code>#d.bullet</code> 原样显示）同一个字符串编译完全没事。</p></section></section>
<section><h2>datetime 与 duration<a href="#datetime-与-duration"><span>#</span></a></h2><p>简历里的日期是 <code>"2021-09"</code> 这样的字符串，显示成 <code>2021.09</code>，进行中的显示 <code>至今</code>。这里为什么不直接把字符串拼起来？因为一旦用 <code>datetime</code> 表示，日期的格式、先后比较、差值就都能交给 Typst。<code>lab/ch11/datetime.typ</code> 把相关用法过了一遍（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch11/datetime.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// datetime and duration: construction, display, arithmetic.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>d</span><span><span> </span><span>= </span></span><span>datetime</span><span>(</span><span>year</span><span>: </span><span>2026</span><span>, </span><span>month</span><span>: </span><span>11</span><span>, </span><span>day</span><span>: </span><span>2</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>d</span><span>.</span><span>weekday</span><span>(), </span><span>1</span><span>) </span><span>// Monday</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>d</span><span>.</span><span>ordinal</span><span>(), </span><span>306</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>d</span><span>.</span><span>display</span><span>(</span><span>"[year]-[month]-[day]"</span><span>), </span><span>"2026-11-02"</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// Dates subtract into durations; durations add back.</span></div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>span</span><span><span> </span><span>= </span></span><span>datetime</span><span>(</span><span>year</span><span>: </span><span>2026</span><span>, </span><span>month</span><span>: </span><span>12</span><span>, </span><span>day</span><span>: </span><span>25</span><span><span>) </span><span>-</span><span> </span></span><span>d</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>span</span><span>.</span><span>days</span><span>(), </span><span>53</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>d</span><span><span> </span><span>+</span><span> </span></span><span>duration</span><span>(</span><span>days</span><span>: </span><span>30</span><span>)).</span><span>month</span><span>(), </span><span>12</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// Time-only values and offsets in display strings.</span></div></div><div><div><div>13</div></div><div><span>#let</span><span> </span><span>t</span><span><span> </span><span>= </span></span><span>datetime</span><span>(</span><span>hour</span><span>: </span><span>9</span><span>, </span><span>minute</span><span>: </span><span>5</span><span>, </span><span>second</span><span>: </span><span>0</span><span>)</span></div></div><div><div><div>14</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>t</span><span>.</span><span>display</span><span>(</span><span>"[hour]:[minute]"</span><span>), </span><span>"09:05"</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>// datetime.today() changes daily: never assert on it.</span></div></div><div><div><div>17</div></div><div><span>=</span><span> Dates</span></div></div><div><div><div>18</div></div><div><span>- due: </span><span>#d</span><span>.</span><span>display</span><span>(</span><span>"[year] 年 [month padding:none] 月 [day padding:none] 日"</span><span>)</span></div></div><div><div><div>19</div></div><div><span>- days until Christmas: </span><span>#span</span><span>.</span><span>days</span><span>()</span></div></div><div><div><div>20</div></div><div><span>- today has year &gt;= 2026: </span><span>#</span><span>(</span><span>datetime</span><span>.</span><span>today</span><span>().</span><span>year</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>2026</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这个文件的输出（<code>pdftotext</code> 读出）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Dates</span></div></div><div><div><div>2</div></div><div><span>• due: 2026 年 11 月 2 日</span></div></div><div><div><div>3</div></div><div><span>• days until Christmas: 53</span></div></div><div><div><div>4</div></div><div><span>• today has year &gt;= 2026: true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>构造。</strong> <code>datetime(year:, month:, day:)</code> 是日期，<code>datetime(hour:, minute:, second:)</code> 是时刻，两组都给就是日期时间。日期的星期（<code>weekday()</code>，周一为 1）、一年中第几天（<code>ordinal()</code>）是方法。</li>
<li><strong>显示。</strong> <code>display</code> 接受方括号格式串。<code>[month]</code> 默认补零成两位，这就是 <code>2021.09</code> 的来源；<code>[month padding:none]</code> 去掉补零，9 月显示成 <code>9</code> 而不是 <code>09</code>（我用 <code>datetime(year: 2026, month: 9, day: 3)</code> 试过）。<code>page.typ</code> 里 <code>title-block</code> 的中文日期用的就是上面 <code>due</code> 那一行的格式串。</li>
<li><strong>算术。</strong> 两个 <code>datetime</code> 相减得到 <code>duration</code>，<code>duration</code> 有 <code>.days()</code>、<code>.hours()</code>、<code>.weeks()</code> 等方法；<code>duration(days: 30)</code> 加到日期上得到新日期。上面 2026-11-02 到 2026-12-25 是 53 天。<code>duration</code> 能按周、日、时等构造，没有“月”：我试了 <code>duration(months: 1)</code>，Typst 报 <code>unexpected argument: months</code>（我猜是因为月的长度不固定）。<code>cv</code> 里的 <code>2021.09–2025.06</code> 因此是两个日期分别格式化再拼，不是算差值。</li>
<li><strong><code>datetime.today()</code>。</strong> 每天都变，所以只能展示不能断言。顺带一个坑：仓库的 <code>flake.nix</code> 在 <code>nix develop</code> 的 shell 钩子里执行了 <code>unset SOURCE_DATE_EPOCH</code>，原因是否则 nix shell 里的 <code>datetime.today()</code> 会是 1980-01-01（这个现象我这次没有重新验证）。</li>
</ul></section>
<section><h2>src/cv.typ 全文<a href="#srccvtyp-全文"><span>#</span></a></h2><p>113 行，我分四段贴，段与段首尾相接，合起来就是 <code>src/cv.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/cv.typ" target="_blank">源文件</a>）。</p><section><h3>两个字段解析函数<a href="#两个字段解析函数"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// One-page CV generated from a dictionary (see examples/cv/cv.yaml).</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"cjk.typ"</span><span>: </span><span>xuan-cjk</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"fonts.typ"</span><span>: </span><span>fonts</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// Resolve a field: plain value, or (zh: ..., en: ...) by language.</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>tr</span><span>(</span><span>field</span><span>, </span><span>lang</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>if</span><span> </span><span>type</span><span>(</span><span>field</span><span><span>) </span><span>==</span><span> </span></span><span>dictionary</span><span> </span><span>and</span><span> (</span><span>lang</span><span> </span><span>in</span><span> </span><span>field</span><span>) { </span><span>field</span><span>.</span><span>at</span><span>(</span><span>lang</span><span>) } </span><span>else</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>field</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// Strings may carry markup, e.g. "*bold* and #link(...)".</span></div></div><div><div><div>13</div></div><div><span>#let</span><span> </span><span>rich</span><span>(</span><span>field</span><span>, </span><span>lang</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>14</div></div><div><span>  </span><span>let</span><span> </span><span>v</span><span><span> </span><span>= </span></span><span>tr</span><span>(</span><span>field</span><span>, </span><span>lang</span><span>)</span></div></div><div><div><div>15</div></div><div><span>  </span><span>if</span><span> </span><span>type</span><span>(</span><span>v</span><span><span>) </span><span>==</span><span> </span></span><span>str</span><span> { </span><span>eval</span><span>(</span><span>v</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>) } </span><span>else</span><span> { </span><span>v</span><span> }</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>tr</code> 做的是“按语言取值”：如果字段是字典并且含有当前语言这个键，取出来；否则原样返回。<code>lang in field</code> 对字典是测试<strong>键</strong>。这两个条件合在一起，让纯字符串（如 <code>email</code>）和 <code>{zh:, en:}</code> 都走同一个调用。字典里没有当前语言时它会原样返回整个字典，所以 <code>cv</code> 才需要在入口先 <code>assert</code> 语言合法。</p><p><code>rich</code> 在 <code>tr</code> 之后再判一次类型：只有字符串才 <code>eval</code>，已经是 content 的（比如 <code>link(...)</code>）就不动。这条类型判断保证 <code>rich</code> 对任何字段都安全。模块里的 <code>tr</code> 和第 8 篇 <code>math.typ</code> 导出的 <code>tr</code>（矩阵迹）重名，但 <code>lib.typ</code> 只导出 <code>cv</code>，所以不冲突。</p></section><section><h3>日期与版式小函数<a href="#日期与版式小函数"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// "2023-09" -&gt; datetime; "present" -&gt; none.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>parse-month</span><span>(</span><span>s</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>if</span><span> </span><span>s</span><span><span> </span><span>==</span><span> </span></span><span>"present"</span><span> { </span><span>return</span><span> </span><span>none</span><span> }</span></div></div><div><div><div>4</div></div><div><span>  </span><span>let</span><span> (</span><span>y</span><span>, </span><span>m</span><span><span>) </span><span>= </span></span><span>s</span><span>.</span><span>split</span><span>(</span><span>"-"</span><span>).</span><span>map</span><span>(</span><span>int</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>datetime</span><span>(</span><span>year</span><span>: </span><span>y</span><span>, </span><span>month</span><span>: </span><span>m</span><span>, </span><span>day</span><span>: </span><span>1</span><span>)</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>fmt-month</span><span>(</span><span>s</span><span>, </span><span>lang</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>9</div></div><div><span>  </span><span>let</span><span> </span><span>d</span><span><span> </span><span>= </span></span><span>parse-month</span><span>(</span><span>s</span><span>)</span></div></div><div><div><div>10</div></div><div><span>  </span><span>if</span><span> </span><span>d</span><span><span> </span><span>==</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> { </span><span>"至今"</span><span> } </span><span>else</span><span> { </span><span>"Present"</span><span> }</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>d</span><span>.</span><span>display</span><span>(</span><span>"[year].[month]"</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>#let</span><span> </span><span>date-</span><span>range</span><span>(</span><span>item</span><span>, </span><span>lang</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>18</div></div><div><span>  </span><span>fmt-month</span><span>(</span><span>item</span><span>.</span><span>start</span><span>, </span><span>lang</span><span><span>) </span><span>+</span><span> </span></span><span>"–"</span><span><span> </span><span>+</span><span> </span></span><span>fmt-month</span><span>(</span><span>item</span><span>.</span><span>end</span><span>, </span><span>lang</span><span>)</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#let</span><span> </span><span>section</span><span>(</span><span>title</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>22</div></div><div><span>  </span><span>v</span><span>(</span><span>0.6em</span><span>)</span></div></div><div><div><div>23</div></div><div><span>  </span><span>text</span><span>(</span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>size</span><span>: </span><span>1.1em</span><span>, </span><span>title</span><span>)</span></div></div><div><div><div>24</div></div><div><span>  </span><span>v</span><span><span>(</span><span>-</span></span><span>0.5em</span><span>)</span></div></div><div><div><div>25</div></div><div><span>  </span><span>line</span><span>(</span><span>length</span><span>: </span><span>100%</span><span>, </span><span>stroke</span><span>: </span><span>0.5pt</span><span>)</span></div></div><div><div><div>26</div></div><div><span>  </span><span>v</span><span><span>(</span><span>-</span></span><span>0.2em</span><span>)</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>// Left: title and role; right: dates.</span></div></div><div><div><div>30</div></div><div><span>#let</span><span> </span><span>entry</span><span>(</span><span>left</span><span>, </span><span>right</span><span>, </span><span>body</span><span>: </span><span>none</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>31</div></div><div><span>  </span><span>grid</span><span>(</span></div></div><div><div><div>32</div></div><div><span>    </span><span>columns</span><span>: (</span><span>1fr</span><span>, </span><span>auto</span><span>),</span></div></div><div><div><div>33</div></div><div><span>    </span><span>left</span><span>, </span><span>right</span><span>,</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span>  </span><span>if</span><span> </span><span>body</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { </span><span>body</span><span> }</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>parse-month</code> 是这一篇最典型的“把字符串提升成类型”：<code>s.split("-").map(int)</code> 得到 <code>(2021, 9)</code>，<code>let (y, m) = ...</code> 解构（第 2 篇），<code>day: 1</code> 是因为 <code>datetime</code> 需要一个完整的日期，简历只关心年月。<code>"present"</code> 直接返回 <code>none</code>，<code>fmt-month</code> 再据此显示 <code>至今</code> / <code>Present</code>。把 <code>"present"</code> 约定成一个关键字而不是写 <code>至今</code>，让 YAML 里的这个字段也不必分语言。</p><p><code>section</code> 里两个负的 <code>v(...)</code> 是把标题和横线收紧，具体数值是仓库里现成的取值，我没有逐个调整来验证它们各自的效果。<code>entry</code> 是两栏 <code>grid</code>，<code>1fr</code> 给标题，<code>auto</code> 给日期，日期因此总是贴右；<code>body: none</code> 的默认值加上 <code>if body != none { body }</code> 让没有正文的条目不多出空白。</p></section><section><h3>bullets 与 set 的作用域<a href="#bullets-与-set-的作用域"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>bullets</span><span>(</span><span>items</span><span>, </span><span>lang</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>, </span><span>justify</span><span>: </span><span>false</span><span>)</span></div></div><div><div><div>3</div></div><div><span>  </span><span>list</span><span>(..</span><span>items</span><span>.</span><span>map</span><span>(</span><span>b</span><span> </span><span>=&gt;</span><span> </span><span>rich</span><span>(</span><span>b</span><span>, </span><span>lang</span><span>)))</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>bullets</code> 里的 <code>set par(...)</code> 只在这个函数体内有效（第 4 篇：作用域是代码块的边界）。<code>xuan-cjk</code> 给中文段落设了两字缩进和两端对齐，对列表项都不合适，所以在这里局部关掉。<code>list(..items.map(...))</code> 把数组展开成 <code>list</code> 的位置参数，每个元素就是一条列表项，这是第 3 篇 <code>..args</code> 的反向用法（调用端展开）。</p></section><section><h3>cv 本体<a href="#cv-本体"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>cv</span><span>(</span><span>data</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>assert</span><span>(</span><span>lang</span><span> </span><span>in</span><span> (</span><span>"zh"</span><span>, </span><span>"en"</span><span>), </span><span>message</span><span>: </span><span>"xuan: cv lang must be zh or en"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>  </span><span>let</span><span> </span><span>t</span><span>(</span><span>field</span><span><span>) </span><span>= </span></span><span>tr</span><span>(</span><span>field</span><span>, </span><span>lang</span><span>)</span></div></div><div><div><div>4</div></div><div><span>  </span><span>let</span><span> </span><span>zh</span><span><span> </span><span>= </span></span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span></div></div><div><div><div>5</div></div><div><span>  </span><span>set</span><span> </span><span>document</span><span>(</span><span>title</span><span>: </span><span>t</span><span>(</span><span>data</span><span>.</span><span>name</span><span><span>) </span><span>+</span><span> </span></span><span>" CV"</span><span>, </span><span>author</span><span>: </span><span>t</span><span>(</span><span>data</span><span>.</span><span>name</span><span>))</span></div></div><div><div><div>6</div></div><div><span>  </span><span>set</span><span> </span><span>page</span><span>(</span><span>paper</span><span>: </span><span>"a4"</span><span>, </span><span>margin</span><span>: (</span><span>x</span><span>: </span><span>1.8cm</span><span>, </span><span>y</span><span>: </span><span>1.5cm</span><span>))</span></div></div><div><div><div>7</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-cjk</span><span>.</span><span>with</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>)</span></div></div><div><div><div>8</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>9</div></div><div><span>  </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>, </span><span>leading</span><span>: </span><span>0.6em</span><span>, </span><span>spacing</span><span>: </span><span>0.7em</span><span>)</span></div></div><div><div><div>10</div></div><div><span>  </span><span>set</span><span> </span><span>list</span><span>(</span><span>indent</span><span>: </span><span>0.5em</span><span>, </span><span>spacing</span><span>: </span><span>0.5em</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>  </span><span>align</span><span>(</span><span>center</span><span>)[</span></div></div><div><div><div>13</div></div><div><span>    </span><span>#text</span><span>(</span><span>size</span><span>: </span><span>22pt</span><span>, </span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>sans</span><span>, </span><span>t</span><span>(</span><span>data</span><span>.</span><span>name</span><span>))</span></div></div><div><div><div>14</div></div><div><span>    </span><span>#v</span><span><span>(</span><span>-</span></span><span>0.2em</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>#</span><span>(</span></div></div><div><div><div>16</div></div><div><span>      </span><span>link</span><span>(</span><span>"mailto:"</span><span><span> </span><span>+</span><span> </span></span><span>data</span><span>.</span><span>email</span><span>, </span><span>data</span><span>.</span><span>email</span><span>),</span></div></div><div><div><div>17</div></div><div><span>      </span><span>data</span><span>.</span><span>phone</span><span>,</span></div></div><div><div><div>18</div></div><div><span>      </span><span>link</span><span>(</span><span>"https://"</span><span><span> </span><span>+</span><span> </span></span><span>data</span><span>.</span><span>github</span><span>, </span><span>data</span><span>.</span><span>github</span><span>),</span></div></div><div><div><div>19</div></div><div><span>      </span><span>link</span><span>(</span><span>"https://"</span><span><span> </span><span>+</span><span> </span></span><span>data</span><span>.</span><span>site</span><span>, </span><span>data</span><span>.</span><span>site</span><span>),</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>).</span><span>join</span><span>(</span><span>h</span><span>(</span><span>1em</span><span><span>) </span><span>+</span><span> </span></span><span>"|"</span><span><span> </span><span>+</span><span> </span></span><span>h</span><span>(</span><span>1em</span><span>))</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>  </span><span>section</span><span>(</span><span>if</span><span> </span><span>zh</span><span> { </span><span>"教育经历"</span><span> } </span><span>else</span><span> { </span><span>"Education"</span><span> })</span></div></div><div><div><div>24</div></div><div><span>  </span><span>for</span><span> </span><span>e</span><span> </span><span>in</span><span> </span><span>data</span><span>.</span><span>education</span><span> {</span></div></div><div><div><div>25</div></div><div><span>    </span><span>entry</span><span>(</span></div></div><div><div><div>26</div></div><div><span><span>      </span></span><span>[*</span><span>#t</span><span>(</span><span>e</span><span>.</span><span>school</span><span>)*, </span><span>#t</span><span>(</span><span>e</span><span>.</span><span>degree</span><span>)],</span></div></div><div><div><div>27</div></div><div><span>      </span><span>date-range</span><span>(</span><span>e</span><span>, </span><span>lang</span><span>),</span></div></div><div><div><div>28</div></div><div><span>      </span><span>body</span><span>: </span><span>if</span><span> </span><span>"detail"</span><span> </span><span>in</span><span> </span><span>e</span><span> { </span><span>rich</span><span>(</span><span>e</span><span>.</span><span>detail</span><span>, </span><span>lang</span><span>) },</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>  </span><span>section</span><span>(</span><span>if</span><span> </span><span>zh</span><span> { </span><span>"实习经历"</span><span> } </span><span>else</span><span> { </span><span>"Experience"</span><span> })</span></div></div><div><div><div>33</div></div><div><span>  </span><span>for</span><span> </span><span>e</span><span> </span><span>in</span><span> </span><span>data</span><span>.</span><span>experience</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>entry</span><span>(</span></div></div><div><div><div>35</div></div><div><span><span>      </span></span><span>[*</span><span>#t</span><span>(</span><span>e</span><span>.</span><span>org</span><span>)*, </span><span>#emph</span><span>(</span><span>t</span><span>(</span><span>e</span><span>.</span><span>role</span><span>))],</span></div></div><div><div><div>36</div></div><div><span>      </span><span>date-range</span><span>(</span><span>e</span><span>, </span><span>lang</span><span>),</span></div></div><div><div><div>37</div></div><div><span>      </span><span>body</span><span>: </span><span>bullets</span><span>(</span><span>e</span><span>.</span><span>bullets</span><span>.</span><span>map</span><span>(</span><span>t</span><span>), </span><span>lang</span><span>),</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>  </span><span>section</span><span>(</span><span>if</span><span> </span><span>zh</span><span> { </span><span>"项目经历"</span><span> } </span><span>else</span><span> { </span><span>"Projects"</span><span> })</span></div></div><div><div><div>42</div></div><div><span>  </span><span>for</span><span> </span><span>e</span><span> </span><span>in</span><span> </span><span>data</span><span>.</span><span>projects</span><span> {</span></div></div><div><div><div>43</div></div><div><span>    </span><span>entry</span><span>(</span></div></div><div><div><div>44</div></div><div><span><span>      </span></span><span>[*</span><span>#t</span><span>(</span><span>e</span><span>.</span><span>name</span><span>)*, </span><span>#emph</span><span>(</span><span>t</span><span>(</span><span>e</span><span>.</span><span>role</span><span>))],</span></div></div><div><div><div>45</div></div><div><span>      </span><span>date-range</span><span>(</span><span>e</span><span>, </span><span>lang</span><span>),</span></div></div><div><div><div>46</div></div><div><span>      </span><span>body</span><span>: </span><span>bullets</span><span>(</span><span>e</span><span>.</span><span>bullets</span><span>.</span><span>map</span><span>(</span><span>t</span><span>), </span><span>lang</span><span>),</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>  </span><span>section</span><span>(</span><span>if</span><span> </span><span>zh</span><span> { </span><span>"技能"</span><span> } </span><span>else</span><span> { </span><span>"Skills"</span><span> })</span></div></div><div><div><div>51</div></div><div><span>  </span><span>for</span><span> </span><span>s</span><span> </span><span>in</span><span> </span><span>data</span><span>.</span><span>skills</span><span> {</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>[*</span><span>#t</span><span>(</span><span>s</span><span>.</span><span>label</span><span>)*</span><span>#if</span><span> </span><span>zh</span><span> [：] </span><span>else</span><span> [: ]</span><span>#rich</span><span>(</span><span>s</span><span>.</span><span>items</span><span>, </span><span>lang</span><span>) </span><span>\ </span><span>]</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>54</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个值得说的设计选择：</p><p><strong><code>cv</code> 是一个普通函数调用，不是 <code>show:</code> 模板。</strong> 第 4 篇的 <code>note</code> 接收一个 <code>body</code> 并用 <code>#show: note.with(...)</code> 套在整篇文档上，因为正文是你写的。简历没有正文，输入是一份字典，所以 <code>#cv(yaml("cv.yaml"), lang: lang)</code> 直接生成整页内容。它仍然用 <code>set page</code>、<code>show: xuan-cjk.with(lang: lang)</code>，这些规则落在 <code>cv</code> 的函数体作用域里，调用一次页面就配好一次。</p><p><strong><code>let t(field) = tr(field, lang)</code> 是闭包。</strong> <code>t</code> 捕获了外层的 <code>lang</code>，后面一百多处调用不用再传。这是第 3 篇讲过的闭包捕获；<code>e.bullets.map(t)</code> 里直接把 <code>t</code> 当参数传给 <code>map</code>，也是同一个原因。<code>bullets(...)</code> 之前先把每条 <code>{zh:, en:}</code> 变成字符串，<code>rich</code> 之后再 <code>eval</code>，顺序不能反。</p><p><strong>联系方式只拼字符串，不 <code>eval</code>。</strong> <code>email</code>、<code>phone</code>、<code>github</code>、<code>site</code> 都是普通字符串，直接放进 <code>link(...)</code>。邮箱、电话这类字段没有需要格式化的内容，不让它们过 <code>eval</code> 就少一处可能被标记语法误读的地方（这一点是设计上的保守，我没有构造出具体的误读例子）。<code>.join(h(1em) + "|" + h(1em))</code> 把四个内容用竖线隔开，竖线两侧各 1em 的水平空白。</p><p><strong><code>set document</code> 写入 PDF 元数据。</strong> 我用 <code>pdfinfo out/examples/cv.pdf</code> 看了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Title:           张三 CV</span></div></div><div><div><div>2</div></div><div><span>Author:          张三</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>英文版则是 <code>Zhang San CV</code> 和 <code>Zhang San</code>。第 16 篇再展开 <code>document</code> 的更多字段。</p><p><strong>可选字段用 <code>"detail" in e</code> 判断。</strong> 教育条目有 <code>detail</code>，实习条目没有。字典的 <code>in</code> 测试键，<code>body: if "detail" in e { rich(...) }</code> 在条件不成立时整个 <code>if</code> 得到 <code>none</code>（没有 <code>else</code> 的 <code>if</code> 就是这样），正好是 <code>entry</code> 参数 <code>body</code> 的默认值。</p><p><strong>技能一节的 <code>\</code>。</strong> <code>[*#t(s.label)*#if zh [：] else [: ]#rich(s.items, lang) \ ]</code> 是一行内容块：粗体标签，中文用全角冒号、英文用半角冒号加空格，然后是 <code>rich</code> 出来的条目，最后用 <code>\</code> 强制换行。中英文冒号不同，所以用 <code>if zh</code> 在内容里分叉。</p></section></section>
<section><h2>编译时间与页数<a href="#编译时间与页数"><span>#</span></a></h2><p>页数前面已经给过：中英文各 1 页，A4 尺寸（<code>pdfinfo</code> 显示 595.276 x 841.89 pts）。时间我用 <code>hyperfine -N</code>，让它直接启动命令而不经过 shell，预热 3 次，各跑 30 次：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ hyperfine -N --warmup 3 --runs 30 "typst compile --root . --ignore-system-fonts examples/cv/main.typ /tmp/p11/cv-zh.pdf" "typst compile --root . --ignore-system-fonts --input lang=en examples/cv/main.typ /tmp/p11/cv-en.pdf"</span></div></div><div><div><div>2</div></div><div><span>Benchmark 1: typst compile --root . --ignore-system-fonts examples/cv/main.typ /tmp/p11/cv-zh.pdf</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Time (mean ± σ):     153.8 ms ±   2.7 ms    [User: 56.2 ms, System: 99.9 ms]</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Range (min … max):   149.0 ms … 159.2 ms    30 runs</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Benchmark 2: typst compile --root . --ignore-system-fonts --input lang=en examples/cv/main.typ /tmp/p11/cv-en.pdf</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>Time (mean ± σ):     153.5 ms ±   7.2 ms    [User: 52.4 ms, System: 103.1 ms]</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>Range (min … max):   145.0 ms … 182.7 ms    30 runs</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>Summary</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>typst compile --root . --ignore-system-fonts --input lang=en examples/cv/main.typ /tmp/p11/cv-en.pdf ran</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>1.00 ± 0.05 times faster than typst compile --root . --ignore-system-fonts examples/cv/main.typ /tmp/p11/cv-zh.pdf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>中文版 153.8 ms，英文版 153.5 ms，两者在噪声之内分不开。用户态时间只有 56 ms，系统态却有 100 ms 上下：绝大部分时间不在排版，我猜是在读那几个很大的 CJK 可变字体文件，这没有拆开验证。</p><p>第 1 篇的基准是另一份 9 页文档，用的是另一个 CJK 字体，数字（226.1 ms）不能直接和这里比。</p></section>
<section><h2>往下一步走<a href="#往下一步走"><span>#</span></a></h2><p>这一篇的模式，数据在文件里，版式在函数里，参数从命令行进来，是整个 <code>xuan</code> 里最容易复用的一个。你可以现在就试两件事：把 <code>cv.yaml</code> 里的 <code>start</code>、<code>end</code> 改成别的月份，看日期显示怎么变；或者给 <code>cv.yaml</code> 加一个新的 <code>awards</code> 小节，同时在 <code>cv.typ</code> 里加一个 <code>section</code> 和一个 <code>for</code> 循环，这就是自己动手把简历模板变成你的模板。如果要真的把一份简历发给别人，别忘了把 <code>examples/cv/cv.yaml</code> 里的联系方式换成你自己的。</p><p>下一篇是同一个套路，只是参数换成了“是否显示解答”：<code>--input solutions=false</code> 经 <code>sys.inputs</code> 读进来，存进一个 <code>state</code>，同一份作业源码就能出带解答和不带解答两个 PDF。多文件的 <code>import</code> 和 <code>include</code>、0.15 的 <code>path</code> 类型也在那里：<a href="/posts/typst-by-xuan-12-homework/">第 12 篇</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-10-bibliography/</id>
      <title type="text">参考文献：BibTeX、Hayagriva 与 GB/T 7714</title>
      <published>2026-10-19T09:00:00.000Z</published>
      <updated>2026-10-19T09:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-10-bibliography/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 10 篇：我用 Typst 0.15.1 编译了 ch10 的 6 个 lab 文件，让同一份三条目的数据分别以 BibTeX 和 Hayagriva YAML 进入文档，用 GB/T 7714、IEEE、APA、Chicago 和一个原样拷来的 CSL 文件各排一遍，量出样式的语言环境跟着 text.lang 走而不是跟着条目走，用 0.15 的 target 与 group 在同一篇文档里放了三个参考文献列表，最后把 bibliography 参数接进 xuan 的 note 模板。</summary>
      <content type="html"><![CDATA[<p>下面这张图是 <code>lab/ch10/multi-bib.typ</code> 的渲染结果：同一份三条目的数据，在一页里排出了三个参考文献列表，编号分别是 <code>[1] [2]</code>、<code>[1]</code> 和 <code>[3]</code>，而正文里没有手写过一个编号。</p>
<p></p><figure><img src="/assets/images/typst-by-xuan/ch10-multi-bib.png" alt="multi-bib.typ 的渲染：蓝色左边线的块里有两条引用和它自己的列表，红色左边线的块里再引一次并从 [1] 重新编号，最后是只含一条的总参考文献" /><figcaption>multi-bib.typ 的渲染：蓝色左边线的块里有两条引用和它自己的列表，红色左边线的块里再引一次并从 [1] 重新编号，最后是只含一条的总参考文献</figcaption></figure><p></p>
<p>这一篇我想弄清楚的是这条路径上的每个环节：Typst 里引用一篇文献的写法和引用一个标签完全一样（<code>@strang2023</code>），编译器发现这个标签不在文档里时，会去已声明的参考文献文件里找同名的键。数据文件有哪两种格式，样式文件（CSL）是什么，为什么同一份数据在中文文档里会冒出“6 版”而在英文列表里是“6th ed.”，以及 0.15 新增的 <code>target</code> 与 <code>group</code> 怎么做出上面这张图。最后我会把这些接进 <code>xuan</code> 的 <code>note</code> 模板，让笔记末尾自动长出一份 GB/T 7714 的列表。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇没有新增 <code>src/</code> 文件。<code>src/note.typ</code> 在这一篇变成<strong>最终版</strong>（文中完整展示，48 行），它相对第 4 篇的版本多了 <code>bibliography</code> 参数和末尾 3 行；<code>lib.typ</code> 与 <code>src/base.typ</code> 仍是此前讲过的状态，这一篇不再改。<code>lab/ch10/</code> 下的 6 个 Typst 文件（<code>bib-basic</code>、<code>bib-hayagriva</code>、<code>custom-csl</code> 三个子目录里的 <code>main.typ</code>，加 <code>cite-forms.typ</code>、<code>multi-bib.typ</code>、<code>fig-bib.typ</code>）全部通过 <code>make ch10</code>，同目录还有数据文件 <code>refs.bib</code>、<code>refs.yml</code> 和样式文件 <code>custom-csl/ams-label.csl</code>。
前置：<a href="/posts/typst-by-xuan-09-figures-tables-code/">第 9 篇</a>（图、表、交叉引用，<code>@label</code> 的解析规则）。如果你想了解 <code>.bib</code> 文件本身的字段和条目类型，我之前写过一篇<a href="/posts/bibtex-and-biblatex-guide/">BibTeX 与 BibLaTeX 入门</a>，这里默认你知道 <code>@book{key, ...}</code> 长什么样。</p></div></div>
<section><h2>数据：一份 .bib，三个条目<a href="#数据一份-bib三个条目"><span>#</span></a></h2><p>整个 ch10 共用一份数据。<code>lab/ch10/refs.bib</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/refs.bib" target="_blank">源文件</a>）里有三本书，两本英文教材，一本中文教材：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@book</span><span>{</span><span>strang2023</span><span>,</span></div></div><div><div><div>2</div></div><div><span>  </span><span>author</span><span>    = </span><span>{</span><span>Strang, Gilbert</span><span>}</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>title</span><span>     = </span><span>{</span><span>Introduction to Linear Algebra</span><span>}</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>edition</span><span>   = </span><span>{</span><span>6</span><span>}</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>publisher</span><span> = </span><span>{</span><span>Wellesley-Cambridge Press</span><span>}</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>address</span><span>   = </span><span>{</span><span>Wellesley, MA</span><span>}</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>year</span><span>      = </span><span>{</span><span>2023</span><span>}</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>isbn</span><span>      = </span><span>{</span><span>978-1-7331466-7-8</span><span>}</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>language</span><span>  = </span><span>{</span><span>en</span><span>}</span><span>,</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>@book</span><span>{</span><span>golub2013</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>author</span><span>    = </span><span>{</span><span>Golub, Gene H. and {Van Loan}, Charles F.</span><span>}</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>title</span><span>     = </span><span>{</span><span>Matrix Computations</span><span>}</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>edition</span><span>   = </span><span>{</span><span>4</span><span>}</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>publisher</span><span> = </span><span>{</span><span>Johns Hopkins University Press</span><span>}</span><span>,</span></div></div><div><div><div>17</div></div><div><span>  </span><span>address</span><span>   = </span><span>{</span><span>Baltimore</span><span>}</span><span>,</span></div></div><div><div><div>18</div></div><div><span>  </span><span>year</span><span>      = </span><span>{</span><span>2013</span><span>}</span><span>,</span></div></div><div><div><div>19</div></div><div><span>  </span><span>isbn</span><span>      = </span><span>{</span><span>978-1-4214-0794-4</span><span>}</span><span>,</span></div></div><div><div><div>20</div></div><div><span>  </span><span>language</span><span>  = </span><span>{</span><span>en</span><span>}</span><span>,</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>@book</span><span>{</span><span>tongji2014</span><span>,</span></div></div><div><div><div>24</div></div><div><span>  </span><span>author</span><span>    = </span><span>{</span><span>同济大学数学系</span><span>}</span><span>,</span></div></div><div><div><div>25</div></div><div><span>  </span><span>title</span><span>     = </span><span>{</span><span>工程数学 线性代数</span><span>}</span><span>,</span></div></div><div><div><div>26</div></div><div><span>  </span><span>edition</span><span>   = </span><span>{</span><span>6</span><span>}</span><span>,</span></div></div><div><div><div>27</div></div><div><span>  </span><span>publisher</span><span> = </span><span>{</span><span>高等教育出版社</span><span>}</span><span>,</span></div></div><div><div><div>28</div></div><div><span>  </span><span>address</span><span>   = </span><span>{</span><span>北京</span><span>}</span><span>,</span></div></div><div><div><div>29</div></div><div><span>  </span><span>year</span><span>      = </span><span>{</span><span>2014</span><span>}</span><span>,</span></div></div><div><div><div>30</div></div><div><span>  </span><span>isbn</span><span>      = </span><span>{</span><span>978-7-04-039661-4</span><span>}</span><span>,</span></div></div><div><div><div>31</div></div><div><span>  </span><span>language</span><span>  = </span><span>{</span><span>zh</span><span>}</span><span>,</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>作者 <code>{Van Loan}</code> 外面的花括号是 BibTeX 的老惯例，告诉解析器这是一个整体的姓；稍后会看到它在某个样式里仍然被拆开，先记着。</p></section>
<section><h2>最小用法：<code>@key</code> 加一行 <code>bibliography</code><a href="#最小用法key-加一行-bibliography"><span>#</span></a></h2><p><code>lab/ch10/bib-basic/main.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/bib-basic/main.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// BibTeX: cite with @key, print with `bibliography`.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>=</span><span> 最小二乘</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>正规方程的几何解释见 </span><span>@strang2023</span><span>，数值稳定性见 </span><span>@golub2013</span><span>[Ch.~5]。</span></div></div><div><div><div>9</div></div><div><span>国内教材参见 </span><span>@tongji2014</span><span>。</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#bibliography</span><span>(</span><span>"../refs.bib"</span><span>, </span><span>style</span><span>: </span><span>"gb-7714-2015-numeric"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个机制都在这几行里。</p><p><strong><code>@strang2023</code> 是 <code>ref</code> 的特例。</strong> 第 9 篇讲过 <code>@label</code> 找文档里的标签。找不到时，Typst 去所有 <code>bibliography</code> 声明的文件里按键名找，找到就把它当成一次 <code>cite</code>。所以引用文献不需要任何额外语法，代价是：文档里没有对应的 <code>bibliography</code> 或键写错时，报的是标签找不到。我在 <code>/tmp/p10</code>（这一篇里 <code>/tmp</code> 下的实验都不在仓库里，代码都在文中）里各试了一次：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>)</span></div></div><div><div><div>2</div></div><div><span>见 </span><span>@nokey</span><span> 与 </span><span>@strang2023</span><span>。</span></div></div><div><div><div>3</div></div><div><span>#bibliography</span><span>(</span><span>"refs.bib"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: label `&lt;nokey&gt;` does not exist in the document</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ nokey.typ:2:2</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ 见 @nokey 与 @strang2023。</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│    ^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把最后一行 <code>#bibliography</code> 去掉，报错就变成 <code>@strang2023</code> 找不到。所以看到“label does not exist”，两个方向都要查：键拼错了，或者忘了声明数据文件。</p><p><strong><code>[Ch.~5]</code> 是 supplement。</strong> <code>@key</code> 后面紧跟的方括号是 content，成为引用的补充说明（页码、章节），由样式决定放在哪、怎么标点。这里 <code>~</code> 是不换行空格，保证 <code>Ch.</code> 和 <code>5</code> 不被拆到两行。</p><p><strong><code>style:</code> 选择样式，数据文件和样式是两回事。</strong> <code>gb-7714-2015-numeric</code> 是 Typst 内置的样式名，对应国标顺序编码制。渲染结果（<code>make ch10</code> 之后 <code>out/ch10/bib-basic.pdf</code>，下图是同一文件的 PNG）：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch10-bib-basic.png" alt="bib-basic.typ 的渲染：正文三处引用显示为上标方括号数字，下方“参考文献”列表按 GB/T 7714 格式排版，英文作者姓大写，文献类型标记 [M]" /><figcaption>bib-basic.typ 的渲染：正文三处引用显示为上标方括号数字，下方“参考文献”列表按 GB/T 7714 格式排版，英文作者姓大写，文献类型标记 [M]</figcaption></figure><p></p><p><code>pdftotext -layout out/ch10/bib-basic.pdf -</code> 给出的纯文本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 最小二乘</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>正规方程的几何解释见[1]，数值稳定性见[2]。 国内教材</span></div></div><div><div><div>3</div></div><div><span>参见[3]。</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>参考文献</span></div></div><div><div><div>6</div></div><div><span>[1] STRANG G. Introduction to Linear Algebra[M]. 6 版 .</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>Wellesley, MA: Wellesley-Cambridge Press, 2023.</span></div></div><div><div><div>8</div></div><div><span>[2] GOLUB G H, VAN LOAN C F. Matrix Computations[M]. 4</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>版. Baltimore: Johns Hopkins University Press, 2013.</span></div></div><div><div><div>10</div></div><div><span>[3] 同济大学数学系. 工程数学 线性代数[M]. 6 版. 北京: 高</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>等教育出版社, 2014.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意三件事。引用按<strong>首次出现的顺序</strong>编号，<code>[1]</code> 是 Strang 而不是按键名字母序。<code>[2, Ch. 5]</code> 这样的补充说明在国标样式里被丢掉了：正文里写的是 <code>[2]</code>，<code>Ch.~5</code> 不见了；换成 <code>ieee</code>（下一节）补充说明就保留了，所以这是样式的决定，不是 <code>@golub2013[Ch.~5]</code> 写错了。第三，英文书的版本写成了“6 版”，这个我留到后面的“语言环境”一节单独讲。</p><p>“参考文献”这个标题不是我写的，是 <code>bibliography</code> 的默认 <code>title</code>，按 <code>text.lang</code> 取本地化名称；<code>xuan-base</code> 里设了 <code>heading.numbering: "1.1"</code>，但这个标题没有编号（原因在最后一节）。</p></section>
<section><h2>另一种数据格式：Hayagriva YAML<a href="#另一种数据格式hayagriva-yaml"><span>#</span></a></h2><p>Typst 自带的参考文献引擎叫 Hayagriva，<code>.bib</code> 只是它能读的格式之一，原生格式是 YAML。<code>lab/ch10/refs.yml</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/refs.yml" target="_blank">源文件</a>）与 <code>refs.bib</code> 条目一一对应，键完全一样。我只贴前两条，第三条同理：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>strang2023</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>type</span><span>: </span><span>book</span></div></div><div><div><div>3</div></div><div><span>  </span><span>title</span><span>: </span><span>Introduction to Linear Algebra</span></div></div><div><div><div>4</div></div><div><span>  </span><span>author</span><span>: </span><span>Strang, Gilbert</span></div></div><div><div><div>5</div></div><div><span>  </span><span>edition</span><span>: </span><span>6</span></div></div><div><div><div>6</div></div><div><span>  </span><span>date</span><span>: </span><span>2023</span></div></div><div><div><div>7</div></div><div><span>  </span><span>language</span><span>: </span><span>en</span></div></div><div><div><div>8</div></div><div><span>  </span><span>publisher</span><span>:</span></div></div><div><div><div>9</div></div><div><span>    </span><span>name</span><span>: </span><span>Wellesley-Cambridge Press</span></div></div><div><div><div>10</div></div><div><span>    </span><span>location</span><span>: </span><span>Wellesley, MA</span></div></div><div><div><div>11</div></div><div><span>  </span><span>serial-number</span><span>:</span></div></div><div><div><div>12</div></div><div><span>    </span><span>isbn</span><span>: </span><span>978-1-7331466-7-8</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>golub2013</span><span>:</span></div></div><div><div><div>15</div></div><div><span>  </span><span>type</span><span>: </span><span>book</span></div></div><div><div><div>16</div></div><div><span>  </span><span>title</span><span>: </span><span>Matrix Computations</span></div></div><div><div><div>17</div></div><div><span>  </span><span>author</span><span>:</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>- </span><span>Golub, Gene H.</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>- </span><span>Van Loan, Charles F.</span></div></div><div><div><div>20</div></div><div><span>  </span><span>edition</span><span>: </span><span>4</span></div></div><div><div><div>21</div></div><div><span>  </span><span>date</span><span>: </span><span>2013</span></div></div><div><div><div>22</div></div><div><span>  </span><span>language</span><span>: </span><span>en</span></div></div><div><div><div>23</div></div><div><span>  </span><span>publisher</span><span>:</span></div></div><div><div><div>24</div></div><div><span>    </span><span>name</span><span>: </span><span>Johns Hopkins University Press</span></div></div><div><div><div>25</div></div><div><span>    </span><span>location</span><span>: </span><span>Baltimore</span></div></div><div><div><div>26</div></div><div><span>  </span><span>serial-number</span><span>:</span></div></div><div><div><div>27</div></div><div><span>    </span><span>isbn</span><span>: </span><span>978-1-4214-0794-4</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对照一下就能看出 BibTeX 里扁平的字段在 YAML 里怎么嵌套：<code>publisher</code> 加 <code>address</code> 变成带 <code>name</code> 与 <code>location</code> 的子字典，<code>isbn</code> 进了 <code>serial-number</code>，多个作者是列表，所以不再需要 <code>{Van Loan}</code> 那种花括号技巧：列表里每一项本来就是一个完整的人名。<code>.bib</code> 的好处是 Zotero、Google Scholar 之类的工具能直接导出，YAML 则好读好手写；我自己的选择是，数据来自工具就留 <code>.bib</code>，条目少又手写就用 YAML。</p><p>两种格式可以混用吗？<code>bibliography</code> 的第一个参数既可以是一个路径，也可以是路径数组。我试了 <code>("refs.bib", "refs.yml")</code>（<code>/tmp/p10/mixed.typ</code>，同一份数据两种格式各一份）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>)</span></div></div><div><div><div>2</div></div><div><span>见 </span><span>@strang2023</span><span> 与 </span><span>@golub2013</span><span>。</span></div></div><div><div><div>3</div></div><div><span>#bibliography</span><span>((</span><span>"refs.bib"</span><span>, </span><span>"refs.yml"</span><span>), </span><span>style</span><span>: </span><span>"ieee"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: duplicate bibliography keys: strang2023, golub2013, tongji2014</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ mixed.typ:3:14</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>3 │ #bibliography(("refs.bib", "refs.yml"), style: "ieee")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│               ^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>数组本身是合法的，只是我这里两个文件的键撞了，所以编译器拒绝。我另外把 <code>refs.bib</code> 拆成 <code>a.bib</code>（Strang）和 <code>b.bib</code>（Golub），用 <code>#bibliography(("a.bib", "b.bib"), style: "ieee")</code> 编译（<code>/tmp/p10/t2.typ</code>），正文 <code>[1]</code> 与 <code>[2]</code> 都解析成功，列表里两条齐全；键不重叠，数组就合并成一个列表。</p><p><code>lab/ch10/bib-hayagriva/main.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/bib-hayagriva/main.typ" target="_blank">源文件</a>）读的就是 YAML：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Hayagriva YAML: same keys, same entries, different file format.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>=</span><span> 最小二乘</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>正规方程的几何解释见 </span><span>@strang2023</span><span>，数值稳定性见 </span><span>@golub2013</span><span>[Ch.~5]。</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>// Style locale follows `text.lang` where the list is placed: an English</span></div></div><div><div><div>11</div></div><div><span>// list inside a Chinese document gets "and" and "6th ed." back.</span></div></div><div><div><div>12</div></div><div><span>// `full: true` lists every entry, cited or not.</span></div></div><div><div><div>13</div></div><div><span>#</span><span>[</span></div></div><div><div><div>14</div></div><div><span>  </span><span>#set</span><span> </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"en"</span><span>)</span></div></div><div><div><div>15</div></div><div><span>  </span><span>#bibliography</span><span>(</span><span>"../refs.yml"</span><span>, </span><span>style</span><span>: </span><span>"ieee"</span><span>, </span><span>full</span><span>: </span><span>true</span><span>, </span><span>title</span><span>: [References])</span></div></div><div><div><div>16</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里新出现三样东西：<code>full: true</code>、<code>title:</code> 和包在 <code>#[ ... ]</code> 里的 <code>set text(lang: "en")</code>。</p><p><code>full: true</code> 让列表包含数据文件里<strong>所有</strong>条目，不管正文有没有引用。正文只引用了 <code>strang2023</code> 和 <code>golub2013</code>，渲染出来列表里有三条，<code>tongji2014</code> 也在里面，编号 <code>[3]</code>。<code>title:</code> 接受 content（覆盖默认标题）或 <code>none</code>（不要标题，后面多列表那一节会用）。</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch10-bib-hayagriva.png" alt="bib-hayagriva.typ 的渲染：中文正文里的引用显示为 [1] 和 [2, Ch. 5]，下方是英文标题 References 的 IEEE 样式列表，英文书版本写作 6th ed.，中文教材也在列表里" /><figcaption>bib-hayagriva.typ 的渲染：中文正文里的引用显示为 [1] 和 [2, Ch. 5]，下方是英文标题 References 的 IEEE 样式列表，英文书版本写作 6th ed.，中文教材也在列表里</figcaption></figure><p></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 最小二乘</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>正规方程的几何解释见 [1]，数值稳定性见 [2, Ch. 5]。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>References</span></div></div><div><div><div>5</div></div><div><span>[1] G. Strang, Introduction to Linear Algebra, 6th ed. Wellesley,</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>MA: Wellesley-Cambridge Press, 2023.</span></div></div><div><div><div>7</div></div><div><span>[2] G. H. Golub and C. F. Van Loan, Matrix Computations, 4th</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>ed. Baltimore: Johns Hopkins University Press, 2013.</span></div></div><div><div><div>9</div></div><div><span>[3] 同济大学数学系, 工程数学 线性代数, 6th ed. 北京: 高等</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>教育出版社, 2014.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>IEEE 样式保留了补充说明（<code>[2, Ch. 5]</code>），国标样式把它丢了，这是同一个 <code>@golub2013[Ch.~5]</code> 写法在不同样式里的两种命运。</p></section>
<section><h2>语言环境：跟着 <code>text.lang</code> 走，不跟着条目走<a href="#语言环境跟着-textlang-走不跟着条目走"><span>#</span></a></h2><p>对比前两份输出：同一条 Strang，在 <code>bib-basic</code> 里是“6 版”，在 <code>bib-hayagriva</code> 里是“6th ed.”；同一条同济教材，在 IEEE 列表里是“6th ed.”，作者之间的连接词是 <code>and</code>。两份数据里每个条目都写了 <code>language</code>（<code>en</code> 或 <code>zh</code>），然而这个字段并没有决定输出：</p><ul>
<li><code>bib-basic</code> 里 <code>strang2023</code> 的 <code>language</code> 是 <code>en</code>，列表里照样出现“6 版”。</li>
<li><code>bib-hayagriva</code> 里 <code>tongji2014</code> 的 <code>language</code> 是 <code>zh</code>，列表里照样是“6th ed.”。</li>
</ul><p>决定输出语言的是列表所在位置的 <code>text.lang</code>。第 5 篇里 <code>xuan-cjk</code> 把整篇文档设成了 <code>lang: "zh"</code>，所以 <code>bib-basic</code> 里的 <code>bibliography</code> 取中文术语（“版”“和”“参考文献”）；<code>bib-hayagriva</code> 里我用 <code>#[ #set text(lang: "en") ... ]</code> 把这一个列表的语言单独改回英文，于是 <code>ed.</code>、<code>and</code>、<code>References</code> 全回来了。</p><p>这个设计的后果是：中英文混合的列表无法让每一条按各自的语言排，列表整体用哪种语言，是你要在列表所在位置选择的事。GB/T 7714 的纯中文环境里英文条目也写成“6 版”那样的中文术语，这时跟着文档 <code>lang: "zh"</code> 走恰好是对的；像 IEEE 这样的英文期刊样式，你要么整篇用 <code>lang: "en"</code>，要么像这里一样给列表单独设。</p><p>（至于条目的 <code>language</code> 字段在别的场景里是否有用，那要翻 Hayagriva 的源码才知道，我没翻。我能断言的只是：在这两份文件、这几个样式里，条目带着 <code>language: en</code> 或 <code>zh</code> 都被 <code>text.lang</code> 盖过了。）</p></section>
<section><h2>样式：同一条目，四种面孔<a href="#样式同一条目四种面孔"><span>#</span></a></h2><p><code>style:</code> 接受内置样式名的字符串。我用 <code>lab/ch10/fig-bib.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/fig-bib.typ" target="_blank">源文件</a>）把同一条 Golub 在四个样式里各排一遍：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// One source, four styles: each block owns a local bibliography.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>9pt</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>sample</span><span>(</span><span>style</span><span><span>) </span><span>= </span></span><span>block</span><span>(</span><span>width</span><span>: </span><span>11cm</span><span>, </span><span>context</span><span> {</span></div></div><div><div><div>9</div></div><div><span>  </span><span>strong</span><span>(</span><span>raw</span><span>(</span><span>style</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>[: ] </span><span>+</span><span> </span><span>cite</span><span>(</span><span>&lt;golub2013&gt;</span><span>)</span></div></div><div><div><div>11</div></div><div><span>  </span><span>bibliography</span><span>(</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"refs.bib"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>style</span><span>: </span><span>style</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>title</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>target</span><span>: </span><span>selector</span><span>(</span><span>cite</span><span>).</span><span>within</span><span>(</span><span>here</span><span>()),</span></div></div><div><div><div>16</div></div><div><span>    </span><span>group</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>18</div></div><div><span>  </span><span>v</span><span>(</span><span>0.8em</span><span>)</span></div></div><div><div><div>19</div></div><div><span>})</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#sample</span><span>(</span><span>"gb-7714-2015-numeric"</span><span>)</span></div></div><div><div><div>22</div></div><div><span>#sample</span><span>(</span><span>"ieee"</span><span>)</span></div></div><div><div><div>23</div></div><div><span>#sample</span><span>(</span><span>"apa"</span><span>)</span></div></div><div><div><div>24</div></div><div><span>#sample</span><span>(</span><span>"chicago-author-date"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里先不管 <code>target</code> 和 <code>group</code>（后面单独一节），只关注它的产出：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch10-styles.png" alt="同一条文献 Matrix Computations 在四个样式下的引用和列表：gb-7714-2015-numeric 为上标 [1] 与大写作者名，ieee 为 [1] 与首字母缩写，apa 为（Golub 与 Van Loan，2013）形式，chicago-author-date 为 Golub 和 Van Loan 2013 形式" /><figcaption>同一条文献 Matrix Computations 在四个样式下的引用和列表：gb-7714-2015-numeric 为上标 [1] 与大写作者名，ieee 为 [1] 与首字母缩写，apa 为（Golub 与 Van Loan，2013）形式，chicago-author-date 为 Golub 和 Van Loan 2013 形式</figcaption></figure><p></p>

<table><thead><tr><th>样式</th><th>正文引用</th><th>条目开头</th></tr></thead><tbody><tr><td><code>gb-7714-2015-numeric</code></td><td>上标 <code>[1]</code></td><td><code>GOLUB G H, VAN LOAN C F. Matrix Computations[M]. 4 版.</code></td></tr><tr><td><code>ieee</code></td><td><code>[1]</code></td><td><code>G. H. Golub 和 C. F. Van Loan, Matrix Computations, 4 版.</code></td></tr><tr><td><code>apa</code></td><td><code>(Golub &amp; Van Loan, 2013)</code></td><td><code>Golub, G. H., &amp; Van Loan, C. F. (2013). Matrix Computations (4 版).</code></td></tr><tr><td><code>chicago-author-date</code></td><td><code>(Golub 和 Van Loan 2013)</code></td><td><code>Golub, Gene H., 和 Charles F. Van Loan. 2013. Matrix Computations. 4 版.</code></td></tr></tbody></table><p>前两个是顺序编码制（正文里放编号，列表按引用顺序），后两个是著者出版年制（正文里放作者和年份，列表按作者字母序）。<code>apa</code> 的 <code>&amp;</code> 和 <code>chicago-author-date</code> 的“和”来源不同：前者是 APA 的规定，后者是 Chicago 样式在 <code>lang: "zh"</code> 下取到的本地化“and”。这也再次印证了上一节：文档语言是中文，连接词就是中文。</p><p>写错样式名会怎样？我试了 <code>style: "gb-7714"</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unknown style: gb-7714</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ badstyle.typ:3:33</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>3 │ #bibliography("refs.bib", style: "gb-7714")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│                                  ^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内置名必须拼全（<code>gb-7714-2015-numeric</code>，或者著者出版年制的 <code>gb-7714-2015-author-date</code>）。至于其他几百种学术期刊的样式，要么在 Typst 文档的 <code>bibliography</code> 页面查内置列表，要么用下面的 CSL 文件。</p></section>
<section><h2>引用的写法：<code>cite</code> 与它的 <code>form</code><a href="#引用的写法cite-与它的-form"><span>#</span></a></h2><p><code>@key</code> 只能表达“一个括号引用”。要表达“作者名”“只要年份”“把引用放进句子里”，用 <code>cite</code> 函数。<code>lab/ch10/cite-forms.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/cite-forms.typ" target="_blank">源文件</a>）在著者出版年制（<code>apa</code>）下把几种写法并排：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// cite forms: `@key`, supplements and `cite(form:)` under author-date style.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>普通引用 </span><span>@strang2023</span><span>；带页码 </span><span>@golub2013</span><span>[p.~12]。</span><span>\</span></div></div><div><div><div>8</div></div><div><span>作者形式：</span><span>#cite</span><span>(</span><span>&lt;golub2013&gt;</span><span>, </span><span>form</span><span>: </span><span>"author"</span><span>)；</span></div></div><div><div><div>9</div></div><div><span>年份形式：</span><span>#cite</span><span>(</span><span>&lt;golub2013&gt;</span><span>, </span><span>form</span><span>: </span><span>"year"</span><span>)；</span></div></div><div><div><div>10</div></div><div><span>文中形式：</span><span>#cite</span><span>(</span><span>&lt;strang2023&gt;</span><span>, </span><span>form</span><span>: </span><span>"prose"</span><span>)。</span><span>\</span></div></div><div><div><div>11</div></div><div><span>隐藏引用只登记条目：</span><span>#cite</span><span>(</span><span>&lt;tongji2014&gt;</span><span>, </span><span>form</span><span>: </span><span>none</span><span>)。</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#bibliography</span><span>(</span><span>"refs.bib"</span><span>, </span><span>style</span><span>: </span><span>"apa"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>选 <code>apa</code> 是因为 <code>author</code>、<code>year</code> 这类形式只在著者出版年制里有明显区别；顺序编码制里我没有编译过这些组合，不下结论。著者出版年制的结果：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch10-cite-forms.png" alt="cite-forms.typ 的渲染：普通引用 (Strang, 2023)，带页码 (Golub &amp; Van Loan, 2013, p. 12)，作者形式只有姓名，年份形式只有 2013，文中形式 Strang (2023)，隐藏引用处为空；下方列表三条文献俱全，同济教材排在最前" /><figcaption>cite-forms.typ 的渲染：普通引用 (Strang, 2023)，带页码 (Golub &amp; Van Loan, 2013, p. 12)，作者形式只有姓名，年份形式只有 2013，文中形式 Strang (2023)，隐藏引用处为空；下方列表三条文献俱全，同济教材排在最前</figcaption></figure><p></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>普通引用 (Strang, 2023)；带页码 (Golub &amp; Van Loan, 2013,</span></div></div><div><div><div>2</div></div><div><span>p. 12)。</span></div></div><div><div><div>3</div></div><div><span>作者形式：Golub &amp; Van Loan； 年份形式：2013； 文中形式：</span></div></div><div><div><div>4</div></div><div><span>Strang (2023)。</span></div></div><div><div><div>5</div></div><div><span>隐藏引用只登记条目：。</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>参考文献</span></div></div><div><div><div>8</div></div><div><span>同济大学数学系. (2014). 工程数学 线性代数 (6 版). 高等教育</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>出版社.</span></div></div><div><div><div>10</div></div><div><span>Golub, G. H., &amp; Van Loan, C. F. (2013). Matrix Computations (4</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>版). Johns Hopkins University Press.</span></div></div><div><div><div>12</div></div><div><span>Strang, G. (2023). Introduction to Linear Algebra (6 版).</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>Wellesley-Cambridge Press.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>form</code> 的取值有 <code>"normal"</code>（默认，<code>@key</code> 就是它）、<code>"author"</code>、<code>"year"</code>、<code>"prose"</code>，以及 <code>none</code>。关键的是最后一个：“隐藏引用只登记条目：”后面是空的，什么都没印，但同济教材仍然出现在了下方的列表里。想让某条文献进列表又不想在正文里提它时，这就是手段。<code>full: true</code> 是整个文件都进列表，<code>form: none</code> 是挑几条进。</p><p>著者出版年制的列表按作者排序，这里同济教材排在 Golub 与 Strang 之前。这一条我只记录结果，没有去区分排序规则是按拼音还是按码点。</p></section>
<section><h2>本地 CSL 文件：不用等 Typst 内置<a href="#本地-csl-文件不用等-typst-内置"><span>#</span></a></h2><p>内置样式够用的时候用内置名；不够用，<code>style:</code> 也可以是一个 <code>.csl</code> 文件的路径。CSL（Citation Style Language）是一种 XML 格式，Zotero 和 Pandoc 也用它，社区在 <a href="https://github.com/citation-style-language/styles" target="_blank">citation-style-language/styles</a> 仓库里维护着大量学术期刊与机构的样式。</p><p><code>lab/ch10/custom-csl/ams-label.csl</code> 是我从那个仓库<strong>原样拷来</strong>的美国数学会 label 样式，153 行，没有改过一个字符。文件头部有它的许可证声明，我保留了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>rights</span><span> </span><span>license</span><span>=</span><span>"http://creativecommons.org/licenses/by-sa/3.0/"</span><span>&gt;This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License&lt;/</span><span>rights</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也就是 CC BY-SA 3.0。这份许可证的要求是署名和相同方式共享，所以这个文件不适用本仓库的 MIT OR Apache-2.0；它保持着自己的头部，仓库里别的文件不依赖它的授权。如果你要在自己的项目里拷它，也请保留这一行 <code>&lt;rights&gt;</code>。</p><p><code>lab/ch10/custom-csl/main.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/custom-csl/main.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A CSL file used as-is. ams-label.csl comes verbatim from</span></div></div><div><div><div>2</div></div><div><span>// github.com/citation-style-language/styles (CC BY-SA 3.0, see its &lt;rights&gt;).</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>=</span><span> 引用</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>矩阵计算 </span><span>@golub2013</span><span> 与线性代数 </span><span>@strang2023</span><span>。</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#bibliography</span><span>(</span><span>"../refs.bib"</span><span>, </span><span>style</span><span>: </span><span>"ams-label.csl"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意 <code>style</code> 的值以 <code>.csl</code> 结尾，Typst 据此把它当成路径，而不是内置名字。路径相对于<strong>写下这个字符串的文件</strong>，所以这里的 <code>"ams-label.csl"</code> 在 <code>custom-csl/</code> 里，而 <code>refs.bib</code> 是 <code>"../refs.bib"</code>（第 12 篇会专门讲路径解析）。</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch10-custom-csl.png" alt="custom-csl 的渲染：引用显示为 [GV13] 和 [Str23] 这样由作者姓缩写加年份后两位构成的标签，下方列表条目前缀为同样的标签，作者名为姓在前" /><figcaption>custom-csl 的渲染：引用显示为 [GV13] 和 [Str23] 这样由作者姓缩写加年份后两位构成的标签，下方列表条目前缀为同样的标签，作者名为姓在前</figcaption></figure><p></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 引用</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>矩阵计算 [GV13] 与线性代数 [Str23]。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>参考文献</span></div></div><div><div><div>5</div></div><div><span>[GV13] Golub, Gene H., 和 Loan, Charles F. Van, Matrix</span></div></div><div><div><div>6</div></div><div><span><span>          </span></span><span>Computations, 4 版, Johns Hopkins University Press,</span></div></div><div><div><div>7</div></div><div><span><span>          </span></span><span>Baltimore, 2013.</span></div></div><div><div><div>8</div></div><div><span>[Str23]   Strang, Gilbert, Introduction to Linear Algebra, 6 版,</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>Wellesley-Cambridge Press, Wellesley, MA, 2023.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>AMS 的 label 样式把 <code>[GV13]</code>、<code>[Str23]</code> 这样的字母标签当作引用，这是前面几个按编号或按作者年份的样式做不到的，也是我拿它当例子的原因。</p><p>输出里有一个我没料到的细节：第一条是 <code>Golub, Gene H., 和 Loan, Charles F. Van</code>，Van Loan 被拆成了 <code>Loan ... Van</code>。前面 <code>ieee</code>、<code>apa</code> 里它都是整体的 <code>Van Loan</code>。我的猜测是：Hayagriva 把 <code>Van</code> 当成姓的“非丢弃小品词”（non-dropping particle），而这个样式要求作者名全部以“姓，名”倒序显示，CSL 在倒序显示时默认把小品词移到名字后面。我没有去验证这个解释，所以只能说：这是这个样式和这条数据配合起来的实际输出，如果你的数据里有这类姓，要在自己的样式里检查一遍。<code>.bib</code> 里的 <code>{Van Loan}</code> 花括号没有挡住它，这一点我是看到输出才确认的。</p></section>
<section><h2>多个参考文献：<code>target</code> 与 <code>group</code>（0.15）<a href="#多个参考文献target-与-group015"><span>#</span></a></h2><p>到 0.14 为止，一个文档只能有一个参考文献列表。0.15 的 <code>bibliography</code> 多了两个参数：</p><ul>
<li><code>target</code>：一个选择器，决定这个列表显示<strong>哪些引用</strong>。默认的 <code>auto</code> 不是“剩余的引用”，而是按 0.15 文档的优先级归属：先看有没有某个显式 <code>target</code> 包含这条引用（有，就归第一个这样的列表）；否则归文档里排在它之后、最近的一个 <code>target: auto</code> 且数据里有这个键的列表；再没有，就归排在它之前、最近的一个。</li>
<li><code>group</code>：决定<strong>编号是否跨列表连续</strong>。<code>auto</code> 是和别的 <code>auto</code> 列表共享同一组（编号连续），<code>none</code> 是自成一组、从头编号。</li>
</ul><p>如果要找个类比：列表是引用表上的一个视图，<code>target</code> 是视图的 WHERE 子句，<code>group</code> 是视图要不要共享同一个序号序列。下面是 <code>lab/ch10/multi-bib.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch10/multi-bib.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 0.15: several bibliographies. `target` picks which citations a list</span></div></div><div><div><div>2</div></div><div><span>// shows; `group` decides whether numbering continues across lists.</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>6</div></div><div><span>#show</span><span> </span><span>bibliography</span><span>: </span><span>set</span><span> </span><span>heading</span><span>(</span><span>offset</span><span>: </span><span>1</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// A local list: cites inside `body` go here and nowhere else.</span></div></div><div><div><div>9</div></div><div><span>#let</span><span> </span><span>local-bib</span><span>(</span><span>body</span><span>, </span><span>group</span><span>: </span><span>auto</span><span><span>) </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>10</div></div><div><span>  </span><span>body</span></div></div><div><div><div>11</div></div><div><span>  </span><span>bibliography</span><span>(</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"refs.bib"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>style</span><span>: </span><span>"ieee"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>title</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>target</span><span>: </span><span>selector</span><span>(</span><span>cite</span><span>).</span><span>within</span><span>(</span><span>here</span><span>()),</span></div></div><div><div><div>16</div></div><div><span>    </span><span>group</span><span>: </span><span>group</span><span>,</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>=</span><span> 正文</span></div></div><div><div><div>21</div></div><div><span>正文引用 </span><span>@tongji2014</span><span>。</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>#block</span><span>(</span><span>stroke</span><span>: (</span><span>left</span><span>: </span><span>1.5pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>), </span><span>inset</span><span>: </span><span>0.8em</span><span>)[</span></div></div><div><div><div>24</div></div><div><span>  </span><span>#local-bib</span><span>[英文教材 </span><span>@strang2023</span><span> 与 </span><span>@golub2013</span><span>。]</span></div></div><div><div><div>25</div></div><div><span>]</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>#block</span><span>(</span><span>stroke</span><span>: (</span><span>left</span><span>: </span><span>1.5pt</span><span><span> </span><span>+</span><span> </span></span><span>red</span><span>), </span><span>inset</span><span>: </span><span>0.8em</span><span>)[</span></div></div><div><div><div>28</div></div><div><span>  </span><span>// group: none restarts at [1]; with `auto` it would continue at [3].</span></div></div><div><div><div>29</div></div><div><span>  </span><span>#local-bib</span><span>(</span><span>group</span><span>: </span><span>none</span><span>)[再引一次 </span><span>@golub2013</span><span>。]</span></div></div><div><div><div>30</div></div><div><span>]</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>#bibliography</span><span>(</span><span>"refs.bib"</span><span>, </span><span>style</span><span>: </span><span>"ieee"</span><span>, </span><span>title</span><span>: [总参考文献])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>local-bib</code> 的写法值得停一下。<code>target: selector(cite).within(here())</code> 的意思是“位于 <code>here()</code> 这个位置之内的 <code>cite</code>”。<code>within</code> 是 0.15 新增的选择器方法，第 4 篇的踩坑里提过它不能用在 <code>show</code> 规则里，只能用在 <code>query</code> 和这样的参数里。而 <code>here()</code> 需要 context，所以整个函数体包在 <code>context { ... }</code> 里（第 7 篇的内容：<code>here()</code> 是当前位置，要在 context 里才有定义）。<code>body</code> 和 <code>bibliography(...)</code> 放在同一个 <code>context</code> 块里，二者所在位置一致，所以 <code>body</code> 里的引用都落在 <code>here()</code> 之内。</p><p>渲染：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch10-multi-bib.png" alt="multi-bib.typ 的渲染：正文引用 [3]；蓝色左边线的块里引用 [1] 与 [2] 并紧跟一个只含这两条的列表；红色左边线的块里再次引用 Golub，编号为 [1]，下方只列这一条；最后的总参考文献列表只含同济教材，编号 [3]" /><figcaption>multi-bib.typ 的渲染：正文引用 [3]；蓝色左边线的块里引用 [1] 与 [2] 并紧跟一个只含这两条的列表；红色左边线的块里再次引用 Golub，编号为 [1]，下方只列这一条；最后的总参考文献列表只含同济教材，编号 [3]</figcaption></figure><p></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 正文</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>正文引用 [3]。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>英文教材 [1] 与 [2]。</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[1] G. Strang, Introduction to Linear Algebra, 6 版 .</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>Wellesley, MA: Wellesley-Cambridge Press, 2023.</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[2] G. H. Golub 和 C. F. Van Loan, Matrix Computations, 4</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>版. Baltimore: Johns Hopkins University Press, 2013.</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>再引一次 [1]。</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[1] G. H. Golub 和 C. F. Van Loan, Matrix Computations, 4</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>版. Baltimore: Johns Hopkins University Press, 2013.</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>总参考文献</span></div></div><div><div><div>17</div></div><div><span>[3] 同济大学数学系, 工程数学 线性代数, 6 版. 北京: 高等教</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>育出版社, 2014.</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读这份输出要注意几点。</p><p><strong>显式 <code>target</code> 优先。</strong> 最后那个没写 <code>target</code> 的列表只显示 <code>tongji2014</code>：<code>@tongji2014</code> 不在任何 <code>within</code> 之内，没有显式列表包含它，于是按 <code>auto</code> 的规则归了它后面最近的列表；<code>strang2023</code> 和 <code>golub2013</code> 被前面的显式列表包含，规则的第一条就把它们分走了，不再出现在总列表里。我另外在 <code>/tmp/p10/prec.typ</code> 里把一个默认列表放在前面、一个显式列表放在后面，中间引用 <code>@strang2023</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#bibliography</span><span>(</span><span>"refs.bib"</span><span>, </span><span>style</span><span>: </span><span>"ieee"</span><span>, </span><span>title</span><span>: [前面的默认列表])</span></div></div><div><div><div>3</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>[引用 </span><span>@strang2023</span><span>。]</span></div></div><div><div><div>5</div></div><div><span>  </span><span>bibliography</span><span>(</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"refs.bib"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>style</span><span>: </span><span>"ieee"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>title</span><span>: [后面的显式列表],</span></div></div><div><div><div>9</div></div><div><span>    </span><span>target</span><span>: </span><span>selector</span><span>(</span><span>cite</span><span>).</span><span>within</span><span>(</span><span>here</span><span>()),</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>前面的默认列表</span></div></div><div><div><div>2</div></div><div><span>引用 [1]。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>后面的显式列表</span></div></div><div><div><div>5</div></div><div><span>[1] G. Strang, Introduction to Linear Algebra, 6th ed.</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>Wellesley, MA: Wellesley-Cambridge Press, 2023.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>条目出现在后面的显式列表里，前面的默认列表是空的：位置在前不如显式声明。</p><p><strong>编号是按列表在文档里出现的顺序给的，不是按引用的先后。</strong> 同济教材是正文里<strong>最先</strong>被引用的，但它的编号是 <code>[3]</code>：它所在的列表在文档的最后。蓝色块的 <code>[1]</code>、<code>[2]</code> 是因为它们的列表更靠前。</p><p><strong><code>group: none</code> 的作用我对照着做了一遍。</strong> 源文件里的注释说“<code>auto</code> 会从 <code>[3]</code> 接着编”，我把红色块那一行改成 <code>group: auto</code> 编译了（<code>/tmp/p10/mb-auto.typ</code>，在 <code>/tmp</code> 里，不在仓库）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>再引一次 [3]。</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[3] G. H. Golub 和 C. F. Van Loan, Matrix Computations, 4</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>版. Baltimore: Johns Hopkins University Press, 2013.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>红块编号变成 <code>[3]</code>，总列表里的同济教材则变成了 <code>[4]</code>。<code>group: auto</code> 的三个列表共用一个编号序列（蓝色 1、2，红色 3，总列表 4）；<code>group: none</code> 的红色列表脱离这个序列，从 <code>[1]</code> 重新开始，总列表则和蓝色列表同属 <code>auto</code> 组，继续用 <code>[3]</code>。</p><p>这个功能的真实用途：每章末尾一个独立的参考文献（学位论文常见），或者把“推荐阅读”和“引用文献”分成两个列表。</p><p><code>#show bibliography: set heading(offset: 1)</code> 让所有参考文献的标题降一级，这样“总参考文献”和上面的 <code>= 正文</code> 不在同一级。</p></section>
<section><h2>把它接进 <code>note</code><a href="#把它接进-note"><span>#</span></a></h2><p>最后一步是让模板替你把列表放在文末。<code>src/note.typ</code> 在这一篇定型，完整的最终版（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/note.typ" target="_blank">源文件</a>）如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"base.typ"</span><span>: </span><span>xuan-base</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"header.typ"</span><span>: </span><span>xuan-footer</span><span>, </span><span>xuan-header</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"page.typ"</span><span>: </span><span>title-block</span><span>, </span><span>xuan-page</span></div></div><div><div><div>4</div></div><div><span>#import</span><span> </span><span>"util.typ"</span><span>: </span><span>ensure-array</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/// Notes and reports.</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>note</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>title</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>subtitle</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>author</span><span>: (),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>date</span><span>: </span><span>auto</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>abstract</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>keywords</span><span>: (),</span></div></div><div><div><div>14</div></div><div><span>  </span><span>toc</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>bibliography</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>paper</span><span>: </span><span>"a4"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>18</div></div><div><span>  </span><span>body</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>20</div></div><div><span>  </span><span>set</span><span> </span><span>document</span><span>(</span></div></div><div><div><div>21</div></div><div><span>    </span><span>title</span><span>: </span><span>title</span><span>,</span></div></div><div><div><div>22</div></div><div><span>    </span><span>author</span><span>: </span><span>ensure-array</span><span>(</span><span>author</span><span>),</span></div></div><div><div><div>23</div></div><div><span>    </span><span>keywords</span><span>: </span><span>keywords</span><span>,</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-page</span><span>.</span><span>with</span><span>(</span><span>paper</span><span>: </span><span>paper</span><span>)</span></div></div><div><div><div>26</div></div><div><span>  </span><span>set</span><span> </span><span>page</span><span>(</span><span>header</span><span>: </span><span>xuan-header</span><span>(), </span><span>footer</span><span>: </span><span>xuan-footer</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>))</span></div></div><div><div><div>27</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-base</span><span>.</span><span>with</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>, </span><span>title</span><span>: </span><span>title</span><span>)</span></div></div><div><div><div>28</div></div><div><span>  </span><span>title-block</span><span>(</span></div></div><div><div><div>29</div></div><div><span>    </span><span>title</span><span>: </span><span>title</span><span>,</span></div></div><div><div><div>30</div></div><div><span>    </span><span>subtitle</span><span>: </span><span>subtitle</span><span>,</span></div></div><div><div><div>31</div></div><div><span>    </span><span>author</span><span>: </span><span>author</span><span>,</span></div></div><div><div><div>32</div></div><div><span>    </span><span>date</span><span>: </span><span>date</span><span>,</span></div></div><div><div><div>33</div></div><div><span>    </span><span>lang</span><span>: </span><span>lang</span><span>,</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span>  </span><span>if</span><span> </span><span>abstract</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>36</div></div><div><span>    </span><span>align</span><span>(</span><span>center</span><span>, </span><span>block</span><span>(</span><span>width</span><span>: </span><span>85%</span><span>, </span><span>align</span><span>(</span><span>left</span><span>, {</span></div></div><div><div><div>37</div></div><div><span>      </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>38</div></div><div><span>      </span><span>strong</span><span>(</span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [摘要] </span><span>else</span><span> [Abstract])</span></div></div><div><div><div>39</div></div><div><span>      </span><span>h</span><span>(</span><span>1em</span><span>)</span></div></div><div><div><div>40</div></div><div><span>      </span><span>abstract</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>})))</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span>  </span><span>if</span><span> </span><span>toc</span><span> { </span><span>outline</span><span>(</span><span>title</span><span>: </span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [目录] </span><span>else</span><span> [Contents]) }</span></div></div><div><div><div>44</div></div><div><span>  </span><span>body</span></div></div><div><div><div>45</div></div><div><span>  </span><span>if</span><span> </span><span>bibliography</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>46</div></div><div><span>    </span><span>bibliography</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这一章它长出的只有两处：参数表里的 <code>bibliography: none,</code>，和末尾的 <code>if bibliography != none { ... }</code> 这 3 行，整个文件 48 行。<code>git log --oneline -- packages/local/xuan/0.1.0/src/note.typ</code> 在仓库里有三条提交：第一条建立文件，第二条（<code>feat(xuan): math, figure, ...</code>）加上这些，第三条（<code>fix(xuan): ...</code>）删掉了我写这篇时发现多余的一行，见下文。</p><p>设计上有三点值得说。</p><p><strong>参数的类型是“一个 <code>bibliography</code> 值”，不是一个路径。</strong> 调用时写 <code>bibliography: bibliography("refs.bib", style: "gb-7714-2015-numeric")</code>（<code>examples/notes/main.typ</code> 里就是这么用的）。我没有让 <code>note</code> 接收 <code>bib-file:</code> 和 <code>bib-style:</code> 两个参数再自己拼，理由是 <code>bibliography</code> 的参数会越来越多（<code>full</code>、<code>title</code>、<code>target</code>、<code>style</code>），转发它们只会让 <code>note</code> 的签名变成 <code>bibliography</code> 的拙劣复制。<code>bibliography(...)</code> 的返回值是一个 content 值，可以像别的 content 一样存进变量、当参数传递；把它直接传进来，<code>note</code> 就不需要知道里面有什么。</p><p><strong>参数叫 <code>bibliography</code>，会遮蔽标准库的同名函数。</strong> <code>note</code> 的参数就叫 <code>bibliography</code>，在函数体里直接写 <code>bibliography</code> 拿到的是参数，上面末尾那段用的就是这个。如果你在 <code>note</code> 里还想调用标准库的 <code>bibliography(...)</code> 函数本身，要写 <code>std.bibliography</code>。现在这个文件不需要，但我保留这条说明，因为文件原先正是用它写了一条 <code>show std.bibliography: ...</code> 规则。</p><p><strong>为什么没有一条把参考文献标题设成不编号的规则。</strong> <code>xuan-base</code> 设了 <code>heading.numbering: "1.1"</code>，文件原先在 <code>note</code> 里加了 <code>show std.bibliography: set heading(numbering: none)</code>，想让“参考文献”前面不带数字。后来发现它是多余的：不经过 <code>note</code> 的 <code>bib-basic</code> 里没有这条规则，标题也没有编号；我还把 <code>note.typ</code> 里这一行删掉，在 <code>/tmp</code> 里用拷贝的包重新编译 <code>examples/notes/main.typ</code>，<code>pdftotext -layout</code> 的输出和带规则的版本逐字相同。所以在 0.15.1 里 <code>bibliography</code> 的标题默认就不编号，这一行已经从仓库里删掉，上面展示的是删除之后的 HEAD 版本。</p><p>整篇笔记的最后一页（<code>examples/notes/main.typ</code> 编译出的 <code>out/examples/notes.pdf</code> 第 4 页，用 <code>typst compile --pages 4 --format png --ppi 72</code> 取出）：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch10-notes-bib.png" alt="examples/notes 的第 4 页：先是术语表的最后几条，然后是未编号的“参考文献”标题和三条 GB/T 7714 格式的条目，页脚为“第 4 页 / 共 4 页”" /><figcaption>examples/notes 的第 4 页：先是术语表的最后几条，然后是未编号的“参考文献”标题和三条 GB/T 7714 格式的条目，页脚为“第 4 页 / 共 4 页”</figcaption></figure><p></p><p>这一页上面的术语表是第 15 篇的内容；现在只需要看下半部分：标题“参考文献”没有编号，三条是 <code>[1]</code> 到 <code>[3]</code>，格式和 <code>bib-basic</code> 里一模一样。<code>examples/notes/refs.bib</code> 与 <code>lab/ch10/refs.bib</code> 逐字相同（我用 <code>diff</code> 比过）。</p></section>
<section><h2>留给你的几条路<a href="#留给你的几条路"><span>#</span></a></h2><p>如果你要排毕业论文，国标有两种：顺序编码制（<code>gb-7714-2015-numeric</code>）和著者出版年制（<code>gb-7714-2015-author-date</code>，我用它编译过 <code>/tmp/p10/t1.typ</code>，正文引用变成 <code>(Strang, 2023)</code> 与 <code>(Golub 等, 2013)</code>）。学校要求哪个就选哪个；学校的细则与内置样式不一致时，轮到 CSL 文件出场：在样式仓库里找最接近的一个，拷下来保留许可证头，再改你要动的 <code>&lt;macro&gt;</code>。如果数据已经躺在 Zotero 里，导出 <code>.bib</code> 直接用。如果你想在文档里读某条文献的某个字段（比如在页脚打印书名），<code>bibliography</code> 不是为这件事设计的，那得回到下一篇的数据加载。</p><p><code>make ch10</code> 通过时 6 个文件各输出一行 <code>ok</code>，没有 <code>*.fail.typ</code>，所以这一章的“失败演示”（<code>@nokey</code>、重复键、未知样式名）都是我在 <code>/tmp/p10</code> 里做的，仓库里没有对应文件。下一篇换个方向，用一份 YAML 生成中英文两版简历：<a href="/posts/typst-by-xuan-11-data-driven-cv/">第 11 篇</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-09-figures-tables-code/</id>
      <title type="text">图、表、代码与交叉引用</title>
      <published>2026-10-19T08:00:00.000Z</published>
      <updated>2026-10-19T08:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-09-figures-tables-code/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 9 篇：我用 Typst 0.15.1 编译了 ch09 的 9 个 lab 文件和一批 /tmp 里的实验，复现了图编号在引用处和目录里被算错的两处 bug，再写出 xuan 的 figure、units、code 三个模块，让图表按章编号、三线表和单位输出开箱可用，并用 tests/units.typ 把 num、qty 的行为钉死。</summary>
      <content type="html"><![CDATA[<p>写到图表，我先把 <code>xuan</code> 里按章编号的做法拆掉，看会坏成什么样。结果是第 1 章里的图被第 2 章的 <code>@a</code> 引用成了“图 2.1”。这一篇的主线就是这个 bug 的两个变种（引用处，目录里），以及 Typst 的 <code>figure</code>、<code>table</code>、<code>raw</code> 这几个元素怎么配合，才能让一份笔记里的图、表、代码清单都带着“章.序号”的编号、能被 <code>@label</code> 引用、风格统一。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有三个最终版文件：<code>src/figure.typ</code>、<code>src/units.typ</code>、<code>src/code.typ</code>。<code>lib.typ</code> 只新增一行（导出 <code>num</code>、<code>qty</code>、<code>qty-range</code>），<code>src/base.typ</code> 是<strong>中间版本</strong>：它在这一篇之前已经接了 <code>math-rules</code>，这一篇的内容是 <code>figure-rules</code> 与 <code>code-rules</code> 两行 import 和两行 <code>show:</code>，我会贴出整个文件，并在正文里指出新增的 import 与 <code>show:</code> 是哪几行。<code>lab/ch09/</code> 下的 9 个文件全部通过 <code>make ch09</code>，<code>make test</code> 里的 <code>tests/units.typ</code> 通过。
前置：<a href="/posts/typst-by-xuan-07-context-counters-state/">第 7 篇</a>（<code>counter</code>、<code>context</code>、定理环境里的 <code>show ref</code>）。图表的编号和定理是同一套机制，那一篇没读的话，下面“为什么要在引用处重算”一节会显得突兀。</p></div></div>
<section><h2>figure：任何东西都可以是一张图<a href="#figure任何东西都可以是一张图"><span>#</span></a></h2><p>先看 <code>lab/ch09/figures.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/figures.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// figure: kinds, captions, labels, placement. Numbers are &lt;chapter&gt;.&lt;n&gt;.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>=</span><span> 投影</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>9</div></div><div><span>  </span><span>image</span><span>(</span><span>"assets/projection.svg"</span><span>, </span><span>width</span><span>: </span><span>60%</span><span>),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>caption</span><span>: [</span><span>$</span><span>b</span><span>$</span><span> 在 </span><span>$</span><span>C(A)</span><span>$</span><span> 上的投影。],</span></div></div><div><div><div>11</div></div><div><span>) </span><span>&lt;fig-proj&gt;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>@fig-proj</span><span> 的残差与列空间正交。第二张图同章，编号递增：</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>16</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>3cm</span><span>, </span><span>height</span><span>: </span><span>1cm</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>230</span><span>))[占位],</span></div></div><div><div><div>17</div></div><div><span>  </span><span>caption</span><span>: [任意内容都可以放进 figure，kind 自动推断为 </span><span>`image`</span><span>。],</span></div></div><div><div><div>18</div></div><div><span>) </span><span>&lt;fig-rect&gt;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>=</span><span> 下一章</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>23</div></div><div><span>  </span><span>image</span><span>(</span><span>"assets/projection.svg"</span><span>, </span><span>width</span><span>: </span><span>40%</span><span>),</span></div></div><div><div><div>24</div></div><div><span>  </span><span>caption</span><span>: [编号在新章重置。],</span></div></div><div><div><div>25</div></div><div><span>  </span><span>placement</span><span>: </span><span>auto</span><span>, </span><span>// float to the top or bottom of the page</span></div></div><div><div><div>26</div></div><div><span>) </span><span>&lt;fig-again&gt;</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>见 </span><span>@fig-proj</span><span>、</span><span>@fig-rect</span><span> 与 </span><span>@fig-again</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>figure</code> 把一段内容包起来，加上标题、编号和 supplement（“图”“表”这个前缀词）。几个值得停一下的点。</p><p><strong><code>kind</code> 决定编号归哪个计数器、supplement 叫什么。</strong> 我没有写 <code>kind</code>，Typst 会看 body：<code>image(...)</code> 是 <code>image</code>，<code>table(...)</code> 是 <code>table</code>，<code>raw</code> 块是 <code>raw</code>。第二张图里放的是一个 <code>rect</code>，它也叫“图 1.2”，源文件注释里写的就是这个。自定义 <code>kind</code> 可以是任意字符串，我在后面的子图和“演示”例子里用。<code>raw</code> 块作为 figure 的 kind 我也编译了一遍，在 <code>figure.typ</code> 那一节后面。</p><p><strong><code>placement: auto</code></strong> 让图成为浮动体：Typst 自己挑页面顶部或底部放。渲染图里最后那张“图 2.1”被甩到了页面最上面，在“1 投影”标题之前，这就是浮动。（这个 lab 页面是 <code>height: auto</code>，所以“页面顶部”就是整个文档的最上面，看起来有点滑稽。）不浮动时，图是一个块，按出现位置排；<code>caption</code> 位置默认在图下方，下面讲表格时会改成上方。</p><p><strong><code>&lt;fig-proj&gt;</code> 这种 label 贴在 <code>figure</code> 上。</strong> 之后 <code>@fig-proj</code> 就能引用。label 必须紧跟在内容之后：如果 <code>figure(...)</code> 本身出现在函数调用的参数里，写成 <code>f(figure(...) &lt;l&gt;, 1)</code> 是语法错误，我在 <code>/tmp/p09/exp/lab.typ</code> 里试过（实验文件，不在仓库里），报 <code>error: expected comma</code>。把它包进内容块 <code>[#figure(...) &lt;l&gt;]</code> 就合法了。</p><p>在仓库里编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make ch09</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch09</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch09/fig-code.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch09/fig-table.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch09/figures.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch09/outline.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch09/raw.typ</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch09/refs.typ</span></div></div><div><div><div>9</div></div><div><span>ok   lab/ch09/subfigures.typ</span></div></div><div><div><div>10</div></div><div><span>ok   lab/ch09/tables.typ</span></div></div><div><div><div>11</div></div><div><span>ok   lab/ch09/units.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>渲染出来是这样（在仓库根目录用 <code>typst compile --root . --ignore-system-fonts --format png --ppi 110 lab/ch09/figures.typ figures.png</code> 生成，本篇其他 lab 渲染图同理）：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-figures.png" alt="figures.typ 的渲染：第 1 章的投影图编号为图 1.1，其后的灰色占位矩形为图 1.2，第 2 章的图因为 placement: auto 浮到页面最顶端，编号重置为图 2.1，末尾的引用依次显示为图 1.1、图 1.2 与图 2.1" /><figcaption>figures.typ 的渲染：第 1 章的投影图编号为图 1.1，其后的灰色占位矩形为图 1.2，第 2 章的图因为 placement: auto 浮到页面最顶端，编号重置为图 2.1，末尾的引用依次显示为图 1.1、图 1.2 与图 2.1</figcaption></figure><p></p><p>编号“1.1”“2.1”不是 Typst 的默认行为。默认的图编号是全文连续的 1、2、3，“按章编号”是 <code>xuan</code> 的 <code>figure.typ</code> 做的，下面就来看它。</p></section>
<section><h2>为什么 ref 的编号要在引用处之外算<a href="#为什么-ref-的编号要在引用处之外算"><span>#</span></a></h2><p>要按章编号，需要两样东西：标题计数器（第几章）和图计数器（本章第几张）。天真的写法是给 <code>figure</code> 设一个编号函数，在里面读 <code>counter(heading)</code>。这个写法我在 <code>/tmp/p09/exp/noref.typ</code> 里试了（实验文件，不在仓库里）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>image</span><span>)).</span><span>update</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>it</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>#set</span><span> </span><span>figure</span><span>(</span><span>numbering</span><span>: </span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>context</span><span> </span><span>numbering</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>"1.1"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>().</span><span>first</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>n</span><span>,</span></div></div><div><div><div>11</div></div><div><span>))</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>=</span><span> 一</span></div></div><div><div><div>14</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>[甲], </span><span>caption</span><span>: [甲图]) </span><span>&lt;a&gt;</span></div></div><div><div><div>15</div></div><div><span>=</span><span> 二</span></div></div><div><div><div>16</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>[乙], </span><span>caption</span><span>: [乙图]) </span><span>&lt;b&gt;</span></div></div><div><div><div>17</div></div><div><span>见 </span><span>@a</span><span> 与 </span><span>@b</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts noref.typ noref.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext -layout noref.pdf -</span></div></div><div><div><div>3</div></div><div><span>1一</span></div></div><div><div><div>4</div></div><div><span><span>                      </span></span><span>甲</span></div></div><div><div><div>5</div></div><div><span><span>                   </span></span><span>图 1.1 甲图</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>2二</span></div></div><div><div><div>8</div></div><div><span><span>                      </span></span><span>乙</span></div></div><div><div><div>9</div></div><div><span><span>                   </span></span><span>图 2.1 乙图</span></div></div><div><div><div>10</div></div><div><span>见 图 2.1 与 图 2.1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>图下面的标题是对的，“见”那一行错了：<code>@a</code> 指向第 1 章的甲图，却被排成了“图 2.1”。</p><p>机制是这样的。<code>@a</code> 展开成一个 <code>ref</code> 元素，它要显示“图 N”，就得拿 <code>figure</code> 的 <code>numbering</code> 对这张图的计数值做格式化。我写的 <code>numbering</code> 是 <code>n =&gt; context numbering("1.1", counter(heading).get().first(), n)</code>，这个 <code>context</code> 是在<strong>调用 numbering 的地方</strong>求值的。图标题里调用它，<code>counter(heading).get()</code> 就是“此刻的章号”，对。<code>ref</code> 里调用它，“此刻”变成了引用所在的位置，也就是第 2 章，于是甲图拿到了乙图的章号。<code>ref</code> 并没有记下“这个编号是在图那里算出来的”。</p><p>打个比方：<code>counter.get()</code> 像看银行 App 里此刻的余额，<code>counter.at(loc)</code> 像调出某一天的对账单。我要的永远是图所在那一天的对账单，而不是翻开 App 那一刻的余额。第 7 篇的定理环境用的是同一招。</p><p>修法是不让 <code>ref</code> 自己去求值：重写 <code>show ref</code>，从 <code>it.element</code> 取出被引用的那张图，用它的 <code>location()</code> 读两个计数器在<strong>图所在位置</strong>的值。<code>/tmp/p09/exp/withref.typ</code>（同样是实验文件）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>image</span><span>)).</span><span>update</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>it</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>#set</span><span> </span><span>figure</span><span>(</span><span>numbering</span><span>: </span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>context</span><span> </span><span>numbering</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>"1.1"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>().</span><span>first</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>n</span><span>,</span></div></div><div><div><div>11</div></div><div><span>))</span></div></div><div><div><div>12</div></div><div><span>#show</span><span> </span><span>ref</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>13</div></div><div><span>  </span><span>let</span><span> </span><span>el</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>element</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> </span><span>el</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>func</span><span><span>() </span><span>==</span><span> </span></span><span>figure</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>loc</span><span><span> </span><span>= </span></span><span>el</span><span>.</span><span>location</span><span>()</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>numbering</span><span>(</span></div></div><div><div><div>17</div></div><div><span>      </span><span>"1.1"</span><span>,</span></div></div><div><div><div>18</div></div><div><span>      </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>19</div></div><div><span>      </span><span>el</span><span>.</span><span>counter</span><span>.</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>link</span><span>(</span><span>loc</span><span>, [</span><span>#el</span><span>.</span><span>supplement</span><span>~</span><span>#n</span><span>])</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> { </span><span>it</span><span> }</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>=</span><span> 一</span></div></div><div><div><div>26</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>[甲], </span><span>caption</span><span>: [甲图]) </span><span>&lt;a&gt;</span></div></div><div><div><div>27</div></div><div><span>=</span><span> 二</span></div></div><div><div><div>28</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>[乙], </span><span>caption</span><span>: [乙图]) </span><span>&lt;b&gt;</span></div></div><div><div><div>29</div></div><div><span>见 </span><span>@a</span><span> 与 </span><span>@b</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts withref.typ withref.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext -layout withref.pdf -</span></div></div><div><div><div>3</div></div><div><span>1一</span></div></div><div><div><div>4</div></div><div><span><span>                      </span></span><span>甲</span></div></div><div><div><div>5</div></div><div><span><span>                   </span></span><span>图 1.1 甲图</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>2二</span></div></div><div><div><div>8</div></div><div><span><span>                      </span></span><span>乙</span></div></div><div><div><div>9</div></div><div><span><span>                   </span></span><span>图 2.1 乙图</span></div></div><div><div><div>10</div></div><div><span>见 图 1.1 与 图 2.1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对了。<code>el.counter.at(loc)</code> 的 <code>el.counter</code> 是这张图所属 kind 的计数器，<code>counter(heading).at(loc)</code> 是标题计数器，<code>.first()</code> 取顶层章号。两个值都由 <code>loc</code> 决定，跟谁来引用无关。</p></section>
<section><h2>figure.typ：把这套规则收进模块<a href="#figuretyp把这套规则收进模块"><span>#</span></a></h2><p>最终版 <code>src/figure.typ</code>（63 行，<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/figure.typ" target="_blank">源文件</a>）完整贴在这里，下面逐段拆：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Figures and tables numbered "&lt;chapter&gt;.&lt;n&gt;", three-line tables.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>figure-kinds</span><span><span> </span><span>= </span><span>(</span></span><span>image</span><span>, </span><span>table</span><span>, </span><span>raw</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>/// "&lt;chapter&gt;.&lt;n&gt;" for the figure `el`, evaluated at its own location.</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>figure-number</span><span>(</span><span>el</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>let</span><span> </span><span>loc</span><span><span> </span><span>= </span></span><span>el</span><span>.</span><span>location</span><span>()</span></div></div><div><div><div>8</div></div><div><span>  </span><span>numbering</span><span>(</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"1.1"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>11</div></div><div><span>    </span><span>el</span><span>.</span><span>counter</span><span>.</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>#let</span><span> </span><span>figure-rules</span><span>(</span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>16</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>figure-kinds</span><span> { </span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>k</span><span>)).</span><span>update</span><span>(</span><span>0</span><span>) }</span></div></div><div><div><div>18</div></div><div><span>    </span><span>it</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>  </span><span>set</span><span> </span><span>figure</span><span>(</span><span>numbering</span><span>: </span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>context</span><span> </span><span>numbering</span><span>(</span></div></div><div><div><div>21</div></div><div><span>    </span><span>"1.1"</span><span>,</span></div></div><div><div><div>22</div></div><div><span>    </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>().</span><span>first</span><span>(),</span></div></div><div><div><div>23</div></div><div><span>    </span><span>n</span><span>,</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>))</span></div></div><div><div><div>25</div></div><div><span>  </span><span>show</span><span> </span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>table</span><span>): </span><span>set</span><span> </span><span>figure</span><span>.</span><span>caption</span><span>(</span><span>position</span><span>: </span><span>top</span><span>)</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>  </span><span>// Three-line tables: no verticals, rules above, below the header, below.</span></div></div><div><div><div>28</div></div><div><span>  </span><span>set</span><span> </span><span>table</span><span>(</span><span>stroke</span><span>: (</span><span>x</span><span>, </span><span>y</span><span>) </span><span>=&gt;</span><span> (</span></div></div><div><div><div>29</div></div><div><span>    </span><span>top</span><span>: </span><span>if</span><span> </span><span>y</span><span><span> </span><span>==</span><span> </span></span><span>0</span><span> { </span><span>1pt</span><span> } </span><span>else</span><span> </span><span>if</span><span> </span><span>y</span><span><span> </span><span>==</span><span> </span></span><span>1</span><span> { </span><span>0.5pt</span><span> } </span><span>else</span><span> { </span><span>0pt</span><span> },</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>))</span></div></div><div><div><div>31</div></div><div><span>  </span><span>show</span><span> </span><span>table</span><span>.</span><span>cell</span><span>.</span><span>where</span><span>(</span><span>y</span><span>: </span><span>0</span><span>): </span><span>strong</span></div></div><div><div><div>32</div></div><div><span>  </span><span>// The closing 1pt rule is an hline after the last row.</span></div></div><div><div><div>33</div></div><div><span>  </span><span>show</span><span> </span><span>table</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>let</span><span> </span><span>f</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>fields</span><span>()</span></div></div><div><div><div>35</div></div><div><span>    </span><span>let</span><span> </span><span>cells</span><span><span> </span><span>= </span></span><span>f</span><span>.</span><span>remove</span><span>(</span><span>"children"</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>let</span><span> </span><span>closed</span><span><span> </span><span>= </span></span><span>cells</span><span>.</span><span>len</span><span><span>() </span><span>&gt;</span><span> </span></span><span>0</span><span> </span><span>and</span><span> </span><span>cells</span><span>.</span><span>last</span><span>().</span><span>func</span><span><span>() </span><span>==</span><span> </span></span><span>table</span><span>.</span><span>hline</span></div></div><div><div><div>37</div></div><div><span>    </span><span>if</span><span> </span><span>closed</span><span> { </span><span>it</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>38</div></div><div><span>      </span><span>table</span><span>(..</span><span>f</span><span>, ..</span><span>cells</span><span>, </span><span>table</span><span>.</span><span>hline</span><span>(</span><span>stroke</span><span>: </span><span>1pt</span><span>))</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>  </span><span>// Outline entries evaluate `numbering` at the outline, not at the figure.</span></div></div><div><div><div>43</div></div><div><span>  </span><span>show</span><span> </span><span>outline</span><span>.</span><span>entry</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>44</div></div><div><span>    </span><span>let</span><span> </span><span>el</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>element</span></div></div><div><div><div>45</div></div><div><span>    </span><span>if</span><span> </span><span>el</span><span>.</span><span>func</span><span><span>() </span><span>==</span><span> </span></span><span>figure</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>kind</span><span> </span><span>in</span><span> </span><span>figure-kinds</span><span> {</span></div></div><div><div><div>46</div></div><div><span>      </span><span>link</span><span>(</span><span>el</span><span>.</span><span>location</span><span>(), </span><span>it</span><span>.</span><span>indented</span><span>(</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>[</span><span>#el</span><span>.</span><span>supplement</span><span>~</span><span>#figure-number</span><span>(</span><span>el</span><span>)],</span></div></div><div><div><div>48</div></div><div><span>        </span><span>it</span><span>.</span><span>inner</span><span>(),</span></div></div><div><div><div>49</div></div><div><span><span>      </span></span><span>))</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> { </span><span>it</span><span> }</span></div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>  </span><span>show</span><span> </span><span>ref</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>54</div></div><div><span>    </span><span>let</span><span> </span><span>el</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>element</span></div></div><div><div><div>55</div></div><div><span>    </span><span>if</span><span> </span><span>el</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>func</span><span><span>() </span><span>==</span><span> </span></span><span>figure</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>kind</span><span> </span><span>in</span><span> </span><span>figure-kinds</span><span> {</span></div></div><div><div><div>56</div></div><div><span>      </span><span>let</span><span> </span><span>supp</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>it</span><span>.</span><span>supplement</span><span><span> </span><span>==</span><span> </span></span><span>auto</span><span> { </span><span>el</span><span>.</span><span>supplement</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>57</div></div><div><span>        </span><span>it</span><span>.</span><span>supplement</span></div></div><div><div><div>58</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span>      </span><span>link</span><span>(</span><span>el</span><span>.</span><span>location</span><span>(), [</span><span>#supp</span><span>~</span><span>#figure-number</span><span>(</span><span>el</span><span>)])</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> { </span><span>it</span><span> }</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span>  </span><span>body</span></div></div><div><div><div>63</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong><code>figure-kinds = (image, table, raw)</code></strong>：这是三种会按章编号的 kind。放在一个数组里，章节重置、<code>show ref</code>、<code>show outline.entry</code> 三处都用它判断“这是不是我的图”。<code>subfigure</code>、自定义 kind 不在里面，所以它们走 Typst 默认的编号，后面会看到。</p><p><strong><code>figure-number(el)</code></strong>：上一节实验里那段计算，抽成函数。<code>numbering("1.1", 章, 序)</code> 把两个整数拼成“2.1”，这里的 <code>"1.1"</code> 是 numbering 模式字符串，两个 <code>1</code> 各吃一个参数。</p><p><strong>章节重置：</strong> <code>show heading.where(level: 1)</code> 先对三种 kind 各调一次 <code>counter(figure.where(kind: k)).update(0)</code>，再把标题原样输出。这样每进入新章，图、表、代码清单的序号都归零。<code>it</code> 原样返回，所以标题的外观不受影响。</p><p><strong><code>set figure(numbering: ...)</code></strong>：图自己的标题里显示的编号。这里允许用 <code>context</code>，因为标题就在图里面，“此刻”和图的位置一致，不会错。</p><p><strong><code>show figure.where(kind: table): set figure.caption(position: top)</code></strong>：表题放在上面，图题保持默认在下面。这是 show-set 规则，第 4 篇讲过，对 <code>kind: table</code> 的图才生效，<code>raw</code> 清单仍是题在下。</p><p><strong>三线表：</strong> <code>set table(stroke: (x, y) =&gt; (top: ...))</code> 给 <code>stroke</code> 一个函数，参数是单元格的列号和行号。第 0 行上边 1pt，第 1 行上边 0.5pt（也就是表头下面），其余 0pt，没有左右竖线。<code>show table.cell.where(y: 0): strong</code> 把第 0 行加粗。最后的 1pt 线在表的下沿，它不是任何一个单元格的“上边”，所以得追加一个 <code>table.hline(stroke: 1pt)</code>。</p><p><strong>这个追加的 show 规则有一个陷阱。</strong> 如果每次都无条件追加 <code>table.hline</code>，新造出来的 <code>table</code> 还是 <code>table</code>，又触发同一条 show 规则。我在 <code>/tmp/p09/exp/nocheck.typ</code> 里试了这种写法，Typst 报 <code>error: maximum show rule depth exceeded</code>，hint 是 <code>maybe a show rule matches its own output</code>。修法是看最后一个子元素是不是已经是 <code>table.hline</code>，是就原样返回，不是才追加。<code>it.fields()</code> 返回表的全部参数（包括 <code>children</code>），我把 <code>children</code> 取出来，再用 <code>..cells</code> 展开重建。第二次进入规则时最后一项已经是 <code>hline</code>，递归停止。</p><p><strong><code>show ref</code></strong>：只接管指向 <code>figure</code> 且 <code>kind</code> 在 <code>figure-kinds</code> 里的引用，其余（标题、公式、书目）原样放行。supplement 有两个来源：<code>it.supplement</code> 是用户写的 <code>@fig-proj[见图]</code> 那种方括号里的内容，<code>auto</code> 表示用户没写，这时用图自己的 <code>el.supplement</code>（中文文档里是“图”“表”）。<code>link(el.location(), ...)</code> 让引用可点击，<code>~</code> 是不换行空格。</p><p><strong><code>show outline.entry</code></strong>：这是 <code>figure.typ</code> 里最后加上的一段（52 行变成 63 行），下一节讲。</p><section><h3>代码清单也是 figure<a href="#代码清单也是-figure"><span>#</span></a></h3><p><code>figure-kinds</code> 里的第三项 <code>raw</code> 是给代码清单的。我在 <code>/tmp/p09/exp/rawfig.typ</code> 里编译了最小例子（实验文件，不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>=</span><span> 代码</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>```rust</span></div></div><div><div><div>9</div></div><div><span>  </span><span>fn</span><span> </span><span>main</span><span>() {}</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>```</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>caption</span><span>: [最小的 Rust 程序],</span></div></div><div><div><div>12</div></div><div><span>) </span><span>&lt;lst-main&gt;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>见 </span><span>@lst-main</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts rawfig.typ rawfig.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext -layout rawfig.pdf -</span></div></div><div><div><div>3</div></div><div><span>1 代码</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>1 fn main() {}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>           </span></span><span>代码 1.1 最小的 Rust 程序</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>见 代码 1.1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>raw</code> 块放进 <code>figure</code> 后 kind 自动是 <code>raw</code>，中文文档里 supplement 是“代码”，编号和图、表各自独立地按章从 1 数起，题注在下面，引用走的也是上面那条 <code>show ref</code>。</p></section></section>
<section><h2>目录里的同一个 bug<a href="#目录里的同一个-bug"><span>#</span></a></h2><p><code>lab/ch09/outline.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/outline.typ" target="_blank">源文件</a>）生成目录、图目录和表目录：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// outline: contents, and lists of figures and tables.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#outline</span><span>(</span><span>title</span><span>: [目录], </span><span>depth</span><span>: </span><span>2</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#outline</span><span>(</span><span>title</span><span>: [图目录], </span><span>target</span><span>: </span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>image</span><span>))</span></div></div><div><div><div>8</div></div><div><span>#outline</span><span>(</span><span>title</span><span>: [表目录], </span><span>target</span><span>: </span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>table</span><span>))</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>=</span><span> 一</span></div></div><div><div><div>11</div></div><div><span>==</span><span> 一点一</span></div></div><div><div><div>12</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>[图], </span><span>caption</span><span>: [第一张图])</span></div></div><div><div><div>13</div></div><div><span>=</span><span> 二</span></div></div><div><div><div>14</div></div><div><span>#figure</span><span>(</span><span>table</span><span>(</span><span>columns</span><span>: </span><span>1</span><span>)[表], </span><span>caption</span><span>: [第一张表])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>outline(target: ...)</code> 的 <code>target</code> 是一个选择器，<code>figure.where(kind: image)</code> 只列图，<code>kind: table</code> 只列表，默认的 <code>target</code> 是标题。</p><p>这个文件我第一次编译，图目录里的条目是“图 0.1 第一张图”，表目录里是“表 0.1 第一张表”，正文里却是“图 1.1”“表 2.1”。原因和上一节一样：目录条目要显示编号，就拿图的 <code>numbering</code> 在<strong>目录所在的位置</strong>求值，那里还没有任何标题，章号是 0。这是我写 <code>figure.typ</code> 时漏掉的，修在 <code>show outline.entry</code> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>// Outline entries evaluate `numbering` at the outline, not at the figure.</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>show outline.entry: it =&gt; {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>let el = it.element</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>if el.func() == figure and el.kind in figure-kinds {</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>link(el.location(), it.indented(</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>[</span><span>#el</span><span>.</span><span>supplement</span><span>~</span><span>#figure-number</span><span>(</span><span>el</span><span>)],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>it.inner(),</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>} else { it }</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>outline.entry</code> 是目录里每一行的元素，<code>it.element</code> 是它指向的东西（图、标题等）。我对指向 <code>figure</code> 的条目重建：前缀文字（<code>indented</code> 的第一个参数）改成 <code>supplement~figure-number(el)</code>，由我自己拿图的位置算；<code>it.inner()</code> 是条目的其余部分，即标题、点线和页码。标题类的条目不动。因为重建时丢了默认的链接，所以外面包一层 <code>link(el.location(), ...)</code>，渲染图里图目录和表目录的条目是蓝色的就是这个缘故，章节目录的条目没有。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext -layout out/ch09/outline.pdf - | head -14</span></div></div><div><div><div>2</div></div><div><span>目录</span></div></div><div><div><div>3</div></div><div><span>1 一 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>1.1 一点一 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div><div><div><div>5</div></div><div><span>2 二 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>图目录</span></div></div><div><div><div>10</div></div><div><span>图 1.1 第一张图 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>表目录</span></div></div><div><div><div>15</div></div><div><span>表 2.1 第一张表 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-outline.png" alt="outline.typ 的渲染：目录列出两章，图目录一行“图 1.1 第一张图”，表目录一行“表 2.1 第一张表”，这两行为蓝色可点击链接，下方正文里图 1.1 在第 1.1 节，表 2.1 在第 2 章" /><figcaption>outline.typ 的渲染：目录列出两章，图目录一行“图 1.1 第一张图”，表目录一行“表 2.1 第一张表”，这两行为蓝色可点击链接，下方正文里图 1.1 在第 1.1 节，表 2.1 在第 2 章</figcaption></figure><p></p><p>（我没有给 <code>xuan</code> 的 <code>note</code> 默认加图目录，需要时自己 <code>#outline(title: [图目录], target: figure.where(kind: image))</code>。）</p></section>
<section><h2>表格：三线表之外的 table<a href="#表格三线表之外的-table"><span>#</span></a></h2><p>前面说三线表的规则在 <code>figure.typ</code> 里，现在看 <code>lab/ch09/tables.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/tables.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// table: header, cell spans, hline/vline, fill and align functions.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>=</span><span> 数据</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>9</div></div><div><span>  </span><span>table</span><span>(</span></div></div><div><div><div>10</div></div><div><span>    </span><span>columns</span><span>: (</span><span>auto</span><span>, </span><span>1fr</span><span>, </span><span>1fr</span><span>),</span></div></div><div><div><div>11</div></div><div><span>    </span><span>align</span><span>: (</span><span>left</span><span>, </span><span>right</span><span>, </span><span>right</span><span>),</span></div></div><div><div><div>12</div></div><div><span>    </span><span>table</span><span>.</span><span>header</span><span>[方法][误差][耗时],</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>[正规方程], [1.2e-3], [4.1 ms],</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>[QR], [8.0e-4], [6.3 ms],</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>[SVD], [7.9e-4], [19.4 ms],</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>17</div></div><div><span>  </span><span>caption</span><span>: [三种最小二乘解法。表题在上，三线表由 xuan 提供。],</span></div></div><div><div><div>18</div></div><div><span>) </span><span>&lt;tab-methods&gt;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>// Everything below is plain `table`: spans, extra rules, fills.</span></div></div><div><div><div>21</div></div><div><span>#table</span><span>(</span></div></div><div><div><div>22</div></div><div><span>  </span><span>columns</span><span>: </span><span>4</span><span>,</span></div></div><div><div><div>23</div></div><div><span>  </span><span>align</span><span>: </span><span>center</span><span>,</span></div></div><div><div><div>24</div></div><div><span>  </span><span>fill</span><span>: (</span><span>x</span><span>, </span><span>y</span><span>) </span><span>=&gt;</span><span> </span><span>if</span><span> </span><span>y</span><span><span> </span><span>&gt;</span><span> </span></span><span>0</span><span> </span><span>and</span><span> </span><span>calc</span><span>.</span><span>odd</span><span>(</span><span>y</span><span>) { </span><span>luma</span><span>(</span><span>245</span><span>) },</span></div></div><div><div><div>25</div></div><div><span>  </span><span>table</span><span>.</span><span>header</span><span>(</span><span>table</span><span>.</span><span>cell</span><span>(</span><span>colspan</span><span>: </span><span>4</span><span>)[合并表头]),</span></div></div><div><div><div>26</div></div><div><span>  </span><span>table</span><span>.</span><span>hline</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>),</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>[a], [b], [c], [d],</span></div></div><div><div><div>28</div></div><div><span>  </span><span>table</span><span>.</span><span>cell</span><span>(</span><span>rowspan</span><span>: </span><span>2</span><span>)[跨行], [1], [2], [3],</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>[4], [5], [6],</span></div></div><div><div><div>30</div></div><div><span>  </span><span>table</span><span>.</span><span>footer</span><span>[页脚][-][-][-],</span></div></div><div><div><div>31</div></div><div><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>见 </span><span>@tab-methods</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-tables.png" alt="tables.typ 的渲染：上方是带表题“表 1.1”的三线表，三行数据，表头加粗；下方是合并表头、跨行单元格和页脚的第二张表，奇数行有浅灰底色" /><figcaption>tables.typ 的渲染：上方是带表题“表 1.1”的三线表，三行数据，表头加粗；下方是合并表头、跨行单元格和页脚的第二张表，奇数行有浅灰底色</figcaption></figure><p></p><p>第一张表是正常用法：<code>columns: (auto, 1fr, 1fr)</code> 一列按内容、两列平分剩余宽度，<code>align: (left, right, right)</code> 按列设对齐，<code>table.header[...]</code> 声明表头行。三线表的线全部来自 <code>xuan</code>，这里一行线条参数都没有写。</p><p>第二张表是 <code>table</code> 的几个其他功能，值得注意它也画成了三线的样子，因为 <code>xuan-base</code> 里的 <code>set table(stroke: ...)</code> 对整篇文档生效：</p><ul>
<li><code>fill: (x, y) =&gt; ...</code>：<code>fill</code> 也可以是函数，这里给奇数行铺浅灰色。函数返回 <code>none</code> 表示不填，所以 <code>if</code> 没有 <code>else</code> 也可以。</li>
<li><code>table.cell(colspan: 4)[...]</code> 与 <code>rowspan: 2</code>：跨列和跨行，单元格里写 <code>table.cell</code> 才能带参数。</li>
<li><code>table.hline(stroke: 0.5pt)</code>：在当前位置画一条横线。和它成对的 <code>table.vline</code> 画竖线。</li>
<li><code>table.footer[...]</code>：页脚行，和 <code>table.header</code> 对应。</li>
</ul><p>三线表和单位放在一起的例子是 <code>lab/ch09/fig-table.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/fig-table.typ" target="_blank">源文件</a>），表里的数字和耗时用了下面要讲的 <code>num</code> 与 <code>qty</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>7</div></div><div><span>  </span><span>table</span><span>(</span></div></div><div><div><div>8</div></div><div><span>    </span><span>columns</span><span>: (</span><span>auto</span><span>, </span><span>auto</span><span>, </span><span>auto</span><span>),</span></div></div><div><div><div>9</div></div><div><span>    </span><span>align</span><span>: (</span><span>left</span><span>, </span><span>right</span><span>, </span><span>right</span><span>),</span></div></div><div><div><div>10</div></div><div><span>    </span><span>table</span><span>.</span><span>header</span><span>[方法][误差][耗时],</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>[正规方程], [</span><span>#num</span><span>(</span><span>"0.0012"</span><span>)], [</span><span>#qty</span><span>(</span><span>4.1</span><span>, </span><span>"ms"</span><span>)],</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>[QR], [</span><span>#num</span><span>(</span><span>"0.0008"</span><span>)], [</span><span>#qty</span><span>(</span><span>6.3</span><span>, </span><span>"ms"</span><span>)],</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>[SVD], [</span><span>#num</span><span>(</span><span>"0.00079"</span><span>)], [</span><span>#qty</span><span>(</span><span>19.4</span><span>, </span><span>"ms"</span><span>)],</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>15</div></div><div><span>  </span><span>caption</span><span>: [三线表与单位],</span></div></div><div><div><div>16</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-table.png" alt="fig-table.typ 的渲染：表题“表 0.1 三线表与单位”在上方，三线表四行，表头加粗，误差列是 0.0012、0.0008、0.00079，耗时列是 4.1 ms、6.3 ms、19.4 ms，数字与单位之间有细空格" /><figcaption>fig-table.typ 的渲染：表题“表 0.1 三线表与单位”在上方，三线表四行，表头加粗，误差列是 0.0012、0.0008、0.00079，耗时列是 4.1 ms、6.3 ms、19.4 ms，数字与单位之间有细空格</figcaption></figure><p></p><p>编号是“表 0.1”而不是“表 1.1”：这个文件里没有任何标题，章号取的是标题计数器的当前值 0。这不是 bug，是 <code>"1.1"</code> 这个编号规则在没有章的情况下的诚实结果；真正的笔记里表总在某一章之下。<code>num("0.00079")</code> 的小数部分有 5 位，从左起每 3 位一组，所以图里是 <code>0.000 79</code>；<code>0.0012</code> 只有 4 位，不分组。</p><p>我另外做了一个 <code>/tmp</code> 里的实验（不在仓库里），把 <code>stroke</code>、<code>fill</code>、<code>align</code> 都写成函数，再加 <code>table.cell(fill:)</code>、<code>table.cell(align:)</code>、<code>table.vline</code>、<code>grid.cell(colspan:)</code>，并让 <code>figure</code> 带一个自定义 <code>kind</code>。<code>/tmp/p09/exp/grid.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>, </span><span>numbering</span><span>: </span><span>"1"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div><span>=</span><span> 方法</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>table</span><span>(</span></div></div><div><div><div>9</div></div><div><span>    </span><span>columns</span><span>: (</span><span>auto</span><span>, </span><span>1fr</span><span>, </span><span>1fr</span><span>),</span></div></div><div><div><div>10</div></div><div><span>    </span><span>align</span><span>: (</span><span>x</span><span>, </span><span>y</span><span>) </span><span>=&gt;</span><span> </span><span>if</span><span> </span><span>x</span><span><span> </span><span>==</span><span> </span></span><span>0</span><span> { </span><span>left</span><span> } </span><span>else</span><span> { </span><span>right</span><span> },</span></div></div><div><div><div>11</div></div><div><span>    </span><span>stroke</span><span>: (</span><span>x</span><span>, </span><span>y</span><span>) </span><span>=&gt;</span><span> (</span></div></div><div><div><div>12</div></div><div><span>      </span><span>top</span><span>: </span><span>if</span><span> </span><span>y</span><span><span> </span><span>==</span><span> </span></span><span>0</span><span> { </span><span>1pt</span><span> } </span><span>else</span><span> </span><span>if</span><span> </span><span>y</span><span><span> </span><span>==</span><span> </span></span><span>1</span><span> { </span><span>0.5pt</span><span> },</span></div></div><div><div><div>13</div></div><div><span>      </span><span>left</span><span>: </span><span>if</span><span> </span><span>x</span><span><span> </span><span>==</span><span> </span></span><span>2</span><span> { </span><span>0.5pt</span><span><span> </span><span>+</span><span> </span></span><span>gray</span><span> },</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>),</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fill</span><span>: (</span><span>x</span><span>, </span><span>y</span><span>) </span><span>=&gt;</span><span> </span><span>if</span><span> </span><span>y</span><span><span> </span><span>&gt;</span><span> </span></span><span>0</span><span> </span><span>and</span><span> </span><span>calc</span><span>.</span><span>even</span><span>(</span><span>y</span><span>) { </span><span>luma</span><span>(</span><span>245</span><span>) },</span></div></div><div><div><div>16</div></div><div><span>    </span><span>table</span><span>.</span><span>header</span><span>[方法][误差][耗时],</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>[A], </span><span>table</span><span>.</span><span>cell</span><span>(</span><span>fill</span><span>: </span><span>yellow</span><span>.</span><span>lighten</span><span>(</span><span>60%</span><span>))[</span><span>#highlight</span><span>[1.2e-3]], [4.1],</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>[B], [8.0e-4], </span><span>table</span><span>.</span><span>cell</span><span>(</span><span>align</span><span>: </span><span>center</span><span>)[6.3],</span></div></div><div><div><div>19</div></div><div><span>    </span><span>table</span><span>.</span><span>vline</span><span>(</span><span>x</span><span>: </span><span>1</span><span>, </span><span>stroke</span><span>: </span><span>0.5pt</span><span><span> </span><span>+</span><span> </span></span><span>red</span><span>),</span></div></div><div><div><div>20</div></div><div><span>    </span><span>table</span><span>.</span><span>footer</span><span>[均值][1.0e-3][5.2],</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>22</div></div><div><span>  </span><span>caption</span><span>: [stroke、fill、align 都可以是函数],</span></div></div><div><div><div>23</div></div><div><span>)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>26</div></div><div><span>  </span><span>grid</span><span>(</span></div></div><div><div><div>27</div></div><div><span>    </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>28</div></div><div><span>    </span><span>gutter</span><span>: </span><span>1em</span><span>,</span></div></div><div><div><div>29</div></div><div><span>    </span><span>grid</span><span>.</span><span>cell</span><span>(</span><span>colspan</span><span>: </span><span>2</span><span>)[</span><span>#rect</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>)[跨两列]],</span></div></div><div><div><div>30</div></div><div><span>    </span><span>rect</span><span>[左], </span><span>rect</span><span>[右],</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>32</div></div><div><span>  </span><span>kind</span><span>: </span><span>"demo"</span><span>,</span></div></div><div><div><div>33</div></div><div><span>  </span><span>supplement</span><span>: [演示],</span></div></div><div><div><div>34</div></div><div><span>  </span><span>numbering</span><span>: </span><span>"A"</span><span>,</span></div></div><div><div><div>35</div></div><div><span>  </span><span>caption</span><span>: [自定义 kind],</span></div></div><div><div><div>36</div></div><div><span>) </span><span>&lt;demo&gt;</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>见 </span><span>@demo</span><span>，页码形式：</span><span>@demo</span><span>[页]，</span><span>@demo</span><span>. </span><span>#ref</span><span>(</span><span>&lt;demo&gt;</span><span>, </span><span>form</span><span>: </span><span>"page"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts grid.typ grid.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext -layout grid.pdf - | head -12</span></div></div><div><div><div>3</div></div><div><span>1 方法</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>表 1.1 stroke、fill、align 都可以是函数</span></div></div><div><div><div>5</div></div><div><span>方法                误差                  耗时</span></div></div><div><div><div>6</div></div><div><span>A                1.2e-3               4.1</span></div></div><div><div><div>7</div></div><div><span>B                8.0e-4        6.3</span></div></div><div><div><div>8</div></div><div><span>均值               1.0e-3               5.2</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>                 </span></span><span>跨两列</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span> </span></span><span>左                    右</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>演示 A 自定义 kind</span></div></div><div><div><div>14</div></div><div><span>见 演示 A，页码形式：页 A，演示 A. 页 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-grid.png" alt="grid.typ 的渲染：带表题“表 1.1”的表，第一列后有红色竖线，误差列后有灰色细线，被 table.cell 单独着色的单元格是黄色，其中“1.2e-3”被 highlight 再标一层黄；下面是跨两列的矩形与“左”“右”两个矩形，图题“演示 A 自定义 kind”" /><figcaption>grid.typ 的渲染：带表题“表 1.1”的表，第一列后有红色竖线，误差列后有灰色细线，被 table.cell 单独着色的单元格是黄色，其中“1.2e-3”被 highlight 再标一层黄；下面是跨两列的矩形与“左”“右”两个矩形，图题“演示 A 自定义 kind”</figcaption></figure><p></p><p>这里有几点是从渲染里观察到的：</p><ul>
<li>用户自己传的 <code>stroke</code> 函数替换了 <code>xuan</code> 的三线规则里的上沿线（上沿 1pt、表头下 0.5pt 是我在函数里重写的），下沿的 1pt 线还在，因为那是 <code>show table</code> 追加的。</li>
<li><code>table.cell(fill: ...)</code> 给单个单元格上色（图里黄色那格），<code>table.cell(align: center)</code> 让“6.3”居中，其余同列单元格仍按表级的 <code>align</code> 函数右对齐。</li>
<li><code>highlight</code> 放进单元格就是普通内容，不需要特殊处理。第 5 篇讲过 <code>highlight</code>、<code>underline</code>、<code>strike</code> 的参数，这里只是它们在表格里的用法。</li>
<li>自定义 <code>kind: "demo"</code>、<code>numbering: "A"</code> 的图不在 <code>figure-kinds</code> 里，所以 <code>xuan</code> 的 <code>show ref</code> 放过它，引用走 Typst 默认的逻辑：“演示 A”。<code>@demo[页]</code> 把 supplement 换成了“页”，得到“页 A”。</li>
<li><code>#ref(&lt;demo&gt;, form: "page")</code> 是 0.15 里 <code>ref</code> 的 <code>form</code> 参数，<code>"page"</code> 形式输出“页 1”，也就是被引用元素所在的页码。它要求页面有编号，我一开始没写 <code>numbering: "1"</code>，编译直接报 <code>cannot reference without page numbering</code>，hint 说明了怎么办，所以上面这份实验文件里的 <code>#set page(... numbering: "1")</code> 是改过的。</li>
</ul></section>
<section><h2>子图：用 kind 自己搭<a href="#子图用-kind-自己搭"><span>#</span></a></h2><p>Typst 没有内置子图。<code>lab/ch09/subfigures.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/subfigures.typ" target="_blank">源文件</a>）用一个新的 kind 搭出 (a)(b)：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Subfigures: a figure whose body is a grid of figures of another kind.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>sub</span><span>(</span><span>caption</span><span><span>) </span><span>= </span></span><span>figure</span><span>(</span></div></div><div><div><div>7</div></div><div><span>  </span><span>kind</span><span>: </span><span>"subfigure"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>numbering</span><span>: </span><span>"(a)"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>supplement</span><span>: [],</span></div></div><div><div><div>10</div></div><div><span>  </span><span>image</span><span>(</span><span>"assets/projection.svg"</span><span>),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>caption</span><span>: </span><span>caption</span><span>,</span></div></div><div><div><div>12</div></div><div><span>)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>=</span><span> 比较</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>17</div></div><div><span>  </span><span>grid</span><span>(</span></div></div><div><div><div>18</div></div><div><span>    </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>column-gutter</span><span>: </span><span>1em</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>[</span><span>#sub</span><span>[原始] </span><span>&lt;sub-a&gt;</span><span>], [</span><span>#sub</span><span>[重复] </span><span>&lt;sub-b&gt;</span><span>],</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>22</div></div><div><span>  </span><span>caption</span><span>: [两个子图，各自编号 (a)(b)。],</span></div></div><div><div><div>23</div></div><div><span>) </span><span>&lt;fig-both&gt;</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>@fig-both</span><span> 中的 (</span><span>@sub-a</span><span>) 与 (</span><span>@sub-b</span><span>)：引用只给出字母，括号写在正文里。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-subfigures.png" alt="subfigures.typ 的渲染：两张相同的投影图并排，图下分别标“(a) 原始”“(b) 重复”，整体图题是“图 1.1 两个子图，各自编号 (a)(b)。”，正文里的引用是“图 1.1 中的 (a) 与 (b)”" /><figcaption>subfigures.typ 的渲染：两张相同的投影图并排，图下分别标“(a) 原始”“(b) 重复”，整体图题是“图 1.1 两个子图，各自编号 (a)(b)。”，正文里的引用是“图 1.1 中的 (a) 与 (b)”</figcaption></figure><p></p><p><code>sub</code> 是一个本地函数，造一个 <code>kind: "subfigure"</code>、<code>numbering: "(a)"</code> 的 <code>figure</code>，放进外层 <code>figure</code> 的 <code>grid</code> 里。<code>"subfigure"</code> 有自己的计数器，所以外层的图是“图 1.1”，里面的两张分别数 (a)、(b)。</p><p>有两处细节是我在 <code>/tmp</code> 里隔离出来的（最小实验，不在仓库里），原样记录下来：第一，<code>supplement: none</code> 时，标题里<strong>不显示编号</strong>，只剩“原始”，加上 <code>supplement: []</code>（空内容，不是 <code>none</code>）才会出现“(a) 原始”；第二，对 <code>numbering: "(a)"</code> 的元素做 <code>@sub-a</code>，引用文字里<strong>只有字母</strong>，括号被剥掉了，所以 lab 里写 <code>(@sub-a)</code> 自己补括号。我没有去查是 Typst 有意这样设计还是副作用；对我来说，知道它这么表现、写法上绕过去就够了。</p></section>
<section><h2>ref：supplement、链接、脚注、引用块<a href="#refsupplement链接脚注引用块"><span>#</span></a></h2><p><code>lab/ch09/refs.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/refs.typ" target="_blank">源文件</a>）把几个互相不太相关的模型元素放在一起：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// ref, supplements, link, footnote, quote, divider, emph/strong rules.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>=</span><span> 引用</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>==</span><span> 小节 </span><span>&lt;sec-a&gt;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>@sec-a</span><span> 引用标题，需要 </span><span>`set heading(numbering: ...)`</span><span>；</span></div></div><div><div><div>11</div></div><div><span>`@sec-a[章节]`</span><span> 可换补充词：</span><span>@sec-a</span><span>[章节]。</span></div></div><div><div><div>12</div></div><div><span>#link</span><span>(</span><span>"https://typst.app"</span><span>)[外部链接]、</span><span>#link</span><span>(</span><span>&lt;sec-a&gt;</span><span>)[内部链接]。</span></div></div><div><div><div>13</div></div><div><span>脚注示例</span><span>#footnote</span><span>[脚注文字自动编号。]。</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>#quote</span><span>(</span><span>block</span><span>: </span><span>true</span><span>, </span><span>attribution</span><span>: [Knuth])[</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>Premature optimization is the root of all evil.</span></div></div><div><div><div>17</div></div><div><span>]</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>#divider</span><span>()</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>// Restyle emphasis: no italics for CJK, a coloured underline for strong.</span></div></div><div><div><div>22</div></div><div><span>#show</span><span> </span><span>emph</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>style</span><span>: </span><span>"normal"</span><span>, </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#8a3"</span><span>))</span></div></div><div><div><div>23</div></div><div><span>#show</span><span> </span><span>strong</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>underline</span><span>(</span><span>offset</span><span>: </span><span>2pt</span><span>, </span><span>it</span><span>)</span></div></div><div><div><div>24</div></div><div><span>_强调_与*加粗*被重写了。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-refs.png" alt="refs.typ 的渲染：小节 1.1 下的文字里，@sec-a 显示为蓝色的“小节 1.1”，“章节 1.1”替换了前缀，外部链接和内部链接是蓝色，脚注上标 1 与页底脚注，块引用右下有署名 Knuth，下面一条横线，最后一段里“强调”是绿色，“加粗”带下划线" /><figcaption>refs.typ 的渲染：小节 1.1 下的文字里，@sec-a 显示为蓝色的“小节 1.1”，“章节 1.1”替换了前缀，外部链接和内部链接是蓝色，脚注上标 1 与页底脚注，块引用右下有署名 Knuth，下面一条横线，最后一段里“强调”是绿色，“加粗”带下划线</figcaption></figure><p></p><ul>
<li><strong><code>@label</code> 与 supplement。</strong> <code>@sec-a</code> 引用一个标题，要求标题有编号（<code>set heading(numbering: ...)</code>，<code>xuan-base</code> 里已经设了 <code>"1.1"</code>），显示成“小节 1.1”，“小节”是标题的默认 supplement。<code>@sec-a[章节]</code> 用方括号替换 supplement，显示成“章节 1.1”。对图，上面讲过 <code>xuan</code> 的 <code>show ref</code> 把用户写的 supplement 放在最前面。</li>
<li><strong><code>link</code>。</strong> <code>#link("https://typst.app")[外部链接]</code> 是 URL，<code>#link(&lt;sec-a&gt;)[内部链接]</code> 指向 label。链接的蓝色来自 <code>xuan-base</code> 里的 <code>show link: set text(fill: rgb("#1f4e9c"))</code>。裸写的 <code>https://...</code> 也会自动变成链接，第 1 篇提过。</li>
<li><strong><code>footnote</code>。</strong> <code>#footnote[...]</code> 在正文处放一个上标编号，内容出现在页底并带一条分隔线。编号自动连续，跟图表编号互不相干。</li>
<li><strong><code>quote</code>。</strong> 默认是行内，<code>block: true</code> 变成缩进的块，<code>attribution</code> 是出处，右下角的署名“Knuth”。行内的 <code>quote</code> 会自动给内容加上引号，我在 <code>/tmp</code> 里编译确认过，这里没有贴。</li>
<li><strong><code>divider</code>。</strong> 0.15 新增的分隔线，一行 <code>#divider()</code>，渲染成一条横线。</li>
<li><strong>改写 <code>emph</code> 和 <code>strong</code>。</strong> 文件末尾两条 show 规则是示范：<code>show emph: set text(style: "normal", fill: ...)</code> 是 show-set，只改样式；<code>show strong: it =&gt; underline(offset: 2pt, it)</code> 是 show 变换，把 <code>*加粗*</code> 换成带下划线的内容。渲染图里“加粗”两个字仍然是粗体，因为 <code>underline</code> 里放的 <code>it</code> 就是原来的 <code>strong</code> 元素，Typst 没有把这条规则对它的输出再应用一遍（我猜它跳过了规则自己输出里的同一元素，没有去读源码确认）。这两条只在这个 lab 文件里，<code>xuan</code> 没有收录。至于中文里是不是该用斜体表示强调，我猜用加粗或着重号更常见，不过这是排版习惯问题，不是 Typst 的限制。</li>
</ul></section>
<section><h2>code.typ：块代码加行号<a href="#codetyp块代码加行号"><span>#</span></a></h2><p><code>raw</code> 是 Typst 的代码元素：反引号单行是行内，三个反引号多行是块，<code>lang</code> 指定高亮语言。<code>lab/ch09/raw.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/raw.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// raw: inline, block, lang tag, line numbers (xuan), tab size, from a file.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>行内 </span><span>`let x = 1`</span><span> 与带语言的 </span><span>#raw</span><span>(</span><span>"fn main() {}"</span><span>, </span><span>lang</span><span>: </span><span>"rust"</span><span>)。</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// Block raw: xuan numbers every line through a `show raw.line` rule.</span></div></div><div><div><div>9</div></div><div><span>```rust</span></div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"你好"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div><span>```</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>// `tab-size` sets the tab width, and only takes effect as a set rule.</span></div></div><div><div><div>16</div></div><div><span>#</span><span>[</span></div></div><div><div><div>17</div></div><div><span>  </span><span>#set</span><span> </span><span>raw</span><span>(</span><span>tab-size</span><span>: </span><span>8</span><span>)</span></div></div><div><div><div>18</div></div><div><span>  </span><span>#raw</span><span>(</span><span>"x</span><span>\t</span><span>y"</span><span>, </span><span>block</span><span>: </span><span>true</span><span>, </span><span>lang</span><span>: </span><span>"text"</span><span>)</span></div></div><div><div><div>19</div></div><div><span>]</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>// Pull code from a file, then show a few lines of it.</span></div></div><div><div><div>22</div></div><div><span>#let</span><span> </span><span>src</span><span><span> </span><span>= </span></span><span>read</span><span>(</span><span>"assets/projection.svg"</span><span>)</span></div></div><div><div><div>23</div></div><div><span>#raw</span><span>(</span><span>src</span><span>.</span><span>split</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>).</span><span>slice</span><span>(</span><span>0</span><span>, </span><span>3</span><span>).</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>), </span><span>block</span><span>: </span><span>true</span><span>, </span><span>lang</span><span>: </span><span>"xml"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-raw.png" alt="raw.typ 的渲染：行内代码有浅灰底；第一个块是带行号的 Rust 代码，关键字和字符串上色；第二个块是 set raw(tab-size: 8) 之后的 x 制表符 y，两字符之间空了 7 格；第三个块是从 projection.svg 里读出来的前三行 XML，第一行因为太长折成三行但行号只有一个" /><figcaption>raw.typ 的渲染：行内代码有浅灰底；第一个块是带行号的 Rust 代码，关键字和字符串上色；第二个块是 set raw(tab-size: 8) 之后的 x 制表符 y，两字符之间空了 7 格；第三个块是从 projection.svg 里读出来的前三行 XML，第一行因为太长折成三行但行号只有一个</figcaption></figure><p></p><p>块和行内的区别是 <code>raw.block</code>。<code>#raw("fn main() {}", lang: "rust")</code> 是函数调用形式，和反引号等价；第二块用 <code>read(...)</code> 读一个文件的内容，切三行再交给 <code>raw(block: true, lang: "xml")</code>，所以文档里展示的代码可以不手抄。<code>xuan</code> 里这个模块只有 28 行（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/code.typ" target="_blank"><code>src/code.typ</code></a>），全文如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Code display: numbered block raw, subtle inline raw.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>code-rules</span><span>(</span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>4</div></div><div><span>  </span><span>show</span><span> </span><span>raw</span><span>.</span><span>where</span><span>(</span><span>block</span><span>: </span><span>false</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>box</span><span>(</span></div></div><div><div><div>5</div></div><div><span>    </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>240</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>inset</span><span>: (</span><span>x</span><span>: </span><span>3pt</span><span>),</span></div></div><div><div><div>7</div></div><div><span>    </span><span>outset</span><span>: (</span><span>y</span><span>: </span><span>3pt</span><span>),</span></div></div><div><div><div>8</div></div><div><span>    </span><span>radius</span><span>: </span><span>2pt</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>it</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span>  </span><span>show</span><span> </span><span>raw</span><span>.</span><span>where</span><span>(</span><span>block</span><span>: </span><span>true</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>show</span><span> </span><span>raw</span><span>.</span><span>line</span><span>: </span><span>line</span><span> </span><span>=&gt;</span><span> </span><span>box</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>grid</span><span>(</span></div></div><div><div><div>13</div></div><div><span>      </span><span>columns</span><span>: (</span><span>2em</span><span>, </span><span>1fr</span><span>),</span></div></div><div><div><div>14</div></div><div><span>      </span><span>column-gutter</span><span>: </span><span>0.8em</span><span>,</span></div></div><div><div><div>15</div></div><div><span>      </span><span>align</span><span>(</span><span>right</span><span>, </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>150</span><span>), </span><span>str</span><span>(</span><span>line</span><span>.</span><span>number</span><span>))), </span><span>line</span><span>.</span><span>body</span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>))</span></div></div><div><div><div>17</div></div><div><span>    </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>, </span><span>justify</span><span>: </span><span>false</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>block</span><span>(</span></div></div><div><div><div>19</div></div><div><span>      </span><span>width</span><span>: </span><span>100%</span><span>,</span></div></div><div><div><div>20</div></div><div><span>      </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>245</span><span>),</span></div></div><div><div><div>21</div></div><div><span>      </span><span>inset</span><span>: </span><span>8pt</span><span>,</span></div></div><div><div><div>22</div></div><div><span>      </span><span>radius</span><span>: </span><span>4pt</span><span>,</span></div></div><div><div><div>23</div></div><div><span>      </span><span>breakable</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>24</div></div><div><span>      </span><span>it</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>  </span><span>body</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>行内部分很简单：<code>raw.where(block: false)</code> 包进一个浅灰底的 <code>box</code>，横向内边距 3pt，纵向用 <code>outset</code> 把底色向外多伸 3pt 而不推开行距，圆角 2pt。</p><p>块部分有三个要点。</p><p><strong>行号来自 <code>raw.line</code>。</strong> Typst 把 <code>raw</code> 的每一行包成一个 <code>raw.line</code> 元素，字段里有 <code>number</code>（行号）和 <code>body</code>（这一行高亮后的内容）。show 规则把每一行换成一个两列 <code>grid</code>：左边 2em 右对齐的灰色行号，右边是 <code>line.body</code>，列间距 0.8em。外面包一层 <code>box(width: 100%)</code> 让每一行成为行内元素并撑满宽度，这样一行太长折行时，续行只缩在代码列里，行号列不跟着折，正如渲染图里 SVG 的第一行折成三行却只有一个“1”。</p><p><strong><code>show raw.line</code> 写在 <code>show raw.where(block: true)</code> 里面。</strong> 这是源文件的写法，我没有试过把它挪到块规则外面，所以不能说外面行不行。</p><p><strong><code>set par(first-line-indent: 0pt, justify: false)</code>。</strong> <code>xuan-cjk</code> 把 <code>first-line-indent</code> 设成 <code>(amount: 2em, all: true)</code>，<code>all: true</code> 表示每一段包括标题后的第一段都缩进。我的本意是不让块代码继承这个缩进，也不让两端对齐拉开单词间距。我在 <code>/tmp/p09/exp/nopar.typ</code> 里删掉这一行，单行代码的渲染和有这一行的一模一样，没有复现出差别；这一行是预防性的，多行、含长行的代码块我没有逐个试。外框 <code>block(width: 100%, fill: luma(245), inset: 8pt, radius: 4pt, breakable: true)</code> 是浅灰圆角底，<code>breakable: true</code> 允许很长的代码跨页（这个我没有量页数）。</p><p><code>fig-code.typ</code> 是为了看效果单独渲染的，注意它里面本身有反引号围栏，所以我用四个反引号包住：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#block</span><span>(</span><span>width</span><span>: </span><span>9cm</span><span>)[</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>```typst</span></div></div><div><div><div>8</div></div><div><span>  </span><span>#let</span><span> </span><span>f</span><span>(</span><span>x</span><span>) </span><span>= </span><span>x</span><span> </span><span>*</span><span> </span><span>2</span></div></div><div><div><div>9</div></div><div><span>  </span><span>#for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>) [</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>第 </span><span>#i</span><span> 项：</span><span>#f</span><span>(</span><span>i</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>```</span></div></div><div><div><div>13</div></div><div><span>]</span></div></div><div><div><div>14</div></div><div><span>行内代码 </span><span>`f(3)`</span><span> 有浅灰底。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-code.png" alt="fig-code.typ 的渲染：灰色圆角块里有四行 Typst 代码，每行左侧是灰色行号，#let、#for 等被高亮；块下方是一行带浅灰底的行内代码 f(3)" /><figcaption>fig-code.typ 的渲染：灰色圆角块里有四行 Typst 代码，每行左侧是灰色行号，#let、#for 等被高亮；块下方是一行带浅灰底的行内代码 f(3)</figcaption></figure><p></p><section><h3>theme、syntaxes 与 tab-size<a href="#themesyntaxes-与-tab-size"><span>#</span></a></h3><p><code>raw</code> 还有三个我没放进 <code>xuan</code> 的参数。<code>theme</code> 换高亮配色（我传的是 <code>.tmTheme</code> 文件路径），<code>syntaxes</code> 登记额外的语法（我传的是 <code>.sublime-syntax</code> 文件路径），<code>tab-size</code> 设制表符宽度。我在 <code>/tmp/p09/exp/</code> 里写了一个玩具语法和玩具主题（也不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>%</span><span>YAML</span><span> </span><span>1.2</span></div></div><div><div><div>2</div></div><div><span>---</span></div></div><div><div><div>3</div></div><div><span>name</span><span>: </span><span>Toy</span></div></div><div><div><div>4</div></div><div><span>file_extensions</span><span>: [</span><span>toy</span><span>]</span></div></div><div><div><div>5</div></div><div><span>scope</span><span>: </span><span>source.toy</span></div></div><div><div><div>6</div></div><div><span>contexts</span><span>:</span></div></div><div><div><div>7</div></div><div><span>  </span><span>main</span><span>:</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>- </span><span>match</span><span>: </span><span>'\b(let|print)\b'</span></div></div><div><div><div>9</div></div><div><span>      </span><span>scope</span><span>: </span><span>keyword.control.toy</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>- </span><span>match</span><span>: </span><span>'\b[0-9]+\b'</span></div></div><div><div><div>11</div></div><div><span>      </span><span>scope</span><span>: </span><span>constant.numeric.toy</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>- </span><span>match</span><span>: </span><span>'#.*$'</span></div></div><div><div><div>13</div></div><div><span>      </span><span>scope</span><span>: </span><span>comment.line.toy</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>主题 <code>toy.tmTheme</code> 是一份 plist，只给了 <code>keyword</code>、<code>constant.numeric</code>、<code>comment</code> 三个 scope 上色：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;?</span><span>xml</span><span> version</span><span>=</span><span>"1.0"</span><span> encoding</span><span>=</span><span>"UTF-8"</span><span>?&gt;</span></div></div><div><div><div>2</div></div><div><span>&lt;</span><span>plist</span><span> </span><span>version</span><span>=</span><span>"1.0"</span><span>&gt;&lt;</span><span>dict</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span>&lt;</span><span>key</span><span>&gt;name&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;Toy&lt;/</span><span>string</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span>&lt;</span><span>key</span><span>&gt;settings&lt;/</span><span>key</span><span>&gt;&lt;</span><span>array</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span>&lt;</span><span>dict</span><span>&gt;&lt;</span><span>key</span><span>&gt;settings&lt;/</span><span>key</span><span>&gt;&lt;</span><span>dict</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span>&lt;</span><span>key</span><span>&gt;foreground&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;#222222&lt;/</span><span>string</span><span>&gt;</span></div></div><div><div><div>7</div></div><div><span>&lt;</span><span>key</span><span>&gt;background&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;#ffffff&lt;/</span><span>string</span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span>&lt;/</span><span>dict</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span>&lt;</span><span>dict</span><span>&gt;&lt;</span><span>key</span><span>&gt;scope&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;keyword&lt;/</span><span>string</span><span>&gt;&lt;</span><span>key</span><span>&gt;settings&lt;/</span><span>key</span><span>&gt;&lt;</span><span>dict</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span>&lt;</span><span>key</span><span>&gt;foreground&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;#c0392b&lt;/</span><span>string</span><span>&gt;&lt;</span><span>key</span><span>&gt;fontStyle&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;bold&lt;/</span><span>string</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span>&lt;</span><span>dict</span><span>&gt;&lt;</span><span>key</span><span>&gt;scope&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;constant.numeric&lt;/</span><span>string</span><span>&gt;&lt;</span><span>key</span><span>&gt;settings&lt;/</span><span>key</span><span>&gt;&lt;</span><span>dict</span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span>&lt;</span><span>key</span><span>&gt;foreground&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;#1f4e9c&lt;/</span><span>string</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span>&lt;</span><span>dict</span><span>&gt;&lt;</span><span>key</span><span>&gt;scope&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;comment&lt;/</span><span>string</span><span>&gt;&lt;</span><span>key</span><span>&gt;settings&lt;/</span><span>key</span><span>&gt;&lt;</span><span>dict</span><span>&gt;</span></div></div><div><div><div>14</div></div><div><span>&lt;</span><span>key</span><span>&gt;foreground&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;#888888&lt;/</span><span>string</span><span>&gt;&lt;</span><span>key</span><span>&gt;fontStyle&lt;/</span><span>key</span><span>&gt;&lt;</span><span>string</span><span>&gt;italic&lt;/</span><span>string</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span>&lt;/</span><span>array</span><span>&gt;&lt;/</span><span>dict</span><span>&gt;&lt;/</span><span>plist</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>然后是 <code>/tmp/p09/exp/raw2.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>toy</span><span><span> </span><span>= </span></span><span>"let x = 42</span><span>\n</span><span>print x # 打印"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>没有语法定义时无色：</span><span>#raw</span><span>(</span><span>toy</span><span>, </span><span>block</span><span>: </span><span>true</span><span>, </span><span>lang</span><span>: </span><span>"toy"</span><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#</span><span>[</span></div></div><div><div><div>11</div></div><div><span>  </span><span>#set</span><span> </span><span>raw</span><span>(</span><span>syntaxes</span><span>: </span><span>"toy.sublime-syntax"</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>登记 syntaxes 之后有色：</span><span>#raw</span><span>(</span><span>toy</span><span>, </span><span>block</span><span>: </span><span>true</span><span>, </span><span>lang</span><span>: </span><span>"toy"</span><span>)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>#set</span><span> </span><span>raw</span><span>(</span><span>theme</span><span>: </span><span>"toy.tmTheme"</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>再换 theme：</span><span>#raw</span><span>(</span><span>toy</span><span>, </span><span>block</span><span>: </span><span>true</span><span>, </span><span>lang</span><span>: </span><span>"toy"</span><span>)</span></div></div><div><div><div>16</div></div><div><span>]</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>#show</span><span> </span><span>raw</span><span>.</span><span>where</span><span>(</span><span>block</span><span>: </span><span>true</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>19</div></div><div><span>缩小字号只影响块：</span><span>`inline`</span><span> 与块</span></div></div><div><div><div>20</div></div><div><span>```typst</span></div></div><div><div><div>21</div></div><div><span>=</span><span> 标题</span></div></div><div><div><div>22</div></div><div><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-raw-theme.png" alt="raw2.typ 的渲染：第一块 toy 代码没有语法定义所以全黑；第二块登记 syntaxes 后 let 与 print 为红色，数字 42 为洋红色，注释为灰色；第三块换了 theme 后关键字变为粗体深红，数字变为蓝色，注释为灰色斜体；最后一块是缩小的 Typst 标题代码" /><figcaption>raw2.typ 的渲染：第一块 toy 代码没有语法定义所以全黑；第二块登记 syntaxes 后 let 与 print 为红色，数字 42 为洋红色，注释为灰色；第三块换了 theme 后关键字变为粗体深红，数字变为蓝色，注释为灰色斜体；最后一块是缩小的 Typst 标题代码</figcaption></figure><p></p><p>没有 <code>syntaxes</code> 时，<code>lang: "toy"</code> 找不到语法，代码不上色也不报错。登记之后按语法里的 scope 上色，配色来自默认主题。再设 <code>theme</code>，同一份语法换成了我在 tmTheme 里给的颜色。最后一块里，<code>show raw.where(block: true): set text(size: 8pt)</code> 只缩块不缩行内，这是 <code>raw</code> 选择器的常用写法。</p><p><code>tab-size</code> 我专门量了。它是一个只对 <code>set</code> 规则可靠的参数，这是我在 <code>/tmp/p09/exp/tab4.typ</code> 里测出来的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>3</div></div><div><span>  </span><span>#let</span><span> </span><span>w</span><span>(</span><span>s</span><span><span>) </span><span>= </span></span><span>measure</span><span>(</span><span>raw</span><span>(</span><span>s</span><span>)).</span><span>width</span></div></div><div><div><div>4</div></div><div><span>  </span><span>#w</span><span>(</span><span>"x</span><span>\t</span><span>y"</span><span>) </span><span>#w</span><span>(</span><span>"x   y"</span><span>) </span><span>#w</span><span>(</span><span>"x       y"</span><span>) </span><span>#w</span><span>(</span><span>"x y"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>]</span></div></div><div><div><div>6</div></div><div><span>#set</span><span> </span><span>raw</span><span>(</span><span>tab-size</span><span>: </span><span>8</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>8</div></div><div><span>  </span><span>#let</span><span> </span><span>w</span><span>(</span><span>s</span><span><span>) </span><span>= </span></span><span>measure</span><span>(</span><span>raw</span><span>(</span><span>s</span><span>)).</span><span>width</span></div></div><div><div><div>9</div></div><div><span>  </span><span>#w</span><span>(</span><span>"x</span><span>\t</span><span>y"</span><span>) </span><span>#w</span><span>(</span><span>"x   y"</span><span>) </span><span>#w</span><span>(</span><span>"x       y"</span><span>) </span><span>#w</span><span>(</span><span>"x y"</span><span>)</span></div></div><div><div><div>10</div></div><div><span>]</span></div></div><div><div><div>11</div></div><div><span>#set</span><span> </span><span>raw</span><span>(</span><span>tab-size</span><span>: </span><span>4</span><span>)</span></div></div><div><div><div>12</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>13</div></div><div><span>  </span><span>#let</span><span> </span><span>w</span><span>(</span><span>s</span><span><span>) </span><span>= </span></span><span>measure</span><span>(</span><span>raw</span><span>(</span><span>s</span><span>)).</span><span>width</span></div></div><div><div><div>14</div></div><div><span>  </span><span>#w</span><span>(</span><span>"x</span><span>\t</span><span>y"</span><span>)</span></div></div><div><div><div>15</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts tab4.typ tab4.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext -layout tab4.pdf -</span></div></div><div><div><div>3</div></div><div><span>15.89pt 26.49pt 47.68pt 15.89pt 47.68pt 26.49pt 47.68pt</span></div></div><div><div><div>4</div></div><div><span>15.89pt 26.49pt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>九个数字按顺序对应：默认 tab-size 下四个值（<code>x⇥y</code>、<code>x</code> 加三个空格再 <code>y</code>、<code>x</code> 加七个空格再 <code>y</code>、<code>x y</code>），<code>set raw(tab-size: 8)</code> 之后同样四个值，最后 <code>tab-size: 4</code> 一个值。等宽字体一个字符 5.3pt，所以 15.89pt 是 3 个字符宽、26.49pt 是 5 个、47.68pt 是 9 个。默认情况下 <code>x⇥y</code> 是 3 个字符宽：<code>x</code>，制表符补到第 2 列（默认 tab-size 为 2），<code>y</code>。<code>set raw(tab-size: 8)</code> 后变成 9 个字符，<code>tab-size: 4</code> 变成 5 个。即“制表符补空格到下一个 tab-size 的整数倍”。</p><p>lab 里的 <code>raw.typ</code> 也有一小段 tab-size：<code>#set raw(tab-size: 8)</code> 之后写 <code>#raw("x\ty", block: true, lang: "text")</code>，渲染图里 x 和 y 之间空了 7 格。lab 里最初的写法是把 <code>tab-size: 8</code> 直接传给 <code>raw(...)</code> 构造函数，那样制表符只有一个空格那么宽，我用 <code>measure</code> 一量也和默认值相同：构造函数里传的 <code>tab-size</code> 在 0.15.1 里没有效果（这一条只来自我在 <code>/tmp</code> 里的测量，现在的仓库里没有这种写法），只有 set 规则生效。至于这是 Typst 的 bug 还是我没读懂文档，我不确定。</p></section></section>
<section><h2>units.typ：数字与单位<a href="#unitstyp数字与单位"><span>#</span></a></h2><p>科学写作里数字和单位的排版有一套规矩：长数字按三位分组，数字和单位之间有细空格，单位是正体，负号是真正的减号而不是连字符。<code>xuan</code> 的 <code>units.typ</code> 把这些规矩收进三个函数。先看 <code>lab/ch09/units.typ</code> 的输入和输出（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch09/units.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// num, qty, qty-range from xuan.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>- </span><span>`num("1234567.891")`</span><span>：</span><span>#num</span><span>(</span><span>"1234567.891"</span><span>)</span></div></div><div><div><div>8</div></div><div><span>- </span><span>`num(-0.000012345)`</span><span>：</span><span>#num</span><span><span>(</span><span>-</span></span><span>0.000012345</span><span>)</span></div></div><div><div><div>9</div></div><div><span>- </span><span>`num(1234)`</span><span>（四位不分组）：</span><span>#num</span><span>(</span><span>1234</span><span>)</span></div></div><div><div><div>10</div></div><div><span>- </span><span>`qty(9.81, "m/s^2")`</span><span>：</span><span>#qty</span><span>(</span><span>9.81</span><span>, </span><span>"m/s^2"</span><span>)</span></div></div><div><div><div>11</div></div><div><span>- </span><span>`qty(1.2e5, "kg m^-1")`</span><span>：</span><span>#qty</span><span>(</span><span>120000</span><span>, </span><span>"kg m^-1"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>- </span><span>`qty-range(20, 25, "°C")`</span><span>：</span><span>#qty-range</span><span>(</span><span>20</span><span>, </span><span>25</span><span>, </span><span>"°C"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>- </span><span>`qty-range(20, 25, "°C", lang: "en")`</span><span>：</span><span>#qty-range</span><span>(</span><span>20</span><span>, </span><span>25</span><span>, </span><span>"°C"</span><span>, </span><span>lang</span><span>: </span><span>"en"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext -layout out/ch09/units.pdf -</span></div></div><div><div><div>2</div></div><div><span>• num("1234567.891") ：1 234 567.891</span></div></div><div><div><div>3</div></div><div><span>• num(-0.000012345) ：−0.000 012 345</span></div></div><div><div><div>4</div></div><div><span>• num(1234) （四位不分组）：1234</span></div></div><div><div><div>5</div></div><div><span>• qty(9.81, "m/s^2") ：9.81 m/s2</span></div></div><div><div><div>6</div></div><div><span>• qty(1.2e5, "kg m^-1") ：120 000 kg m−1</span></div></div><div><div><div>7</div></div><div><span>• qty-range(20, 25, "°C") ：20～25 °C</span></div></div><div><div><div>8</div></div><div><span>• qty-range(20, 25, "°C", lang: "en") ：20–25 °C</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>pdftotext</code> 把上标抹平，所以是 <code>m/s2</code>，渲染图里是上标。）</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch09-units.png" alt="units.typ 的渲染：七行列表，数字按千位用细空格分组，负号是真正的减号，m/s² 与 kg m⁻¹ 中的指数是上标，温度范围在中文里用全角波浪线“～”，英文里用短横" /><figcaption>units.typ 的渲染：七行列表，数字按千位用细空格分组，负号是真正的减号，m/s² 与 kg m⁻¹ 中的指数是上标，温度范围在中文里用全角波浪线“～”，英文里用短横</figcaption></figure><p></p><p>最终版 <code>src/units.typ</code>（66 行，<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/units.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Numbers and quantities: `num`, `qty`, `qty-range`.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>thin</span><span><span> </span><span>= </span></span><span>"</span><span>\u{2009}</span><span>"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>/// Group a run of digits by thin spaces. From the right for integers,</span></div></div><div><div><div>6</div></div><div><span>/// from the left for fractions; runs shorter than 5 digits stay intact.</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>group</span><span>(</span><span>digits</span><span>, </span><span>from-right</span><span>: </span><span>true</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>let</span><span> </span><span>cs</span><span><span> </span><span>= </span></span><span>digits</span><span>.</span><span>clusters</span><span>()</span></div></div><div><div><div>9</div></div><div><span>  </span><span>if</span><span> </span><span>cs</span><span>.</span><span>len</span><span><span>() </span><span>&lt;</span><span> </span></span><span>5</span><span> { </span><span>return</span><span> </span><span>digits</span><span> }</span></div></div><div><div><div>10</div></div><div><span>  </span><span>let</span><span> </span><span>out</span><span><span> </span><span>= </span><span>()</span></span></div></div><div><div><div>11</div></div><div><span>  </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>cs</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>12</div></div><div><span>  </span><span>for</span><span> (</span><span>i</span><span>, </span><span>c</span><span>) </span><span>in</span><span> </span><span>cs</span><span>.</span><span>enumerate</span><span>() {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>boundary</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>from-right</span><span> { </span><span>calc</span><span>.</span><span>rem</span><span>(</span><span>n</span><span><span> </span><span>-</span><span> </span></span><span>i</span><span>, </span><span>3</span><span><span>) </span><span>==</span><span> </span></span><span>0</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>14</div></div><div><span>      </span><span>calc</span><span>.</span><span>rem</span><span>(</span><span>i</span><span>, </span><span>3</span><span><span>) </span><span>==</span><span> </span></span><span>0</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> </span><span>i</span><span><span> </span><span>&gt;</span><span> </span></span><span>0</span><span> </span><span>and</span><span> </span><span>boundary</span><span> { </span><span>out</span><span>.</span><span>push</span><span>(</span><span>thin</span><span>) }</span></div></div><div><div><div>17</div></div><div><span>    </span><span>out</span><span>.</span><span>push</span><span>(</span><span>c</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>  </span><span>out</span><span>.</span><span>join</span><span>()</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>/// Digits grouped by thin spaces, minus rendered as U+2212.</span></div></div><div><div><div>23</div></div><div><span>#let</span><span> </span><span>num</span><span>(</span><span>value</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>24</div></div><div><span>  </span><span>let</span><span> </span><span>s</span><span><span> </span><span>= </span></span><span>str</span><span>(</span><span>value</span><span>).</span><span>replace</span><span>(</span><span>"-"</span><span>, </span><span>"</span><span>\u{2212}</span><span>"</span><span>)</span></div></div><div><div><div>25</div></div><div><span>  </span><span>let</span><span> </span><span>sign</span><span><span> </span><span>= </span></span><span>""</span></div></div><div><div><div>26</div></div><div><span>  </span><span>if</span><span> </span><span>s</span><span>.</span><span>starts-with</span><span>(</span><span>"</span><span>\u{2212}</span><span>"</span><span>) {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>sign</span><span><span> </span><span>=</span><span> </span></span><span>"</span><span>\u{2212}</span><span>"</span></div></div><div><div><div>28</div></div><div><span>    </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>s</span><span>.</span><span>slice</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>  </span><span>let</span><span> </span><span>parts</span><span><span> </span><span>= </span></span><span>s</span><span>.</span><span>split</span><span>(</span><span>"."</span><span>)</span></div></div><div><div><div>31</div></div><div><span>  </span><span>let</span><span> </span><span>whole</span><span><span> </span><span>= </span></span><span>group</span><span>(</span><span>parts</span><span>.</span><span>at</span><span>(</span><span>0</span><span>))</span></div></div><div><div><div>32</div></div><div><span>  </span><span>let</span><span> </span><span>frac</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>parts</span><span>.</span><span>len</span><span><span>() </span><span>&gt;</span><span> </span></span><span>1</span><span> {</span></div></div><div><div><div>33</div></div><div><span>    </span><span>"."</span><span><span> </span><span>+</span><span> </span></span><span>group</span><span>(</span><span>parts</span><span>.</span><span>at</span><span>(</span><span>1</span><span>), </span><span>from-right</span><span>: </span><span>false</span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> { </span><span>""</span><span> }</span></div></div><div><div><div>35</div></div><div><span>  </span><span>sign</span><span><span> </span><span>+</span><span> </span></span><span>whole</span><span><span> </span><span>+</span><span> </span></span><span>frac</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>/// Render a unit string such as "m/s^2" or "kg m^-1" upright.</span></div></div><div><div><div>39</div></div><div><span>#let</span><span> </span><span>unit</span><span>(</span><span>spec</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>40</div></div><div><span>  </span><span>let</span><span> </span><span>pat</span><span><span> </span><span>= </span></span><span>regex</span><span>(</span><span>"(</span><span>\s</span><span>+|/)|([^</span><span>\s</span><span>/^]+)(?:</span><span>\^</span><span>([-+]?</span><span>\d</span><span>+))?"</span><span>)</span></div></div><div><div><div>41</div></div><div><span>  </span><span>let</span><span> </span><span>parts</span><span><span> </span><span>= </span><span>()</span></span></div></div><div><div><div>42</div></div><div><span>  </span><span>for</span><span> </span><span>m</span><span> </span><span>in</span><span> </span><span>spec</span><span>.</span><span>matches</span><span>(</span><span>pat</span><span>) {</span></div></div><div><div><div>43</div></div><div><span>    </span><span>let</span><span> </span><span>sep</span><span><span> </span><span>= </span></span><span>m</span><span>.</span><span>captures</span><span>.</span><span>at</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>if</span><span> </span><span>sep</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>45</div></div><div><span>      </span><span>parts</span><span>.</span><span>push</span><span>(</span><span>if</span><span> </span><span>sep</span><span><span> </span><span>==</span><span> </span></span><span>"/"</span><span> { </span><span>"/"</span><span> } </span><span>else</span><span> { </span><span>thin</span><span> })</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>47</div></div><div><span>      </span><span>parts</span><span>.</span><span>push</span><span>(</span><span>m</span><span>.</span><span>captures</span><span>.</span><span>at</span><span>(</span><span>1</span><span>))</span></div></div><div><div><div>48</div></div><div><span>      </span><span>let</span><span> </span><span>p</span><span><span> </span><span>= </span></span><span>m</span><span>.</span><span>captures</span><span>.</span><span>at</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>49</div></div><div><span>      </span><span>if</span><span> </span><span>p</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { </span><span>parts</span><span>.</span><span>push</span><span>(</span><span>super</span><span>(</span><span>p</span><span>.</span><span>replace</span><span>(</span><span>"-"</span><span>, </span><span>"</span><span>\u{2212}</span><span>"</span><span>))) }</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>52</div></div><div><span>  </span><span>text</span><span>(</span><span>style</span><span>: </span><span>"normal"</span><span>, </span><span>parts</span><span>.</span><span>join</span><span>())</span></div></div><div><div><div>53</div></div><div><span>}</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>/// Number plus unit, kept on one line.</span></div></div><div><div><div>56</div></div><div><span>#let</span><span> </span><span>qty</span><span>(</span><span>value</span><span>, </span><span>spec</span><span><span>) </span><span>= </span></span><span>box</span><span>(</span><span>num</span><span>(</span><span>value</span><span><span>) </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>unit</span><span>(</span><span>spec</span><span>))</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>/// Range with unit: `lo～hi unit` (zh) or `lo–hi unit` (en).</span></div></div><div><div><div>59</div></div><div><span>/// `lang: auto` follows `text.lang` at the call site.</span></div></div><div><div><div>60</div></div><div><span>#let</span><span> </span><span>qty-</span><span>range</span><span>(</span><span>lo</span><span>, </span><span>hi</span><span>, </span><span>spec</span><span>, </span><span>lang</span><span>: </span><span>auto</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>61</div></div><div><span>  </span><span>let</span><span> </span><span>build</span><span>(</span><span>l</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>62</div></div><div><span>    </span><span>let</span><span> </span><span>dash</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>l</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> { </span><span>"～"</span><span> } </span><span>else</span><span> { </span><span>"–"</span><span> }</span></div></div><div><div><div>63</div></div><div><span>    </span><span>box</span><span>(</span><span>num</span><span>(</span><span>lo</span><span><span>) </span><span>+</span><span> </span></span><span>dash</span><span><span> </span><span>+</span><span> </span></span><span>num</span><span>(</span><span>hi</span><span><span>) </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>unit</span><span>(</span><span>spec</span><span>))</span></div></div><div><div><div>64</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>65</div></div><div><span>  </span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>auto</span><span> { </span><span>context</span><span> </span><span>build</span><span>(</span><span>text</span><span>.</span><span>lang</span><span>) } </span><span>else</span><span> { </span><span>build</span><span>(</span><span>lang</span><span>) }</span></div></div><div><div><div>66</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong><code>group</code>：分组。</strong> 输入是一串数字字符。<code>.clusters()</code> 切成字素（这里就是单个数字，第 2 篇讲过字节、字素和码点的区别），少于 5 位直接返回，所以 <code>1234</code> 保持原样，测试里 <code>num("1234")</code> 断言的就是这一点。整数部分要从右往左每三位断开：第 <code>i</code> 个字符前面插入分隔，条件是它到末尾的距离 <code>n - i</code> 是 3 的倍数且不是第一个。小数部分反过来从左往右，条件是 <code>i</code> 是 3 的倍数。所以 <code>1234567.891</code> 得到 <code>1 234 567.891</code>，<code>0.123456</code> 得到 <code>0.123 456</code>。分隔符 <code>thin</code> 是 U+2009 细空格，是一个真正的字符，所以文本里能被复制出来。</p><p><strong><code>num</code>：加上符号与小数点。</strong> <code>str(value)</code> 把数字或字符串统一成字符串（<code>num(-0.000012345)</code> 传的是浮点数，<code>str</code> 的结果是 <code>-0.000012345</code>）。先把 <code>-</code> 换成 U+2212，再用 <code>starts-with</code> 把它剥出来，用 <code>s.slice(3)</code> 去掉：U+2212 在 UTF-8 里是 3 个字节，而 <code>slice</code> 按字节算，这又是第 2 篇的主题。然后按 <code>.</code> 切整数与小数，各自 <code>group</code>，拼回去。<code>num</code> 返回 <code>str</code>，不是 content，所以后面能直接做字符串断言。它不做四舍五入，要显示几位就传几位，浮点数传进来之前自己想好。</p><p><strong><code>unit</code>：单位字符串解析。</strong> <code>"m/s^2"</code> 和 <code>"kg m^-1"</code> 用一条正则拆开：<code>(\s+|/)|([^\s/^]+)(?:\^([-+]?\d+))?</code>。分支一匹配分隔符（空白或 <code>/</code>），分支二匹配一个单位记号，后面可以跟 <code>^</code> 与一个整数指数。<code>spec.matches(pat)</code> 返回所有匹配，每个匹配的 <code>captures</code> 按顺序给出三个括号里的内容；<code>captures.at(0)</code> 非 <code>none</code> 说明是分隔符，<code>/</code> 原样输出，空白换成细空格；否则是记号，指数非空时放进 <code>super(...)</code>，指数里的 <code>-</code> 同样换成真减号。最后 <code>text(style: "normal", parts.join())</code> 保证单位是正体，即使周围在斜体里。</p><p><strong><code>qty</code> 与 <code>qty-range</code>。</strong> <code>qty(value, spec)</code> 是 <code>num</code> 加细空格加 <code>unit</code>，用 <code>box</code> 包起来：<code>box</code> 是行内不可断行的容器，一个数量不会在数字和单位之间被折到两行。<code>qty-range</code> 的破折号随语言：中文 <code>～</code>，英文 <code>–</code>。<code>lang: auto</code> 时用 <code>context build(text.lang)</code>，在调用位置读当前语言；既然返回的是 <code>context</code>，在 <code>plain-text</code> 这类要把内容变成字符串的地方就读不到文字，所以测试里总是显式传 <code>lang:</code>。</p></section>
<section><h2>测试：把 num 与 qty 的行为钉死<a href="#测试把-num-与-qty-的行为钉死"><span>#</span></a></h2><p><code>tests/units.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/tests/units.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>thin</span><span><span> </span><span>= </span></span><span>"</span><span>\u{2009}</span><span>"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// Groups of three from the decimal point outwards, from 5 digits up.</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"1234567.891"</span><span>), </span><span>"1"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"234"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"567.891"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"1234"</span><span>), </span><span>"1234"</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"12345"</span><span>), </span><span>"12"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"345"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"0.123456"</span><span>), </span><span>"0."</span><span><span> </span><span>+</span><span> </span></span><span>"123"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"456"</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span><span>(</span><span>-</span></span><span>1234567</span><span>), </span><span>"</span><span>\u{2212}</span><span>1"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"234"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"567"</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// Quantities: upright unit, thin space after the number, `^n` superscripts.</span></div></div><div><div><div>13</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>qty</span><span>(</span><span>9.81</span><span>, </span><span>"m/s^2"</span><span>)), </span><span>"9.81"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"m/s2"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span></div></div><div><div><div>15</div></div><div><span>  </span><span>plain-text</span><span>(</span><span>qty-range</span><span>(</span><span>0</span><span>, </span><span>50</span><span>, </span><span>"mm"</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)),</span></div></div><div><div><div>16</div></div><div><span>  </span><span>"0～50"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"mm"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div><div><div><div>18</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>qty-range</span><span>(</span><span>0</span><span>, </span><span>50</span><span>, </span><span>"mm"</span><span>, </span><span>lang</span><span>: </span><span>"en"</span><span>)), </span><span>"0–50"</span><span><span> </span><span>+</span><span> </span></span><span>thin</span><span><span> </span><span>+</span><span> </span></span><span>"mm"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>assert.eq</code> 失败时整个编译失败，<code>make test</code> 逐个编译 <code>tests/*.typ</code>，编译通过就是测试通过：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make test</span></div></div><div><div><div>2</div></div><div><span>typst compile tests/glossary.typ</span></div></div><div><div><div>3</div></div><div><span>typst compile tests/homework.typ</span></div></div><div><div><div>4</div></div><div><span>typst compile tests/slides.typ</span></div></div><div><div><div>5</div></div><div><span>typst compile tests/theorem.typ</span></div></div><div><div><div>6</div></div><div><span>typst compile tests/units.typ</span></div></div><div><div><div>7</div></div><div><span>typst compile tests/util.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>qty</code> 返回的是 <code>box</code>，不是字符串，所以测试里用第 3 篇写的 <code>plain-text</code> 把它展平，再和字符串比较。<code>plain-text(qty(9.81, "m/s^2"))</code> 得到 <code>"9.81" + thin + "m/s2"</code>：指数 <code>2</code> 作为普通字符进了字符串，上标的“格式”在 <code>plain-text</code> 里是不见的，这点要记住，它测的是文字内容，不是排版。</p><p>要确认这些断言真的能失败，我在 <code>/tmp/p09/exp/bad.typ</code> 里写了一条故意错的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>num</span><span>(</span><span>"12345"</span><span>), </span><span>"12345"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts bad.typ bad.pdf</span></div></div><div><div><div>2</div></div><div><span>error: equality assertion failed: value "12\u{2009}345" was not equal to "12345"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ bad.typ:2:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>2 │ #assert.eq(num("12345"), "12345")</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>失败信息里细空格以 <code>\u{2009}</code> 出现，正是第 1 篇讲过的转义写法，这样肉眼看不见的字符在报错里也能认出来。</p></section>
<section><h2>三个文件怎么接进 xuan<a href="#三个文件怎么接进-xuan"><span>#</span></a></h2><p><code>figure.typ</code> 和 <code>code.typ</code> 都不在 <code>lib.typ</code> 里导出：用户不需要直接调用它们，只需要 <code>xuan-base</code> 把它们的规则装上。<code>lib.typ</code> 这一篇新增的只有一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"src/units.typ"</span><span>: </span><span>num</span><span>, </span><span>qty</span><span>, </span><span>qty-range</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>src/base.typ</code> 的当前版本如下，这一篇新增的是 <code>code-rules</code> 与 <code>figure-rules</code> 的 import，和两行 <code>show: figure-rules</code>、<code>show: code-rules</code>：即上面 import 区里的 <code>code.typ</code>、<code>figure.typ</code> 两行，与末尾 <code>show: figure-rules</code>、<code>show: code-rules</code> 两行。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"cjk.typ"</span><span>: </span><span>xuan-cjk</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"code.typ"</span><span>: </span><span>code-rules</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"figure.typ"</span><span>: </span><span>figure-rules</span></div></div><div><div><div>4</div></div><div><span>#import</span><span> </span><span>"math.typ"</span><span>: </span><span>math-rules</span></div></div><div><div><div>5</div></div><div><span>#import</span><span> </span><span>"theorem.typ"</span><span>: </span><span>theorem-rules</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/// Document-wide defaults: CJK typography, heading numbering and spacing,</span></div></div><div><div><div>8</div></div><div><span>/// link colour, theorem, equation, figure and code rules.</span></div></div><div><div><div>9</div></div><div><span>#let</span><span> </span><span>xuan-base</span><span>(</span><span>body</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>, </span><span>title</span><span>: </span><span>none</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>10</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-cjk</span><span>.</span><span>with</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>)</span></div></div><div><div><div>11</div></div><div><span>  </span><span>set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>: </span><span>set</span><span> </span><span>block</span><span>(</span><span>above</span><span>: </span><span>1.4em</span><span>, </span><span>below</span><span>: </span><span>0.9em</span><span>)</span></div></div><div><div><div>13</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.4em</span><span>)</span></div></div><div><div><div>14</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>set</span><span> </span><span>block</span><span>(</span><span>above</span><span>: </span><span>1.8em</span><span>, </span><span>below</span><span>: </span><span>1em</span><span>)</span></div></div><div><div><div>15</div></div><div><span>  </span><span>show</span><span> </span><span>link</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>))</span></div></div><div><div><div>16</div></div><div><span>  </span><span>show</span><span>: </span><span>theorem-rules</span></div></div><div><div><div>17</div></div><div><span>  </span><span>show</span><span>: </span><span>math-rules</span></div></div><div><div><div>18</div></div><div><span>  </span><span>show</span><span>: </span><span>figure-rules</span></div></div><div><div><div>19</div></div><div><span>  </span><span>show</span><span>: </span><span>code-rules</span></div></div><div><div><div>20</div></div><div><span>  </span><span>body</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>figure-rules</code> 里的 <code>show ref</code>、<code>show table</code> 与 <code>code-rules</code> 里的 <code>show raw</code> 作用在不同的元素上。<code>theorem-rules</code> 里的定理也是 <code>figure</code>（<code>kind: "xuan-theorem"</code>），它们和 <code>figure-rules</code> 共存靠的是 <code>figure-kinds</code> 这份白名单：<code>xuan-theorem</code> 不在里面，所以 <code>figure-rules</code> 的 <code>show ref</code> 放过定理的引用，由 <code>theorem-rules</code> 自己的 <code>show ref</code> 处理。</p></section>
<section><h2>我没有做的事<a href="#我没有做的事"><span>#</span></a></h2><p><code>figure</code> 的 <code>scope: "parent"</code>（跨栏浮动）我没有编译，<code>table</code> 的 <code>inset</code>、<code>gutter</code>、<code>stroke</code> 参数的全部取值我也没有一一试，这些在官方文档里有。<code>image</code> 我只用了 SVG 的 <code>image("assets/projection.svg", width: 60%)</code>，PNG、JPEG、PDF 页面的差别没有测。有兴趣的话可以拿 <code>lab/ch09/</code> 的文件当起点，每次改一个参数，对照渲染图看。</p><p>下一篇是参考文献：BibTeX、Hayagriva、GB/T 7714 和 <code>cite</code> 的各种形式。图、表、代码和公式在这一篇之后都有了统一的编号，文献引用会是最后一类要接进 <code>note</code> 的东西。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-08-math/</id>
      <title type="text">数学公式</title>
      <published>2026-10-19T07:00:00.000Z</published>
      <updated>2026-10-19T07:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-08-math/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 8 篇：我用 Typst 0.15.1 编译了 ch08 的 9 个 lab 文件，把最小二乘和 SVD 两篇笔记里的公式重排了一遍（三个公式的源码从 236 个字符降到 169 个），用 eval 和 XeTeX 核对了 32 行 LaTeX 到 Typst 对照表，在 /tmp 里复现了编号函数带 context 时 @label 引用串章，再写出 xuan 的 math.typ，让公式按“章.序号”编号。</summary>
      <content type="html"><![CDATA[<p>我从自己的线性代数笔记里挑了三个公式：最小二乘的正规方程解、SVD 的外积展开，还有一个 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的矩阵乘法，用 Typst 重新排了一遍。三个公式十来行源码，编译出来是下面这样，而且它们已经带着 <code>(0.1)</code>、<code>(0.2)</code>、<code>(0.3)</code> 的编号（为什么是 0，后面讲）。这一篇我把 Typst 的数学模式从头跑一遍：怎么写，怎么对齐，哪里和直觉不一样，最后写出 <code>xuan</code> 里负责“按章编号、引用不串章”的 <code>math.typ</code>。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有一个新文件 <code>src/math.typ</code>，是<strong>最终版</strong>（41 行，文中完整展示）。<code>lib.typ</code> 和 <code>src/base.typ</code> 仍是<strong>中间版本</strong>：<code>lib.typ</code> 加一行 <code>import</code>，<code>base.typ</code> 加一行 <code>import</code> 和一行 <code>show: math-rules</code>，都在文中给出。<code>lab/ch08/</code> 下 9 个文件全部通过 <code>make ch08</code>。
仓库 HEAD 的 <code>base.typ</code> 还多 <code>import</code> 了 <code>figure.typ</code> 和 <code>code.typ</code>（第 9 篇才讲），所以和前面几篇一样，先把仓库里的 <code>src/</code> 整个拷过来，把那两个文件当黑盒。
前置：<a href="/posts/typst-by-xuan-07-context-counters-state/">第 7 篇</a>（<code>context</code>、计数器、<code>show ref</code> 和 <code>el.location()</code>）。这一篇的编号机制是第 7 篇定理环境的方程版，我不会重讲计数器本身。</p></div></div>
<p></p><figure><img src="/assets/images/typst-by-xuan/ch08-math.png" alt="三行块公式：x 帽等于 argmin 范数平方，等于 A 转置 A 的逆乘 A 转置 b；A 等于 U Sigma V 转置，等于 sigma_i u_i v_i 转置的求和；二阶矩阵乘列向量。每行右侧有编号 (0.1) 到 (0.3)" /><figcaption>三行块公式：x 帽等于 argmin 范数平方，等于 A 转置 A 的逆乘 A 转置 b；A 等于 U Sigma V 转置，等于 sigma_i u_i v_i 转置的求和；二阶矩阵乘列向量。每行右侧有编号 (0.1) 到 (0.3)</figcaption></figure><p></p>
<section><h2>先看这三行是怎么写的<a href="#先看这三行是怎么写的"><span>#</span></a></h2><p><code>lab/ch08/fig-math.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch08/fig-math.typ" target="_blank">源文件</a>）就是上面那张图：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A small gallery rendered as one PNG for the post.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>$</span></div></div><div><div><div>8</div></div><div><span>  </span><span>hat</span><span>(x) = </span><span>argmin</span><span>_</span><span>(</span><span>x </span><span>in</span><span> </span><span>RR</span><span>^n</span><span>)</span><span> </span><span>norm</span><span>(A x - b)_2^2</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>= (A^T A)^</span><span>(</span><span>-1</span><span>)</span><span> A^T b</span></div></div><div><div><div>10</div></div><div><span>$</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>$</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>A = U </span><span>Sigma</span><span> V^T = </span><span>sum</span><span>_</span><span>(</span><span>i=1</span><span>)</span><span>^r </span><span>sigma</span><span>_i u_i v_i^T,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>quad</span><span> </span><span>sigma</span><span>_1 &gt;= </span><span>sigma</span><span>_2 &gt;= </span><span>dots</span><span>.</span><span>h</span><span>.</span><span>c</span><span> &gt;= </span><span>sigma</span><span>_r &gt; 0</span></div></div><div><div><div>15</div></div><div><span>$</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>$</span><span> </span><span>mat</span><span>(1, 2; 3, 4) </span><span>vec</span><span>(x, y) = </span><span>vec</span><span>(x + 2 y, 3 x + 4 y) </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读一遍就能看出 Typst 数学语法的几个基本约定，后面每一节都是在展开其中一条：</p><ul>
<li>单个字母是变量，<code>x y</code> 之间的空格只是分隔，不是乘号。<code>sigma</code>、<code>Sigma</code>、<code>RR</code> 这类多字母名字是符号或函数。</li>
<li><code>_</code> 和 <code>^</code> 是下标和上标。紧跟着的括号是<strong>分组</strong>，括号本身不显示：<code>_(x in RR^n)</code> 的括号不见了。</li>
<li>但括号如果是被附着的对象，就照常显示：<code>(A^T A)^(-1)</code> 里 <code>(A^T A)</code> 的括号在图里还在。</li>
<li><code>argmin</code>、<code>norm</code>、<code>mat</code>、<code>vec</code> 是函数，参数用逗号隔开，<code>mat</code> 里分号分行。</li>
<li><code>&gt;=</code> 自动变成 <span><span>≥\geq</span><span><span><span></span><span>≥</span></span></span></span>，<code>dots.h.c</code> 是居中的省略号 <span><span>⋯\cdots</span><span><span><span></span><span>⋯</span></span></span></span>。</li>
</ul><p>我用 Typst 0.15.1 编译 ch08 全部 9 个文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make ch08</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch08</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch08/alignment.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch08/fig-math.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch08/identifiers.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch08/inline-vs-block.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch08/latex-map.typ</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch08/math-functions.typ</span></div></div><div><div><div>9</div></div><div><span>ok   lab/ch08/matrices.typ</span></div></div><div><div><div>10</div></div><div><span>ok   lab/ch08/numbering.typ</span></div></div><div><div><div>11</div></div><div><span>ok   lab/ch08/operators.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整章 0.79 秒（<code>time make ch08</code>，在 <code>nix develop</code> 里，含 9 个文件的编译）。32 行对照表那一页单独编译只要 0.129 秒。</p></section>
<section><h2>数学模式：<code>$</code> 的两种形状<a href="#数学模式-的两种形状"><span>#</span></a></h2><p>第 1 篇我说过 Typst 有标记、代码、数学三种模式，<code>$</code> 是进入数学模式的门。门有两种开法，由空格决定：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Math mode: `$x$` is inline, `$ x $` (spaces inside) is a block equation.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Inline: 级数 </span><span>$</span><span>sum</span><span>_</span><span>(</span><span>k=1</span><span>)</span><span>^n k = n(n+1)/2</span><span>$</span><span> 压在行内，</span></div></div><div><div><div>7</div></div><div><span>上下限变成角标；块公式则展开：</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>$</span><span> </span><span>sum</span><span>_</span><span>(</span><span>k=1</span><span>)</span><span>^n k = n(n+1)/2 </span><span>$</span><span> </span><span>&lt;gauss&gt;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Typst 的 </span><span>`$ ... $`</span><span> 两侧有空格即为块公式。公式 </span><span>@gauss</span><span> 带编号，</span></div></div><div><div><div>12</div></div><div><span>行内公式不带。用 </span><span>`math.display`</span><span> 与 </span><span>`math.inline`</span><span> 可以强制样式：</span></div></div><div><div><div>13</div></div><div><span>$</span><span>display</span><span>(</span><span>sum</span><span>_</span><span>(</span><span>k=1</span><span>)</span><span>^n k)</span><span>$</span><span>。</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>// Text in math needs quotes; multi-letter names are otherwise symbols.</span></div></div><div><div><div>16</div></div><div><span>$</span><span> </span><span>"rank"</span><span>(A) = </span><span>dim</span><span> </span><span>"col"</span><span>(A) </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-inline-vs-block.png" alt="行内求和的上下限变成角标，块公式的求和号上下带限；块公式 (0.1) 被引用为“式 (0.1)”；display 强制行内公式使用块样式；最后一行 rank(A) 等于 dim col(A) 编号 (0.2)" /><figcaption>行内求和的上下限变成角标，块公式的求和号上下带限；块公式 (0.1) 被引用为“式 (0.1)”；display 强制行内公式使用块样式；最后一行 rank(A) 等于 dim col(A) 编号 (0.2)</figcaption></figure><p></p><p><code>$...$</code> 内侧两边都是空格才是块公式。只有一边有空格会怎样？我没有凭文档猜，直接让 <code>eval</code> 把四种写法编译出来，读 <code>equation</code> 元素的 <code>block</code> 字段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command typst eval '("$x$", "$ x$", "$x $", "$ x $").map(s =&gt; eval(s).block)' --format yaml</span></div></div><div><div><div>2</div></div><div><span>- false</span></div></div><div><div><div>3</div></div><div><span>- false</span></div></div><div><div><div>4</div></div><div><span>- false</span></div></div><div><div><div>5</div></div><div><span>- true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只有两边都有空格的那个是块公式；一边有空格的还是行内。顺带一提，<code>typstyle</code> 会把一边有空格的 <code>$ x$</code> 规整成 <code>$x$</code>，类型不变，仍是行内。</p><p>同一个 <code>sum_(k=1)^n k</code>，行内排成 <span><span>∑k=1n\sum_{k=1}^n</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>k</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的角标形式（压住行高），块里才把上下限放在求和号上下。这是排版里“display style”和“text style”的区别，Typst 把它绑定在 <code>block</code> 上，需要时用 <code>display(...)</code>、<code>inline(...)</code> 对一小段强制切换。</p><p><strong>公式本身也是一个元素。</strong> <code>$ ... $</code> 只是 <code>math.equation</code> 的语法糖，也可以直接调用函数，传 <code>block: true</code> 和 <code>numbering</code>。这在对齐那一节会用到。字体方面，<code>xuan-cjk</code> 里有一行 <code>show math.equation: set text(font: fonts.math)</code>，公式统一用 New Computer Modern Math（第 5 篇讲过），所以图里的数学字体是 Typst 内嵌的，不依赖系统。</p></section>
<section><h2>标识符、文本和符号<a href="#标识符文本和符号"><span>#</span></a></h2><p><code>lab/ch08/identifiers.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch08/identifiers.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// In math, single letters are variables; longer names look up symbols.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>- 单字母：</span><span>$</span><span>x y</span><span>$</span><span> 是两个变量，</span><span>$</span><span>x y z</span><span>$</span><span> 里空格只是分隔。</span></div></div><div><div><div>7</div></div><div><span>- 多字母：</span><span>$</span><span>alpha</span><span> </span><span>beta</span><span>$</span><span> 是希腊字母，</span><span>`$ab$`</span><span> 查找名为 </span><span>`ab`</span><span> 的变量。</span></div></div><div><div><div>8</div></div><div><span>- 符号与修饰：</span><span>$</span><span>arrow</span><span>.</span><span>r</span><span>$</span><span>、</span><span>$</span><span>arrow</span><span>.</span><span>r</span><span>.</span><span>double</span><span>$</span><span>、</span><span>$</span><span>lt</span><span>.</span><span>eq</span><span>$</span><span>、</span><span>$</span><span>times</span><span>$</span><span>、</span><span>$</span><span>dots</span><span>.</span><span>h</span><span>$</span><span>。</span></div></div><div><div><div>9</div></div><div><span>- 引号里是文本：</span><span>$</span><span>"if"</span><span> x &gt; 0</span><span>$</span><span>；</span><span>`#`</span><span> 进入代码：</span><span>$</span><span>#</span><span>(</span><span>1</span><span><span> </span><span>+</span><span> </span></span><span>2</span><span>) x</span><span>$</span><span>。</span></div></div><div><div><div>10</div></div><div><span>- 转义与 Unicode：</span><span>$</span><span>\{</span><span> x </span><span>\}</span><span>$</span><span>、</span><span>$</span><span>\u{2211}</span><span>$</span><span>。</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#let</span><span> </span><span>v</span><span><span> </span><span>= </span></span><span>$</span><span>bold</span><span>(v)</span><span>$</span></div></div><div><div><div>13</div></div><div><span>$</span><span> </span><span>norm</span><span>(v) = </span><span>sqrt</span><span>(v^T v) </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-identifiers.png" alt="五条要点的渲染：xy 与 xyz 是变量，alpha beta 是希腊字母，若干箭头与比较符号，if 以正体文本显示，3x 来自代码，花括号与求和号；末行是 v 的范数等于 v 转置 v 的平方根" /><figcaption>五条要点的渲染：xy 与 xyz 是变量，alpha beta 是希腊字母，若干箭头与比较符号，if 以正体文本显示，3x 来自代码，花括号与求和号；末行是 v 的范数等于 v 转置 v 的平方根</figcaption></figure><p></p><p>这一页最值得停下来的是<strong>多字母名字的查找规则</strong>。在数学模式里，一个多字母的标识符不会被拆成一串变量，而是被当作名字去查：先查数学作用域（<code>alpha</code>、<code>sum</code>、<code>mat</code> 都在这里），再查你的代码作用域（所以 <code>#import "@local/xuan:0.1.0": *</code> 之后，<code>rank</code>、<code>tr</code> 就能在公式里用了）。都查不到就报错。我把 <code>$ab + 1$</code> 单独编译了一下（<code>/tmp/p8/unknown-ab.typ</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unknown variable: ab</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ unknown-ab.typ:1:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ $ab + 1$</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= hint: if you meant to display multiple letters as is, try adding spaces between each letter: `a b`</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= hint: or if you meant to display this as text, try placing it in quotes: `"ab"`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器两条提示正好对应两种需求：想要两个变量，写 <code>a b</code>；想要一个词，写 <code>"ab"</code>。<code>"if"</code>、<code>"rank"</code> 这类文本放在引号里，就是正体字，不会被当成变量斜排。顺便说，<code>xuan</code> 把 <code>rank</code> 做成了运算符（后面讲），所以 <code>rank(A)</code> 不用引号，而 <code>"col"(A)</code> 这种临时用一次的词仍然用引号。</p><p><strong>符号的“点语法”。</strong> <code>arrow.r.double</code> 是 <code>arrow</code> 符号加上 <code>r</code>、<code>double</code> 两个修饰。我找符号不翻文档，直接问编译器：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command bash -c "typst eval 'repr(sym.chevron)' | jq -r . | head -8"</span></div></div><div><div><div>2</div></div><div><span>symbol(</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>("l", "⟨"),</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>("l.curly", "⧼"),</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>("l.dot", "⦑"),</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>("l.closed", "⦉"),</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>("l.double", "⟪"),</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>("r", "⟩"),</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>("r.curly", "⧽"),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>typst eval</code> 默认输出 JSON，所以用 <code>jq -r .</code> 把字符串还原。<code>sym.arrow.r</code> 同样能这样列出它的 51 个变体（<code>double</code>、<code>long</code>、<code>bar</code>、<code>squiggly</code> 等，我用 <code>grep -c</code> 数过）。注意 0.15.1 里没有 <code>angle.l</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unknown symbol modifier</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ no-angle.typ:1:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ $ angle.l u, v angle.r $</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│         ^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 0.15.1 里尖括号叫 <code>chevron.l</code> / <code>chevron.r</code>，上面 <code>repr</code> 的输出里就有。这条命令适合“想批量看一个符号有哪些变体”的时候，比翻文档快。</p><p>最后两个入口：<code>#</code> 进入代码，<code>$#(1 + 2) x$</code> 得到 <span><span>3x3x</span><span><span><span></span><span>3</span><span>x</span></span></span></span>，<code>#</code> 后面的括号表达式整体作为一个值被插入；反斜杠转义 <code>\{</code>，<code>\u{2211}</code> 直接给 Unicode。</p></section>
<section><h2>上下标、分式、根号和定界符<a href="#上下标分式根号和定界符"><span>#</span></a></h2><p>附着（attachment）规则在上一节的图里已经出现：<code>_</code> <code>^</code> 后面跟一个字母、一个数字或一个符号，要多个就加括号分组。我把没放进 lab 的写法收在 <code>/tmp/p8/tricks.typ</code> 里，下面分段读，整个文件一次性编译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>numbering</span><span>: </span><span>none</span><span>)</span></div></div><div><div><div>5</div></div><div><span>// attachments</span></div></div><div><div><div>6</div></div><div><span>$</span></div></div><div><div><div>7</div></div><div><span>  </span><span>attach</span><span>(A, </span><span>tl</span><span>: 1, </span><span>tr</span><span>: 2, </span><span>bl</span><span>: 3, </span><span>br</span><span>: 4, t: 5, b: 6) </span><span>quad</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>x_</span><span>(</span><span>i+1</span><span>)</span><span>^</span><span>(</span><span>2n</span><span>)</span><span> </span><span>quad</span><span> A^T </span><span>quad</span><span> A^</span><span>top</span><span> </span><span>quad</span><span> A^</span><span>(</span><span>upright</span><span>(T)</span><span>)</span><span> </span><span>quad</span><span> A^</span><span>sans</span><span>(T)</span></div></div><div><div><div>9</div></div><div><span>$</span></div></div><div><div><div>10</div></div><div><span>// augmented matrix</span></div></div><div><div><div>11</div></div><div><span>$</span><span> </span><span>mat</span><span>(1, 2, 3; 4, 5, 6; </span><span>augment</span><span>: </span><span>#2</span><span>) </span><span>$</span></div></div><div><div><div>12</div></div><div><span>$</span><span> </span><span>mat</span><span>(1, 2; 3, 4; </span><span>augment</span><span>: </span><span>#</span><span>(</span><span>vline</span><span>: </span><span>1</span><span>)) </span><span>$</span></div></div><div><div><div>13</div></div><div><span>// rows from arrays</span></div></div><div><div><div>14</div></div><div><span>#let</span><span> </span><span>rows</span><span><span> </span><span>= </span><span>((</span></span><span>1</span><span>, </span><span>2</span><span>), (</span><span>3</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>15</div></div><div><span>$</span><span> </span><span>mat</span><span>(..</span><span>#rows</span><span>) </span><span>$</span></div></div><div><div><div>16</div></div><div><span>$</span><span> </span><span>mat</span><span>(</span><span>sigma</span><span>_1, , ; , </span><span>dots</span><span>.</span><span>down</span><span>, ; , , </span><span>sigma</span><span>_r) </span><span>$</span></div></div><div><div><div>17</div></div><div><span>$</span><span> </span><span>mat</span><span>(</span><span>delim</span><span>: </span><span>"|"</span><span>, a, b; c, d) = a d - b c </span><span>$</span></div></div><div><div><div>18</div></div><div><span>$</span><span> </span><span>vec</span><span>(1, 2, 3, </span><span>delim</span><span>: </span><span>"["</span><span>) </span><span>quad</span><span> </span><span>vec</span><span>(a, b, </span><span>gap</span><span>: </span><span>#1em</span><span>) </span><span>$</span></div></div><div><div><div>19</div></div><div><span>// cases spacing</span></div></div><div><div><div>20</div></div><div><span>$</span><span> </span><span>cases</span><span>(1 &amp; x &gt; 0, 0) </span><span>quad</span><span> </span><span>cases</span><span>(1 &amp; </span><span>quad</span><span> x &gt; 0, 0) </span><span>$</span></div></div><div><div><div>21</div></div><div><span>$</span><span> </span><span>cases</span><span>(</span><span>sigma</span><span>_i^2 &amp; i = j, 0 &amp; i != j) </span><span>$</span></div></div><div><div><div>22</div></div><div><span>// bold</span></div></div><div><div><div>23</div></div><div><span>$</span></div></div><div><div><div>24</div></div><div><span>  </span><span>bold</span><span>(x) </span><span>quad</span><span> </span><span>bold</span><span>(</span><span>upright</span><span>(x)) </span><span>quad</span><span> </span><span>upright</span><span>(</span><span>bold</span><span>(x)) </span><span>quad</span></div></div><div><div><div>25</div></div><div><span>  </span><span>bold</span><span>(A) </span><span>quad</span><span> </span><span>bold</span><span>(</span><span>upright</span><span>(A))</span></div></div><div><div><div>26</div></div><div><span>$</span></div></div><div><div><div>27</div></div><div><span>// frac</span></div></div><div><div><div>28</div></div><div><span>$</span><span> (a + b) / c </span><span>quad</span><span> </span><span>frac</span><span>(a + b, c) </span><span>quad</span><span> (a+b)^2 / </span><span>(</span><span>c+d</span><span>)</span><span> </span><span>quad</span><span> a </span><span>slash</span><span> b </span><span>$</span></div></div><div><div><div>29</div></div><div><span>$</span><span> x' </span><span>quad</span><span> f'(x) </span><span>quad</span><span> f''(x) </span><span>quad</span><span> x^</span><span>prime</span><span> </span><span>quad</span><span> x^</span><span>(</span><span>(n)</span><span>)</span><span> </span><span>$</span></div></div><div><div><div>30</div></div><div><span>$</span><span> </span><span>lr</span><span>(| x / 2 |) </span><span>quad</span><span> </span><span>lr</span><span>(</span><span>chevron</span><span>.</span><span>l</span><span> u, v </span><span>chevron</span><span>.</span><span>r</span><span>) </span><span>quad</span><span> {x </span><span>in</span><span> </span><span>RR</span><span> </span><span>mid</span><span>(|) x &gt; 0} </span><span>$</span></div></div><div><div><div>31</div></div><div><span>$</span><span> P = A (A^T A)^</span><span>(</span><span>-1</span><span>)</span><span> A^T </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-tricks.png" alt="tricks.typ 的渲染：六个位置都有附着的 A；增广矩阵；用数组拼出的矩阵；带空位的对角矩阵；行列式；两种定界符的向量；两种 cases；粗体变体；分式；撇号；auto-scale 的定界符" /><figcaption>tricks.typ 的渲染：六个位置都有附着的 A；增广矩阵；用数组拼出的矩阵；带空位的对角矩阵；行列式；两种定界符的向量；两种 cases；粗体变体；分式；撇号；auto-scale 的定界符</figcaption></figure><p></p><p>先看第一组。<code>attach(A, tl: 1, tr: 2, bl: 3, br: 4, t: 5, b: 6)</code> 一次给六个位置挂东西：左上、右上、左下、右下、正上、正下。<code>x_(i+1)^(2n)</code> 是日常写法，括号分组且不显示。转置有四种写法，我把它们放大看过（<code>/tmp/p8/transpose.typ</code>，24pt）：<code>A^T</code> 是斜体 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span>（字母 T 当变量），<code>A^upright(T)</code> 是衬线正体，<code>A^top</code> 是 <span><span>⊤\top</span><span><span><span></span><span>⊤</span></span></span></span> 符号，<code>A^sans(T)</code> 是无衬线<strong>斜体</strong>，不是正体。我最后在 <code>la.typ</code> 里选了 <code>top</code>，因为它不会和一个叫 <code>T</code> 的变量混淆。</p><p><strong>分式用除号，括号自动消掉。</strong> <code>(a + b) / c</code> 和 <code>frac(a + b, c)</code> 渲染完全一样（图里第一排前两个）：除号两侧的括号组被当作分子分母，括号不显示。<code>a / b</code> 是竖排分式，<code>a slash b</code> 才是斜线 <span><span>a/ba/b</span><span><span><span></span><span>a</span><span>/</span><span>b</span></span></span></span>。如果你真想要显示的括号，括号里再套一层：<code>x^((n))</code> 的结果是 <span><span>x(n)x^{(n)}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>(</span><span>n</span><span>)</span></span></span></span></span></span></span></span></span></span></span></span>，外层括号被吃掉，内层留下，这和上一节 <code>_(...)</code> 是一回事。撇号 <code>x'</code> 与 <code>x^prime</code> 相同，连写 <code>f''(x)</code> 得到二阶导。</p><p>根号有两个函数：<code>sqrt(x)</code>，以及带根指数的 <code>root(3, x)</code>，对照表里会再出现。</p><p><strong>定界符默认自动伸缩。</strong> <code>(...)</code>、<code>[...]</code>、<code>{...}</code> 里包着高内容时，括号随之变大，<code>lr(...)</code> 是把这件事显式化：<code>lr(| x / 2 |)</code> 的竖线随分式拉高。<code>mid(|)</code> 让中间的竖线也跟着伸缩，用在集合记号 <code>{x in RR mid(|) x &gt; 0}</code> 里。<code>abs</code>、<code>norm</code>、<code>floor</code>、<code>ceil</code> 是带对应定界符的简写，在 <code>matrices.typ</code> 里一起出现：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// vec, mat, cases and delimiters.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>$</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>A = </span><span>mat</span><span>(1, 2; 3, 4), </span><span>quad</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>x = </span><span>vec</span><span>(x_1, x_2, x_3), </span><span>quad</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>B = </span><span>mat</span><span>(</span><span>delim</span><span>: </span><span>"["</span><span>, 1, 0; 0, 1)</span></div></div><div><div><div>10</div></div><div><span>$</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>$</span><span> </span><span>mat</span><span>(1, </span><span>dots</span><span>.</span><span>h</span><span>, 0; </span><span>dots</span><span>.</span><span>v</span><span>, </span><span>dots</span><span>.</span><span>down</span><span>, </span><span>dots</span><span>.</span><span>v</span><span>; 0, </span><span>dots</span><span>.</span><span>h</span><span>, 1) </span><span>$</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>$</span></div></div><div><div><div>15</div></div><div><span>  </span><span>"sgn"</span><span>(x) = </span><span>cases</span><span>(</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>1 &amp; </span><span>"if"</span><span> x &gt; 0,</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>0 &amp; </span><span>"if"</span><span> x = 0,</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>-1 &amp; </span><span>"otherwise"</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>20</div></div><div><span>$</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>// lr scales delimiters; abs, norm, floor, ceil are shorthands.</span></div></div><div><div><div>23</div></div><div><span>$</span></div></div><div><div><div>24</div></div><div><span>  </span><span>lr</span><span>((</span><span>sum</span><span>_i a_i / b_i)) </span><span>quad</span><span> </span><span>abs</span><span>(x) </span><span>quad</span><span> </span><span>norm</span><span>(v) </span><span>quad</span><span> </span><span>floor</span><span>(x/2) </span><span>quad</span><span> </span><span>ceil</span><span>(x/2)</span></div></div><div><div><div>25</div></div><div><span>$</span></div></div><div><div><div>26</div></div><div><span>$</span><span> {x </span><span>in</span><span> </span><span>RR</span><span> | x &gt; 0} = </span><span>lr</span><span>({x </span><span>in</span><span> </span><span>RR</span><span> </span><span>mid</span><span>(|) x &gt; 0}) </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-matrices.png" alt="matrices.typ 的渲染：圆括号矩阵、竖向量与方括号单位阵；省略号矩阵；三分支 cases；被拉高的圆括号、绝对值、范数、下取整与上取整；集合记号两种写法渲染相同" /><figcaption>matrices.typ 的渲染：圆括号矩阵、竖向量与方括号单位阵；省略号矩阵；三分支 cases；被拉高的圆括号、绝对值、范数、下取整与上取整；集合记号两种写法渲染相同</figcaption></figure><p></p><p>最后一行的两种集合写法在图里一模一样：内容不高的时候，自动伸缩和显式 <code>lr</code>、<code>mid</code> 没有差别，要看出区别得换成很高的内容（上一节 <code>tricks.typ</code> 里的 <code>lr(| x / 2 |)</code> 就是高的）。</p></section>
<section><h2>矩阵、向量和 cases<a href="#矩阵向量和-cases"><span>#</span></a></h2><p><code>mat</code> 的约定是逗号分列、分号分行；<code>vec</code> 是只有一列。上面的 <code>tricks.typ</code> 里我试了几个不在 lab 文件里的用法，这里把每一个的结论读出来：</p><ul>
<li><strong><code>delim</code></strong> 决定外框：<code>mat(delim: "[", ...)</code> 是方括号，<code>mat(delim: "|", ...)</code> 直接得到行列式的竖线，图里 <span><span>∣abcd∣=ad−bc\begin{vmatrix} a &amp; b \\ c &amp; d \end{vmatrix} = ad - bc</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span></span></span></span> 就是这样排的。<code>vec</code> 同样接受 <code>delim</code>，还有 <code>gap</code> 调行距。</li>
<li><strong><code>augment</code></strong> 画增广竖线：<code>augment: #2</code> 在第 2 列后切一刀（线性方程组的增广矩阵），<code>augment: #(vline: 1)</code> 用字典控制竖线位置。</li>
<li><strong>数组展开成矩阵</strong>：<code>mat(..#rows)</code>，其中 <code>rows</code> 是数组的数组，<code>#</code> 让 <code>rows</code> 从代码进入数学，<code>..</code> 展开成参数，每个内层数组是一行。这是“用代码生成矩阵”的入口。</li>
<li><strong>留空</strong>：逗号之间什么都不写就是空格子，<code>mat(sigma_1, , ; , dots.down, ; , , sigma_r)</code> 是只写了对角线和 <span><span>⋱\ddots</span><span><span><span></span><span>⋱</span></span></span></span> 的对角阵，后面 SVD 那一节会用。</li>
</ul><p><code>cases</code> 有一个我花了一点时间才弄明白的细节：<code>&amp;</code> 在 <code>cases</code> 里只是对齐点，<strong>不插空隙</strong>。<code>tricks.typ</code> 里 <code>cases(1 &amp; x &gt; 0, 0)</code> 渲染成 <code>1x&gt;0</code>，两个东西挤在一起，而 <code>cases(1 &amp; quad x &gt; 0, 0)</code> 才分得开。<code>matrices.typ</code> 里的 <code>"if"</code> 前面看起来有间距，我没有细查是不是文本元素自带的，总之别指望它，<strong>写 <code>quad</code></strong>。对照表第 23 行我一开始也犯了这个错，现在它写成不用 <code>&amp;</code>、直接在值和条件之间放 <code>quad</code> 的形式。</p></section>
<section><h2>对齐：<code>&amp;</code> 和 <code>\</code><a href="#对齐-和-"><span>#</span></a></h2><p><code>lab/ch08/alignment.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `&amp;` marks the alignment point, `\` ends the line.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>$</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>(a + b)^2 &amp; = a^2 + 2 a b + b^2 </span><span>\</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>&amp; = a^2 + b^2 + 2 a b</span></div></div><div><div><div>9</div></div><div><span>$</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// Several columns: `&amp;` alternates right- and left-aligned.</span></div></div><div><div><div>12</div></div><div><span>$</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>x &amp; = 1 &amp; </span><span>quad</span><span> y &amp; = 2 </span><span>\</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>z &amp; = 3 &amp; </span><span>quad</span><span> w &amp; = 4</span></div></div><div><div><div>15</div></div><div><span>$</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>// An equation with a custom number, and without one.</span></div></div><div><div><div>18</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>numbering</span><span>: </span><span>none</span><span>)</span></div></div><div><div><div>19</div></div><div><span>$</span><span> f(x) = x^2 </span><span>$</span></div></div><div><div><div>20</div></div><div><span>#math</span><span>.</span><span>equation</span><span>(</span><span>block</span><span>: </span><span>true</span><span>, </span><span>numbering</span><span>: </span><span>"(A)"</span><span>)[</span><span>$</span><span> g(x) = x^3 </span><span>$</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-alignment.png" alt="alignment.typ 的渲染：第一个公式两行等号对齐，右侧一个编号 (0.1)；第二个公式两行四列，右侧一个编号 (0.2)；随后无编号的 f(x) 等于 x 平方；最后 g(x) 等于 x 立方，编号写成 (C)" /><figcaption>alignment.typ 的渲染：第一个公式两行等号对齐，右侧一个编号 (0.1)；第二个公式两行四列，右侧一个编号 (0.2)；随后无编号的 f(x) 等于 x 平方；最后 g(x) 等于 x 立方，编号写成 (C)</figcaption></figure><p></p><p><code>\</code> 换行，<code>&amp;</code> 标记对齐点：第一个 <code>&amp;</code> 左侧右对齐，右侧左对齐，所以 <code>&amp; =</code> 把两行的等号对到一条竖线上。多个 <code>&amp;</code> 交替地右对齐、左对齐，第二个公式就是两列“变量等于值”。</p><p>图里有两件事值得读出来。第一，<strong>一个块公式只有一个编号</strong>，不管它有几行，编号垂直居中。想给每一行单独编号，就得把每一行写成独立的公式。第二，最后一行：我给 <code>g(x)</code> 指定了编号格式 <code>"(A)"</code>，渲染出来是 <strong>(C)</strong>，不是 (A)。原因是计数器只数“有编号的”公式：前两个是 1、2，中间 <code>numbering: none</code> 的 <code>f(x)</code> 不计，<code>g(x)</code> 是第 3 个，字母编号 <code>"A"</code> 的第 3 个就是 C。这个细节下面讲编号时会用到。</p></section>
<section><h2>运算符：<code>op</code>、<code>limits</code> 和 <code>scripts</code><a href="#运算符oplimits-和-scripts"><span>#</span></a></h2><p><code>lab/ch08/operators.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Built-in operators, `op`, and limits vs scripts placement.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>$</span><span> </span><span>sin</span><span> x + </span><span>log</span><span> n + </span><span>lim</span><span>_</span><span>(</span><span>n -&gt; </span><span>oo</span><span>)</span><span> a_n + </span><span>max</span><span>_i x_i </span><span>$</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// xuan adds rank, tr, diag, sgn; argmin/argmax put the subscript below.</span></div></div><div><div><div>9</div></div><div><span>$</span><span> </span><span>rank</span><span>(A) = </span><span>tr</span><span>(P), </span><span>quad</span><span> </span><span>diag</span><span>(1, 2, 3), </span><span>quad</span><span> </span><span>sgn</span><span>(x) </span><span>$</span></div></div><div><div><div>10</div></div><div><span>$</span><span> </span><span>hat</span><span>(x) = </span><span>argmin</span><span>_</span><span>(</span><span>x </span><span>in</span><span> </span><span>RR</span><span>^n</span><span>)</span><span> </span><span>norm</span><span>(A x - b)^2 </span><span>$</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// op makes new operators; limits controls where scripts go.</span></div></div><div><div><div>13</div></div><div><span>#let</span><span> </span><span>Var</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"Var"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>#let</span><span> </span><span>esssup</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"ess sup"</span><span>, </span><span>limits</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>15</div></div><div><span>$</span><span> </span><span>Var</span><span>(X) = </span><span>EE</span><span>[X^2] - </span><span>EE</span><span>[X]^2, </span><span>quad</span><span> </span><span>esssup</span><span>_</span><span>(</span><span>x </span><span>in</span><span> X</span><span>)</span><span> f(x) </span><span>$</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>// scripts/limits force a placement for one expression.</span></div></div><div><div><div>18</div></div><div><span>$</span><span> </span><span>scripts</span><span>(</span><span>sum</span><span>)_</span><span>(</span><span>i=1</span><span>)</span><span>^n a_i </span><span>quad</span><span> </span><span>limits</span><span>(</span><span>integral</span><span>)_0^1 f </span><span>$</span></div></div><div><div><div>19</div></div><div><span>$</span><span> </span><span>integral</span><span>_0^1 f(x) </span><span>dif</span><span> x </span><span>quad</span><span> </span><span>sum</span><span>_</span><span>(</span><span>i=1</span><span>)</span><span>^n </span><span>quad</span><span> </span><span>product</span><span>_</span><span>(</span><span>i=1</span><span>)</span><span>^n </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-operators.png" alt="operators.typ 的渲染：sin、log、lim 与 max 带下限；rank、tr、diag、sgn；argmin 下方带条件；自定义的 Var 与 ess sup；scripts 把求和号的上下限改成角标，limits 把积分号的上下限改成上下；最后一行积分上下限是角标，求和与乘积的上下限在上下" /><figcaption>operators.typ 的渲染：sin、log、lim 与 max 带下限；rank、tr、diag、sgn；argmin 下方带条件；自定义的 Var 与 ess sup；scripts 把求和号的上下限改成角标，limits 把积分号的上下限改成上下；最后一行积分上下限是角标，求和与乘积的上下限在上下</figcaption></figure><p></p><p>内置运算符（<code>sin</code>、<code>log</code>、<code>lim</code>、<code>max</code>、<code>dim</code>、<code>det</code> 等）是正体，和后面的变量之间有细空隙，<code>lim</code>、<code>max</code> 的下标默认放在<strong>正下方</strong>（limits）。<code>sum</code>、<code>product</code>、<code>integral</code> 这类“大算符”在块公式里：求和、乘积默认 limits，积分默认角标（scripts）；<code>inline-vs-block.typ</code> 的图里可以看到，在行内公式里求和的上下限退成了角标。想破例，就用 <code>scripts(...)</code> 或 <code>limits(...)</code> 包一下，上面倒数第二行就是把 <code>sum</code> 强行改成角标、把 <code>integral</code> 强行改成上下限。</p><p>内置没有的运算符自己造：<code>math.op("Var")</code> 得到一个正体的 <code>Var</code>，<code>math.op("ess sup", limits: true)</code> 得到一个下标放在下面的运算符，名字里的空格会保留。<code>xuan</code> 的 <code>math.typ</code> 开头六行就是这样做的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>rank</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"rank"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>tr</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"tr"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>diag</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"diag"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>sgn</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"sgn"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>argmin</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"argmin"</span><span>, </span><span>limits</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>argmax</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"argmax"</span><span>, </span><span>limits</span><span>: </span><span>true</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>argmin</code> 和 <code>argmax</code> 需要 <code>limits: true</code>，因为 <span><span>x^=arg⁡min⁡x\hat{x} = \arg\min_x</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>ar<span>g</span></span><span></span><span><span>min</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的下标放在旁边就太丑了，图里第三行是下标放在 <code>argmin</code> 正下方的样子。这些名字被 <code>lib.typ</code> 导出，<code>#import ...: *</code> 之后既能在公式里用，也进了你的代码作用域：你的代码里如果有一个变量也叫 <code>tr</code> 或 <code>rank</code>，要么改名，要么换成 <code>import ... as xuan</code> 的写法（第 12 篇讲）。</p><p>微分号 <code>dif</code> 在上面最后一行出现：<code>integral_0^1 f(x) dif x</code> 得到正体的 <span><span>dx\mathrm{d}x</span><span><span><span></span><span>d</span><span>x</span></span></span></span>，不用像手写那样记住 <code>\,\mathrm{d}</code>。</p></section>
<section><h2>变体、装饰、class 和代码<a href="#变体装饰class-和代码"><span>#</span></a></h2><p><code>lab/ch08/math-functions.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Variants, decorations and code inside math.</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>$</span></div></div><div><div><div>7</div></div><div><span>  </span><span>bold</span><span>(A) </span><span>quad</span><span> </span><span>upright</span><span>(d) </span><span>quad</span><span> </span><span>cal</span><span>(L) </span><span>quad</span><span> </span><span>bb</span><span>(R) </span><span>quad</span><span> </span><span>frak</span><span>(g) </span><span>quad</span><span> </span><span>sans</span><span>(T)</span></div></div><div><div><div>8</div></div><div><span>  </span><span>quad</span><span> </span><span>mono</span><span>(x) </span><span>quad</span><span> </span><span>italic</span><span>(x)</span></div></div><div><div><div>9</div></div><div><span>$</span></div></div><div><div><div>10</div></div><div><span>$</span></div></div><div><div><div>11</div></div><div><span>  </span><span>cancel</span><span>(x) / </span><span>cancel</span><span>(x) y, </span><span>quad</span></div></div><div><div><div>12</div></div><div><span>  </span><span>underbrace</span><span>(1 + 2 + 3, </span><span>"六"</span><span>) </span><span>quad</span><span> </span><span>overbrace</span><span>(a + b, </span><span>"和"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>$</span></div></div><div><div><div>14</div></div><div><span>$</span><span> x' </span><span>quad</span><span> x'' </span><span>quad</span><span> f'(x) </span><span>quad</span><span> x^</span><span>(</span><span>(n)</span><span>)</span><span> </span><span>quad</span><span> x_i^2 </span><span>quad</span><span> </span><span>sum</span><span>_</span><span>(</span><span>i=1</span><span>)</span><span>^n x_i^2 </span><span>$</span></div></div><div><div><div>15</div></div><div><span>$</span><span> </span><span>root</span><span>(3, x) </span><span>quad</span><span> </span><span>sqrt</span><span>(x^2 + y^2) </span><span>quad</span><span> </span><span>frac</span><span>(a, b) </span><span>quad</span><span> a </span><span>slash</span><span> b </span><span>$</span></div></div><div><div><div>16</div></div><div><span>$</span><span> </span><span>class</span><span>(</span><span>"relation"</span><span>, ~) </span><span>quad</span><span> a </span><span>class</span><span>(</span><span>"binary"</span><span>, </span><span>star</span><span>) b </span><span>$</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>// Code in math: `#` embeds, and loops produce math.</span></div></div><div><div><div>19</div></div><div><span>#let</span><span> </span><span>terms</span><span><span> </span><span>= </span></span><span>range</span><span>(</span><span>1</span><span>, </span><span>5</span><span>).</span><span>map</span><span>(</span><span>k</span><span> </span><span>=&gt;</span><span> </span><span>$</span><span>x_</span><span>#k</span><span>$</span><span>)</span></div></div><div><div><div>20</div></div><div><span>$</span><span> </span><span>#terms</span><span>.</span><span>join</span><span>(</span><span>$</span><span>+</span><span>$</span><span>) = s </span><span>$</span></div></div><div><div><div>21</div></div><div><span>$</span><span> </span><span>#for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>4</span><span>) [</span><span>$</span><span>#k</span><span>^</span><span>2</span><span>$</span><span> ] </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-math-functions.png" alt="math-functions.typ 的渲染：粗斜体 A、正体 d、花体 L、黑板粗体 R、哥特体 g、无衬线 T、等宽 x 与斜体 x；约分划线、六与和的括号标注；撇号与上下标；根号与分式；class 的两个例子；用代码拼出的 x1 加 x2 加 x3 加 x4 等于 s；最后一行是 1 的平方、2 的平方、3 的平方，没有空隙" /><figcaption>math-functions.typ 的渲染：粗斜体 A、正体 d、花体 L、黑板粗体 R、哥特体 g、无衬线 T、等宽 x 与斜体 x；约分划线、六与和的括号标注；撇号与上下标；根号与分式；class 的两个例子；用代码拼出的 x1 加 x2 加 x3 加 x4 等于 s；最后一行是 1 的平方、2 的平方、3 的平方，没有空隙</figcaption></figure><p></p><p><strong>变体函数。</strong> <code>bold</code>、<code>upright</code>、<code>cal</code>、<code>bb</code>、<code>frak</code>、<code>sans</code>、<code>mono</code>、<code>italic</code> 就是对应的字体变体。有一件事会咬到从别处来的人：<code>bold(x)</code> 得到的是<strong>粗斜体</strong>，因为数学里的字母默认斜体，<code>bold</code> 只加粗不改斜。想要“粗正体”的向量（很多教材用这个），要写成 <code>bold(upright(x))</code>，两层的顺序可以互换（<code>tricks.typ</code> 里的第三个和第二个渲染相同）。我在最后的 <code>la.typ</code> 里为此写了一行 <code>#let vb(x) = $bold(upright(#x))$</code>，后面会看到。</p><p><strong>装饰。</strong> 我另写了一个 <code>/tmp/p8/decorations.typ</code> 看参数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>numbering</span><span>: </span><span>none</span><span>)</span></div></div><div><div><div>5</div></div><div><span>// class changes spacing, not the glyph.</span></div></div><div><div><div>6</div></div><div><span>$</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>a x b </span><span>quad</span><span> a </span><span>class</span><span>(</span><span>"relation"</span><span>, x) b </span><span>quad</span><span> a </span><span>class</span><span>(</span><span>"binary"</span><span>, x) b </span><span>quad</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>a </span><span>class</span><span>(</span><span>"punctuation"</span><span>, x) b</span></div></div><div><div><div>9</div></div><div><span>$</span></div></div><div><div><div>10</div></div><div><span>$</span></div></div><div><div><div>11</div></div><div><span>  </span><span>cancel</span><span>(x + y) </span><span>quad</span><span> </span><span>cancel</span><span>(x, </span><span>angle</span><span>: </span><span>#30deg</span><span>) </span><span>quad</span></div></div><div><div><div>12</div></div><div><span>  </span><span>cancel</span><span>(x, </span><span>cross</span><span>: </span><span>#true</span><span>) </span><span>quad</span><span> </span><span>cancel</span><span>(x, </span><span>inverted</span><span>: </span><span>#true</span><span>)</span></div></div><div><div><div>13</div></div><div><span>$</span></div></div><div><div><div>14</div></div><div><span>$</span></div></div><div><div><div>15</div></div><div><span>  </span><span>overline</span><span>(x + y) </span><span>quad</span><span> </span><span>underline</span><span>(x + y) </span><span>quad</span><span> </span><span>overbracket</span><span>(x + y) </span><span>quad</span></div></div><div><div><div>16</div></div><div><span>  </span><span>underbracket</span><span>(x + y, </span><span>"n"</span><span>) </span><span>quad</span><span> </span><span>tilde</span><span>(x) </span><span>quad</span><span> </span><span>hat</span><span>(x y)</span></div></div><div><div><div>17</div></div><div><span>$</span></div></div><div><div><div>18</div></div><div><span>$</span><span> </span><span>underbrace</span><span>(a + b + c, 3 </span><span>"项"</span><span>) </span><span>quad</span><span> </span><span>overbrace</span><span>(a + b + c, </span><span>"和"</span><span> = s) </span><span>$</span></div></div><div><div><div>19</div></div><div><span>$</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>x^2 </span><span>quad</span><span> x_2 </span><span>quad</span><span> </span><span>scripts</span><span>(</span><span>sum</span><span>)_</span><span>(</span><span>i=1</span><span>)</span><span>^n </span><span>quad</span><span> </span><span>limits</span><span>(</span><span>integral</span><span>)_0^1 </span><span>quad</span></div></div><div><div><div>21</div></div><div><span>  </span><span>lim</span><span>_</span><span>(</span><span>n-&gt;</span><span>oo</span><span>)</span><span> </span><span>quad</span><span> </span><span>limits</span><span>(</span><span>lim</span><span>)_</span><span>(</span><span>n-&gt;</span><span>oo</span><span>)</span><span> </span><span>quad</span><span> </span><span>scripts</span><span>(</span><span>lim</span><span>)_</span><span>(</span><span>n-&gt;</span><span>oo</span><span>)</span></div></div><div><div><div>22</div></div><div><span>$</span></div></div><div><div><div>23</div></div><div><span>$</span><span> </span><span>"d"</span><span>/</span><span>(</span><span>"d"</span><span>x</span><span>)</span><span> f(x) = </span><span>lim</span><span>_</span><span>(</span><span>h -&gt; 0</span><span>)</span><span> (f(x + h) - f(x)) / h </span><span>$</span></div></div><div><div><div>24</div></div><div><span>$</span></div></div><div><div><div>25</div></div><div><span>  </span><span>vec</span><span>(1, 2) </span><span>times</span><span> </span><span>vec</span><span>(3, 4) </span><span>quad</span><span> </span><span>bold</span><span>(u) </span><span>dot</span><span> </span><span>bold</span><span>(v) </span><span>quad</span></div></div><div><div><div>26</div></div><div><span>  </span><span>chevron</span><span>.</span><span>l</span><span> u, v </span><span>chevron</span><span>.</span><span>r</span></div></div><div><div><div>27</div></div><div><span>$</span></div></div><div><div><div>28</div></div><div><span>$</span><span> </span><span>text</span><span>(</span><span><span>#</span><span>red</span></span><span>, x^2) </span><span>quad</span><span> </span><span>#text</span><span>(</span><span>red</span><span>)[</span><span>$</span><span>x^2</span><span>$</span><span>] </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-decorations.png" alt="decorations.typ 的渲染：class 改变前后空隙；cancel 的斜线、自定义角度、交叉与反向；上划线、下划线、括号标注与帽子；带标注的下括号和上括号；scripts 与 limits 的各种放置；导数的定义；向量叉乘与内积；红色的 x 平方" /><figcaption>decorations.typ 的渲染：class 改变前后空隙；cancel 的斜线、自定义角度、交叉与反向；上划线、下划线、括号标注与帽子；带标注的下括号和上括号；scripts 与 limits 的各种放置；导数的定义；向量叉乘与内积；红色的 x 平方</figcaption></figure><p></p><ul>
<li><code>cancel(x, angle: #30deg)</code>、<code>cross: #true</code>、<code>inverted: #true</code> 是约分划线的三个开关，图里第二排依次是它们。</li>
<li><code>underbrace(a, b)</code> 和 <code>overbrace(a, b)</code> 的第二个参数是标注，可以是文本，也可以是表达式（<code>"和" = s</code>）。</li>
<li><code>overline</code>、<code>underline</code>、<code>overbracket</code>、<code>underbracket</code>、<code>tilde</code>、<code>hat</code> 各自加一种装饰。</li>
<li><code>text(#red, ...)</code> 和 <code>#text(red)[$...$]</code> 两种写法都能给公式上色，前者把 <code>#red</code> 当作参数写在数学里。</li>
</ul><p><strong><code>class</code>：间距属于类别，不属于字形。</strong> 数学排版里每个原子有一个类别（普通、二元运算、关系、标点等），间距由类别决定。第一排我用同一个 <code>x</code> 做了四次实验：不写 <code>class</code> 的 <code>a x b</code> 紧挨着（普通），<code>class("relation", x)</code> 和 <code>class("binary", x)</code> 两侧都空开，<code>class("punctuation", x)</code> 在这个位置看起来和普通的没有区别。这是你自己造一个“看起来像运算符”的符号时需要的开关。</p><p><strong>代码在数学里。</strong> 我另写了 <code>/tmp/p8/code-in-math.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>numbering</span><span>: </span><span>none</span><span>)</span></div></div><div><div><div>5</div></div><div><span>// `#` embeds a code expression, and values can be math.</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>terms</span><span><span> </span><span>= </span></span><span>range</span><span>(</span><span>1</span><span>, </span><span>5</span><span>).</span><span>map</span><span>(</span><span>k</span><span> </span><span>=&gt;</span><span> </span><span>$</span><span>x_</span><span>#k</span><span>$</span><span>)</span></div></div><div><div><div>7</div></div><div><span>$</span><span> </span><span>#terms</span><span>.</span><span>join</span><span>(</span><span>$</span><span>+</span><span>$</span><span>) = s </span><span>$</span></div></div><div><div><div>8</div></div><div><span>$</span><span> </span><span>#</span><span>range</span><span>(</span><span>1</span><span>, </span><span>4</span><span>).</span><span>map</span><span>(</span><span>k</span><span> </span><span>=&gt;</span><span> </span><span>$</span><span>#k</span><span>^</span><span>2</span><span>$</span><span>).</span><span>join</span><span>(</span><span>$</span><span>,</span><span>$</span><span>) </span><span>$</span></div></div><div><div><div>9</div></div><div><span>$</span><span> </span><span>#for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>4</span><span>) [</span><span>$</span><span>#k</span><span>^</span><span>2</span><span>$</span><span> ] </span><span>$</span></div></div><div><div><div>10</div></div><div><span>$</span></div></div><div><div><div>11</div></div><div><span>  </span><span>#</span><span>{</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>3</span></div></div><div><div><div>13</div></div><div><span>    </span><span>$</span><span>x^</span><span>#n</span><span>$</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>} </span><span>quad</span><span> x^</span><span>#</span><span>(</span><span>2</span><span><span> </span><span>+</span><span> </span></span><span>3</span><span>) </span><span>quad</span><span> </span><span>#</span><span>(</span><span>2</span><span><span> </span><span>+</span><span> </span></span><span>3</span><span>)^x</span></div></div><div><div><div>15</div></div><div><span>$</span></div></div><div><div><div>16</div></div><div><span>// display and inline force a style for one expression.</span></div></div><div><div><div>17</div></div><div><span>$</span></div></div><div><div><div>18</div></div><div><span>  </span><span>sum</span><span>_</span><span>(</span><span>k=1</span><span>)</span><span>^n k = (n(n + 1)) / 2 </span><span>quad</span></div></div><div><div><div>19</div></div><div><span>  </span><span>display</span><span>(</span><span>sum</span><span>_</span><span>(</span><span>k=1</span><span>)</span><span>^n k) </span><span>quad</span><span> </span><span>inline</span><span>(</span><span>sum</span><span>_</span><span>(</span><span>k=1</span><span>)</span><span>^n k)</span></div></div><div><div><div>20</div></div><div><span>$</span></div></div><div><div><div>21</div></div><div><span>// Text and colour inside math.</span></div></div><div><div><div>22</div></div><div><span>$</span><span> f(x) := </span><span>cases</span><span>(x^2 &amp; </span><span>"if"</span><span> x &gt;= 0, -x &amp; </span><span>"otherwise"</span><span>) </span><span>$</span></div></div><div><div><div>23</div></div><div><span>$</span><span> </span><span>text</span><span>(</span><span><span>#</span><span>red</span></span><span>, x^2) </span><span>quad</span><span> </span><span>#text</span><span>(</span><span>red</span><span>)[</span><span>$</span><span>x^2</span><span>$</span><span>] </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-code-in-math.png" alt="code-in-math.typ 的渲染：x1 加到 x4 等于 s；1 的平方、2 的平方、3 的平方，逗号分隔；同样三项连写没有逗号和空隙；x 的 3 次、5 次与 5 的 x 次；块与行内两种求和；分段函数；红色的 x 平方" /><figcaption>code-in-math.typ 的渲染：x1 加到 x4 等于 s；1 的平方、2 的平方、3 的平方，逗号分隔；同样三项连写没有逗号和空隙；x 的 3 次、5 次与 5 的 x 次；块与行内两种求和；分段函数；红色的 x 平方</figcaption></figure><p></p><p><code>#terms.join($+$)</code> 是我最喜欢的一类写法：数组里放的是数学内容，<code>join</code> 的分隔符也是数学内容，拼出 <span><span>x1+x2+x3+x4x_1 + x_2 + x_3 + x_4</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。第三行是个陷阱，也是 <code>math-functions.typ</code> 最后一行的写法：<code>#for ... [$#k^2$ ]</code> 循环体里那个空格在公式里<strong>没有保留</strong>，三个平方连成 <code>1²2²3²</code>；第二行用 <code>map</code> 加 <code>join($,$)</code> 就能拿到正确的分隔。我没有深挖空格被吃掉的原因，实验给出的结论是：要分隔符，用 <code>join</code>，不要指望循环体里的空白。</p><p>另一个提醒：<code>x^#(2 + 3)</code> 得到 <span><span>x5x^5</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span></span></span></span></span></span></span></span>，<code>#(2 + 3)^x</code> 得到 <span><span>5x5^x</span><span><span><span></span><span><span>5</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span></span></span></span></span></span></span></span>，<code>#</code> 后面的括号表达式整体作为一个值被插入，值本身不会再被拆开。</p></section>
<section><h2>公式编号：为什么我自己写了一个 <code>math.typ</code><a href="#公式编号为什么我自己写了一个-mathtyp"><span>#</span></a></h2><p>前面所有渲染图里的公式都带着编号，<code>(0.1)</code>、<code>(0.2)</code>，说明 <code>xuan-base</code> 已经替我们开了公式编号。Typst 自带的 <code>math.equation</code> 编号是全文连续的 <code>(1)</code>、<code>(2)</code>……笔记和教材要的是<strong>按章编号</strong>：第 2 章的第 1 个公式是 <code>(2.1)</code>，引用时也显示 <code>(2.1)</code>。<code>xuan/src/math.typ</code> 就是做这件事的，全文 41 行，一次性贴出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Math operators and chapter-wise equation numbering.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>rank</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"rank"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>tr</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"tr"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>diag</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"diag"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>sgn</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"sgn"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>argmin</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"argmin"</span><span>, </span><span>limits</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>argmax</span><span><span> </span><span>= </span></span><span>math</span><span>.</span><span>op</span><span>(</span><span>"argmax"</span><span>, </span><span>limits</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>/// "(&lt;chapter&gt;.&lt;n&gt;)" for the equation `el`, evaluated at its own location.</span></div></div><div><div><div>11</div></div><div><span>#let</span><span> </span><span>equation-number</span><span>(</span><span>el</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>12</div></div><div><span>  </span><span>let</span><span> </span><span>loc</span><span><span> </span><span>= </span></span><span>el</span><span>.</span><span>location</span><span>()</span></div></div><div><div><div>13</div></div><div><span>  </span><span>numbering</span><span>(</span></div></div><div><div><div>14</div></div><div><span>    </span><span>"(1.1)"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>16</div></div><div><span>    </span><span>counter</span><span>(</span><span>math</span><span>.</span><span>equation</span><span>).</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>/// Show rules: number equations per chapter and make `@label` agree.</span></div></div><div><div><div>21</div></div><div><span>#let</span><span> </span><span>math-rules</span><span>(</span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>22</div></div><div><span>  </span><span>set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>numbering</span><span>: </span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>context</span><span> </span><span>numbering</span><span>(</span></div></div><div><div><div>23</div></div><div><span>    </span><span>"(1.1)"</span><span>,</span></div></div><div><div><div>24</div></div><div><span>    </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>().</span><span>first</span><span>(),</span></div></div><div><div><div>25</div></div><div><span>    </span><span>n</span><span>,</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>))</span></div></div><div><div><div>27</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>28</div></div><div><span>    </span><span>counter</span><span>(</span><span>math</span><span>.</span><span>equation</span><span>).</span><span>update</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>it</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>  </span><span>show</span><span> </span><span>ref</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>let</span><span> </span><span>el</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>element</span></div></div><div><div><div>33</div></div><div><span>    </span><span>if</span><span> </span><span>el</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>func</span><span><span>() </span><span>==</span><span> </span></span><span>math</span><span>.</span><span>equation</span><span> {</span></div></div><div><div><div>34</div></div><div><span>      </span><span>let</span><span> </span><span>supp</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>it</span><span>.</span><span>supplement</span><span><span> </span><span>==</span><span> </span></span><span>auto</span><span> { </span><span>el</span><span>.</span><span>supplement</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>35</div></div><div><span>        </span><span>it</span><span>.</span><span>supplement</span></div></div><div><div><div>36</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span>      </span><span>link</span><span>(</span><span>el</span><span>.</span><span>location</span><span>(), [</span><span>#supp</span><span>~</span><span>#equation-number</span><span>(</span><span>el</span><span>)])</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> { </span><span>it</span><span> }</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>  </span><span>body</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它在 <code>xuan</code> 里的接线只有两处。<code>lib.typ</code> 新增一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"src/math.typ"</span><span>: </span><span>argmax</span><span>, </span><span>argmin</span><span>, </span><span>diag</span><span>, </span><span>rank</span><span>, </span><span>sgn</span><span>, </span><span>tr</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>src/base.typ</code>（中间版本）新增两行，一行 <code>import</code>，一行放在 <code>show: theorem-rules</code> 后面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"math.typ"</span><span>: </span><span>math-rules</span></div></div><div><div><div>2</div></div><div><span>// ...</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>show: theorem-rules</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>show: math-rules</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这段代码分三块读。后面 <code>/tmp/p8/</code> 里的实验文件都用 <code>typst compile --ignore-system-fonts x.typ x.pdf</code> 编译，再用 <code>pdftotext</code> 读文字；<code>@local/xuan:0.1.0</code> 靠 <code>nix develop</code> 设好的 <code>TYPST_PACKAGE_PATH</code> 找到。</p><p><strong>1. <code>set math.equation(numbering: ...)</code>：编号是个函数。</strong> <code>numbering</code> 既可以是 <code>"(1.1)"</code> 这样的格式串，也可以是函数。内置的公式计数器 <code>counter(math.equation)</code> 只数公式，不知道章。所以我传一个函数：收到公式序号 <code>n</code>，再自己去读 <code>counter(heading).get().first()</code> 当章号，拼成 <code>numbering("(1.1)", 章, n)</code>。</p><p><strong>2. <code>show heading.where(level: 1)</code>：每章把公式计数器归零。</strong> 这条规则和第 7 篇定理环境里那条一模一样。没有它会怎样？我写了一个只保留“按章拼号”、去掉归零的 <code>/tmp/p8/no-reset.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>numbering</span><span>: </span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>numbering</span><span>(</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"(1.1)"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>().</span><span>first</span><span>(),</span></div></div><div><div><div>5</div></div><div><span>  </span><span>n</span><span>,</span></div></div><div><div><div>6</div></div><div><span>))</span></div></div><div><div><div>7</div></div><div><span>=</span><span> 一</span></div></div><div><div><div>8</div></div><div><span>$</span><span> x = 1 </span><span>$</span></div></div><div><div><div>9</div></div><div><span>=</span><span> 二</span></div></div><div><div><div>10</div></div><div><span>$</span><span> y = 1 </span><span>$</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext no-reset.pdf - | grep "("</span></div></div><div><div><div>2</div></div><div><span>(1.1)</span></div></div><div><div><div>3</div></div><div><span>(2.2)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二章的第一个公式叫 <code>(2.2)</code>，因为公式计数器没归零，它是全文第 2 个。</p><p><strong>3. <code>show ref</code>：这才是我折腾最久的地方。</strong> 现在公式编号对了，<code>@label</code> 引用呢？我做了两个只差一个词的对照实验。<code>ref-context.typ</code> 的编号函数带 <code>context</code>（和 <code>xuan</code> 现在的写法一样），<code>ref-plain.typ</code> 不带，其余完全相同，而且<strong>都没有 <code>show ref</code></strong>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>numbering</span><span>: </span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>context</span><span> </span><span>numbering</span><span>(</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"(1.1)"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>().</span><span>first</span><span>(),</span></div></div><div><div><div>4</div></div><div><span>  </span><span>n</span><span>,</span></div></div><div><div><div>5</div></div><div><span>))</span></div></div><div><div><div>6</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>counter</span><span>(</span><span>math</span><span>.</span><span>equation</span><span>).</span><span>update</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>9</div></div><div><span>  </span><span>it</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div><span>=</span><span> 一</span></div></div><div><div><div>12</div></div><div><span>$</span><span> x = 1 </span><span>$</span><span> </span><span>&lt;a&gt;</span></div></div><div><div><div>13</div></div><div><span>$</span><span> x = 2 </span><span>$</span><span> </span><span>&lt;a2&gt;</span></div></div><div><div><div>14</div></div><div><span>=</span><span> 二</span></div></div><div><div><div>15</div></div><div><span>$</span><span> y = 1 </span><span>$</span><span> </span><span>&lt;b&gt;</span></div></div><div><div><div>16</div></div><div><span>引用: </span><span>@a</span><span> </span><span>@a2</span><span> </span><span>@b</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>ref-plain.typ</code> 就是把第一行的 <code>context</code> 去掉。编译后各取正文里的编号和引用行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext ref-context.pdf - | grep -E "引用|\("</span></div></div><div><div><div>2</div></div><div><span>(1.1)</span></div></div><div><div><div>3</div></div><div><span>(1.2)</span></div></div><div><div><div>4</div></div><div><span>(2.1)</span></div></div><div><div><div>5</div></div><div><span>引用: Equation (2.1) Equation (2.2) Equation (2.1)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>$ pdftotext ref-plain.pdf - | grep -E "引用|\("</span></div></div><div><div><div>8</div></div><div><span>(1.1)</span></div></div><div><div><div>9</div></div><div><span>(1.2)</span></div></div><div><div><div>10</div></div><div><span>(2.1)</span></div></div><div><div><div>11</div></div><div><span>引用: Equation (1.1) Equation (1.2) Equation (2.1)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>公式本身的编号两边一样，都对。差别在引用：带 <code>context</code> 的版本，<code>@a</code>（第 1 章第 1 个公式）在第 2 章里引用时显示 <code>(2.1)</code>，章号成了<strong>引用者所在的章</strong>；不带 <code>context</code> 的版本则一切正常。</p><p>我对这个现象的解释是（这一句是推断，我没有读编译器源码）：引用要把“公式的编号”重新显示一遍，函数形式的 <code>numbering</code> 会在被引用公式的位置上被调用，所以读到的章号是公式所在章。但函数里的 <code>context</code> 把求值推迟成了“一块带上下文的内容”，这块内容最后摆在引用的位置，<code>counter(heading).get()</code> 就在引用处求值了。</p><p>这就是编号发票的问题：发票号在开票那一刻就定了，之后谁来查，都应该看到同一个号，而不是按查询人所在的柜台重新编一遍。<code>xuan</code> 的做法是 <code>show ref</code> 里不再依赖“显示时求值”，而是直接取被引用元素的位置：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let el = it.element</span></div></div><div><div><div>2</div></div><div><span>// ...</span></div></div><div><div><div>3</div></div><div><span>link(el.location(), [</span><span>#supp</span><span>~</span><span>#equation-number</span><span>(</span><span>el</span><span>)])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>equation-number(el)</code> 里 <code>let loc = el.location()</code>，然后用 <code>counter(heading).at(loc)</code> 和 <code>counter(math.equation).at(loc)</code> 算编号，答案只由公式自己的位置决定，不管谁调用。</p><p>上面的对照实验也说明，<code>xuan</code> 的编号函数里那个 <code>context</code> 对默认的 <code>@label</code> 是多余的：去掉它，默认引用就能给出正确的章号。<code>show ref</code> 仍然有用：</p><ul>
<li>默认引用不是蓝色链接。我在 <code>ref-plain.typ</code> 前面加了一行 <code>#show link: set text(fill: red)</code> 再编译成 PNG（<code>/tmp/p8/v4.typ</code>），三个引用仍是黑字，规则 <code>show ref</code> 里显式调用的 <code>link(...)</code> 才把 <code>xuan-base</code> 里的链接颜色带进来。</li>
<li><code>xuan</code> 在 supplement 和编号之间放了 <code>~</code>（不换行空格），保证“式”和 <code>(2.1)</code> 不会在行尾断开。这是代码里写的行为，我没有单独去量默认引用用的是哪种空格。</li>
<li>它能和第 7 篇的定理引用共存：两条 <code>show ref</code> 规则互不匹配时各自 <code>else { it }</code> 放行。我编译了 <code>/tmp/p8/both-refs.typ</code>（一个定理、一个引理、各夹一个公式，章号 1 和 2）：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 一</span></div></div><div><div><div>4</div></div><div><span>#theorem</span><span>[</span><span>$</span><span>a = b</span><span>$</span><span>.] </span><span>&lt;t1&gt;</span></div></div><div><div><div>5</div></div><div><span>$</span><span> x = 1 </span><span>$</span><span> </span><span>&lt;e1&gt;</span></div></div><div><div><div>6</div></div><div><span>=</span><span> 二</span></div></div><div><div><div>7</div></div><div><span>#lemma</span><span>[</span><span>$</span><span>c</span><span>$</span><span>.] </span><span>&lt;l1&gt;</span></div></div><div><div><div>8</div></div><div><span>$</span><span> y = 2 </span><span>$</span><span> </span><span>&lt;e2&gt;</span></div></div><div><div><div>9</div></div><div><span>见 </span><span>@t1</span><span>、</span><span>@e1</span><span>、</span><span>@l1</span><span>、</span><span>@e2</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext both-refs.pdf - | grep 见</span></div></div><div><div><div>2</div></div><div><span>见 定理 1.1、式 (1.1)、引理 2.1、式 (2.1)。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个引用的章号和序号都对。</p><p><strong>supplement：“式”从哪里来。</strong> <code>lab/ch08/numbering.typ</code> 把上面这些放在一起：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Equation numbers per chapter, labels, and supplements (set by xuan-base).</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>=</span><span> 第一章</span></div></div><div><div><div>7</div></div><div><span>$</span><span> A x = b </span><span>$</span><span> </span><span>&lt;eq-ls&gt;</span></div></div><div><div><div>8</div></div><div><span>$</span><span> A^T A x = A^T b </span><span>$</span><span> </span><span>&lt;eq-normal&gt;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>=</span><span> 第二章</span></div></div><div><div><div>11</div></div><div><span>$</span><span> x^+ = (A^T A)^</span><span>(</span><span>-1</span><span>)</span><span> A^T b </span><span>$</span><span> </span><span>&lt;eq-pinv&gt;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>由 </span><span>@eq-ls</span><span> 得 </span><span>@eq-normal</span><span>，再得 </span><span>@eq-pinv</span><span>。</span></div></div><div><div><div>14</div></div><div><span>第二章的编号从 1 重新开始，且引用显示的是公式所在的章，</span></div></div><div><div><div>15</div></div><div><span>而不是引用所在的章。</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>#ref</span><span>(</span><span>&lt;eq-normal&gt;</span><span>, </span><span>supplement</span><span>: [式])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-numbering.png" alt="numbering.typ 的渲染：第 1 章下有 (1.1) 与 (1.2)，第 2 章下有 (2.1)；正文里的三个引用显示为蓝色的“式 (1.1)”“式 (1.2)”“式 (2.1)”，最后一行是显式 supplement 的引用“式 (1.2)”" /><figcaption>numbering.typ 的渲染：第 1 章下有 (1.1) 与 (1.2)，第 2 章下有 (2.1)；正文里的三个引用显示为蓝色的“式 (1.1)”“式 (1.2)”“式 (2.1)”，最后一行是显式 supplement 的引用“式 (1.2)”</figcaption></figure><p></p><p><code>@eq-ls</code> 得到 <code>式 (1.1)</code>：补语“式”是 Typst 按文档语言给 <code>math.equation</code> 配的默认 supplement（<code>xuan-base</code> 把 <code>text.lang</code> 设成 <code>"zh"</code>；英文是 <code>Equation</code>，我在下面实验里看到了）。我又做了两个实验，<code>supplement-zh.typ</code> 和 <code>supplement-en.typ</code>，读 <code>pdftotext</code> 输出：</p><p><code>supplement-zh.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 一</span></div></div><div><div><div>4</div></div><div><span>$</span><span> x = 1 </span><span>$</span><span> </span><span>&lt;a&gt;</span></div></div><div><div><div>5</div></div><div><span>见 </span><span>@a</span><span>，</span><span>#ref</span><span>(</span><span>&lt;a&gt;</span><span>, </span><span>supplement</span><span>: [公式])，</span><span>#ref</span><span>(</span><span>&lt;a&gt;</span><span>, </span><span>supplement</span><span>: </span><span>none</span><span>)。</span></div></div><div><div><div>6</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>supplement</span><span>: [公式])</span></div></div><div><div><div>7</div></div><div><span>$</span><span> y = 2 </span><span>$</span><span> </span><span>&lt;b&gt;</span></div></div><div><div><div>8</div></div><div><span>再见 </span><span>@b</span><span> 与 </span><span>@a</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>supplement-en.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>xuan-base</span><span>.</span><span>with</span><span>(</span><span>lang</span><span>: </span><span>"en"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>=</span><span> One</span></div></div><div><div><div>4</div></div><div><span>$</span><span> x = 1 </span><span>$</span><span> </span><span>&lt;a&gt;</span></div></div><div><div><div>5</div></div><div><span>See </span><span>@a</span><span>, </span><span>#ref</span><span>(</span><span>&lt;a&gt;</span><span>, </span><span>supplement</span><span>: [式]), </span><span>#ref</span><span>(</span><span>&lt;a&gt;</span><span>, </span><span>supplement</span><span>: </span><span>none</span><span>),</span></div></div><div><div><div>6</div></div><div><span>#ref</span><span>(</span><span>&lt;a&gt;</span><span>, </span><span>form</span><span>: </span><span>"page"</span><span>).</span></div></div><div><div><div>7</div></div><div><span>#set</span><span> </span><span>math</span><span>.</span><span>equation</span><span>(</span><span>supplement</span><span>: [Eq.])</span></div></div><div><div><div>8</div></div><div><span>$</span><span> y = 2 </span><span>$</span><span> </span><span>&lt;b&gt;</span></div></div><div><div><div>9</div></div><div><span>Then </span><span>@b</span><span> and </span><span>@a</span><span>.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext supplement-zh.pdf - | grep -E "见|再见"</span></div></div><div><div><div>2</div></div><div><span>见 式 (1.1)，公式 (1.1)， (1.1)。</span></div></div><div><div><div>3</div></div><div><span>再见 公式 (1.2) 与 式 (1.1)。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdftotext supplement-en.pdf - | grep -E "See|Then"</span></div></div><div><div><div>2</div></div><div><span>See Equation (1.1), 式 (1.1), (1.1), Equation (1.1).</span></div></div><div><div><div>3</div></div><div><span>Then Eq. (1.2) and Equation (1.1).</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三种改法的行为：<code>#ref(&lt;a&gt;, supplement: [公式])</code> 只改这一次引用；<code>supplement: none</code> 去掉补语；<code>#set math.equation(supplement: [公式])</code> 改的是<strong>之后每个公式元素自己的 supplement</strong>，所以 <code>@b</code> 变成“公式 (1.2)”，而此前定义的 <code>@a</code> 依然是“式 (1.1)”。supplement 是公式的属性，不是文档的全局设置。</p><p>两个我在实验里碰到的毛病，也一并记下：</p><ul>
<li><code>supplement: none</code> 之后输出前面多了一个空格（<code>" (1.1)"</code>），是 <code>[#supp~#equation-number(el)]</code> 里那个 <code>~</code> 还在。</li>
<li>英文那一行的第四个引用 <code>#ref(&lt;a&gt;, form: "page")</code> 本来应该给出页码，输出却还是 <code>Equation (1.1)</code>：<code>xuan</code> 的 <code>show ref</code> 没有处理 <code>form</code>，把它吞掉了。要页码引用就别对公式用这个参数，这是第一版的限制。</li>
</ul><p>还有最后一个，也是前面所有图里一直在出现的：<strong>第一个标题之前的公式编号是 <code>(0.1)</code></strong>，因为 <code>counter(heading).get().first()</code> 在没有标题时读到 0。笔记总是有标题的，所以我没有给它特殊处理，但你在 <code>fig-math.typ</code> 这种没有标题的小文件里就会看到 <code>(0.1)</code>，这是它的来源。关闭编号的办法在对齐那一节已经出现：<code>#set math.equation(numbering: none)</code>。</p></section>
<section><h2>32 行对照表<a href="#32-行对照表"><span>#</span></a></h2><p>我把常用的写法收在 <code>lab/ch08/latex-map.typ</code>，用 xuan 的三线表渲染（表本身的机制是第 9 篇的内容）。这个文件的数据是一个 32 项的数组，每项是 LaTeX 写法、Typst 写法、渲染结果三元组，最后一个 <code>#table(...)</code> 把它铺开。这里只贴数组头三项和铺表的那几行，完整文件在<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch08/latex-map.typ" target="_blank">仓库</a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>map</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>(</span><span>"</span><span>\\</span><span>frac{a}{b}"</span><span>, </span><span>"a/b"</span><span>, </span><span>$</span><span>a/b</span><span>$</span><span>),</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(</span><span>"</span><span>\\</span><span>sqrt{x}"</span><span>, </span><span>"sqrt(x)"</span><span>, </span><span>$</span><span>sqrt</span><span>(x)</span><span>$</span><span>),</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(</span><span>"</span><span>\\</span><span>sqrt[3]{x}"</span><span>, </span><span>"root(3, x)"</span><span>, </span><span>$</span><span>root</span><span>(3, x)</span><span>$</span><span>),</span></div></div><div><div><div>5</div></div><div><span>  </span><span>// ... 29 more rows ...</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div><span>#table</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>columns</span><span>: (</span><span>1fr</span><span>, </span><span>1fr</span><span>, </span><span>auto</span><span>),</span></div></div><div><div><div>9</div></div><div><span>  </span><span>table</span><span>.</span><span>header</span><span>[LaTeX][Typst][渲染],</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>..</span><span>map</span><span>.</span><span>map</span><span>(</span><span>r</span><span> </span><span>=&gt;</span><span> (</span><span>raw</span><span>(</span><span>r</span><span>.</span><span>at</span><span>(</span><span>0</span><span>)), </span><span>raw</span><span>(</span><span>r</span><span>.</span><span>at</span><span>(</span><span>1</span><span>)), </span><span>r</span><span>.</span><span>at</span><span>(</span><span>2</span><span>))).</span><span>flatten</span><span>(),</span></div></div><div><div><div>11</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>渲染出来是一页：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-latex-map.png" alt="32 行三列的对照表：左列 LaTeX 写法，中列 Typst 写法，右列渲染出的公式；三线表样式，表头加粗" /><figcaption>32 行三列的对照表：左列 LaTeX 写法，中列 Typst 写法，右列渲染出的公式；三线表样式，表头加粗</figcaption></figure><p></p><p>下面是同一张表的文字版，方便复制。我先说怎么验证，再给表。</p><p><strong>Typst 列</strong>：每个单元格的字符串是我手写的，和右边渲染出的公式是两份数据，容易写岔。我用 <code>eval(..., mode: "math")</code> 把字符串重新求值，和渲染列比较：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>sc</span><span><span> </span><span>= </span><span>(</span></span><span>rank</span><span>: </span><span>rank</span><span>, </span><span>tr</span><span>: </span><span>tr</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#for</span><span> </span><span>r</span><span> </span><span>in</span><span> </span><span>map</span><span> [</span></div></div><div><div><div>3</div></div><div><span>  </span><span>#let</span><span> </span><span>alts</span><span><span> </span><span>= </span></span><span>r</span><span>.</span><span>at</span><span>(</span><span>1</span><span>).</span><span>split</span><span>(</span><span>" or "</span><span>)</span></div></div><div><div><div>4</div></div><div><span>  </span><span>#let</span><span> </span><span>res</span><span><span> </span><span>= </span></span><span>alts</span><span>.</span><span>map</span><span>(</span><span>a</span><span> </span><span>=&gt;</span><span> </span><span>eval</span><span>(</span><span>a</span><span>, </span><span>mode</span><span>: </span><span>"math"</span><span>, </span><span>scope</span><span>: </span><span>sc</span><span><span>) </span><span>==</span><span> </span></span><span>r</span><span>.</span><span>at</span><span>(</span><span>2</span><span>))</span></div></div><div><div><div>5</div></div><div><span>  </span><span>#r</span><span>.</span><span>at</span><span>(</span><span>1</span><span>) : </span><span>#res</span><span>.</span><span>map</span><span>(</span><span>repr</span><span>).</span><span>join</span><span>(</span><span>" "</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这段循环接在 <code>map</code> 的定义后面（完整文件是 <code>/tmp/p8/mapcheck.typ</code>）。<code>eval(..., mode: "math")</code> 返回的是一整个 <code>equation</code> 元素，所以要和渲染列的 <code>r.at(2)</code> 直接比；和它的 <code>.body</code> 比会全部是 <code>false</code>。正确的比较给出四行带 <code>false</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>RR or bb(R) : true false</span></div></div><div><div><div>2</div></div><div><span>upright(d) x or dif x : false true</span></div></div><div><div><div>3</div></div><div><span>(x) or lr((x)) : false true</span></div></div><div><div><div>4</div></div><div><span>quad, thin : false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前三行是“或”写法：两个备选里只有一个和渲染列结构相同，另一个是同一个公式的另一种写法。渲染列画的是 <code>RR</code>、<code>dif x</code>、<code>lr((x))</code>，另一边的 <code>bb(R)</code>、<code>upright(d) x</code>、<code>(x)</code> 渲染出的字形我在 <code>/tmp/p8/t2.typ</code> 里并排看过，是一样的。最后一行 <code>quad, thin</code> 的渲染列本来就不是这个字符串对应的东西，而是 <code>a quad b thin c</code> 的演示，所以 <code>false</code> 是预期的。其余 28 行的 Typst 写法，第一个备选就与渲染列完全相同。也就是说除最后一行外，31 行的 Typst 写法都被机器验证过，没有一行是靠我读文档。</p><p><strong>LaTeX 列</strong>：我用系统的 XeTeX 3.141592653-2.6-0.999997（TeX Live 2025）、<code>amsmath</code>、<code>amssymb</code>、<code>cancel</code>、<code>mathtools</code> 把这一列的写法编译了一遍（<code>/tmp/p8/map.tex</code>，输出 1 页，无报错）。表里有两行写成 <code>..</code> 的简写：<code>pmatrix</code> 和 <code>cases</code>，我编译时写成了完整的环境。</p>

<table><thead><tr><th>LaTeX</th><th>Typst</th></tr></thead><tbody><tr><td><code>\frac{a}{b}</code></td><td><code>a/b</code></td></tr><tr><td><code>\sqrt{x}</code></td><td><code>sqrt(x)</code></td></tr><tr><td><code>\sqrt[3]{x}</code></td><td><code>root(3, x)</code></td></tr><tr><td><code>x_i^2</code></td><td><code>x_i^2</code></td></tr><tr><td><code>\sum_{i=1}^n</code></td><td><code>sum_(i=1)^n</code></td></tr><tr><td><code>\int_0^1 f\,dx</code></td><td><code>integral_0^1 f dif x</code></td></tr><tr><td><code>\lim_{n\to\infty}</code></td><td><code>lim_(n -&gt; oo)</code></td></tr><tr><td><code>\alpha, \beta</code></td><td><code>alpha, beta</code></td></tr><tr><td><code>\leq, \geq, \neq</code></td><td><code>lt.eq, gt.eq, eq.not</code></td></tr><tr><td><code>\to, \Rightarrow</code></td><td><code>-&gt;, =&gt;</code></td></tr><tr><td><code>\mathbf{A}</code></td><td><code>bold(A)</code></td></tr><tr><td><code>\mathbb{R}</code></td><td><code>RR</code> 或 <code>bb(R)</code></td></tr><tr><td><code>\mathcal{L}</code></td><td><code>cal(L)</code></td></tr><tr><td><code>\mathrm{d}x</code></td><td><code>upright(d) x</code> 或 <code>dif x</code></td></tr><tr><td><code>\hat{x}, \bar{x}</code></td><td><code>hat(x), macron(x)</code></td></tr><tr><td><code>\vec{v}</code></td><td><code>arrow(v)</code></td></tr><tr><td><code>\dot{x}, \ddot{x}</code></td><td><code>dot(x), dot.double(x)</code></td></tr><tr><td><code>\left( x \right)</code></td><td><code>(x)</code> 或 <code>lr((x))</code></td></tr><tr><td><code>\lvert x \rvert</code></td><td><code>abs(x)</code></td></tr><tr><td><code>\lVert x \rVert</code></td><td><code>norm(x)</code></td></tr><tr><td><code>\lfloor x \rfloor</code></td><td><code>floor(x)</code></td></tr><tr><td><code>\begin{pmatrix}..\end{pmatrix}</code></td><td><code>mat(1, 2; 3, 4)</code></td></tr><tr><td><code>\begin{cases}..\end{cases}</code></td><td><code>cases(1 quad x &gt; 0, 0 quad x &lt;= 0)</code></td></tr><tr><td><code>a &amp;= b \\ &amp;= c</code></td><td><code>a &amp;= b \ &amp;= c</code></td></tr><tr><td><code>\text{if}</code></td><td><code>"if"</code></td></tr><tr><td><code>\operatorname{rank}</code></td><td><code>op("rank")</code>，或 xuan 里的 <code>rank</code></td></tr><tr><td><code>\underbrace{x}_{n}</code></td><td><code>underbrace(x, n)</code></td></tr><tr><td><code>\cancel{x}</code></td><td><code>cancel(x)</code></td></tr><tr><td><code>\cdot, \times, \otimes</code></td><td><code>dot, times, times.o</code></td></tr><tr><td><code>\ldots, \cdots</code></td><td><code>dots, dots.c</code></td></tr><tr><td><code>\nabla, \partial</code></td><td><code>nabla, partial</code></td></tr><tr><td><code>\quad, \,</code></td><td><code>quad, thin</code></td></tr></tbody></table><p>表里有三处需要补一句：</p><ul>
<li>第 12 行 <code>RR</code>：<code>RR</code> 是 Typst 预定义的黑板粗体 R，和 <code>bb(R)</code> 同形。<code>NN</code>、<code>ZZ</code>、<code>QQ</code>、<code>CC</code> 同理，我把它们和 <code>RR</code> 一起放大编译过（<code>/tmp/p8/transpose.typ</code> 末尾那一排）。</li>
<li>第 23 行 <code>cases</code>：值和条件之间没有 <code>&amp;</code> 对齐，而是显式写 <code>quad</code> 隔开，原因见上一节。</li>
<li>第 26 行 <code>rank</code>：<code>rank</code> 不是 Typst 内置的，是 <code>xuan</code> 导出的运算符；没有 <code>xuan</code> 时请用 <code>op("rank")</code>。</li>
</ul></section>
<section><h2>回到线性代数<a href="#回到线性代数"><span>#</span></a></h2><p>最后，我用这一整篇的写法，把<a href="/posts/linear-algebra-16-least-squares/">最小二乘</a>和<a href="/posts/linear-algebra-29-svd/">奇异值分解</a>两篇笔记里的几个公式重排一遍，写成 <code>/tmp/p8/la.typ</code>，每个公式都带编号和标签，正文用 <code>@eq-...</code> 互相引用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// Bold upright vectors, the way the linear algebra posts write \mathbf.</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>vb</span><span>(</span><span>x</span><span><span>) </span><span>= </span></span><span>$</span><span>bold</span><span>(</span><span>upright</span><span>(</span><span>#x</span><span>))</span><span>$</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>=</span><span> 最小二乘</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>三个点 </span><span>$</span><span>(1, 1)</span><span>$</span><span>、</span><span>$</span><span>(2, 2)</span><span>$</span><span>、</span><span>$</span><span>(3, 2)</span><span>$</span><span> 拟合直线，无解的方程组</span></div></div><div><div><div>11</div></div><div><span>$</span><span>A </span><span>vb</span><span>(x) = </span><span>vb</span><span>(b)</span><span>$</span><span> 换成正规方程：</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>$</span><span> A^</span><span>top</span><span> A </span><span>hat</span><span>(</span><span>vb</span><span>(x)) = A^</span><span>top</span><span> </span><span>vb</span><span>(b) </span><span>$</span><span> </span><span>&lt;eq-normal&gt;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>解出来 </span><span>$</span><span>hat</span><span>(</span><span>vb</span><span>(x)) = (2/3, 1/2)</span><span>$</span><span>，误差向量 </span><span>$</span><span>vb</span><span>(e) = </span><span>vb</span><span>(b) - A </span><span>hat</span><span>(</span><span>vb</span><span>(x))</span></div></div><div><div><div>16</div></div><div><span>= (-1/6, 1/3, -1/6)</span><span>$</span><span>，它垂直于 </span><span>$</span><span>A</span><span>$</span><span> 的列空间。投影矩阵是</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>$</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>P = A (A^</span><span>top</span><span> A)^</span><span>(</span><span>-1</span><span>)</span><span> A^</span><span>top</span><span>, </span><span>quad</span><span> </span><span>vb</span><span>(p) = P </span><span>vb</span><span>(b), </span><span>quad</span><span> </span><span>vb</span><span>(e) = (I - P) </span><span>vb</span><span>(b)</span></div></div><div><div><div>20</div></div><div><span>$</span><span> </span><span>&lt;eq-proj&gt;</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>=</span><span> 奇异值分解</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>任何矩阵都能写成 </span><span>$</span><span>A = U </span><span>Sigma</span><span> V^</span><span>top</span><span>$</span><span>。逐列看就是</span></div></div><div><div><div>25</div></div><div><span>$</span><span>A </span><span>vb</span><span>(v)_i = </span><span>sigma</span><span>_i </span><span>vb</span><span>(u)_i</span><span>$</span><span>，其中 </span><span>$</span><span>sigma</span><span>_i &gt; 0</span><span>$</span><span> 是拉伸的倍数。</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>$</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>A^</span><span>top</span><span> A = V </span><span>Sigma</span><span>^</span><span>top</span><span> </span><span>Sigma</span><span> V^</span><span>top</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>= V </span><span>mat</span><span>(</span><span>sigma</span><span>_1^2, , ; , </span><span>dots</span><span>.</span><span>down</span><span>, ; , , </span><span>sigma</span><span>_n^2) V^</span><span>top</span></div></div><div><div><div>30</div></div><div><span>$</span><span> </span><span>&lt;eq-ata&gt;</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>$</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>(A </span><span>vb</span><span>(v)_i)^</span><span>top</span><span> (A </span><span>vb</span><span>(v)_j) = </span><span>cases</span><span>(</span></div></div><div><div><div>34</div></div><div><span>    </span><span>sigma</span><span>_i^2 &amp; </span><span>quad</span><span> i = j,</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>0 &amp; </span><span>quad</span><span> i != j,</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span>$</span><span> </span><span>&lt;eq-orth&gt;</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>和 </span><span>@eq-normal</span><span> 一样，</span><span>@eq-ata</span><span> 里出现了 </span><span>$</span><span>A^</span><span>top</span><span> A</span><span>$</span><span>。</span><span>@eq-orth</span><span> 说明</span></div></div><div><div><div>40</div></div><div><span>$</span><span>vb</span><span>(u)_i = A </span><span>vb</span><span>(v)_i </span><span>slash</span><span> </span><span>sigma</span><span>_i</span><span>$</span><span> 天然标准正交。</span></div></div><div><div><div>41</div></div><div><span>Strang 黑板上的矩阵 </span><span>$</span><span>A = </span><span>mat</span><span>(4, 4; -3, 3)</span><span>$</span><span> 给出</span></div></div><div><div><div>42</div></div><div><span>$</span><span>sigma</span><span>_1 = </span><span>sqrt</span><span>(32) </span><span>approx</span><span> 5.6569</span><span>$</span><span> 和 </span><span>$</span><span>sigma</span><span>_2 = </span><span>sqrt</span><span>(18) </span><span>approx</span><span> 4.2426</span><span>$</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch08-la-demo.png" alt="la.typ 的渲染：第 1 章“最小二乘”下有正规方程 (1.1) 与投影矩阵 (1.2)；第 2 章“奇异值分解”下有 A 转置 A 的对角化 (2.1) 与正交性的 cases (2.2)；正文里的引用都是蓝色“式 (x.y)”，并且 @eq-normal 在第 2 章里仍然显示 (1.1)" /><figcaption>la.typ 的渲染：第 1 章“最小二乘”下有正规方程 (1.1) 与投影矩阵 (1.2)；第 2 章“奇异值分解”下有 A 转置 A 的对角化 (2.1) 与正交性的 cases (2.2)；正文里的引用都是蓝色“式 (x.y)”，并且 @eq-normal 在第 2 章里仍然显示 (1.1)</figcaption></figure><p></p><p>图里最后一段的 <code>@eq-normal</code> 是在第 2 章里引用第 1 章的公式，显示 <code>式 (1.1)</code>，没有串章。这个文件里还有两个我在上面铺垫过的小点：对角阵用空格子写成 <code>mat(sigma_1^2, , ; , dots.down, ; , , sigma_n^2)</code>，<code>cases</code> 里每个 <code>&amp;</code> 后面都加了 <code>quad</code>。</p><p><strong>源码长度。</strong> 这两篇笔记里的公式原来是 LaTeX 写的。我拿 SVD 那篇的 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 展开、最小二乘那篇的正规方程和投影矩阵这三个，数了源码的字符数。Typst 版本我按这一篇的写法，正规方程用了上面 <code>vb</code> 宏（宏的定义本身 1 行，没计入）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>/tmp/p8/count.sh</span></div></div><div><div><div>2</div></div><div><span>latex-svd</span><span> </span><span>145</span></div></div><div><div><div>3</div></div><div><span>typst-svd</span><span> </span><span>115</span></div></div><div><div><div>4</div></div><div><span>latex-ne</span><span> </span><span>54</span></div></div><div><div><div>5</div></div><div><span>typst-ne</span><span> </span><span>32</span></div></div><div><div><div>6</div></div><div><span>latex-p</span><span>  </span><span>37</span></div></div><div><div><div>7</div></div><div><span>typst-p</span><span>  </span><span>22</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>count.sh</code> 就是对每条公式 <code>printf '%s' ... | wc -m</code>，脚本在 <code>/tmp/p8/count.sh</code>，LaTeX 公式取自两篇笔记里的原文，SVD 那条原文分成两行，我用一个空格接成一行。）三个公式合计 236 个字符降到 169 个，少 28%。我没想拿这个数字证明什么，LaTeX 的每个反斜杠和花括号都有它的用处。单看差别最大的一条，SVD 的对角阵，145 个字符对 115 个，光是环境名 <code>\begin{bmatrix}</code> 加 <code>\end{bmatrix}</code> 就是 28 个字符，在 Typst 里是 <code>mat(</code> 加 <code>)</code>，共 5 个。</p></section>
<section><h2>接下来<a href="#接下来"><span>#</span></a></h2><p>数学公式是 Typst 里第一个我觉得“写起来不比手写慢”的部分，唯一需要适应的是多字母名字要么加空格要么加引号。下一篇进入<strong>图、表、代码块和交叉引用</strong>：图和表的编号是同一个思路，也要按章编号、引用不串章，<code>figure.typ</code> 里有一条和 <code>math.typ</code> 结构相同的 <code>show ref</code>，读 <code>el.location()</code> 的方式也一样。顺带还有三线表、<code>raw</code> 的行号，以及 <code>units.typ</code> 里的 <code>num</code> 和 <code>qty</code>。</p><p>两件留给读者的事：把 <code>math.typ</code> 里的 <code>context</code> 去掉，对照上面的实验，看 <code>numbering.typ</code> 的输出有没有变；给 <code>equation-number</code> 补上 <code>form: "page"</code> 的分支，让 <code>#ref(&lt;a&gt;, form: "page")</code> 给出页码。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-07-context-counters-state/</id>
      <title type="text">context、计数器与状态：Typst 如何“多遍”编译</title>
      <published>2026-10-19T06:00:00.000Z</published>
      <updated>2026-10-19T06:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-07-context-counters-state/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 7 篇：我用 Typst 0.15.1 编译了 ch07 的 9 个 lab 文件，用 --timings 数出各个文档实际编译了 1 到 5 遍，在 /tmp 里复现了去掉 show ref 之后定理引用的章号错误，再写出 xuan 的 header.typ 与 theorem.typ，让页眉跟随章节、定理按“章.序号”编号并能用 @label 引用。</summary>
      <content type="html"><![CDATA[<p><code>#let size = text.size</code> 这一行会被 Typst 拒绝，报的是 <code>error: can only be used when context is known</code>，措辞很客气，还建议“把这个包在 <code>context</code> 表达式里”。这条报错背后是 Typst 最不像脚本语言的那部分设计：一份文档不是顺着源码从头执行到尾就完事的，页码、编号、目录、页眉都依赖“这个东西最后落在哪一页”，而这个答案只有排完版才知道。这一篇我把 <code>context</code>、<code>counter</code>、<code>state</code>、<code>query</code> 和 <code>metadata</code> 逐个跑一遍，数出编译器到底把文档编译了几遍，最后用它们写出 <code>xuan</code> 的页眉和定理环境。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有两个新文件：<code>src/header.typ</code> 和 <code>src/theorem.typ</code>，都是<strong>最终版</strong>，文中完整展示。<code>lib.typ</code>、<code>src/base.typ</code>、<code>src/note.typ</code> 仍是第 4 篇的<strong>中间版本</strong>：它们当时已经 <code>import</code> 了这两个文件并调用了 <code>xuan-header()</code>、<code>xuan-footer()</code> 和 <code>theorem-rules</code>，这一篇没有再往里加行，只是把被调用的那一头讲清楚。<code>lab/ch07/</code> 下的 9 个文件（含一个预期失败、一个预期警告的）全部通过 <code>make ch07</code>。
前置：<a href="/posts/typst-by-xuan-06-page-and-layout/">第 6 篇</a>（页面与布局，<code>page</code> 的 <code>header</code>/<code>footer</code> 参数）。<code>fonts.typ</code>、<code>cjk.typ</code>、<code>page.typ</code> 在第 5、6 篇已经写好。</p></div></div>
<section><h2>为什么 <code>text.size</code> 要在 context 里<a href="#为什么-textsize-要在-context-里"><span>#</span></a></h2><p><code>lab/ch07/no-context.fail.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/no-context.fail.typ" target="_blank">源文件</a>）只有两行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Style values are only known where the content ends up, hence `context`.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>size</span><span><span> </span><span>= </span></span><span>text</span><span>.</span><span>size</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make ch07</code> 把 stderr 存在 <code>out/ch07/no-context.stderr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: can only be used when context is known</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch07/no-context.fail.typ:2:17</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #let size = text.size</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│                  ^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= hint: try wrapping this in a `context` expression</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= hint: the `context` expression should wrap everything that depends on this function</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>道理在第 4 篇已经埋下了：<code>set text(size: ...)</code> 往样式表里登记规则，规则的作用范围是源码里的一段区间，同一个函数写在别处，字号可以完全不同。<code>let size = text.size</code> 这一行在源码顶层求值，Typst 此刻不知道这个值将来会被放到哪里，所以“当前字号”这个问题没有答案。解法是把问题推迟：<code>context</code> 后面的表达式不立刻求值，整个包成一块不透明的 content，等它被放进文档、环境已知时再算。</p><p>我在 <code>/tmp/p7/val.typ</code> 里验证它到底有多“延迟”（这个目录下的实验都不在仓库里，代码全在文中，可以原样粘贴）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>value</span><span><span> </span><span>= </span></span><span>context</span><span> </span><span>text</span><span>.</span><span>size</span></div></div><div><div><div>3</div></div><div><span>#value</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>#text</span><span>(</span><span>size</span><span>: </span><span>20pt</span><span>)[</span><span>#value</span><span>] </span><span>\</span></div></div><div><div><div>5</div></div><div><span>#</span><span>[</span><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>7pt</span><span>); </span><span>#value</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdftotext -layout</code> 的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>11pt</span></div></div><div><div><div>2</div></div><div><span>20pt</span></div></div><div><div><div>3</div></div><div><span>7pt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>value</code> 只写了一次，却在三个地方算出三个值。它不是“字号”，而是“到时候去查字号的指令”。Typst 文档里的说法是 context 表达式的 body 会被求值零次、一次或多次，取决于它被放到几个地方。（这也是后面页眉的工作方式：同一个 <code>context</code> 块摆在每一页，每页各自算一遍。）</p><p>不透明带来三个后果，我各试了一次。</p><p><strong>第一，不能对结果做运算。</strong> <code>/tmp/p7/add.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>f</span><span><span> </span><span>= </span></span><span>context</span><span> </span><span>text</span><span>.</span><span>size</span></div></div><div><div><div>2</div></div><div><span>#</span><span>(</span><span>f</span><span><span> </span><span>+</span><span> </span></span><span>1pt</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: cannot add content and length</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ add.typ:2:2</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #(f + 1pt)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│   ^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>f</code> 的类型是 content，不是 length。想做运算，运算本身必须写进 context 里面：<code>#context text.size + 1pt</code>。</p><p><strong>第二，比较不会报错，只会悄悄为假。</strong> <code>/tmp/p7/fn.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>f</span><span><span> </span><span>= </span></span><span>context</span><span> </span><span>text</span><span>.</span><span>size</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>g</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>f</span><span><span> </span><span>==</span><span> </span></span><span>11pt</span><span> [是] </span><span>else</span><span> [否]</span></div></div><div><div><div>3</div></div><div><span>#g</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出是 <code>否</code>。字号明明是 11pt（默认值），但 <code>f</code> 是 content，content 与 length 比较得 <code>false</code>，没有任何警告。我觉得这是 <code>context</code> 最容易踩的坑：报错的错还好办，静默的错要靠记住“context 的结果只能被放置，不能被检查”来躲。</p><p><strong>第三，<code>measure</code>、<code>here</code>、<code>layout</code> 这一类函数也需要 context。</strong> <code>here()</code> 放在顶层会得到和上面一模一样的 <code>can only be used when context is known</code>，我另外试了 <code>measure([abc]).width</code>，同样的错误。</p><p>有一类地方不需要自己写 <code>context</code>：show 规则。第 4 篇说过 <code>show heading: it =&gt; ...</code> 里的函数在元素被实现化时调用，那一刻位置已知，所以 <code>/tmp/p7/showctx.typ</code> 可以直接读计数器：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1."</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span> </span><span>heading</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> [</span><span>#it</span><span>.</span><span>body</span><span>（</span><span>#counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>()）]</span></div></div><div><div><div>4</div></div><div><span>=</span><span> 甲</span></div></div><div><div><div>5</div></div><div><span>=</span><span> 乙</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>甲（(1,)） 乙（(2,)）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>根据文档，所有 show 规则都提供样式 context，但只有作用在可定位（locatable）元素上的 show 规则才提供位置 context，标题是可定位的。这一点在后面的 <code>show ref</code> 里会用到。</p><section><h3>lab：context、here 与 measure<a href="#labcontexthere-与-measure"><span>#</span></a></h3><p><code>lab/ch07/context.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/context.typ" target="_blank">源文件</a>）把上面几件事放在一起：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// Style values like `text.size` exist only inside `context`.</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>14pt</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>size-now</span><span><span> </span><span>= </span></span><span>context</span><span> </span><span>text</span><span>.</span><span>size</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// The result is content that is resolved when it is placed.</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>size-now</span><span>), </span><span>content</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#text</span><span>(</span><span>size</span><span>: </span><span>20pt</span><span>)[在 20pt 里：</span><span>#size-now</span><span>]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// `here()` is the current location; `locate`-style queries read its page.</span></div></div><div><div><div>12</div></div><div><span>#context</span><span> [第 </span><span>#here</span><span>().</span><span>page</span><span>() 页，位置 </span><span>#here</span><span>().</span><span>position</span><span>()。]</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>// `context` also gives access to `measure` and `layout`.</span></div></div><div><div><div>15</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>16</div></div><div><span>  </span><span>let</span><span> </span><span>w</span><span><span> </span><span>= </span></span><span>measure</span><span>([中文abc]).</span><span>width</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>[这行文字宽 </span><span>#calc</span><span>.</span><span>round</span><span>(</span><span>w</span><span>.</span><span>pt</span><span>(), </span><span>digits</span><span>: </span><span>1</span><span>) pt。]</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>#pagebreak</span><span>()</span></div></div><div><div><div>21</div></div><div><span>#context</span><span> [这是第 </span><span>#here</span><span>().</span><span>page</span><span>() 页。]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译输出（<code>pdftotext -layout out/ch07/context.pdf -</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>在 20pt 里：20pt</span></div></div><div><div><div>2</div></div><div><span>第 1 页，位置 (page: 1, x: 28.35pt, y: 77.27pt)。</span></div></div><div><div><div>3</div></div><div><span>这行文字宽 50.9 pt。</span></div></div><div><div><div>4</div></div><div><span>这是第 2 页。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>size-now</code> 在 14pt 的环境里定义，被放进 <code>text(size: 20pt)</code> 之后读出 20pt：求值的环境是放置处，不是定义处。<code>assert.eq(type(size-now), content)</code> 把“它是 content”钉死了。<code>measure</code> 返回的是 <code>length</code>，<code>.pt()</code> 给出 <code>float</code>，不取整的话会打出一长串浮点尾巴，所以文件里用 <code>calc.round(w.pt(), digits: 1)</code> 取一位小数，得 50.9 pt。</p></section></section>
<section><h2>位置：<code>here</code>、<code>location</code> 与 <code>locate</code><a href="#位置herelocation-与-locate"><span>#</span></a></h2><p><code>here()</code> 返回当前位置，类型是 <code>location</code>。<code>location</code> 本身没有多少内容，它是一个指向文档里某一点的句柄，拿着它可以去问“这一点在第几页、页面上什么坐标”，也可以交给 <code>counter.at</code>、<code>state.at</code> 去问“那一点的计数器是几”。<code>/tmp/p7/loc.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>6cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>=</span><span> 标题</span></div></div><div><div><div>3</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>let</span><span> </span><span>loc</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>heading</span><span>).</span><span>first</span><span>().</span><span>location</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>[类型 </span><span>#type</span><span>(</span><span>loc</span><span>)，页 </span><span>#loc</span><span>.</span><span>page</span><span>()，位置 </span><span>#loc</span><span>.</span><span>position</span><span>() </span><span>\ </span><span>]</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>[here() 的 repr：</span><span>#repr</span><span>(</span><span>here</span><span>())]</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>标题</span></div></div><div><div><div>2</div></div><div><span>类型 location，页 1，位置 (page: 1, x: 20.25pt, y: 20.25pt)</span></div></div><div><div><div>3</div></div><div><span>here() 的 repr：location(..)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>repr(here())</code> 是 <code>location(..)</code>，说明它的内部结构不对外暴露，能做的只有调方法。其中 <code>.page()</code> 是物理页序号，从 1 开始，与页码计数器无关；<code>.position()</code> 是带 <code>page</code>、<code>x</code>、<code>y</code> 的字典。</p><p><code>locate</code> 的用法在不同版本里变过，所以我动手前先编译。在 0.15.1 里，<code>locate</code> 接受<strong>标签或选择器</strong>，要求它恰好匹配一个元素，返回那个元素的 <code>location</code>。<code>/tmp/p7/posn.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>8cm</span><span>, </span><span>height</span><span>: </span><span>4cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>=</span><span> 甲 </span><span>&lt;a&gt;</span></div></div><div><div><div>3</div></div><div><span>#pagebreak</span><span>()</span></div></div><div><div><div>4</div></div><div><span>=</span><span> 乙 </span><span>&lt;b&gt;</span></div></div><div><div><div>5</div></div><div><span>#context</span><span> [乙在 </span><span>#locate</span><span>(</span><span>&lt;b&gt;</span><span>).</span><span>position</span><span>() </span><span>\ </span><span>页码 </span><span>#locate</span><span>(</span><span>&lt;b&gt;</span><span>).</span><span>page</span><span>()]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>乙在 (page: 2, x: 13.5pt, y: 13.5pt)</span></div></div><div><div><div>2</div></div><div><span>页码 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它没有任何弃用警告。我同时试了两种老写法，都不能用：<code>locate(loc =&gt; loc.page())</code> 这种回调形式会得到 <code>error: only element functions can be used as selectors</code>；标签出现不止一次则是 <code>error: label </code><code> occurs multiple times in the document</code>。所以我现在的习惯是：想知道“我自己在哪”用 <code>here()</code>，想知道“那个有标签的东西在哪”用 <code>locate(&lt;label&gt;)</code>，不再写回调。</p><section><h3>嵌套的 context 看的是最内层<a href="#嵌套的-context-看的是最内层"><span>#</span></a></h3><p><code>context</code> 里面还可以再写 <code>context</code>，规则是<strong>读到的永远是最内层的环境</strong>。<code>/tmp/p7/nest.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"mycounter"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#c</span><span>.</span><span>update</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>5</div></div><div><span>  </span><span>#c</span><span>.</span><span>update</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>外层：</span><span>#c</span><span>.</span><span>display</span><span>() </span><span>\</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>内层：</span><span>#context</span><span> </span><span>c</span><span>.</span><span>display</span><span>()</span></div></div><div><div><div>8</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>外层：1</span></div></div><div><div><div>2</div></div><div><span>内层：2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>外层的 <code>c.display()</code> 是在外层 context 的位置上求值的，而 <code>c.update(2)</code> 在这个 context 的内部，位于“求值点”之后，所以外层读到 1。内层 <code>context</code> 的求值被推迟到 <code>c.update(2)</code> 之后，读到 2。我第一次看到这个结果以为是 bug，后来想明白了：一块 context 在<strong>它开始的那一点</strong>读取世界，里面的 <code>update</code> 影响的是它之后的世界。</p></section></section>
<section><h2>编译器怎么“多遍”工作<a href="#编译器怎么多遍工作"><span>#</span></a></h2><p>目录里要写页码，页码要排完版才知道，排版又要先有目录的长度。Typst 的做法是干脆多编译几遍：第一遍用占位值，排完版把所有内省的答案收集起来，第二遍用这些答案重排，直到答案不再变化。官方文档说上限是 5 遍，超过就停下并发出 <code>document did not converge within five attempts</code> 的警告。我想亲眼数一数，于是用 <code>--timings</code>（第 16 篇会专门讲）把每次编译的事件写成 JSON，再用 <code>jq</code> 数 <code>iter (N)</code> 事件的个数：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>typst</span><span> </span><span>compile</span><span> </span><span>--timings</span><span> </span><span>t.json</span><span> </span><span>lab/ch07/counters.typ</span><span> </span><span>out.pdf</span></div></div><div><div><div>2</div></div><div><span>jq</span><span> </span><span>'[.[]|select(.name|startswith("iter"))|select(.ph=="B")]|length'</span><span> </span><span>t.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在仓库的 <code>nix develop</code> 里跑了一遍我手头的所有文档，结果：</p>

<table><thead><tr><th>文档</th><th>编译遍数</th></tr></thead><tbody><tr><td><code>/tmp/p7/plain.typ</code>（一个标题加一句话，无内省）</td><td>1</td></tr><tr><td><code>lab/ch07/context.typ</code>、<code>state.typ</code></td><td>2</td></tr><tr><td><code>lab/ch07/counters.typ</code>、<code>query.typ</code>、<code>running-header.typ</code>、<code>theorems.typ</code></td><td>3</td></tr><tr><td><code>examples/notes/main.typ</code>（4 页笔记，含目录和术语表）</td><td>4</td></tr><tr><td><code>lab/ch07/diverge.warn.typ</code></td><td>5，并输出警告</td></tr></tbody></table><p>一个没有内省的文档只编译 1 遍；<code>#context 1</code> 这样不读任何位置相关信息的 context 也是 1 遍（我另外试过）。一旦有东西去问“别处的值是多少”，至少要 2 遍：第一遍收集，第二遍确认答案没变。<code>counters.typ</code> 要 3 遍，我猜原因是它里面有一条在第二遍才稳定下来的依赖（<code>final()</code> 的值要等整份文档的更新跑完），不过这是推测，<code>--timings</code> 只告诉我遍数，没有告诉我是谁拖了第三遍。</p><p>收敛的判据，我能确认的只有这些：<code>--timings</code> 的事件里每一遍都有 <code>introspect pages</code> 和 <code>check stabilized</code>，只有发散的那份文档额外出现了 <code>analyze introspections</code>。我的读法是，每遍结束后编译器比较内省结果与上一遍是否一致，一致就停。</p><section><h3>前向引用<a href="#前向引用"><span>#</span></a></h3><p>多遍编译最直接的好处是<strong>能引用后面的东西</strong>。<code>/tmp/p7/fwd.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>10cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1."</span><span>)</span></div></div><div><div><div>3</div></div><div><span>本文共 </span><span>#context</span><span> </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>final</span><span>().</span><span>first</span><span>() </span><span>节</span><span>，</span></div></div><div><div><div>4</div></div><div><span>第 </span><span>#context</span><span> </span><span>query</span><span>(</span><span>&lt;last&gt;</span><span>).</span><span>first</span><span>().</span><span>location</span><span>().</span><span>page</span><span>() </span><span>页收尾</span><span>。</span></div></div><div><div><div>5</div></div><div><span>=</span><span> 甲</span></div></div><div><div><div>6</div></div><div><span>=</span><span> 乙</span></div></div><div><div><div>7</div></div><div><span>=</span><span> 丙 </span><span>&lt;last&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>本文共 3 节， 第 1 页收尾。</span></div></div><div><div><div>2</div></div><div><span>1. 甲</span></div></div><div><div><div>3</div></div><div><span>2. 乙</span></div></div><div><div><div>4</div></div><div><span>3. 丙</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>开头那句话在三个标题之前，却知道总共有 3 节，最后一节在第 1 页。这个文档编译了 3 遍。</p></section><section><h3>收敛的和不收敛的<a href="#收敛的和不收敛的"><span>#</span></a></h3><p>再看一个会自己稳定下来的“自引用”。<code>/tmp/p7/conv.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>s</span><span><span> </span><span>= </span></span><span>state</span><span>(</span><span>"s"</span><span>, </span><span>0</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#context</span><span> </span><span>s</span><span>.</span><span>update</span><span>(</span><span>calc</span><span>.</span><span>min</span><span>(</span><span>s</span><span>.</span><span>final</span><span><span>() </span><span>+</span><span> </span></span><span>1</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>3</div></div><div><span>#context</span><span> </span><span>s</span><span>.</span><span>get</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>s</code> 的更新依赖它自己的最终值，但 <code>calc.min(..., 3)</code> 给了它一个天花板。结果页面上是 <code>3</code>，stderr 为空，<code>--timings</code> 数出 4 遍：初值 0、1、2、3，然后确认 3 不再变化。</p><p>把天花板拿掉就是 <code>lab/ch07/diverge.warn.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/diverge.warn.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A state whose new value depends on its own final value never settles:</span></div></div><div><div><div>2</div></div><div><span>// every layout pass changes the input of the next one.</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>s</span><span><span> </span><span>= </span></span><span>state</span><span>(</span><span>"s"</span><span>, </span><span>0</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#context</span><span> </span><span>s</span><span>.</span><span>update</span><span>(</span><span>s</span><span>.</span><span>final</span><span><span>() </span><span>+</span><span> </span></span><span>1</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#context</span><span> </span><span>s</span><span>.</span><span>get</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每一遍里 <code>s.final()</code> 都比上一遍大 1，永远追不上自己。<code>make ch07</code> 的结果是 <code>ok   lab/ch07/diverge.warn.typ (warned as expected)</code>，stderr（<code>out/ch07/diverge.stderr</code>）一字不改：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>warning: document did not converge within five attempts</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>= hint: see 2 additional warnings for more details</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>= hint: see https://typst.app/help/convergence for help</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>warning: value of `state("s")` did not converge</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>┌─ lab/ch07/diverge.warn.typ:4:18</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>8</div></div><div><span>4 │ #context s.update(s.final() + 1)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>│                   ^^^^^^^^^</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>= hint: the following values were observed:</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>- run 1: `0`</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>- run 2: `1`</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>- run 3: `2`</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>- run 4: `3`</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>- run 5: `4`</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>- final: `5`</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>= hint: see https://typst.app/help/state-convergence for help</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>warning: value of `state("s")` did not converge</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>┌─ lab/ch07/diverge.warn.typ:5:9</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>23</div></div><div><span>5 │ #context s.get()</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>│          ^^^^^^^</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>= hint: the following values were observed:</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>- run 1: `0`</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>- run 2: `1`</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>- run 3: `2`</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>- run 4: `3`</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>- run 5: `4`</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>- final: `5`</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>= hint: see https://typst.app/help/state-convergence for help</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这份诊断比“超过 5 遍了”有用得多：它指出是哪个 state、哪一行读到的值在变，并逐遍列出观察到的值，第 1 到第 5 遍是 0 到 4，最后一次排出来是 5。那两条 <code>warning: value of state("s") did not converge</code> 分别指向 <code>s.final()</code> 和 <code>s.get()</code>，也就是“读取了不稳定值”的两个位置。<code>--timings</code> 显示这个文档恰好跑了 5 遍，与警告里的 <code>run 1</code> 到 <code>run 5</code> 对得上。</p><p>警告只是警告，文档照样输出了 PDF，页面上 <code>s.get()</code> 印出的是 <code>4</code>（第 5 遍读到的值，而 <code>final</code> 已经是 5），哪一个都不是稳定的答案。写模板的时候我把它当成“自己设计错了”的信号：如果一个 state 的更新依赖自己的 <code>final()</code>，几乎一定应该换一种做法，比如先 <code>query</code> 出元素再统一算。</p></section></section>
<section><h2>counter：带层级的整数数组<a href="#counter带层级的整数数组"><span>#</span></a></h2><p><code>counter</code> 是 Typst 里的命名整数序列，但它的值不是一个整数，而是一个<strong>数组</strong>：标题计数器 <code>(2, 1)</code> 表示“第 2 章第 1 节”。<code>lab/ch07/counters.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/counters.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// A counter is a named sequence of integer arrays.</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>notes</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"note"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>note</span><span>(</span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>notes</span><span>.</span><span>step</span><span>()</span></div></div><div><div><div>8</div></div><div><span>  </span><span>block</span><span>(</span><span>stroke</span><span>: (</span><span>left</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>), </span><span>inset</span><span>: (</span><span>left</span><span>: </span><span>6pt</span><span>), [</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>*注 </span><span>#context</span><span> </span><span>notes</span><span>.</span><span>display</span><span><span>()</span><span>*</span><span>：</span></span><span>#body</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>])</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>=</span><span> 第一节</span></div></div><div><div><div>14</div></div><div><span>#note</span><span>[第一条]</span></div></div><div><div><div>15</div></div><div><span>#note</span><span>[第二条]</span></div></div><div><div><div>16</div></div><div><span>#metadata</span><span>(</span><span>none</span><span>) </span><span>&lt;end-of-one&gt;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>=</span><span> 第二节</span></div></div><div><div><div>19</div></div><div><span>// `update` sets the value; functions can compute the next one.</span></div></div><div><div><div>20</div></div><div><span>#notes</span><span>.</span><span>update</span><span>(</span><span>10</span><span>)</span></div></div><div><div><div>21</div></div><div><span>#note</span><span>[从 10 重新开始，变成 11]</span></div></div><div><div><div>22</div></div><div><span>#notes</span><span>.</span><span>update</span><span>(</span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>n</span><span><span> </span><span>*</span><span> </span></span><span>2</span><span>)</span></div></div><div><div><div>23</div></div><div><span>#note</span><span>[22 之后加一得 23]</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>// Built-in counters: `page` and `heading`.</span></div></div><div><div><div>26</div></div><div><span>#context</span><span> [标题计数器现在是 </span><span>#counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>()，页码 </span><span>#counter</span><span>(</span><span>page</span><span>).</span><span>get</span><span>()。]</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>// `at` reads a counter at a label; `final` reads it at the end of the document.</span></div></div><div><div><div>29</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>笔记总数 </span><span>#notes</span><span>.</span><span>final</span><span>().</span><span>first</span><span>()。</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>第一节结束时 heading 计数为 </span><span>#counter</span><span>(</span><span>heading</span><span>).</span><span>at</span><span>(</span><span>&lt;end-of-one&gt;</span><span>)。</span></div></div><div><div><div>32</div></div><div><span>]</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>// 0.15: `display(at:)` formats the value at a given location.</span></div></div><div><div><div>35</div></div><div><span>#context</span><span> </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>display</span><span>(</span><span>"1.1"</span><span>, </span><span>at</span><span>: </span><span>&lt;end-of-one&gt;</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 第一节</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>注 1：第一条</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>注 2：第二条</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>2 第二节</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>注 11：从 10 重新开始，变成 11</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>注 23：22 之后加一得 23</span></div></div><div><div><div>8</div></div><div><span>标题计数器现在是 (2,)，页码 (1,)。</span></div></div><div><div><div>9</div></div><div><span>笔记总数 23。 第一节结束时 heading 计数为 (1,)。</span></div></div><div><div><div>10</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>逐条读：</p><ul>
<li><code>step()</code> 加 1，<code>update(n)</code> 直接设值，<code>update(f)</code> 接一个函数，参数是旧值。<code>notes.update(10)</code> 之后下一个 <code>step()</code> 得 11，<code>update(n =&gt; n * 2)</code> 把 11 变成 22，再 <code>step()</code> 就是 23。这几个调用本身都是“往文档里放一条更新指令”，不是立刻改变量，它们和函数 <code>note</code> 里的 <code>notes.step()</code> 一样，只有被放进文档才生效。</li>
<li><code>display()</code> 把当前值按计数器自己的编号格式显示，<code>notes.display()</code> 缺省是 <code>"1"</code>。必须在 context 里调用，因为它要问“此刻几号”。</li>
<li><code>get()</code> 是数组，所以页码是 <code>(1,)</code> 而不是 <code>1</code>。取第一个分量用 <code>.first()</code>，这是后面页脚里 <code>counter(page).get().first()</code> 的由来。</li>
<li><code>at(label)</code> 回到某个位置去读值，<code>final()</code> 读文档末尾的值，二者都是“时间旅行”，都需要 context。</li>
<li>这个文件最后一行是 0.15 新增的 <code>display(at:)</code>：不用先 <code>at</code> 再格式化，一步到位。</li>
</ul><p>注意同一块 context 里的两个读数：<code>get()</code> 读的是此刻，标题计数器是 <code>(2,)</code>；<code>at(&lt;end-of-one&gt;)</code> 读的是标签所在的位置，标签贴在第一节的两条笔记之后，所以是 <code>(1,)</code>，<code>display(at:)</code> 给出 <code>1</code>。<code>at</code> 只认标签落在文档的哪一点，不管你在哪里写这行读取代码。</p><section><h3>层级、<code>step(level:)</code>、页码计数器<a href="#层级steplevel页码计数器"><span>#</span></a></h3><p>计数器的层级我另外做了一个实验，<code>/tmp/p7/cx.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>, </span><span>numbering</span><span>: </span><span>"1"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>"c"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>=</span><span> 甲</span></div></div><div><div><div>5</div></div><div><span>==</span><span> 子</span></div></div><div><div><div>6</div></div><div><span>==</span><span> 子二</span></div></div><div><div><div>7</div></div><div><span>=</span><span> 乙</span></div></div><div><div><div>8</div></div><div><span>==</span><span> 子三</span></div></div><div><div><div>9</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>heading：</span><span>#counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>() </span><span>\</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>display()：</span><span>#counter</span><span>(</span><span>heading</span><span>).</span><span>display</span><span>() </span><span>\</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>display("1.1")：</span><span>#counter</span><span>(</span><span>heading</span><span>).</span><span>display</span><span>(</span><span>"1.1"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>]</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>#c</span><span>.</span><span>step</span><span>() </span><span>#c</span><span>.</span><span>step</span><span>() </span><span>#c</span><span>.</span><span>step</span><span>()</span></div></div><div><div><div>16</div></div><div><span>#context</span><span> [get：</span><span>#c</span><span>.</span><span>get</span><span>() </span><span>\ </span><span>display("I")：</span><span>#c</span><span>.</span><span>display</span><span>(</span><span>"I"</span><span>)]</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>#c</span><span>.</span><span>update</span><span>((</span><span>2</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>19</div></div><div><span>#context</span><span> [update((2, 5))：</span><span>#c</span><span>.</span><span>get</span><span>() </span><span>\ </span><span>display("1.1")：</span><span>#c</span><span>.</span><span>display</span><span>(</span><span>"1.1"</span><span>)]</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#c</span><span>.</span><span>step</span><span>(</span><span>level</span><span>: </span><span>2</span><span>)</span></div></div><div><div><div>22</div></div><div><span>#context</span><span> [step(level: 2)：</span><span>#c</span><span>.</span><span>get</span><span>()]</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>#c</span><span>.</span><span>step</span><span>(</span><span>level</span><span>: </span><span>1</span><span>)</span></div></div><div><div><div>25</div></div><div><span>#context</span><span> [step(level: 1)：</span><span>#c</span><span>.</span><span>get</span><span>()]</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>#counter</span><span>(</span><span>page</span><span>).</span><span>update</span><span>(</span><span>10</span><span>)</span></div></div><div><div><div>28</div></div><div><span>#pagebreak</span><span>()</span></div></div><div><div><div>29</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>页码计数器：</span><span>#counter</span><span>(</span><span>page</span><span>).</span><span>get</span><span>() </span><span>\</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>物理页：</span><span>#here</span><span>().</span><span>page</span><span>() </span><span>\</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>display("i")：</span><span>#counter</span><span>(</span><span>page</span><span>).</span><span>display</span><span>(</span><span>"i"</span><span>)</span></div></div><div><div><div>33</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1甲</span></div></div><div><div><div>2</div></div><div><span>1.1 子</span></div></div><div><div><div>3</div></div><div><span>1.2 子二</span></div></div><div><div><div>4</div></div><div><span>2乙</span></div></div><div><div><div>5</div></div><div><span>2.1 子三</span></div></div><div><div><div>6</div></div><div><span>heading：(2, 1)</span></div></div><div><div><div>7</div></div><div><span>display()：2.1</span></div></div><div><div><div>8</div></div><div><span>display(“1.1”)：2.1</span></div></div><div><div><div>9</div></div><div><span>get：(3,)</span></div></div><div><div><div>10</div></div><div><span>display(“I”)：III</span></div></div><div><div><div>11</div></div><div><span>update((2, 5))：(2, 5)</span></div></div><div><div><div>12</div></div><div><span>display(“1.1”)：2.5</span></div></div><div><div><div>13</div></div><div><span>step(level: 2)：(2, 6)</span></div></div><div><div><div>14</div></div><div><span>step(level: 1)：(3,)</span></div></div><div><div><div>15</div></div><div><span>页码计数器：(11,)</span></div></div><div><div><div>16</div></div><div><span>物理页：2</span></div></div><div><div><div>17</div></div><div><span>display(“i”)：xi</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（输出里的 <code>“1.1”</code>、<code>“i”</code> 是 PDF 里智能引号排出的弯引号，与计数器无关。）</p><ul>
<li>内置的 <code>counter(heading)</code> 在标题处自动 <code>step</code>，一级标题 <code>step</code> 时把二级以下清零，所以第二章的第一节是 <code>(2, 1)</code>。</li>
<li><code>step(level: 2)</code> 只增加第二个分量；<code>step(level: 1)</code> 增加第一个并<strong>把后面的分量丢掉</strong>，<code>(2, 6)</code> 变成 <code>(3,)</code>。</li>
<li>数组形式的 <code>update((2, 5))</code> 一次设多层。<code>display("1.1")</code> 在这个数组上输出 <code>2.5</code>。</li>
<li><code>counter(page)</code> 是页码计数器：<code>update(10)</code> 之后分页，下一页的计数器是 11，而 <code>here().page()</code> 仍是物理页 2。<code>display("i")</code> 用小写罗马数字格式化出 <code>xi</code>。页码计数器与物理页是两回事，所以页码可以从任意数字开始而不影响 <code>location.page()</code>。</li>
</ul></section><section><h3>元素计数器：<code>counter(figure.where(kind: ...))</code><a href="#元素计数器counterfigurewherekind-"><span>#</span></a></h3><p>编号不只来自标题和页码。每种 <code>figure</code> 有自己的计数器，可以用 <code>where</code> 选出来。<code>/tmp/p7/cfig.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>(</span><span>width</span><span>: </span><span>1cm</span><span>, </span><span>height</span><span>: </span><span>1cm</span><span>), </span><span>caption</span><span>: [甲])</span></div></div><div><div><div>3</div></div><div><span>#figure</span><span>(</span></div></div><div><div><div>4</div></div><div><span>  </span><span>table</span><span>(</span></div></div><div><div><div>5</div></div><div><span>    </span><span>columns</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>[x],</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>8</div></div><div><span>  </span><span>caption</span><span>: [乙],</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div><span>#figure</span><span>(</span><span>rect</span><span>(</span><span>width</span><span>: </span><span>1cm</span><span>, </span><span>height</span><span>: </span><span>1cm</span><span>), </span><span>caption</span><span>: [丙])</span></div></div><div><div><div>11</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>图 </span><span>#counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>image</span><span>)).</span><span>final</span><span>().</span><span>first</span><span>()，</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>表 </span><span>#counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>table</span><span>)).</span><span>final</span><span>().</span><span>first</span><span>()，</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>全部 figure </span><span>#counter</span><span>(</span><span>figure</span><span>).</span><span>final</span><span>()</span></div></div><div><div><div>15</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>                       </span></span><span>Figure 1: 甲</span></div></div><div><div><div>2</div></div><div><span><span>                           </span></span><span>x</span></div></div><div><div><div>3</div></div><div><span><span>                       </span></span><span>Table 1: 乙</span></div></div><div><div><div>4</div></div><div><span><span>                       </span></span><span>Figure 2: 丙</span></div></div><div><div><div>5</div></div><div><span>图 2， 表 1， 全部 figure (3,)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>rect</code> 放进 <code>figure</code> 被归到 <code>image</code> 这一类，<code>table</code> 另起一类，各自计数；不带 <code>where</code> 的 <code>counter(figure)</code> 数的是所有 figure，总共 3 个。这是第 9 篇里 <code>xuan</code> 把图和表按“章.序号”分别编号的基础，这篇的定理环境用同样的办法，下面会看到。</p></section></section>
<section><h2>state：任意类型的“带历史的值”<a href="#state任意类型的带历史的值"><span>#</span></a></h2><p><code>counter</code> 只能是整数数组。要记一个字符串、一个字典、一个不断增长的列表，用 <code>state</code>。我用一个类比讲它，只用这一次：<strong>state 是一本流水账</strong>。每次 <code>update</code> 是账本上新增的一行，<code>get()</code> 是“翻到我这一行，把到这里为止的所有记录累加起来的结果”，<code>final()</code> 是翻到最后一页。账不是在你写 <code>update</code> 的瞬间改的，而是在排版时按文档顺序一行一行算出来的。</p><p><code>lab/ch07/state.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/state.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// State is a value that changes through the document, like a counter that can</span></div></div><div><div><div>4</div></div><div><span>// hold anything.</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>mode</span><span><span> </span><span>= </span></span><span>state</span><span>(</span><span>"mode"</span><span>, </span><span>"正常"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>当前模式：</span><span>#context</span><span> </span><span>mode</span><span>.</span><span>get</span><span>()</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#mode</span><span>.</span><span>update</span><span>(</span><span>"加粗"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#let</span><span> </span><span>tagged</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>10</div></div><div><span>  </span><span>if</span><span> </span><span>mode</span><span>.</span><span>get</span><span><span>() </span><span>==</span><span> </span></span><span>"加粗"</span><span> { </span><span>strong</span><span>(</span><span>body</span><span>) } </span><span>else</span><span> { </span><span>body</span><span> }</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div><span>#tagged</span><span>[在“加粗”模式下]</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#mode</span><span>.</span><span>update</span><span>(</span><span>"正常"</span><span>)</span></div></div><div><div><div>15</div></div><div><span>#tagged</span><span>[回到正常模式]</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>// `at` and `final`.</span></div></div><div><div><div>18</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>最终模式：</span><span>#mode</span><span>.</span><span>final</span><span>()。</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>第 </span><span>#here</span><span>().</span><span>page</span><span>() 页开头的模式：</span><span>#mode</span><span>.</span><span>at</span><span>(</span><span>&lt;top&gt;</span><span>)。</span></div></div><div><div><div>21</div></div><div><span>]</span></div></div><div><div><div>22</div></div><div><span>#metadata</span><span>(</span><span>none</span><span>) </span><span>&lt;top&gt;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>// A running maximum, updated with a function.</span></div></div><div><div><div>25</div></div><div><span>#let</span><span> </span><span>best</span><span><span> </span><span>= </span></span><span>state</span><span>(</span><span>"best"</span><span>, </span><span>0</span><span>)</span></div></div><div><div><div>26</div></div><div><span>#for</span><span> </span><span>n</span><span> </span><span>in</span><span> (</span><span>3</span><span>, </span><span>9</span><span>, </span><span>4</span><span>) { </span><span>best</span><span>.</span><span>update</span><span>(</span><span>b</span><span> </span><span>=&gt;</span><span> </span><span>calc</span><span>.</span><span>max</span><span>(</span><span>b</span><span>, </span><span>n</span><span>)) }</span></div></div><div><div><div>27</div></div><div><span>最大值：</span><span>#context</span><span> </span><span>best</span><span>.</span><span>get</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>当前模式：正常</span></div></div><div><div><div>2</div></div><div><span>在“加粗”模式下</span></div></div><div><div><div>3</div></div><div><span>回到正常模式</span></div></div><div><div><div>4</div></div><div><span>最终模式：正常。 第 1 页开头的模式：正常。</span></div></div><div><div><div>5</div></div><div><span>最大值：9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdftotext</code> 看不出粗体，但第二行在 PDF 里是粗体：<code>tagged</code> 在 <code>mode.get()</code> 为“加粗”时用 <code>strong</code>，它放在 <code>update("加粗")</code> 之后、<code>update("正常")</code> 之前，所以只有它加粗。这个例子里 <code>tagged</code> 是<strong>在 <code>update</code> 之后才定义的函数</strong>，却能读到 <code>update</code> 的结果，这又是流水账：函数体在放置处求值，那时账已经记到了那一行。<code>best.update(b =&gt; calc.max(b, n))</code> 是带函数的更新，循环三次后最大值 9。</p><section><h3>update 什么时候生效<a href="#update-什么时候生效"><span>#</span></a></h3><p>state 最容易让人困惑的是“我已经 <code>update</code> 了为什么读出来还是旧的”。我在 <code>/tmp/p7/st.typ</code> 里把几种情形放在一起：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>s</span><span><span> </span><span>= </span></span><span>state</span><span>(</span><span>"s"</span><span>, </span><span>0</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>x</span><span><span> </span><span>= </span></span><span>s</span><span>.</span><span>update</span><span>(</span><span>5</span><span>)</span></div></div><div><div><div>4</div></div><div><span>未放置：</span><span>#context</span><span> </span><span>s</span><span>.</span><span>get</span><span><span>() </span><span>\</span></span></div></div><div><div><div>5</div></div><div><span>#x</span></div></div><div><div><div>6</div></div><div><span>放置之后：</span><span>#context</span><span> </span><span>s</span><span>.</span><span>get</span><span><span>() </span><span>\</span></span></div></div><div><div><div>7</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>s</span><span>.</span><span>update</span><span>(</span><span>9</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>[同一个 context 里 update 之后立刻读：</span><span>#s</span><span>.</span><span>get</span><span>()]</span></div></div><div><div><div>10</div></div><div><span>} </span><span>\</span></div></div><div><div><div>11</div></div><div><span>下一个 context：</span><span>#context</span><span> </span><span>s</span><span>.</span><span>get</span><span>()</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#let</span><span> </span><span>log</span><span><span> </span><span>= </span></span><span>state</span><span>(</span><span>"log"</span><span>, ())</span></div></div><div><div><div>14</div></div><div><span>#let</span><span> </span><span>note</span><span>(</span><span>t</span><span><span>) </span><span>= </span></span><span>log</span><span>.</span><span>update</span><span>(</span><span>l</span><span> </span><span>=&gt;</span><span> </span><span>l</span><span><span> </span><span>+</span><span> (</span></span><span>t</span><span>,))</span></div></div><div><div><div>15</div></div><div><span>#note</span><span>(</span><span>"甲"</span><span>) </span><span>#note</span><span>(</span><span>"乙"</span><span>)</span></div></div><div><div><div>16</div></div><div><span>#context</span><span> [此刻：</span><span>#log</span><span>.</span><span>get</span><span>().</span><span>join</span><span>(</span><span>"、"</span><span>)]</span></div></div><div><div><div>17</div></div><div><span>#note</span><span>(</span><span>"丙"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>此刻：</span><span>#log</span><span>.</span><span>get</span><span>().</span><span>join</span><span>(</span><span>"、"</span><span>) </span><span>\</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>最终：</span><span>#log</span><span>.</span><span>final</span><span>().</span><span>join</span><span>(</span><span>"、"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>未放置：0</span></div></div><div><div><div>2</div></div><div><span>放置之后：5</span></div></div><div><div><div>3</div></div><div><span>同一个 context 里 update 之后立刻读：5</span></div></div><div><div><div>4</div></div><div><span>下一个 context：9</span></div></div><div><div><div>5</div></div><div><span>此刻：甲、乙 此刻：甲、乙、丙</span></div></div><div><div><div>6</div></div><div><span>最终：甲、乙、丙</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>let x = s.update(5)</code> 只是创建了一条“更新指令”，值是 content。在 <code>#x</code> 把它放进文档之前，<code>s.get()</code> 读到 0；放置之后读到 5。<strong>没被放进文档的 update 等于没写</strong>，这也是在函数里调用 <code>s.update(...)</code> 却忘了返回它的典型症状。</li>
<li><code>context { s.update(9); [...#s.get()] }</code> 在 <code>update(9)</code> 之后立刻读，读到的是 5 而不是 9，和上面嵌套 context 的结论一致：这块 context 在开始处读世界，里面的 <code>update</code> 影响的是它之后的内容。下一个 context 就读到 9。</li>
<li>最后一组是账本的样子：同样的 <code>log.get().join("、")</code>，第一次放在两条记录之后，读到“甲、乙”；放在第三条之后，读到“甲、乙、丙”。<code>final()</code> 不管放在哪里都是整本账。</li>
</ul></section><section><h3>counter 还是 state<a href="#counter-还是-state"><span>#</span></a></h3><p>我现在的选择标准很粗：值是递增的整数，而且想用 <code>numbering</code> 格式化，就用 counter（定理编号、脚注编号都是）；值是别的类型，或者更新规则是“取最大”“追加一项”这种，用 state。官方文档对 state 有一段警告，说它容易被滥用，因为 <code>update</code> 的结果依赖文档顺序，必须反复排版才能稳定，我的理解是能用 <code>query</code> 一次算出来的就不要用 state 去累加，上面的发散例子就是这类滥用的极端形式。</p></section></section>
<section><h2>query 与 metadata：问文档“里面有什么”<a href="#query-与-metadata问文档里面有什么"><span>#</span></a></h2><p><code>counter</code> 和 <code>state</code> 问的是“某处的值”，<code>query</code> 问的是“文档里有哪些元素”。<code>lab/ch07/query.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/query.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// `query` returns every element that matches a selector, document-wide.</span></div></div><div><div><div>5</div></div><div><span>#outline</span><span>(</span><span>title</span><span>: [自制目录], </span><span>target</span><span>: </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>))</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>=</span><span> 简介</span></div></div><div><div><div>8</div></div><div><span>=</span><span> 方法 </span><span>&lt;methods&gt;</span></div></div><div><div><div>9</div></div><div><span>==</span><span> 细节</span></div></div><div><div><div>10</div></div><div><span>=</span><span> 结果</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>13</div></div><div><span>  </span><span>let</span><span> </span><span>all</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>heading</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>[文档里有 </span><span>#all</span><span>.</span><span>len</span><span>() 个标题：</span><span>#all</span><span>.</span><span>map</span><span>(</span><span>h</span><span> </span><span>=&gt;</span><span> </span><span>h</span><span>.</span><span>body</span><span>).</span><span>join</span><span>(</span><span>"、"</span><span>)。]</span></div></div><div><div><div>15</div></div><div><span>  </span><span>linebreak</span><span>()</span></div></div><div><div><div>16</div></div><div><span>  </span><span>let</span><span> </span><span>after</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>).</span><span>after</span><span>(</span><span>&lt;methods&gt;</span><span>))</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>[</span><span>`&lt;methods&gt;`</span><span> 之后有 </span><span>#after</span><span>.</span><span>len</span><span>() 个一级标题，最后一个在第</span></div></div><div><div><div>18</div></div><div><span>    </span><span>#after</span><span>.</span><span>last</span><span>().</span><span>location</span><span>().</span><span>page</span><span>() 页。]</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>// `metadata` carries arbitrary data to a query without printing anything.</span></div></div><div><div><div>22</div></div><div><span>#metadata</span><span>((</span><span>author</span><span>: </span><span>"bfmhno3"</span><span>)) </span><span>&lt;meta&gt;</span></div></div><div><div><div>23</div></div><div><span>#context</span><span> </span><span>query</span><span>(</span><span>&lt;meta&gt;</span><span>).</span><span>first</span><span>().</span><span>value</span><span>.</span><span>author</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>自制目录</span></div></div><div><div><div>2</div></div><div><span>1 简介 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div><div><div><div>3</div></div><div><span>2 方法 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div><div><div><div>4</div></div><div><span>3 结果 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ⁠1</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>1 简介</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>2 方法</span></div></div><div><div><div>9</div></div><div><span>2.1 细节</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>3 结果</span></div></div><div><div><div>12</div></div><div><span>文档里有 5 个标题：自制目录、简介、方法、细节、结果。</span></div></div><div><div><div>13</div></div><div><span>&lt;methods&gt; 之后有 2 个一级标题，最后一个在第 1 页。</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>bfmhno3</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（目录里的点线我缩短了，其余原样。）有几个细节：</p><ul>
<li><code>outline</code> 本身就是一次 <code>query</code>：<code>target: heading.where(level: 1)</code> 只收一级标题，所以目录里没有“细节”。</li>
<li>文档里“有 5 个标题”，比肉眼看到的多一个：<code>outline(title: [自制目录])</code> 的标题也是 <code>heading</code>（只是 <code>outlined: false</code>），<code>query(heading)</code> 把它一起算进来了。我第一次数出 5 而不是 4 愣了一下。</li>
<li><code>after(&lt;methods&gt;)</code> 默认包含锚点本身，所以“方法”“结果”两个一级标题都在里面。我另外试了 <code>/tmp/p7/incl.typ</code>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=</span><span> 甲</span></div></div><div><div><div>2</div></div><div><span>=</span><span> 乙 </span><span>&lt;m&gt;</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 丙</span></div></div><div><div><div>4</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>含锚点：</span><span>#query</span><span>(</span><span>selector</span><span>(</span><span>heading</span><span>).</span><span>after</span><span>(</span><span>&lt;m&gt;</span><span>)).</span><span>len</span><span>() </span><span>\</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>不含：</span><span>#query</span><span>(</span><span>selector</span><span>(</span><span>heading</span><span>).</span><span>after</span><span>(</span><span>&lt;m&gt;</span><span>, </span><span>inclusive</span><span>: </span><span>false</span><span>)).</span><span>len</span><span>()</span></div></div><div><div><div>7</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>含锚点：2</span></div></div><div><div><div>2</div></div><div><span>不含：1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意要写 <code>selector(heading).after(...)</code>：<code>heading.after</code> 不存在，报错是 <code>function `heading` does not contain field `after` </code>。第 4 篇讲的 <code>before</code>/<code>after</code>/<code>within</code> 选择器不能驱动 <code>show</code>，现在你知道它们该在哪用了：<code>query</code> 里。</p><ul>
<li><code>metadata</code> 是一个不输出任何内容的元素，唯一的用途是把一份数据挂在一个位置上，让别处的 <code>query</code> 取走。<code>query(&lt;meta&gt;).first().value.author</code> 取回 <code>bfmhno3</code>。第 12 篇的作业模板就是这样把每道题的分值藏在 <code>metadata</code> 里，再在页眉块里 <code>query</code> 出来求和。</li>
</ul><section><h3>运行页眉：<code>lab/ch07/running-header.typ</code><a href="#运行页眉labch07running-headertyp"><span>#</span></a></h3><p>把 <code>here()</code> 和 <code>query</code> 合起来，就能做“页眉跟随章节”。<code>lab/ch07/running-header.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/running-header.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span></div></div><div><div><div>2</div></div><div><span>  </span><span>width</span><span>: </span><span>12cm</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>height</span><span>: </span><span>7cm</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>margin</span><span>: (</span><span>top</span><span>: </span><span>2cm</span><span>, </span><span>rest</span><span>: </span><span>1cm</span><span>),</span></div></div><div><div><div>5</div></div><div><span>  </span><span>header</span><span>: </span><span>context</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>// The header sees the document as of its own page.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>here-page</span><span><span> </span><span>= </span></span><span>here</span><span>().</span><span>page</span><span>()</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>starts</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>)).</span><span>any</span><span>(</span><span>h</span><span> </span><span>=&gt;</span><span> (</span></div></div><div><div><div>9</div></div><div><span>      </span><span>h</span><span>.</span><span>location</span><span>().</span><span>page</span><span><span>() </span><span>==</span><span> </span></span><span>here-page</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>))</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> </span><span>not</span><span> </span><span>starts</span><span> {</span></div></div><div><div><div>12</div></div><div><span>      </span><span>let</span><span> </span><span>before</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>).</span><span>before</span><span>(</span><span>here</span><span>()))</span></div></div><div><div><div>13</div></div><div><span>      </span><span>if</span><span> </span><span>before</span><span>.</span><span>len</span><span><span>() </span><span>&gt;</span><span> </span></span><span>0</span><span> { </span><span>emph</span><span>(</span><span>before</span><span>.</span><span>last</span><span>().</span><span>body</span><span>) }</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>16</div></div><div><span>)</span></div></div><div><div><div>17</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1."</span><span>)</span></div></div><div><div><div>18</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>pagebreak</span><span>(</span><span>weak</span><span>: </span><span>true</span><span><span>) </span><span>+</span><span> </span></span><span>it</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>=</span><span> 第一章</span></div></div><div><div><div>21</div></div><div><span>#lorem</span><span>(</span><span>80</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>=</span><span> 第二章</span></div></div><div><div><div>24</div></div><div><span>#lorem</span><span>(</span><span>120</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>header:</code> 接受一个 content，我给的是一个 <code>context</code> 块，所以它在每一页各被求值一次，每次 <code>here()</code> 是那一页页眉的位置。逻辑分两步：先 <code>query</code> 出所有一级标题，看有没有哪个落在当前页（<code>h.location().page() == here-page</code>），有就不显示页眉，章首页不需要；没有则取“当前位置之前最近的一个一级标题”，用 <code>before(here())</code> 选出来，<code>.last()</code> 就是它。</p><p>渲染（4 页，我把 4 页并排放进一张图里）：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch07-running-header.png" alt="running-header.typ 的 4 页并排：第 1 页和第 3 页分别以“第一章”“第二章”标题开头，页眉空白；第 2 页和第 4 页是续页，左上角分别写着斜体的“第一章”“第二章”" /><figcaption>running-header.typ 的 4 页并排：第 1 页和第 3 页分别以“第一章”“第二章”标题开头，页眉空白；第 2 页和第 4 页是续页，左上角分别写着斜体的“第一章”“第二章”</figcaption></figure><p></p><p>第 1、3 页是章首页，没有页眉；第 2、4 页的页眉分别是“第一章”“第二章”。这个文档编译了 3 遍。</p></section><section><h3><code>xuan-header</code> 与 <code>xuan-footer</code><a href="#xuan-header-与-xuan-footer"><span>#</span></a></h3><p><code>xuan</code> 的页眉在这个思路上多了两样东西：右边放文档标题，下面加一条细线。<code>src/header.typ</code> 整个文件如下（27 行，<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/header.typ" target="_blank">源文件</a>），这是最终版：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Running header: current chapter on the left, document title on the right.</span></div></div><div><div><div>2</div></div><div><span>/// Suppressed on pages that start a level-1 heading.</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>xuan-header</span><span><span>() </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>let</span><span> </span><span>starts</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>)).</span><span>any</span><span>(</span><span>h</span><span> </span><span>=&gt;</span><span> (</span></div></div><div><div><div>5</div></div><div><span>    </span><span>h</span><span>.</span><span>location</span><span>().</span><span>page</span><span><span>() </span><span>==</span><span> </span></span><span>here</span><span>().</span><span>page</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>))</span></div></div><div><div><div>7</div></div><div><span>  </span><span>if</span><span> </span><span>not</span><span> </span><span>starts</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>before</span><span><span> </span><span>= </span></span><span>query</span><span>(</span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>).</span><span>before</span><span>(</span><span>here</span><span>()))</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>chapter</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>before</span><span>.</span><span>len</span><span><span>() </span><span>&gt;</span><span> </span></span><span>0</span><span> { </span><span>before</span><span>.</span><span>last</span><span>().</span><span>body</span><span> } </span><span>else</span><span> { </span><span>none</span><span> }</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>title</span><span><span> </span><span>= </span></span><span>document</span><span>.</span><span>title</span></div></div><div><div><div>11</div></div><div><span>    </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>0.85em</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>grid</span><span>(</span></div></div><div><div><div>13</div></div><div><span>      </span><span>columns</span><span>: (</span><span>1fr</span><span>, </span><span>1fr</span><span>),</span></div></div><div><div><div>14</div></div><div><span>      </span><span>align</span><span>(</span><span>left</span><span>, </span><span>chapter</span><span>), </span><span>align</span><span>(</span><span>right</span><span>, </span><span>title</span><span>),</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>v</span><span><span>(</span><span>-</span></span><span>0.5em</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>line</span><span>(</span><span>length</span><span>: </span><span>100%</span><span>, </span><span>stroke</span><span>: </span><span>0.4pt</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>/// Footer: `第 x 页 / 共 y 页` (zh) or `x / y` (en).</span></div></div><div><div><div>22</div></div><div><span>#let</span><span> </span><span>xuan-footer</span><span>(</span><span>lang</span><span>: </span><span>"zh"</span><span><span>) </span><span>= </span></span><span>context</span><span> {</span></div></div><div><div><div>23</div></div><div><span>  </span><span>let</span><span> </span><span>x</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>page</span><span>).</span><span>get</span><span>().</span><span>first</span><span>()</span></div></div><div><div><div>24</div></div><div><span>  </span><span>let</span><span> </span><span>y</span><span><span> </span><span>= </span></span><span>counter</span><span>(</span><span>page</span><span>).</span><span>final</span><span>().</span><span>first</span><span>()</span></div></div><div><div><div>25</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>0.85em</span><span>)</span></div></div><div><div><div>26</div></div><div><span>  </span><span>align</span><span>(</span><span>center</span><span>, </span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [第 </span><span>#x</span><span> 页 / 共 </span><span>#y</span><span> 页] </span><span>else</span><span> [</span><span>#x</span><span> / </span><span>#y</span><span>])</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>和 lab 里的版本相比：</p><ul>
<li><code>xuan-header()</code> 是一个<strong>返回 content 的函数</strong>，不是 content 本身。<code>note</code> 里写 <code>set page(header: xuan-header())</code>，调用发生在 <code>set</code> 的时候，得到的是一块 <code>context</code>，之后每一页再各自求值一次，和上面讲的“一块 context 被放在多处”完全一致。</li>
<li>右边是 <code>document.title</code>。<code>document</code> 的字段也是 context 才能读的样式值，<code>note</code> 里的 <code>set document(title: title, ...)</code> 在页眉求值之前就登记好了。左边的章名来自 <code>query</code>；文档第一个一级标题之前，<code>chapter</code> 是 <code>none</code>，<code>align(left, none)</code> 什么也不画，页眉只剩标题。我用 <code>/tmp/p7/pre.typ</code>（<code>note</code> 加 1500 词的 <code>lorem</code>，再跟一个一级标题）排了一遍，第 1 页页眉右上角确实只有标题。</li>
<li><code>grid(columns: (1fr, 1fr), ...)</code> 让左右各占一半，<code>v(-0.5em)</code> 把细线往上收一点，<code>line(length: 100%, stroke: 0.4pt)</code> 画 0.4pt 的横线。</li>
<li><code>xuan-footer</code> 用 <code>counter(page).get().first()</code> 和 <code>counter(page).final().first()</code> 拿到“当前页”和“总页数”，这就是上面 <code>final()</code> 的用途。总页数在第一遍是占位，之后收敛。中文版是 <code>第 x 页 / 共 y 页</code>，英文版是 <code>x / y</code>。</li>
</ul><p>在 <code>examples/notes/main.typ</code> 的 4 页输出里，第 2 页的页眉是 <code>最小二乘</code> 在左、<code>线性代数笔记：最小二乘与 SVD</code> 在右（<code>pdftotext -layout</code> 的一行，我用 <code>grep</code> 看过）。整个笔记编译 4 遍。</p></section></section>
<section><h2>定理环境：为什么 <code>show ref</code> 要读 <code>el.location()</code><a href="#定理环境为什么-show-ref-要读-ellocation"><span>#</span></a></h2><p>页眉只读不写，定理环境要写：它要自己维护一个计数器，每章清零，并且让 <code>@label</code> 引用能打出“定理 1.2”。先看 <code>lab/ch07/theorems.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch07/theorems.typ" target="_blank">源文件</a>）怎么用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>note</span><span>.</span><span>with</span><span>(</span><span>title</span><span>: </span><span>"定理环境"</span><span>, </span><span>author</span><span>: </span><span>"bfmhno3"</span><span>, </span><span>date</span><span>: </span><span>datetime</span><span>(</span></div></div><div><div><div>4</div></div><div><span>  </span><span>year</span><span>: </span><span>2026</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>month</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>day</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>7</div></div><div><span>))</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>=</span><span> 向量空间</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#definition</span><span>(</span></div></div><div><div><div>12</div></div><div><span>  </span><span>title</span><span>: </span><span>"线性无关"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>)[若 </span><span>$</span><span>sum</span><span> c_i v_i = 0</span><span>$</span><span> 蕴含所有 </span><span>$</span><span>c_i = 0</span><span>$</span><span>，则称 </span><span>$</span><span>v_i</span><span>$</span><span> 线性无关。] </span><span>&lt;def-li&gt;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>#theorem</span><span>(</span><span>title</span><span>: </span><span>"秩–零化度"</span><span>)[</span><span>$</span><span>"rank"</span><span>(A) + </span><span>dim</span><span> </span><span>ker</span><span>(A) = n</span><span>$</span><span>。] </span><span>&lt;thm-rn&gt;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>#proof</span><span>[略。]</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>=</span><span> 正交性</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#lemma</span><span>[正交向量线性无关。] </span><span>&lt;lem-orth&gt;</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>见 </span><span>@def-li</span><span>、</span><span>@thm-rn</span><span> 与 </span><span>@lem-orth</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出（<code>pdftotext -layout</code> 的正文部分）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 向量空间</span></div></div><div><div><div>2</div></div><div><span>定义 1.1 (线性无关). 若 ∑ 𝑐𝑖 𝑣𝑖 = 0 蕴含所有 𝑐𝑖 = 0，则称 𝑣𝑖 线性无关。</span></div></div><div><div><div>3</div></div><div><span>定理 1.2 (秩–零化度). rank(𝐴) + dim ker(𝐴) = 𝑛。</span></div></div><div><div><div>4</div></div><div><span>证明. 略。                                                         ∎</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>2 正交性</span></div></div><div><div><div>7</div></div><div><span>引理 2.1. 正交向量线性无关。</span></div></div><div><div><div>8</div></div><div><span><span>         </span></span><span>见 定义 1.1、定理 1.2 与 引理 2.1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意三点：定义和定理共用一个计数器（定义 1.1 之后是定理 1.2，不是“定理 1.1”）；证明不编号；第二章的第一个引理从 2.1 重新开始，不是 1.3。</p><p><code>fig-theorem.typ</code> 渲染出来是这样：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch07-theorem.png" alt="fig-theorem.typ 的渲染：一级标题“向量空间”下有“定义 1.1 (线性无关)”“定理 1.2 (秩零化度)”和以黑色方块结尾的证明，证明里的“定义 1.1”是蓝色链接；一级标题“正交性”下有“引理 2.1”，其中“定理 1.2”是蓝色斜体链接" /><figcaption>fig-theorem.typ 的渲染：一级标题“向量空间”下有“定义 1.1 (线性无关)”“定理 1.2 (秩零化度)”和以黑色方块结尾的证明，证明里的“定义 1.1”是蓝色链接；一级标题“正交性”下有“引理 2.1”，其中“定理 1.2”是蓝色斜体链接</figcaption></figure><p></p><section><h3><code>src/theorem.typ</code><a href="#srctheoremtyp"><span>#</span></a></h3><p>这个文件有 78 行，下面是完整的最终版（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/theorem.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Numbered environments (theorem, lemma, ...) that share one counter,</span></div></div><div><div><div>2</div></div><div><span>// restart at every level-1 heading and can be referenced with `@label`.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>theorem-kind</span><span><span> </span><span>= </span></span><span>"xuan-theorem"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/// "&lt;chapter&gt;.&lt;n&gt;" for the environment figure `el`.</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>env-number</span><span>(</span><span>el</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>let</span><span> </span><span>loc</span><span><span> </span><span>= </span></span><span>el</span><span>.</span><span>location</span><span>()</span></div></div><div><div><div>9</div></div><div><span>  </span><span>numbering</span><span>(</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"1.1"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>12</div></div><div><span>    </span><span>el</span><span>.</span><span>counter</span><span>.</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(),</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>/// Build an environment function `(title: none, body)`.</span></div></div><div><div><div>17</div></div><div><span>#let</span><span> </span><span>make-env</span><span>(</span><span>supplement-zh</span><span>, </span><span>supplement-en</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>(</span><span>title</span><span>: </span><span>none</span><span>, </span><span>body</span><span>) </span><span>=&gt;</span><span> </span><span>figure</span><span>(</span></div></div><div><div><div>19</div></div><div><span>    </span><span>kind</span><span>: </span><span>theorem-kind</span><span>,</span></div></div><div><div><div>20</div></div><div><span>    </span><span>supplement</span><span>: </span><span>context</span><span> {</span></div></div><div><div><div>21</div></div><div><span>      </span><span>if</span><span> </span><span>text</span><span>.</span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> { </span><span>supplement-zh</span><span> } </span><span>else</span><span> { </span><span>supplement-en</span><span> }</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>23</div></div><div><span>    </span><span>caption</span><span>: </span><span>title</span><span>,</span></div></div><div><div><div>24</div></div><div><span>    </span><span>outlined</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>25</div></div><div><span>    </span><span>gap</span><span>: </span><span>0pt</span><span>,</span></div></div><div><div><div>26</div></div><div><span>    </span><span>body</span><span>,</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>#let</span><span> </span><span>theorem</span><span><span> </span><span>= </span></span><span>make-env</span><span>(</span><span>"定理"</span><span>, </span><span>"Theorem"</span><span>)</span></div></div><div><div><div>31</div></div><div><span>#let</span><span> </span><span>lemma</span><span><span> </span><span>= </span></span><span>make-env</span><span>(</span><span>"引理"</span><span>, </span><span>"Lemma"</span><span>)</span></div></div><div><div><div>32</div></div><div><span>#let</span><span> </span><span>corollary</span><span><span> </span><span>= </span></span><span>make-env</span><span>(</span><span>"推论"</span><span>, </span><span>"Corollary"</span><span>)</span></div></div><div><div><div>33</div></div><div><span>#let</span><span> </span><span>definition</span><span><span> </span><span>= </span></span><span>make-env</span><span>(</span><span>"定义"</span><span>, </span><span>"Definition"</span><span>)</span></div></div><div><div><div>34</div></div><div><span>#let</span><span> </span><span>example</span><span><span> </span><span>= </span></span><span>make-env</span><span>(</span><span>"例"</span><span>, </span><span>"Example"</span><span>)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>/// Unnumbered proof ending with a right-aligned ∎.</span></div></div><div><div><div>37</div></div><div><span>#let</span><span> </span><span>proof</span><span>(</span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>38</div></div><div><span>  </span><span>block</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>above</span><span>: </span><span>0.8em</span><span>, </span><span>below</span><span>: </span><span>0.8em</span><span>, {</span></div></div><div><div><div>39</div></div><div><span>    </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>emph</span><span>(</span><span>context</span><span> </span><span>if</span><span> </span><span>text</span><span>.</span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [证明.] </span><span>else</span><span> [Proof.])</span></div></div><div><div><div>41</div></div><div><span>    </span><span>h</span><span>(</span><span>0.5em</span><span>)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>body</span></div></div><div><div><div>43</div></div><div><span>    </span><span>h</span><span>(</span><span>1fr</span><span>)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>sym</span><span>.</span><span>qed</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>})</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>/// Show rules for the environments; applied by `xuan-base`.</span></div></div><div><div><div>49</div></div><div><span>#let</span><span> </span><span>theorem-rules</span><span>(</span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>50</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>51</div></div><div><span>    </span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>theorem-kind</span><span>)).</span><span>update</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>it</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>54</div></div><div><span>  </span><span>show</span><span> </span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>theorem-kind</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>block</span><span>(</span></div></div><div><div><div>55</div></div><div><span>    </span><span>width</span><span>: </span><span>100%</span><span>,</span></div></div><div><div><div>56</div></div><div><span>    </span><span>above</span><span>: </span><span>0.8em</span><span>,</span></div></div><div><div><div>57</div></div><div><span>    </span><span>below</span><span>: </span><span>0.8em</span><span>,</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>59</div></div><div><span>      </span><span>set</span><span> </span><span>align</span><span>(</span><span>left</span><span>)</span></div></div><div><div><div>60</div></div><div><span>      </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>61</div></div><div><span>      </span><span>strong</span><span>[</span><span>#it</span><span>.</span><span>supplement</span><span> </span><span>#env-number</span><span>(</span><span>it</span><span>)]</span></div></div><div><div><div>62</div></div><div><span>      </span><span>if</span><span> </span><span>it</span><span>.</span><span>caption</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> [ (</span><span>#it</span><span>.</span><span>caption</span><span>.</span><span>body</span><span>)]</span></div></div><div><div><div>63</div></div><div><span><span>      </span></span><span>[.]</span></div></div><div><div><div>64</div></div><div><span>      </span><span>h</span><span>(</span><span>0.5em</span><span>)</span></div></div><div><div><div>65</div></div><div><span>      </span><span>emph</span><span>(</span><span>it</span><span>.</span><span>body</span><span>)</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>67</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>68</div></div><div><span>  </span><span>show</span><span> </span><span>ref</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>69</div></div><div><span>    </span><span>let</span><span> </span><span>el</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>element</span></div></div><div><div><div>70</div></div><div><span>    </span><span>if</span><span> </span><span>el</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>func</span><span><span>() </span><span>==</span><span> </span></span><span>figure</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>kind</span><span><span> </span><span>==</span><span> </span></span><span>theorem-kind</span><span> {</span></div></div><div><div><div>71</div></div><div><span>      </span><span>let</span><span> </span><span>supp</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>it</span><span>.</span><span>supplement</span><span><span> </span><span>==</span><span> </span></span><span>auto</span><span> { </span><span>el</span><span>.</span><span>supplement</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>72</div></div><div><span>        </span><span>it</span><span>.</span><span>supplement</span></div></div><div><div><div>73</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>74</div></div><div><span>      </span><span>link</span><span>(</span><span>el</span><span>.</span><span>location</span><span>(), [</span><span>#supp</span><span>~</span><span>#env-number</span><span>(</span><span>el</span><span>)])</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> { </span><span>it</span><span> }</span></div></div><div><div><div>76</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>77</div></div><div><span>  </span><span>body</span></div></div><div><div><div>78</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我把设计拆成五个决定。</p><p><strong>1. 环境就是 <code>figure</code>。</strong> 所有环境函数都返回 <code>figure(kind: "xuan-theorem", ...)</code>。Typst 的 <code>figure</code> 自带“同 kind 的 figure 共用一个计数器”、“可以被 <code>@label</code> 引用”、“有 supplement（引用时的前缀词）”这三样东西，定理环境需要的全有，所以不用自己造轮子。<code>caption: title</code> 把定理的标题塞进 caption 槽，<code>outlined: false</code> 不让它们出现在 <code>outline(target: figure)</code> 里，<code>gap: 0pt</code> 去掉 body 与 caption 之间的间距。五种环境共用同一个 <code>kind</code>，所以共用一个计数器：定义 1.1 之后是定理 1.2。</p><p><strong>2. supplement 是 <code>context</code>。</strong> <code>supplement: context { if text.lang == "zh" ... }</code> 根据语言选“定理”或 “Theorem”。<code>xuan-base</code> 通过 <code>xuan-cjk</code> 把 <code>lang</code> 设进 <code>text</code>，这里读 <code>text.lang</code> 才能跟着走，所以必须推迟到 context 里。</p><p><strong>3. 每章清零。</strong> <code>show heading.where(level: 1): it =&gt; { counter(...).update(0); it }</code> 在每个一级标题前面放一条“把定理计数器清零”的指令，再输出标题本身。<code>counter(figure.where(kind: theorem-kind))</code> 选中的就是这些环境的计数器。这里的 <code>update(0)</code> 和上面 <code>step</code>/<code>update</code> 的行为完全一样：是放进文档的一条指令，在排版时按文档顺序执行。</p><p><strong>4. 编号由 <code>env-number</code> 算：<code>counter(heading).at(loc)</code> 加 <code>el.counter.at(loc)</code>。</strong> 章号取标题计数器在这个环境所在位置的值，序号取环境自己的计数器在同一位置的值，用 <code>numbering("1.1", 章, 序)</code> 拼成 <code>1.2</code>。这里的关键是 <code>loc = el.location()</code>：不管谁来调用 <code>env-number</code>，答案都由环境元素自己的位置决定。</p><p><strong>5. <code>show ref</code> 同样读 <code>el.location()</code>。</strong> 重点来了。<code>@thm-rn</code> 本来就能引用 <code>figure</code>，为什么还要重写 <code>show ref</code>？我实验了三版。</p><section><h4>实验一：去掉 <code>show ref</code><a href="#实验一去掉-show-ref"><span>#</span></a></h4><p>我把 <code>theorem.typ</code> 拷成 <code>/tmp/p7/noref.typ</code>，删掉 <code>show ref</code> 那一段，其余不动，再写一份 <code>/tmp/p7/noref-test.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"noref.typ"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>theorem-rules</span></div></div><div><div><div>5</div></div><div><span>=</span><span> 甲</span></div></div><div><div><div>6</div></div><div><span>#theorem</span><span>[一] </span><span>&lt;a&gt;</span></div></div><div><div><div>7</div></div><div><span>#lemma</span><span>[二] </span><span>&lt;b&gt;</span></div></div><div><div><div>8</div></div><div><span>=</span><span> 乙</span></div></div><div><div><div>9</div></div><div><span>#theorem</span><span>[三] </span><span>&lt;c&gt;</span></div></div><div><div><div>10</div></div><div><span>见 </span><span>@a</span><span>、</span><span>@b</span><span>、</span><span>@c</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1甲</span></div></div><div><div><div>2</div></div><div><span>Theorem 1.1. 一</span></div></div><div><div><div>3</div></div><div><span>Lemma 1.2. 二</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>2乙</span></div></div><div><div><div>6</div></div><div><span>Theorem 2.1. 三</span></div></div><div><div><div>7</div></div><div><span>见 Theorem 1、Lemma 2、Theorem 1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（这里没有设语言，所以 supplement 是英文。）定理本身的编号是对的（<code>env-number</code> 在起作用），可引用成了 <code>Theorem 1</code>、<code>Lemma 2</code>、<code>Theorem 1</code>：默认的 <code>ref</code> 只用 figure 自己的编号，没有章号，而且 <code>@a</code> 和 <code>@c</code> 变成了同一个“Theorem 1”。在这种情况下，写 <code>@c</code> 的读者去找“Theorem 1”会找到两处。</p></section><section><h4>实验二：最直觉的修法，让 <code>numbering</code> 去读标题计数器<a href="#实验二最直觉的修法让-numbering-去读标题计数器"><span>#</span></a></h4><p>既然 figure 的 <code>numbering</code> 可以是函数，能不能在函数里直接读 <code>counter(heading).get()</code> 拼出章号？<code>/tmp/p7/hack.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>"thm"</span><span>)).</span><span>update</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>it</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>thm</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>figure</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>kind</span><span>: </span><span>"thm"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>supplement</span><span>: [定理],</span></div></div><div><div><div>10</div></div><div><span>  </span><span>numbering</span><span>: </span><span>n</span><span> </span><span>=&gt;</span><span> </span><span>context</span><span> </span><span>numbering</span><span>(</span><span>"1.1"</span><span>, </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>get</span><span>().</span><span>first</span><span>(), </span><span>n</span><span>),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>body</span><span>,</span></div></div><div><div><div>12</div></div><div><span>)</span></div></div><div><div><div>13</div></div><div><span>=</span><span> 甲</span></div></div><div><div><div>14</div></div><div><span>#thm</span><span>[一] </span><span>&lt;a&gt;</span></div></div><div><div><div>15</div></div><div><span>=</span><span> 乙</span></div></div><div><div><div>16</div></div><div><span>#thm</span><span>[二] </span><span>&lt;b&gt;</span></div></div><div><div><div>17</div></div><div><span>见 </span><span>@a</span><span> 与 </span><span>@b</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1甲</span></div></div><div><div><div>2</div></div><div><span><span>                     </span></span><span>一</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>2乙</span></div></div><div><div><div>5</div></div><div><span><span>                     </span></span><span>二</span></div></div><div><div><div>6</div></div><div><span>见 定理 2.1 与 定理 2.1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>@a</code> 指的明明是第一章的定理，却显示成了 <code>2.1</code>：<code>ref</code> 在<strong>引用所在的位置</strong>调用 <code>numbering</code>，那时标题计数器是 2，所以第一章的定理被印上了第二章的章号。（我猜 <code>ref</code> 的实现就是这样，因为输出与这种读法完全吻合，不过我没有读编译器的源码去证实。）<code>get()</code> 取的是“此刻”，而此刻是引用处，不是定理处。</p></section><section><h4>实验三：读 <code>el.location()</code><a href="#实验三读-ellocation"><span>#</span></a></h4><p>办法是不要在引用处去问“此刻几号”，而是拿到被引用元素的位置，去问<strong>那个位置</strong>的计数器。<code>/tmp/p7/fixed.typ</code> 是 <code>theorem.typ</code> 的缩小版：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>"thm"</span><span>)).</span><span>update</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>it</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>number-of</span><span>(</span><span>el</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>let</span><span> </span><span>loc</span><span><span> </span><span>= </span></span><span>el</span><span>.</span><span>location</span><span>()</span></div></div><div><div><div>9</div></div><div><span>  </span><span>numbering</span><span>(</span><span>"1.1"</span><span>, </span><span>counter</span><span>(</span><span>heading</span><span>).</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>(), </span><span>el</span><span>.</span><span>counter</span><span>.</span><span>at</span><span>(</span><span>loc</span><span>).</span><span>first</span><span>())</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div><span>#let</span><span> </span><span>thm</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>figure</span><span>(</span><span>kind</span><span>: </span><span>"thm"</span><span>, </span><span>supplement</span><span>: [定理], </span><span>body</span><span>)</span></div></div><div><div><div>12</div></div><div><span>#show</span><span> </span><span>figure</span><span>.</span><span>where</span><span>(</span></div></div><div><div><div>13</div></div><div><span>  </span><span>kind</span><span>: </span><span>"thm"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>block</span><span>[*</span><span>#it</span><span>.</span><span>supplement</span><span> </span><span>#number-of</span><span>(</span><span>it</span><span>)* </span><span>#it</span><span>.</span><span>body</span><span>]</span></div></div><div><div><div>15</div></div><div><span>#show</span><span> </span><span>ref</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>16</div></div><div><span>  </span><span>let</span><span> </span><span>el</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>element</span></div></div><div><div><div>17</div></div><div><span>  </span><span>if</span><span> </span><span>el</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>func</span><span><span>() </span><span>==</span><span> </span></span><span>figure</span><span> </span><span>and</span><span> </span><span>el</span><span>.</span><span>kind</span><span><span> </span><span>==</span><span> </span></span><span>"thm"</span><span> {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>link</span><span>(</span><span>el</span><span>.</span><span>location</span><span>(), [</span><span>#el</span><span>.</span><span>supplement</span><span>~</span><span>#number-of</span><span>(</span><span>el</span><span>)])</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> { </span><span>it</span><span> }</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div><span>=</span><span> 甲</span></div></div><div><div><div>22</div></div><div><span>#thm</span><span>[一] </span><span>&lt;a&gt;</span></div></div><div><div><div>23</div></div><div><span>=</span><span> 乙</span></div></div><div><div><div>24</div></div><div><span>#thm</span><span>[二] </span><span>&lt;b&gt;</span></div></div><div><div><div>25</div></div><div><span>见 </span><span>@a</span><span> 与 </span><span>@b</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1甲</span></div></div><div><div><div>2</div></div><div><span><span>                     </span></span><span>定理 1.1 一</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>2乙</span></div></div><div><div><div>5</div></div><div><span><span>                     </span></span><span>定理 2.1 二</span></div></div><div><div><div>6</div></div><div><span>见 定理 1.1 与 定理 2.1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对了。<code>it.element</code> 是 <code>ref</code> 指向的元素，<code>el.location()</code> 是它的位置句柄，<code>counter.at(loc)</code> 是“时间旅行”，无论这条 <code>@label</code> 写在哪一章，答案都一样。<code>xuan</code> 的 <code>theorem.typ</code> 就是这个版本，只是多做了三件事：<code>it.supplement == auto</code> 时用元素自己的 supplement，否则尊重用户写的 <code>@thm-rn[式]</code> 之类的自定义；<code>link(el.location(), ...)</code> 让引用可点击，跳到定理处（所以渲染图里“定义 1.1”是蓝色的）；<code>~</code> 是不换行空格，不让“定理”和“1.2”被拆在两行。</p><p>还有一个条件判断：<code>it.element</code> 在引用文献时是 <code>none</code>，在引用别的元素（标题、公式）时不是 <code>figure</code>，这两类要原样放过去（<code>else { it }</code>），否则整份文档的引用都被这条规则吞掉。我把 <code>el != none and</code> 删掉，拷成 <code>/tmp/p7/nog.typ</code>，再排一份带 <code>@smith</code> 和 <code>#bibliography("refs.bib")</code> 的文档，得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: type none has no method `func`</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>┌─ nog.typ:70:7</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>70 │     if el.func() == figure and el.kind == theorem-kind {</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>│        ^^^^^^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>while showing ref element at nog-test.typ:4:2</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>@smith</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保留判断的版本里 <code>@smith</code> 照常显示为 <code>[1]</code>。第 8、9 篇的 <code>math.typ</code>、<code>figure.typ</code> 里的 <code>show ref</code> 用的是同一个模式。</p><p>两处调用 <code>env-number(el)</code>：定理自己的 show 规则（画“定理 1.2.”那个标题）和 <code>show ref</code>（画引用文字）。它们用同一个函数、同一个 <code>el.location()</code>，所以定理处和引用处的编号永远一致，不可能出现“正文是 1.2，引用成了 1.3”。</p></section></section><section><h3>这个模块怎么测<a href="#这个模块怎么测"><span>#</span></a></h3><p><code>tests/theorem.typ</code> 的 19 行是我对这个模块的回归断言（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/tests/theorem.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>xuan-base</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>=</span><span> 第一章</span></div></div><div><div><div>5</div></div><div><span>#theorem</span><span>[甲] </span><span>&lt;t1&gt;</span></div></div><div><div><div>6</div></div><div><span>#lemma</span><span>[乙] </span><span>&lt;t2&gt;</span></div></div><div><div><div>7</div></div><div><span>#proof</span><span>[略。]</span></div></div><div><div><div>8</div></div><div><span>#example</span><span>[丙] </span><span>&lt;t3&gt;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>=</span><span> 第二章</span></div></div><div><div><div>11</div></div><div><span>#theorem</span><span>[丁] </span><span>&lt;t4&gt;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>14</div></div><div><span>  </span><span>// One counter is shared by all environments and restarts at each chapter.</span></div></div><div><div><div>15</div></div><div><span>  </span><span>let</span><span> </span><span>n</span><span>(</span><span>l</span><span><span>) </span><span>= </span></span><span>counter</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>"xuan-theorem"</span><span>)).</span><span>at</span><span>(</span><span>l</span><span>).</span><span>first</span><span>()</span></div></div><div><div><div>16</div></div><div><span>  </span><span>assert</span><span>.</span><span>eq</span><span>((</span><span>n</span><span>(</span><span>&lt;t1&gt;</span><span>), </span><span>n</span><span>(</span><span>&lt;t2&gt;</span><span>), </span><span>n</span><span>(</span><span>&lt;t3&gt;</span><span>), </span><span>n</span><span>(</span><span>&lt;t4&gt;</span><span>)), (</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>1</span><span>))</span></div></div><div><div><div>17</div></div><div><span>  </span><span>// `proof` is unnumbered.</span></div></div><div><div><div>18</div></div><div><span>  </span><span>assert</span><span>.</span><span>eq</span><span>(</span><span>query</span><span>(</span><span>figure</span><span>.</span><span>where</span><span>(</span><span>kind</span><span>: </span><span>"xuan-theorem"</span><span>)).</span><span>len</span><span>(), </span><span>4</span><span>)</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它用的全是这一篇的东西：<code>counter.at(label)</code>、<code>query</code>、context 里的 <code>assert.eq</code>。四个环境的序号是 <code>1, 2, 3, 1</code>，说明三种环境共用一个计数器、第二章重新开始；<code>query</code> 数出 4 个 figure，说明 <code>proof</code> 没有被算进去。<code>make test</code> 到第 16 篇再讲。</p></section></section>
<section><h2>跑一遍<a href="#跑一遍"><span>#</span></a></h2><p>这一篇的 9 个 lab 文件一起编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make ch07</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch07</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch07/context.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch07/counters.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch07/diverge.warn.typ (warned as expected)</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch07/fig-theorem.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch07/no-context.fail.typ (failed as expected)</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch07/query.typ</span></div></div><div><div><div>9</div></div><div><span>ok   lab/ch07/running-header.typ</span></div></div><div><div><div>10</div></div><div><span>ok   lab/ch07/state.typ</span></div></div><div><div><div>11</div></div><div><span>ok   lab/ch07/theorems.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>接下来<a href="#接下来"><span>#</span></a></h2><p>到这里 <code>xuan</code> 的 <code>src/</code> 已经有 <code>util</code>、<code>fonts</code>、<code>cjk</code>、<code>page</code>、<code>header</code>、<code>theorem</code>、<code>base</code>、<code>note</code> 八个文件，一份带章节页眉、总页数页脚、定理环境的 A4 笔记已经可以排了。缺的是数学：<code>theorem.typ</code> 的例子里那些 <code>$sum c_i v_i = 0$</code> 现在只是借用，下一篇专门讲 Typst 的数学模式，同时补上 <code>src/math.typ</code>，把公式编号也改成“章.序号”，用的正是今天这一套 <code>counter</code> 加 <code>show ref</code>。</p><p>如果想自己多玩一会儿：把 <code>diverge.warn.typ</code> 里的 <code>s.final()</code> 换成 <code>s.at(&lt;end&gt;)</code>，在末尾放 <code>#metadata(none) &lt;end&gt;</code>，再用 <code>--timings</code> 数遍数，看它是不是比 5 遍少。我没有试，不知道编译器会不会把这种写法也判成发散。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-06-page-and-layout/</id>
      <title type="text">页面与布局：盒子、网格与浮动</title>
      <published>2026-10-19T05:00:00.000Z</published>
      <updated>2026-10-19T05:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-06-page-and-layout/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 6 篇：我用 Typst 0.15.1 编译了 ch06 的 6 个 lab 文件和十几个临时实验，实测 fr 怎样分配剩余空间、弱间距怎样折叠、float 怎样改变正文流，再把纸张、边距和标题块收进 @local/xuan:0.1.0 的 page.typ。</summary>
      <content type="html"><![CDATA[<p>下面这段 Typst 在一个句子中间放了一个旋转的盒子：<code>前 #rotate(10deg, ...) 后</code>。编出来的 PDF 上，“前”“后”和盒子各占一行，一句话被劈成三段。Typst 没有出错，这是它的布局模型：页面里是一条自上而下的块级流，段落是其中的一种块，段落里面才是自左向右的行内流，而 <code>rotate</code> 站在了块这一边。这一篇我把页面和布局的全部构件逐个跑一遍，每个都用数字回答“它到底占多少空间”，最后把纸张、边距和标题块收进 <code>xuan</code> 的 <code>page.typ</code>。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有 <code>src/page.typ</code>（<strong>最终版</strong>，40 行，下面完整展示）。<code>lib.typ</code> 和 <code>note.typ</code> 不变：第 4 篇的 <code>lib.typ</code> 已经带着 <code>#import "src/page.typ": title-block, xuan-page</code> 这一行，<code>note</code> 也已经在调用它们，这一篇只是把黑盒打开。<code>lab/ch06/</code> 下的 6 个文件全部通过 <code>make ch06</code>。
前置：<a href="/posts/typst-by-xuan-05-fonts-and-cjk/">第 5 篇</a>（字体与中文排版）。本篇示例的字体列表 <code>("Libertinus Serif", "Noto Serif CJK SC")</code> 就是第 5 篇 <code>fonts.serif</code> 的内容，写死在每个文件里是为了让它们能单独编译。</p></div></div>
<p>先在仓库里确认 lab 能跑，环境是 Typst 0.15.1，字体来自 flake（<code>typst --ignore-system-fonts</code> 由 <code>scripts/chapter.sh</code> 传入）：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make ch06</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch06</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch06/blocks.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch06/fig-layout.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch06/grid.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch06/page.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch06/place.typ</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch06/transforms.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>6 个文件的 stderr 加起来是 0 字节：没有警告。本文里标注 <code>/tmp/p6</code> 的实验不在仓库里，代码全文都在文中，可以原样粘贴；它们和 lab 文件一样用 <code>typstyle -l 80</code> 检查过格式。</p>
<section><h2>布局模型：块级流与行内流<a href="#布局模型块级流与行内流"><span>#</span></a></h2><p>先把那个让我吃惊的实验摆出来（<code>/tmp/p6/post/inline.typ</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>tag</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>box</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>3pt</span><span>, </span><span>body</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>前 </span><span>#tag</span><span>[box] 中 </span><span>#tag</span><span>[box] 后，行内容器不打断段落。</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>前 </span><span>#rotate</span><span>(</span><span>10deg</span><span>, </span><span>tag</span><span>[旋转]) 后，rotate 独占一行。</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>前 </span><span>#scale</span><span>(</span><span>120%</span><span>, </span><span>tag</span><span>[缩放]) 后，scale 也是。</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>前 </span><span>#rect</span><span>[矩形] 后，rect 是块级。</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>前 </span><span>#box</span><span>(</span><span>rotate</span><span>(</span><span>10deg</span><span>, </span><span>tag</span><span>[旋转])) 后，包进 box 就回到行内。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-inline.png" alt="五个段落：第一段两个带边框的 box 嵌在句子中间；第二到四段里 rotate、scale、rect 把句子切成前、盒子、后三行；最后一段把 rotate 包进 box，旋转的盒子又回到了句子中间" /><figcaption>五个段落：第一段两个带边框的 box 嵌在句子中间；第二到四段里 rotate、scale、rect 把句子切成前、盒子、后三行；最后一段把 rotate 包进 box，旋转的盒子又回到了句子中间</figcaption></figure><p></p><p>读这张图，Typst 0.15.1 的行为是：</p><ol>
<li><code>box</code> 是<strong>行内</strong>元素：和文字、<code>h()</code> 一样排在行里，随文字折行。</li>
<li><code>rect</code> 是<strong>块级</strong>元素：它出现在段落中间，就把当前段落在这里截断，自己占一整行，后面的文字另起一段。</li>
<li><code>rotate</code> 和 <code>scale</code> 在这个版本里同样是块级。我没有去查文档里怎么说，这条结论只来自上面这张渲染图。解决办法也在图里：外面套一层 <code>box</code>，它就成了行内元素。</li>
</ol><p>这就是 Typst 的布局模型，两层嵌套：</p><ul>
<li>外层是<strong>块流</strong>：页面（或 <code>block</code>、<code>grid</code> 的单元格）里，块级元素自上而下堆叠，相邻块之间有间距（<code>block.spacing</code>，<code>par.spacing</code>）。段落、<code>block</code>、<code>grid</code>、<code>table</code>、<code>stack</code>、<code>align</code>、<code>pad</code>、<code>rect</code>、<code>columns</code> 都在这一层：我把它们各自塞进句子中间，<code>pdftotext</code> 看到的都是“前、元素、后”三行。</li>
<li>内层是<strong>行流</strong>：一个段落由一串行内元素组成，文字、<code>box</code>、<code>h()</code>、公式、符号，按宽度折行、对齐、两端对齐。</li>
</ul><p>CSS 用过的读者可以直接对号入座：<code>box</code> 近似 <code>display: inline-block</code>，<code>block</code> 近似 <code>display: block</code>（我用这个类比只这一次，后面不再打比方）。段落是自动形成的：连续的行内内容被隐式收成一个段落，遇到块级元素就收尾。</p><section><h3>box 和 block 的参数<a href="#box-和-block-的参数"><span>#</span></a></h3><p><code>lab/ch06/blocks.typ</code> 是这一节的主角（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch06/blocks.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// `box` is inline, `block` is a paragraph-level container.</span></div></div><div><div><div>5</div></div><div><span>行内的 </span><span>#box</span><span>(</span><span>fill</span><span>: </span><span>yellow</span><span>, </span><span>inset</span><span>: </span><span>3pt</span><span>, </span><span>baseline</span><span>: </span><span>20%</span><span>)[box] 不会打断段落，</span></div></div><div><div><div>6</div></div><div><span>而 </span><span>#block</span><span>(</span><span>fill</span><span>: </span><span>aqua</span><span>, </span><span>inset</span><span>: </span><span>6pt</span><span>, </span><span>width</span><span>: </span><span>100%</span><span>)[block 独占一行，可以设宽度、</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>圆角、描边、内边距和断页策略。]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#block</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>radius</span><span>: </span><span>4pt</span><span>, </span><span>inset</span><span>: </span><span>8pt</span><span>, </span><span>breakable</span><span>: </span><span>false</span><span>)[</span></div></div><div><div><div>10</div></div><div><span>  </span><span>`breakable: false`</span><span> 保证这个块不会被分页切开。</span></div></div><div><div><div>11</div></div><div><span>]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>// Alignment, padding, spacing.</span></div></div><div><div><div>14</div></div><div><span>#align</span><span>(</span><span>right</span><span>)[右对齐]</span></div></div><div><div><div>15</div></div><div><span>#align</span><span>(</span><span>center</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>, </span><span>rect</span><span>(</span><span>height</span><span>: </span><span>1.5cm</span><span>, </span><span>width</span><span>: </span><span>100%</span><span>)[居中且垂直居中])</span></div></div><div><div><div>16</div></div><div><span>#pad</span><span>(</span><span>x</span><span>: </span><span>1cm</span><span>, </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>)[</span><span>`pad`</span><span> 向内缩 1cm])</span></div></div><div><div><div>17</div></div><div><span>左 </span><span>#h</span><span>(</span><span>1fr</span><span>) 右（</span><span>`1fr`</span><span> 吃掉剩余空间）</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>#v</span><span>(</span><span>1em</span><span>)</span></div></div><div><div><div>20</div></div><div><span>上面空了 </span><span>`1em`</span><span>，下面是 </span><span>`stack`</span><span>：</span></div></div><div><div><div>21</div></div><div><span>#stack</span><span>(</span></div></div><div><div><div>22</div></div><div><span>  </span><span>dir</span><span>: </span><span>ltr</span><span>,</span></div></div><div><div><div>23</div></div><div><span>  </span><span>spacing</span><span>: </span><span>6pt</span><span>,</span></div></div><div><div><div>24</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>1cm</span><span>),</span></div></div><div><div><div>25</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>2cm</span><span>),</span></div></div><div><div><div>26</div></div><div><span>  </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>1cm</span><span>),</span></div></div><div><div><div>27</div></div><div><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>// `repeat` fills a line; `hide` keeps the space but not the ink.</span></div></div><div><div><div>30</div></div><div><span>目录项 </span><span>#box</span><span>(</span><span>width</span><span>: </span><span>1fr</span><span>, </span><span>repeat</span><span>[.]) 12</span></div></div><div><div><div>31</div></div><div><span>隐藏的 </span><span>#hide</span><span>[看不见] 仍占位</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-blocks.png" alt="blocks.typ 的渲染：一行里嵌着黄底的 box，下面是浅蓝底占满行宽的 block，接着是带圆角描边的块、右对齐的文字、居中的方框、向内缩进的方框、左右两端对齐的一行字、三个并排的方框、带点线引导的目录项和一处空白" /><figcaption>blocks.typ 的渲染：一行里嵌着黄底的 box，下面是浅蓝底占满行宽的 block，接着是带圆角描边的块、右对齐的文字、居中的方框、向内缩进的方框、左右两端对齐的一行字、三个并排的方框、带点线引导的目录项和一处空白</figcaption></figure><p></p><p>逐项看：</p><ul>
<li><code>box(baseline: 20%)</code> 把盒子相对文字基线抬高 20%，所以渲染图里黄色小盒子比周围文字略高。<code>box</code> 能设 <code>width</code>、<code>height</code>、<code>inset</code>、<code>outset</code>、<code>fill</code>、<code>stroke</code>、<code>radius</code>、<code>clip</code>，这些 <code>block</code> 也都有。</li>
<li><code>block</code> 多出来的是<strong>块间距</strong>（<code>above</code>、<code>below</code>，或一次性写 <code>spacing</code>）和<strong>断页策略</strong>。第一个 <code>block</code> 出现在句子中间，“而”字之后被截断，渲染图第一段确实停在“而”字上：这是上面第 2 条的又一次验证。</li>
<li><code>breakable: false</code> 的效果我专门验证了（<code>/tmp/p6/post/breakable.typ</code>）。页面只有 4cm 高，装不下 6 行内容，默认的块会被分页切开，不可断的块整个挪到下一页：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>7cm</span><span>, </span><span>height</span><span>: </span><span>4cm</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>lines</span><span><span> </span><span>= </span><span>[一 </span></span><span>\ </span><span>二 </span><span>\ </span><span>三 </span><span>\ </span><span>四 </span><span>\ </span><span>五 </span><span>\ </span><span>六]</span></div></div><div><div><div>4</div></div><div><span>前一行。</span></div></div><div><div><div>5</div></div><div><span>#block</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>4pt</span><span>, </span><span>lines</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#pagebreak</span><span>()</span></div></div><div><div><div>7</div></div><div><span>前一行。</span></div></div><div><div><div>8</div></div><div><span>#block</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>4pt</span><span>, </span><span>breakable</span><span>: </span><span>false</span><span>, </span><span>lines</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用 <code>pdftotext -f N -l N</code> 逐页取文字，共 4 页：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>p1: 前一行。  一 二 三 四</span></div></div><div><div><div>2</div></div><div><span>p2: 五 六</span></div></div><div><div><div>3</div></div><div><span>p3: 前一行。</span></div></div><div><div><div>4</div></div><div><span>p4: 一 二 三 四 五 六</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>默认的块在第 1 页放了四行，剩下两行去了第 2 页；<code>breakable: false</code> 的块把第 3 页只留给“前一行。”，六行整体落在第 4 页。代价是第 3 页下面一大片空白，所以这个参数适合小块（定理框、代码片段），不适合整页大表格。</p></section><section><h3>align、pad、h、v、stack、repeat、hide<a href="#alignpadhvstackrepeathide"><span>#</span></a></h3><p>blocks.typ 的后半段是一组“摆位置”的小工具，它们的共同点是：每个都是普通元素，没有特殊布局通道。</p><ul>
<li><code>align(right)[...]</code> 在当前容器宽度内对齐内容。参数是对齐值，可以用 <code>+</code> 组合水平和垂直：<code>center + horizon</code>（水平居中加垂直居中）。例子里给 <code>rect</code> 设了 <code>height: 1.5cm</code>，让垂直方向有空间可以居中。</li>
<li><code>pad(x: 1cm, ...)</code> 给内容加外侧留白，<code>rect</code> 因此向内缩了 1cm。参数有 <code>left right top bottom x y rest</code> 几种写法。</li>
<li><code>h(1fr)</code> 和 <code>v(1em)</code> 是水平和垂直间距，下面两节专门讲。</li>
<li><code>stack(dir: ltr, spacing: 6pt, ...)</code> 把一组子元素沿一个方向依次摆放，<code>dir</code> 取 <code>ltr rtl ttb btt</code>，<code>spacing</code> 是子元素之间的距离。stack 的子项里可以直接写 <code>1fr</code> 这样的比例间距，见下一节。</li>
<li><code>repeat[.]</code> 把内容重复到填满可用宽度，<code>box(width: 1fr, repeat[.])</code> 就是目录里的点线引导。我另外试了 <code>gap</code>（<code>/tmp/p6/post/hide-repeat.typ</code>）：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>#context</span><span> [显示时 </span><span>#measure</span><span>[看不见].</span><span>width</span><span>，`</span><span>hide</span><span>` 后 </span><span>#measure</span><span>(</span><span>hide</span><span>[看不见]).</span><span>width</span><span>]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>第一章 </span><span>#box</span><span>(</span><span>width</span><span>: </span><span>1fr</span><span>, </span><span>repeat</span><span>[.]) 3 </span><span>\</span></div></div><div><div><div>6</div></div><div><span>第二章 </span><span>#box</span><span>(</span><span>width</span><span>: </span><span>1fr</span><span>, </span><span>repeat</span><span>(</span><span>gap</span><span>: </span><span>4pt</span><span>)[·]) 17</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-hide-repeat.png" alt="上面一行文字报告 hide 前后宽度都是 33pt；下面两行是目录项，第一行的点线引导很密，第二行用 gap: 4pt 之后点间距明显变大" /><figcaption>上面一行文字报告 hide 前后宽度都是 33pt；下面两行是目录项，第一行的点线引导很密，第二行用 gap: 4pt 之后点间距明显变大</figcaption></figure><p></p><p>输出的第一行是 <code>显示时 33pt，hide 后 33pt</code>：<code>hide</code> 只擦掉墨迹，宽度分毫不差，这就是 blocks.typ 里“看不见却仍占位”的含义。常见用法是幻灯片的逐步显示，第 13 篇会用到。</p></section></section>
<section><h2>fr 到底怎么分配空间<a href="#fr-到底怎么分配空间"><span>#</span></a></h2><p><code>1fr</code> 是 Typst 里“按份额瓜分剩余空间”的单位，网格列、行、<code>h</code>、<code>v</code>、<code>stack</code> 间距、<code>box</code> 宽度都认它。我想知道分配顺序，于是让每一格自己报告宽度（<code>/tmp/p6/post/fr.typ</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>w</span><span>(</span><span>len</span><span><span>) </span><span>= </span></span><span>calc</span><span>.</span><span>round</span><span>(</span><span>len</span><span>.</span><span>pt</span><span>(), </span><span>digits</span><span>: </span><span>2</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 12cm of usable width = 340.16pt. `auto` is measured first, fixed lengths</span></div></div><div><div><div>6</div></div><div><span>// next, and `fr` shares what is left.</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>probe</span><span><span> </span><span>= </span></span><span>block</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>layout</span><span>(</span><span>s</span><span> </span><span>=&gt;</span><span> </span><span>repr</span><span>(</span><span>w</span><span>(</span><span>s</span><span>.</span><span>width</span><span>))))</span></div></div><div><div><div>8</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>9</div></div><div><span>  </span><span>columns</span><span>: (</span><span>auto</span><span>, </span><span>2cm</span><span>, </span><span>1fr</span><span>, </span><span>2fr</span><span>),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>stroke</span><span>: </span><span>0.4pt</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>inset</span><span>: </span><span>4pt</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>[auto], </span><span>probe</span><span>, </span><span>probe</span><span>, </span><span>probe</span><span>,</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div><div><div><div>14</div></div><div><span>#context</span><span> [</span><span>`auto`</span><span> 列内容宽：</span><span>#repr</span><span>(</span><span>w</span><span>(</span><span>measure</span><span>[auto].</span><span>width</span><span>))]</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>// On a line, a box with an `fr` width behaves like `h(fr)`.</span></div></div><div><div><div>17</div></div><div><span>#let</span><span> </span><span>slot</span><span>(</span><span>width</span><span><span>) </span><span>= </span></span><span>box</span><span>(</span><span>width</span><span>: </span><span>width</span><span>, </span><span>layout</span><span>(</span><span>s</span><span> </span><span>=&gt;</span><span> </span><span>repr</span><span>(</span><span>w</span><span>(</span><span>s</span><span>.</span><span>width</span><span>))))</span></div></div><div><div><div>18</div></div><div><span>#slot</span><span>(</span><span>3cm</span><span>) </span><span>#slot</span><span>(</span><span>1fr</span><span>) </span><span>#slot</span><span>(</span><span>2fr</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>// stack takes bare fr values as spacing.</span></div></div><div><div><div>21</div></div><div><span>#stack</span><span>(</span><span>dir</span><span>: </span><span>ltr</span><span>, </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>1cm</span><span>), </span><span>1fr</span><span>, </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>1cm</span><span>), </span><span>3fr</span><span>, </span><span>rect</span><span>(</span><span>width</span><span>: </span><span>1cm</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-fr.png" alt="第一行是四列的网格，格子里分别写着 auto、48.69、77.19、162.38；下面一行是 auto 列内容宽 19.89；再下面一行是 85.04、83.21、166.41 三个数字；最后一行是三个方块，第一和第二个靠近，第二和第三个相距很远" /><figcaption>第一行是四列的网格，格子里分别写着 auto、48.69、77.19、162.38；下面一行是 auto 列内容宽 19.89；再下面一行是 85.04、83.21、166.41 三个数字；最后一行是三个方块，第一和第二个靠近，第二和第三个相距很远</figcaption></figure><p></p><p>（<code>layout</code> 和 <code>measure</code> 在后面单独讲，这里只借它们读出尺寸：<code>layout(s =&gt; ...)</code> 里的 <code>s.width</code> 是这个盒子分到的宽度。）</p><p>第一行网格，页面可用宽度 14cm − 2 × 1cm = 12cm = 340.16pt，每格 <code>inset: 4pt</code>，也就是内容比格子窄 8pt。数据读出来是：</p><ul>
<li><code>auto</code> 列：内容 <code>auto</code> 宽 19.89pt，加 8pt 内边距，列宽 27.89pt。</li>
<li><code>2cm</code> 列：探针报告 48.69pt，加 8pt 是 56.69pt，正好 2cm。</li>
<li>剩下 340.16 − 27.89 − 56.69 = 255.58pt 给 <code>fr</code> 列。<code>1fr</code> 的探针报告 77.19pt（列宽 85.19pt），<code>2fr</code> 是 162.38pt（列宽 170.38pt），列宽之比恰好 1 : 2，三份各 85.19pt。</li>
</ul><p>所以顺序是：<strong>先量 <code>auto</code> 列的内容，再扣掉固定长度（和百分比），剩下的按 fr 份数平分。</strong> 固定长度和百分比我另外验证了一遍（<code>/tmp/p6/post/fr-overflow.typ</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>10cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>w</span><span>(</span><span>len</span><span><span>) </span><span>= </span></span><span>calc</span><span>.</span><span>round</span><span>(</span><span>len</span><span>.</span><span>pt</span><span>(), </span><span>digits</span><span>: </span><span>2</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>probe</span><span><span> </span><span>= </span></span><span>block</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>layout</span><span>(</span><span>s</span><span> </span><span>=&gt;</span><span> </span><span>repr</span><span>(</span><span>w</span><span>(</span><span>s</span><span>.</span><span>width</span><span>))))</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// Fixed tracks first: 200pt + 100pt already exceed the 283.46pt page.</span></div></div><div><div><div>6</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>7</div></div><div><span>  </span><span>columns</span><span>: (</span><span>200pt</span><span>, </span><span>100pt</span><span>, </span><span>1fr</span><span>),</span></div></div><div><div><div>8</div></div><div><span>  </span><span>probe</span><span>, </span><span>probe</span><span>, </span><span>probe</span><span>,</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// Percentages are relative to the grid width.</span></div></div><div><div><div>12</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>13</div></div><div><span>  </span><span>columns</span><span>: (</span><span>50%</span><span>, </span><span>1fr</span><span>),</span></div></div><div><div><div>14</div></div><div><span>  </span><span>probe</span><span>, </span><span>probe</span><span>,</span></div></div><div><div><div>15</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>200.0             100.0</span></div></div><div><div><div>2</div></div><div><span>141.73   141.73</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一个网格里，两个固定列合计 300pt，已经超过页面的 283.46pt，它们照样是 200pt 和 100pt，没有被挤压；<code>1fr</code> 那一列的探针没有输出任何可见内容，我没有再去量它的确切宽度，只记结论：固定长度优先，<code>fr</code> 吃剩下的。第二个网格里 <code>50%</code> 是 141.73pt，正好是页面宽度的一半，也是 <code>1fr</code> 分到的另一半：百分比的基准是网格自身的宽度。</p><p>行内的规则一样。<code>box(width: 1fr)</code> 在一行里和 <code>h(1fr)</code> 等价：<code>#slot(3cm) #slot(1fr) #slot(2fr)</code> 那一行，固定的 3cm 是 85.04pt，340.16 − 85.04 − 83.21 − 166.41 = 5.5pt，是两个空格占去的宽度，剩下 249.62pt 按 1 : 2 分成 83.21pt 和 166.41pt，和报告的数字对得上。<code>stack</code> 里直接写 <code>1fr</code>、<code>3fr</code> 当间距，渲染图最后一行三个 1cm 方块之间的空隙之比就是 1 : 3。</p></section>
<section><h2>弱间距与强间距<a href="#弱间距与强间距"><span>#</span></a></h2><p><code>v(...)</code> 和 <code>h(...)</code> 有一个 <code>weak</code> 参数。弱间距遇到别的间距，或者落在行首、页首这类边缘，就会被吞掉一部分或全部。具体吞多少、怎么吞，我用 <code>/tmp/p6/post/weak.typ</code> 量了一遍。<code>y()</code> 函数在每个段落末尾报告当前纵坐标，全局 <code>par.spacing</code> 设成 10pt：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>10cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>spacing</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>y</span><span><span>() </span><span>= </span></span><span>context</span><span> </span><span>repr</span><span>(</span><span>calc</span><span>.</span><span>round</span><span>(</span><span>here</span><span>().</span><span>position</span><span>().</span><span>y</span><span>.</span><span>pt</span><span>(), </span><span>digits</span><span>: </span><span>1</span><span>))</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>段落 A </span><span>#y</span><span>()</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>段落 B </span><span>#y</span><span>()</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#v</span><span>(</span><span>30pt</span><span>)</span></div></div><div><div><div>11</div></div><div><span>段落 C（前面有强 </span><span>`v(30pt)`</span><span>）</span><span>#y</span><span>()</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#v</span><span>(</span><span>4pt</span><span>, </span><span>weak</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>14</div></div><div><span>段落 D（弱 </span><span>`v(4pt)`</span><span> 比段落间距 10pt 小）</span><span>#y</span><span>()</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>#v</span><span>(</span><span>30pt</span><span>, </span><span>weak</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>17</div></div><div><span>#v</span><span>(</span><span>20pt</span><span>, </span><span>weak</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>18</div></div><div><span>段落 E（两个弱 </span><span>`v`</span><span> 取较大的 30pt）</span><span>#y</span><span>()</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>// Weak horizontal space vanishes at a line start.</span></div></div><div><div><div>21</div></div><div><span>#h</span><span>(</span><span>2cm</span><span>, </span><span>weak</span><span>: </span><span>true</span><span>)行首的弱 </span><span>`h(2cm)`</span><span> 被丢弃，</span><span>#h</span><span>(</span><span>2cm</span><span>)强的不会。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>段落 A 35.6</span></div></div><div><div><div>2</div></div><div><span>段落 B 52.9</span></div></div><div><div><div>3</div></div><div><span>段落 C（前面有强 v(30pt)）100.2</span></div></div><div><div><div>4</div></div><div><span>段落 D（弱 v(4pt) 比段落间距 10pt 小）111.5</span></div></div><div><div><div>5</div></div><div><span>段落 E（两个弱 v 取较大的 30pt）148.8</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-weak.png" alt="渲染图：A、B 两段间距正常，B 和 C 之间有一大块空白，C 和 D 几乎紧挨着，D 和 E 之间的空白中等；最后一行的行首没有缩进，句中的强 h 留出了一大段空白" /><figcaption>渲染图：A、B 两段间距正常，B 和 C 之间有一大块空白，C 和 D 几乎紧挨着，D 和 E 之间的空白中等；最后一行的行首没有缩进，句中的强 h 留出了一大段空白</figcaption></figure><p></p><p>A 到 B 的纵坐标差是 17.3pt，其中 10pt 是段落间距，剩下的 7.3pt 是一行的高度。把每一对相邻段落的纵坐标差扣掉这 7.3pt，就是实际的间隙：</p><ul>
<li>A 到 B：10pt，纯段落间距。</li>
<li>B 到 C：47.3 − 7.3 = <strong>40pt</strong>。强 <code>v(30pt)</code> 没有取代段落间距，而是叠在它上面：10 + 30。</li>
<li>C 到 D：11.3 − 7.3 = <strong>4pt</strong>。弱 <code>v(4pt)</code> 遇到 10pt 的段落间距，结果不是 10pt，也不是 14pt，而是 4pt：弱间距<strong>取代</strong>了段落间距，即使它更小。</li>
<li>D 到 E：37.3 − 7.3 = <strong>30pt</strong>。两个相邻的弱 <code>v</code>（30pt 和 20pt）合并成较大的那个。</li>
<li>最后一行：行首的弱 <code>h(2cm)</code> 被丢掉，句子从页边直接开始；行内的强 <code>h(2cm)</code> 照常留白，渲染图上能看到一段明显的空隙。</li>
</ul><p>我对这组数字的解释是：Typst 内部给间距分了强弱等级，段落间距最弱，显式 <code>v(weak: true)</code> 比它强一档，强 <code>v</code> 不参与折叠。这是从数据反推的，没有去读排版引擎源码，细节以后遇到反例再修正。实用的结论只有两条：想在标题前后加固定留白，用 <code>v(..., weak: true)</code>，不会和段落间距叠加；想要无条件的额外空隙，用强 <code>v</code>。</p></section>
<section><h2>grid：二维排版<a href="#grid二维排版"><span>#</span></a></h2><p><code>grid</code> 把一组子元素按列轨道和行轨道摆成表格。<code>lab/ch06/grid.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch06/grid.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// Track sizes: auto fits content, a length is fixed, `fr` shares the rest.</span></div></div><div><div><div>5</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>6</div></div><div><span>  </span><span>columns</span><span>: (</span><span>auto</span><span>, </span><span>3cm</span><span>, </span><span>1fr</span><span>),</span></div></div><div><div><div>7</div></div><div><span>  </span><span>rows</span><span>: (</span><span>auto</span><span>, </span><span>1cm</span><span>),</span></div></div><div><div><div>8</div></div><div><span>  </span><span>gutter</span><span>: </span><span>4pt</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>stroke</span><span>: </span><span>0.4pt</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>inset</span><span>: </span><span>5pt</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>align</span><span>: (</span><span>left</span><span>, </span><span>center</span><span>, </span><span>right</span><span>),</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>[auto], [3cm], [1fr],</span></div></div><div><div><div>13</div></div><div><span>  </span><span>grid</span><span>.</span><span>cell</span><span>(</span><span>colspan</span><span>: </span><span>2</span><span>)[跨两列], [右],</span></div></div><div><div><div>14</div></div><div><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>// `fill` and `align` accept functions of (column, row).</span></div></div><div><div><div>17</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>18</div></div><div><span>  </span><span>columns</span><span>: (</span><span>1fr</span><span><span>,) </span><span>*</span><span> </span></span><span>4</span><span>,</span></div></div><div><div><div>19</div></div><div><span>  </span><span>fill</span><span>: (</span><span>x</span><span>, </span><span>y</span><span>) </span><span>=&gt;</span><span> </span><span>if</span><span> </span><span>calc</span><span>.</span><span>odd</span><span>(</span><span>x</span><span><span> </span><span>+</span><span> </span></span><span>y</span><span>) { </span><span>luma</span><span>(</span><span>235</span><span>) },</span></div></div><div><div><div>20</div></div><div><span>  </span><span>inset</span><span>: </span><span>6pt</span><span>,</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>..</span><span>range</span><span>(</span><span>8</span><span>).</span><span>map</span><span>(</span><span>i</span><span> </span><span>=&gt;</span><span> [</span><span>#i</span><span>]),</span></div></div><div><div><div>22</div></div><div><span>)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>// `columns` splits the page flow instead.</span></div></div><div><div><div>25</div></div><div><span>#columns</span><span>(</span><span>2</span><span>, </span><span>gutter</span><span>: </span><span>1em</span><span>)[</span></div></div><div><div><div>26</div></div><div><span>  </span><span>#lorem</span><span>(</span><span>30</span><span>)</span></div></div><div><div><div>27</div></div><div><span>  </span><span>#colbreak</span><span>()</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>第二栏。</span></div></div><div><div><div>29</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-grid.png" alt="grid.typ 的渲染：第一个网格有两行三列，第一行是 auto、3cm、1fr 三个格子，第二行第一格跨两列写着跨两列，右下格内容靠右；第二个网格是 2 行 4 列，数字 0 到 7，奇偶交替的格子是浅灰底；最下面是两栏文字，左栏是 Lorem ipsum，右栏只有第二栏三个字" /><figcaption>grid.typ 的渲染：第一个网格有两行三列，第一行是 auto、3cm、1fr 三个格子，第二行第一格跨两列写着跨两列，右下格内容靠右；第二个网格是 2 行 4 列，数字 0 到 7，奇偶交替的格子是浅灰底；最下面是两栏文字，左栏是 Lorem ipsum，右栏只有第二栏三个字</figcaption></figure><p></p><p>这个文件涉及的点：</p><ul>
<li><strong><code>columns</code> 与 <code>rows</code></strong>：每个轨道可以是 <code>auto</code>、固定长度、百分比或 <code>fr</code>，规则就是上一节量过的。只给一个整数 <code>columns: 3</code> 时，我用三个内容宽度悬殊的格子试过，探针报告三列都是 113.39pt，也就是 340.16pt 的三等分，而不是各自贴着内容。</li>
<li><strong>单元格按顺序填入</strong>：子元素依次填满第一行、第二行，不够一行的补空。<code>grid.cell(colspan: 2)</code> 让一个格子跨两列。</li>
<li><strong><code>gutter</code></strong>：轨道之间的间隙，横竖同时设；也可以分开写 <code>column-gutter</code>、<code>row-gutter</code>。渲染图里第一个网格的格线之间有 4pt 空隙就来自这里。</li>
<li><strong><code>stroke</code>、<code>inset</code></strong>：每个格子的描边和内边距。</li>
<li><strong><code>align</code></strong>：可以是单个对齐值，也可以是每列一个的元组：<code>(left, center, right)</code> 让第一列靠左、第二列居中、第三列靠右。</li>
<li><strong><code>fill</code> 和 <code>align</code> 接受函数 <code>(x, y) =&gt; ...</code></strong>：参数是列号和行号（从 0 起）。<code>calc.odd(x + y)</code> 给出棋盘格，函数返回 <code>none</code>（条件为假时 <code>if</code> 的返回值）就不填色。</li>
<li><strong><code>(1fr,) * 4</code></strong> 是一个含四个 <code>1fr</code> 的元组，用来写等宽四列；<code>..range(8).map(i =&gt; [#i])</code> 把数组展开成 8 个子元素，这是第 3 篇讲过的参数展开。</li>
</ul><p>最后的 <code>columns(2, gutter: 1em)[...]</code> 和网格不同：它是把<strong>内容流</strong>拆成两栏，内容先填满左栏再溢出到右栏，<code>colbreak()</code> 强制从右栏开始。渲染图里左栏是 <code>lorem(30)</code> 的 7 行，右栏只有“第二栏。”三个字，因为 <code>colbreak</code> 之后的内容就这么少。</p><p>至于 <code>table</code>，它在 <code>grid</code> 的基础上加了表头、表脚和默认的框线样式，第 9 篇再讲。</p><section><h3>一个跨列的页面骨架<a href="#一个跨列的页面骨架"><span>#</span></a></h3><p><code>lab/ch06/fig-layout.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch06/fig-layout.typ" target="_blank">源文件</a>）把上面的规则合在一个小图里：固定列宽、固定行高、<code>gutter</code>、跨三列的 <code>grid.cell</code>。<code>cell</code> 是个局部函数，用 <code>rect(width: 100%, height: 100%)</code> 填满分到的格子：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>cell</span><span>(</span><span>fill</span><span>, </span><span>body</span><span><span>) </span><span>= </span></span><span>rect</span><span>(</span></div></div><div><div><div>5</div></div><div><span>  </span><span>width</span><span>: </span><span>100%</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>height</span><span>: </span><span>100%</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>fill</span><span>: </span><span>fill</span><span>.</span><span>lighten</span><span>(</span><span>70%</span><span>),</span></div></div><div><div><div>8</div></div><div><span>  </span><span>stroke</span><span>: </span><span>0.5pt</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>align</span><span>(</span><span>center</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>, </span><span>body</span><span>),</span></div></div><div><div><div>10</div></div><div><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>13</div></div><div><span>  </span><span>columns</span><span>: (</span><span>2cm</span><span>, </span><span>4cm</span><span>, </span><span>2cm</span><span>),</span></div></div><div><div><div>14</div></div><div><span>  </span><span>rows</span><span>: (</span><span>1cm</span><span>, </span><span>3cm</span><span>, </span><span>1cm</span><span>),</span></div></div><div><div><div>15</div></div><div><span>  </span><span>gutter</span><span>: </span><span>3pt</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>grid</span><span>.</span><span>cell</span><span>(</span><span>colspan</span><span>: </span><span>3</span><span>, </span><span>cell</span><span>(</span><span>blue</span><span>)[header]),</span></div></div><div><div><div>17</div></div><div><span>  </span><span>cell</span><span>(</span><span>red</span><span>)[左栏 2cm],</span></div></div><div><div><div>18</div></div><div><span>  </span><span>cell</span><span>(</span><span>green</span><span>)[正文 4cm],</span></div></div><div><div><div>19</div></div><div><span>  </span><span>cell</span><span>(</span><span>red</span><span>)[侧栏 2cm],</span></div></div><div><div><div>20</div></div><div><span>  </span><span>grid</span><span>.</span><span>cell</span><span>(</span><span>colspan</span><span>: </span><span>3</span><span>, </span><span>cell</span><span>(</span><span>blue</span><span>)[footer]),</span></div></div><div><div><div>21</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-layout.png" alt="fig-layout.typ 的渲染：最上和最下是跨三列的蓝色横条，写着 header 和 footer，中间一行是三格：左右两侧窄的红色格子写着左栏 2cm 和侧栏 2cm，中间宽的绿色格子写着正文 4cm" /><figcaption>fig-layout.typ 的渲染：最上和最下是跨三列的蓝色横条，写着 header 和 footer，中间一行是三格：左右两侧窄的红色格子写着左栏 2cm 和侧栏 2cm，中间宽的绿色格子写着正文 4cm</figcaption></figure><p></p><p>页眉页脚两条各占一行并跨满三列，中间三格的宽度比是 2 : 4 : 2。</p></section></section>
<section><h2>place：脱离流的定位与浮动<a href="#place脱离流的定位与浮动"><span>#</span></a></h2><p><code>place</code> 是 Typst 里“不占位置”的那一类。<code>lab/ch06/place.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch06/place.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>8cm</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// `place` takes its content out of the flow and positions it in the parent.</span></div></div><div><div><div>5</div></div><div><span>#place</span><span>(</span><span>top</span><span><span> </span><span>+</span><span> </span></span><span>right</span><span>, </span><span>dx</span><span><span>: </span><span>-</span></span><span>4pt</span><span>, </span><span>dy</span><span>: </span><span>4pt</span><span>, </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)[角标])</span></div></div><div><div><div>6</div></div><div><span>#place</span><span>(</span><span>bottom</span><span><span> </span><span>+</span><span> </span></span><span>center</span><span>, </span><span>float</span><span>: </span><span>true</span><span>, </span><span>rect</span><span>(</span><span>fill</span><span>: </span><span>aqua</span><span>, </span><span>width</span><span>: </span><span>100%</span><span>)[</span></div></div><div><div><div>7</div></div><div><span>  </span><span>`float: true`</span><span> 的内容会被推到页面顶部或底部，正文绕开。</span></div></div><div><div><div>8</div></div><div><span>])</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#lorem</span><span>(</span><span>70</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#place</span><span>(</span></div></div><div><div><div>13</div></div><div><span>  </span><span>top</span><span><span> </span><span>+</span><span> </span></span><span>center</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>scope</span><span>: </span><span>"parent"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>float</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>text</span><span>(</span><span>size</span><span>: </span><span>8pt</span><span>)[</span><span>`scope: "parent"`</span><span> 让浮动体跨过分栏。],</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这个文件编出来是 2 页，第一页：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-place.png" alt="place.typ 第 1 页：一段 Lorem ipsum 正文，右上角有一个红色的角标压在正文最后一个词上，页面底部有一块浅蓝色的矩形，里面写着 float: true" /><figcaption>place.typ 第 1 页：一段 Lorem ipsum 正文，右上角有一个红色的角标压在正文最后一个词上，页面底部有一块浅蓝色的矩形，里面写着 float: true</figcaption></figure><p></p><p>第二页：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-place-parent.png" alt="place.typ 第 2 页：顶部居中是一行小字，写着 scope: parent 让浮动体跨过分栏，下面是第一页放不下的两行正文" /><figcaption>place.typ 第 2 页：顶部居中是一行小字，写着 scope: parent 让浮动体跨过分栏，下面是第一页放不下的两行正文</figcaption></figure><p></p><p><strong>普通 <code>place</code>（没有 <code>float</code>）是零尺寸的。</strong> 红色“角标”按 <code>top + right</code> 定位到父容器的右上角，再偏移 <code>dx: -4pt, dy: 4pt</code>，渲染图里它压在第一行正文的末尾：正文没有给它让位，因为它根本不在流里。这是一个特点：叠层标注方便，但小心遮住内容。</p><p><strong><code>float: true</code> 才会改变正文流。</strong> 我把几种情形做成对照实验（<code>/tmp/p6/post/float.typ</code>），每个 <code>#page[...]</code> 里两个段落，A 在前，B 在后，<code>place</code> 夹在中间，页面 4cm 高、边距 8pt，<code>bar</code> 是 1cm 高的色块：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>8cm</span><span>, </span><span>height</span><span>: </span><span>4cm</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>y</span><span>(</span><span>tag</span><span><span>) </span><span>= </span></span><span>context</span><span> [</span><span>#tag</span><span>：</span><span>#calc</span><span>.</span><span>round</span><span>(</span><span>here</span><span>().</span><span>position</span><span>().</span><span>y</span><span>.</span><span>pt</span><span>(), </span><span>digits</span><span>: </span><span>1</span><span>)pt]</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>bar</span><span><span> </span><span>= </span></span><span>rect</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>height</span><span>: </span><span>1cm</span><span>, </span><span>fill</span><span>: </span><span>aqua</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#page</span><span>[</span><span>#y</span><span>[基线 A]</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>  </span><span>#y</span><span>[基线 B]]</span></div></div><div><div><div>9</div></div><div><span>#page</span><span>[</span><span>#y</span><span>[A] </span><span>#place</span><span>(</span><span>top</span><span><span> </span><span>+</span><span> </span></span><span>right</span><span>, </span><span>bar</span><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>  </span><span>#y</span><span>[普通 place 之后的 B]]</span></div></div><div><div><div>12</div></div><div><span>#page</span><span>[</span><span>#y</span><span>[A] </span><span>#place</span><span>(</span><span>top</span><span>, </span><span>float</span><span>: </span><span>true</span><span>, </span><span>bar</span><span>)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>#y</span><span>[顶部 float 之后的 B]]</span></div></div><div><div><div>15</div></div><div><span>#page</span><span>[</span><span>#y</span><span>[A] </span><span>#place</span><span>(</span><span>top</span><span>, </span><span>float</span><span>: </span><span>true</span><span>, </span><span>clearance</span><span>: </span><span>0pt</span><span>, </span><span>bar</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>  </span><span>#y</span><span>[clearance: 0pt 的 B]]</span></div></div><div><div><div>18</div></div><div><span>#page</span><span>[</span><span>#y</span><span>[A] </span><span>#place</span><span>(</span><span>bottom</span><span>, </span><span>float</span><span>: </span><span>true</span><span>, </span><span>bar</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>  </span><span>#y</span><span>[底部 float 之后的 B]]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这 5 页报告的纵坐标（pt）：</p>

<table><thead><tr><th>情形</th><th>A</th><th>B</th><th>A 相对基线下移</th></tr></thead><tbody><tr><td>基线（没有 place）</td><td>15.3</td><td>34.6</td><td>0</td></tr><tr><td>普通 <code>place(top + right)</code></td><td>15.3</td><td>34.6</td><td>0</td></tr><tr><td><code>float: true</code>，顶部</td><td>58.6</td><td>77.9</td><td>43.3</td></tr><tr><td><code>float: true, clearance: 0pt</code></td><td>43.6</td><td>62.9</td><td>28.3</td></tr><tr><td><code>float: true</code>，底部</td><td>15.3</td><td>34.6</td><td>0</td></tr></tbody></table><p>规律很清楚：</p><ul>
<li>普通 <code>place</code> 对 A、B 的位置毫无影响。</li>
<li>顶部浮动体把后面的所有内容往下推它自己的高度，再加上 <code>clearance</code>：1cm 是 28.35pt，默认 <code>clearance</code> 在 10pt 字号下让位置多推了 15pt（43.3 − 28.3，看起来是 1.5em，这个倍数是我从数字推的，没有核对文档），设成 0 就只剩 28.3pt。</li>
<li>底部浮动体把空间留在页面下沿，上面的文字一点没动。</li>
<li>放不下的浮动体会被推到下一页，前面 place.typ 的第二页就是例子：<code>scope: "parent"</code> 的浮动体在文件末尾才出现，第一页已经满了，它去了第 2 页的顶部。</li>
</ul><p><strong><code>scope: "parent"</code> 是给多栏用的。</strong> place.typ 的页面是单栏，所以那个例子里 <code>scope</code> 没有可见的效果，要看效果得有栏。<code>/tmp/p6/post/columns.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>8cm</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>, </span><span>columns</span><span>: </span><span>2</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>9pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#place</span><span>(</span><span>top</span><span><span> </span><span>+</span><span> </span></span><span>center</span><span>, </span><span>scope</span><span>: </span><span>"parent"</span><span>, </span><span>float</span><span>: </span><span>true</span><span>, </span><span>text</span><span>(</span></div></div><div><div><div>4</div></div><div><span>  </span><span>size</span><span>: </span><span>14pt</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>weight</span><span>: </span><span>"bold"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>)[跨栏标题 scope: "parent"])</span></div></div><div><div><div>7</div></div><div><span>#lorem</span><span>(</span><span>60</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#colbreak</span><span>()</span></div></div><div><div><div>9</div></div><div><span>右栏。</span><span>#lorem</span><span>(</span><span>20</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-columns-parent.png" alt="双栏页面：顶部是横跨两栏的粗体标题，下面左栏是一整段 Lorem ipsum，右栏以右栏两个字开头，同样是 Lorem ipsum 但更短" /><figcaption>双栏页面：顶部是横跨两栏的粗体标题，下面左栏是一整段 Lorem ipsum，右栏以右栏两个字开头，同样是 Lorem ipsum 但更短</figcaption></figure><p></p><p>默认的浮动体只能放进一栏里，<code>scope: "parent"</code> 让它的参照物变成栏的父容器，也就是整个页面宽度，标题因此横跨两栏，两栏正文都被推到它下面。论文里跨栏的大图大表就是这么放的。</p></section>
<section><h2>变换：只改外观，不改布局<a href="#变换只改外观不改布局"><span>#</span></a></h2><p><code>lab/ch06/transforms.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch06/transforms.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// Transforms change appearance, not layout: the original box stays in place.</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>tag</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>box</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>4pt</span><span>, </span><span>body</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#tag</span><span>[原样] </span><span>#h</span><span>(</span><span>1cm</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#rotate</span><span>(</span><span>20deg</span><span>, </span><span>tag</span><span>[旋转 20°]) </span><span>#h</span><span>(</span><span>1cm</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#scale</span><span>(</span><span>x</span><span>: </span><span>150%</span><span>, </span><span>y</span><span>: </span><span>80%</span><span>, </span><span>tag</span><span>[缩放]) </span><span>#h</span><span>(</span><span>1cm</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#skew</span><span>(</span><span>ax</span><span><span>: </span><span>-</span></span><span>15deg</span><span>, </span><span>tag</span><span>[斜切]) </span><span>#h</span><span>(</span><span>1cm</span><span>)</span></div></div><div><div><div>11</div></div><div><span>#move</span><span>(</span><span>dx</span><span>: </span><span>6pt</span><span>, </span><span>dy</span><span><span>: </span><span>-</span></span><span>6pt</span><span>, </span><span>tag</span><span>[平移])</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>// `rotate(reflow: true)` makes the layout follow the rotation.</span></div></div><div><div><div>14</div></div><div><span>#rotate</span><span>(</span><span>90deg</span><span>, </span><span>reflow</span><span>: </span><span>true</span><span>, </span><span>tag</span><span>[reflow])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-transforms.png" alt="transforms.typ 的渲染：六个带边框的小盒子竖着排成一列，分别是原样、旋转 20 度、横向放大纵向缩小、向左斜切、平移和旋转 90 度的 reflow 盒子，最后一个盒子是竖排的，它占的高度比横着时大" /><figcaption>transforms.typ 的渲染：六个带边框的小盒子竖着排成一列，分别是原样、旋转 20 度、横向放大纵向缩小、向左斜切、平移和旋转 90 度的 reflow 盒子，最后一个盒子是竖排的，它占的高度比横着时大</figcaption></figure><p></p><p>四个变换 <code>rotate</code>、<code>scale</code>、<code>skew</code>、<code>move</code>：</p><ul>
<li><code>rotate(20deg, ...)</code> 绕内容中心旋转，<code>origin</code> 可以改旋转中心。</li>
<li><code>scale(x: 150%, y: 80%, ...)</code> 横纵独立缩放，只写一个百分比（<code>scale(120%, ...)</code>）是等比缩放。</li>
<li><code>skew(ax: -15deg, ...)</code> 沿水平方向斜切，<code>ay</code> 是垂直方向。</li>
<li><code>move(dx: 6pt, dy: -6pt, ...)</code> 平移。</li>
</ul><p>这一节开头的注释 “Transforms change appearance, not layout” 是核心：<strong>变换后的内容在布局里占的仍然是变换之前的尺寸。</strong> 那个文件里六个盒子竖着排，是因为它们都是块级元素（上一节的结论），不是变换造成的。真正看占位，要放到行里（<code>/tmp/p6/post/transforms-inline.typ</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>8cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>t</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>box</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>3pt</span><span>, </span><span>body</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>前 </span><span>#box</span><span>(</span><span>rotate</span><span>(</span><span>20deg</span><span>, </span><span>t</span><span>[旋转])) </span><span>#box</span><span>(</span><span>scale</span><span>(</span><span>150%</span><span>, </span><span>t</span><span>[缩放]))</span></div></div><div><div><div>6</div></div><div><span>#box</span><span>(</span><span>skew</span><span>(</span><span>ax</span><span>: </span><span>20deg</span><span>, </span><span>t</span><span>[斜切])) 后。变换只改外观，</span></div></div><div><div><div>7</div></div><div><span>每个盒子在行里占的仍是原尺寸。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-transforms-inline.png" alt="一行文字中间有三个带边框的盒子：旋转的、放大的、斜切的，它们互相重叠，放大的盒子明显盖住了左右邻居" /><figcaption>一行文字中间有三个带边框的盒子：旋转的、放大的、斜切的，它们互相重叠，放大的盒子明显盖住了左右邻居</figcaption></figure><p></p><p>放大 150% 的“缩放”盒子压住了左右邻居，旋转和斜切的盒子也彼此交叠：布局只给每个盒子留了原尺寸的位置，变换后的图形溢出来。如果希望布局跟着变，就像 transforms.typ 最后一行那样用 <code>rotate(90deg, reflow: true, ...)</code>，渲染图里最后一个盒子是竖着的，它占的高度就是旋转之后的高度。</p></section>
<section><h2>layout 与 measure：在布局阶段读尺寸<a href="#layout-与-measure在布局阶段读尺寸"><span>#</span></a></h2><p>前面两节已经借用过它们了。这里把它们讲清楚。</p><ul>
<li><code>layout(size =&gt; ...)</code>：回调里能拿到<strong>当前容器</strong>分到的宽高。内容不是在书写时确定，而是在放进具体容器时才生成。</li>
<li><code>measure(content)</code>：返回一个有 <code>width</code> 和 <code>height</code> 的字典，是内容<strong>自然尺寸</strong>。<code>measure(width: 3cm, content)</code> 在限定宽度下重新折行再量。</li>
</ul><p><code>/tmp/p6/post/layout.typ</code>，我让它同时展示三件事：容器尺寸、按容器收缩内容、量未折行和折行后的尺寸：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// `layout` hands you the size of the box it lives in.</span></div></div><div><div><div>5</div></div><div><span>#block</span><span>(</span><span>width</span><span>: </span><span>6cm</span><span>, </span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>3pt</span><span>, </span><span>layout</span><span>(</span><span>size</span><span> </span><span>=&gt;</span><span> [</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>宽 </span><span>#size</span><span>.</span><span>width</span><span>，</span><span>高</span><span> </span><span>#size</span><span>.</span><span>height</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>`50%`</span><span> 换算为 </span><span>#</span><span>(</span><span>50%</span><span><span> </span><span>*</span><span> </span></span><span>size</span><span>.</span><span>width</span><span>)</span></div></div><div><div><div>8</div></div><div><span>]))</span></div></div><div><div><div>9</div></div><div><span>#box</span><span>(</span><span>width</span><span>: </span><span>5cm</span><span>, </span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>3pt</span><span>, </span><span>layout</span><span>(</span><span>size</span><span> </span><span>=&gt;</span><span> [宽 </span><span>#size</span><span>.</span><span>width</span><span>]))</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// Shrink to fit: scale down only when the content is wider than its box.</span></div></div><div><div><div>12</div></div><div><span>#let</span><span> </span><span>fit</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>layout</span><span>(</span><span>size</span><span> </span><span>=&gt;</span><span> {</span></div></div><div><div><div>13</div></div><div><span>  </span><span>let</span><span> </span><span>natural</span><span><span> </span><span>= </span></span><span>measure</span><span>(</span><span>body</span><span>).</span><span>width</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> </span><span>natural</span><span><span> </span><span>&gt;</span><span> </span></span><span>size</span><span>.</span><span>width</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>scale</span><span>(</span><span>size</span><span>.</span><span>width</span><span><span> </span><span>/</span><span> </span></span><span>natural</span><span><span> </span><span>*</span><span> </span></span><span>100%</span><span>, </span><span>reflow</span><span>: </span><span>true</span><span>, </span><span>body</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>body</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>})</span></div></div><div><div><div>20</div></div><div><span>#block</span><span>(</span></div></div><div><div><div>21</div></div><div><span>  </span><span>width</span><span>: </span><span>5cm</span><span>,</span></div></div><div><div><div>22</div></div><div><span>  </span><span>stroke</span><span>: </span><span>0.5pt</span><span>,</span></div></div><div><div><div>23</div></div><div><span>  </span><span>inset</span><span>: </span><span>3pt</span><span>,</span></div></div><div><div><div>24</div></div><div><span>  </span><span>fit</span><span>[一行很长很长很长很长很长很长的文字],</span></div></div><div><div><div>25</div></div><div><span>)</span></div></div><div><div><div>26</div></div><div><span>#block</span><span>(</span><span>width</span><span>: </span><span>5cm</span><span>, </span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>3pt</span><span>, </span><span>fit</span><span>[短文字])</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>// `measure` needs a context; give it a width to measure wrapped text.</span></div></div><div><div><div>29</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>30</div></div><div><span>  </span><span>let</span><span> </span><span>m</span><span><span> </span><span>= </span></span><span>measure</span><span>[一行很长很长很长很长很长很长的文字]</span></div></div><div><div><div>31</div></div><div><span>  </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>measure</span><span>(</span><span>width</span><span>: </span><span>3cm</span><span>)[一行很长很长很长很长很长很长的文字]</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>[自然宽 </span><span>#m</span><span>.</span><span>width</span><span>，</span><span>高</span><span> </span><span>#m</span><span>.</span><span>height</span><span>；</span><span>限宽</span><span> 3cm 后宽 </span><span>#n</span><span>.</span><span>width</span><span>，</span><span>高</span><span> </span><span>#n</span><span>.</span><span>height</span><span>]</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>宽 164.08pt，高 float.inf * 1pt</span></div></div><div><div><div>2</div></div><div><span>50% 换算为 82.04pt</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>宽 135.73pt</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>一行很长很长很长很长很长很长的文字</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>短文字</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>自然宽 187pt，高 8.02pt；限宽 3cm 后宽 77pt，高 38.36pt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-layout-fn.png" alt="layout.typ 的渲染：第一个框里写着宽 164.08pt 和高 float.inf * 1pt，下面一行是 50% 的换算；第二个框写着宽 135.73pt；第三个框里长句被缩小后刚好放进框内；第四个框里短文字保持原样；最后一行是自然宽与限宽后的数值" /><figcaption>layout.typ 的渲染：第一个框里写着宽 164.08pt 和高 float.inf * 1pt，下面一行是 50% 的换算；第二个框写着宽 135.73pt；第三个框里长句被缩小后刚好放进框内；第四个框里短文字保持原样；最后一行是自然宽与限宽后的数值</figcaption></figure><p></p><p>几个读数：</p><ul>
<li>6cm 宽的块，减去 <code>inset: 3pt</code> 的两侧，内部宽 170.08 − 6 = <strong>164.08pt</strong>，<code>layout</code> 看到的是减过内边距的内部宽度。<code>50%</code> 乘这个宽度得到 82.04pt。这是 <code>layout</code> 最常见的用途：把比例换算成绝对长度。</li>
<li><code>size.height</code> 是 <code>float.inf * 1pt</code>：我给的 <code>block</code> 没有固定高度，高度向下不设上限，所以 <code>layout</code> 只能说“无限”。需要真实高度时容器要显式给 <code>height</code>。</li>
<li><code>fit</code> 是一个自己写的“缩小以适应”：自然宽 187pt 的长句放不进 5cm（141.73pt）的框，就用 <code>scale(.., reflow: true)</code> 按 <code>size.width / natural</code> 缩小，渲染图上它确实塞进了框里；短句没超，原样返回。注意 <code>layout</code> 的回调里可以直接调 <code>measure</code>，不需要再写 <code>context</code>。</li>
<li>限宽 3cm 后同样一句话折成了 3 行：宽度 77pt（每行 7 个字，每字 11pt），高度从 8.02pt 涨到 38.36pt。这个数字对得上：3 × 8.02 + 2 × 7.15 = 38.36，7.15pt 是 11pt 字号下 <code>0.65em</code> 的行距（<code>par.leading</code> 的默认值，我是从算术对上才认出来的，没有另外查）。</li>
</ul><p><code>measure</code> 在没有 <code>context</code> 的地方会被拒绝：</p><div><figure><figcaption><span>measure[x].width</span></figcaption><pre><code></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: can only be used when context is known</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ measure-fail.typ:1:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #measure[x].width</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= hint: try wrapping this in a `context` expression</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= hint: the `context` expression should wrap everything that depends on this function</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为内容的自然尺寸取决于周围的样式（字号、字体），所以必须知道上下文。<code>layout</code> 的回调里已经有上下文，<code>#context</code> 则是给没有 <code>layout</code> 的地方用的。<code>context</code> 是下一篇的主题，这里先当成“告诉 Typst 这段表达式要在知道环境之后再算”的标记。</p></section>
<section><h2>page：纸张、边距、页眉页脚、背景<a href="#page纸张边距页眉页脚背景"><span>#</span></a></h2><p><code>page</code> 和前面的元素不太一样，它不是流里的一个块，而是<strong>页面容器</strong>本身的参数，所以几乎总是用 <code>set</code> 设置。<code>lab/ch06/page.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch06/page.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `page` is a set-rule target: size, margins, header, footer, numbering.</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span></div></div><div><div><div>3</div></div><div><span>  </span><span>paper</span><span>: </span><span>"a5"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>margin</span><span>: (</span><span>x</span><span>: </span><span>2cm</span><span>, </span><span>top</span><span>: </span><span>2.5cm</span><span>, </span><span>bottom</span><span>: </span><span>2cm</span><span>),</span></div></div><div><div><div>5</div></div><div><span>  </span><span>binding</span><span>: </span><span>left</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>header</span><span>: </span><span>context</span><span> [</span></div></div><div><div><div>7</div></div><div><span>    </span><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>9pt</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>第 </span><span>#counter</span><span>(</span><span>page</span><span>).</span><span>display</span><span>() 页</span></div></div><div><div><div>9</div></div><div><span>    </span><span>#h</span><span>(</span><span>1fr</span><span>) </span><span>`page`</span><span> 演示</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>11</div></div><div><span>  </span><span>footer</span><span>: </span><span>align</span><span>(</span><span>center</span><span>, </span><span>context</span><span> </span><span>counter</span><span>(</span><span>page</span><span>).</span><span>display</span><span>(</span><span>"1 / 1"</span><span>, </span><span>both</span><span>: </span><span>true</span><span>)),</span></div></div><div><div><div>12</div></div><div><span>  </span><span>numbering</span><span>: </span><span>"1"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#fffdf5"</span><span>),</span></div></div><div><div><div>14</div></div><div><span>  </span><span>background</span><span>: </span><span>place</span><span>(</span><span>center</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>, </span><span>text</span><span>(</span><span>60pt</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>240</span><span>), </span><span>"DRAFT"</span><span>)),</span></div></div><div><div><div>15</div></div><div><span>  </span><span>bleed</span><span>: </span><span>3mm</span><span>,</span></div></div><div><div><div>16</div></div><div><span>)</span></div></div><div><div><div>17</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>=</span><span> 第一页</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>正文。</span><span>#lorem</span><span>(</span><span>60</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>#pagebreak</span><span>()</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>=</span><span> 第二页</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>`pagebreak`</span><span> 强制换页；</span><span>`pagebreak(weak: true)`</span><span> 在已经是新页时不再多换一页。</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>#pagebreak</span><span>(</span><span>weak</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>columns</span><span>: </span><span>2</span><span>)</span></div></div><div><div><div>32</div></div><div><span>=</span><span> 双栏</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>#lorem</span><span>(</span><span>80</span><span>)</span></div></div><div><div><div>35</div></div><div><span>#colbreak</span><span>()</span></div></div><div><div><div>36</div></div><div><span>右栏从这里开始。</span><span>#lorem</span><span>(</span><span>40</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编出来是 3 页。第 1 页：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-page-first.png" alt="page.typ 第 1 页：A5 纸，淡黄色底，页眉左边是第 1 页、右边是 page 演示，一级标题第一页和一小段正文，页面中间有很淡的 DRAFT 水印，页脚居中写着 1 / 3" /><figcaption>page.typ 第 1 页：A5 纸，淡黄色底，页眉左边是第 1 页、右边是 page 演示，一级标题第一页和一小段正文，页面中间有很淡的 DRAFT 水印，页脚居中写着 1 / 3</figcaption></figure><p></p><p>第 3 页，双栏：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-page-columns.png" alt="page.typ 第 3 页：页眉是第 3 页，标题双栏横在左栏顶端，左栏是一整栏 Lorem ipsum，右栏以右栏从这里开始起头，页面中部同样有很淡的 DRAFT 水印，页脚是 3 / 3" /><figcaption>page.typ 第 3 页：页眉是第 3 页，标题双栏横在左栏顶端，左栏是一整栏 Lorem ipsum，右栏以右栏从这里开始起头，页面中部同样有很淡的 DRAFT 水印，页脚是 3 / 3</figcaption></figure><p></p><p>逐个参数：</p><ul>
<li><strong><code>paper</code></strong>：<code>"a5"</code>、<code>"a4"</code>、<code>"us-letter"</code> 之类的纸型名；也可以直接给 <code>width</code> 和 <code>height</code>，还可以设 <code>flipped: true</code> 横置。省略时是 A4：我用 <code>context [页宽 #page.width ...]</code> 读出默认值，页宽 595.28pt，页高 841.89pt，<code>pdfinfo</code> 报告 595.276 x 841.89 pts (A4)。</li>
<li><strong><code>margin</code></strong>：可以是一个长度（四边相同），也可以是字典：<code>(x: 2cm, top: 2.5cm, bottom: 2cm)</code> 如这个文件所写；<code>rest</code> 表示“其余各边”，<code>(rest: 1cm, top: 2cm)</code> 我试过，左边缘落在 28.35pt 也就是 1cm 处；<code>inside</code> 与 <code>outside</code> 用于装订，见下面。默认值是 <code>auto</code>：我读到 A4 默认页面的正文左缘在 70.87pt，正好是 25mm。（我记得公式是页面短边的 2.5/21，这里只验证了 A4 这一个结果。）</li>
<li><strong><code>header</code> 与 <code>footer</code></strong>：任意内容，画在页边距区域里。这里页眉用了 <code>context</code> 和 <code>counter(page).display()</code> 显示当前页码，页脚用 <code>display("1 / 1", both: true)</code> 显示“当前 / 总数”，渲染图里是 <code>1 / 3</code>。计数器与 <code>context</code> 留到下一篇系统地讲。</li>
<li><strong><code>numbering</code></strong>：页码的格式，和标题编号一样吃 <code>"1"</code>、<code>"一"</code>、<code>"i"</code> 这类字符串或函数。</li>
<li><strong><code>fill</code></strong>：纸张底色。</li>
<li><strong><code>background</code></strong>：画在正文下面的东西，这里用 <code>place(center + horizon, ...)</code> 放了一个很淡的 <code>DRAFT</code>。每一页都会重复一次。</li>
<li><strong><code>columns</code></strong>：整页分栏。这个文件最后的 <code>set page(columns: 2)</code> 之后，标题和正文都进了两栏，<code>colbreak()</code> 把右栏起头的位置钉住。</li>
<li><strong><code>bleed</code></strong>：出血。<code>bleed: 3mm</code> 让页面的实际尺寸向四周扩出 3mm，用于印刷裁切。我用 <code>/tmp/p6/post/bleed.typ</code> 验证：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>6cm</span><span>, </span><span>height</span><span>: </span><span>4cm</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>, </span><span>bleed</span><span>: </span><span>3mm</span><span>, </span><span>fill</span><span>: </span><span>yellow</span><span>)</span></div></div><div><div><div>2</div></div><div><span>出血 3mm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdfinfo -box bleed.pdf | grep -E "Media|Trim|Bleed"</span></div></div><div><div><div>2</div></div><div><span>MediaBox:            0.00     0.00   187.09   130.39</span></div></div><div><div><div>3</div></div><div><span>BleedBox:            0.00     0.00   187.09   130.39</span></div></div><div><div><div>4</div></div><div><span>TrimBox:             8.50     8.50   178.58   121.89</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>6cm × 4cm 的页面是 170.08 × 113.39pt，每边多出 3mm = 8.50pt，MediaBox 变成 187.09 × 130.39pt；<code>TrimBox</code> 的 8.50 到 178.58 标出裁切线，印刷厂靠它知道哪里该切。<code>fill: yellow</code> 铺满了整个出血区，这正是出血要的效果。</p><section><h3>装订、foreground 与强制换页<a href="#装订foreground-与强制换页"><span>#</span></a></h3><p><code>binding</code> 决定哪一侧是装订侧，<code>margin</code> 里的 <code>inside</code> 和 <code>outside</code> 配合它让左右页的边距互为镜像。<code>/tmp/p6/post/binding.typ</code>，每页用 <code>here().position().x</code> 报告正文左缘：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span></div></div><div><div><div>2</div></div><div><span>  </span><span>width</span><span>: </span><span>8cm</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>height</span><span>: </span><span>3cm</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>margin</span><span>: (</span><span>inside</span><span>: </span><span>3cm</span><span>, </span><span>outside</span><span>: </span><span>0.5cm</span><span>),</span></div></div><div><div><div>5</div></div><div><span>  </span><span>binding</span><span>: </span><span>left</span><span>,</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>9pt</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>x</span><span><span>() </span><span>= </span></span><span>context</span><span> [第 </span><span>#here</span><span>().</span><span>page</span><span>() 页，正文左缘 </span><span>#calc</span><span>.</span><span>round</span><span>(</span><span>here</span><span>().</span><span>position</span><span>().</span><span>x</span><span>.</span><span>pt</span><span>(), </span><span>digits</span><span>: </span><span>1</span><span>)pt]</span></div></div><div><div><div>9</div></div><div><span>#x</span><span>()</span></div></div><div><div><div>10</div></div><div><span>#pagebreak</span><span>()</span></div></div><div><div><div>11</div></div><div><span>#x</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>第 1 页，正文左缘 85pt</span></div></div><div><div><div>2</div></div><div><span>第 2 页，正文左缘 14.2pt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>奇数页的装订侧在左边，所以 <code>inside: 3cm</code>（85pt）落在左边；偶数页镜像过来，<code>inside</code> 在右边，左边只剩 <code>outside: 0.5cm</code>（14.2pt）。我把 <code>binding: left</code> 改成 <code>binding: right</code>，两页的读数互换：第 1 页 14.2pt，第 2 页 85pt。双面打印的书稿用这一组参数。</p><p><code>foreground</code> 和 <code>background</code> 对称：前者画在正文<strong>上面</strong>。<code>/tmp/p6/post/foreground.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span></div></div><div><div><div>2</div></div><div><span>  </span><span>width</span><span>: </span><span>8cm</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>height</span><span>: </span><span>4cm</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>margin</span><span>: </span><span>12pt</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>background</span><span>: </span><span>place</span><span>(</span><span>center</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>, </span><span>text</span><span>(</span><span>40pt</span><span>, </span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>225</span><span>), [背景])),</span></div></div><div><div><div>6</div></div><div><span>  </span><span>foreground</span><span>: </span><span>place</span><span>(</span><span>top</span><span><span> </span><span>+</span><span> </span></span><span>right</span><span>, </span><span>dx</span><span><span>: </span><span>-</span></span><span>6pt</span><span>, </span><span>dy</span><span>: </span><span>6pt</span><span>, </span><span>rotate</span><span><span>(</span><span>-</span></span><span>12deg</span><span>, </span><span>rect</span><span>(</span></div></div><div><div><div>7</div></div><div><span>    </span><span>stroke</span><span>: </span><span>red</span><span><span> </span><span>+</span><span> </span></span><span>1pt</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>inset</span><span>: </span><span>3pt</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>text</span><span>(</span><span>red</span><span>, </span><span>size</span><span>: </span><span>9pt</span><span>)[已审阅],</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>))),</span></div></div><div><div><div>11</div></div><div><span>)</span></div></div><div><div><div>12</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>13</div></div><div><span>背景在正文下面，前景盖在正文上面。</span><span>#lorem</span><span>(</span><span>15</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-foreground.png" alt="一个小页面：浅灰色的背景两个字在正文下方，正文的第一行末尾被一个红色边框、略微倾斜的已审阅图章盖住了一部分" /><figcaption>一个小页面：浅灰色的背景两个字在正文下方，正文的第一行末尾被一个红色边框、略微倾斜的已审阅图章盖住了一部分</figcaption></figure><p></p><p><code>background</code> 里的浅灰大字被正文压在下面，<code>foreground</code> 里的红色图章盖住了第一行的句号。水印用前者，审阅章、裁切标记用后者。</p><p>换页方面，<code>page.typ</code> 这个 lab 已经演示了 <code>pagebreak()</code> 和 <code>pagebreak(weak: true)</code>。还有两个常用变体（<code>/tmp/p6/post/breaks.typ</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>6cm</span><span>, </span><span>height</span><span>: </span><span>3cm</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>第 1 页</span></div></div><div><div><div>4</div></div><div><span>#pagebreak</span><span>(</span><span>to</span><span>: </span><span>"odd"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>这里是奇数页，第 </span><span>#context</span><span> </span><span>here</span><span>().</span><span>page</span><span>() </span><span>页</span></div></div><div><div><div>6</div></div><div><span>#pagebreak</span><span>(</span><span>to</span><span>: </span><span>"odd"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>下一个奇数页，第 </span><span>#context</span><span> </span><span>here</span><span>().</span><span>page</span><span>() </span><span>页</span></div></div><div><div><div>8</div></div><div><span>#page</span><span>(</span></div></div><div><div><div>9</div></div><div><span>  </span><span>paper</span><span>: </span><span>"a6"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>flipped</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>margin</span><span>: </span><span>1cm</span><span>,</span></div></div><div><div><div>12</div></div><div><span>)[用 </span><span>`page(..)[..]`</span><span> 单独开一页：A6 横向]</span></div></div><div><div><div>13</div></div><div><span>#page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>4pt</span><span>)[</span><span>`auto`</span><span> 尺寸贴着内容]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Pages: 7</span></div></div><div><div><div>2</div></div><div><span>Page    1 size:  170.079 x 85.0394 pts</span></div></div><div><div><div>3</div></div><div><span>Page    2 size:  170.079 x 85.0394 pts</span></div></div><div><div><div>4</div></div><div><span>Page    3 size:  170.079 x 85.0394 pts</span></div></div><div><div><div>5</div></div><div><span>Page    4 size:  170.079 x 85.0394 pts</span></div></div><div><div><div>6</div></div><div><span>Page    5 size:  170.079 x 85.0394 pts</span></div></div><div><div><div>7</div></div><div><span>Page    6 size:  419.528 x 297.638 pts</span></div></div><div><div><div>8</div></div><div><span>Page    7 size:  97.9422 x 16.019 pts</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>pagebreak(to: "odd")</code> 保证下一页是奇数页，必要时塞入空白页：第 1 页的下一个奇数页是第 3 页，第 2 页是自动补的空白页；再来一次，第 4 页空白，内容落在第 5 页，共 5 页。我用 <code>pdftotext -f N -l N</code> 逐页确认：第 3 页是“这里是奇数页，第 3 页”，第 5 页是“下一个奇数页，第 5 页”，第 2、4 页是空的。章首右页起排就靠它。</li>
<li><code>page(...)[...]</code> 是函数形式：只给这一块内容单独开一页，不影响后面。第 6 页是 A6 横向（419.528 × 297.638pt），第 7 页 <code>width: auto, height: auto</code> 贴着内容收缩，只有 97.94 × 16.02pt，一个字条大小的 PDF 页面。仓库里的 <code>fig-*.typ</code> 用 <code>#set page(width: auto, height: auto, margin: 12pt)</code> 做同样的事：页面贴着内容，截出来的 PNG 没有多余的白边。</li>
</ul><p>还有一个陷阱：<strong>在文档中间 <code>set page(...)</code> 会强制换页</strong>，页面参数是整页的属性，改了就得另起一页。<code>/tmp/p6/post/midset.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>6cm</span><span>, </span><span>height</span><span>: </span><span>3cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>第一页</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>fill</span><span>: </span><span>yellow</span><span>)</span></div></div><div><div><div>5</div></div><div><span>这一行在第 </span><span>#context</span><span> </span><span>here</span><span>().</span><span>page</span><span>() </span><span>页</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Pages: 2</span></div></div><div><div><div>2</div></div><div><span>这一行在第 2 页。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我没有写 <code>pagebreak</code>，只是改了底色，文档就多出了一页。这也解释了 page.typ lab 为什么只有 3 页而不是 4 页：<code>pagebreak(weak: true)</code> 已经把内容带到第 3 页了，随后的 <code>set page(columns: 2)</code> 要求换页，但当前页还是空的，不会再多出一页。</p></section></section>
<section><h2>xuan 的 page.typ<a href="#xuan-的-pagetyp"><span>#</span></a></h2><p>上面这些零件，<code>xuan</code> 只用到很小的一部分，因为选择已经在 <code>note</code> 里做完了：纸张默认 A4，边距 2.5cm。<code>src/page.typ</code> 全文 40 行（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/page.typ" target="_blank">源文件</a>）。它是<strong>最终版</strong>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Paper and margins.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>xuan-page</span><span>(</span><span>body</span><span>, </span><span>paper</span><span>: </span><span>"a4"</span><span>, </span><span>margin</span><span>: </span><span>2.5cm</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>set</span><span> </span><span>page</span><span>(</span><span>paper</span><span>: </span><span>paper</span><span>, </span><span>margin</span><span>: </span><span>margin</span><span>)</span></div></div><div><div><div>4</div></div><div><span>  </span><span>body</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/// Centered title, subtitle, authors and date.</span></div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>title-block</span><span>(</span></div></div><div><div><div>9</div></div><div><span>  </span><span>title</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>subtitle</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>author</span><span>: (),</span></div></div><div><div><div>12</div></div><div><span>  </span><span>date</span><span>: </span><span>auto</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>15</div></div><div><span>  </span><span>let</span><span> </span><span>authors</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>type</span><span>(</span><span>author</span><span><span>) </span><span>==</span><span> </span></span><span>array</span><span> { </span><span>author</span><span> } </span><span>else</span><span> { (</span><span>author</span><span>,) }</span></div></div><div><div><div>16</div></div><div><span>  </span><span>let</span><span> </span><span>shown</span><span><span> </span><span>= </span></span><span>if</span><span> </span><span>date</span><span><span> </span><span>==</span><span> </span></span><span>auto</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>today</span><span><span> </span><span>= </span></span><span>datetime</span><span>.</span><span>today</span><span>()</span></div></div><div><div><div>18</div></div><div><span>    </span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> {</span></div></div><div><div><div>19</div></div><div><span>      </span><span>today</span><span>.</span><span>display</span><span>(</span><span>"[year] 年 [month padding:none] 月 [day padding:none] 日"</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>21</div></div><div><span>      </span><span>today</span><span>.</span><span>display</span><span>(</span><span>"[month repr:long] [day padding:none], [year]"</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>type</span><span>(</span><span>date</span><span><span>) </span><span>==</span><span> </span></span><span>datetime</span><span> {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>date</span><span>.</span><span>display</span><span>(</span><span>"[year]-[month]-[day]"</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>date</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>  </span><span>align</span><span>(</span><span>center</span><span>, </span><span>block</span><span>(</span><span>width</span><span>: </span><span>100%</span><span>, </span><span>spacing</span><span>: </span><span>2em</span><span>)[</span></div></div><div><div><div>29</div></div><div><span>    </span><span>#if</span><span> </span><span>title</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>30</div></div><div><span>      </span><span>block</span><span>(</span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.8em</span><span>, </span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>title</span><span>), </span><span>below</span><span>: </span><span>0.6em</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>    </span><span>#if</span><span> </span><span>subtitle</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>33</div></div><div><span>      </span><span>block</span><span>(</span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.2em</span><span>, </span><span>subtitle</span><span>), </span><span>below</span><span>: </span><span>0.8em</span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>    </span><span>#if</span><span> </span><span>authors</span><span>.</span><span>len</span><span><span>() </span><span>&gt;</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>36</div></div><div><span>      </span><span>block</span><span>(</span><span>authors</span><span>.</span><span>join</span><span>(</span><span>h</span><span>(</span><span>1.5em</span><span>)), </span><span>below</span><span>: </span><span>0.6em</span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>    </span><span>#if</span><span> </span><span>shown</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> { </span><span>shown</span><span> }</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>])</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>xuan-page：为什么 set 写在函数里也管用<a href="#xuan-page为什么-set-写在函数里也管用"><span>#</span></a></h3><p><code>xuan-page(body, ...)</code> 的形状是第 4 篇讲的模板形状：最后一个位置参数是 <code>body</code>，通过 <code>#show: xuan-page.with(paper: "a4")</code> 接收“文档剩下的所有内容”。函数体第一行 <code>set page(...)</code> 的作用域是这个函数体，也就是后面返回的 <code>body</code>；<code>body</code> 里的内容整个落在这条规则之下。这和第 4 篇 <code>scope.typ</code> 里 <code>let f() = { set text(fill: red) ... }</code> 是同一个机制。</p><p><code>note</code> 在调用 <code>xuan-page</code> 之后又写了 <code>set page(header: ..., footer: ...)</code>：这是第二条 <code>set page</code>，只设了页眉页脚，和第一条设的字段不重叠，两条规则一起生效，所以 <code>paper</code> 和 <code>margin</code> 还在。又因为这两条规则都出现在页面还空着的时候，不会像上一节的 <code>midset.typ</code> 那样额外换页（我用 <code>set page(paper: "a6")</code> 后接 <code>set page(fill: yellow)</code> 试过，没有多出页面）。</p><p>页边距为什么选 2.5cm？A4 的 <code>margin: auto</code> 在上面验证过是 25mm，所以对 A4 来说 <code>xuan-page</code> 的默认就是 Typst 的默认值。我把它写成显式参数，是为了让调用方一眼知道页面是 A4，而且能改（<code>note(paper: "a5")</code> 往下传的就是这个参数）。</p></section><section><h3>title-block：一个把几种输入统一成显示文字的函数<a href="#title-block一个把几种输入统一成显示文字的函数"><span>#</span></a></h3><p><code>title-block</code> 不排版页面，它排“标题、副标题、作者、日期”四行，用上面讲过的 <code>align</code>、<code>block</code>、<code>h</code> 搭起来：</p><ul>
<li><strong>作者归一化</strong>：<code>author</code> 可以是字符串，也可以是数组。第一行 <code>if type(author) == array { author } else { (author,) }</code> 统一成数组，空数组 <code>()</code> 则不显示作者那一行（后面的 <code>authors.len() &gt; 0</code>）。</li>
<li><strong>日期的三种来源</strong>：<code>auto</code> 取今天，中文格式 <code>2026 年 10 月 19 日</code>（<code>[month padding:none]</code> 去掉前导零），英文格式 <code>October 19, 2026</code>；传入 <code>datetime</code> 就显示 ISO 风格的 <code>2026-10-19</code>；其他值（字符串、内容）原样显示，所以可以写 <code>date: "Draft 2"</code>。我分别编译了这三种。<code>auto</code> 的中英文输出（今天，在 nix shell 里）是 <code>2026 年 10 月 19 日</code> 和 <code>October 19, 2026</code>。</li>
<li><strong>垂直结构</strong>：外面是 <code>align(center, block(width: 100%, spacing: 2em)[...])</code>，里面每一行是一个单独的 <code>block</code>，用 <code>below:</code> 控制它和下一行的间距：标题 0.6em、副标题 0.8em、作者 0.6em。块间距这一块用的就是前面讲的块流模型：竖向距离靠 <code>block.below</code> 和 <code>spacing</code> 决定，不需要手写 <code>v()</code>。</li>
<li><strong>作者之间的间距</strong>：<code>authors.join(h(1.5em))</code> 把作者名用一个 1.5em 的水平间距连起来，不加逗号，保持页面简洁。</li>
<li><strong><code>#if title != none { block(...) }</code></strong>：这是一段内容块里的代码块，<code>if</code> 为假时什么也不产生，所以缺省的标题、副标题不会留下空行。</li>
</ul><p><code>/tmp/p6/post/xuan-title.typ</code>（日期用 <code>datetime(...)</code> 写死是为了让输出每天一样；<code>#set page(height: auto)</code> 只是为了截图，不是 xuan 的一部分）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>xuan-page</span><span>.</span><span>with</span><span>(</span><span>paper</span><span>: </span><span>"a6"</span><span>, </span><span>margin</span><span>: </span><span>1.5cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>// Shrink the page to its content, only to keep the screenshot small.</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>height</span><span>: </span><span>auto</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>))</span></div></div><div><div><div>6</div></div><div><span>#title-block</span><span>(</span></div></div><div><div><div>7</div></div><div><span>  </span><span>title</span><span>: </span><span>"线性代数笔记"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>subtitle</span><span>: </span><span>"最小二乘与 SVD"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>author</span><span>: (</span><span>"张三"</span><span>, </span><span>"李四"</span><span>),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>date</span><span>: </span><span>datetime</span><span>(</span><span>year</span><span>: </span><span>2026</span><span>, </span><span>month</span><span>: </span><span>10</span><span>, </span><span>day</span><span>: </span><span>19</span><span>),</span></div></div><div><div><div>11</div></div><div><span>)</span></div></div><div><div><div>12</div></div><div><span>#title-block</span><span>(</span></div></div><div><div><div>13</div></div><div><span>  </span><span>title</span><span>: </span><span>"Notes on SVD"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>author</span><span>: </span><span>"Zhang San"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>date</span><span>: </span><span>"Draft 2"</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>lang</span><span>: </span><span>"en"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch06-xuan-title.png" alt="A6 纸：上面是居中的粗体中文标题线性代数笔记，下面是副标题、两位作者名和日期 2026-10-19，再往下是英文标题 Notes on SVD，作者 Zhang San，日期位置写着 Draft 2" /><figcaption>A6 纸：上面是居中的粗体中文标题线性代数笔记，下面是副标题、两位作者名和日期 2026-10-19，再往下是英文标题 Notes on SVD，作者 Zhang San，日期位置写着 Draft 2</figcaption></figure><p></p><p>第一块是中文标题，作者名之间隔着 1.5em，日期显示为 ISO 格式；第二块里 <code>author: "Zhang San"</code> 是个字符串，被包成单元素数组，<code>date: "Draft 2"</code> 原样显示。<code>lang: "en"</code> 在这里只影响 <code>date: auto</code> 的格式，这个例子里没用上。</p><p><code>page.typ</code> 为什么把这两个函数放在一起？因为它们都在回答同一个问题：文档第一页的物理形状是什么。<code>xuan-page</code> 决定纸，<code>title-block</code> 决定纸上第一块内容。其余的页面特性（页眉、页脚、页码）放在 <code>header.typ</code>，因为它们要靠 <code>context</code> 和计数器才能写，那是下一篇的事。</p></section></section>
<section><h2>下一步<a href="#下一步"><span>#</span></a></h2><p><code>xuan-header</code> 里有一句 <code>query(heading.where(level: 1).before(here()))</code>，它要回答“这一页上方最近的章标题是什么”。这句话为什么要放在 <code>context</code> 里、<code>here()</code> 在这里又是什么，以及 Typst 为什么要把同一份文档排好几遍才收敛，是<a href="/posts/typst-by-xuan-07-context-counters-state/">第 7 篇</a>的内容。本篇用过的 <code>here().position()</code> 就是它的近亲。</p><p>如果想自己多做几个对照，仓库里 <code>lab/ch06/</code> 的 6 个文件都是现成的底稿：把 <code>blocks.typ</code> 里的 <code>inset</code> 改成 0 看边框怎么挤，把 <code>grid.typ</code> 的 <code>columns</code> 改成 <code>(1fr, 1fr, 1fr)</code> 再加一个宽内容，看 <code>auto</code> 和 <code>fr</code> 怎么争空间。<code>make ch06</code> 不到两秒。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-05-fonts-and-cjk/</id>
      <title type="text">字体与中文排版</title>
      <published>2026-10-19T04:00:00.000Z</published>
      <updated>2026-10-19T04:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-05-fonts-and-cjk/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 5 篇：我用 Typst 0.15.1 和 Noto CJK 可变字体编译了 ch05 的 6 个 lab 文件，量出 cjk-latin-spacing 开关相差 12 pt，查清字体列表按字符回退、0.15 的 variations 与 PDF 里 ExtraLight 字体名的来历，再把结论收进 xuan 的 fonts.typ 和 cjk.typ。</summary>
      <content type="html"><![CDATA[<p><code>pdffonts</code> 对一份常规字重的中文 PDF 打印出的字体名是 <code>NotoSerifCJKsc-ExtraLight</code>。我没有要过 ExtraLight，正文明明是常规字重。这个名字的答案要拆成三层才讲得清：Typst 怎么在一串字体里给每个字符挑字体，可变字体在 PDF 里怎么命名，以及中文排版里 <code>text</code> 和 <code>par</code> 的哪几个参数真正有用。这一篇把这三层逐个跑通，最后收成 <code>xuan</code> 的两个文件：<code>fonts.typ</code>（13 行）和 <code>cjk.typ</code>（21 行）。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>src/fonts.typ</code> 和 <code>src/cjk.typ</code>，两个都是<strong>最终版本</strong>，之后不会再改。第 4 篇的 <code>base.typ</code> 里已经有一行 <code>show: xuan-cjk.with(lang: lang)</code>，那一篇我把它当黑盒用，这一篇打开它。<code>lab/ch05/</code> 下的 6 个文件（含一个预期产生警告的）全部通过 <code>make ch05</code>。
前置：<a href="/posts/typst-by-xuan-04-set-and-show/">第 4 篇</a>（<code>set</code>、<code>show</code>、<code>xuan</code> 包骨架）。环境和第 1 篇一样：<code>nix develop</code> 里的 Typst 0.15.1，字体由 <code>flake.nix</code> 通过 <code>TYPST_FONT_PATHS</code> 提供。
在仓库里，<code>xuan</code> 的源文件位于 <code>packages/local/xuan/0.1.0/src/</code>，文中说的 <code>src/fonts.typ</code> 都指这个目录。</p></div></div>
<section><h2>先看 Typst 能看见哪些字体<a href="#先看-typst-能看见哪些字体"><span>#</span></a></h2><p>写 <code>set text(font: ...)</code> 之前，先问 Typst 它手里有什么。<code>typst fonts</code> 列出所有家族名，每行一个。我先在仓库的 <code>nix develop</code> 里跑加了 <code>--ignore-system-fonts</code> 的版本，这也是 <code>scripts/chapter.sh</code> 编译每个 lab 文件时用的参数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst fonts --ignore-system-fonts</span></div></div><div><div><div>2</div></div><div><span>DejaVu Sans Mono</span></div></div><div><div><div>3</div></div><div><span>Libertinus Serif</span></div></div><div><div><div>4</div></div><div><span>Maple Mono NF</span></div></div><div><div><div>5</div></div><div><span>New Computer Modern</span></div></div><div><div><div>6</div></div><div><span>New Computer Modern Math</span></div></div><div><div><div>7</div></div><div><span>Noto Sans CJK HK</span></div></div><div><div><div>8</div></div><div><span>Noto Sans CJK JP</span></div></div><div><div><div>9</div></div><div><span>Noto Sans CJK KR</span></div></div><div><div><div>10</div></div><div><span>Noto Sans CJK SC</span></div></div><div><div><div>11</div></div><div><span>Noto Sans CJK TC</span></div></div><div><div><div>12</div></div><div><span>Noto Sans Mono CJK HK</span></div></div><div><div><div>13</div></div><div><span>Noto Sans Mono CJK JP</span></div></div><div><div><div>14</div></div><div><span>Noto Sans Mono CJK KR</span></div></div><div><div><div>15</div></div><div><span>Noto Sans Mono CJK SC</span></div></div><div><div><div>16</div></div><div><span>Noto Sans Mono CJK TC</span></div></div><div><div><div>17</div></div><div><span>Noto Serif CJK HK</span></div></div><div><div><div>18</div></div><div><span>Noto Serif CJK JP</span></div></div><div><div><div>19</div></div><div><span>Noto Serif CJK KR</span></div></div><div><div><div>20</div></div><div><span>Noto Serif CJK SC</span></div></div><div><div><div>21</div></div><div><span>Noto Serif CJK TC</span></div></div><div><div><div>22</div></div><div><span>$ typst fonts --ignore-system-fonts | wc -l</span></div></div><div><div><div>23</div></div><div><span>20</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>20 个家族，来源有两处：</p><ul>
<li>DejaVu Sans Mono、Libertinus Serif、New Computer Modern、New Computer Modern Math 这 4 个<strong>嵌在 Typst 可执行文件里</strong>，不用装任何东西就能用。</li>
<li>其余 16 个来自 <code>TYPST_FONT_PATHS</code>：<code>flake.nix</code> 的 <code>shellHook</code> 把 Noto Serif CJK、Noto Sans CJK 和 Maple Mono NF CN 三个目录用 <code>:</code> 拼起来导出，其中 15 个是 Noto 家族，1 个是 Maple Mono NF。Maple Mono 只给第 1 篇的基准测试用，<code>xuan</code> 自己只点名大约 6 个家族：Libertinus Serif、Noto Serif CJK SC、Noto Sans CJK SC、DejaVu Sans Mono、Noto Sans Mono CJK SC、New Computer Modern Math。</li>
</ul><p>不加限制会怎样？我在同一个 shell 里直接跑 <code>typst fonts | wc -l</code>，得到 284，因为这台机器的系统字体里已经有 229 个 Noto 家族（<code>env -u TYPST_FONT_PATHS typst fonts | grep -c Noto</code> 的输出）。同一份源文件在另一台机器上会看见另一批字体，编译结果可能不一样。<strong>仓库里每个编译命令都带 <code>--ignore-system-fonts</code>，原因就在这里：让字体集合只由嵌入字体加 flake 提供的字体决定，你和我编出来的 PDF 用的是同一批字体。</strong></p><p>CLI 上和字体有关的开关就这几个，我把每一个都跑了一遍：</p>

<table><thead><tr><th>开关</th><th>作用</th><th>我在这个 shell 里看到的家族数</th></tr></thead><tbody><tr><td>（无）</td><td>嵌入字体 + 系统字体 + <code>TYPST_FONT_PATHS</code></td><td>284</td></tr><tr><td><code>--ignore-system-fonts</code></td><td>去掉系统字体</td><td>20</td></tr><tr><td><code>env -u TYPST_FONT_PATHS ... --ignore-system-fonts</code></td><td>再去掉环境变量里的目录</td><td>4（只剩嵌入字体）</td></tr><tr><td><code>--ignore-system-fonts --ignore-embedded-fonts</code></td><td>去掉嵌入字体，保留环境变量</td><td>16</td></tr><tr><td><code>--font-path &lt;dir&gt;</code>（只给第一个 Noto 目录，不带环境变量）</td><td>额外扫描一个目录</td><td>9（4 嵌入 + 5 个 Noto Serif CJK）</td></tr></tbody></table><p><code>--font-path</code> 和环境变量 <code>TYPST_FONT_PATHS</code> 等价，后者用 <code>:</code> 分隔多个目录。官方文档写的优先级是 <code>--font-path</code> 高于系统字体，系统字体高于嵌入字体。</p><p><code>--ignore-embedded-fonts</code> 值得单独做个实验，因为它会让一个常见的“找不到字体”变得很安静。我写了个只有一行的文件，要求 Libertinus Serif，再把嵌入字体关掉：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --ignore-system-fonts --ignore-embedded-fonts emb.typ emb.pdf</span></div></div><div><div><div>2</div></div><div><span>warning: unknown font family: libertinus serif</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ emb.typ:1:16</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>1 │ #set text(font: "Libertinus Serif")</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│                 ^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>$ echo $?</span></div></div><div><div><div>9</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>警告，不是错误，退出码 0。文件只有两行：<code>#set text(font: "Libertinus Serif")</code> 加一行 <code>Typst 排版</code>。看 <code>pdffonts</code> 画了什么：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdffonts emb.pdf</span></div></div><div><div><div>2</div></div><div><span>name                                 type              encoding         emb sub uni object ID</span></div></div><div><div><div>3</div></div><div><span>------------------------------------ ----------------- ---------------- --- --- --- ---------</span></div></div><div><div><div>4</div></div><div><span>GZMBIX+NotoSerifCJKjp-ExtraLight     CID TrueType      Identity-H       yes yes yes      8  0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Typst 悄悄换了一个字体，连 <code>Typst</code> 这几个拉丁字母都由 <code>NotoSerifCJKjp</code> 画。这是下一节要讲的“最后兜底”：指定的字体不存在，就去所有可用字体里找一个最像的。注意它挑的是<strong>日文</strong>版本的 Noto Serif，而不是你想要的简体版。</p></section>
<section><h2>字体列表：每个字符各找各的<a href="#字体列表每个字符各找各的"><span>#</span></a></h2><p><code>text</code> 的 <code>font</code> 参数接受三种东西：一个字符串，一个字符串数组（优先级列表），或者数组里夹着带 <code>covers</code> 的字典。<code>lab/ch05/fonts.typ</code> 的前半（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch05/fonts.typ" target="_blank">源文件</a>，文件分两段引用，中间没有改动）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// A font list is a fallback chain: Typst picks the first family covering</span></div></div><div><div><div>4</div></div><div><span>// each character. Latin falls to Libertinus, Chinese to Noto Serif CJK SC.</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>Fallback：Typst 排版 abc 123。</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// `covers` restricts a family to a set of characters.</span></div></div><div><div><div>10</div></div><div><span>#let</span><span> </span><span>latin-only</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>11</div></div><div><span>  </span><span>name</span><span>: </span><span>"Noto Serif CJK SC"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>covers</span><span>: </span><span>regex</span><span>(</span><span>"[</span><span>\u{4e00}</span><span>-</span><span>\u{9fff}</span><span>]"</span><span>),</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div><div><div><div>14</div></div><div><span>#text</span><span>(</span><span>font</span><span>: (</span></div></div><div><div><div>15</div></div><div><span>  </span><span>latin-only</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>"Libertinus Serif"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>))[Han 走 Noto，English 走 Libertinus。]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>列表的语义不是“选第一个存在的字体”，而是“对每个字符，选第一个包含这个字符的字形的字体”。</strong> 这像 shell 查 <code>$PATH</code>：每个命令独立地从左往右找第一个可执行文件，一个目录里没有，就看下一个目录（这个类比我只用这一次）。所以 <code>("Libertinus Serif", "Noto Serif CJK SC")</code> 的意思是：Latin 字母和数字由 Libertinus 画，Libertinus 里没有的汉字由 Noto Serif CJK SC 画。</p><p>顺序为什么重要？Noto Serif CJK 自带一套 Latin 字母，所以把两个字体颠倒过来，Latin 也会被 Noto 抢走。更隐蔽的是标点：Libertinus 有 U+201C 和 U+201D（弯引号），Noto 也有。我写了一个实验 <code>order.typ</code>，同一句话用三种字体列表各排一遍：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>14pt</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>, </span><span>region</span><span>: </span><span>"cn"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>sample</span><span><span> </span><span>= </span><span>[Typst 2026 (括号) “引号”，句号。]</span></span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>a</span><span><span> </span><span>= </span><span>(</span></span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>b</span><span><span> </span><span>= </span><span>(</span></span><span>"Noto Serif CJK SC"</span><span>, </span><span>"Libertinus Serif"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>(</span><span>name</span><span>: </span><span>"Libertinus Serif"</span><span>, </span><span>covers</span><span>: </span><span>"latin-in-cjk"</span><span>),</span></div></div><div><div><div>8</div></div><div><span>  </span><span>"Noto Serif CJK SC"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>11</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>gutter</span><span>: </span><span>0.8em</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>align</span><span>: </span><span>left</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>[</span><span>`A`</span><span>], </span><span>text</span><span>(</span><span>font</span><span>: </span><span>a</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>[</span><span>`B`</span><span>], </span><span>text</span><span>(</span><span>font</span><span>: </span><span>b</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>[</span><span>`C`</span><span>], </span><span>text</span><span>(</span><span>font</span><span>: </span><span>c</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-order.png" alt="三行同一句话：A 行字母来自 Libertinus，引号是窄的西文弯引号；B 行全部字符来自 Noto，字母偏宽，引号是全角的；C 行字母来自 Libertinus，引号是全角的。A 是朴素写法，C 是 xuan 的写法" /><figcaption>三行同一句话：A 行字母来自 Libertinus，引号是窄的西文弯引号；B 行全部字符来自 Noto，字母偏宽，引号是全角的；C 行字母来自 Libertinus，引号是全角的。A 是朴素写法，C 是 xuan 的写法</figcaption></figure><p></p><p>这三行的区别，用 <code>pdffonts</code> 看得更直接：A 用了 <code>LibertinusSerif-Regular</code> 和 <code>NotoSerifCJKsc</code> 两个字体，B 只用了 <code>NotoSerifCJKsc</code> 一个，C 又是两个。我还量了 <code>“引号”</code> 四个字符在 A 和 C 下的宽度（<code>quote-width.typ</code>，<code>a</code> 和 <code>c</code> 与上面 <code>order.typ</code> 里的定义相同）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>14pt</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>, </span><span>region</span><span>: </span><span>"cn"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>a</span><span><span> </span><span>= </span><span>(</span></span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(</span><span>name</span><span>: </span><span>"Libertinus Serif"</span><span>, </span><span>covers</span><span>: </span><span>"latin-in-cjk"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"Noto Serif CJK SC"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>let</span><span> </span><span>w</span><span>(</span><span>font</span><span><span>) </span><span>= </span></span><span>measure</span><span>(</span><span>text</span><span>(</span><span>font</span><span>: </span><span>font</span><span>)[“引号”]).</span><span>width</span><span>.</span><span>pt</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>[A：</span><span>#w</span><span>(</span><span>a</span><span>) pt，C：</span><span>#w</span><span>(</span><span>c</span><span>) pt]</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A：38.5 pt，C：42 pt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两枚引号加起来，A 占 10.5 pt，C 占 14 pt，宽度差了 3.5 pt，眼睛也看得出来。看图：</p><ul>
<li><strong>A</strong>（朴素写法）：字母和数字是 Libertinus，窄而紧凑，但 <code>“引号”</code> 的弯引号也来自 Libertinus，是西文那种窄引号，比汉字小一圈。</li>
<li><strong>B</strong>：一切都由 Noto 画，字母明显变宽，弯引号是全角占位的，符合中文习惯。</li>
<li><strong>C</strong>：给 Libertinus 加了 <code>covers: "latin-in-cjk"</code>。官方文档对这个预设的描述是：覆盖所有码点，除了那些“在 Latin 字体里也有，但最好取自 CJK 字体”的（原文 covers all codepoints except for those which exist in Latin fonts, but should preferably be taken from CJK fonts）。结果是字母和数字仍然来自 Libertinus，引号和括号交给了 Noto。</li>
</ul><p><strong>A 是最朴素的写法，也是最容易踩的坑，C 是 <code>xuan</code> 用的。</strong> A 行的引号和 B 行的肉眼可见地不一样，我跑了 <code>pdffonts</code> 和上面的宽度测量来确认原因：Libertinus 里有 U+201C 和 U+201D，A 的列表里它排在 Noto 前面，所以引号归它。C 给 Libertinus 加 <code>covers: "latin-in-cjk"</code>，把这类字符让给 Noto，引号就是全角的。后面显示的 <code>fonts.typ</code> 用的就是 C。</p><section><h3>covers：给一个家族划定职责<a href="#covers给一个家族划定职责"><span>#</span></a></h3><p><code>covers</code> 也可以是一个正则，官方文档说<strong>只能是“一个点、一个字母或一个字符类”</strong>。上面 <code>fonts.typ</code> 里的 <code>latin-only</code> 就是一个字符类：Noto Serif CJK SC 只负责 U+4E00 到 U+9FFF 的汉字，其他码点直接跳过。渲染出来：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-covers.png" alt="一行文字：Han 走 Noto，English 走 Libertinus。汉字“走”由 Noto 画出，英文是 Libertinus，逗号和句号是兜底字体的无衬线字形" /><figcaption>一行文字：Han 走 Noto，English 走 Libertinus。汉字“走”由 Noto 画出，英文是 Libertinus，逗号和句号是兜底字体的无衬线字形</figcaption></figure><p></p><p>注意，名字叫 <code>latin-only</code> 不准确，它其实是“汉字只用 Noto”。更值得看的是这行里的逗号和句号 <code>，。</code>。这两个字符不在 Libertinus 里，Noto Serif CJK SC 又被 <code>covers</code> 限定在汉字区间，两个字体都跳过了它们。这时 Typst 的行为是“最后兜底”：从所有可用字体里找一个有这个字形的。我对这行单独跑了 <code>pdffonts</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdffonts cov.pdf</span></div></div><div><div><div>2</div></div><div><span>name                                 type              encoding         emb sub uni object ID</span></div></div><div><div><div>3</div></div><div><span>------------------------------------ ----------------- ---------------- --- --- --- ---------</span></div></div><div><div><div>4</div></div><div><span>KFQELT+LibertinusSerif-Regular-Identity-H CID Type 0C       Identity-H       yes yes yes      8  0</span></div></div><div><div><div>5</div></div><div><span>TPBOKN+NotoSerifCJKsc-ExtraLight     CID TrueType      Identity-H       yes yes yes     11  0</span></div></div><div><div><div>6</div></div><div><span>OXUTFP+NotoSansCJKjp-Thin            CID TrueType      Identity-H       yes yes yes     14  0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第三行就是它：<code>Noto Sans CJK JP</code>，一个无衬线的日文字体，被拿去画两个全角标点。<code>lab/ch05/fonts.typ</code> 里的整页渲染里也有同样的字体，因为这个文件的 <code>latin-only</code> 例子就是这样写的。解法是在列表末尾再追加一次不带 <code>covers</code> 的 <code>"Noto Serif CJK SC"</code> 作为兜底。我在 <code>cov2.typ</code> 里这样写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>14pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>han</span><span><span> </span><span>= </span><span>(</span></span><span>name</span><span>: </span><span>"Noto Serif CJK SC"</span><span>, </span><span>covers</span><span>: </span><span>regex</span><span>(</span><span>"[</span><span>\u{4e00}</span><span>-</span><span>\u{9fff}</span><span>]"</span><span>))</span></div></div><div><div><div>4</div></div><div><span>#text</span><span>(</span><span>font</span><span>: (</span></div></div><div><div><div>5</div></div><div><span>  </span><span>han</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>"Libertinus Serif"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>"Noto Serif CJK SC"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>))[Han 走 Noto，English 走 Libertinus。]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdffonts</code> 里 <code>NotoSansCJKjp-Thin</code> 消失了，只剩 Libertinus 和 <code>NotoSerifCJKsc</code> 两行。（实验文件在 <code>/tmp/p5</code>，没有放进仓库，代码就在文中。）</p></section><section><h3>找不到、不兜底、没字体<a href="#找不到不兜底没字体"><span>#</span></a></h3><p>lab 里有两个文件专门看“字体找不到”。第一个是 <code>unknown-font.warn.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch05/unknown-font.warn.typ" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A family that no font provides only produces a warning; text falls back.</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"No Such Font"</span><span>, </span><span>"Libertinus Serif"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>Typst falls back to the next family.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make ch05</code> 会检查它退出码为 0 且 stderr 非空，stderr 存在 <code>out/ch05/unknown-font.stderr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>warning: unknown font family: no such font</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch05/unknown-font.warn.typ:2:16</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #set text(font: ("No Such Font", "Libertinus Serif"))</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Typst 把家族名转成小写再报告，警告针对的是整个列表（指向整个数组，不是单个名字）。文字照样排出来，用的是列表里的下一个。</p><p>第二个是 <code>missing-glyph.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch05/missing-glyph.typ" target="_blank">源文件</a>），演示 <code>fallback</code> 参数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// With fallback off, a missing character is drawn as a tofu box and Typst</span></div></div><div><div><div>2</div></div><div><span>// stays silent. Compare with the default.</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>"Libertinus Serif"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>默认：Latin 与 汉字 </span><span>\</span></div></div><div><div><div>5</div></div><div><span>#text</span><span>(</span><span>fallback</span><span>: </span><span>false</span><span>)[关闭：Latin 与 汉字]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-missing-glyph.png" alt="两行字：第一行“默认：Latin 与 汉字”正常显示，汉字来自兜底字体；第二行关闭兜底后，所有汉字和冒号变成方框，只有 Latin 正常" /><figcaption>两行字：第一行“默认：Latin 与 汉字”正常显示，汉字来自兜底字体；第二行关闭兜底后，所有汉字和冒号变成方框，只有 Latin 正常</figcaption></figure><p></p><p>（这张图是同一份文件，我把页面设成了自动大小以便截图。）<code>fallback: true</code> 是默认值，Typst 会翻遍所有可用字体找一个有这个字符的。关掉它之后，汉字和全角冒号变成豆腐块，而且<strong>没有任何警告</strong>，<code>out/ch05/missing-glyph.stderr</code> 是空的，这也是为什么 <code>missing-glyph.typ</code> 不是 <code>.warn.typ</code>。官方文档也写了这一点：目前没有“字体缺字”的警告。结论是：<strong>排版出豆腐块时，Typst 不会告诉你，只能靠肉眼检查，或者配合 <code>pdffonts</code> 看用了哪些字体。</strong></p><p>把这两个“静默”场景合起来，再看一个更极端的：不设 <code>TYPST_FONT_PATHS</code>，也不装任何 CJK 字体，编译同一份 <code>emb.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ env -u TYPST_FONT_PATHS typst compile --ignore-system-fonts emb.typ emb3.pdf</span></div></div><div><div><div>2</div></div><div><span>$ echo $?</span></div></div><div><div><div>3</div></div><div><span>0</span></div></div><div><div><div>4</div></div><div><span>$ pdffonts emb3.pdf</span></div></div><div><div><div>5</div></div><div><span>name                                 type              encoding         emb sub uni object ID</span></div></div><div><div><div>6</div></div><div><span>------------------------------------ ----------------- ---------------- --- --- --- ---------</span></div></div><div><div><div>7</div></div><div><span>MOITFO+LibertinusSerif-Regular-Identity-H CID Type 0C       Identity-H       yes yes yes      8  0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>退出码 0，stderr 空，PDF 里只有 Libertinus。没有任何字体能画“排版”两个字，汉字渲染成什么我没有逐像素看，但 Typst 没有提醒你一个字。<strong>中文文档一定要让你的构建环境显式提供 CJK 字体</strong>，这就是 <code>flake.nix</code> 里 <code>noto-fonts-cjk-serif</code> 的意义。</p></section></section>
<section><h2>字重、字形与 0.15 的 <code>variations</code><a href="#字重字形与-015-的-variations"><span>#</span></a></h2><p><code>lab/ch05/fonts.typ</code> 的后半：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Weight, style, tracking, spacing.</span></div></div><div><div><div>2</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>"bold"</span><span>)[粗体 bold] </span><span>#text</span><span>(</span><span>style</span><span>: </span><span>"italic"</span><span>)[italic 斜体]</span></div></div><div><div><div>3</div></div><div><span>#text</span><span>(</span><span>tracking</span><span>: </span><span>2pt</span><span>)[字距 tracking] </span><span>#text</span><span>(</span><span>spacing</span><span>: </span><span>200%</span><span>)[词距 word spacing]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 0.15: variable fonts expose axes through `variations`.</span></div></div><div><div><div>6</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>300</span><span>)[Light 300 轻] </span><span>\</span></div></div><div><div><div>7</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>900</span><span>)[Black 900 黑] </span><span>\</span></div></div><div><div><div>8</div></div><div><span>#text</span><span>(</span><span>variations</span><span>: (</span><span>wght</span><span>: </span><span>600</span><span>))[wght=600 手动指定]</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>// OpenType features and number styles.</span></div></div><div><div><div>11</div></div><div><span>#text</span><span>(</span><span>features</span><span>: (</span><span>"smcp"</span><span>,))[Small caps via feature] </span><span>\</span></div></div><div><div><div>12</div></div><div><span>#text</span><span>(</span><span>number-type</span><span>: </span><span>"old-style"</span><span>)[0123456789] vs</span></div></div><div><div><div>13</div></div><div><span>#text</span><span>(</span><span>number-type</span><span>: </span><span>"lining"</span><span>)[0123456789]</span></div></div><div><div><div>14</div></div><div><span>#smallcaps</span><span>[smallcaps] </span><span>#upper</span><span>[upper] </span><span>#lower</span><span>[LOWER]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个文件编出来是这样：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-fonts.png" alt="fonts.typ 的渲染：回退示例，Han 走 Noto，粗体、斜体、字距和词距变化，三行不同字重（Light 300、Black 900、wght=600），小型大写，旧式数字对比，smallcaps、upper、lower" /><figcaption>fonts.typ 的渲染：回退示例，Han 走 Noto，粗体、斜体、字距和词距变化，三行不同字重（Light 300、Black 900、wght=600），小型大写，旧式数字对比，smallcaps、upper、lower</figcaption></figure><p></p><p>前面几个参数逐个过：</p><ul>
<li><code>weight</code>：整数 100 到 900 或名字（<code>"bold"</code> 是 700，<code>"light"</code> 是 300，<code>"regular"</code> 是 400）。</li>
<li><code>style</code>：<code>"normal"</code>、<code>"italic"</code>、<code>"oblique"</code>。图里 <code>italic 斜体</code> 一行，<code>italic</code> 是斜的，<code>斜体</code> 两个字<strong>仍是直立的</strong>：Noto CJK 没有斜体，Typst 找不到斜体字形就退回 normal，不会把汉字强行倾斜（官方文档写的是 When neither an italic nor an oblique style is available, Typst selects the normal style，这张图是它的实例）。</li>
<li><code>tracking</code>：字符之间额外加的距离。图里 <code>t r a c k i n g</code> 被撑开了，汉字“字距”也跟着撑开。</li>
<li><code>spacing</code>：词间距，只影响空格，所以 <code>word spacing</code> 里两个单词隔得很远，中间没有空格的汉字不受影响。它可以是绝对长度，也可以是相对于该字体空格宽度的百分比。</li>
<li><code>size</code>：第 4 篇已经讲了 <code>em</code> 相对继承字号。</li>
</ul><section><h3>字体名里的 ExtraLight<a href="#字体名里的-extralight"><span>#</span></a></h3><p>现在回答开头那个问题。<code>typst fonts --variants</code> 把每个字体的变体列出来，我看 Noto Serif CJK SC：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst fonts --ignore-system-fonts --variants</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>Noto Serif CJK SC</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>└ /nix/store/4gqvskjagg0w5zs36y6dg7h0nzinh1ci-noto-fonts-cjk-serif-2.003/share/fonts/opentype/noto-cjk/NotoSerifCJK-VF.otf.ttc (Variable)</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>Style: Normal</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>Stretch: 100%</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>Weight: 200-900 (Default: 200)</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只有一个文件，标着 <code>(Variable)</code>，字重轴范围 200 到 900，<strong>默认实例是 200</strong>，即 ExtraLight。对照一下 Libertinus Serif，它是六个静态字体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Libertinus Serif</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>├ (Embedded)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│   Style: Normal, Weight: 400, Stretch: 100%</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>├ (Embedded)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│   Style: Normal, Weight: 700, Stretch: 100%</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>├ (Embedded)</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>│   Style: Italic, Weight: 400, Stretch: 100%</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>├ (Embedded)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>│   Style: Italic, Weight: 700, Stretch: 100%</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>├ (Embedded)</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>│   Style: Normal, Weight: 600, Stretch: 100%</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>└ (Embedded)</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>Style: Italic, Weight: 600, Stretch: 100%</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可变字体的文件里只有一份轮廓数据加上一根字重轴。Typst 按 <code>weight</code> 去那根轴上取对应的值，把结果插值后嵌进 PDF，但 PDF 里给这个字体对象起名字用的是文件的<strong>默认实例名</strong>，也就是 ExtraLight。我验证了一下：写一个文件，同一个 Noto Serif CJK SC，用 400、700、900 三种字重各排两个字：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>"Noto Serif CJK SC"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>400</span><span>)[常规]</span></div></div><div><div><div>3</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>700</span><span>)[粗体]</span></div></div><div><div><div>4</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>900</span><span>)[黑体]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pdffonts var-names.pdf</span></div></div><div><div><div>2</div></div><div><span>name                                 type              encoding         emb sub uni object ID</span></div></div><div><div><div>3</div></div><div><span>------------------------------------ ----------------- ---------------- --- --- --- ---------</span></div></div><div><div><div>4</div></div><div><span>YDGOLY+NotoSerifCJKsc-ExtraLight     CID TrueType      Identity-H       yes yes yes      8  0</span></div></div><div><div><div>5</div></div><div><span>YEUBDD+NotoSerifCJKsc-ExtraLight     CID TrueType      Identity-H       yes yes yes     11  0</span></div></div><div><div><div>6</div></div><div><span>BINIWB+NotoSerifCJKsc-ExtraLight     CID TrueType      Identity-H       yes yes yes     14  0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个字重，三个 PDF 字体对象，名字全是 <code>ExtraLight</code>。对象数等于不同字重数，说明每个字重确实各有一份实例，只是名字没有跟着变。<code>lab/ch05/fonts.typ</code> 里用了 400、700、300、900、600 五个字重的汉字，<code>pdffonts</code> 里就有五个 <code>NotoSerifCJKsc-ExtraLight</code>，再加上前面说过那个兜底的 <code>NotoSansCJKjp-Thin</code>。<strong>所以 <code>pdffonts</code> 看到 ExtraLight 不代表字重错了</strong>，眼睛看渲染图才是准的，看看上面 <code>Light 300</code>、<code>Black 900</code> 三行实际是不同的粗细。</p></section><section><h3><code>weight</code> 与 <code>variations</code><a href="#weight-与-variations"><span>#</span></a></h3><p>Typst 0.15 把可变字体的轴暴露成了 <code>text.variations</code>，是一个字典，键是轴名。我想知道它和 <code>weight</code> 的关系，写了三个只差一行的文件，各自编成 PNG：</p><div><figure><figcaption><span>var-a.typ</span></figcaption><pre><code><div><div><div>1</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>600</span><span>)[手动指定 wght]</span></div></div><div><div><div>2</div></div><div><span>// var-b.typ</span></div></div><div><div><div>3</div></div><div><span>#text</span><span>(</span><span>variations</span><span>: (</span><span>wght</span><span>: </span><span>600</span><span>))[手动指定 wght]</span></div></div><div><div><div>4</div></div><div><span>// var-c.typ</span></div></div><div><div><div>5</div></div><div><span>#text</span><span>(</span><span>weight</span><span>: </span><span>400</span><span>, </span><span>variations</span><span>: (</span><span>wght</span><span>: </span><span>900</span><span>))[手动指定 wght]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（三个文件的开头都是 <code>#set page(width: auto, height: auto, margin: 8pt)</code> 和 <code>#set text(font: "Noto Serif CJK SC", size: 20pt)</code>，这里省略。）</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ md5sum var-?.png</span></div></div><div><div><div>2</div></div><div><span>f300a241cae78175ce1a0ac8b6d8a88a  var-a.png</span></div></div><div><div><div>3</div></div><div><span>f300a241cae78175ce1a0ac8b6d8a88a  var-b.png</span></div></div><div><div><div>4</div></div><div><span>348adae211869a8a34c3bc488febd2b2  var-c.png</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>var-a</code> 和 <code>var-b</code> 的 PNG 字节完全一致，也就是说 <code>weight: 600</code> <strong>就是</strong> <code>variations: (wght: 600)</code>：官方文档说，用可变字体时 Typst 会自动根据 <code>weight</code> 配置 <code>wght</code>，根据 <code>size</code> 配置 <code>opsz</code>，根据 <code>style</code> 配置 <code>ital</code> 或 <code>slnt</code>，根据 <code>stretch</code> 配置 <code>wdth</code>。<code>var-c</code> 里 <code>variations</code> 把 <code>weight: 400</code> 覆盖成了 900，渲染出来是最粗的那档。所以平时直接用 <code>weight</code>，只有你要调 <code>weight</code> 之外的轴（比如某些字体的 <code>slnt</code>、<code>opsz</code> 想手动指定）时才写 <code>variations</code>。</p></section></section>
<section><h2>OpenType 特性与数字<a href="#opentype-特性与数字"><span>#</span></a></h2><p>字体里除了字形还有大量“特性”（feature），<code>text</code> 给常用的几个单独开了参数，其余走 <code>features</code>。<code>features.typ</code> 这个实验在 <code>/tmp/p5</code>，没有放进仓库：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>默认数字：1234567890 </span><span>\</span></div></div><div><div><div>5</div></div><div><span>#text</span><span>(</span><span>number-type</span><span>: </span><span>"old-style"</span><span>)[old-style：1234567890] </span><span>\</span></div></div><div><div><div>6</div></div><div><span>#text</span><span>(</span><span>number-type</span><span>: </span><span>"lining"</span><span>)[lining：1234567890] </span><span>\</span></div></div><div><div><div>7</div></div><div><span>#text</span><span>(</span><span>slashed-zero</span><span>: </span><span>true</span><span>)[slashed-zero：0O0] </span><span>\</span></div></div><div><div><div>8</div></div><div><span>#text</span><span>(</span><span>fractions</span><span>: </span><span>true</span><span>)[fractions：1/2 3/4] </span><span>\</span></div></div><div><div><div>9</div></div><div><span>#text</span><span>(</span><span>features</span><span>: (</span><span>"smcp"</span><span>, </span><span>"c2sc"</span><span>))[features: Small Caps] </span><span>\</span></div></div><div><div><div>10</div></div><div><span>#text</span><span>(</span><span>features</span><span>: (</span><span>smcp</span><span>: </span><span>1</span><span>))[dict form: Small Caps] </span><span>\</span></div></div><div><div><div>11</div></div><div><span>#smallcaps</span><span>[smallcaps 函数] </span><span>#upper</span><span>[upper 大写] </span><span>#lower</span><span>[LOWER 小写]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-features.png" alt="七行实验结果：默认数字与 lining 数字相同，old-style 数字高低错落，slashed-zero 的 0 带斜线，fractions 把 1/2 和 3/4 排成分数形，features 数组和字典两种写法都得到小型大写，smallcaps 把小写变成小型大写，upper 全大写，lower 全小写" /><figcaption>七行实验结果：默认数字与 lining 数字相同，old-style 数字高低错落，slashed-zero 的 0 带斜线，fractions 把 1/2 和 3/4 排成分数形，features 数组和字典两种写法都得到小型大写，smallcaps 把小写变成小型大写，upper 全大写，lower 全小写</figcaption></figure><p></p><p>这几个我都在图里看到了效果：</p><ul>
<li><code>number-type</code>：<code>"old-style"</code> 数字像小写字母一样高低错落（适合正文），<code>"lining"</code> 一样高（适合表格）。Libertinus 默认就是 lining，所以第一行和第三行看着一样。</li>
<li><code>slashed-zero</code>：<code>0</code> 带斜线，和字母 <code>O</code> 区分。</li>
<li><code>fractions</code>：把 <code>1/2</code> 排成分数形。</li>
<li><code>features</code>：直接给 OpenType 特性标签，数组形式（<code>("smcp", "c2sc")</code>）开启一组，字典形式（<code>(smcp: 1)</code>）可以指定特性取值。<code>smcp</code> 把小写换成小型大写字母，<code>c2sc</code> 把大写也换成小型大写；<code>#text(features: ("smcp",))</code> 与 <code>#smallcaps[...]</code> 是同一个效果的两种写法，后者作为元素函数可以被 <code>show smallcaps</code> 单独设计。</li>
<li><code>upper</code>、<code>lower</code>、<code>smallcaps</code>：这三个是函数，不是字体特性，<code>upper</code> 和 <code>lower</code> 把文字转成大写和小写。</li>
<li>汉字不受这些特性影响：图里 <code>函数</code>、<code>大写</code>、<code>小写</code> 都没变。</li>
</ul><p><code>text</code> 还有 <code>ligatures</code>、<code>kerning</code>、<code>number-width</code>、<code>stylistic-set</code> 等参数。我在 12pt 的 Libertinus 上试了开关，渲染图里我看不出肉眼可辨的差别，所以这里不下结论，需要的话去官方文档的 <code>text</code> 页查。</p></section>
<section><h2><code>lang</code> 与 <code>region</code>：不只是语言标签<a href="#lang-与-region不只是语言标签"><span>#</span></a></h2><p><code>lang</code> 官方文档列的用途是连字符断词规则、智能引号和“其他语言相关的事”。<code>region</code> 呢，只有一句“让文本处理管线做出更明智的选择”。对中文，这个“更明智”体现在<strong>字形</strong>上。我写了个实验，同一串文字用五种语言设置各排一遍，并用 <code>measure</code> 量宽度：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>16pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>s</span><span><span> </span><span>= </span><span>[“你好”，直（括号）。门户：「引」]</span></span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>w</span><span>(</span><span>lang</span><span>, </span><span>region</span><span><span>) </span><span>= </span></span><span>context</span><span> </span><span>measure</span><span>(</span><span>text</span><span>(</span></div></div><div><div><div>5</div></div><div><span>  </span><span>lang</span><span>: </span><span>lang</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>region</span><span>: </span><span>region</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>s</span><span>,</span></div></div><div><div><div>8</div></div><div><span>)).</span><span>width</span></div></div><div><div><div>9</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>10</div></div><div><span>  </span><span>columns</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>gutter</span><span>: </span><span>0.6em</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>[</span><span>`en`</span><span>], </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"en"</span><span>, </span><span>s</span><span>), [</span><span>#w</span><span>(</span><span>"en"</span><span>, </span><span>none</span><span>)],</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>[</span><span>`zh`</span><span>], </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"zh"</span><span>, </span><span>s</span><span>), [</span><span>#w</span><span>(</span><span>"zh"</span><span>, </span><span>none</span><span>)],</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>[</span><span>`zh-cn`</span><span>], </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"zh"</span><span>, </span><span>region</span><span>: </span><span>"cn"</span><span>, </span><span>s</span><span>), [</span><span>#w</span><span>(</span><span>"zh"</span><span>, </span><span>"cn"</span><span>)],</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>[</span><span>`zh-tw`</span><span>], </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"zh"</span><span>, </span><span>region</span><span>: </span><span>"tw"</span><span>, </span><span>s</span><span>), [</span><span>#w</span><span>(</span><span>"zh"</span><span>, </span><span>"tw"</span><span>)],</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>[</span><span>`ja`</span><span>], </span><span>text</span><span>(</span><span>lang</span><span>: </span><span>"ja"</span><span>, </span><span>s</span><span>), [</span><span>#w</span><span>(</span><span>"ja"</span><span>, </span><span>none</span><span>)],</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-lang.png" alt="五行同一串文字：en、zh、zh-cn 三行看起来一样，宽度都是 228pt；zh-tw 一行标点位置变成居中，门字旁的户字字形不同，宽度 244pt；ja 一行户字写作日文字形，宽度 228pt" /><figcaption>五行同一串文字：en、zh、zh-cn 三行看起来一样，宽度都是 228pt；zh-tw 一行标点位置变成居中，门字旁的户字字形不同，宽度 244pt；ja 一行户字写作日文字形，宽度 228pt</figcaption></figure><p></p><p>数字：<code>en</code>、<code>zh</code>、<code>zh-cn</code>、<code>ja</code> 的宽度都是 228 pt，<code>zh-tw</code> 是 244 pt，宽了 16 pt。图里 <code>zh-tw</code> 一行的 <code>，</code> <code>。</code> <code>：</code> 位置和其他几行明显不同，“户”字在 <code>tw</code> 和 <code>ja</code> 下也写成了另外的字形。官方文档说 <code>lang</code> 和 <code>script</code> 一起决定“字体特性（如字形替换）怎么实现”，我猜这里的区别来自 Noto 字体里按语言区分的字形替换表，但我没有去核对字体内部的特性表，只是观察结果。</p><p>这对 <code>xuan</code> 的意义是：简体中文我写 <code>lang: "zh", region: "cn"</code>。图里 <code>zh</code> 一行不写 <code>region</code> 看起来和 <code>zh-cn</code> 一样，所以光看这一串字，<code>region: "cn"</code> 不是必需的；我写它是为了明确选了简体，不依赖字体的默认字形，也能和 <code>tw</code> 区分开。<code>xuan-cjk</code> 对英文文档把 <code>region</code> 设成 <code>none</code>，只给 <code>lang: "zh"</code> 设 <code>"cn"</code>。</p><p>顺便说 <code>smartquote</code>。它把输入的直引号 <code>"</code> <code>'</code> 换成弯引号，换成哪种由 <code>lang</code> 决定，<code>set smartquote(enabled: false)</code> 关掉，<code>set smartquote(quotes: ...)</code> 自定义。直角引号的实验放在下面的装饰一节里一起跑。</p></section>
<section><h2><code>cjk-latin-spacing</code>：汉字和字母之间的四分之一<a href="#cjk-latin-spacing汉字和字母之间的四分之一"><span>#</span></a></h2><p>中文排版有个老习惯：汉字和拉丁字母、数字之间留一点空隙。Typst 默认就会加，参数是 <code>text.cjk-latin-spacing</code>，取 <code>auto</code>（默认）或 <code>none</code>。<code>lab/ch05/cjk-latin-spacing.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch05/cjk-latin-spacing.typ" target="_blank">源文件</a>）把两种设置的宽度量出来，直接印进 PDF：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// Typst inserts a quarter-em gap between Chinese and Latin/digits by default.</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>sample</span><span><span> </span><span>= </span><span>[中文Typst排版2026年]</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>let</span><span> </span><span>width</span><span>(</span><span>spacing</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>measure</span><span>(</span></div></div><div><div><div>10</div></div><div><span>      </span><span>text</span><span>(</span><span>cjk-latin-spacing</span><span>: </span><span>spacing</span><span>, </span><span>size</span><span>: </span><span>12pt</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>).</span><span>width</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>  </span><span>let</span><span> </span><span>on</span><span><span> </span><span>= </span></span><span>width</span><span>(</span><span>auto</span><span>)</span></div></div><div><div><div>14</div></div><div><span>  </span><span>let</span><span> </span><span>off</span><span><span> </span><span>= </span></span><span>width</span><span>(</span><span>none</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>[</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>- </span><span>`auto`</span><span>：</span><span>#on</span><span>.</span><span>pt</span><span>() pt</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>- </span><span>`none`</span><span>：</span><span>#off</span><span>.</span><span>pt</span><span>() pt</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>- 差值：</span><span>#</span><span>(</span><span>on</span><span><span> </span><span>-</span><span> </span></span><span>off</span><span>).</span><span>pt</span><span>() pt，共 </span><span>#</span><span>((</span><span>on</span><span><span> </span><span>-</span><span> </span></span><span>off</span><span><span>) </span><span>/</span><span> (</span></span><span>0.25</span><span><span> </span><span>*</span><span> </span></span><span>12pt</span><span>)) 处缝隙</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>#text</span><span>(</span><span>size</span><span>: </span><span>12pt</span><span>, </span><span>cjk-latin-spacing</span><span>: </span><span>auto</span><span>, </span><span>sample</span><span>) </span><span>\</span></div></div><div><div><div>23</div></div><div><span>#text</span><span>(</span><span>size</span><span>: </span><span>12pt</span><span>, </span><span>cjk-latin-spacing</span><span>: </span><span>none</span><span>, </span><span>sample</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>measure</code> 和 <code>context</code> 第 7 篇才讲，这里只借用：<code>measure</code> 返回内容的实际尺寸。<code>pdftotext</code> 读出来的结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>• auto：121.776 pt</span></div></div><div><div><div>2</div></div><div><span>• none：109.776 pt</span></div></div><div><div><div>3</div></div><div><span>• 差值：12 pt，共 4 处缝隙</span></div></div><div><div><div>4</div></div><div><span>中文 Typst 排版 2026 年</span></div></div><div><div><div>5</div></div><div><span>中文 Typst 排版 2026 年</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>样本 <code>中文Typst排版2026年</code> 里汉字和字母之间有 4 处交界：<code>文|T</code>、<code>t|排</code>、<code>版|2</code>、<code>6|年</code>。12 pt 下差值正好 12 pt，每处 3 pt，也就是 <strong>0.25em</strong>。这就是“默认插入四分之一个字宽”的实测值。</p><p>问题来了：我在源文件里手敲空格，会不会和自动空隙叠加？我又写了个实验 <code>space.typ</code>（<code>/tmp/p5</code>），分别量 <code>中文Typst</code> 和 <code>中文 Typst</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span></div></div><div><div><div>2</div></div><div><span>  </span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>),</span></div></div><div><div><div>3</div></div><div><span>  </span><span>size</span><span>: </span><span>12pt</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>region</span><span>: </span><span>"cn"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div><span>#context</span><span> {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>let</span><span> </span><span>w</span><span>(</span><span>x</span><span>, </span><span>spacing</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>measure</span><span>(</span><span>text</span><span>(</span><span>cjk-latin-spacing</span><span>: </span><span>spacing</span><span>, </span><span>x</span><span>)).</span><span>width</span><span>.</span><span>pt</span><span>()</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>  </span><span>for</span><span> </span><span>s</span><span> </span><span>in</span><span> (</span><span>auto</span><span>, </span><span>none</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>tight</span><span><span> </span><span>= </span></span><span>w</span><span>([中文Typst], </span><span>s</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>spaced</span><span><span> </span><span>= </span></span><span>w</span><span>([中文 Typst], </span><span>s</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>[</span><span>#repr</span><span>(</span><span>s</span><span>)：无空格 </span><span>#tight</span><span> pt，有空格 </span><span>#spaced</span><span> pt </span><span>\ </span><span>]</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>auto：无空格 54.456 pt，有空格 54.456 pt</span></div></div><div><div><div>2</div></div><div><span>none：无空格 51.456 pt，有空格 54.456 pt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>auto</code> 下有没有空格宽度一样（54.456 pt），<strong>没有叠加</strong>；<code>none</code> 下才是纯粹的“一个空格”宽度 3 pt。结论：用 <code>auto</code> 的话，写不写那个空格结果相同，不用在源文件里为排版好看再敲空格。</p><p><code>lab/ch05/fig-cjk.typ</code> 把四种设置并排放，产出 <code>out/figures/ch05-cjk.png</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>size</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>sample</span><span><span> </span><span>= </span><span>[中文Typst排版2026年，数字100%。]</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>7</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>gutter</span><span>: </span><span>1em</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>row-gutter</span><span>: </span><span>0.8em</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>align</span><span>: </span><span>left</span><span><span> </span><span>+</span><span> </span></span><span>horizon</span><span>,</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>[</span><span>`auto`</span><span>], </span><span>text</span><span>(</span><span>cjk-latin-spacing</span><span>: </span><span>auto</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>[</span><span>`none`</span><span>], </span><span>text</span><span>(</span><span>cjk-latin-spacing</span><span>: </span><span>none</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>[</span><span>`strong`</span><span>], </span><span>text</span><span>(</span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>[</span><span>`light`</span><span>], </span><span>text</span><span>(</span><span>weight</span><span>: </span><span>300</span><span>, </span><span>sample</span><span>),</span></div></div><div><div><div>15</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-cjk.png" alt="四行同一句话：auto 行汉字与字母数字之间有空隙；none 行紧贴；strong 行是粗体，汉字和字母都变粗；light 行比 auto 略轻" /><figcaption>四行同一句话：auto 行汉字与字母数字之间有空隙；none 行紧贴；strong 行是粗体，汉字和字母都变粗；light 行比 auto 略轻</figcaption></figure><p></p><p><code>strong</code> 一行说明可变字体的粗体是真粗体：汉字和字母一起变粗（这里的“字母”来自 Libertinus Bold，是另一个字体文件）。<code>light</code> 一行用的是 Noto 的 300 实例，汉字比 <code>auto</code> 行略轻。Libertinus 只有 400、600、700 三档（见上面的 <code>--variants</code>），请求 300 时取最近的一档：我另外把 <code>Abc</code> 用 <code>weight: 300</code> 和 <code>weight: 400</code> 各编成 PNG，两张图字节完全相同，所以字母没变。</p></section>
<section><h2>文字装饰、引号与换行<a href="#文字装饰引号与换行"><span>#</span></a></h2><p>这一节把还没出场的 <code>text</code> 周边函数一起过一遍。实验文件是 <code>/tmp/p5/deco.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span></div></div><div><div><div>3</div></div><div><span>  </span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>),</span></div></div><div><div><div>4</div></div><div><span>  </span><span>size</span><span>: </span><span>12pt</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>region</span><span>: </span><span>"cn"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#highlight</span><span>[高亮]</span></div></div><div><div><div>10</div></div><div><span>#highlight</span><span>(</span><span>fill</span><span>: </span><span>yellow</span><span>.</span><span>lighten</span><span>(</span><span>50%</span><span>), </span><span>extent</span><span>: </span><span>2pt</span><span>)[带扩展的高亮] </span><span>\</span></div></div><div><div><div>11</div></div><div><span>#underline</span><span>[下划线 underline]</span></div></div><div><div><div>12</div></div><div><span>#underline</span><span>(</span><span>offset</span><span>: </span><span>2pt</span><span>, </span><span>stroke</span><span>: </span><span>1pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>)[偏移的蓝色下划线]</span></div></div><div><div><div>13</div></div><div><span>#underline</span><span>(</span><span>evade</span><span>: </span><span>false</span><span>)[不避让 gjpqy] </span><span>#underline</span><span>[避让 gjpqy] </span><span>\</span></div></div><div><div><div>14</div></div><div><span>#strike</span><span>[删除线 strike] </span><span>#overline</span><span>[上划线 overline] </span><span>\</span></div></div><div><div><div>15</div></div><div><span>H</span><span>#sub</span><span>[2]O 与 x</span><span>#super</span><span>[2] </span><span>\</span></div></div><div><div><div>16</div></div><div><span>#text</span><span>(</span><span>baseline</span><span>: </span><span>3pt</span><span>)[手动基线 baseline] </span><span>\</span></div></div><div><div><div>17</div></div><div><span>"smart" 与 'single'，英文里 </span><span>#text</span><span>(</span><span>lang</span><span>: </span><span>"en"</span><span>)["smart" and 'single'] </span><span>\</span></div></div><div><div><div>18</div></div><div><span>#</span><span>[</span></div></div><div><div><div>19</div></div><div><span>  </span><span>#set</span><span> </span><span>smartquote</span><span>(</span><span>enabled</span><span>: </span><span>false</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>"关闭后" 保持直引号</span></div></div><div><div><div>21</div></div><div><span>] </span><span>\</span></div></div><div><div><div>22</div></div><div><span>#</span><span>[</span></div></div><div><div><div>23</div></div><div><span>  </span><span>#set</span><span> </span><span>smartquote</span><span>(</span><span>quotes</span><span>: (</span><span>double</span><span>: (</span><span>"「"</span><span>, </span><span>"」"</span><span>), </span><span>single</span><span>: (</span><span>"『"</span><span>, </span><span>"』"</span><span>)))</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>"直角引号" 与 'nested'</span></div></div><div><div><div>25</div></div><div><span>] </span><span>\</span></div></div><div><div><div>26</div></div><div><span>第一行 </span><span>\ </span><span>第二行 </span><span>#linebreak</span><span>() 第三行 </span><span>\</span></div></div><div><div><div>27</div></div><div><span>#lorem</span><span>(</span><span>12</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-deco.png" alt="装饰效果：高亮背景，下划线和偏移的蓝色下划线，不避让的下划线穿过 gjpqy 的下伸部分而避让的绕开，删除线和上划线，H2O 的下标 2，x 的上标 2，手动基线抬高，智能引号为弯引号，关闭后是直引号，自定义后是直角引号，换行，然后是一行拉丁文占位文字" /><figcaption>装饰效果：高亮背景，下划线和偏移的蓝色下划线，不避让的下划线穿过 gjpqy 的下伸部分而避让的绕开，删除线和上划线，H2O 的下标 2，x 的上标 2，手动基线抬高，智能引号为弯引号，关闭后是直引号，自定义后是直角引号，换行，然后是一行拉丁文占位文字</figcaption></figure><p></p><ul>
<li><code>highlight</code>：给内容加背景色，<code>fill</code> 改颜色，<code>extent</code> 让背景向两侧多伸出一段，图里第二个高亮比第一个宽一圈。</li>
<li><code>underline</code>、<code>strike</code>、<code>overline</code>：下划线、删除线、上划线。<code>offset</code> 调离基线的距离，<code>stroke</code> 调线型，<code>evade</code>（默认 <code>true</code>）让线条在遇到字母下伸部分（<code>g</code>、<code>j</code>、<code>p</code>、<code>q</code>、<code>y</code>）时绕开，图里 <code>evade: false</code> 的那一条直接穿过了字母。对中文来说汉字没有下伸，<code>evade</code> 只在夹杂英文时可见。</li>
<li><code>sub</code>、<code>super</code>：下标和上标，缩小字号并移动基线。这里排版用的是文字，公式里的上下标是第 8 篇的内容。</li>
<li><code>baseline</code>：手动抬升或降低基线，是 <code>text</code> 的参数。</li>
<li><code>smartquote</code>：<code>"smart" 与 'single'</code> 在 <code>zh</code> 下得到的是 <code>“smart” 与 ‘single’</code>，弯引号。<code>set smartquote(enabled: false)</code> 保留直引号；<code>set smartquote(quotes: (double: (...), single: (...)))</code> 换成别的引号，我换成了直角引号 <code>「」</code> <code>『』</code>，中文排版中偶尔需要。</li>
<li><code>linebreak</code>：<code>\</code> 是 <code>#linebreak()</code> 的简写，强制换行，图里“第一行 第二行 第三行”各占一行。</li>
<li><code>lorem(n)</code>：生成 n 个词的 lorem ipsum 占位文字。注意它只会生成拉丁文，想要中文占位文字得自己写一段。</li>
</ul><p><code>upper</code>、<code>lower</code>、<code>smallcaps</code> 在上一节已经出现。第 9 篇会再用到 <code>highlight</code>、<code>underline</code> 等，在表格和引用里。</p></section>
<section><h2>中文编号：<code>"一"</code> 和 <code>"壹"</code><a href="#中文编号一-和-壹"><span>#</span></a></h2><p>第 4 篇的 <code>set heading(numbering: "一、")</code> 用的就是中文数字。<code>numbering</code> 的模式字符串里，<code>1</code>、<code>a</code>、<code>A</code>、<code>i</code>、<code>I</code> 是拉丁系，<code>一</code> 和 <code>壹</code> 是中文系，我对 <code>n = 1, 10, 11, 21, 100, 1234</code> 各跑一遍。实验文件 <code>/tmp/p5/numbering.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span></div></div><div><div><div>3</div></div><div><span>  </span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>),</span></div></div><div><div><div>4</div></div><div><span>  </span><span>size</span><span>: </span><span>12pt</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#for</span><span> </span><span>n</span><span> </span><span>in</span><span> (</span><span>1</span><span>, </span><span>10</span><span>, </span><span>11</span><span>, </span><span>21</span><span>, </span><span>100</span><span>, </span><span>1234</span><span>) {</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>[</span><span>#n</span><span>：</span><span>#numbering</span><span>(</span><span>"一"</span><span>, </span><span>n</span><span>)，</span><span>#numbering</span><span>(</span><span>"壹"</span><span>, </span><span>n</span><span>) </span><span>\ </span><span>]</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div><span>#numbering</span><span>(</span><span>"①"</span><span>, </span><span>21</span><span>)，</span><span>#numbering</span><span>(</span><span>"①"</span><span>, </span><span>50</span><span>) </span><span>\</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"一、"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>=</span><span> 概述</span></div></div><div><div><div>15</div></div><div><span>==</span><span> 小节</span></div></div><div><div><div>16</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"第一章"</span><span>)</span></div></div><div><div><div>17</div></div><div><span>=</span><span> 另起</span></div></div><div><div><div>18</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: (..</span><span>n</span><span>) </span><span>=&gt;</span><span> </span><span>"第"</span><span><span> </span><span>+</span><span> </span></span><span>numbering</span><span>(</span><span>"一"</span><span>, ..</span><span>n</span><span>.</span><span>pos</span><span><span>()) </span><span>+</span><span> </span></span><span>"节"</span><span>)</span></div></div><div><div><div>19</div></div><div><span>=</span><span> 函数形式</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#set</span><span> </span><span>enum</span><span>(</span><span>numbering</span><span>: </span><span>"（一）"</span><span>)</span></div></div><div><div><div>22</div></div><div><span>+ 条目</span></div></div><div><div><div>23</div></div><div><span>+ 条目</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-numbering.png" alt="编号实验：数字 1、10、11、21、100、1234 的小写和大写中文数字，圈号 21 和 50，一级标题一、概述，二级标题一、一、小节，第二章，第三节，以及括号中文数字的列表" /><figcaption>编号实验：数字 1、10、11、21、100、1234 的小写和大写中文数字，圈号 21 和 50，一级标题一、概述，二级标题一、一、小节，第二章，第三节，以及括号中文数字的列表</figcaption></figure><p></p><p><code>pdftotext</code> 读出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1：一，壹</span></div></div><div><div><div>2</div></div><div><span>10：十，拾</span></div></div><div><div><div>3</div></div><div><span>11：十一，拾壹</span></div></div><div><div><div>4</div></div><div><span>21：二十一，贰拾壹</span></div></div><div><div><div>5</div></div><div><span>100：一百，壹佰</span></div></div><div><div><div>6</div></div><div><span>1234：一千二百三十四，壹仟贰佰叁拾肆</span></div></div><div><div><div>7</div></div><div><span>㉑，㊿</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>"一"</code> 给小写中文数字，<code>"壹"</code> 给大写（财务用），两者都能处理到 1234，都是完整的“十、百、千”读法，没有位数限制的错误。</li>
<li><code>"①"</code> 只覆盖 1 到 50，到 100 时我在这个版本里得到的是一条警告 <code>the number 100 is too large to be represented with the arabic.o numeral system</code>，附带 <code>this will become a hard error in the future</code>，输出退化成阿拉伯数字 <code>100</code>。用圈号数字要注意不超过 50。</li>
<li>标题的 <code>"一、"</code> 在二级标题上变成 <code>一、一、</code>。我猜是模式只有一个计数符号，第二级的计数器按同一个模式再追加一次，没有去查源码。多级标题要么只给一级设中文，要么用函数形式。</li>
<li>把模式换成 <code>"第一章"</code> 时，计数器并没有重置：接着上面的 <code>概述</code> 变成 <code>第二章</code>，因为 <code>numbering</code> 只是改了显示，计数器是标题自己的。</li>
<li>函数形式 <code>(..n) =&gt; "第" + numbering("一", ..n.pos()) + "节"</code> 可以随意拼装，<code>..n.pos()</code> 把收到的计数器值展开成 <code>numbering</code> 的参数。</li>
<li>列表同理：<code>set enum(numbering: "（一）")</code> 用的是全角括号，条目内容紧接其后。</li>
</ul></section>
<section><h2>段落：<code>par</code> 的几个参数<a href="#段落par-的几个参数"><span>#</span></a></h2><p><code>lab/ch05/par.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch05/par.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span></div></div><div><div><div>3</div></div><div><span>  </span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>),</span></div></div><div><div><div>4</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>size</span><span>: </span><span>11pt</span><span>,</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// Chinese convention: indent every paragraph by two characters.</span></div></div><div><div><div>9</div></div><div><span>// `all: true` also indents the first paragraph after a heading.</span></div></div><div><div><div>10</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span></div></div><div><div><div>11</div></div><div><span>  </span><span>justify</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>leading</span><span>: </span><span>0.9em</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>spacing</span><span>: </span><span>1.2em</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>first-line-indent</span><span>: (</span><span>amount</span><span>: </span><span>2em</span><span>, </span><span>all</span><span>: </span><span>true</span><span>),</span></div></div><div><div><div>15</div></div><div><span>)</span></div></div><div><div><div>16</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1."</span><span>)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>=</span><span> 缩进</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>排版中文时，每段首行缩进两个汉字宽，行距比西文略大。段落之间不需要额外空行，但这里用 </span><span>`spacing`</span><span> 留了一点。</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>// 0.15: first-line-indent folds, so a later rule can change one key.</span></div></div><div><div><div>23</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: (</span><span>amount</span><span>: </span><span>0pt</span><span>))</span></div></div><div><div><div>24</div></div><div><span>这一段把 amount 改成了 0，</span><span>`all`</span><span> 仍然继承自上面的规则。</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>// Hanging indent for references.</span></div></div><div><div><div>27</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>, </span><span>hanging-indent</span><span>: </span><span>2em</span><span>)</span></div></div><div><div><div>28</div></div><div><span>[1] Knuth, D. E. _The TeXbook_. Addison-Wesley, 1984. 这条参考文献的第二行会整体悬挂缩进。</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>// Line breaking: optimized (default) vs simple.</span></div></div><div><div><div>31</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>hanging-indent</span><span>: </span><span>0pt</span><span>, </span><span>linebreaks</span><span>: </span><span>"simple"</span><span>)</span></div></div><div><div><div>32</div></div><div><span>简单断行直接贪心填满每一行，不做全段落优化，所以右边缘的空白更不均匀一些。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-par.png" alt="par.typ 的渲染：一级标题后第一段首行缩进两个汉字宽，第二段同样缩进；amount 设为 0 之后下一段不缩进；参考文献条目第一行顶格，第二行缩进；最后一段使用简单断行" /><figcaption>par.typ 的渲染：一级标题后第一段首行缩进两个汉字宽，第二段同样缩进；amount 设为 0 之后下一段不缩进；参考文献条目第一行顶格，第二行缩进；最后一段使用简单断行</figcaption></figure><p></p><p>逐个说。</p><p><strong><code>justify</code>、<code>leading</code>、<code>spacing</code>。</strong> <code>justify: true</code> 两端对齐。<code>leading</code> 是行与行之间的距离（不含字体本身的行高），<code>spacing</code> 是段与段之间的距离。我读了这些默认值（<code>#context</code> 里把 <code>par.leading</code>、<code>par.spacing</code>、<code>par.first-line-indent</code>、<code>text.lang</code> 等打印出来）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>默认 leading 0.65em，spacing 1.2em，行首缩进 (amount: 0pt, all: false)，字号 11pt，lang en，</span></div></div><div><div><div>2</div></div><div><span>region none，cjk-latin-spacing auto</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>xuan-cjk</code> 把 <code>leading</code> 提到 <code>0.9em</code>。我的感觉是汉字方块占满整个字面，同样的行距看起来比拉丁文更挤，所以多给一点。</p><p><strong><code>first-line-indent</code> 与它的 <code>all</code>。</strong> 中文习惯每段缩进两个汉字，即 <code>2em</code>。<code>first-line-indent</code> 接受一个长度，也接受 <code>(amount: ..., all: ...)</code> 的字典。<strong><code>all</code> 默认为 <code>false</code></strong>，意思是只给“紧跟在其他段落后面”的段落缩进，标题后第一段不缩进，这是西文习惯。我用并排实验确认了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>2em</span><span>)</span></div></div><div><div><div>2</div></div><div><span>// vs</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: (</span><span>amount</span><span>: </span><span>2em</span><span>, </span><span>all</span><span>: </span><span>true</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-indent-all.png" alt="左右两列同样的内容。左列 all 缺省：标题后第一段顶格，其余段缩进；右列 all 为 true：标题后第一段也缩进两个汉字宽" /><figcaption>左右两列同样的内容。左列 all 缺省：标题后第一段顶格，其余段缩进；右列 all 为 true：标题后第一段也缩进两个汉字宽</figcaption></figure><p></p><p>左边（<code>all</code> 缺省）标题“标题”后面的“标题后的第一段。”是顶格的，右边（<code>all: true</code>）也缩进了。中文排版要求每段都缩，所以 <code>xuan-cjk</code> 用 <code>all: true</code>。（这个并排实验是 <code>/tmp/p5/indent-all.typ</code>，每列里就是上面这条 <code>set par</code> 加几段文字。）</p><p><strong>折叠（fold）。</strong> 第 4 篇讲过 <code>set</code> 规则会折叠，0.15 起 <code>first-line-indent</code> 也折叠，所以 lab 里 <code>#set par(first-line-indent: (amount: 0pt))</code> 只写了 <code>amount</code>，按 lab 注释的说法 <code>all</code> 仍然继承。图里这一段确实不缩进了，<code>all</code> 继承与否单看这张图分辨不出来。</p><p><strong><code>hanging-indent</code>。</strong> 悬挂缩进，是第一行以外的所有行缩进。参考文献和术语表用得到。图里 <code>[1] Knuth...</code> 的第一行顶格，续行缩进了 2em。lab 里先把 <code>first-line-indent</code> 设回 <code>0pt</code> 再设悬挂，免得两个缩进叠在一起。</p><p><strong><code>linebreaks</code>。</strong> <code>"optimized"</code> 是默认的整段最优断行算法，<code>"simple"</code> 是逐行贪心填满。最后一段用的是 <code>"simple"</code>。我没有量化这两种方式的区别，在 12 厘米宽的段落里肉眼几乎看不出来；这个参数对长段落、窄栏、拉丁文混排的差别更大一些。</p></section>
<section><h2>收进 <code>xuan</code>：<code>fonts.typ</code> 与 <code>cjk.typ</code><a href="#收进-xuanfontstyp-与-cjktyp"><span>#</span></a></h2><p>前面所有结论，在 <code>xuan</code> 里就是这两个文件。<code>src/fonts.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/fonts.typ" target="_blank">源文件</a>），13 行，最终版：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Font stacks. Libertinus, DejaVu and New Computer Modern ship inside the</span></div></div><div><div><div>2</div></div><div><span>// Typst binary; the Noto CJK families come from the flake (TYPST_FONT_PATHS).</span></div></div><div><div><div>3</div></div><div><span>// `latin-in-cjk` keeps Libertinus away from CJK punctuation such as “”, so</span></div></div><div><div><div>4</div></div><div><span>// those fall through to the full-width glyphs of Noto Serif CJK SC.</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>fonts</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>6</div></div><div><span>  </span><span>serif</span><span>: (</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(</span><span>name</span><span>: </span><span>"Libertinus Serif"</span><span>, </span><span>covers</span><span>: </span><span>"latin-in-cjk"</span><span>),</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"Noto Serif CJK SC"</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>),</span></div></div><div><div><div>10</div></div><div><span>  </span><span>sans</span><span>: (</span><span>"Noto Sans CJK SC"</span><span>,),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>mono</span><span>: (</span><span>"DejaVu Sans Mono"</span><span>, </span><span>"Noto Sans Mono CJK SC"</span><span>),</span></div></div><div><div><div>12</div></div><div><span>  </span><span>math</span><span>: (</span><span>"New Computer Modern Math"</span><span>,),</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个字典，四个键，都是字体列表（<code>sans</code> 后面的逗号让它是一个元素的数组，而不是字符串）：</p><ul>
<li><code>serif</code>：正文。Latin 与数字走 Libertinus，汉字走 Noto Serif CJK SC。<code>covers: "latin-in-cjk"</code> 让 Libertinus 不碰引号这类中文标点，它们落到 Noto 的全角字形上，前面的实验已经讲过。</li>
<li><code>sans</code>：无衬线，第 13 篇的幻灯片用。</li>
<li><code>mono</code>：代码。同样的思路，DejaVu Sans Mono 排字母符号，Noto Sans Mono CJK SC 补汉字。</li>
<li><code>math</code>：公式字体。第 8 篇会讲，这里只有一项，因为数学字体要有 OpenType math 表，普通字体用不了。</li>
</ul><p>注释里已经把来源写明：Libertinus、DejaVu 和 New Computer Modern 是嵌在 Typst 里的，Noto CJK 来自 flake。所以你要在别的环境用 <code>xuan</code>，<strong>必须自己保证 Noto CJK 系列在字体路径里</strong>，否则就是前面那个“静默豆腐块”场景。</p><p><code>src/cjk.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/cjk.typ" target="_blank">源文件</a>），21 行，最终版：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"fonts.typ"</span><span>: </span><span>fonts</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>/// Typography defaults for Chinese (`lang: "zh"`) or English body text.</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>xuan-cjk</span><span>(</span><span>body</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>5</div></div><div><span>  </span><span>let</span><span> </span><span>zh</span><span><span> </span><span>= </span></span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span></div></div><div><div><div>6</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span></div></div><div><div><div>7</div></div><div><span>    </span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>serif</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>lang</span><span>: </span><span>lang</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>region</span><span>: </span><span>if</span><span> </span><span>zh</span><span> { </span><span>"cn"</span><span> } </span><span>else</span><span> { </span><span>none</span><span> },</span></div></div><div><div><div>10</div></div><div><span>    </span><span>cjk-latin-spacing</span><span>: </span><span>auto</span><span>,</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>12</div></div><div><span>  </span><span>set</span><span> </span><span>par</span><span>(</span></div></div><div><div><div>13</div></div><div><span>    </span><span>justify</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>leading</span><span>: </span><span>0.9em</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>spacing</span><span>: </span><span>1.2em</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>first-line-indent</span><span>: </span><span>if</span><span> </span><span>zh</span><span> { (</span><span>amount</span><span>: </span><span>2em</span><span>, </span><span>all</span><span>: </span><span>true</span><span>) } </span><span>else</span><span> { </span><span>0pt</span><span> },</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>18</div></div><div><span>  </span><span>show</span><span> </span><span>raw</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>mono</span><span>)</span></div></div><div><div><div>19</div></div><div><span>  </span><span>show</span><span> </span><span>math</span><span>.</span><span>equation</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: </span><span>fonts</span><span>.</span><span>math</span><span>)</span></div></div><div><div><div>20</div></div><div><span>  </span><span>body</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它的形状就是第 4 篇讲的模板：<code>(body, lang:)</code> 参数，最后返回 <code>body</code>，前面登记一串规则。拆开看：</p><ul>
<li><code>let zh = lang == "zh"</code>：后面三处要分语言，先算一次。</li>
<li><code>set text(...)</code>：字体列表、<code>lang</code>、按语言选 <code>region</code>（中文 <code>"cn"</code>，其他 <code>none</code>）、<code>cjk-latin-spacing: auto</code>。<code>auto</code> 就是默认值，我显式写出来是让读这个文件的人不必去查默认值。</li>
<li><code>set par(...)</code>：中文缩进两个字，英文不缩进（<code>0pt</code>）。其余三项两种语言共用：两端对齐，行距 0.9em，段距 1.2em。<code>first-line-indent</code> 值是 <code>if</code> 表达式，分支结果是一个字典或一个长度，两者都合法。</li>
<li><code>show raw: set text(font: fonts.mono)</code>：行内和块级的 <code>raw</code> 都换成等宽栈。这是 <code>show ... : set</code> 形式，第 4 篇讲过，它不产生嵌套。</li>
<li><code>show math.equation: set text(font: fonts.math)</code>：公式用数学字体。这里不用 <code>set text(font:)</code>，因为那会影响正文，只想让公式变。</li>
</ul><p>我用这个模板编了一份很小的文档，看它实际用了哪些字体。<code>use.typ</code>（<code>/tmp/p5</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#show</span><span>: </span><span>xuan-cjk</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>=</span><span> 使用 xuan-cjk</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>正文用 Libertinus 排西文，用 Noto Serif CJK SC 排汉字：Typst 0.15.1 的 </span><span>`raw`</span><span> 与公式 </span><span>$</span><span>x^2 + y^2</span><span>$</span><span> 各有自己的字体。</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>第二段缩进两个汉字宽，*加粗* 取的是可变字体 wght 轴上 700 的那个实例。</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>```rust</span></div></div><div><div><div>13</div></div><div><span>fn</span><span> </span><span>main</span><span>() { </span><span>println!</span><span>(</span><span>"你好"</span><span>); }</span></div></div><div><div><div>14</div></div><div><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root / --ignore-system-fonts use.typ use.pdf</span></div></div><div><div><div>2</div></div><div><span>$ pdffonts use.pdf</span></div></div><div><div><div>3</div></div><div><span>name                                 type              encoding         emb sub uni object ID</span></div></div><div><div><div>4</div></div><div><span>------------------------------------ ----------------- ---------------- --- --- --- ---------</span></div></div><div><div><div>5</div></div><div><span>BJAXUM+NotoSerifCJKsc-ExtraLight     CID TrueType      Identity-H       yes yes yes     19  0</span></div></div><div><div><div>6</div></div><div><span>CXRIIC+LibertinusSerif-Bold-Identity-H CID Type 0C       Identity-H       yes yes yes     22  0</span></div></div><div><div><div>7</div></div><div><span>ZLKZTV+NotoSerifCJKsc-ExtraLight     CID TrueType      Identity-H       yes yes yes     25  0</span></div></div><div><div><div>8</div></div><div><span>PQNDLO+LibertinusSerif-Regular-Identity-H CID Type 0C       Identity-H       yes yes yes     28  0</span></div></div><div><div><div>9</div></div><div><span>WWBOFQ+DejaVuSansMono                CID TrueType      Identity-H       yes yes yes     31  0</span></div></div><div><div><div>10</div></div><div><span>HJDJUN+NewCMMath-Book-Identity-H     CID Type 0C       Identity-H       yes yes yes     34  0</span></div></div><div><div><div>11</div></div><div><span>GGUNCA+NotoSansMonoCJKsc-Regular     CID TrueType      Identity-H       yes yes yes     37  0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>--root /</code> 是因为文件放在 <code>/tmp</code> 下，Typst 要求源文件必须在项目根目录里。）七个字体对象，对得上：两个 <code>NotoSerifCJKsc</code>（常规加标题或加粗两个字重），Libertinus 的 Bold 和 Regular，DejaVu Sans Mono 画代码里的 <code>fn main</code> 等拉丁字符，<code>NewCMMath-Book</code> 画 <span><span>x2+y2x^2 + y^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，<code>NotoSansMonoCJKsc-Regular</code> 画代码里的“你好”。</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch05-xuan-cjk.png" alt="xuan-cjk 排出的一小段文档：一级标题，首行缩进两个汉字宽的正文，行内等宽文字与公式，加粗的汉字，以及带语法着色的代码块" /><figcaption>xuan-cjk 排出的一小段文档：一级标题，首行缩进两个汉字宽的正文，行内等宽文字与公式，加粗的汉字，以及带语法着色的代码块</figcaption></figure><p></p></section>
<section><h2>跑一遍<a href="#跑一遍"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make ch05</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch05</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch05/cjk-latin-spacing.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch05/fig-cjk.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch05/fonts.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch05/missing-glyph.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch05/par.typ</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch05/unknown-font.warn.typ (warned as expected)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>六个文件，五个正常输出，一个按预期给出警告。</p><p>一个新坑：这一篇的小实验放在 <code>/tmp</code> 下，我一开始给 <code>typst compile</code> 传了 <code>--root ~/Projects/typst-lab</code>，得到 <code>error: source file must be contained in project root</code>，因为源文件必须在根目录之内。改成 <code>--root /</code> 才编过。仓库里的 lab 文件本来就在仓库根下，<code>--root .</code> 就够了。</p></section>
<section><h2>接下来<a href="#接下来"><span>#</span></a></h2><p>字体和段落解决了“字怎么排”，下一步是“字放在哪里”：页面大小、页边距、盒子、网格、浮动。第 6 篇讲 <code>page</code>、<code>block</code>、<code>box</code>、<code>grid</code>、<code>place</code> 和 <code>layout</code>，并把 <code>xuan</code> 的 <code>page.typ</code> 写出来。想先动手的话，<code>lab/ch05/</code> 里的字体列表还是 A 的写法，可以把它换成 <code>fonts.typ</code> 里 <code>serif</code> 的写法，自己对比引号的变化：<a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1/lab/ch05" target="_blank"><code>lab/ch05</code></a>。下一篇：<a href="/posts/typst-by-xuan-06-page-and-layout/">页面与布局：盒子、网格与浮动</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-04-set-and-show/</id>
      <title type="text">set 与 show：样式是规则，不是命令</title>
      <published>2026-10-19T03:00:00.000Z</published>
      <updated>2026-10-19T03:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-04-set-and-show/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 4 篇：我用 Typst 0.15.1 编译了 ch04 的 6 个 lab 文件和一批临时实验，弄清 set、show、选择器和 numbering 各自管什么，再把它们收进本地包 @local/xuan:0.1.0 的 base 与 note 模板，让 #show: note.with(...) 一行排出带目录的 A4 笔记。</summary>
      <content type="html"><![CDATA[<p>在 Typst 里改字号只需要写一行 <code>#set text(size: 14pt)</code>，但这一行不是“从现在起把字号调成 14pt”的命令，而是往样式表里登记一条规则：从这里到所在作用域结束，所有 <code>text</code> 的 <code>size</code> 默认值是 14pt。区别听上去很学究，直到你要写模板的时候：一个模板，就是一组被登记进去的规则，加上一个把正文包起来的函数。这一篇我把 <code>set</code>、<code>show</code>、选择器和编号逐个跑一遍，然后用它们搭出 <code>xuan</code> 的第一版 <code>note</code> 模板。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：本地包 <code>@local/xuan:0.1.0</code> 的骨架，即 <code>typst.toml</code>、<code>lib.typ</code>、<code>src/base.typ</code>、<code>src/note.typ</code>。这四个文件里 <code>typst.toml</code> 是最终版，其余三个是<strong>中间版本</strong>，后面的章节还会往里加 import 和参数，我会在文中说明每个文件之后长了什么。<code>lab/ch04/</code> 下的 6 个文件（含一个预期失败的）全部通过 <code>make ch04</code>。
<code>note</code> 依赖的 <code>fonts.typ</code>、<code>cjk.typ</code>、<code>page.typ</code>、<code>header.typ</code>、<code>theorem.typ</code> 要到第 5 到第 7 篇才逐个讲，现在先把仓库里的 <code>src/</code> 整个拷过来用，把它们当黑盒就行。
前置：<a href="/posts/typst-by-xuan-03-functions-and-content/">第 3 篇</a>（函数、<code>arguments</code>、content 树）。<code>util.typ</code> 在那一篇已经写好。</p></div></div>
<section><h2>set：往样式表里登记默认值<a href="#set往样式表里登记默认值"><span>#</span></a></h2><p>我先把整个 <code>lab/ch04/set-rules.typ</code> 摆出来（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch04/set-rules.typ" target="_blank">源文件</a>），后面逐条拆：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// A set rule changes defaults from this point to the end of its scope.</span></div></div><div><div><div>4</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>11pt</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#set</span><span> </span><span>par</span><span>(</span><span>justify</span><span>: </span><span>true</span><span>, </span><span>leading</span><span>: </span><span>0.8em</span><span>)</span></div></div><div><div><div>6</div></div><div><span>正文使用 11pt。</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#</span><span>[</span></div></div><div><div><div>9</div></div><div><span>  </span><span>// Inside a content block the rule ends at the closing bracket.</span></div></div><div><div><div>10</div></div><div><span>  </span><span>#set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>, </span><span>size</span><span>: </span><span>14pt</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>这一段是蓝色 14pt。</span></div></div><div><div><div>12</div></div><div><span>]</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>回到 11pt。</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>// A conditional set rule.</span></div></div><div><div><div>17</div></div><div><span>#let</span><span> </span><span>draft</span><span><span> </span><span>= </span></span><span>true</span></div></div><div><div><div>18</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>) </span><span>if</span><span> </span><span>draft</span></div></div><div><div><div>19</div></div><div><span>草稿状态下整篇变红。</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>// Rules fold: `stroke` of a table merges with the previous set rule.</span></div></div><div><div><div>22</div></div><div><span>#set</span><span> </span><span>table</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>6pt</span><span>)</span></div></div><div><div><div>23</div></div><div><span>#set</span><span> </span><span>table</span><span>(</span><span>fill</span><span>: (</span><span>x</span><span>, </span><span>y</span><span>) </span><span>=&gt;</span><span> </span><span>if</span><span> </span><span>y</span><span><span> </span><span>==</span><span> </span></span><span>0</span><span> { </span><span>luma</span><span>(</span><span>230</span><span>) })</span></div></div><div><div><div>24</div></div><div><span>#table</span><span>(</span></div></div><div><div><div>25</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>[名称], [值],</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>[alpha], [0.05],</span></div></div><div><div><div>28</div></div><div><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>// Set rules also work on numbering.</span></div></div><div><div><div>31</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"一、"</span><span>)</span></div></div><div><div><div>32</div></div><div><span>=</span><span> 第一节</span></div></div><div><div><div>33</div></div><div><span>=</span><span> 第二节</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>在仓库里用 Typst 0.15.1 编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop --command make ch04</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch04</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch04/fig-show-heading.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch04/not-settable.fail.typ (failed as expected)</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch04/selectors.typ</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch04/set-rules.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch04/show-rules.typ</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch04/first-note/main.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>渲染出来是这样：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-set-rules.png" alt="set-rules.typ 的渲染：正文 11pt，方括号内一段蓝色 14pt，之后回到 11pt，从 draft 那行起所有文字包括表格变红，最后两个标题带“一、”“二、”编号" /><figcaption>set-rules.typ 的渲染：正文 11pt，方括号内一段蓝色 14pt，之后回到 11pt，从 draft 那行起所有文字包括表格变红，最后两个标题带“一、”“二、”编号</figcaption></figure><p></p><p>几个值得停一下的地方。</p><p><strong>作用域就是代码块的边界。</strong> 方括号 <code>#[ ... ]</code> 里的 <code>set text(fill: blue)</code> 在右方括号处结束，下一段回到 11pt。这和 shell 里 <code>( cd /tmp; export X=1 )</code> 一个意思：子 shell 里改的环境变量，括号一关就消失。函数体和 <code>for</code> 循环体同样是作用域。我在 <code>/tmp/p4/scope.typ</code> 里验证了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>f</span><span><span>() </span><span>= </span><span>{</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>[函数里变红]</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>#f</span><span>() 函数外不受影响。</span></div></div><div><div><div>7</div></div><div><span>#for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>[循环内 </span><span>#i</span><span> ]</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div><span>循环外还是黑色。</span></div></div><div><div><div>12</div></div><div><span>#show</span><span> </span><span>heading</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>purple</span><span>)</span></div></div><div><div><div>13</div></div><div><span>#</span><span>[</span></div></div><div><div><div>14</div></div><div><span>  </span><span>#show</span><span> </span><span>heading</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>orange</span><span>)</span></div></div><div><div><div>15</div></div><div><span>  </span><span>=</span><span> 块内标题</span></div></div><div><div><div>16</div></div><div><span>]</span></div></div><div><div><div>17</div></div><div><span>=</span><span> 块外标题</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-scope.png" alt="函数里的文字为红色，函数外黑色；循环内的两段为蓝色，循环外黑色；方括号内的标题为橙色，方括号外的标题为紫色" /><figcaption>函数里的文字为红色，函数外黑色；循环内的两段为蓝色，循环外黑色；方括号内的标题为橙色，方括号外的标题为紫色</figcaption></figure><p></p><p>这几个 <code>/tmp/p4</code> 下的小实验没有放进仓库，代码就在文中，你可以原样粘贴。</p><p><strong>后写的覆盖先写的。</strong> <code>set text(size: 14pt)</code> 之后，同一作用域里再写一次，以后一次为准。再看一个容易忽略的细节：<code>size</code> 接受相对单位，<code>2em</code> 是相对继承来的字号，不是相对 11pt 之类的固定值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>2em</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#context</span><span> </span><span>text</span><span>.</span><span>size</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#</span><span>[</span></div></div><div><div><div>6</div></div><div><span>  </span><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>2em</span><span>)</span></div></div><div><div><div>7</div></div><div><span>  </span><span>#context</span><span> </span><span>text</span><span>.</span><span>size</span></div></div><div><div><div>8</div></div><div><span>]</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#context</span><span> </span><span>text</span><span>.</span><span>size</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>20pt</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>40pt</span></div></div><div><div><div>4</div></div><div><span>20pt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一次 <code>2em</code> 乘的是 10pt，方括号里再来一次就是 20pt 的两倍。<code>xuan</code> 里一级标题写的 <code>set text(size: 1.4em)</code> 就依赖这个行为：它放大的是正文字号，所以正文字号变了，标题也跟着变。（<code>context text.size</code> 是第 7 篇的内容，这里只借它读出当前字号。）</p><p><strong><code>set ... if</code> 是条件登记。</strong> <code>#set text(fill: red) if draft</code> 在 <code>draft</code> 为真时才登记。注意渲染图里 <code>draft</code> 那行之后的所有内容都变红了，表格里的字和后面的标题也不例外，因为规则一直有效到文件结束。</p><p><strong>规则会折叠（fold）。</strong> 对字典或描边这类复合值，后一条规则只覆盖它写出的那几个键，其余保留。我在 <code>/tmp/p4/fold.typ</code> 里跑了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>table</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>table</span><span>(</span><span>stroke</span><span>: (</span><span>bottom</span><span>: </span><span>2pt</span><span><span> </span><span>+</span><span> </span></span><span>red</span><span>))</span></div></div><div><div><div>4</div></div><div><span>#table</span><span>(</span></div></div><div><div><div>5</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>[a], [b],</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>[c], [d],</span></div></div><div><div><div>8</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-fold.png" alt="一个两行两列的表格，所有格线是细蓝线，只有最下面一条是粗红线" /><figcaption>一个两行两列的表格，所有格线是细蓝线，只有最下面一条是粗红线</figcaption></figure><p></p><p>第二条规则只指定了 <code>bottom</code>，其余三边保留蓝色细线。如果它是覆盖语义，表格会只剩一条红线。</p><p><strong>不是所有字段都能 <code>set</code>。</strong> <code>heading</code> 的 <code>body</code> 是内容本身，不属于样式，所以：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `body` is not a parameter of `set`: only settable fields are accepted.</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>body</span><span>: [x])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unexpected argument: body</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch04/not-settable.fail.typ:2:13</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #set heading(body: [x])</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│              ^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条错误信息是 <code>make ch04</code> 从 <code>out/ch04/not-settable.stderr</code> 里存下来的。判断一个参数能不能 <code>set</code>，最快的办法就是试，反正报错只有这一行。</p></section>
<section><h2>show：改写某一类元素<a href="#show改写某一类元素"><span>#</span></a></h2><p><code>set</code> 只能改元素的参数。要换掉元素长什么样，或者给一类元素多套一层，就轮到 <code>show</code>。<code>lab/ch04/show-rules.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch04/show-rules.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// `show &lt;selector&gt;: set ...` restyles matching elements.</span></div></div><div><div><div>4</div></div><div><span>#show</span><span> </span><span>heading</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.4em</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// `show &lt;selector&gt;: it =&gt; ...` replaces them with new content.</span></div></div><div><div><div>8</div></div><div><span>#show</span><span> </span><span>strong</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>, </span><span>it</span><span>.</span><span>body</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#show</span><span> </span><span>"Typst"</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>box</span><span>(</span><span>fill</span><span>: </span><span>yellow</span><span>.</span><span>lighten</span><span>(</span><span>60%</span><span>), </span><span>it</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#show</span><span> </span><span>regex</span><span>(</span><span>"</span><span>\d</span><span>+%"</span><span>): </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>green</span><span>, </span><span>weight</span><span>: </span><span>"bold"</span><span>, </span><span>it</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>=</span><span> 标题一</span></div></div><div><div><div>13</div></div><div><span>==</span><span> 标题二</span></div></div><div><div><div>14</div></div><div><span>这里有 *不再加粗但变红* 的文字，提到 Typst，完成度 80%。</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>// Wrapping a function as a show rule: the usual template shape.</span></div></div><div><div><div>17</div></div><div><span>#let</span><span> </span><span>boxed</span><span>(</span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>18</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>0.9em</span><span>)</span></div></div><div><div><div>19</div></div><div><span>  </span><span>block</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span>, </span><span>inset</span><span>: </span><span>8pt</span><span>, </span><span>body</span><span>)</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div><span>#show</span><span>: </span><span>boxed</span></div></div><div><div><div>22</div></div><div><span>整段内容都被 </span><span>`boxed`</span><span> 包住。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-show-rules.png" alt="show-rules.typ 的渲染：两个蓝色标题，一级更大；正文里“不再加粗但变红”为红色非粗体，Typst 带黄色底色，80% 为绿色粗体；最后一行被带边框的盒子包住，字号稍小" /><figcaption>show-rules.typ 的渲染：两个蓝色标题，一级更大；正文里“不再加粗但变红”为红色非粗体，Typst 带黄色底色，80% 为绿色粗体；最后一行被带边框的盒子包住，字号稍小</figcaption></figure><p></p><p><code>show</code> 的冒号后面有三种形态，对应这个文件的三段：</p><ol>
<li><code>show X: set ...</code>：只改匹配元素的样式。<code>show heading: set text(fill: blue)</code> 与全局 <code>set text</code> 的区别是只作用在标题上。</li>
<li><code>show X: it =&gt; ...</code>：函数接收匹配到的元素 <code>it</code>，返回的内容替换原位。<code>strong</code> 那条取 <code>it.body</code>，把粗体外壳丢了，所以渲染里是红色的非粗体字。</li>
<li><code>show: f</code>：没有选择器，匹配“后面所有内容”，函数 <code>f</code> 收到整段剩余文档。这就是模板的形状，下一节专讲。</li>
</ol><p>选择器 <code>X</code> 不一定是元素函数：字符串 <code>"Typst"</code> 匹配字面文字，<code>regex("\d+%")</code> 匹配正则（渲染里 <code>80%</code> 变绿加粗），标签 <code>&lt;name&gt;</code> 匹配带那个标签的元素。</p><p><strong>多条 show 规则叠在同一元素上，谁先谁后？</strong> 直觉上后写的会覆盖先写的，实验结果没有这么简单。<code>/tmp/p4/order.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>14pt</span><span>)</span></div></div><div><div><div>4</div></div><div><span>大小是 </span><span>#context</span><span> </span><span>text</span><span>.</span><span>size</span><span>。</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#show</span><span> </span><span>"Typst"</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>, </span><span>it</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#show</span><span> </span><span>"Typst"</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>, </span><span>it</span><span>)</span></div></div><div><div><div>8</div></div><div><span>这里的 Typst 是什么颜色？</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#show</span><span> </span><span>strong</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> [(</span><span>#it</span><span>.</span><span>body</span><span>)]</span></div></div><div><div><div>11</div></div><div><span>#show</span><span> </span><span>strong</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>green</span><span>, </span><span>it</span><span>)</span></div></div><div><div><div>12</div></div><div><span>*粗体*</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#show</span><span> </span><span>heading</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> [</span><span>#it</span><span>]</span></div></div><div><div><div>15</div></div><div><span>=</span><span> 不会死循环</span></div></div><div><div><div>16</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>2</span><span>): </span><span>none</span></div></div><div><div><div>17</div></div><div><span>==</span><span> 这一行消失了</span></div></div><div><div><div>18</div></div><div><span>#show</span><span> </span><span>"旧"</span><span>: </span><span>"新"</span></div></div><div><div><div>19</div></div><div><span>旧词被换成新词。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-rule-order.png" alt="order.typ 的渲染：Typst 一词是红色，不是后写的蓝色；粗体被括号包住且整体为绿色；一级标题可见，二级标题消失；旧词被替换成新词" /><figcaption>order.typ 的渲染：Typst 一词是红色，不是后写的蓝色；粗体被括号包住且整体为绿色；一级标题可见，二级标题消失；旧词被替换成新词</figcaption></figure><p></p><p>读这张图：</p><ul>
<li><code>"Typst"</code> 是<strong>红色</strong>，先写的红色规则赢了。两条规则的输出里都含有原来的 <code>it</code>，读法是：后写的规则先匹配（外层），它输出的 <code>text(fill: blue, it)</code> 里又出现了一个 <code>"Typst"</code>，这个内层的被先写的规则接着包成红色，红在内层，字面上盖过蓝。</li>
<li><code>strong</code> 那组把这个读法钉死了：结果是括号包着的绿色文字。绿色（后写）在外层，括号替换（先写）在内层。</li>
<li>所以：<strong>后写的 show 规则在外，先写的在内，每条规则都会看到前面规则的输出。</strong> 想用颜色覆盖前面的 show 规则，最稳的办法是用 <code>show X: set text(...)</code>，<code>set</code> 形式不产生嵌套。</li>
<li><code>show heading: it =&gt; [#it]</code> 没有无限递归（有输出就说明它终止了）。</li>
<li><code>show heading.where(level: 2): none</code> 让匹配元素消失，<code>show "旧": "新"</code> 这类字符串替换也能直接写。</li>
</ul><p>渲染里还可以看一个常用组合：<code>lab/ch04/fig-show-heading.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch04/fig-show-heading.typ" target="_blank">源文件</a>）把标题换成带蓝色下划线的块，用到了标题的 <code>it.body</code>。这是文件里定义“后”那一半的部分（完整文件还有一个 <code>grid</code> 把前后并排放在一起）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>styled</span><span><span> </span><span>= </span><span>{</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>block</span><span>(</span></div></div><div><div><div>3</div></div><div><span>    </span><span>width</span><span>: </span><span>5cm</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>stroke</span><span>: (</span><span>bottom</span><span>: </span><span>1pt</span><span><span> </span><span>+</span><span> </span></span><span>blue</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>inset</span><span>: (</span><span>bottom</span><span>: </span><span>4pt</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>, </span><span>it</span><span>.</span><span>body</span><span>),</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>8</div></div><div><span>  </span><span>before</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make ch04</code> 编出来的图：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-show-heading.png" alt="左边是默认样式的“标题”和“正文。”，右边同样的内容，标题变成蓝色并带蓝色下划线" /><figcaption>左边是默认样式的“标题”和“正文。”，右边同样的内容，标题变成蓝色并带蓝色下划线</figcaption></figure><p></p><p>一个陷阱：<code>it =&gt; ... it.body</code> 只取了标题的正文，<strong>编号没了</strong>。我在 <code>/tmp/p4/t1.typ</code> 里验证：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1."</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#show</span><span> </span><span>heading</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>, </span><span>it</span><span>.</span><span>body</span><span>)</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 无号</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdftotext</code> 的输出只有 <code>无号</code>，没有 <code>1.</code>。如果标题有编号又想自己画标题，要把编号拼回去，或者改用 <code>show heading: set ...</code> 的形式。<code>xuan</code> 的 <code>base</code> 里所有标题调整都用后者，所以编号完好。</p></section>
<section><h2>选择器：能选什么<a href="#选择器能选什么"><span>#</span></a></h2><p><code>lab/ch04/selectors.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch04/selectors.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1."</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// where: filter by field.</span></div></div><div><div><div>5</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>2</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>)</span></div></div><div><div><div>6</div></div><div><span>// label: only the element carrying &lt;special&gt;.</span></div></div><div><div><div>7</div></div><div><span>#show</span><span> </span><span>&lt;special&gt;</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)</span></div></div><div><div><div>8</div></div><div><span>// or: either selector.</span></div></div><div><div><div>9</div></div><div><span>#show</span><span> </span><span>selector</span><span>(</span><span>strong</span><span>).</span><span>or</span><span>(</span><span>emph</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>purple</span><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>=</span><span> 一级标题</span></div></div><div><div><div>12</div></div><div><span>==</span><span> 二级标题</span></div></div><div><div><div>13</div></div><div><span>#</span><span>[这是 *粗体* 与 _斜体_。] </span><span>&lt;special&gt;</span></div></div><div><div><div>14</div></div><div><span>#metadata</span><span>(</span><span>none</span><span>) </span><span>&lt;mark&gt;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>=</span><span> 标记之后的一级标题</span></div></div><div><div><div>17</div></div><div><span>==</span><span> 标题里的 _斜体_</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>// `before`, `after` and 0.15 `within` cannot drive a show rule, but they work</span></div></div><div><div><div>20</div></div><div><span>// in `query`.</span></div></div><div><div><div>21</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>标记之后有 </span><span>#query</span><span>(</span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>).</span><span>after</span><span>(</span><span>&lt;mark&gt;</span><span>)).</span><span>len</span><span>() 个一级标题，</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>之前有 </span><span>#query</span><span>(</span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>).</span><span>before</span><span>(</span><span>&lt;mark&gt;</span><span>)).</span><span>len</span><span>() 个。</span></div></div><div><div><div>24</div></div><div><span>]</span></div></div><div><div><div>25</div></div><div><span>#context</span><span> [</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>标题里的斜体有 </span><span>#query</span><span>(</span><span>selector</span><span>(</span><span>emph</span><span>).</span><span>within</span><span>(</span><span>heading</span><span>)).</span><span>len</span><span>() 处，</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>全文共 </span><span>#query</span><span>(</span><span>emph</span><span>).</span><span>len</span><span>() 处。</span></div></div><div><div><div>28</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-selectors.png" alt="selectors.typ 的渲染：二级标题蓝色；“这是”“与”和句号是红色，“粗体”“斜体”是紫色；后面两行是查询得到的计数" /><figcaption>selectors.typ 的渲染：二级标题蓝色；“这是”“与”和句号是红色，“粗体”“斜体”是紫色；后面两行是查询得到的计数</figcaption></figure><p></p><p>这个文件里 <code>&lt;special&gt;</code> 贴在一个用 <code>#[ ... ]</code> 括起来的整句上，所以整句被选中：“这是”“与”和句号是红色。“粗体”和“斜体”仍然是紫色，因为它们各自还有一条 <code>strong</code>/<code>emph</code> 的规则，我观察到的结果是里层的颜色盖过外层。标签总是附着在<strong>紧挨它之前的那个元素</strong>上，所以标签与元素之间有没有空格会改变选中的范围。<code>/tmp/p4/label.typ</code>（不在仓库里）把这一点单独拿出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#show</span><span> </span><span>&lt;warn&gt;</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#show</span><span> </span><span>&lt;warn&gt;</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>block</span><span>(</span><span>stroke</span><span>: </span><span>0.5pt</span><span><span> </span><span>+</span><span> </span></span><span>red</span><span>, </span><span>inset</span><span>: </span><span>4pt</span><span>, </span><span>it</span><span>)</span></div></div><div><div><div>4</div></div><div><span>普通段落。</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>这一段带标签。 </span><span>&lt;warn&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>直接贴着标签的 *词*</span><span>&lt;warn&gt;</span><span> 才被选中。</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#</span><span>[</span><span>#set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>); 用函数调用加标签：</span><span>#text</span><span>[蓝色]</span><span>&lt;warn&gt;</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-label.png" alt="第一行普通文字无框；“这一段带标签。”被红框包住；“词”被红框包住，其余文字无框；最后“蓝色”被红框包住，但“用函数调用加标签：”是蓝色字" /><figcaption>第一行普通文字无框；“这一段带标签。”被红框包住；“词”被红框包住，其余文字无框；最后“蓝色”被红框包住，但“用函数调用加标签：”是蓝色字</figcaption></figure><p></p><p>第二段里 <code>。 &lt;warn&gt;</code> 中间有空格，标签落在了前面那句文字上；第三段 <code>*词*&lt;warn&gt;</code> 中间没空格，落在 <code>strong</code> 元素上，所以只框住“词”字。（最后一行的蓝色来自方括号里的 <code>set</code>，红框来自标签规则，两条规则互不干扰。）</p><p>选择器清单，除 <code>and</code> 外每一项都在上面的文件里出现过：</p><ul>
<li>元素函数：<code>heading</code>、<code>strong</code>、<code>emph</code>。</li>
<li>带字段过滤的：<code>heading.where(level: 2)</code>。</li>
<li>字符串和正则：<code>"Typst"</code>、<code>regex("\d+%")</code>。</li>
<li>标签：<code>&lt;special&gt;</code>。</li>
<li>组合：<code>selector(strong).or(emph)</code>。</li>
<li>求交集：<code>selector(heading).and(&lt;x&gt;)</code>，实验见下面。</li>
<li>位置关系：<code>.before(&lt;mark&gt;)</code>、<code>.after(&lt;mark&gt;)</code>，以及 0.15 新增的 <code>.within(heading)</code>，意为“位于某个 heading 内部”。写法是 <code>selector(emph).within(heading)</code>，不是 <code>emph.within(...)</code>。</li>
</ul><p><code>and</code> 不在上面的文件里，我另写了 <code>/tmp/p4/and.typ</code>（不在仓库里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#show</span><span> </span><span>selector</span><span>(</span><span>heading</span><span>).</span><span>and</span><span>(</span><span>&lt;x&gt;</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 带标签 </span><span>&lt;x&gt;</span></div></div><div><div><div>4</div></div><div><span>=</span><span> 无标签</span></div></div><div><div><div>5</div></div><div><span>#text</span><span>[正文] </span><span>&lt;x&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译通过，渲染里只有“带标签”这个标题变红：“无标签”没有 <code>&lt;x&gt;</code>，带 <code>&lt;x&gt;</code> 的“正文”不是标题，两个条件都满足才算。</p><p><strong>位置关系的三个选择器不能驱动 <code>show</code>。</strong> 我直接试了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>).</span><span>before</span><span>(</span><span>&lt;m&gt;</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)</span></div></div><div><div><div>2</div></div><div><span>=</span><span> 标题</span></div></div><div><div><div>3</div></div><div><span>#metadata</span><span>(</span><span>none</span><span>) </span><span>&lt;m&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: this selector cannot be used with show</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ before-show.typ:1:6</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #show heading.where(level: 1).before(&lt;m&gt;): set text(fill: red)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>within</code> 同样不行，但它的措辞不一样，还附带一句提示：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#show</span><span> </span><span>selector</span><span>(</span><span>emph</span><span>).</span><span>within</span><span>(</span><span>heading</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)</span></div></div><div><div><div>2</div></div><div><span>=</span><span> 带 _斜体_ 的标题</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: this selector cannot currently be used with show</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ within-show.typ:1:6</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #show selector(emph).within(heading): set text(fill: red)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= hint: support for this is planned for the future</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我猜原因是 show 规则在元素被“实现化”（realize）时匹配，那一刻元素还没有文档里的位置，而 <code>before</code>/<code>after</code> 需要比较位置，不过这只是猜测，官方提示只说了“计划支持”。无论如何，它们在 <code>query</code> 里能用，上面的文件已经算出来了：标记前后各 1 个一级标题，标题里的斜体 1 处，全文斜体 2 处。<code>query</code> 要放在 <code>context</code> 里，这两样第 7 篇专讲，现在知道有这么一条路就行。</p></section>
<section><h2>numbering：编号是一个字符串，或一个函数<a href="#numbering编号是一个字符串或一个函数"><span>#</span></a></h2><p><code>set heading(numbering: ...)</code> 在上面两个文件里各出现了一次：<code>"一、"</code> 和 <code>"1."</code>。<code>numbering</code> 参数的值要么是<strong>编号模式字符串</strong>，要么是一个<strong>函数</strong>。<code>numbering()</code> 本身也是一个普通函数，可以直接调用，这是测试模式的最快办法。<code>/tmp/p4/numbering.typ</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>12cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>8pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#numbering</span><span>(</span><span>"1.1.1"</span><span>, </span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>) </span><span>\</span></div></div><div><div><div>3</div></div><div><span>#numbering</span><span>(</span><span>"1.a.i"</span><span>, </span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>) </span><span>\</span></div></div><div><div><div>4</div></div><div><span>#numbering</span><span>(</span><span>"A.I."</span><span>, </span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>) </span><span>\</span></div></div><div><div><div>5</div></div><div><span>#numbering</span><span>(</span><span>"(1)"</span><span>, </span><span>12</span><span>) </span><span>\</span></div></div><div><div><div>6</div></div><div><span>#numbering</span><span>(</span><span>"a"</span><span>, </span><span>27</span><span>) </span><span>\</span></div></div><div><div><div>7</div></div><div><span>#numbering</span><span>(</span><span>"I"</span><span>, </span><span>14</span><span>) </span><span>\</span></div></div><div><div><div>8</div></div><div><span>#numbering</span><span>(</span><span>"一、"</span><span>, </span><span>3</span><span>) </span><span>\</span></div></div><div><div><div>9</div></div><div><span>#numbering</span><span>(</span><span>"一"</span><span>, </span><span>23</span><span>) </span><span>\</span></div></div><div><div><div>10</div></div><div><span>#numbering</span><span>(</span><span>"壹"</span><span>, </span><span>23</span><span>) </span><span>\</span></div></div><div><div><div>11</div></div><div><span>#numbering</span><span>(</span><span>"①"</span><span>, </span><span>7</span><span>) </span><span>\</span></div></div><div><div><div>12</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: (..</span><span>n</span><span>) </span><span>=&gt;</span><span> {</span></div></div><div><div><div>13</div></div><div><span>  </span><span>let</span><span> </span><span>nums</span><span><span> </span><span>= </span></span><span>n</span><span>.</span><span>pos</span><span>()</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> </span><span>nums</span><span>.</span><span>len</span><span><span>() </span><span>==</span><span> </span></span><span>1</span><span> { </span><span>"第 "</span><span><span> </span><span>+</span><span> </span></span><span>str</span><span>(</span><span>nums</span><span>.</span><span>first</span><span><span>()) </span><span>+</span><span> </span></span><span>" 章"</span><span> } </span><span>else</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>nums</span><span>.</span><span>map</span><span>(</span><span>str</span><span>).</span><span>join</span><span>(</span><span>"."</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>})</span></div></div><div><div><div>18</div></div><div><span>=</span><span> 起步</span></div></div><div><div><div>19</div></div><div><span>==</span><span> 细节</span></div></div><div><div><div>20</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1."</span><span>)</span></div></div><div><div><div>21</div></div><div><span>=</span><span> 换了编号</span></div></div><div><div><div>22</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>none</span><span>)</span></div></div><div><div><div>23</div></div><div><span>=</span><span> 无编号</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pdftotext -layout</code> 的输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1.2.3</span></div></div><div><div><div>2</div></div><div><span>1.b.iii</span></div></div><div><div><div>3</div></div><div><span>A.II.III.</span></div></div><div><div><div>4</div></div><div><span>(12)</span></div></div><div><div><div>5</div></div><div><span>aa</span></div></div><div><div><div>6</div></div><div><span>XIV</span></div></div><div><div><div>7</div></div><div><span>三、</span></div></div><div><div><div>8</div></div><div><span>二十三</span></div></div><div><div><div>9</div></div><div><span>贰拾叁</span></div></div><div><div><div>10</div></div><div><span>⑦</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>第 1 章 起步</span></div></div><div><div><div>13</div></div><div><span>1.1 细节</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>2. 换了编号</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>无编号</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>要点：</p><ul>
<li>模式串里的 <code>1</code>、<code>a</code>、<code>A</code>、<code>i</code>、<code>I</code>、<code>一</code>、<code>壹</code>、<code>①</code> 等是“计数符号”，其余字符（点、括号、顿号）原样保留。传入的计数比符号多时，最后一个符号连同它前面的分隔符会重复：我另外试了 <code>numbering("1.a", 1, 2, 3)</code>，输出是 <code>1.b.c</code>。</li>
<li><code>numbering("a", 27)</code> 是 <code>aa</code>，我还试了 28 和 53，分别是 <code>ab</code> 和 <code>ba</code>，26 是 <code>z</code>，进位方式和电子表格的列名一样。</li>
<li>中文数字 <code>一</code> 和大写数字 <code>壹</code> 都是内置的，23 分别是“二十三”和“贰拾叁”。第 5 篇会把它们用到中文标题里。</li>
<li>函数形式接收所有层级的计数，返回字符串或 content。上面的例子给一级标题加“第 N 章”，其余层级用点号连接。</li>
<li>换模式串或者 <code>set heading(numbering: none)</code> 都<strong>不会重置计数器</strong>：输出里“换了编号”是 <code>2.</code>，因为前面已经有了“起步”这一章，计数一直在往上走。重置计数器是第 7 篇的内容。</li>
</ul></section>
<section><h2>模板：<code>show: f.with(...)</code><a href="#模板show-fwith"><span>#</span></a></h2><p>前面说 <code>show: f</code> 把后面所有内容交给 <code>f</code>。模板就是这个形状的函数：<code>body</code> 是最后一个位置参数，其他参数有默认值，体内先 <code>set</code> 再 <code>show</code>，最后输出 <code>body</code>。先写一个最小的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>mini</span><span>(</span><span>title</span><span>: </span><span>none</span><span>, </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>set</span><span> </span><span>page</span><span>(</span><span>paper</span><span>: </span><span>"a6"</span><span>, </span><span>margin</span><span>: </span><span>1.5cm</span><span>)</span></div></div><div><div><div>3</div></div><div><span>  </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>10pt</span><span>)</span></div></div><div><div><div>4</div></div><div><span>  </span><span>set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>blue</span><span>)</span></div></div><div><div><div>6</div></div><div><span>  </span><span>align</span><span>(</span><span>center</span><span>, </span><span>text</span><span>(</span><span>1.6em</span><span>, </span><span>strong</span><span>(</span><span>title</span><span>)))</span></div></div><div><div><div>7</div></div><div><span>  </span><span>body</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/mini:0.1.0"</span><span>: </span><span>mini</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>mini</span><span>.</span><span>with</span><span>(</span><span>title</span><span>: </span><span>"迷你模板"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 一节</span></div></div><div><div><div>4</div></div><div><span>正文。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里 <code>mini.with(title: "迷你模板")</code> 是第 3 篇讲过的 <code>.with</code>，返回一个只差 <code>body</code> 的新函数。<code>show: </code> 后面没有选择器，所以这个新函数拿到的就是“从这一行往后的整个文档”。下面这样写完全等价：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/mini:0.1.0"</span><span>: </span><span>mini</span></div></div><div><div><div>2</div></div><div><span>#show</span><span>: </span><span>it</span><span> </span><span>=&gt;</span><span> </span><span>mini</span><span>(</span><span>it</span><span>, </span><span>title</span><span>: </span><span>"迷你模板"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>=</span><span> 一节</span></div></div><div><div><div>4</div></div><div><span>正文。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把两份源文件各编成 PNG，用 <code>cmp</code> 比较，输出是 <code>identical</code>。写成函数体里先 <code>set</code> 后 <code>show</code> 还有一个原因：函数体也是作用域，这些规则只对 <code>body</code> 生效，不会漏到调用者的后续代码里。</p><p><code>#show: x</code> 不一定要放在文件开头，放在中间就只管它后面的那一半文档。</p></section>
<section><h2>把模板装成包：<code>@local/xuan:0.1.0</code><a href="#把模板装成包localxuan010"><span>#</span></a></h2><p>文件开头那句 <code>#import "@local/mini:0.1.0": mini</code> 里的 <code>@local/mini:0.1.0</code> 是一个<strong>包说明符</strong>，形状是 <code>@命名空间/包名:版本</code>。<code>@preview</code> 是 Typst Universe 的命名空间（第 14 篇），<code>@local</code> 是留给本机包的。Typst 怎么把说明符变成磁盘上的目录？规则是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;package-path&gt;/&lt;命名空间&gt;/&lt;包名&gt;/&lt;版本&gt;/typst.toml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我在 <code>/tmp/p4/mini/</code> 里放了这个最小包，目录是这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ find packages -type f | sort</span></div></div><div><div><div>2</div></div><div><span>packages/local/mini/0.1.0/lib.typ</span></div></div><div><div><div>3</div></div><div><span>packages/local/mini/0.1.0/typst.toml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先不告诉 Typst 包在哪，直接编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile doc.typ doc.pdf</span></div></div><div><div><div>2</div></div><div><span>error: package not found (searched for @local/mini:0.1.0)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ doc.typ:1:8</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>1 │ #import "@local/mini:0.1.0": mini</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│         ^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（这一步我在 <code>typst-lab</code> 的 <code>nix develop</code> 里跑，环境变量 <code>TYPST_PACKAGE_PATH</code> 已经指向 <code>typst-lab/packages</code>，那里只有 <code>xuan</code>，所以找不到 <code>mini</code>。）</p><p>告诉 Typst 去哪找，有两种等价写法，一个是命令行参数，一个是环境变量：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --package-path packages doc.typ doc.pdf</span></div></div><div><div><div>2</div></div><div><span>$ TYPST_PACKAGE_PATH=$PWD/packages typst compile doc.typ doc2.pdf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两条命令都以退出码 0 结束。命令行参数和环境变量同时存在时，参数覆盖环境变量，我把 <code>TYPST_PACKAGE_PATH</code> 设成 <code>typst-lab/packages</code> 的同时传 <code>--package-path packages</code> 就编译成功了。</p><p><code>typst-lab</code> 的 <code>flake.nix</code> 里 <code>shellHook</code> 把它设成 <code>$PWD/packages</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>export</span><span> </span><span>TYPST_PACKAGE_PATH</span></span><span>=</span><span>"$PWD/packages"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以仓库里每一份文档都可以直接写 <code>#import "@local/xuan:0.1.0": *</code>，不需要任何安装步骤。</p><p>如果既不传参数也不设环境变量，<code>@local</code> 回落到系统默认目录。我把 <code>XDG_DATA_HOME</code> 指到 <code>/tmp/p4/xdg</code>，在 <code>$XDG_DATA_HOME/typst/packages/local/mini</code> 放了一个符号链接指向包目录，取消 <code>TYPST_PACKAGE_PATH</code> 后直接 <code>typst compile doc.typ doc3.pdf</code>，退出码 0。这条路就是 <code>typst-lab</code> 里 <code>make install</code> 在做的事：往 <code>~/.local/share/typst/packages/local/</code> 链一个 <code>xuan</code>，这样你在仓库之外写文档也能 <code>@local/xuan:0.1.0</code>（第 16 篇会用到）。</p><section><h3><code>typst.toml</code><a href="#typsttoml"><span>#</span></a></h3><p><code>xuan</code> 的 <code>typst.toml</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/typst.toml" target="_blank">源文件</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"xuan"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>entrypoint</span><span> = </span><span>"lib.typ"</span></div></div><div><div><div>5</div></div><div><span>compiler</span><span> = </span><span>"0.15.0"</span></div></div><div><div><div>6</div></div><div><span>authors</span><span> = [</span><span>"bfmhno3"</span><span>]</span></div></div><div><div><div>7</div></div><div><span>license</span><span> = </span><span>"MIT OR Apache-2.0"</span></div></div><div><div><div>8</div></div><div><span>description</span><span> = </span><span>"Notes, homework, slides and CV templates for Chinese and English writing"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>[</span><span>template</span><span>]</span></div></div><div><div><div>11</div></div><div><span>path</span><span> = </span><span>"template"</span></div></div><div><div><div>12</div></div><div><span>entrypoint</span><span> = </span><span>"main.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>name</code> 和 <code>version</code> 必须与目录名 <code>xuan/0.1.0</code> 对得上，<code>entrypoint</code> 是 <code>import</code> 这个包时真正被加载的文件，<code>compiler</code> 声明最低编译器版本。这里写的是 0.15.0 而不是我编译用的 0.15.1：编辑器的语言服务器 tinymist 0.15.2 内嵌的是 Typst 0.15.0，写 0.15.1 它会拒绝导入这个包（第 16 篇讲 tinymist）。<code>[template]</code> 一节让 <code>typst init @local/xuan:0.1.0 项目目录</code> 能拷出一份起始文档，对应的 <code>template/</code> 目录第 16 篇才建，现在它还不存在，当前章节编译不用它。这个文件之后没有再改。</p></section><section><h3><code>lib.typ</code>：只做再导出<a href="#libtyp只做再导出"><span>#</span></a></h3><p><code>lib.typ</code> 里不写任何逻辑，只用 <code>#import</code> 把其他文件里的名字汇到一处，这样使用者只需要 <code>#import "@local/xuan:0.1.0": *</code>。下面是第 4 章结束时的状态，也是仓库历史里最早的版本（<code>tutorial-v1</code> 标签里是最终状态）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"src/util.typ"</span><span>: </span><span>ensure-array</span><span>, </span><span>pick</span><span>, </span><span>plain-text</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"src/fonts.typ"</span><span>: </span><span>fonts</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"src/cjk.typ"</span><span>: </span><span>xuan-cjk</span></div></div><div><div><div>4</div></div><div><span>#import</span><span> </span><span>"src/page.typ"</span><span>: </span><span>title-block</span><span>, </span><span>xuan-page</span></div></div><div><div><div>5</div></div><div><span>#import</span><span> </span><span>"src/header.typ"</span><span>: </span><span>xuan-footer</span><span>, </span><span>xuan-header</span></div></div><div><div><div>6</div></div><div><span>#import</span><span> </span><span>"src/theorem.typ"</span><span>: (</span></div></div><div><div><div>7</div></div><div><span>  </span><span>corollary</span><span>, </span><span>definition</span><span>, </span><span>example</span><span>, </span><span>lemma</span><span>, </span><span>make-env</span><span>, </span><span>proof</span><span>, </span><span>theorem</span><span>,</span></div></div><div><div><div>8</div></div><div><span>)</span></div></div><div><div><div>9</div></div><div><span>#import</span><span> </span><span>"src/base.typ"</span><span>: </span><span>xuan-base</span></div></div><div><div><div>10</div></div><div><span>#import</span><span> </span><span>"src/note.typ"</span><span>: </span><span>note</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行是第 3 篇写好的 <code>util.typ</code>。其余几行指向后面几篇的文件，就是前面说的“先拷过来”。这是<strong>中间版本</strong>：之后每一章有新模块，这里就多几行 <code>#import</code>，到 <code>tutorial-v1</code> 它长到 16 行（<code>slides</code>、<code>cv</code>、<code>homework</code>、<code>glossary</code>、<code>math</code>、<code>units</code> 的导入是后面章节加的）。</p><p>包内部文件之间用相对路径互相 import（<code>"src/util.typ"</code>、<code>"cjk.typ"</code>），相对的是<strong>写这行 import 的文件</strong>自己所在的目录，不是调用者的目录。这一点在第 12 篇讲模块时再展开。</p></section><section><h3><code>src/base.typ</code>：全文通用的默认值<a href="#srcbasetyp全文通用的默认值"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"cjk.typ"</span><span>: </span><span>xuan-cjk</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"theorem.typ"</span><span>: </span><span>theorem-rules</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>/// Document-wide defaults: CJK typography, heading numbering and spacing,</span></div></div><div><div><div>5</div></div><div><span>/// link colour, theorem environments.</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>xuan-base</span><span>(</span><span>body</span><span>, </span><span>lang</span><span>: </span><span>"zh"</span><span>, </span><span>title</span><span>: </span><span>none</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-cjk</span><span>.</span><span>with</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>)</span></div></div><div><div><div>8</div></div><div><span>  </span><span>set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>: </span><span>set</span><span> </span><span>block</span><span>(</span><span>above</span><span>: </span><span>1.4em</span><span>, </span><span>below</span><span>: </span><span>0.9em</span><span>)</span></div></div><div><div><div>10</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>set</span><span> </span><span>text</span><span>(</span><span>size</span><span>: </span><span>1.4em</span><span>)</span></div></div><div><div><div>11</div></div><div><span>  </span><span>show</span><span> </span><span>heading</span><span>.</span><span>where</span><span>(</span><span>level</span><span>: </span><span>1</span><span>): </span><span>set</span><span> </span><span>block</span><span>(</span><span>above</span><span>: </span><span>1.8em</span><span>, </span><span>below</span><span>: </span><span>1em</span><span>)</span></div></div><div><div><div>12</div></div><div><span>  </span><span>show</span><span> </span><span>link</span><span>: </span><span>set</span><span> </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>rgb</span><span>(</span><span>"#1f4e9c"</span><span>))</span></div></div><div><div><div>13</div></div><div><span>  </span><span>show</span><span>: </span><span>theorem-rules</span></div></div><div><div><div>14</div></div><div><span>  </span><span>body</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>15 行，每一行都是这一篇学过的东西：</p><ul>
<li><code>show: xuan-cjk.with(lang: lang)</code>：<strong>模板可以嵌套</strong>。<code>xuan-cjk</code> 本身也是 <code>(body, lang:)</code> 形状的函数，它被另一个模板用 <code>show:</code> 调用。字体和段落规则在第 5 篇。</li>
<li><code>set heading(numbering: "1.1")</code>：登记标题编号模式。</li>
<li>两条 <code>show heading ...: set block(...)</code>：全部标题和一级标题各自的上下间距。三条标题规则都是 show-set 形式，没有产生嵌套，也就没有前面说的先后顺序问题。</li>
<li><code>show heading.where(level: 1): set text(size: 1.4em)</code>：一级标题放大 1.4 倍，相对继承字号（上面 <code>em</code> 实验）。</li>
<li><code>show link: set text(fill: ...)</code>：链接的颜色。</li>
<li><code>show: theorem-rules</code>：又是一个无选择器的 show，把定理环境的规则装进来（第 7 篇）。</li>
</ul><p>之后它还会长：到 <code>tutorial-v1</code>，文件是 21 行，多了 <code>math-rules</code>、<code>figure-rules</code>、<code>code-rules</code> 三个 import 和对应三行 <code>show:</code>（第 8 到第 9 篇）。</p></section><section><h3><code>src/note.typ</code>：把一切装配起来<a href="#srcnotetyp把一切装配起来"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"base.typ"</span><span>: </span><span>xuan-base</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"header.typ"</span><span>: </span><span>xuan-footer</span><span>, </span><span>xuan-header</span></div></div><div><div><div>3</div></div><div><span>#import</span><span> </span><span>"page.typ"</span><span>: </span><span>title-block</span><span>, </span><span>xuan-page</span></div></div><div><div><div>4</div></div><div><span>#import</span><span> </span><span>"util.typ"</span><span>: </span><span>ensure-array</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/// Notes and reports.</span></div></div><div><div><div>7</div></div><div><span>#let</span><span> </span><span>note</span><span>(</span></div></div><div><div><div>8</div></div><div><span>  </span><span>title</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>subtitle</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>author</span><span>: (),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>date</span><span>: </span><span>auto</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>abstract</span><span>: </span><span>none</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>keywords</span><span>: (),</span></div></div><div><div><div>14</div></div><div><span>  </span><span>toc</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>paper</span><span>: </span><span>"a4"</span><span>,</span></div></div><div><div><div>16</div></div><div><span>  </span><span>lang</span><span>: </span><span>"zh"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>  </span><span>body</span><span>,</span></div></div><div><div><div>18</div></div><div><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>19</div></div><div><span>  </span><span>set</span><span> </span><span>document</span><span>(</span></div></div><div><div><div>20</div></div><div><span>    </span><span>title</span><span>: </span><span>title</span><span>,</span></div></div><div><div><div>21</div></div><div><span>    </span><span>author</span><span>: </span><span>ensure-array</span><span>(</span><span>author</span><span>),</span></div></div><div><div><div>22</div></div><div><span>    </span><span>keywords</span><span>: </span><span>keywords</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>24</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-page</span><span>.</span><span>with</span><span>(</span><span>paper</span><span>: </span><span>paper</span><span>)</span></div></div><div><div><div>25</div></div><div><span>  </span><span>set</span><span> </span><span>page</span><span>(</span><span>header</span><span>: </span><span>xuan-header</span><span>(), </span><span>footer</span><span>: </span><span>xuan-footer</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>))</span></div></div><div><div><div>26</div></div><div><span>  </span><span>show</span><span>: </span><span>xuan-base</span><span>.</span><span>with</span><span>(</span><span>lang</span><span>: </span><span>lang</span><span>, </span><span>title</span><span>: </span><span>title</span><span>)</span></div></div><div><div><div>27</div></div><div><span>  </span><span>title-block</span><span>(</span></div></div><div><div><div>28</div></div><div><span>    </span><span>title</span><span>: </span><span>title</span><span>,</span></div></div><div><div><div>29</div></div><div><span>    </span><span>subtitle</span><span>: </span><span>subtitle</span><span>,</span></div></div><div><div><div>30</div></div><div><span>    </span><span>author</span><span>: </span><span>author</span><span>,</span></div></div><div><div><div>31</div></div><div><span>    </span><span>date</span><span>: </span><span>date</span><span>,</span></div></div><div><div><div>32</div></div><div><span>    </span><span>lang</span><span>: </span><span>lang</span><span>,</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span>  </span><span>if</span><span> </span><span>abstract</span><span><span> </span><span>!=</span><span> </span></span><span>none</span><span> {</span></div></div><div><div><div>35</div></div><div><span>    </span><span>align</span><span>(</span><span>center</span><span>, </span><span>block</span><span>(</span><span>width</span><span>: </span><span>85%</span><span>, </span><span>align</span><span>(</span><span>left</span><span>, {</span></div></div><div><div><div>36</div></div><div><span>      </span><span>set</span><span> </span><span>par</span><span>(</span><span>first-line-indent</span><span>: </span><span>0pt</span><span>)</span></div></div><div><div><div>37</div></div><div><span>      </span><span>strong</span><span>(</span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [摘要] </span><span>else</span><span> [Abstract])</span></div></div><div><div><div>38</div></div><div><span>      </span><span>h</span><span>(</span><span>1em</span><span>)</span></div></div><div><div><div>39</div></div><div><span>      </span><span>abstract</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>})))</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span>  </span><span>if</span><span> </span><span>toc</span><span> { </span><span>outline</span><span>(</span><span>title</span><span>: </span><span>if</span><span> </span><span>lang</span><span><span> </span><span>==</span><span> </span></span><span>"zh"</span><span> [目录] </span><span>else</span><span> [Contents]) }</span></div></div><div><div><div>43</div></div><div><span>  </span><span>body</span></div></div><div><div><div>44</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>44 行。三件事值得点出来：</p><ul>
<li>前面是一串 <code>set</code>/<code>show: ...with</code>，<strong>顺序有意义</strong>：先装页面，再装全文默认值，最后才输出标题块、摘要和目录，它们已经处在这些规则的作用域里。</li>
<li><code>ensure-array(author)</code> 来自 <code>util.typ</code>，让调用者既可以传 <code>"bfmhno3"</code> 也可以传 <code>("a", "b")</code>。</li>
<li><code>outline()</code> 生成目录，具体用法第 9 篇。<code>xuan-page</code>、<code>title-block</code>、<code>xuan-header</code>、<code>xuan-footer</code> 分别在第 6、7 篇。</li>
</ul><p>它也是中间版本：之后 <code>note</code> 多一个 <code>bibliography: none,</code> 参数和文末 4 行把参考文献放到末尾的代码（第 10 篇）。</p></section><section><h3>用起来<a href="#用起来"><span>#</span></a></h3><p><code>lab/ch04/first-note/main.typ</code>（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch04/first-note/main.typ" target="_blank">源文件</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#show</span><span>: </span><span>note</span><span>.</span><span>with</span><span>(</span></div></div><div><div><div>4</div></div><div><span>  </span><span>title</span><span>: </span><span>"我的第一份笔记"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>author</span><span>: </span><span>"bfmhno3"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>abstract</span><span>: [用 </span><span>`xuan`</span><span> 的 </span><span>`note`</span><span> 模板写一页带目录的文档。],</span></div></div><div><div><div>7</div></div><div><span>  </span><span>keywords</span><span>: (</span><span>"Typst"</span><span>, </span><span>"笔记"</span><span>),</span></div></div><div><div><div>8</div></div><div><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>=</span><span> 开始</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>模板把页边距、字体、标题编号和页眉页脚都收进了一次 </span><span>`show:`</span><span>。</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>==</span><span> 小节</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>后面的篇章会把这个模板一点点补全。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出 <code>out/ch04/first-note.pdf</code>，1 页：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch04-first-note.png" alt="first-note 的第一页：居中的标题“我的第一份笔记”，下面是作者和日期，摘要框，目录，两级编号标题和正文，页脚是“第 1 页 / 共 1 页”" /><figcaption>first-note 的第一页：居中的标题“我的第一份笔记”，下面是作者和日期，摘要框，目录，两级编号标题和正文，页脚是“第 1 页 / 共 1 页”</figcaption></figure><p></p><p>为了确认“本章检查点”这个说法站得住，我从仓库历史最早的一次提交里取出包（只有本章之前写好的那些文件），放到 <code>/tmp</code> 下，用 <code>--package-path</code> 指过去编译同一份 <code>main.typ</code>，同样 1 页（上面那张图就是这次编译的结果）。你不需要重复这一步：<code>tutorial-v1</code> 标签里的是最终状态，本文里引用的早期版本以文中所列内容为准。它与最终版本的输出相比，<code>pdftotext</code> 只有一处差别：行内代码 <code>show:</code> 和后面句号之间的空格。我猜这来自后面章节给行内代码加的样式，没有去追。</p></section></section>
<section><h2>几个常见的坑<a href="#几个常见的坑"><span>#</span></a></h2><ol>
<li><strong>用 <code>set</code> 想改的参数不是 settable。</strong> 报错只有一行 <code>unexpected argument</code>，看上面的 <code>not-settable.fail.typ</code>。</li>
<li><strong>想用 <code>show</code> 选“某个标记之后的”元素。</strong> 选择器位置关系不能进 <code>show</code>，只能放在 <code>query</code> 里数一数、取出来。<code>query</code> 本身第 7 篇专讲。</li>
<li><strong><code>show X: it =&gt; ... it.body</code></strong> 会丢掉元素自己的属性，比如标题编号、<code>strong</code> 的粗体。要么重新拼，要么改用 <code>show X: set ...</code>。</li>
<li><strong>多条 <code>show</code> 转换规则的先后。</strong> 后写的在外层。要覆盖就用 <code>set</code>。</li>
<li><strong>标签空格。</strong> <code>*词*&lt;warn&gt;</code> 与 <code>*词* &lt;warn&gt;</code> 不是同一回事。</li>
</ol></section>
<section><h2>接下来<a href="#接下来"><span>#</span></a></h2><p>这一篇把 <code>xuan</code> 的骨架立起来了，但 <code>base.typ</code> 里 <code>xuan-cjk</code> 到底设了什么，我只说了“字体和段落规则”。第 5 篇就是它：<code>text</code> 的字体列表、<code>cjk-latin-spacing</code>、<code>par</code> 的首行缩进，以及 Typst 0.15 新增的可变字体 <code>variations</code>。想自己先试的话，从 <code>lab/ch05/</code> 开始：<a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1/lab/ch05" target="_blank"><code>lab/ch05</code></a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-03-functions-and-content/</id>
      <title type="text">函数与 content：一切都是函数调用</title>
      <published>2026-10-19T02:00:00.000Z</published>
      <updated>2026-10-19T02:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-03-functions-and-content/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 3 篇：我用 Typst 0.15.1 写了命名参数、..args、闭包和 .with，亲手触发了“外部变量只读”的编译错误，打印出 [*a* b] 的 content 树，再据此写出 xuan 的第一个模块 util.typ 和它的 assert 测试。</summary>
      <content type="html"><![CDATA[<p>在 Typst 里写 <code>[*a* b]</code>，编译器看到的不是一串字符，而是这个：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sequence(strong(body: [a]), [ ], [b])</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p><code>*a*</code> 是对函数 <code>strong</code> 的一次调用，<code>b</code> 是一段文本，中间的空格是第三个元素，三者被一个 <code>sequence</code> 串起来。这一篇的全部内容都围绕这一行：函数怎么定义、参数怎么传、闭包能看到什么，以及“一切都是函数调用”落到 content 上到底长什么样。最后用这些知识写出 <code>xuan</code> 的第一个模块 <code>src/util.typ</code>。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>packages/local/xuan/0.1.0/src/util.typ</code>（final，33 行，一个字都不会再改），<code>lab/ch03/</code> 下四个实验文件，以及 <code>tests/util.typ</code> 里对 util 的断言。<code>make ch03</code> 通过（三个正常文件加一个预期失败的 <code>capture-mutation.fail.typ</code>），<code>make test</code> 通过。
<code>lab/ch03/content-tree.typ</code> 从 <code>@local/xuan:0.1.0</code> 导入 <code>plain-text</code>，而包的清单 <code>typst.toml</code> 和入口 <code>lib.typ</code> 要到<a href="/posts/typst-by-xuan-04-set-and-show/">下一篇</a>才从零搭起；现在先把仓库里的 <code>packages/</code> 整个拿来用即可。<code>lib.typ</code> 的第一行就是 <code>#import "src/util.typ": ensure-array, pick, plain-text</code>，其余行以后再长。
前置：<a href="/posts/typst-by-xuan-02-code-mode/">第 2 篇：代码模式</a>。所有命令都在 <code>typst-lab</code> 的 <code>nix develop</code> 里用 Typst 0.15.1 跑出，仓库固定在 <a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1" target="_blank"><code>tutorial-v1</code></a> 标签。</p></div></div>
<section><h2>函数是值<a href="#函数是值"><span>#</span></a></h2><p>先看 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch03/functions.typ" target="_blank"><code>lab/ch03/functions.typ</code></a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// Named and default parameters; the last expression is the return value.</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>badge</span><span>(</span><span>text-fill</span><span>: </span><span>blue</span><span>, </span><span>size</span><span>: </span><span>1em</span><span>, </span><span>body</span><span><span>) </span><span>= </span></span><span>box</span><span>(</span></div></div><div><div><div>5</div></div><div><span>  </span><span>fill</span><span>: </span><span>text-fill</span><span>.</span><span>lighten</span><span>(</span><span>80%</span><span>),</span></div></div><div><div><div>6</div></div><div><span>  </span><span>inset</span><span>: (</span><span>x</span><span>: </span><span>0.4em</span><span>, </span><span>y</span><span>: </span><span>0.2em</span><span>),</span></div></div><div><div><div>7</div></div><div><span>  </span><span>radius</span><span>: </span><span>3pt</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>text</span><span>(</span><span>fill</span><span>: </span><span>text-fill</span><span>, </span><span>size</span><span>: </span><span>size</span><span>, </span><span>body</span><span>),</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#badge</span><span>[默认] </span><span>#badge</span><span>(</span><span>text-fill</span><span>: </span><span>red</span><span>)[红色] </span><span>#badge</span><span>(</span><span>size</span><span>: </span><span>1.4em</span><span>)[大号]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>// Functions are values: store them, pass them, return them.</span></div></div><div><div><div>14</div></div><div><span>#let</span><span> </span><span>twice</span><span>(</span><span>f</span><span><span>) </span><span>= </span></span><span>x</span><span> </span><span>=&gt;</span><span> </span><span>f</span><span>(</span><span>f</span><span>(</span><span>x</span><span>))</span></div></div><div><div><div>15</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>twice</span><span>(</span><span>x</span><span> </span><span>=&gt;</span><span> </span><span>x</span><span><span> </span><span>+</span><span> </span></span><span>3</span><span>)(</span><span>10</span><span>), </span><span>16</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>// `.with` fixes some arguments and returns a new function.</span></div></div><div><div><div>18</div></div><div><span>#let</span><span> </span><span>warn</span><span><span> </span><span>= </span></span><span>badge</span><span>.</span><span>with</span><span>(</span><span>text-fill</span><span>: </span><span>orange</span><span>)</span></div></div><div><div><div>19</div></div><div><span>#warn</span><span>[警告]</span></div></div><div><div><div>20</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>warn</span><span>), </span><span>function</span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>// Recursion works because `let` binds the name before the body runs.</span></div></div><div><div><div>23</div></div><div><span>#let</span><span> </span><span>fib</span><span>(</span><span>n</span><span><span>) </span><span>= </span></span><span>if</span><span> </span><span>n</span><span><span> </span><span>&lt;</span><span> </span></span><span>2</span><span> { </span><span>n</span><span> } </span><span>else</span><span> { </span><span>fib</span><span>(</span><span>n</span><span><span> </span><span>-</span><span> </span></span><span>1</span><span><span>) </span><span>+</span><span> </span></span><span>fib</span><span>(</span><span>n</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span>) }</span></div></div><div><div><div>24</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>range</span><span>(</span><span>10</span><span>).</span><span>map</span><span>(</span><span>fib</span><span>), (</span><span>0</span><span>, </span><span>1</span><span>, </span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>5</span><span>, </span><span>8</span><span>, </span><span>13</span><span>, </span><span>21</span><span>, </span><span>34</span><span>))</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>// Anonymous functions are closures over the scope where they were written.</span></div></div><div><div><div>27</div></div><div><span>#let</span><span> </span><span>make-adder</span><span>(</span><span>n</span><span><span>) </span><span>= </span></span><span>x</span><span> </span><span>=&gt;</span><span> </span><span>x</span><span><span> </span><span>+</span><span> </span></span><span>n</span></div></div><div><div><div>28</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>make-adder</span><span>(</span><span>5</span><span>)(</span><span>1</span><span>), </span><span>6</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>函数定义就是 <code>let</code> 加一个参数列表。</strong> <code>#let badge(text-fill: blue, size: 1em, body) = ...</code> 里，带冒号的是命名参数，冒号右边是默认值；最后那个没有默认值的 <code>body</code> 是位置参数，调用时既可以写 <code>badge("x")</code>，也可以把最后一个内容参数挪到方括号里写成 <code>badge[x]</code>。后一种写法不是特例，<code>[...]</code> 就是一个 content 值，作为最后一个位置参数跟在调用后面而已。<code>#badge(size: 1.4em)[大号]</code> 里圆括号管命名参数，方括号管 <code>body</code>。</p><p><strong>没有 <code>return</code>。</strong> 函数体是一个表达式，值就是返回值。<code>badge</code> 的体就是一次 <code>box(...)</code> 调用；<code>fib</code> 的体是 <code>if</code> 表达式，两个分支各给出一个值。需要多条语句时写 <code>{ ... }</code>，花括号里最后一个表达式（或者前面所有表达式 join 起来的结果）是返回值，规则在第 2 篇讲过，这里不再重复。</p><p><strong>函数是值。</strong> <code>twice(f)</code> 接受一个函数，返回另一个函数；<code>.with</code> 把部分参数固定住，得到新的函数（<code>warn</code> 就是默认颜色改成橙色的 <code>badge</code>）。<code>type(warn)</code> 是 <code>function</code>，这和 <code>type(1)</code> 是 <code>int</code> 没有区别，所以函数可以放进数组、字典，传给 <code>map</code>，被别的函数返回。</p><p>递归不需要额外声明：<code>let</code> 先绑定名字再求值函数体，所以 <code>fib</code> 在体内能看到自己。（我没有给 <code>fib(30)</code> 计时，看它的写法就知道指数级；真要算斐波那契，用 <code>range</code> 加 <code>fold</code> 循环。）</p><p>编译一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make ch03</span></div></div><div><div><div>2</div></div><div><span>ok   lab/ch03/arguments.typ</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch03/capture-mutation.fail.typ (failed as expected)</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch03/content-tree.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch03/functions.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个 <code>assert</code> 都过了，页面上是三个彩色标签和一个橙色的“警告”。</p></section>
<section><h2>参数：收集与展开<a href="#参数收集与展开"><span>#</span></a></h2><p><a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch03/arguments.typ" target="_blank"><code>lab/ch03/arguments.typ</code></a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// `..args` collects the extra positional and named arguments.</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>describe</span><span>(..</span><span>args</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>5</div></div><div><span>  </span><span>let</span><span> </span><span>pos</span><span><span> </span><span>= </span></span><span>args</span><span>.</span><span>pos</span><span>()</span></div></div><div><div><div>6</div></div><div><span>  </span><span>let</span><span> </span><span>named</span><span><span> </span><span>= </span></span><span>args</span><span>.</span><span>named</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>[位置参数 </span><span>#pos</span><span>.</span><span>len</span><span>() 个：</span><span>#repr</span><span>(</span><span>pos</span><span>)；命名参数 </span><span>#named</span><span>.</span><span>len</span><span>() 个：</span><span>#repr</span><span>(</span><span>named</span><span>)。]</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#describe</span><span>(</span><span>1</span><span>, </span><span>"二"</span><span>, </span><span>a</span><span>: </span><span>3</span><span>, </span><span>b</span><span>: </span><span>4</span><span>) </span><span>\</span></div></div><div><div><div>11</div></div><div><span>#describe</span><span>()</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>// Spreading goes the other way: `..xs` expands an array or a dictionary.</span></div></div><div><div><div>14</div></div><div><span>#let</span><span> </span><span>rect-of</span><span>(</span><span>w</span><span>, </span><span>h</span><span>, </span><span>fill</span><span>: </span><span>none</span><span><span>) </span><span>= </span></span><span>rect</span><span>(</span><span>width</span><span>: </span><span>w</span><span>, </span><span>height</span><span>: </span><span>h</span><span>, </span><span>fill</span><span>: </span><span>fill</span><span>)</span></div></div><div><div><div>15</div></div><div><span>#let</span><span> </span><span>size</span><span><span> </span><span>= </span><span>(</span></span><span>2cm</span><span>, </span><span>1cm</span><span>)</span></div></div><div><div><div>16</div></div><div><span>#let</span><span> </span><span>style</span><span><span> </span><span>= </span><span>(</span></span><span>fill</span><span>: </span><span>aqua</span><span>)</span></div></div><div><div><div>17</div></div><div><span>#rect-of</span><span>(..</span><span>size</span><span>, ..</span><span>style</span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>// 0.15: named values of `arguments` are available as fields.</span></div></div><div><div><div>20</div></div><div><span>#let</span><span> </span><span>args</span><span><span> </span><span>= </span></span><span>arguments</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>color</span><span>: </span><span>"red"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>args</span><span>.</span><span>color</span><span>, </span><span>"red"</span><span>)</span></div></div><div><div><div>22</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>args</span><span>.</span><span>pos</span><span>(), (</span><span>1</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>23</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>args</span><span>.</span><span>named</span><span>(), (</span><span>color</span><span>: </span><span>"red"</span><span>))</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>// Forwarding: wrap a function and pass everything through.</span></div></div><div><div><div>26</div></div><div><span>#let</span><span> </span><span>loud</span><span>(..</span><span>args</span><span><span>) </span><span>= </span></span><span>text</span><span>(</span><span>weight</span><span>: </span><span>"bold"</span><span>, ..</span><span>args</span><span>)</span></div></div><div><div><div>27</div></div><div><span>#loud</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)[转发命名参数]</span></div></div><div><div><div>28</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>calc</span><span>.</span><span>max</span><span>(..(</span><span>3</span><span>, </span><span>9</span><span>, </span><span>4</span><span>)), </span><span>9</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我用 <code>pdftotext</code> 读出前两行的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>位置参数 2 个：(1, "二")；命名参数 2 个：(a: 3, b: 4)。</span></div></div><div><div><div>2</div></div><div><span>位置参数 0 个：()；命名参数 0 个：(:)。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>(:)</code> 是空字典的写法，<code>()</code> 是空数组，这两个字面量在第 2 篇出现过。</p><p><strong><code>..args</code> 把剩下的参数全部收进一个 <code>arguments</code> 值。</strong> 它不是数组也不是字典，而是两者的合体：<code>.pos()</code> 取出位置参数（数组），<code>.named()</code> 取出命名参数（字典）。0.15 起可以直接用字段访问读命名值，<code>args.color</code> 等于 <code>args.named().color</code>，上面三条断言里第一条用的就是这个新写法。</p><p><strong><code>..xs</code> 出现在调用处是反向操作：展开。</strong> <code>rect-of(..size, ..style)</code> 把数组 <code>(2cm, 1cm)</code> 展开成两个位置参数，把字典 <code>(fill: aqua)</code> 展开成命名参数，等价于 <code>rect-of(2cm, 1cm, fill: aqua)</code>。<code>calc.max(..(3, 9, 4))</code> 同理，<code>calc.max</code> 本身也是接受任意个位置参数的函数。</p><p>两件事合起来就是转发：<code>loud(..args)</code> 不关心调用者传了什么，原样交给 <code>text</code>，再额外塞一个 <code>weight: "bold"</code>。写模板的时候，这是给内置函数包一层默认样式最省事的形式，<code>xuan</code> 后面的 <code>note</code>、<code>slide</code> 都会用到。</p></section>
<section><h2>闭包：捕获的是当时的值<a href="#闭包捕获的是当时的值"><span>#</span></a></h2><p><code>make-adder(5)(1)</code> 得到 6，说明内层的 <code>x =&gt; x + n</code> 记住了 <code>n</code>。问题是记住的是什么：值，还是变量本身？我写了一个小实验（放在 <code>/tmp</code> 里，没有进仓库）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>1</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>show-n</span><span><span> </span><span>= </span><span>() </span></span><span>=&gt;</span><span> </span><span>n</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>2</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>fs</span><span><span> </span><span>= </span></span><span>range</span><span>(</span><span>3</span><span>).</span><span>map</span><span>(</span><span>i</span><span> </span><span>=&gt;</span><span> () </span><span>=&gt;</span><span> </span><span>i</span><span><span> </span><span>*</span><span> </span></span><span>10</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>total</span><span><span> </span><span>= </span><span>(</span></span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>).</span><span>fold</span><span>(</span><span>0</span><span>, (</span><span>acc</span><span>, </span><span>x</span><span>) </span><span>=&gt;</span><span> </span><span>acc</span><span><span> </span><span>+</span><span> </span></span><span>x</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>counter-values</span><span>(</span><span>xs</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>let</span><span> </span><span>count</span><span><span> </span><span>= </span></span><span>0</span></div></div><div><div><div>8</div></div><div><span>  </span><span>for</span><span> </span><span>x</span><span> </span><span>in</span><span> </span><span>xs</span><span> { </span><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>x</span><span> }</span></div></div><div><div><div>9</div></div><div><span>  </span><span>count</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在文档里用 <code>#repr(...)</code> 把这几个值逐个打印出来，结果依次是（表达式和结果的对应是我按顺序整理的）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>show-n()               1</span></div></div><div><div><div>2</div></div><div><span>n                      2</span></div></div><div><div><div>3</div></div><div><span>fs.map(f =&gt; f())       (0, 10, 20)</span></div></div><div><div><div>4</div></div><div><span>total                  6</span></div></div><div><div><div>5</div></div><div><span>counter-values((4, 5)) 9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>show-n</code> 是在 <code>n</code> 等于 1 的时候定义的，之后我又 <code>let n = 2</code> 遮蔽了它，<code>show-n()</code> 仍然返回 1。也就是说：<strong>闭包在创建的那一刻拿走了外部变量的值</strong>，之后外面怎么变，它都看不见。<code>range(3).map(i =&gt; () =&gt; i * 10)</code> 里每一轮的 <code>i</code> 各是各的，三个小函数分别捕获 0、1、2，没有“全都拿到最后一个值”这种坑。</p><p>这个模型像拍照：闭包带走的是创建时刻的一张照片，不是一个通往冰箱的钥匙。照片上的东西可以看，不能在照片上做改动让真冰箱里的东西变化。</p><p>后果在 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch03/capture-mutation.fail.typ" target="_blank"><code>lab/ch03/capture-mutation.fail.typ</code></a> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Variables from outside a function are read-only inside it.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>count</span><span><span> </span><span>= </span></span><span>0</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>bump</span><span><span>() </span><span>= </span><span>{</span></span></div></div><div><div><div>4</div></div><div><span>  </span><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>1</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>#bump</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: variables from outside the function are read-only and cannot be modified</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch03/capture-mutation.fail.typ:4:2</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>4 │   count += 1</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│   ^^^^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>while calling `bump` at lab/ch03/capture-mutation.fail.typ:6:1</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>bump()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 <code>make ch03</code> 抓到的 <code>out/ch03/capture-mutation.stderr</code> 原文。<code>chapter.sh</code> 要求 <code>*.fail.typ</code> 必须编译失败，所以这个文件失败才算“ok”。</p><p>错误信息说得很准：外部变量<strong>只读</strong>。函数里面自己 <code>let</code> 出来的变量是可以改的，上面实验里的 <code>counter-values</code> 就在循环里 <code>count += x</code>，结果 9，没有任何问题。只有<strong>从外面捕获来的</strong>变量不能被改。如果想让函数“累加”，办法是让状态流经参数和返回值：<code>fold</code>，或者把新值作为返回值交回去。需要真正跨函数、跨页面的可变状态时，Typst 有 <code>state</code>，第 7 篇会讲，它的代价（二次求值、收敛）是这篇的纯函数模型换来的。</p><p>（说“纯函数”并不严格：<code>panic</code> 和 <code>assert</code> 是副作用，但它们只会终止编译，而不改变任何值。这也是我愿意放心在 <code>tests/</code> 里大量使用 <code>assert</code> 的原因。）</p></section>
<section><h2>content 树<a href="#content-树"><span>#</span></a></h2><p>回到开头那行 <code>sequence(...)</code>。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch03/content-tree.typ" target="_blank"><code>lab/ch03/content-tree.typ</code></a>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: </span><span>plain-text</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// Markup is sugar for element function calls.</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span><span>[*粗体* 与 _斜体_]</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>c</span><span>), </span><span>content</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>repr</span><span>(</span><span>c</span><span>.</span><span>func</span><span>()), </span><span>"sequence"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>(</span><span>c</span><span>.</span><span>has</span><span>(</span><span>"children"</span><span>))</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>repr</span><span>(</span><span>strong</span><span>[a].</span><span>func</span><span>()), </span><span>"strong"</span><span>)</span></div></div><div><div><div>11</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>strong</span><span>[a].</span><span>body</span><span>, [a])</span></div></div><div><div><div>12</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>heading</span><span>(</span><span>level</span><span>: </span><span>2</span><span>)[x].</span><span>level</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>// `fields()` lists every field, `at` reads one, `has` tests for it.</span></div></div><div><div><div>15</div></div><div><span>#let</span><span> </span><span>h</span><span><span> </span><span>= </span></span><span>heading</span><span>(</span><span>level</span><span>: </span><span>2</span><span>, [标题])</span></div></div><div><div><div>16</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>h</span><span>.</span><span>fields</span><span>().</span><span>level</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>17</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>h</span><span>.</span><span>at</span><span>(</span><span>"level"</span><span>), </span><span>2</span><span>)</span></div></div><div><div><div>18</div></div><div><span>#assert</span><span>(</span><span>h</span><span>.</span><span>has</span><span>(</span><span>"body"</span><span>))</span></div></div><div><div><div>19</div></div><div><span>#assert</span><span>(</span><span>not</span><span> </span><span>h</span><span>.</span><span>has</span><span>(</span><span>"nothing"</span><span>))</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>// What the compiler actually sees:</span></div></div><div><div><div>22</div></div><div><span>#raw</span><span>(</span><span>repr</span><span>([*a* b]), </span><span>block</span><span>: </span><span>true</span><span>, </span><span>lang</span><span>: </span><span>"text"</span><span>)</span></div></div><div><div><div>23</div></div><div><span>#raw</span><span>(</span><span>repr</span><span>(</span><span>c</span><span>.</span><span>children</span><span>.</span><span>map</span><span>(</span><span>x</span><span> </span><span>=&gt;</span><span> </span><span>x</span><span>.</span><span>func</span><span>())), </span><span>block</span><span>: </span><span>true</span><span>, </span><span>lang</span><span>: </span><span>"text"</span><span>)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>// Flatten a tree back to a string.</span></div></div><div><div><div>26</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>([*粗体* 与 _斜体_]), </span><span>"粗体 与 斜体"</span><span>)</span></div></div><div><div><div>27</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>[= 标题 *加粗*], </span><span>"标题 加粗"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译出的页面上这两个 <code>raw</code> 块的内容是（<code>pdftotext</code> 读出）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sequence(strong(body: [a]), [ ], [b])</span></div></div><div><div><div>2</div></div><div><span>(strong, space, text, space, emph)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二行来自 <code>c.children.map(x =&gt; x.func())</code>：<code>[*粗体* 与 _斜体_]</code> 有五个子节点，依次是 <code>strong</code>、一个空格（<code>space</code>）、文本“与”（<code>text</code>）、又一个空格、<code>emph</code>。</p><p><strong>Markup 是语法糖，每个构造对应一个元素函数。</strong> <code>*粗体*</code> 就是 <code>strong[粗体]</code>，<code>= 标题</code> 是 <code>heading(level: 1)[标题]</code>，空行分段是 <code>parbreak()</code>，连一个空格都是 <code>space</code> 元素。<code>content</code> 是这一切的类型：一棵由元素构成的树，字符串变成文本元素 <code>text</code>，多个元素并排就套一层 <code>sequence</code>。</p><p>这棵树可以被读出来，有四个入口：</p><ul>
<li><code>c.func()</code> 返回元素函数本身，<code>[*a*].func() == strong</code> 为真（我编译验证过）。上面我用 <code>repr(c.func())</code> 取名字，是因为 <code>sequence</code> 和 <code>space</code> 这类内部元素没有公开的名字可以引用。</li>
<li><code>c.has("body")</code> 问有没有这个字段，<code>c.at("level")</code> 或 <code>c.level</code> 读字段，<code>c.fields()</code> 一次拿到全部字段的字典。</li>
<li>字段是什么由元素决定。<code>strong</code> 有 <code>body</code>，<code>heading</code> 有 <code>level</code> 和 <code>body</code>，<code>text</code> 有 <code>text</code>（一个 <code>str</code>），<code>sequence</code> 有 <code>children</code>（一个数组），这些都是上面断言的真实结果。</li>
<li>反过来，所有元素函数都能用关键字参数构造，<code>heading(level: 2, [标题])</code> 与 <code>== 标题</code> 是同一棵树。</li>
</ul><p>后面的章节会不断回到这里：show 规则（第 4 篇）匹配的就是这些元素，<code>query</code>（第 7 篇）返回的就是这些元素，<code>figure</code> 的 <code>kind</code>、<code>ref</code> 的 <code>supplement</code> 也都是元素字段。</p></section>
<section><h2>第一个模块：<code>util.typ</code><a href="#第一个模块utiltyp"><span>#</span></a></h2><p>树能读，就能写一个遍历它的函数。<code>xuan</code> 里的第一个模块 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/packages/local/xuan/0.1.0/src/util.typ" target="_blank"><code>src/util.typ</code></a> 就是这样来的，它是 final 版，全文如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Small helpers shared by every other module.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>/// Flatten content to a plain string.</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>plain-text</span><span>(</span><span>it</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>5</div></div><div><span>  </span><span>if</span><span> </span><span>type</span><span>(</span><span>it</span><span><span>) </span><span>==</span><span> </span></span><span>str</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>it</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>type</span><span>(</span><span>it</span><span><span>) </span><span>==</span><span> </span></span><span>content</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span> </span><span>it</span><span>.</span><span>has</span><span>(</span><span>"text"</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>      </span><span>it</span><span>.</span><span>text</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>repr</span><span>(</span><span>it</span><span>.</span><span>func</span><span><span>()) </span><span>==</span><span> </span></span><span>"space"</span><span> {</span></div></div><div><div><div>11</div></div><div><span>      </span><span>" "</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>it</span><span>.</span><span>has</span><span>(</span><span>"children"</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>      </span><span>let</span><span> </span><span>parts</span><span><span> </span><span>= </span></span><span>it</span><span>.</span><span>children</span><span>.</span><span>map</span><span>(</span><span>plain-text</span><span>)</span></div></div><div><div><div>14</div></div><div><span>      </span><span>if</span><span> </span><span>parts</span><span>.</span><span>len</span><span><span>() </span><span>==</span><span> </span></span><span>0</span><span> { </span><span>""</span><span> } </span><span>else</span><span> { </span><span>parts</span><span>.</span><span>join</span><span>() }</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>it</span><span>.</span><span>has</span><span>(</span><span>"child"</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>      </span><span>plain-text</span><span>(</span><span>it</span><span>.</span><span>child</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>it</span><span>.</span><span>has</span><span>(</span><span>"body"</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>      </span><span>plain-text</span><span>(</span><span>it</span><span>.</span><span>body</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>20</div></div><div><span>      </span><span>""</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>""</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>/// Wrap a single value in an array; arrays pass through.</span></div></div><div><div><div>28</div></div><div><span>#let</span><span> </span><span>ensure-array</span><span>(</span><span>x</span><span><span>) </span><span>= </span></span><span>if</span><span> </span><span>type</span><span>(</span><span>x</span><span><span>) </span><span>==</span><span> </span></span><span>array</span><span> { </span><span>x</span><span> } </span><span>else</span><span> { (</span><span>x</span><span>,) }</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>/// `default` when `value` is `auto` or `none`.</span></div></div><div><div><div>31</div></div><div><span>#let</span><span> </span><span>pick</span><span>(</span><span>value</span><span>, </span><span>default</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>32</div></div><div><span>  </span><span>if</span><span> </span><span>value</span><span><span> </span><span>==</span><span> </span></span><span>auto</span><span> </span><span>or</span><span> </span><span>value</span><span><span> </span><span>==</span><span> </span></span><span>none</span><span> { </span><span>default</span><span> } </span><span>else</span><span> { </span><span>value</span><span> }</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>plain-text</code> 是个递归函数，分支顺序就是 content 的节点种类：</p><ol>
<li>已经是 <code>str</code>，原样返回。</li>
<li>有 <code>text</code> 字段（文本元素），返回它。</li>
<li>是 <code>space</code>，返回一个空格。</li>
<li>有 <code>children</code>（<code>sequence</code>），每个孩子递归，再 <code>join</code>。空序列的 <code>join</code> 会得到 <code>none</code>，所以单独判断长度并返回 <code>""</code>，保证结果永远是字符串。</li>
<li>有 <code>child</code>，递归它。</li>
<li>有 <code>body</code>（<code>strong</code>、<code>emph</code>、<code>heading</code>、<code>box</code> 等），递归它。</li>
<li>其他（图片、<code>h(1em)</code> 等）返回 <code>""</code>，不是 content（比如 <code>none</code>）也返回 <code>""</code>。公式要分情况：<code>$x + y = 1$</code> 这种扁平的公式在树上就是文本，结果是 <code>"x + y = 1"</code>，<code>$arrow.r$</code> 得到 <code>"→"</code>；带结构的部分（求和、上下标、分式）会被丢掉，<code>$sum_(i=1)^n i = a/b$</code> 只剩 <code>" i = "</code>。这是我编译出来的结果，所以 <code>plain-text</code> 不适合拿来提取公式文字。</li>
</ol><p>另外两个小函数：<code>ensure-array</code> 让调用方可以写 <code>author: "张三"</code> 也可以写 <code>author: ("张三", "李四")</code>，<code>pick</code> 把 <code>auto</code> 和 <code>none</code> 都当作“没给”，给标题之类的参数提供默认值。以后 <code>note.typ</code> 会 <code>#import "util.typ": ensure-array</code>，<code>glossary.typ</code> 会用 <code>pick</code>。</p><section><h3>为什么要处理 <code>child</code><a href="#为什么要处理-child"><span>#</span></a></h3><p>第 5 个分支最不起眼，却不能省。看下面这个例子（放在 <code>/tmp</code>，<code>naive</code> 是和 <code>util.typ</code> 同一套逻辑，只是少了 <code>child</code> 分支）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span><span>[前 </span></span><span>#text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)[红字 _斜_] 后]</span></div></div><div><div><div>2</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>c</span><span>), </span><span>"前 红字 斜 后"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>naive</span><span>(</span><span>c</span><span>), </span><span>"前  后"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两条断言都通过，屏幕上的结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tree : sequence(</span></div></div><div><div><div>2</div></div><div><span>[前],</span></div></div><div><div><div>3</div></div><div><span>[ ],</span></div></div><div><div><div>4</div></div><div><span>styled(child: sequence([红字], [ ], emph(body: [斜])), ..),</span></div></div><div><div><div>5</div></div><div><span>[ ],</span></div></div><div><div><div>6</div></div><div><span>[后],</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div><div><div><div>8</div></div><div><span>xuan : "前 红字 斜 后"</span></div></div><div><div><div>9</div></div><div><span>naive: "前  后"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>text(fill: red)[...]</code> 不是一个有 <code>body</code> 的元素，在 0.15.1 里它在树上是 <code>styled(child: ..., ..)</code>：一个 <code>styled</code> 节点，字段只有 <code>child</code> 和 <code>styles</code>（我对 <code>text(fill: red)[x]</code> 打印过 <code>fields().keys()</code>，结果就是 <code>("child", "styles")</code>），<code>styles</code> 里存着那条红色的规则，<code>child</code> 才是被包裹的内容。<code>#[#set text(...) ...]</code> 这种带 <code>set</code> 的内容块在树上同样是 <code>styled</code>。没有 <code>child</code> 分支的版本走到这里就掉进“其他”分支，红字整个丢了，得到 <code>"前  后"</code>（两个空格，因为前后各有一个 <code>space</code>）。</p><p>所以只要被 <code>text(...)</code> 包过、或者带 <code>set</code> 的片段混进来，不处理 <code>child</code> 就会静默丢字，不报错。</p></section><section><h3>测试<a href="#测试"><span>#</span></a></h3><p><a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/tests/util.typ" target="_blank"><code>tests/util.typ</code></a> 就是那些断言的家：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: *</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>([*粗* 与 _斜_]), </span><span>"粗 与 斜"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>([a </span><span>#h</span><span>(</span><span>1em</span><span>) b]), </span><span>"a  b"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>"already a string"</span><span>), </span><span>"already a string"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>none</span><span>), </span><span>""</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>ensure-array</span><span>(</span><span>1</span><span>), (</span><span>1</span><span>,))</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>ensure-array</span><span>((</span><span>1</span><span>, </span><span>2</span><span>)), (</span><span>1</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>pick</span><span>(</span><span>auto</span><span>, </span><span>3</span><span>), </span><span>3</span><span>)</span></div></div><div><div><div>12</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>pick</span><span>(</span><span>none</span><span>, </span><span>3</span><span>), </span><span>3</span><span>)</span></div></div><div><div><div>13</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>pick</span><span>(</span><span>0</span><span>, </span><span>3</span><span>), </span><span>0</span><span>)</span></div></div><div><div><div>14</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>pick</span><span>(</span><span>false</span><span>, </span><span>3</span><span>), </span><span>false</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>// `text(..., body)` and `box` wrap their content in other elements.</span></div></div><div><div><div>17</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)[红 *色*]), </span><span>"红 色"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>plain-text</span><span>(</span><span>box</span><span>[a</span><span>#super</span><span>[2]]), </span><span>"a2"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几处刻意选的样例：<code>a #h(1em) b</code> 的结果是两个空格，因为 <code>h(1em)</code> 落进“其他”分支被当成空串，两侧的空格保留；<code>pick(0, 3)</code> 和 <code>pick(false, 3)</code> 保证 <code>0</code> 与 <code>false</code> 不被当作“没给”（只有 <code>auto</code> 和 <code>none</code> 才是）；最后两行是上一节 <code>child</code> 的回归测试。</p><p>跑一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make test</span></div></div><div><div><div>2</div></div><div><span>typst compile tests/glossary.typ</span></div></div><div><div><div>3</div></div><div><span>typst compile tests/homework.typ</span></div></div><div><div><div>4</div></div><div><span>typst compile tests/slides.typ</span></div></div><div><div><div>5</div></div><div><span>typst compile tests/theorem.typ</span></div></div><div><div><div>6</div></div><div><span>typst compile tests/units.typ</span></div></div><div><div><div>7</div></div><div><span>typst compile tests/util.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有任何输出就是通过：<code>assert</code> 失败会让 <code>typst compile</code> 返回非零，<code>make</code> 随即停下。整套测试在我的机器上用时 0.4 秒。后面几个测试文件（<code>units</code>、<code>homework</code>、<code>glossary</code>……）会在它们各自的模块出现的那一篇再讲，它们现在都已经在仓库里，所以输出里也一并列出了。</p><p>为了确认断言真的会失败，我把期望值改错编译了一次（<code>/tmp</code> 里的临时文件，没有改仓库）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: equality assertion failed: value "a" was not equal to "b"</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ bad.typ:2:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #assert.eq(plain-text([a]), "b")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>合起来用一次<a href="#合起来用一次"><span>#</span></a></h2><p>函数、参数转发、content 树、<code>util.typ</code>，放在一起能做什么？我写了一个一次性的小文件（在 <code>/tmp</code>，不属于仓库）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#import</span><span> </span><span>"@local/xuan:0.1.0"</span><span>: </span><span>pick</span><span>, </span><span>plain-text</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>lead</span><span>(</span><span>who</span><span>: </span><span>auto</span><span>, </span><span>body</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>4</div></div><div><span>  </span><span>let</span><span> </span><span>name</span><span><span> </span><span>= </span></span><span>pick</span><span>(</span><span>who</span><span>, </span><span>"匿名"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>strong</span><span>[</span><span>#name</span><span><span>：] </span><span>+</span><span> </span></span><span>body</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>chars</span><span>(</span><span>body</span><span><span>) </span><span>= </span></span><span>plain-text</span><span>(</span><span>body</span><span>).</span><span>clusters</span><span>().</span><span>filter</span><span>(</span><span>c</span><span> </span><span>=&gt;</span><span> </span><span>c</span><span><span> </span><span>!=</span><span> </span></span><span>" "</span><span>).</span><span>len</span><span>()</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#let</span><span> </span><span>note-box</span><span>(..</span><span>args</span><span>, </span><span>body</span><span><span>) </span><span>= </span></span><span>block</span><span>(</span></div></div><div><div><div>11</div></div><div><span>  </span><span>stroke</span><span>: </span><span>0.5pt</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>inset</span><span>: </span><span>6pt</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>..</span><span>args</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>body</span><span>,</span></div></div><div><div><div>15</div></div><div><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>#let</span><span> </span><span>c</span><span><span> </span><span>= </span><span>[</span></span><span>#lead</span><span>(</span><span>who</span><span>: </span><span>"我"</span><span>)[函数也是 _值_。] </span><span>#lead</span><span>[没署名。]]</span></div></div><div><div><div>18</div></div><div><span>#c</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>#note-box</span><span>(</span><span>fill</span><span>: </span><span>luma</span><span>(</span><span>240</span><span>))[字符数：</span><span>#chars</span><span>(</span><span>c</span><span>)]</span></div></div><div><div><div>21</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>chars</span><span>(</span><span>c</span><span>), </span><span>15</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>页面上的两行文字是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>我：函数也是 值。 匿名：没署名。</span></div></div><div><div><div>2</div></div><div><span>字符数：15</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里每一项都对应前面讲过的一条：</p><ul>
<li><code>lead</code> 的 <code>who: auto</code> 配合 <code>pick</code> 实现“没给就用默认值”；<code>strong[...] + body</code> 是把两个 content 相加，结果仍是一个 content。我把 <code>lead(who: "我")[函数也是 _值_。]</code> 的 <code>repr</code> 打出来看过，加号把它们压平成一个 <code>sequence</code>：<code>strong(body: sequence([我], [：]))</code>、<code>[函数也是]</code>、<code>[ ]</code>、<code>emph(body: [值])</code>、<code>[。]</code> 依次排开，没有多嵌套一层。</li>
<li><code>chars</code> 先用 <code>plain-text</code> 把树压成字符串，再用 <code>.clusters()</code> 按字素簇切开、滤掉空格、数个数。第 2 篇说过 <code>"中文".len()</code> 是 6 个字节，所以这里必须用 <code>clusters</code>，数出来的 15 才是人眼看到的字数：“我：函数也是值。”8 个，“匿名：没署名。”7 个。</li>
<li><code>note-box(..args, body)</code> 把 <code>body</code> 之外的所有参数转发给 <code>block</code>，调用时只写 <code>fill: luma(240)</code>，<code>stroke</code> 和 <code>inset</code> 用它内置的默认值。</li>
</ul><p>函数的参数里既有普通值也有 content，二者在语法上没有任何区别，区别只在 <code>type()</code> 返回什么。这是 Typst 里“模板”其实就是函数的原因：<code>note</code>、<code>slide</code>、<code>homework</code> 都是这样一个吃 <code>..args</code> 和一个 <code>body</code> 的函数。</p></section>
<section><h2>接下来<a href="#接下来"><span>#</span></a></h2><p>现在函数和 content 都能写能读了，但这棵树目前只是被看着：我们还没有改变任何一个节点的样子。下一篇用 <code>set</code> 和 <code>show</code> 去改样式，同时第一次把 <code>xuan</code> 搭成一个真正的包，<code>typst.toml</code>、<code>lib.typ</code>、<code>base.typ</code> 和 <code>note.typ</code>（intermediate）都会在那里出现，到时 <code>lab/ch03/content-tree.typ</code> 里那行 <code>@local/xuan:0.1.0</code> 的导入才算完整落地。</p><p>如果想自己多玩一会儿：试着给 <code>plain-text</code> 加一个 <code>sequence</code> 之外的分支，比如把 <code>h(1em)</code> 也翻译成空格，再看看 <code>tests/util.typ</code> 里哪条断言会因此变红。（我猜是 <code>"a  b"</code> 那条，但这一条我没有试，留给你。）</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-02-code-mode/</id>
      <title type="text">代码模式：值、类型与控制流</title>
      <published>2026-10-19T01:00:00.000Z</published>
      <updated>2026-10-19T01:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-02-code-mode/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 2 篇：我用 Typst 0.15.1 的 typst eval 和 6 个实验文件把代码模式逐项撞了一遍，量出两个汉字的 len() 是 6 而 clusters 只有 2，撞出字符边界和类型错误，并亲手触发 assert、assert.eq 与 panic 看真实报错。</summary>
      <content type="html"><![CDATA[<p>先问 Typst 一个问题：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval '"中文".len()'</span></div></div><div><div><div>2</div></div><div><span>6</span></div></div><div><div><div>3</div></div><div><span>$ typst eval '"中文".clusters()'</span></div></div><div><div><div>4</div></div><div><span>["中","文"]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>两个汉字，长度是 6。这个 6 会咬人：只要你在 Typst 里切过中文字符串，就会撞上它。这一篇我们把 Typst 的“代码”半边从头走一遍：值和类型、字符串、绑定、控制流、数组和字典、断言。标记语法在<a href="/posts/typst-by-xuan-01-first-document/">上一篇</a>已经过了一遍，这里的所有东西你都可以在 <code>#</code> 后面写出来。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>lab/ch02/</code> 下的 6 个实验文件（4 个正常编译，2 个预期失败的 <code>*.fail.typ</code>），<code>make ch02</code> 通过。还没有任何 <code>xuan</code> 包文件，包从第 3 篇开始长。
涉及的 Typst 特性：三种模式与 <code>#</code> 嵌入规则、<code>none</code>/<code>auto</code>/<code>bool</code>/<code>int</code>/<code>float</code>/<code>decimal</code>/<code>str</code>、长度与比例类、<code>version</code>/<code>label</code>/<code>regex</code>/<code>symbol</code>、<code>let</code>、遮蔽、解构、<code>if</code>/<code>for</code>/<code>while</code>/<code>break</code>/<code>continue</code>、数组与字典方法、<code>assert</code>/<code>assert.eq</code>/<code>panic</code>/<code>repr</code>/<code>type</code>。
前置：<a href="/posts/typst-by-xuan-01-first-document/">第 1 篇</a>（能进 <code>nix develop</code>，会 <code>typst compile</code>）。</p></div></div>
<section><h2>环境与实验场<a href="#环境与实验场"><span>#</span></a></h2><p>和上一篇一样，在 <a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1" target="_blank">typst-lab</a> 里进 <code>nix develop</code>，版本是 <code>typst 0.15.1 (9dfd3a08)</code>。本章的文件在 <a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1/lab/ch02" target="_blank"><code>lab/ch02/</code></a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>lab/ch02/</span></div></div><div><div><div>2</div></div><div><span>├── types.typ</span></div></div><div><div><div>3</div></div><div><span>├── strings.typ</span></div></div><div><div><div>4</div></div><div><span>├── collections.typ</span></div></div><div><div><div>5</div></div><div><span>├── control.typ</span></div></div><div><div><div>6</div></div><div><span>├── str-index.fail.typ</span></div></div><div><div><div>7</div></div><div><span>└── type-error.fail.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整章跑一遍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make ch02</span></div></div><div><div><div>2</div></div><div><span>./scripts/chapter.sh ch02</span></div></div><div><div><div>3</div></div><div><span>ok   lab/ch02/collections.typ</span></div></div><div><div><div>4</div></div><div><span>ok   lab/ch02/control.typ</span></div></div><div><div><div>5</div></div><div><span>ok   lab/ch02/str-index.fail.typ (failed as expected)</span></div></div><div><div><div>6</div></div><div><span>ok   lab/ch02/strings.typ</span></div></div><div><div><div>7</div></div><div><span>ok   lab/ch02/type-error.fail.typ (failed as expected)</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch02/types.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文件里大量使用 <code>assert</code>：只要有一条断言不成立，整个文件编译失败，<code>make ch02</code> 就红。所以“编译通过”本身就是证据，我在正文里只额外贴你看不到的东西：PDF 里的输出和报错。</p><p>另外一个小工具：<code>typst eval '表达式'</code> 直接对一段代码求值，不用建文件。输出默认是 JSON，所以长度、小数这类非 JSON 类型会被序列化成字符串（后面能看到 <code>"70.87pt"</code>）。它在第 16 篇还会出场，这里当计算器用。</p></section>
<section><h2>三种模式，一个 <code>#</code><a href="#三种模式一个-"><span>#</span></a></h2><p>Typst 的源文件有三种“模式”：</p><ul>
<li><strong>标记模式</strong>：文件的默认状态，写文章用的。</li>
<li><strong>代码模式</strong>：写表达式的地方，在标记里用 <code>#</code> 进入，或者在 <code>{ }</code> 里。</li>
<li><strong>数学模式</strong>：<code>$ ... $</code> 里面，第 8 篇单讲。</li>
</ul><p>进入代码模式的规则只有一条：<code>#</code> 后面跟一个表达式，Typst 取“最长的合法表达式”，到不能再延伸为止。光读规则不好记，我写了一个临时文件 <code>/tmp/ch02/modes.typ</code>（不进仓库）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>3</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>name</span><span><span> </span><span>= </span></span><span>"Typst"</span></div></div><div><div><div>3</div></div><div><span>Markup: n 是 </span><span>#n</span><span>，</span><span>name</span><span> 是 </span><span>#name</span><span>.</span></div></div><div><div><div>4</div></div><div><span>Field: </span><span>#name</span><span>.</span><span>len</span><span>() 个字节，</span><span>#</span><span>(</span><span>n</span><span><span> </span><span>+</span><span> </span></span><span>1</span><span>) 与 </span><span>#n</span><span>+1 与 </span><span>#n</span><span>;+1。</span></div></div><div><div><div>5</div></div><div><span>Math: </span><span>$</span><span>n^2 = </span><span>#</span><span>(</span><span>n</span><span><span> </span><span>*</span><span> </span></span><span>n</span><span>)</span><span>$</span><span> 与 </span><span>$</span><span>#name</span><span>$</span><span> 与 </span><span>$</span><span>"text"</span><span>$</span><span>。</span></div></div><div><div><div>6</div></div><div><span>Content in code: </span><span>#</span><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>let</span><span> </span><span>b</span><span><span> </span><span>= </span><span>[*粗* 与 _斜_]</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>b</span><span><span> </span><span>+</span><span> [，拼接]</span></span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div><span>Content arg: </span><span>#strong</span><span>[参数] </span><span>#text</span><span>(</span><span>fill</span><span>: </span><span>red</span><span>)[红色] </span><span>#emph</span><span>(</span><span>name</span><span>)</span></div></div><div><div><div>11</div></div><div><span>Strings in content: </span><span>#"*不是粗体*"</span><span> 与 *粗体*。</span></div></div><div><div><div>12</div></div><div><span>Escape: </span><span>\#</span><span>n 与 </span><span>\$</span><span>。</span></div></div><div><div><div>13</div></div><div><span>#let</span><span> </span><span>joined</span><span><span> </span><span>= </span><span>{</span></span></div></div><div><div><div>14</div></div><div><span>  </span><span>"a"</span></div></div><div><div><div>15</div></div><div><span>  </span><span>"b"</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div><span>#joined</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . modes.typ modes.pdf &amp;&amp; pdftotext modes.pdf -</span></div></div><div><div><div>2</div></div><div><span>Markup: n 是 3，name 是 Typst. Field: 5 个字节，4 与 3+1 与 3+1。 Math: 𝑛2 = 9 与 Typst 与</span></div></div><div><div><div>3</div></div><div><span>text。 Content in code: 粗 与 斜，拼接 Content arg: 参数 红色 Typst Strings in content: *不是粗体</span></div></div><div><div><div>4</div></div><div><span>* 与 粗体。 Escape: #n 与 $。 ab</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>pdftotext</code> 把换行压平了，但每一项都能对上。）逐条看：</p><ul>
<li><code>n 是 #n</code>：没有 <code>#</code> 的 <code>n</code> 只是字母，<code>#n</code> 才是变量 <code>n</code> 的值 3。</li>
<li><code>#name.</code> 行末的句号没有被吃掉：<code>.</code> 后面必须紧跟标识符才算字段访问。<code>#name.len()</code> 则是方法调用，得到 5（<code>"Typst"</code> 五个字节）。</li>
<li><code>#(n + 1)</code> 是 4。<code>#n+1</code> 输出 <code>3+1</code>：<code>#n</code> 是表达式，<code>+1</code> 回到标记里当文字。想让 <code>+</code> 属于表达式必须加括号；<code>#n;+1</code> 里的 <code>;</code> 只是显式结束表达式，自己不输出。</li>
<li><code>$#name$</code> 在数学模式里再次用 <code>#</code> 回到代码模式，所以 <code>Typst</code> 是直立体字符串，而 <code>n^2</code> 的 <code>n</code> 是数学斜体 𝑛。</li>
<li><code>{ ... }</code> 是<strong>代码块</strong>：里面每一行都是语句，没有 <code>#</code>；<code>[ ... ]</code> 是<strong>内容块</strong>，里面回到标记。两者可以任意嵌套。</li>
<li>代码里的字符串 <code>"*不是粗体*"</code> 是字符串，星号原样输出，不会被当成加粗。</li>
<li><code>\#n</code> 转义，输出 <code>#n</code>。</li>
</ul><p>标记里的 <code>#</code> 加 <code>(</code>、<code>[</code>、<code>{</code> 的组合有个固定读法：函数调用后面可以跟一个或多个 <code>[...]</code> 当最后的内容参数，比如 <code>#strong[参数]</code> 等价于 <code>#strong([参数])</code>。第 3 篇讲函数时会回到这里。</p></section>
<section><h2>值与类型<a href="#值与类型"><span>#</span></a></h2><p>Typst 里每个值都有类型，<code>type(x)</code> 返回它，<code>repr(x)</code> 返回它的“源码样子”。我把一串典型值塞进数组，两列表格打出来（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch02/types.typ" target="_blank"><code>types.typ</code></a>，省略开头的 <code>#set page(...)</code> 一行，下同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Every built-in value has a type; `type` returns it, `repr` shows the source.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>samples</span><span><span> </span><span>= </span><span>(</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>none</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>auto</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>true</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>42</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>3.14</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>decimal</span><span>(</span><span>"0.1"</span><span>),</span></div></div><div><div><div>9</div></div><div><span>  </span><span>"字"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>2.5cm</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>50%</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>1em</span><span><span> </span><span>+</span><span> </span></span><span>3pt</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>1fr</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>90deg</span><span>,</span></div></div><div><div><div>15</div></div><div><span>  </span><span>version</span><span>(</span><span>0</span><span>, </span><span>15</span><span>, </span><span>1</span><span>),</span></div></div><div><div><div>16</div></div><div><span>  </span><span>&lt;label&gt;</span><span>,</span></div></div><div><div><div>17</div></div><div><span>  </span><span>regex</span><span>(</span><span>"a+"</span><span>),</span></div></div><div><div><div>18</div></div><div><span>  </span><span>sym</span><span>.</span><span>arrow</span><span>.</span><span>r</span><span>,</span></div></div><div><div><div>19</div></div><div><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>#table</span><span>(</span></div></div><div><div><div>22</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>[*repr*], [*type*],</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>..</span><span>samples</span><span>.</span><span>map</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> (</span><span>raw</span><span>(</span><span>repr</span><span>(</span><span>v</span><span>)), </span><span>raw</span><span>(</span><span>repr</span><span>(</span><span>type</span><span>(</span><span>v</span><span>))))).</span><span>flatten</span><span>(),</span></div></div><div><div><div>25</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（<code>..</code> 展开和 <code>.map</code>、<code>.flatten</code> 稍后细讲，这里先看结果。）<code>pdftotext -layout out/ch02/types.pdf -</code> 的前 16 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>repr                               type</span></div></div><div><div><div>2</div></div><div><span>none                               type(none)</span></div></div><div><div><div>3</div></div><div><span>auto                               type(auto)</span></div></div><div><div><div>4</div></div><div><span>true                               bool</span></div></div><div><div><div>5</div></div><div><span>42                                 int</span></div></div><div><div><div>6</div></div><div><span>3.14                               float</span></div></div><div><div><div>7</div></div><div><span>decimal("0.1")                     decimal</span></div></div><div><div><div>8</div></div><div><span>"字"                                str</span></div></div><div><div><div>9</div></div><div><span>70.87pt                            length</span></div></div><div><div><div>10</div></div><div><span>50%                                ratio</span></div></div><div><div><div>11</div></div><div><span>3pt + 1em                          length</span></div></div><div><div><div>12</div></div><div><span>1fr                                fraction</span></div></div><div><div><div>13</div></div><div><span>90deg                              angle</span></div></div><div><div><div>14</div></div><div><span>version(0, 15, 1)                  version</span></div></div><div><div><div>15</div></div><div><span>&lt;label&gt;                            label</span></div></div><div><div><div>16</div></div><div><span>regex("a+")                        regex</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>有几处值得停一下。</p><p><strong><code>none</code> 和 <code>auto</code> 是两个不同的值。</strong> <code>none</code> 表示“没有”，<code>auto</code> 表示“你帮我定”。比如页面的 <code>margin: auto</code> 是“按纸张大小算”，<code>fill: none</code> 是“不填充”。它们各自有一个类型，打印出来是 <code>type(none)</code> 和 <code>type(auto)</code>，有点绕，但我实测确实如此。<code>none</code> 还有一个小脾气：它对 <code>+</code> 是单位元，<code>none + 1</code> 得 <code>1</code>。</p><p><strong><code>2.5cm</code> 的 <code>repr</code> 是 <code>70.87pt</code>。</strong> 长度在内部以 pt 存储，<code>cm</code> 只是字面量后缀。需要换算时用方法：<code>2.5cm.mm()</code> 得 <code>25.0</code>，<code>(1em + 3pt).em</code> 得 <code>1.0</code>。<code>1em + 3pt</code> 保留成两项的和（<code>3pt + 1em</code>），因为 <code>em</code> 要等字号确定才知道多长。</p><p><strong>长度、比例、相对、分数。</strong> <code>50%</code> 是 <code>ratio</code>，<code>50% + 10pt</code> 是 <code>relative</code>（百分比加绝对长度，布局时才解析），<code>1fr</code> 是 <code>fraction</code>（分剩余空间的份数，第 6 篇用）。它们按量纲相加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval '(2cm + 5mm, 50% + 10pt, 2fr + 1fr)'</span></div></div><div><div><div>2</div></div><div><span>["70.87pt","50% + 10pt","3fr"]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>整数是 64 位，0.15 新增了 <code>int.min</code> 和 <code>int.max</code>。</strong> 实验文件里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Floats are binary, decimals are not.</span></div></div><div><div><div>2</div></div><div><span>#assert</span><span>(</span><span>0.1</span><span><span> </span><span>+</span><span> </span></span><span>0.2</span><span><span> </span><span>!=</span><span> </span></span><span>0.3</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>decimal</span><span>(</span><span>"0.1"</span><span><span>) </span><span>+</span><span> </span></span><span>decimal</span><span>(</span><span>"0.2"</span><span>), </span><span>decimal</span><span>(</span><span>"0.3"</span><span>))</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// Integers are 64 bit; 0.15 added `int.min` and `int.max`.</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>int</span><span>.</span><span>max</span><span>, </span><span>9223372036854775807</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>int</span><span>.</span><span>min</span><span><span>, </span><span>-</span></span><span>9223372036854775808</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整数溢出不会回绕，也不会悄悄变成浮点，直接报错（临时文件 <code>overflow.typ</code>，内容 <code>#(int.max + 1)</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: value is too large</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ overflow.typ:1:2</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #(int.max + 1)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│   ^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>浮点就是 IEEE 754：<code>0.1 + 0.2</code> 是 <code>0.30000000000000004</code>，所以断言写 <code>!=</code>。要精确十进制（金额、报表），用 <code>decimal("0.1")</code>，字符串进、字符串出。<code>/</code> 永远返回浮点，<code>7 / 2</code> 是 <code>3.5</code>，<code>4 / 2</code> 是 <code>2.0</code>，类型是 <code>float</code>；整除用 <code>calc.quo(7, 2)</code> 得 <code>3</code>，取余 <code>calc.rem(7, 2)</code> 得 <code>1</code>。</p><p><strong>单位和版本。</strong> <code>90deg</code> 的 <code>angle</code> 用 <code>.rad()</code> 转弧度；<code>version(0, 15, 1) &gt; version(0, 14)</code> 逐段比较，<code>sys.version</code> 是当前编译器版本。这一组在 <code>types.typ</code> 末尾：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Units convert through methods.</span></div></div><div><div><div>2</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>2.5cm</span><span>.</span><span>mm</span><span>(), </span><span>25.0</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>1em</span><span><span> </span><span>+</span><span> </span></span><span>3pt</span><span>).</span><span>em</span><span>, </span><span>1.0</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>(</span><span>calc</span><span>.</span><span>abs</span><span>(</span><span>90deg</span><span>.</span><span>rad</span><span><span>() </span><span>-</span><span> </span></span><span>calc</span><span>.</span><span>pi</span><span><span> </span><span>/</span><span> </span></span><span>2</span><span><span>) </span><span>&lt;</span><span> </span></span><span>1e-12</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>// Versions compare component-wise.</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>(</span><span>version</span><span>(</span><span>0</span><span>, </span><span>15</span><span>, </span><span>1</span><span><span>) </span><span>&gt;</span><span> </span></span><span>version</span><span>(</span><span>0</span><span>, </span><span>14</span><span>))</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>(</span><span>sys</span><span>.</span><span>version</span><span><span> </span><span>&gt;=</span><span> </span></span><span>version</span><span>(</span><span>0</span><span>, </span><span>15</span><span>, </span><span>1</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong><code>label</code>、<code>regex</code>、<code>symbol</code>。</strong> <code>&lt;label&gt;</code> 是标签，第 1 篇用来给东西起名、用 <code>@</code> 引用；<code>regex("a+")</code> 是正则；<code>sym.arrow.r</code> 是符号。符号是个“带修饰符变体的字符”：<code>sym.arrow.r</code> 默认是 <code>→</code>，<code>sym.arrow.r.double</code> 是 <code>⇒</code>，<code>.long</code> 是 <code>⟶</code>。<code>repr</code> 会把整个符号连同全部变体打出来，这一格的 <code>repr</code> 有 54 行、1224 字节，所以表格最后一行特别长，我就让它留在输出里。要注意符号不是字符串：<code>sym.arrow.r == "→"</code> 是 <code>false</code>，<code>type</code> 是 <code>symbol</code>。</p><p>还有一个坑：<code>type</code> 的结果本身也是值，可以拿来比较。断言里写 <code>type(42) == int</code>，不是 <code>== "integer"</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>none</span><span>), </span><span>type</span><span>(</span><span>none</span><span>))</span></div></div><div><div><div>2</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>42</span><span>), </span><span>int</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>3.14</span><span>), </span><span>float</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>1em</span><span><span> </span><span>+</span><span> </span></span><span>3pt</span><span>), </span><span>length</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>type</span><span>(</span><span>50%</span><span><span> </span><span>+</span><span> </span></span><span>2pt</span><span>), </span><span>relative</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>字符串：字节、码点和簇<a href="#字符串字节码点和簇"><span>#</span></a></h2><p>回到开头那个 6。Typst 的 <code>str</code> 是 UTF-8，<code>len()</code> 数字节。一个汉字在 UTF-8 里占 3 字节，所以两个汉字是 6。这就像量一本书：<code>len()</code> 量书脊的厚度（毫米），<code>clusters()</code> 数它有几本，两个数都对，但你要哪个得想清楚。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch02/strings.typ" target="_blank"><code>strings.typ</code></a> 把三种“长度”并排放：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A str is UTF-8: len() counts bytes, not characters.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>s</span><span><span> </span><span>= </span></span><span>"中文"</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>s</span><span>.</span><span>len</span><span>(), </span><span>6</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>s</span><span>.</span><span>clusters</span><span>().</span><span>len</span><span>(), </span><span>2</span><span>)</span></div></div><div><div><div>5</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>s</span><span>.</span><span>codepoints</span><span>().</span><span>len</span><span>(), </span><span>2</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// A decomposed é is one grapheme cluster but two codepoints and three bytes.</span></div></div><div><div><div>8</div></div><div><span>#let</span><span> </span><span>e</span><span><span> </span><span>= </span></span><span>"e</span><span>\u{0301}</span><span>"</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>e</span><span>.</span><span>len</span><span>(), </span><span>3</span><span>)</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>e</span><span>.</span><span>codepoints</span><span>().</span><span>len</span><span>(), </span><span>2</span><span>)</span></div></div><div><div><div>11</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>e</span><span>.</span><span>clusters</span><span>().</span><span>len</span><span>(), </span><span>1</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文件末尾把它们做成一张表：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#table</span><span>(</span></div></div><div><div><div>2</div></div><div><span>  </span><span>columns</span><span>: </span><span>4</span><span>,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>[*str*], [*bytes*], [*codepoints*], [*clusters*],</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>..(</span><span>s</span><span>, </span><span>e</span><span>, </span><span>"abc"</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>.</span><span>map</span><span>(</span><span>x</span><span> </span><span>=&gt;</span><span> (</span></div></div><div><div><div>6</div></div><div><span>      </span><span>raw</span><span>(</span><span>repr</span><span>(</span><span>x</span><span>)),</span></div></div><div><div><div>7</div></div><div><span>      </span><span>str</span><span>(</span><span>x</span><span>.</span><span>len</span><span>()),</span></div></div><div><div><div>8</div></div><div><span>      </span><span>str</span><span>(</span><span>x</span><span>.</span><span>codepoints</span><span>().</span><span>len</span><span>()),</span></div></div><div><div><div>9</div></div><div><span>      </span><span>str</span><span>(</span><span>x</span><span>.</span><span>clusters</span><span>().</span><span>len</span><span>()),</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>.</span><span>flatten</span><span>(),</span></div></div><div><div><div>12</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>str          bytes codepoints clusters</span></div></div><div><div><div>2</div></div><div><span>"中文"         6     2           2</span></div></div><div><div><div>3</div></div><div><span>"e\u{301}"   3     2           1</span></div></div><div><div><div>4</div></div><div><span>"abc"        3     3           3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三列的意义：</p><ul>
<li><code>len()</code>：字节数，索引和切片用的单位。</li>
<li><code>codepoints()</code>：Unicode 码点，<code>"中".to-unicode()</code> 是 <code>20013</code>，反过来 <code>str.from-unicode(20013)</code> 是 <code>"中"</code>。</li>
<li><code>clusters()</code>：字素簇，也就是人眼数到的“一个字”。<code>é</code> 如果写成 <code>e</code> 加组合重音符 <code>\u{0301}</code>，是一个簇、两个码点、三个字节。</li>
</ul><p><code>\u{...}</code> 是字符串里的码点转义，上一篇在标记里也见过。想在 Typst 里按“字”数数或裁剪标题，用 <code>clusters()</code>，不要用 <code>slice</code>。</p><p><strong><code>slice</code> 和 <code>at</code> 用字节偏移，落在字符中间就报错。</strong> 合法的切法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Slicing takes byte offsets and must land on a character boundary.</span></div></div><div><div><div>2</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>s</span><span>.</span><span>slice</span><span>(</span><span>0</span><span>, </span><span>3</span><span>), </span><span>"中"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>s</span><span>.</span><span>at</span><span>(</span><span>3</span><span>), </span><span>"文"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>故意切在“中”的中间（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch02/str-index.fail.typ" target="_blank"><code>str-index.fail.typ</code></a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Byte offset 1 is inside the three-byte character 中.</span></div></div><div><div><div>2</div></div><div><span>#"中文"</span><span>.</span><span>slice</span><span>(</span><span>1</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>out/ch02/str-index.stderr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: string index 1 is not a character boundary</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch02/str-index.fail.typ:2:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #"中文".slice(1)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>报错里没有“推荐偏移”，只告诉你 1 不是边界。可以这样自查：<code>.len()</code> 倍数不对劲时，把字符串转 <code>clusters()</code> 或 <code>codepoints()</code> 数组再切，数组按元素下标，不会落到字符中间。</p><p>字符串的其他常用方法也都在同一个文件里，每条都带断言：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Conversions and searching.</span></div></div><div><div><div>2</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>"中"</span><span>.</span><span>to-unicode</span><span>(), </span><span>20013</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>str</span><span>.</span><span>from-unicode</span><span>(</span><span>20013</span><span>), </span><span>"中"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>"a,b,,c"</span><span>.</span><span>split</span><span>(</span><span>","</span><span>), (</span><span>"a"</span><span>, </span><span>"b"</span><span>, </span><span>""</span><span>, </span><span>"c"</span><span>))</span></div></div><div><div><div>5</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>"  trim me "</span><span>.</span><span>trim</span><span>(), </span><span>"trim me"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>"Typst"</span><span>.</span><span>replace</span><span>(</span><span>regex</span><span>(</span><span>"[aeiou]"</span><span>), </span><span>"_"</span><span>), </span><span>"Typst"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>"2026-10-19"</span><span>.</span><span>find</span><span>(</span><span>regex</span><span>(</span><span>"</span><span>\d</span><span>+"</span><span>)), </span><span>"2026"</span><span>)</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>"ab"</span><span><span> </span><span>*</span><span> </span></span><span>3</span><span>, </span><span>"ababab"</span><span>)</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>upper</span><span>(</span><span>"typst"</span><span>), </span><span>"TYPST"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意 <code>"Typst".replace(regex("[aeiou]"), "_")</code> 的结果仍是 <code>"Typst"</code>：没有元音，我一开始还以为我写错了正则。另外 <code>upper</code> 是函数而不是字符串方法：<code>"ab".upper()</code> 会报 <code>type string has no method `upper` </code>，要写 <code>upper("ab")</code>。</p></section>
<section><h2>类型之间不隐式转换<a href="#类型之间不隐式转换"><span>#</span></a></h2><p>字符串和整数相加，别的语言会替你拼接或者转换。Typst 不会（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch02/type-error.fail.typ" target="_blank"><code>type-error.fail.typ</code></a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Typst never coerces between types.</span></div></div><div><div><div>2</div></div><div><span>#</span><span>(</span><span>"1"</span><span><span> </span><span>+</span><span> </span></span><span>1</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>out/ch02/type-error.stderr</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: cannot add string and integer</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch02/type-error.fail.typ:2:2</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>2 │ #("1" + 1)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│   ^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>要转就显式写：<code>str(42) + "!"</code> 得 <code>"42!"</code>，<code>int("42") + 1</code> 得 <code>43</code>，<code>float("1.5")</code> 得 <code>1.5</code>。<code>str(3.0)</code> 得 <code>"3"</code>，不是 <code>"3.0"</code>：整数值的浮点转字符串时小数点会丢，要保留的话得自己格式化（我实测过，没有深究原因）。我猜这种严格是刻意的：排版里的数据多来自文件，类型不对应该尽早报错。</p><section><h3>字符串不是内容<a href="#字符串不是内容"><span>#</span></a></h3><p>Typst 里 <code>"a"</code> 和 <code>[a]</code> 是两种值：前者是 <code>str</code>，后者是 <code>content</code>，上一篇所有的标记最终都是 <code>content</code>。两者之间有一条单向的路，字符串可以“升级”成内容，内容不能“降级”回字符串：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval 'type([a] + [b])'</span></div></div><div><div><div>2</div></div><div><span>"content"</span></div></div><div><div><div>3</div></div><div><span>$ typst eval '"a" + [b]'</span></div></div><div><div><div>4</div></div><div><span>{"func":"sequence","children":[{"func":"text","text":"a"},{"func":"text","text":"b"}]}</span></div></div><div><div><div>5</div></div><div><span>$ typst eval 'str([a])'</span></div></div><div><div><div>6</div></div><div><span>error: expected integer, float, decimal, version, bytes, label, type, or string, found content</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>┌─ &lt;input-expression&gt;:1:4</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span>1 │ str([a])</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>│     ^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第 3 篇讲 content 的内部结构，并在 <code>src/util.typ</code> 里写一个 <code>plain-text</code> 来做这个“降级”。另外 <code>for</code> 遍历字符串是按簇走的：<code>for c in "中e\u{301}x"</code> 一共循环 3 次（这个字符串有 4 个码点、7 个字节，<code>typst eval</code> 数出来的是 3）。</p></section></section>
<section><h2>绑定、遮蔽与解构<a href="#绑定遮蔽与解构"><span>#</span></a></h2><p><code>let</code> 绑定一个名字。再次 <code>let</code> 同名是<strong>遮蔽</strong>，不是修改，之前捕获了旧值的函数不受影响（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch02/control.typ" target="_blank"><code>control.typ</code></a> 开头）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// `let` binds; a later `let` shadows, the old value is untouched.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>x</span><span><span> </span><span>= </span></span><span>1</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>f</span><span><span>() </span><span>= </span></span><span>x</span></div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>x</span><span><span> </span><span>= </span></span><span>2</span></div></div><div><div><div>5</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>f</span><span>(), </span><span>1</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>x</span><span>, </span><span>2</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>f</code> 定义时看见的 <code>x</code> 是 1，后来的 <code>let x = 2</code> 不会回头改它。这和 Typst “函数是纯的”是一回事，下一篇会专门讲，并给出修改外部变量的报错。</p><p>函数也用 <code>let</code> 定义：<code>#let f() = x</code> 是 <code>#let f = () =&gt; x</code> 的简写。</p><p><strong>解构。</strong> <code>let</code> 左边可以是数组或字典的模式，<code>..</code> 收集剩余部分（<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch02/collections.typ" target="_blank"><code>collections.typ</code></a> 末尾）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Destructuring in `let` and in `for`.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> (</span><span>first</span><span>, ..</span><span>rest</span><span><span>) </span><span>= </span></span><span>xs</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>first</span><span>, </span><span>3</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>rest</span><span>, (</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>1</span><span>))</span></div></div><div><div><div>5</div></div><div><span>#for</span><span> (</span><span>k</span><span>, </span><span>v</span><span>) </span><span>in</span><span> </span><span>d</span><span> [</span><span>#k</span><span>: </span><span>#repr</span><span>(</span><span>v</span><span>) </span><span>\ </span><span>]</span></div></div><div><div><div>6</div></div><div><span>#for</span><span> (</span><span>i</span><span>, </span><span>x</span><span>) </span><span>in</span><span> </span><span>xs</span><span>.</span><span>enumerate</span><span>() [</span><span>#i</span><span>=</span><span>#x</span><span> </span><span>#h</span><span>(</span><span>0.5em</span><span>)]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>name: "Typst"</span></div></div><div><div><div>2</div></div><div><span>year: 2023</span></div></div><div><div><div>3</div></div><div><span>stable: true</span></div></div><div><div><div>4</div></div><div><span>0=3 1=1 2=2 3=3 4=1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>for (k, v) in d</code> 遍历字典得到键值对；<code>for (i, x) in xs.enumerate()</code> 是数组带下标遍历。<code>[...]</code> 紧跟在 <code>for</code> 头后面是内容块作循环体，每轮产出一小段内容，所有轮的产出拼接起来，这就是下一节的“连接”。</p></section>
<section><h2>控制流和“块的值”<a href="#控制流和块的值"><span>#</span></a></h2><p><code>if</code> 是表达式，有值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// if / else is an expression.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>sign</span><span>(</span><span>n</span><span><span>) </span><span>= </span></span><span>if</span><span> </span><span>n</span><span><span> </span><span>&gt;</span><span> </span></span><span>0</span><span> { </span><span>1</span><span> } </span><span>else</span><span> </span><span>if</span><span> </span><span>n</span><span><span> </span><span>&lt;</span><span> </span></span><span>0</span><span><span> { </span><span>-</span></span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> }</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span><span>((</span><span>-</span></span><span>5</span><span>, </span><span>0</span><span>, </span><span>7</span><span>).</span><span>map</span><span>(</span><span>sign</span><span><span>), (</span><span>-</span></span><span>1</span><span>, </span><span>0</span><span>, </span><span>1</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>if</code>、<code>for</code>、<code>while</code> 都能用在 <code>#</code> 后面直接产出内容，也能作为表达式给变量赋值。关键规则是<strong>代码块的值是它里面所有语句值的“连接”</strong>（join）：字符串连字符串，内容连内容，数组连数组，没有值的语句（<code>let</code>、<code>none</code>）不贡献。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Code blocks join the values of their statements (strings with strings,</span></div></div><div><div><div>2</div></div><div><span>// content with content; mixing a string and an integer is an error).</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>joined</span><span><span> </span><span>= </span><span>{</span></span></div></div><div><div><div>4</div></div><div><span>  </span><span>"a"</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"b"</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>joined</span><span>, </span><span>"ab"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>类型不一致就是错，我拿临时文件 <code>joinerr.typ</code> 试过：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat joinerr.typ</span></div></div><div><div><div>2</div></div><div><span>#{</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>"a"</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>3</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>$ typst compile --root . joinerr.typ x.pdf</span></div></div><div><div><div>7</div></div><div><span>error: cannot join string with integer</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>┌─ joinerr.typ:3:2</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>10</div></div><div><span>3 │   3</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>│   ^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>循环也遵守这条规则，<code>for</code> 的值就是各轮值的连接。想要数组，每轮产出一个单元素数组 <code>(i,)</code>，连起来就是数组；<code>break</code> 和 <code>continue</code> 照常可用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Loops; `break` and `continue` work in both.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>collatz</span><span>(</span><span>n</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>let</span><span> </span><span>steps</span><span><span> </span><span>= </span></span><span>0</span></div></div><div><div><div>4</div></div><div><span>  </span><span>while</span><span> </span><span>n</span><span><span> </span><span>!=</span><span> </span></span><span>1</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>calc</span><span>.</span><span>even</span><span>(</span><span>n</span><span>) { </span><span>calc</span><span>.</span><span>quo</span><span>(</span><span>n</span><span>, </span><span>2</span><span>) } </span><span>else</span><span> { </span><span>3</span><span><span> </span><span>*</span><span> </span></span><span>n</span><span><span> </span><span>+</span><span> </span></span><span>1</span><span> }</span></div></div><div><div><div>6</div></div><div><span>    </span><span>steps</span><span><span> </span><span>+=</span><span> </span></span><span>1</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>  </span><span>steps</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>collatz</span><span>(</span><span>27</span><span>), </span><span>111</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#let</span><span> </span><span>odds</span><span><span> </span><span>= </span></span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>10</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>  </span><span>if</span><span> </span><span>i</span><span><span> </span><span>&gt;</span><span> </span></span><span>7</span><span> { </span><span>break</span><span> }</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> </span><span>calc</span><span>.</span><span>even</span><span>(</span><span>i</span><span>) { </span><span>continue</span><span> }</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>(</span><span>i</span><span>,)</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>odds</span><span>, (</span><span>1</span><span>, </span><span>3</span><span>, </span><span>5</span><span>, </span><span>7</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>while</code> 里的赋值 <code>n = ...</code> 和 <code>steps += 1</code> 能修改函数<strong>自己的</strong>局部变量（对外层变量不行，下一篇）。27 要 111 步，这是个有名的数字，上面的断言也验证了。</p><p>文件最后两行把值嵌回标记：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Embedding code in markup: `#` takes the longest valid expression.</span></div></div><div><div><div>2</div></div><div><span>Collatz(27) = </span><span>#collatz</span><span>(</span><span>27</span><span>) 步，奇数 </span><span>#odds</span><span>.</span><span>map</span><span>(</span><span>str</span><span>).</span><span>join</span><span>(</span><span>"、"</span><span>)。</span></div></div><div><div><div>3</div></div><div><span>#</span><span>(</span><span>"一"</span><span><span> </span><span>+</span><span> </span></span><span>"二"</span><span>)，</span><span>#</span><span>(</span><span>1</span><span><span> </span><span>+</span><span> </span></span><span>2</span><span>) 与 </span><span>#1</span><span>+2 不同：后者先输出 1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Collatz(27) = 111 步，奇数 1、3、5、7。 一二，3 与 1+2 不同：后者先</span></div></div><div><div><div>2</div></div><div><span>输出 1。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运算符一览，都是你预期的那些：<code>+ - * /</code>、比较 <code>== != &lt; &lt;= &gt; &gt;=</code>、逻辑 <code>and or not</code>、成员 <code>in</code> 和 <code>not in</code>（对数组、字符串、字典的键都有效）、赋值 <code>= += -= *= /=</code>。这一串我也跑了一遍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval '(1 + 2 * 3 - 4, 2 in (1, 2, 3), "ell" in "hello", "x" in (x: 1))'</span></div></div><div><div><div>2</div></div><div><span>[3,true,true,true]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>几个我专门核对过的细节，因为别的语言里它们不一样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval '(1 == 1.0, "a" &lt; "b", (1, 2) == (1, 2), (a: 1) == (a: 1), 7 - -2)'</span></div></div><div><div><div>2</div></div><div><span>[true,true,true,true,9]</span></div></div><div><div><div>3</div></div><div><span>$ typst eval '(calc.pow(2, 10), calc.round(3.14159, digits: 2), calc.clamp(15, 0, 10), calc.gcd(12, 18))'</span></div></div><div><div><div>4</div></div><div><span>[1024,3.14,10,6]</span></div></div><div><div><div>5</div></div><div><span>$ typst eval 'if none { 1 } else { 2 }'</span></div></div><div><div><div>6</div></div><div><span>error: expected boolean, found none</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>┌─ &lt;input-expression&gt;:1:3</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>9</div></div><div><span>1 │ if none { 1 } else { 2 }</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>│    ^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>1 == 1.0</code> 是 <code>true</code>：整数和浮点可以比较，也可以混着算，<code>1 + 1.0</code> 得 <code>2.0</code>，结果是 <code>float</code>。</li>
<li>数组和字典按内容比较，不是比较引用，所以 <code>assert.eq</code> 可以直接比整个集合。</li>
<li>条件必须是 <code>bool</code>。<code>if none</code> 报 <code>expected boolean, found none</code>，<code>if 0</code> 报 <code>expected boolean, found integer</code>，没有“零或空算假”这回事。想判断数组空不空，写 <code>xs.len() == 0</code>。</li>
<li>数学函数在 <code>calc</code> 模块里：<code>calc.pow</code>、<code>calc.round(x, digits: 2)</code>、<code>calc.clamp</code>、<code>calc.gcd</code>、<code>calc.quo</code>、<code>calc.rem</code>、<code>calc.even</code>。</li>
</ul></section>
<section><h2>数组与字典<a href="#数组与字典"><span>#</span></a></h2><p>数组 <code>(1, 2, 3)</code>，字典 <code>(name: "x", year: 1)</code>，空数组 <code>()</code>，空字典 <code>(:)</code>。单元素数组必须带逗号，<code>(1,)</code>，否则它只是加了括号的 <code>1</code>。字典保持插入顺序。</p><p>多数方法不改原值，返回新值；少数（<code>push</code>、<code>pop</code>、<code>insert</code>、<code>remove</code>）会就地修改变量，后面单独讲。<a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch02/collections.typ" target="_blank"><code>collections.typ</code></a> 的前半：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Arrays</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>xs</span><span><span> </span><span>= </span><span>(</span></span><span>3</span><span>, </span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>1</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>xs</span><span>.</span><span>map</span><span>(</span><span>x</span><span> </span><span>=&gt;</span><span> </span><span>x</span><span><span> </span><span>*</span><span> </span></span><span>x</span><span>), (</span><span>9</span><span>, </span><span>1</span><span>, </span><span>4</span><span>, </span><span>9</span><span>, </span><span>1</span><span>))</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>xs</span><span>.</span><span>filter</span><span>(</span><span>x</span><span> </span><span>=&gt;</span><span> </span><span>x</span><span><span> </span><span>&gt;</span><span> </span></span><span>1</span><span>), (</span><span>3</span><span>, </span><span>2</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>5</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>xs</span><span>.</span><span>fold</span><span>(</span><span>0</span><span>, (</span><span>sum</span><span>, </span><span>x</span><span>) </span><span>=&gt;</span><span> </span><span>sum</span><span><span> </span><span>+</span><span> </span></span><span>x</span><span>), </span><span>10</span><span>)</span></div></div><div><div><div>6</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>xs</span><span>.</span><span>dedup</span><span>(), (</span><span>3</span><span>, </span><span>1</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>xs</span><span>.</span><span>sorted</span><span>(), (</span><span>1</span><span>, </span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>xs</span><span>.</span><span>sorted</span><span>(</span><span>key</span><span>: </span><span>x</span><span> </span><span>=&gt;</span><span><span> </span><span>-</span></span><span>x</span><span>), (</span><span>3</span><span>, </span><span>3</span><span>, </span><span>2</span><span>, </span><span>1</span><span>, </span><span>1</span><span>))</span></div></div><div><div><div>9</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>"a"</span><span>, </span><span>"b"</span><span>).</span><span>enumerate</span><span>(), ((</span><span>0</span><span>, </span><span>"a"</span><span>), (</span><span>1</span><span>, </span><span>"b"</span><span>)))</span></div></div><div><div><div>10</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>"a"</span><span>, </span><span>"b"</span><span>).</span><span>zip</span><span>((</span><span>1</span><span>, </span><span>2</span><span>)), ((</span><span>"a"</span><span>, </span><span>1</span><span>), (</span><span>"b"</span><span>, </span><span>2</span><span>)))</span></div></div><div><div><div>11</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>"a"</span><span>, </span><span>"b"</span><span>, </span><span>"c"</span><span>).</span><span>join</span><span>(</span><span>", "</span><span>), </span><span>"a, b, c"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>range</span><span>(</span><span>1</span><span>, </span><span>5</span><span>, </span><span>inclusive</span><span>: </span><span>true</span><span>), (</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>13</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>xs</span><span>.</span><span>at</span><span>(</span><span>10</span><span>, </span><span>default</span><span>: </span><span>none</span><span>), </span><span>none</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>要点：</p><ul>
<li><code>map</code>、<code>filter</code>、<code>fold</code>：箭头 <code>x =&gt; ...</code> 是匿名函数，<code>fold</code> 的第一个参数是初值。</li>
<li><code>dedup</code> 保留首次出现的顺序，<code>sorted(key: ...)</code> 按键排序。</li>
<li><code>enumerate</code>、<code>zip</code> 返回数组的数组，配合解构很顺手。</li>
<li><code>join(", ")</code> 用分隔符连接；不带参数就直接连接。</li>
<li><code>range(1, 5)</code> 默认不含终点，得 <code>(1, 2, 3, 4)</code>；0.15 新增 <code>inclusive: true</code> 让它含终点。</li>
<li><code>at(i, default: ...)</code> 越界时返回默认值。不写 <code>default</code> 就是错：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval '(1, 2, 3).at(5)'</span></div></div><div><div><div>2</div></div><div><span>error: array index out of bounds (index: 5, len: 3) and no default value was specified</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ &lt;input-expression&gt;:1:0</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>1 │ (1, 2, 3).at(5)</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│ ^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其他常用的我也跑过：<code>first()</code>、<code>last()</code>、<code>slice(1)</code>、<code>rev()</code>、<code>sum()</code>、<code>contains(2)</code>、<code>chunks(2)</code>、<code>windows(2)</code>、<code>any</code>/<code>all</code>、<code>position</code>、<code>find</code>，行为和字面意思一致。</p><p>字典同理，后半段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Dictionaries keep insertion order.</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>d</span><span><span> </span><span>= </span><span>(</span></span><span>name</span><span>: </span><span>"Typst"</span><span>, </span><span>year</span><span>: </span><span>2023</span><span>, </span><span>stable</span><span>: </span><span>true</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>d</span><span>.</span><span>keys</span><span>(), (</span><span>"name"</span><span>, </span><span>"year"</span><span>, </span><span>"stable"</span><span>))</span></div></div><div><div><div>4</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>(</span><span>d</span><span>.</span><span>at</span><span>(</span><span>"version"</span><span>, </span><span>default</span><span>: </span><span>"?"</span><span>), </span><span>"?"</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>// 0.15: `map` and `filter` on a dictionary act on the values.</span></div></div><div><div><div>7</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>a</span><span>: </span><span>1</span><span>, </span><span>b</span><span>: </span><span>2</span><span>, </span><span>c</span><span>: </span><span>3</span><span>).</span><span>map</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> </span><span>v</span><span><span> </span><span>*</span><span> </span></span><span>2</span><span>), (</span><span>a</span><span>: </span><span>2</span><span>, </span><span>b</span><span>: </span><span>4</span><span>, </span><span>c</span><span>: </span><span>6</span><span>))</span></div></div><div><div><div>8</div></div><div><span>#assert</span><span>.</span><span>eq</span><span>((</span><span>a</span><span>: </span><span>1</span><span>, </span><span>b</span><span>: </span><span>2</span><span>, </span><span>c</span><span>: </span><span>3</span><span>).</span><span>filter</span><span>(</span><span>v</span><span> </span><span>=&gt;</span><span> </span><span>v</span><span><span> </span><span>&gt;</span><span> </span></span><span>1</span><span>), (</span><span>b</span><span>: </span><span>2</span><span>, </span><span>c</span><span>: </span><span>3</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>0.15 给字典加了 <code>map</code> 和 <code>filter</code>。要小心：回调只拿到<strong>值</strong>，拿不到键，结果仍是字典，键保持不变。<code>d.y</code> 访问不存在的键是个明确的错误，<code>dictionary does not contain key "y"</code>，用 <code>d.at("y", default: ...)</code> 才安全。</p><section><h3>就地修改的方法<a href="#就地修改的方法"><span>#</span></a></h3><p><code>push</code>、<code>pop</code>、<code>insert</code>、<code>remove</code> 这几个方法改的是变量本身，不是返回副本。字符串的 <code>+=</code> 同理。临时文件 <code>mut.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>xs</span><span><span> </span><span>= </span><span>(</span></span><span>1</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#xs</span><span>.</span><span>push</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>3</div></div><div><span>#let</span><span> </span><span>d</span><span><span> </span><span>= </span><span>(</span></span><span>a</span><span>: </span><span>1</span><span>)</span></div></div><div><div><div>4</div></div><div><span>#</span><span>{ </span><span>d</span><span>.</span><span>insert</span><span>(</span><span>"b"</span><span>, </span><span>2</span><span>) }</span></div></div><div><div><div>5</div></div><div><span>#let</span><span> </span><span>last</span><span><span> </span><span>= </span></span><span>xs</span><span>.</span><span>pop</span><span>()</span></div></div><div><div><div>6</div></div><div><span>#let</span><span> </span><span>s</span><span><span> </span><span>= </span></span><span>"ab"</span></div></div><div><div><div>7</div></div><div><span>#</span><span>{ </span><span>s</span><span><span> </span><span>+=</span><span> </span></span><span>"c"</span><span> }</span></div></div><div><div><div>8</div></div><div><span>#raw</span><span>(</span><span>repr</span><span>(</span><span>xs</span><span><span>) </span><span>+</span><span> </span></span><span>" "</span><span><span> </span><span>+</span><span> </span></span><span>repr</span><span>(</span><span>d</span><span><span>) </span><span>+</span><span> </span></span><span>" "</span><span><span> </span><span>+</span><span> </span></span><span>str</span><span>(</span><span>last</span><span><span>) </span><span>+</span><span> </span></span><span>" "</span><span><span> </span><span>+</span><span> </span></span><span>s</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(1, 2) (a: 1, b: 2) 3 abc</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>xs</code> 先 <code>push(3)</code> 变成 <code>(1, 2, 3)</code>，<code>pop()</code> 又取走 3，所以最后是 <code>(1, 2)</code>；<code>d</code> 多了键 <code>b</code>；字符串长成 <code>abc</code>。<code>#xs.push(3)</code> 在标记里不输出任何东西，因为方法返回 <code>none</code>。</p><p>有一条限制：这种修改只能发生在变量所在的那一层。如果在函数里对<strong>函数外</strong>的数组 <code>push</code>，Typst 拒绝：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: variables from outside the function are read-only and cannot be modified</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ mut2.typ:9:13</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>9 │ #let f() = { xs.push(9) }</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│              ^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>while calling `f` at mut2.typ:10:1</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>f()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>mut2.typ</code> 是 <code>mut.typ</code> 末尾追加 <code>#let f() = { xs.push(9) }</code> 和 <code>#f()</code> 两行。原因和解决办法放到下一篇，那里有仓库里的正式实验 <code>capture-mutation.fail.typ</code>。）</p></section></section>
<section><h2>诊断：assert、panic、repr 和 type<a href="#诊断assertpanicrepr-和-type"><span>#</span></a></h2><p>开发时你要靠它们说话。我把四个入口在 <code>/tmp/ch02/</code> 下各触发了一遍，贴真实报错。</p><p><strong><code>assert(cond, message: ...)</code></strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat assert.typ</span></div></div><div><div><div>2</div></div><div><span>#assert(1 + 1 == 3, message: "arithmetic is broken")</span></div></div><div><div><div>3</div></div><div><span>$ typst compile --root . assert.typ x.pdf</span></div></div><div><div><div>4</div></div><div><span>error: assertion failed: arithmetic is broken</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>┌─ assert.typ:1:1</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span>1 │ #assert(1 + 1 == 3, message: "arithmetic is broken")</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong><code>assert.eq(a, b)</code></strong> 更好用，不匹配时告诉你左值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat asserteq.typ</span></div></div><div><div><div>2</div></div><div><span>#assert.eq(2 * 3, 7)</span></div></div><div><div><div>3</div></div><div><span>$ typst compile --root . asserteq.typ x.pdf</span></div></div><div><div><div>4</div></div><div><span>error: equality assertion failed: value 6 was not equal to 7</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>┌─ asserteq.typ:1:1</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span>1 │ #assert.eq(2 * 3, 7)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>加 <code>message:</code> 则输出 <code>equality assertion failed: area mismatch</code>，不再给值。所以我的习惯是：不加消息，让它自己说值。本章所有“断言就是测试”的文件都是这个风格，第 16 篇的 <code>tests/</code> 目录整个是 <code>assert.eq</code>。</p><p><strong><code>panic(...)</code></strong>：无条件失败，接受多个参数。0.15 起字符串参数按原样打印，不再带引号。我写了个会触发它的函数（<code>panic.typ</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>area</span><span>(</span><span>w</span><span>, </span><span>h</span><span><span>) </span><span>= </span><span>{</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>if</span><span> </span><span>w</span><span><span> </span><span>&lt;</span><span> </span></span><span>0</span><span> </span><span>or</span><span> </span><span>h</span><span><span> </span><span>&lt;</span><span> </span></span><span>0</span><span> { </span><span>panic</span><span>(</span><span>"negative side: "</span><span><span> </span><span>+</span><span> </span></span><span>repr</span><span>((</span><span>w</span><span>, </span><span>h</span><span>))) }</span></div></div><div><div><div>3</div></div><div><span>  </span><span>w</span><span><span> </span><span>*</span><span> </span></span><span>h</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div><span>#area</span><span>(</span><span>3</span><span><span>, </span><span>-</span></span><span>2</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . panic.typ panic.pdf; echo "exit=$?"</span></div></div><div><div><div>2</div></div><div><span>error: panicked with: negative side: (3, -2)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ panic.typ:2:22</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>2 │   if w &lt; 0 or h &lt; 0 { panic("negative side: " + repr((w, h))) }</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>while calling `area` at panic.typ:5:1</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>area(3, -2)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>exit=1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两点：消息里没有引号，<code>repr((w, h))</code> 生成的 <code>(3, -2)</code> 是我主动拼进去的；报错还带了调用栈 <code>while calling area at ...</code>，这是 panic 比 assert 好用的地方，库作者在自己的函数里检查参数用它（第 15 篇的 <code>term</code> 会这么用）。</p><p>再验证“字符串原样、多参数”（<code>panic2.typ</code>，内容 <code>#panic("path: C:\\data\\\"x\"", "second", 42)</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: panicked with: path: C:\data\"x", second, 42</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ panic2.typ:1:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #panic("path: C:\\data\\\"x\"", "second", 42)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>源码里的 <code>\\</code> 和 <code>\"</code> 是字符串转义，运行后字符串本身是 <code>path: C:\data\"x"</code>，报错里原样出现，三个参数用逗号隔开。</p><p><strong><code>repr</code> 和 <code>type</code></strong>：<code>repr(x)</code> 给出值的“源码”字符串，<code>type(x)</code> 给出类型。对内容用 <code>repr</code> 能看到树结构（下一篇的主角）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval 'repr([*a* b])'</span></div></div><div><div><div>2</div></div><div><span>"sequence(strong(body: [a]), [ ], [b])"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一行告诉你 <code>[*a* b]</code> 其实是三个子节点的序列：一个 <code>strong</code>、一个空格、一个文字 <code>b</code>。</p><p>在调试时把 <code>#raw(repr(x))</code> 直接放进文档，是最快的 print 调试，这一章的表格就是这么来的。</p><p>一个收尾小练习，自己先猜再跑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst eval '"中文".slice(0, 3) + "!" * 2'</span></div></div><div><div><div>2</div></div><div><span>"中!!"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>slice(0, 3)</code> 刚好取走第一个汉字的 3 个字节，<code>"!" * 2</code> 是 <code>"!!"</code>，<code>*</code> 比 <code>+</code> 先算。下一篇进入函数：<code>#let f(x) = ...</code> 只是开头，真正有意思的是 <code>arguments</code>、闭包为什么不能改外面的变量，以及 <code>[*a* b]</code> 那棵树怎么被你的函数遍历。<a href="/posts/typst-by-xuan-03-functions-and-content/">第 3 篇</a>见。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/typst-by-xuan-01-first-document/</id>
      <title type="text">第一份文档：标记语法、编译与 watch</title>
      <published>2026-10-19T00:00:00.000Z</published>
      <updated>2026-10-19T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/typst-by-xuan-01-first-document/"/>
      <summary type="text">用 Xuan 从零学 Typst 第 1 篇：我在 Typst 0.15.1 里编译了四份成品文档，用同一份 8 节、30 个公式、5 张表的中文文档对比 Typst 与 latexmk（226 ms 对 3.131 s，快 13.85 倍），并亲手跑了 compile、watch、fonts 和 --root，把标记语法与三种模式逐条编译了一遍。</summary>
      <content type="html"><![CDATA[<p>我手上有一个叫 <code>xuan</code>（宣纸）的 Typst 模板包，加一个叫 <code>typst-lab</code> 的仓库，它们能产出四种文档：带目录和公式编号的笔记、可以开关解答的作业、幻灯片、中英文双语的简历。下面是四份成品的第一页，全部由 Typst 0.15.1 编译而来：</p>
<p></p><figure><img src="/assets/images/typst-by-xuan/ch01-tour-notes.png" alt="笔记第一页：标题、摘要、目录，正文有带编号的公式和交叉引用" /><figcaption>笔记第一页：标题、摘要、目录，正文有带编号的公式和交叉引用</figcaption></figure><p></p>
<p></p><figure><img src="/assets/images/typst-by-xuan/ch01-tour-homework.png" alt="作业第一页：课程、姓名、学号、总分 100，四道题，每题下方有带左侧竖线的解答块" /><figcaption>作业第一页：课程、姓名、学号、总分 100，四道题，每题下方有带左侧竖线的解答块</figcaption></figure><p></p>
<p></p><figure><img src="/assets/images/typst-by-xuan/ch01-tour-slides.png" alt="幻灯片标题页：Typst 一小时入门，居中的标题、副标题、作者和日期" /><figcaption>幻灯片标题页：Typst 一小时入门，居中的标题、副标题、作者和日期</figcaption></figure><p></p>
<p></p><figure><img src="/assets/images/typst-by-xuan/ch01-tour-cv.png" alt="简历：张三，教育经历、实习经历、项目经历、技能四个分节，一页 A4" /><figcaption>简历：张三，教育经历、实习经历、项目经历、技能四个分节，一页 A4</figcaption></figure><p></p>
<p>这个系列要做的事：从一份空文件开始，一章一章把 <code>xuan</code> 和这四份文档重新写出来。写完第 16 章，你的目录和我的仓库一致，而 Typst 这门语言（标记、脚本、样式、布局、数学、数据、模块、包、WebAssembly 插件、导出）会在第一次被用到的地方讲清楚。不需要先读文档。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：克隆好的 <code>typst-lab</code> 仓库和一个能用的 <code>nix develop</code> 环境，<code>make ch01</code> 通过（4 个文件全部 <code>ok</code>，其中 <code>unclosed.fail.typ</code> 按预期编译失败），以及对 Typst 标记语法、三种模式和 <code>compile</code>/<code>watch</code>/<code>fonts</code>/<code>--root</code> 四组命令行选项的第一手认识。<code>xuan</code> 包的源文件在这一篇里还一个都没有写，从第 3 篇起才出现。
涉及的 Typst 特性：标记语法（段落、强调、行内与块级 raw、自动链接、标题、三种列表、标签与引用、换行与转义、注释）、标记/代码/数学三种模式与 <code>#</code> 嵌入规则、<code>typst compile</code>、<code>typst watch</code>、<code>typst fonts</code>、<code>--root</code>、<code>--font-path</code>、<code>--ignore-system-fonts</code>、<code>--ignore-embedded-fonts</code>。
这是第 1 篇，没有前置章节。</p></div></div>
<section><h2>先看一个数字<a href="#先看一个数字"><span>#</span></a></h2><p>我很快就会让你去写 Typst，但在那之前我想先交代为什么值得换：同一份文档，Typst 比 LaTeX 快多少。</p><p>我用脚本 <code>scripts/gen-bench.sh</code> 生成了两份内容等价的文档（一份 <code>.typ</code>，一份 <code>.tex</code>）：8 节，30 个带编号的公式，5 张表，目录，20 处交叉引用，大段中英混排正文。两边都用同一个字体 Maple Mono NF CN 来排中文，LaTeX 用 <code>fontspec</code> + <code>xeCJK</code> + <code>amsmath</code> + <code>booktabs</code> + <code>hyperref</code>，由 <code>latexmk -xelatex</code> 编译。</p><p>为什么是 Maple Mono 而不是正文该用的 Noto Serif CJK？因为 <code>xdvipdfmx</code> 嵌不进可变字体版的 Noto TTC（报 <code>sfnt: table not found</code>），而我这台机器上唯一的静态 CJK 字体就是 Maple Mono。对比要公平，就得让两个引擎吃同一个静态 TTF。（顺带一提，这意味着这份基准不是排版好看的基准，只是引擎速度的基准。）</p><p>跑法是 <code>make bench</code>，里面是 <code>hyperfine</code>，每轮 LaTeX 之前先 <code>latexmk -C</code> 清掉辅助文件，所以 LaTeX 是冷启动：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make bench</span></div></div><div><div><div>2</div></div><div><span>Benchmark 1: typst compile --root . lab/ch01/bench/same.typ out/bench/same.pdf</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Time (mean ± σ):     226.1 ms ±   5.4 ms    [User: 145.9 ms, System: 85.8 ms]</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Range (min … max):   214.6 ms … 236.3 ms    13 runs</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Benchmark 2: latexmk -xelatex -interaction=nonstopmode -outdir=out/bench/tex lab/ch01/bench/same.tex &gt;/dev/null</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>Time (mean ± σ):      3.131 s ±  0.072 s    [User: 2.416 s, System: 0.687 s]</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>Range (min … max):    3.025 s …  3.252 s    10 runs</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Summary</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>typst compile --root . lab/ch01/bench/same.typ out/bench/same.pdf ran</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>13.85 ± 0.46 times faster than latexmk -xelatex -interaction=nonstopmode -outdir=out/bench/tex lab/ch01/bench/same.tex &gt;/dev/null</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>226 ms 对 3.131 s，14 倍。输出都是 9 页（<code>pdfinfo</code> 数的）。机器是 AMD Ryzen 7 PRO 6850U，16 个线程，LaTeX 是 TeX Live 2025（<code>XeTeX 3.141592653-2.6-0.999997</code>）。注意 <code>nix develop</code> 不提供 <code>xelatex</code> 和 <code>latexmk</code>，它们是我系统里自带的；你的机器上没有的话，<code>make bench</code> 会打印 <code>skip: xelatex not found</code>，然后只测 Typst 那一半。</p><p>几条诚实的限定：这是一台机器上、一份合成文档的数字，我猜换成你的论文会有出入。LaTeX 一侧我用的是 <code>latexmk</code>，它会按需要重复调用 <code>xelatex</code> 直到目录和引用稳定，我没有数它实际跑了几遍，所以这 14 倍里有多少是“遍数”、有多少是引擎本身，我说不清。但对“每次保存都想看结果”的日常写作来说，3 秒和 0.2 秒是两种体验，下面的 <code>watch</code> 会让你直接看到。</p></section>
<section><h2>这个系列怎么读<a href="#这个系列怎么读"><span>#</span></a></h2><p>规矩有四条。</p><p><strong>固定的代码快照。</strong> 参考实现是仓库 <a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1" target="_blank"><code>typst-lab</code></a> 的 <code>tutorial-v1</code> 标签。这一篇的所有命令都在它上面跑过。每章末尾的检查点会告诉你哪些文件已经是最终版（必须和标签一致），哪些还是中间版本，后面的篇章会继续改它们。</p><p><strong>每章一个实验目录。</strong> 第 N 篇对应仓库里的 <a href="https://github.com/bfmhno3/typst-lab/tree/tutorial-v1/lab/ch01" target="_blank"><code>lab/chNN/</code></a>，里面是一堆小的 <code>.typ</code> 文件，<code>make chNN</code> 一次编译全部。我引用它们时会写出路径。文件我都按 <code>typstyle -l 80</code> 格式化过，贴在文中的就是它们的原样；太长的文件我会只贴有教学价值的部分并明说。</p><p><strong><code>*.fail.typ</code> 与 <code>*.warn.typ</code>。</strong> 凡是文件名带 <code>.fail.typ</code> 的，它的使命就是编译失败；带 <code>.warn.typ</code> 的必须编译成功但有警告。<code>scripts/chapter.sh</code> 会检查这个预期，期望失败却成功了，脚本本身就报错。仓库里文件的错误信息，我都是从 <code>out/chNN/*.stderr</code> 里原样贴出来的；少数是我在 <code>/tmp</code> 里为讲解临时写的小实验，我会明说。</p><p><strong>真实输出。</strong> 本系列的所有命令输出，都是在 Typst 0.15.1（<code>9dfd3a08</code>）上跑出来的。输出偶尔会被我截断（用 <code>...</code> 标出），但不会改写。版本不同时，错误信息的措辞可能略有出入。</p><p>下面是整个系列的地图：</p>

<table><thead><tr><th>篇</th><th>标题</th><th>主要的 Typst 内容</th></tr></thead><tbody><tr><td>1</td><td>第一份文档：标记语法、编译与 watch</td><td>标记语法、三种模式、<code>compile</code>/<code>watch</code>/<code>fonts</code>、<code>--root</code></td></tr><tr><td>2</td><td><a href="/posts/typst-by-xuan-02-code-mode/">代码模式：值、类型与控制流</a></td><td>值与类型、<code>let</code>、<code>if</code>/<code>for</code>/<code>while</code>、数组与字典、<code>assert</code></td></tr><tr><td>3</td><td><a href="/posts/typst-by-xuan-03-functions-and-content/">函数与 content：一切都是函数调用</a></td><td>函数、<code>..args</code>、闭包、content 树、<code>xuan</code> 的 <code>util.typ</code></td></tr><tr><td>4</td><td><a href="/posts/typst-by-xuan-04-set-and-show/">set 与 show：样式是规则，不是命令</a></td><td><code>set</code>/<code>show</code>、选择器、第一个模板 <code>note</code>、包的目录结构</td></tr><tr><td>5</td><td><a href="/posts/typst-by-xuan-05-fonts-and-cjk/">字体与中文排版</a></td><td>字体回退、<code>text</code> 参数、<code>cjk-latin-spacing</code>、<code>par</code>、中文编号</td></tr><tr><td>6</td><td><a href="/posts/typst-by-xuan-06-page-and-layout/">页面与布局：盒子、网格与浮动</a></td><td><code>page</code>、<code>block</code>/<code>box</code>、<code>grid</code>、<code>place</code>、<code>rotate</code>、<code>layout</code>/<code>measure</code></td></tr><tr><td>7</td><td><a href="/posts/typst-by-xuan-07-context-counters-state/">context、计数器与状态：Typst 如何“多遍”编译</a></td><td><code>context</code>、<code>counter</code>、<code>state</code>、<code>query</code>、定理环境、收敛警告</td></tr><tr><td>8</td><td><a href="/posts/typst-by-xuan-08-math/">数学公式</a></td><td>数学模式全部语法、LaTeX 对照表、公式编号</td></tr><tr><td>9</td><td><a href="/posts/typst-by-xuan-09-figures-tables-code/">图、表、代码与交叉引用</a></td><td><code>figure</code>、<code>table</code>、<code>raw</code>、<code>ref</code>、<code>outline</code>、<code>footnote</code>、单位</td></tr><tr><td>10</td><td><a href="/posts/typst-by-xuan-10-bibliography/">参考文献：BibTeX、Hayagriva 与 GB/T 7714</a></td><td><code>bibliography</code>、<code>cite</code>、CSL、多个参考文献表</td></tr><tr><td>11</td><td><a href="/posts/typst-by-xuan-11-data-driven-cv/">数据驱动：用一份 YAML 生成中英文简历</a></td><td><code>yaml</code>/<code>csv</code>/<code>json</code>、<code>sys.inputs</code>、<code>eval</code>、<code>datetime</code></td></tr><tr><td>12</td><td><a href="/posts/typst-by-xuan-12-homework/">作业模板：模块、多文件与条件编译</a></td><td><code>import</code>、<code>include</code>、<code>path</code> 类型、条件编译解答</td></tr><tr><td>13</td><td><a href="/posts/typst-by-xuan-13-slides-and-drawing/">幻灯片与绘图</a></td><td>图形函数、颜色、渐变、<code>tiling</code>、幻灯片模板</td></tr><tr><td>14</td><td><a href="/posts/typst-by-xuan-14-packages-ecosystem/">生态：Typst Universe 与第三方包</a></td><td><code>@preview</code> 包、CeTZ、Lilaq、Touying、<code>typst.toml</code></td></tr><tr><td>15</td><td><a href="/posts/typst-by-xuan-15-wasm-plugin/">用 Rust 写一个 WebAssembly 插件：按拼音排序术语表</a></td><td><code>plugin</code>、字节协议、Rust <code>wasm-minimal-protocol</code></td></tr><tr><td>16</td><td><a href="/posts/typst-by-xuan-16-export-and-tooling/">导出、工具链与 CI</a></td><td>PDF 标准、PNG/SVG/HTML、<code>typst eval</code>、tinymist、typstyle、CI</td></tr></tbody></table><p>顺序就是依赖顺序：每一篇只会用到前面讲过的东西。</p></section>
<section><h2>装环境<a href="#装环境"><span>#</span></a></h2><p><code>typst-lab</code> 有一个 <code>flake.nix</code>，<code>nix develop</code> 进去就是我用的全部工具，字体也在里面：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>--branch</span><span> </span><span>tutorial-v1</span><span> </span><span>https://github.com/bfmhno3/typst-lab.git</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>typst-lab</span></div></div><div><div><div>3</div></div><div><span>nix</span><span> </span><span>develop</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>进去的时候 shell 会打印一行横幅，我这边是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>typst-lab dev shell: typst 0.15.1 (9dfd3a08), 0.15.0, rustc 1.97.1 (8bab26f4f 2026-07-14) (built from a source tarball)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个版本依次是 Typst 0.15.1、typstyle 0.15.0（代码格式化器，第 16 篇用）、Rust 1.97.1（第 15 篇用）。<code>flake.lock</code> 钉死的 nixpkgs 版本和这个博客的一样，所以你装出来的版本应该就是这几个。没有 Nix 的话，你需要自己装 Typst 0.15.1 和 Noto Serif CJK、Noto Sans CJK 字体，再把字体目录写进环境变量 <code>TYPST_FONT_PATHS</code>、把仓库里的 <code>packages/</code> 写进 <code>TYPST_PACKAGE_PATH</code>；我没有在非 Nix 环境里验证过完整流程，所以这条路径上的输出我不替你担保。</p><p>shell 还做了三件事，后面都会用到：把字体目录放进 <code>TYPST_FONT_PATHS</code>，把仓库的 <code>packages/</code> 放进 <code>TYPST_PACKAGE_PATH</code>（第 4 篇讲为什么），以及 <code>unset SOURCE_DATE_EPOCH</code>（不然 <code>datetime.today()</code> 在 nix shell 里永远是 1980-01-01）。</p><p>之后所有命令都在仓库根目录、<code>nix develop</code> 里执行，Makefile 开头有个守卫，不在 shell 里就报 <code>run inside nix develop</code>。</p></section>
<section><h2>第一次编译<a href="#第一次编译"><span>#</span></a></h2><p>最小的文件是 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch01/hello.typ" target="_blank"><code>lab/ch01/hello.typ</code></a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=</span><span> 你好，Typst</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>这是第一份文档。保存后 </span><span>`typst watch`</span><span> 会立刻重新编译。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有 <code>\documentclass</code>，没有 <code>\begin{document}</code>，没有导言区。第一行 <code>=</code> 加空格是一级标题，空一行是新段落，反引号包起来的是行内代码。编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile hello.typ</span></div></div><div><div><div>2</div></div><div><span>$ ls</span></div></div><div><div><div>3</div></div><div><span>hello.pdf  hello.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有输出就是成功。输出文件默认是同名的 <code>.pdf</code>。这个文件在我机器上编译一次，<code>hyperfine -N</code> 测得：带 <code>--ignore-system-fonts</code> 是 83.5 ± 5.4 ms，不带是 182.9 ± 2.6 ms。多出来的约 100 ms，我猜是扫描这台机器上 284 个系统字体族的代价（下面讲 <code>typst fonts</code> 时你会看到这个数字），我没有单独验证。两个数字都含进程启动。</p><p>有一件事我先点一下：这个文件没有指定字体，但标题和正文里的汉字显示出来了。我用 <code>pdffonts</code> 看了（带 <code>--ignore-system-fonts</code> 编译出来的）PDF，里面嵌了 <code>LibertinusSerif</code>（拉丁字母，Typst 内置的默认字体）、<code>DejaVuSansMono</code>（行内代码，也是内置的）和一个叫 <code>NotoSansCJKjp-Thin</code> 的字体（汉字）。Libertinus 里没有汉字，所以 Typst 往后找，找到了我环境变量里的 Noto Sans CJK。这个“碰巧能用”不可靠（连选中了哪个变体都不由我决定），第 5 篇我们会把每一层字体都显式钉死，那个奇怪的字体名也到时再解释。</p><section><h3>watch：保存即编译<a href="#watch保存即编译"><span>#</span></a></h3><p><code>typst watch</code> 和 <code>compile</code> 参数一样，但进程不退出，文件一变就重新编译。我在一个临时目录里拷了 <code>hello.typ</code>，后台起 watch，隔几秒往文件末尾追加一行，再追加一次：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>/tmp/t01</span><span> &amp;&amp; </span><span>cp</span><span> </span><span>lab/ch01/hello.typ</span><span> </span><span>/tmp/t01/</span><span> &amp;&amp; </span><span>cd</span><span> </span><span>/tmp/t01</span></div></div><div><div><div>2</div></div><div><span>typst</span><span> </span><span>watch</span><span> </span><span>hello.typ</span><span> &gt;</span><span>watch.log</span><span> 2&gt;&amp;1 &amp;</span></div></div><div><div><div>3</div></div><div><span>W</span><span>=</span><span>$!</span></div></div><div><div><div>4</div></div><div><span>sleep</span><span> </span><span>3</span><span>; </span><span>echo</span><span> </span><span>"这是新增的一行。"</span><span> &gt;&gt; </span><span>hello.typ</span></div></div><div><div><div>5</div></div><div><span>sleep</span><span> </span><span>2</span><span>; </span><span>echo</span><span> </span><span>"再改一次。"</span><span> &gt;&gt; </span><span>hello.typ</span></div></div><div><div><div>6</div></div><div><span>sleep</span><span> </span><span>2</span><span>; </span><span>kill</span><span> </span><span>$W</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>日志里（我去掉了重复打印的头两行 <code>watching hello.typ</code> / <code>writing to hello.pdf</code>：输出到文件时 watch 每次都会重复它们，我猜在真终端里它们是原地刷新的，这一点我没有核实）有价值的行是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[08:19:30] compiling ...</span></div></div><div><div><div>2</div></div><div><span>[08:19:30] compiled successfully in 50.47 ms</span></div></div><div><div><div>3</div></div><div><span>[08:19:33] compiling ...</span></div></div><div><div><div>4</div></div><div><span>[08:19:33] compiled successfully in 3.36 ms</span></div></div><div><div><div>5</div></div><div><span>[08:19:35] compiling ...</span></div></div><div><div><div>6</div></div><div><span>[08:19:35] compiled successfully in 2.66 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>首次编译 50.47 ms，之后每次增量编译 3.36 ms 和 2.66 ms。我猜是 Typst 缓存了没变的部分，改一行只重排受影响的内容，不过我没有去读它的源码确认。（我这份文档只有一个段落，所以三毫秒是下限，不是你 100 页论文的数字。）</p><p>这就是我说的“保存即看到”：编辑器里按保存，旁边开一个 PDF 阅读器，PDF 在你抬头之前已经刷新。第 16 篇我们会让 tinymist 在编辑器里做同样的事，带实时预览。</p></section></section>
<section><h2>标记语法：一次过一遍<a href="#标记语法一次过一遍"><span>#</span></a></h2><p>下面是本篇的主角 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch01/markup-tour.typ" target="_blank"><code>lab/ch01/markup-tour.typ</code></a>，我把 Typst 日常写作用到的标记几乎都塞进了它：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>14cm</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>1cm</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>heading</span><span>(</span><span>numbering</span><span>: </span><span>"1.1"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>=</span><span> 标记语法速览 </span><span>&lt;tour&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>普通段落，*加粗*，_强调_，行内代码 </span><span>`let x = 1`</span><span><span>，自动链接 </span><span>https://typst.app。</span></span></div></div><div><div><div>7</div></div><div><span>空行分段；行末加反斜杠 </span><span>\</span></div></div><div><div><div>8</div></div><div><span>可以强制换行。不换行空格 10~km，连字符 -- 与 --- 分别是 en dash 与 em dash。</span></div></div><div><div><div>9</div></div><div><span>智能引号："double" 与 'single'。</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>==</span><span> 列表与术语</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>- 无序列表</span></div></div><div><div><div>14</div></div><div><span>- 第二项</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>- 嵌套一层</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>+ 有序列表</span></div></div><div><div><div>18</div></div><div><span>+ 编号自动递增</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>/ 术语: 这是术语列表的说明。</span></div></div><div><div><div>21</div></div><div><span>/ Typst: 一门基于标记的排版语言。</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>==</span><span> 代码块与标签 </span><span>&lt;code&gt;</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>```rust</span></div></div><div><div><div>26</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>println!</span><span>(</span><span>"hello"</span><span>);</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div><span>```</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>转义：</span><span>\#</span><span>、</span><span>\*</span><span>、</span><span>\_</span><span>，Unicode：</span><span>\u{4e2d}\u{6587}</span><span>。</span></div></div><div><div><div>32</div></div><div><span>引用：见 </span><span>@tour</span><span> 与 </span><span>@code</span><span>。</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>// 这一行是注释，不会出现在输出里。</span></div></div><div><div><div>35</div></div><div><span>/* 块注释</span></div></div><div><div><div>36</div></div><div><span>也一样。 */</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>开头两行 <code>#set</code> 先不管（它们是第 4 篇的主题，这里只是让页面窄一点、标题带编号，方便截图和引用）。编译它，用 <code>--pages 1 --format png --ppi 72</code> 渲染成图：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . --ignore-system-fonts lab/ch01/markup-tour.typ out/ch01/markup-tour.pdf</span></div></div><div><div><div>2</div></div><div><span>$ typst compile --root . --ignore-system-fonts --pages 1 --format png --ppi 72 lab/ch01/markup-tour.typ out/figures/ch01-markup-page.png</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结果：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch01-markup-page.png" alt="markup-tour.typ 的第一页：一级标题带编号 1，下面是加粗、强调、行内代码、自动链接，两个二级标题 1.1 与 1.2，列表、术语表、带语法高亮的 Rust 代码块，以及“见 Section 1 与 Section 1.2”" /><figcaption>markup-tour.typ 的第一页：一级标题带编号 1，下面是加粗、强调、行内代码、自动链接，两个二级标题 1.1 与 1.2，列表、术语表、带语法高亮的 Rust 代码块，以及“见 Section 1 与 Section 1.2”</figcaption></figure><p></p><p>我把每一条语法对应到它的效果，逐个说明。</p><section><h3>行内格式<a href="#行内格式"><span>#</span></a></h3><p><strong><code>*加粗*</code>、<code>_强调_</code>。</strong> 星号包的是 strong，下划线包的是 emph。它们是 <code>strong[...]</code> 和 <code>emph[...]</code> 这两个函数调用的语法糖，第 3 篇会证明这一点。</p><p>中文里不需要空格，<code>中文*加粗*中文</code> 照样生效。但紧贴在英文单词里的星号就不会触发加粗。我编译了一个暂存文件 <code>cjkstrong.typ</code>（不在仓库里，只是我为这一节做的实验）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>中文*加粗*中文，English*not*Strong，中_强调_文。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pdftotext</code> 抽出来的文字是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>中文加粗中文，English*not*Strong，中强调文。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>汉字之间的 <code>*...*</code> 被当作加粗，而紧贴在英文单词里的 <code>*not*</code> 原样输出了星号。我另外试了 <code>2*3*4 与 a*b*c</code>，输出仍是 <code>2*3*4 与 a*b*c</code>：夹在字母数字之间的星号不触发加粗。</p><p><strong>行内代码与代码块。</strong> 单个反引号包的是行内 raw，等宽字体；三个反引号包的是块级 raw，开头紧跟语言名（上面的 <code>rust</code>）就会按该语言高亮。raw 里面的 <code>*</code>、<code>_</code>、<code>#</code> 都不再有含义，所以里面可以放任何字符。</p><p><strong>自动链接。</strong> 以 <code>http://</code> 或 <code>https://</code> 开头的片段不需要任何包装就是可点击链接。句子末尾的标点不会被吞：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>网址 </span><span>https://typst.app/docs，之后。</span></span></div></div><div><div><div>2</div></div><div><span><span>再 </span><span>https://typst.app/docs</span><span>. 之后。</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用 <code>typst eval 'query(link).map(l =&gt; l.dest)' --in link.typ --format json</code> 看这两个链接（一个后面跟全角逗号，一个后面跟英文句号和空格），得到 <code>["https://typst.app/docs","https://typst.app/docs"]</code>：标点没有被吞进链接。</p></section><section><h3>段落、换行与空格<a href="#段落换行与空格"><span>#</span></a></h3><p><strong>空行分段。</strong> 一个空行是一个新段落，两行之间的单个换行只是空白。</p><p><strong><code>\</code> 强制换行。</strong> 行末写反斜杠，段落内断行，不开新段。上面的“空行分段；行末加反斜杠 \”之后下一行就是强制换行的结果。</p><p><strong><code>~</code> 不换行空格。</strong> <code>10~km</code> 是一个不可拆的空格，<code>10</code> 与 <code>km</code> 不会被拆到两行。</p><p><strong><code>--</code> 与 <code>---</code>。</strong> 分别是 en dash（U+2013）与 em dash（U+2014）。这是 Typst 的“shorthand”，在编译阶段替换。同类的还有 <code>...</code> 变成 U+2026 省略号：我编译 <code>省略号...</code> 得到 <code>省略号…</code>。但箭头 <code>-&gt;</code> 和 <code>&lt;=</code> 在标记模式里原样输出（我编译验证过）；换到数学模式 <code>$a -&gt; b &lt;= c$</code>，它们变成了 <code>→</code> 和 <code>≤</code>。符号简写的这一半，第 8 篇再讲。</p><p><strong>智能引号。</strong> 直引号 <code>"double"</code> 与 <code>'single'</code> 会变成弯引号，图里正是 “double” 与 ‘single’；我加了 <code>#set text(lang: "zh")</code> 再编译，抽出来的还是弯引号。这是 <code>smartquote</code> 的工作，第 5 篇再细讲。</p></section><section><h3>标题、列表与术语<a href="#标题列表与术语"><span>#</span></a></h3><p><strong>标题。</strong> 行首 <code>=</code> 加空格是一级标题，<code>==</code> 是二级，以此类推。<code>=</code> 后面如果没有空格，它就只是普通文字：我编译 <code>=标题</code>，输出的是原样的 <code>=标题</code>。</p><p><strong>列表。</strong> 三种：</p><ul>
<li><code>-</code> 无序列表，缩进两个空格再写 <code>-</code> 就是嵌套（图里嵌套一层变成了三角形项目符号）。</li>
<li><code>+</code> 有序列表，编号自动递增，不用手写 1. 2. 3.。</li>
<li><code>/ 术语: 说明</code> 术语列表，冒号前面是术语，后面是解释。</li>
</ul><p>三种列表是三个函数的语法糖：<code>list</code>、<code>enum</code>、<code>terms</code>。</p></section><section><h3>标签、引用与转义<a href="#标签引用与转义"><span>#</span></a></h3><p><strong><code>&lt;tour&gt;</code> 与 <code>@tour</code>。</strong> 在标题（或任何元素）后面写 <code>&lt;名字&gt;</code> 给它贴一个标签，之后在任何地方写 <code>@名字</code> 引用它。图里的 “见 Section 1 与 Section 1.2” 就是 <code>@tour</code> 和 <code>@code</code> 的渲染结果，前缀 <code>Section</code> 是因为这个文件没有设 <code>lang</code>。我把 <code>lang</code> 设成 <code>"zh"</code> 编译了另一个暂存文件 <code>refzh.typ</code>，同一种引用变成“见 小节 1。”，第 5 篇和第 9 篇会处理这类本地化。</p><p>引用有一个前提，我故意没满足的时候能看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=</span><span> 引言 </span><span>&lt;intro&gt;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>见 </span><span>@intro</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: cannot reference heading without numbering</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ noref.typ:3:2</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>3 │ 见 @intro。</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│    ^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= hint: you can enable heading numbering with `#set heading(numbering: "1.")`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>标题没有编号的时候，引用不知道该显示什么，Typst 直接报错并告诉你怎么修。这就是 <code>markup-tour.typ</code> 开头 <code>#set heading(numbering: "1.1")</code> 的原因。</p><p><strong>转义。</strong> <code>\#</code>、<code>\*</code>、<code>\_</code> 让特殊字符按字面输出，<code>\u{4e2d}\u{6587}</code> 是按码点写 Unicode，输出“中文”。我另外试了 <code>\u{e9}</code> 与 <code>\u{20ac}</code>，编译后 <code>pdftotext</code> 抽出的是 <code>é€</code>，花括号里是十六进制码点。<code>\#65</code> 输出的只是字面的 <code>#65</code>。</p><p><strong>注释。</strong> <code>//</code> 到行尾，<code>/* ... */</code> 可以跨行，它们都不会出现在输出里。</p></section><section><h3>语法一览<a href="#语法一览"><span>#</span></a></h3>

<table><thead><tr><th>想要的效果</th><th>写法</th><th>对应函数</th></tr></thead><tbody><tr><td>加粗</td><td><code>*文字*</code></td><td><code>strong</code></td></tr><tr><td>强调</td><td><code>_文字_</code></td><td><code>emph</code></td></tr><tr><td>行内代码</td><td><code>`code`</code></td><td><code>raw</code></td></tr><tr><td>代码块</td><td>三个反引号加语言名</td><td><code>raw(block: true)</code></td></tr><tr><td>一级标题</td><td><code>= 标题</code></td><td><code>heading(level: 1)</code></td></tr><tr><td>无序列表</td><td><code>- 项</code></td><td><code>list</code></td></tr><tr><td>有序列表</td><td><code>+ 项</code></td><td><code>enum</code></td></tr><tr><td>术语列表</td><td><code>/ 术语: 说明</code></td><td><code>terms</code></td></tr><tr><td>标签</td><td><code>&lt;name&gt;</code></td><td>贴在前一个元素上</td></tr><tr><td>引用</td><td><code>@name</code></td><td><code>ref</code></td></tr><tr><td>强制换行</td><td>行末 <code>\</code></td><td><code>linebreak</code></td></tr><tr><td>不换行空格</td><td><code>~</code></td><td>编译期替换</td></tr><tr><td>en/em dash</td><td><code>--</code> / <code>---</code></td><td>编译期替换</td></tr><tr><td>转义</td><td><code>\#</code>、<code>\u{4e2d}</code></td><td>编译期替换</td></tr><tr><td>注释</td><td><code>//</code>、<code>/* */</code></td><td>无</td></tr></tbody></table></section></section>
<section><h2>三种模式<a href="#三种模式"><span>#</span></a></h2><p>上面的 <code>#set heading(...)</code> 是我偷偷用到的第一处“代码”。Typst 有三种模式，在它们之间切换，是这门语言最核心的设计。我用一个暂存文件 <code>modes.typ</code>（不在仓库里）把三种模式混在一起：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#let</span><span> </span><span>w</span><span><span> </span><span>= </span></span><span>3</span></div></div><div><div><div>2</div></div><div><span>#let</span><span> </span><span>name</span><span><span> </span><span>= </span></span><span>"Typst"</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// 代码模式：# 后面接一个表达式，到表达式结束为止</span></div></div><div><div><div>5</div></div><div><span>宽度是 </span><span>#w</span><span> 厘米，</span><span>#name</span><span>.</span><span>len</span><span>() 个字母。</span></div></div><div><div><div>6</div></div><div><span>// 方法调用、字段访问会被吃进去，标点不会</span></div></div><div><div><div>7</div></div><div><span>先 </span><span>#name</span><span>，</span><span>再</span><span> </span><span>#name</span><span>.</span></div></div><div><div><div>8</div></div><div><span>// 想让表达式带上后面的文字，用括号或者 #[]</span></div></div><div><div><div>9</div></div><div><span>#</span><span>(</span><span>w</span><span><span> </span><span>+</span><span> </span></span><span>1</span><span>)cm，</span><span>#</span><span>(</span><span>name</span><span>)er，</span><span>#upper</span><span>[hello]world</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// 代码块内部是代码模式，方括号回到标记模式</span></div></div><div><div><div>12</div></div><div><span>#</span><span>{</span></div></div><div><div><div>13</div></div><div><span>  </span><span>let</span><span> </span><span>n</span><span><span> </span><span>= </span></span><span>2</span><span><span> </span><span>+</span><span> </span></span><span>3</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>[n 等于 </span><span>#n</span><span>，]</span></div></div><div><div><div>15</div></div><div><span>  </span><span>"字符串也会被拼进来。"</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>// 数学模式</span></div></div><div><div><div>19</div></div><div><span>行内 </span><span>$</span><span>x^2 + y^2 = z^2</span><span>$</span><span>，块级：</span></div></div><div><div><div>20</div></div><div><span>$</span><span> </span><span>sum</span><span>_</span><span>(</span><span>i=1</span><span>)</span><span>^n i = (n(n+1)) / 2 </span><span>$</span></div></div><div><div><div>21</div></div><div><span>数学里嵌代码：</span><span>$</span><span>#w</span><span> </span><span>dot</span><span> 2 = </span><span>#</span><span>(</span><span>w</span><span><span> </span><span>*</span><span> </span></span><span>2</span><span>)</span><span>$</span><span>。</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译（<code>typst compile modes.typ</code>）后，<code>pdftotext -layout</code> 抽出的文字是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>宽度是 3 厘米，5 个字母。 先 Typst，再 Typst. 4cm，Typster，HELLOworld</span></div></div><div><div><div>2</div></div><div><span>n 等于 5，字符串也会被拼进来。</span></div></div><div><div><div>3</div></div><div><span>行内 𝑥2 + 𝑦2 = 𝑧 2 ，块级：</span></div></div><div><div><div>4</div></div><div><span><span>                             </span></span><span>𝑛</span></div></div><div><div><div>5</div></div><div><span><span>                                   </span></span><span>𝑛(𝑛 + 1)</span></div></div><div><div><div>6</div></div><div><span><span>                            </span></span><span>∑𝑖 =</span></div></div><div><div><div>7</div></div><div><span><span>                            </span></span><span>𝑖=1</span></div></div><div><div><div>8</div></div><div><span><span>                                      </span></span><span>2</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>数学里嵌代码：3 ⋅ 2 = 6。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（数学公式在文本抽取里会被拆散，这不影响 PDF 里的样子，第 8 篇我们会直接看渲染图。）</p><p>三种模式的规则，逐条对着这个输出看。</p><p><strong>标记模式是默认模式。</strong> 文件开头就在标记模式：你写的字就是文字，<code>*</code>、<code>_</code>、<code>=</code>、<code>-</code> 这些有含义。这像一场普通话的对话，偶尔夹一句另一种语言，说完自动切回来：<code>#</code> 就是那个“我要换一种说法”的信号。</p><p><strong><code>#</code> 进入代码模式，只管一个表达式。</strong> <code>#w</code> 取变量值，<code>#name.len()</code> 连方法调用一起吃进去（“Typst”有 5 个字母），<code>#upper[hello]</code> 是函数调用，方括号里是内容参数。表达式到哪里结束：标识符、字段访问和方法调用、函数调用的括号和方括号是表达式的一部分；其他字符，包括中文逗号和句号，结束它。所以 <code>先 #name，再 #name.</code> 里第二个 <code>.</code> 后面没有标识符，句号留在文字里，输出的是 <code>Typst.</code>。</p><p><strong>想紧贴后面的字，用括号。</strong> <code>#(w + 1)cm</code> 把 <code>w + 1</code> 算完得到 4，后面的 <code>cm</code> 原样输出，最终是 <code>4cm</code>。<code>#(name)er</code> 同理得到 <code>Typster</code>。没有括号写 <code>#namer</code> 就是在引用变量 <code>namer</code>，会报“unknown variable”。</p><p><strong><code>#{ ... }</code> 是代码块，<code>[ ... ]</code> 是内容块。</strong> 花括号里是代码模式，每一行是一条语句，<code>let</code> 声明变量；方括号里又回到标记模式，<code>[n 等于 #n，]</code> 是一段含代码的内容。块里最后那个字符串字面量也被拼进了输出：代码块把每条语句的结果“连接”（join）起来，这一点第 2 篇讲得很细。</p><p><strong><code>$ ... $</code> 是数学模式。</strong> 美元符号紧贴内容是行内公式，美元符号与内容之间各留一个空格是块级公式（居中，单独一行）。上标 <code>^</code>、下标 <code>_</code>、分式 <code>/</code>、求和 <code>sum</code> 都是数学模式自己的语法，和标记模式里的符号含义不同。在数学里再次嵌入代码也是 <code>#</code>：<code>$#w dot 2 = #(w * 2)$</code> 得到 <code>3 ⋅ 2 = 6</code>。</p><p>这一节的规则，我压成一张表：</p>

<table><thead><tr><th>位置</th><th>默认模式</th><th>进入下一种的符号</th></tr></thead><tbody><tr><td>文件顶层、<code>[...]</code> 内部</td><td>标记</td><td><code>#</code> 进入代码，<code>$</code> 进入数学</td></tr><tr><td><code>#{ ... }</code>、<code>#(...)</code>、函数参数</td><td>代码</td><td><code>[...]</code> 回到标记，<code>$...$</code> 进入数学</td></tr><tr><td><code>$ ... $</code></td><td>数学</td><td><code>#</code> 进入代码</td></tr></tbody></table><p>进入代码之后就不需要再写 <code>#</code> 了：<code>#{ ... }</code> 里面的 <code>let</code>、函数调用前面都没有 <code>#</code>，只有从方括号回到标记模式、再嵌入代码时才重新需要它（<code>[n 等于 #n，]</code>）。</p></section>
<section><h2>编译失败长什么样<a href="#编译失败长什么样"><span>#</span></a></h2><p>我在 <a href="https://github.com/bfmhno3/typst-lab/blob/tutorial-v1/lab/ch01/unclosed.fail.typ" target="_blank"><code>lab/ch01/unclosed.fail.typ</code></a> 里故意写了一个忘记关闭的加粗：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=</span><span> 标题</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>*忘记关闭的加粗</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unclosed delimiter</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ lab/ch01/unclosed.fail.typ:3:0</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>3 │ *忘记关闭的加粗</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│ ^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Typst 指出第 3 行第 0 列，高亮的是起始的那个 <code>*</code>，也就是没关的那个。<code>make ch01</code> 里它显示为 <code>ok   lab/ch01/unclosed.fail.typ (failed as expected)</code>。</p></section>
<section><h2>命令行：fonts 与 —root<a href="#命令行fonts-与-root"><span>#</span></a></h2><p>到这里已经够写一份笔记了。下面把命令行补全。<code>typst --help</code> 里的子命令只有这些：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Commands:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>compile      Compiles an input file into a supported output format [aliases: c]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>watch        Watches an input file and recompiles on changes [aliases: w]</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>init         Initializes a new project from a template</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>eval         Evaluates a piece of Typst code, optionally in the context of a document</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>fonts        Lists all discovered fonts in system and custom font paths</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>completions  Generates shell completion scripts</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>info         Displays debugging information about Typst</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>help         Print this message or the help of the given subcommand(s)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（我把换行折回了一行。<code>init</code> 在第 14、16 篇，<code>eval</code> 在第 16 篇。）</p><section><h3><code>typst fonts</code> 与字体的三层来源<a href="#typst-fonts-与字体的三层来源"><span>#</span></a></h3><p>Typst 会从三个地方找字体：二进制里内置的、系统安装的、命令行或环境变量指定的目录。<code>typst fonts</code> 列出它找到的全部，每行一个字体族。在我的机器上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst fonts | wc -l</span></div></div><div><div><div>2</div></div><div><span>284</span></div></div><div><div><div>3</div></div><div><span>$ typst fonts --ignore-system-fonts | wc -l</span></div></div><div><div><div>4</div></div><div><span>20</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>284 个字体族（这台机器装了很多字体）对 20 个。<code>--ignore-system-fonts</code> 把系统字体全部忽略，只剩内置字体加上 <code>TYPST_FONT_PATHS</code> 里的目录，这是我给整个系列设的规矩：所有 <code>make</code> 目标都带这个选项，这样同一份文档在你的机器上和在我的机器上用的是同一组字体，不会因为我多装了几个字体而让输出悄悄不同。</p><p>这 20 个是：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst fonts --ignore-system-fonts</span></div></div><div><div><div>2</div></div><div><span>DejaVu Sans Mono</span></div></div><div><div><div>3</div></div><div><span>Libertinus Serif</span></div></div><div><div><div>4</div></div><div><span>Maple Mono NF</span></div></div><div><div><div>5</div></div><div><span>New Computer Modern</span></div></div><div><div><div>6</div></div><div><span>New Computer Modern Math</span></div></div><div><div><div>7</div></div><div><span>Noto Sans CJK HK</span></div></div><div><div><div>8</div></div><div><span>Noto Sans CJK JP</span></div></div><div><div><div>9</div></div><div><span>Noto Sans CJK KR</span></div></div><div><div><div>10</div></div><div><span>Noto Sans CJK SC</span></div></div><div><div><div>11</div></div><div><span>Noto Sans CJK TC</span></div></div><div><div><div>12</div></div><div><span>Noto Sans Mono CJK HK</span></div></div><div><div><div>13</div></div><div><span>Noto Sans Mono CJK JP</span></div></div><div><div><div>14</div></div><div><span>Noto Sans Mono CJK KR</span></div></div><div><div><div>15</div></div><div><span>Noto Sans Mono CJK SC</span></div></div><div><div><div>16</div></div><div><span>Noto Sans Mono CJK TC</span></div></div><div><div><div>17</div></div><div><span>Noto Serif CJK HK</span></div></div><div><div><div>18</div></div><div><span>Noto Serif CJK JP</span></div></div><div><div><div>19</div></div><div><span>Noto Serif CJK KR</span></div></div><div><div><div>20</div></div><div><span>Noto Serif CJK SC</span></div></div><div><div><div>21</div></div><div><span>Noto Serif CJK TC</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>前面四个（DejaVu Sans Mono、Libertinus Serif、New Computer Modern 与它的 Math）是 Typst 内置的，不需要安装；Noto 的 15 个和 Maple Mono 来自 flake 设置的 <code>TYPST_FONT_PATHS</code>。我把环境变量拿掉，看每个选项单独的效果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ env -u TYPST_FONT_PATHS typst fonts --ignore-system-fonts | wc -l</span></div></div><div><div><div>2</div></div><div><span>4</span></div></div><div><div><div>3</div></div><div><span>$ env -u TYPST_FONT_PATHS typst fonts --ignore-system-fonts --ignore-embedded-fonts | wc -l</span></div></div><div><div><div>4</div></div><div><span>0</span></div></div><div><div><div>5</div></div><div><span>$ SER=$(echo $TYPST_FONT_PATHS | tr ':' '\n' | grep serif)</span></div></div><div><div><div>6</div></div><div><span>$ env -u TYPST_FONT_PATHS typst fonts --ignore-system-fonts --font-path "$SER" | wc -l</span></div></div><div><div><div>7</div></div><div><span>9</span></div></div><div><div><div>8</div></div><div><span>$ env -u TYPST_FONT_PATHS typst fonts --ignore-system-fonts --ignore-embedded-fonts --font-path "$SER" | wc -l</span></div></div><div><div><div>9</div></div><div><span>5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>没有任何额外路径、忽略系统字体：只剩 4 个内置的。</li>
<li>再加 <code>--ignore-embedded-fonts</code>：连内置字体也扔掉，0 个。</li>
<li><code>--font-path</code> 指向 Noto Serif CJK 的目录（递归搜索）：4 + 5 = 9 个。</li>
<li>同时忽略内置字体：只剩那 5 个 Noto Serif CJK（HK、JP、KR、SC、TC）。</li>
</ul><p><code>--font-path</code> 用系统的路径分隔符（Linux 上是冒号）接受多个目录，环境变量 <code>TYPST_FONT_PATHS</code> 是它的等价写法，这就是 flake 里那一长串路径的来源。<code>--ignore-embedded-fonts</code> 平常用不到；上面的实验里它把内置的 4 个字体族全部去掉了，所以需要“文档绝不能依赖内置字体”的时候可以打开它。</p><p>字体的真正学问，比如为什么 <code>NotoSerifCJKsc-ExtraLight</code> 这个名字会出现在 PDF 里，还有字体回退列表，我们留给第 5 篇。</p></section><section><h3><code>--root</code>：谁是项目的根<a href="#--root谁是项目的根"><span>#</span></a></h3><p><code>import</code> 和 <code>include</code> 里以 <code>/</code> 开头的路径，相对于“项目根”，而根默认是输入文件所在的目录。我在 <code>/tmp/t01/proj</code> 里放了两个文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>proj/</span></div></div><div><div><div>2</div></div><div><span>├── common.typ</span></div></div><div><div><div>3</div></div><div><span>└── sub/</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>└── page.typ</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>子目录里的页面。</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"/common.typ"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先按默认编译 <code>sub/page.typ</code>，根是 <code>sub/</code>，找不到 <code>/common.typ</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile sub/page.typ</span></div></div><div><div><div>2</div></div><div><span>error: file not found (searched at /tmp/t01/proj/sub/common.typ)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>┌─ sub/page.typ:3:9</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>5</div></div><div><span>3 │ #include "/common.typ"</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│          ^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>指定根目录之后，同一个文件编译通过：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ typst compile --root . sub/page.typ</span></div></div><div><div><div>2</div></div><div><span>$ pdftotext sub/page.pdf -</span></div></div><div><div><div>3</div></div><div><span>子目录里的页面。</span></div></div><div><div><div>4</div></div><div><span>共用的页脚。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这也是仓库所有 <code>make</code> 目标都带 <code>--root .</code> 的原因：文档放在 <code>lab/ch12/modules/</code> 之类的子目录里，但它们都想用从仓库根算起的路径。<code>--root</code> 同时还限定了可读文件的范围：我把根设成 <code>sub</code>，让 <code>sub/o.typ</code> 去 <code>#include "../common.typ"</code>，得到</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: path `"../common.typ"` would escape the project root</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>┌─ sub/o.typ:1:9</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>4</div></div><div><span>1 │ #include "../common.typ"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>│          ^^^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= hint: cannot access files outside of the project sandbox</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= hint: you can adjust the project root with the `--root` argument</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第 12 篇讲模块的时候会再回来。</p></section></section>
<section><h2>跑一遍 <code>make ch01</code><a href="#跑一遍-make-ch01"><span>#</span></a></h2><p>四个选项加上 <code>typst compile</code> 的 <code>--pages</code>、<code>--format</code>、<code>--ppi</code>，就是 <code>lab/ch01/extra.sh</code> 做的事：它读 <code>examples/*/main.typ</code>，把每份成品的第一页渲染成 72 dpi 的 PNG，也就是本文开头的四张图。命令是 <code>make ch01</code>，它会先 <code>make examples</code>（编译全部成品）。第一次跑要联网：<code>examples/ecosystem</code> 会从 Typst Universe 下载 <code>@preview</code> 包（第 14 篇的内容），之后走缓存。然后它编译 <code>lab/ch01/</code> 里的每个文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make ch01</span></div></div><div><div><div>2</div></div><div><span>typst compile examples/cv/main.typ</span></div></div><div><div><div>3</div></div><div><span>typst compile examples/ecosystem/main.typ</span></div></div><div><div><div>4</div></div><div><span>typst compile examples/homework/main.typ</span></div></div><div><div><div>5</div></div><div><span>typst compile examples/notes/main.typ</span></div></div><div><div><div>6</div></div><div><span>typst compile examples/slides/main.typ</span></div></div><div><div><div>7</div></div><div><span>./scripts/chapter.sh ch01</span></div></div><div><div><div>8</div></div><div><span>ok   lab/ch01/fig-markup.typ</span></div></div><div><div><div>9</div></div><div><span>ok   lab/ch01/hello.typ</span></div></div><div><div><div>10</div></div><div><span>ok   lab/ch01/markup-tour.typ</span></div></div><div><div><div>11</div></div><div><span>ok   lab/ch01/unclosed.fail.typ (failed as expected)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>五份成品的页数（<code>pdfinfo</code>）：笔记 4 页，作业 1 页（<code>homework-questions.pdf</code> 同样 1 页），简历 1 页（中文和英文版各一页），幻灯片 17 页，<code>ecosystem</code> 1 页（第 14 篇的）。其中 <code>fig-markup.typ</code> 把一段标记源码和它的渲染结果并排放在一张图里：</p><p></p><figure><img src="/assets/images/typst-by-xuan/ch01-markup.png" alt="左边是标记语法的源码，右边是同一段源码被 eval 之后的渲染结果：标题、加粗、强调、行内代码、列表、编号、术语" /><figcaption>左边是标记语法的源码，右边是同一段源码被 eval 之后的渲染结果：标题、加粗、强调、行内代码、列表、编号、术语</figcaption></figure><p></p><p>它的源码有 12 行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#set</span><span> </span><span>page</span><span>(</span><span>width</span><span>: </span><span>auto</span><span>, </span><span>height</span><span>: </span><span>auto</span><span>, </span><span>margin</span><span>: </span><span>12pt</span><span>)</span></div></div><div><div><div>2</div></div><div><span>#set</span><span> </span><span>text</span><span>(</span><span>font</span><span>: (</span><span>"Libertinus Serif"</span><span>, </span><span>"Noto Serif CJK SC"</span><span>), </span><span>lang</span><span>: </span><span>"zh"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#let</span><span> </span><span>src</span><span><span> </span><span>= </span></span><span>"= 标题</span><span>\n</span><span>*粗体* _斜体_ `code`</span><span>\n</span><span>- 项一</span><span>\n</span><span>+ 编号一</span><span>\n</span><span>/ 术语: 说明"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#grid</span><span>(</span></div></div><div><div><div>7</div></div><div><span>  </span><span>columns</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>gutter</span><span>: </span><span>1.5em</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>align</span><span>: </span><span>horizon</span><span>,</span></div></div><div><div><div>10</div></div><div><span>  </span><span>raw</span><span>(</span><span>src</span><span>, </span><span>lang</span><span>: </span><span>"typ"</span><span>, </span><span>block</span><span>: </span><span>true</span><span>),</span></div></div><div><div><div>11</div></div><div><span>  </span><span>block</span><span>(</span><span>width</span><span>: </span><span>6cm</span><span>, </span><span>eval</span><span>(</span><span>src</span><span>, </span><span>mode</span><span>: </span><span>"markup"</span><span>)),</span></div></div><div><div><div>12</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（这里用到了 <code>eval(src, mode: "markup")</code>：把一个字符串当 Typst 标记来编译。第 11 篇我们会用它让 YAML 里的字符串带格式。左边源码里的 <code>*粗体*</code>、<code>_斜体_</code> 被上了色，是 <code>raw(..., lang: "typ")</code> 的效果，我猜 <code>typ</code> 指的是 Typst 标记的高亮，没有去查。<code>#grid</code>、<code>#set</code> 都是后面几篇的内容，这里只需要看效果。）</p></section>
<section><h2>接下来<a href="#接下来"><span>#</span></a></h2><p>下一篇我们进入代码模式的内部：<code>#let w = 3</code> 里的 <code>3</code> 是什么类型，两个汉字的字符串为什么 <code>len()</code> 是 6 而 <code>clusters().len()</code> 是 2，<code>if</code>、<code>for</code> 的结果为什么能直接拼进文档。<code>modes.typ</code> 里那个“代码块把每条语句拼起来”的行为，到那时会有一张完整的规则表。</p><p>在那之前，你可以先把 <code>watch</code> 开着，自己试三件事：把 <code>markup-tour.typ</code> 里的 <code>@tour</code> 换成一个不存在的标签看错误信息；把 <code>-- ---</code> 换成 <code>----</code> 看它变成什么；把 <code>#set page(width: 14cm ...)</code> 删掉，看 Typst 的默认页面长什么样。错误信息的格式一旦认识，后面十五篇里的所有 <code>*.fail.typ</code> 你都读得懂。</p><p>下一篇：<a href="/posts/typst-by-xuan-02-code-mode/">代码模式：值、类型与控制流</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-34-final-review/</id>
      <title type="text">18.06 第 34 讲：期末复习</title>
      <published>2026-10-18T11:20:00.000Z</published>
      <updated>2026-10-18T11:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-34-final-review/"/>
      <summary type="text">MIT 18.06 第 34 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 Strang 期末复习课上的每道题都算了一遍，Markov 矩阵从 (0, 10, 0) 出发迭代 10 步，离稳态 (3, 3, 4) 只剩 7.24e-07。</summary>
      <content type="html"><![CDATA[<p>最后一讲没有新内容，Strang 的做法是翻出老试卷，把题一道一道念出来，让全班一起想。我把每道题都交给 NumPy 过了一遍：一个 3 × 2 矩阵同时满足“一个右端项无解、另一个恰好一个解”，<span><span>det⁡(ATA)=1\det(A^\mathsf{T}A) = 1</span><span><span><span></span><span>det</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 而 <span><span>det⁡(AAT)=0\det(AA^\mathsf{T}) = 0</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；Markov 矩阵从 <span><span>(0,10,0)(0, 10, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 出发迭代 10 步，离稳态 <span><span>(3,3,4)(3, 3, 4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 只剩 <span><span>7.24×10−77.24 \times 10^{-7}</span><span><span><span></span><span>7.24</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=RWvi4Vx4CDc" target="_blank">Lecture 34: Final course review</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-34-final-course-review/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 全书第 1 到 8 章
前置：<a href="/posts/linear-algebra-33-pseudoinverse/">第 33 讲：左逆、右逆与伪逆</a>
一句话：期末题考的几乎都是同一件事：看到 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>nn</span><span><span><span></span><span>n</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 就想到四个子空间，看到解的个数就想到零空间。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几句是考试安排，可以跳过。第一道题（3 × <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 矩阵，一个右端项无解、一个有唯一解）值得认真看，他一步步把 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>nn</span><span><span><span></span><span>n</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 的关系推出来。</li>
<li>求 Markov 矩阵稳态向量的那一段，他在黑板上猜 <span><span>(3,3,4)(3, 3, 4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，猜之前冷了一会儿场，可以倍速。“总人数不变所以 <span><span>c2=1c_2 = 1</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>”这一句要听。</li>
<li>2 × 2 矩阵的快问快答和最后的最小二乘，本篇都已经算过，倍速即可。</li>
<li>结尾他向全班道谢。这是整门课的最后几句话，我觉得值得原速看完。</li>
</ul></div></div>
<section><h2>一个矩阵，三个数<a href="#一个矩阵三个数"><span>#</span></a></h2><p><strong>他的第一道题：</strong> 一个 <span><span>3×n3 \times n</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，已知 <span><span>Ax=(1,0,0)A\mathbf{x} = (1, 0, 0)</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 无解，<span><span>Ax=(0,1,0)A\mathbf{x} = (0, 1, 0)</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 恰好有一个解。<span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>nn</span><span><span><span></span><span>n</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 能说什么？</p><p>推理只有两步。有的右端项无解，说明不是每一行都有主元，所以 <span><span>r&lt;mr &lt; m</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span>，而 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 本来就是 3。有解时只有一个解，说明零空间里只有零向量，所以 <span><span>r=nr = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>，列无关。合起来是 <span><span>r=n&lt;m=3r = n &lt; m = 3</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>。</p><p>他要一个例子。我猜他心里想的是最省事的那个：只有一列 <span><span>(0,1,0)(0, 1, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。他也允许加第二列，我取 <span><span>(0,0,1)(0, 0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，这样 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 恰好是单位阵，后面的判断题好查：</p><span><span><span>A=[001001]A = \begin{bmatrix} 0 &amp; 0 \\ 1 &amp; 0 \\ 0 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>接着是三道判断题：<span><span>det⁡(ATA)=det⁡(AAT)\det(A^\mathsf{T}A) = \det(AA^\mathsf{T})</span><span><span><span></span><span>det</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span>(</span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span></span></span></span>；<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆；<span><span>AATAA^\mathsf{T}</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 正定。最后他还要求证明 <span><span>ATy=cA^\mathsf{T}\mathbf{y} = \mathbf{c}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span></span></span></span> 对每个 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span> 都有解，而且有无穷多个解。下面一次全算：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 3 x 2 的例子：列空间里有 (0, 1, 0) 和 (0, 0, 1)，没有 (1, 0, 0)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>e1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>e2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"m, n, r ="</span><span>, m, n, r)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># Ax = (1, 0, 0) 没有解：最小二乘的残差不为 0</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> name, rhs </span><span>in</span><span> [(</span><span>"(1, 0, 0)"</span><span>, e1), (</span><span>"(0, 1, 0)"</span><span>, e2)]:</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>x, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, rhs, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>18</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>19</div></div><div><span>            </span><span>f</span><span>"Ax = </span><span>{</span><span>name</span><span>}</span><span>：x = </span><span>{</span><span>x</span><span>}</span><span>，残差 = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> x </span><span>-</span><span> rhs)</span></span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 三个判断题</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>AtA </span><span>=</span><span> A.T </span><span>@</span><span> A</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>AAt </span><span>=</span><span> A </span><span>@</span><span> A.T</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"AtA ="</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(AtA)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(AtA) ="</span><span><span>, np.linalg.</span><span>det</span><span>(AtA), </span></span><span>"，det(AAt) ="</span><span><span>, np.linalg.</span><span>det</span><span>(AAt))</span></span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"AAt 的特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(AAt))</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"AAt 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(AAt))</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span># A^T y = c 对每个 c 有解，而且解不唯一：左零空间的维数是 m - r</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span><span>, </span><span>-</span></span><span>5.0</span><span>])</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>y, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A.T, c, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T y = c 的一个解 y ="</span><span>, y, </span><span>"，残差 ="</span><span><span>, np.linalg.</span><span>norm</span><span>(A.T </span><span>@</span><span> y </span><span>-</span><span> c))</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"左零空间的维数 m - r ="</span><span><span>, m </span><span>-</span><span> r)</span></span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>y_other </span><span>=</span><span> y </span><span>+</span><span> </span><span>7.0</span><span><span> </span><span>*</span><span> e1</span></span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>38</div></div><div><span>        </span><span>"加上左零空间里的 7 * (1, 0, 0) 之后仍然是解："</span><span>,</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(A.T </span><span>@</span><span> y_other, c),</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m, n, r = 3 2 2</span></div></div><div><div><div>2</div></div><div><span>Ax = (1, 0, 0)：x = [0. 0.]，残差 = 1.0000</span></div></div><div><div><div>3</div></div><div><span>Ax = (0, 1, 0)：x = [1. 0.]，残差 = 0.0000</span></div></div><div><div><div>4</div></div><div><span>AtA =</span></div></div><div><div><div>5</div></div><div><span>[[1. 0.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[0. 1.]]</span></div></div><div><div><div>7</div></div><div><span>det(AtA) = 1.0 ，det(AAt) = 0.0</span></div></div><div><div><div>8</div></div><div><span>AAt 的特征值： [0. 1. 1.]</span></div></div><div><div><div>9</div></div><div><span>AAt 的秩： 2</span></div></div><div><div><div>10</div></div><div><span>A^T y = c 的一个解 y = [ 0.  2. -5.] ，残差 = 0.0</span></div></div><div><div><div>11</div></div><div><span>左零空间的维数 m - r = 1</span></div></div><div><div><div>12</div></div><div><span>加上左零空间里的 7 * (1, 0, 0) 之后仍然是解： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出对着题目一条条读。</p><ul>
<li><span><span>r=n=2&lt;m=3r = n = 2 &lt; m = 3</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，和推理一致；<span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 的最小二乘残差是 1.0000，没有精确解，<span><span>(0,1,0)(0, 1, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 的残差是 0.0000。</li>
<li><span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 是单位阵，可逆（列无关），为真。</li>
<li><span><span>det⁡(ATA)=1.0\det(A^\mathsf{T}A) = 1.0</span><span><span><span></span><span>det</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1.0</span></span></span></span>，<span><span>det⁡(AAT)=0.0\det(AA^\mathsf{T}) = 0.0</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0</span></span></span></span>，所以第一条为假。陷阱在于 <span><span>det⁡(AB)=det⁡(BA)\det(AB) = \det(BA)</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span>B</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span>(</span><span>B</span><span>A</span><span>)</span></span></span></span> 只对两个方阵成立：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是 3 × 2 时，<span><span>AATAA^\mathsf{T}</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是 3 × 3、秩只有 2。</li>
<li><span><span>AATAA^\mathsf{T}</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的特征值是 <span><span>0,1,10, 1, 1</span><span><span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span></span></span></span>，有 0，所以不是正定，只是半正定。他说的“第一行一定全是 0”在这个例子里就是特征值 0 的来源。</li>
<li><span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是 <span><span>n×mn \times m</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span>，行无关（<span><span>r=nr = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>），所以 <span><span>ATy=cA^\mathsf{T}\mathbf{y} = \mathbf{c}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span></span></span></span> 总有解；左零空间的维数是 <span><span>m−r=1&gt;0m - r = 1 &gt; 0</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以解有无穷多个。代码里给解加上 <span><span>7(1,0,0)7(1, 0, 0)</span><span><span><span></span><span>7</span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，仍然满足方程。</li>
</ul><p>整道题没有一步用到消元，全靠 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 和四个子空间的维数（这套账 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 在<a href="/posts/linear-algebra-10-four-fundamental-subspaces/">第 10 讲</a>里算过）。</p></section>
<section><h2>三个列向量能说明什么<a href="#三个列向量能说明什么"><span>#</span></a></h2><p><strong>第二道题：</strong> <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 有三列 <span><span>v1,v2,v3\mathbf{v}_1, \mathbf{v}_2, \mathbf{v}_3</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。(a) 解 <span><span>Ax=v1−v2+v3A\mathbf{x} = \mathbf{v}_1 - \mathbf{v}_2 + \mathbf{v}_3</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>；(b) 如果这个组合等于零，解唯一吗；(c) 如果三个向量标准正交，<span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的哪个组合最接近 <span><span>v3\mathbf{v}_3</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p><p>(a) 是第一讲的“按列乘”，<span><span>x=(1,−1,1)\mathbf{x} = (1, -1, 1)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。(b) 组合为零，说明这个 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 在零空间里，解永远不唯一，因为总能把它加到任何解上。(c) 想象 <span><span>v1,v2,v3\mathbf{v}_1, \mathbf{v}_2, \mathbf{v}_3</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>、<span><span>yy</span><span><span><span></span><span>y</span></span></span></span>、<span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 轴，<span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 轴到 <span><span>xyxy</span><span><span><span></span><span>x</span><span>y</span></span></span></span> 平面的最近点就是原点，答案是 0 份和 0 份。</p><p>我用固定种子的随机向量验证三问，(c) 用 <code>np.linalg.qr</code> 造一组标准正交的列：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># (a) A x = v1 - v2 + v3，按列乘</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>V </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"(a) A @ x 等于 v1 - v2 + v3："</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(V </span><span>@</span><span> x, V[:, </span><span>0</span><span><span>] </span><span>-</span><span> V[:, </span></span><span>1</span><span><span>] </span><span>+</span><span> V[:, </span></span><span>2</span><span>]),</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># (b) 如果 v1 - v2 + v3 = 0，则 (1, -1, 1) 在零空间里，解不唯一</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> V.</span><span>copy</span><span>()</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>W[:, </span><span>2</span><span><span>] </span><span>=</span><span> W[:, </span></span><span>1</span><span><span>] </span><span>-</span><span> W[:, </span></span><span>0</span><span>]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"(b) W @ x ="</span><span><span>, W </span><span>@</span><span> x)</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"    W 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(W), </span></span><span>"&lt; 3，零空间不只有 0"</span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># (c) 如果 v1, v2, v3 标准正交，v3 在 v1, v2 平面上的最近点是 0</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>Q, _ </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>3</span><span>)))</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>coef, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(Q[:, :</span><span>2</span><span>], Q[:, </span><span>2</span><span>], </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>26</div></div><div><span>        </span><span>"(c) Q^T Q 与单位阵的距离："</span><span>,</span></div></div><div><div><div>27</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q.T </span><span>@</span><span> Q </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>))</span><span>:.1e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"    最近组合的系数 ="</span><span>,</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>np.</span><span>round</span><span>(coef, </span><span>6</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>,</span></div></div><div><div><div>32</div></div><div><span>        </span><span>"，误差 ="</span><span>,</span></div></div><div><div><div>33</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q[:, :</span></span><span>2</span><span><span>] </span><span>@</span><span> coef </span><span>-</span><span> Q[:, </span></span><span>2</span><span>])</span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(a) A @ x 等于 v1 - v2 + v3： True</span></div></div><div><div><div>2</div></div><div><span>(b) W @ x = [0. 0. 0. 0.]</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>W 的秩： 2 &lt; 3，零空间不只有 0</span></div></div><div><div><div>4</div></div><div><span>(c) Q^T Q 与单位阵的距离： 3.3e-16</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>最近组合的系数 = [0. 0.] ，误差 = 1.0000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>(b) 里 <span><span>WxW\mathbf{x}</span><span><span><span></span><span>W</span><span>x</span></span></span></span> 是零向量，秩是 2，小于 3。(c) 里 <span><span>QTQQ^\mathsf{T}Q</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span></span></span></span> 离单位阵只差 <span><span>3.3×10−163.3 \times 10^{-16}</span><span><span><span></span><span>3.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，最近组合的系数是两个 0，误差 1.0000，恰好是 <span><span>v3\mathbf{v}_3</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 自己的长度。标准正交的好处就在这里：投影时各个方向互不干扰，不用解正规方程。</p></section>
<section><h2>Markov 矩阵与稳态<a href="#markov-矩阵与稳态"><span>#</span></a></h2><p><strong>第三道题：</strong> 给一个 Markov 矩阵，问特征值，再问从 <span><span>u0=(0,10,0)\mathbf{u}_0 = (0, 10, 0)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 出发，<span><span>Aku0A^k\mathbf{u}_0</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 趋向哪里。</p><p>字幕是语音转写，矩阵的数字被听写得一团糟。我按他念的性质还原：每列和为 1、第一列加第二列等于两倍第三列、迹是 0.8、<span><span>(3,3,4)(3, 3, 4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 是 <span><span>λ=1\lambda = 1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的特征向量，得到</p><span><span><span>A=[0.20.40.30.40.20.30.40.40.4]A = \begin{bmatrix} 0.2 &amp; 0.4 &amp; 0.3 \\ 0.4 &amp; 0.2 &amp; 0.3 \\ 0.4 &amp; 0.4 &amp; 0.4 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0.2</span></span></span><span><span></span><span><span>0.4</span></span></span><span><span></span><span><span>0.4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0.4</span></span></span><span><span></span><span><span>0.2</span></span></span><span><span></span><span><span>0.4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0.3</span></span></span><span><span></span><span><span>0.3</span></span></span><span><span></span><span><span>0.4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>这个矩阵同时满足上面全部四条，我认为就是黑板上的那个，不过这是还原，不是抄录。他的推理顺序是：列有线性关系，所以奇异，有一个特征值 0；Markov 矩阵必有特征值 1；迹是 0.8，所以第三个是 <span><span>0.8−0−1=−0.20.8 - 0 - 1 = -0.2</span><span><span><span></span><span>0.8</span><span></span><span>−</span><span></span></span><span><span></span><span>0</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.2</span></span></span></span>。稳态只剩 <span><span>c2x2c_2\mathbf{x}_2</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>x2\mathbf{x}_2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>λ=1\lambda = 1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的特征向量，<span><span>c2c_2</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 靠“总人数不变”：起点总共 10 人，所以 <span><span>c2=1c_2 = 1</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，<span><span>u∞=(3,3,4)\mathbf{u}_\infty = (3, 3, 4)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.2</span><span>, </span><span>0.4</span><span>, </span><span>0.3</span><span>], [</span><span>0.4</span><span>, </span><span>0.2</span><span>, </span><span>0.3</span><span>], [</span><span>0.4</span><span>, </span><span>0.4</span><span>, </span><span>0.4</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>u0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>10.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"列和："</span><span><span>, A.</span><span>sum</span><span>(</span></span><span>axis</span><span>=</span><span>0</span><span>))</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"第一列加第二列等于两倍第三列："</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(A[:, </span><span>0</span><span><span>] </span><span>+</span><span> A[:, </span></span><span>1</span><span>], </span><span>2</span><span><span> </span><span>*</span><span> A[:, </span></span><span>2</span><span>]),</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹："</span><span><span>, np.</span><span>trace</span><span>(A), </span></span><span>"，det："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(A)</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A).real))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 稳态：lambda = 1 的特征向量，按总人数 10 缩放</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>lam, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> S[:, np.</span><span>argmin</span><span>(np.</span><span>abs</span><span>(lam </span><span>-</span><span> </span><span>1.0</span><span>))].real</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>steady </span><span>=</span><span> </span><span>10.0</span><span><span> </span><span>*</span><span> x </span><span>/</span><span> x.</span><span>sum</span><span>()</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>22</div></div><div><span>        </span><span>"稳态 u_inf ="</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>steady,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>"，A @ u_inf 不变："</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(A </span><span>@</span><span> steady, steady),</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 误差按 (-0.2)^k 衰减</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> u0.</span><span>copy</span><span>()</span></div></div><div><div><div>30</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>11</span><span>):</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> A </span><span>@</span><span> u</span></div></div><div><div><div>32</div></div><div><span>        </span><span>if</span><span> k </span><span>in</span><span> (</span><span>1</span><span>, </span><span>2</span><span>, </span><span>5</span><span>, </span><span>10</span><span>):</span></div></div><div><div><div>33</div></div><div><span>            </span><span>print</span><span>(</span></div></div><div><div><div>34</div></div><div><span>                </span><span>f</span><span>"k = </span><span>{</span><span>k</span><span>:2d</span><span>}</span><span>：u = </span><span>{</span><span>u</span><span>}</span><span>，到稳态的距离 ="</span><span>,</span></div></div><div><div><div>35</div></div><div><span>                </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(u </span><span>-</span><span> steady)</span></span><span>:.2e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>列和： [1. 1. 1.]</span></div></div><div><div><div>2</div></div><div><span>第一列加第二列等于两倍第三列： True</span></div></div><div><div><div>3</div></div><div><span>迹： 0.8 ，det： -6.7e-18</span></div></div><div><div><div>4</div></div><div><span>特征值： [-0.2  0.   1. ]</span></div></div><div><div><div>5</div></div><div><span>稳态 u_inf = [3. 3. 4.] ，A @ u_inf 不变： True</span></div></div><div><div><div>6</div></div><div><span>k =  1：u = [4. 2. 4.]，到稳态的距离 = 1.41e+00</span></div></div><div><div><div>7</div></div><div><span>k =  2：u = [2.8 3.2 4. ]，到稳态的距离 = 2.83e-01</span></div></div><div><div><div>8</div></div><div><span>k =  5：u = [3.0016 2.9984 4.    ]，到稳态的距离 = 2.26e-03</span></div></div><div><div><div>9</div></div><div><span>k = 10：u = [3. 3. 4.]，到稳态的距离 = 7.24e-07</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>特征值是 <span><span>−0.2,0,1-0.2, 0, 1</span><span><span><span></span><span>−</span><span>0.2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span></span></span></span>，稳态正好是 <span><span>(3,3,4)(3, 3, 4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，每一步误差大约缩小到原来的 <span><span>0.20.2</span><span><span><span></span><span>0.2</span></span></span></span> 倍：<span><span>k=5k = 5</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 时离稳态 <span><span>2.26×10−32.26 \times 10^{-3}</span><span><span><span></span><span>2.26</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>k=10k = 10</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时 <span><span>7.24×10−77.24 \times 10^{-7}</span><span><span><span></span><span>7.24</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，从第 1 步到第 10 步缩小了约 <span><span>2×1062 \times 10^6</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 倍。行列式打印出 <span><span>−6.7×10−18-6.7 \times 10^{-18}</span><span><span><span></span><span>−</span><span>6.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>18</span></span></span></span></span></span></span></span></span></span></span></span>，那是浮点误差，真值是 0。</p></section>
<section><h2>2 × 2 快问快答<a href="#2--2-快问快答"><span>#</span></a></h2><p><strong>第四组：</strong> 他念性质，全班给 2 × 2 的例子，没有就说没有。</p><ul>
<li>投影到过 <span><span>a=(4,−3)\mathbf{a} = (4, -3)</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>−</span><span>3</span><span>)</span></span></span></span> 的直线：<span><span>P=aaT/aTaP = \mathbf{a}\mathbf{a}^\mathsf{T} / \mathbf{a}^\mathsf{T}\mathbf{a}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span></span>，分母是个数，直接除。</li>
<li>特征值 0 和 3，特征向量 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 和 <span><span>(2,1)(2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>：<span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>。</li>
<li>不能写成 <span><span>BTBB^\mathsf{T}B</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span></span></span></span> 的实矩阵：<span><span>BTBB^\mathsf{T}B</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span></span></span></span> 总是对称的，所以任意一个不对称矩阵都行。</li>
<li>特征向量正交但不对称：反对称矩阵或正交（旋转）矩阵，特征向量是复的，但在复内积下正交。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 1. 投影到过 a = (4, -3) 的直线</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span><span>], [</span><span>-</span></span><span>3.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> a </span><span>@</span><span> a.T </span><span>/</span><span> (a.T </span><span>@</span><span> a)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"P @ P == P："</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P),</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"，P.T == P："</span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(P.T, P),</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 2. 特征值 0 与 3，特征向量 (1, 2) 与 (2, 1)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>Lam </span><span>=</span><span> np.</span><span>diag</span><span>([</span><span>0.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> S </span><span>@</span><span> Lam </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"A = S Lam S^-1 ="</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(A)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A).real))</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 3. 不对称的矩阵不可能写成 B^T B</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"N 对称吗："</span><span><span>, np.</span><span>allclose</span><span>(N, N.T))</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span># 4. 反对称与正交矩阵：特征向量复正交，但矩阵不对称</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> </span><span>0.7</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>K </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>[[np.</span><span>cos</span><span>(theta), np.</span><span>sin</span><span>(theta)], [</span><span>-</span><span>np.</span><span>sin</span><span>(theta), np.</span><span>cos</span><span>(theta)]]</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>for</span><span> name, M </span><span>in</span><span> [(</span><span>"反对称 K"</span><span>, K), (</span><span>"旋转 Q"</span><span>, Q)]:</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>lam, V </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(M)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>inner </span><span>=</span><span> np.</span><span>vdot</span><span>(V[:, </span><span>0</span><span>], V[:, </span><span>1</span><span>])</span></div></div><div><div><div>40</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>name,</span></div></div><div><div><div>42</div></div><div><span>            </span><span>"：特征值"</span><span>,</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>lam,</span></div></div><div><div><div>44</div></div><div><span>            </span><span>"，复内积 ="</span><span>,</span></div></div><div><div><div>45</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>abs</span><span>(inner)</span><span>:.1e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>46</div></div><div><span>            </span><span>"，对称："</span><span>,</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>np.</span><span>allclose</span><span>(M, M.T),</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P =</span></div></div><div><div><div>2</div></div><div><span>[[ 0.64 -0.48]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[-0.48  0.36]]</span></div></div><div><div><div>4</div></div><div><span>P @ P == P： True ，P.T == P： True</span></div></div><div><div><div>5</div></div><div><span>A = S Lam S^-1 =</span></div></div><div><div><div>6</div></div><div><span>[[ 4. -2.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ 2. -1.]]</span></div></div><div><div><div>8</div></div><div><span>特征值： [0. 3.]</span></div></div><div><div><div>9</div></div><div><span>N 对称吗： False</span></div></div><div><div><div>10</div></div><div><span>反对称 K ：特征值 [0.+1.j 0.-1.j] ，复内积 = 0.0e+00 ，对称： False</span></div></div><div><div><div>11</div></div><div><span>旋转 Q ：特征值 [0.7648+0.6442j 0.7648-0.6442j] ，复内积 = 0.0e+00 ，对称： False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 满足 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span> 与 <span><span>PT=PP^\mathsf{T} = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>。<span><span>SΛS−1S\Lambda S^{-1}</span><span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 给出 <span><span>[4−22−1]\begin{bmatrix} 4 &amp; -2 \\ 2 &amp; -1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，特征值回到 0 和 3。最后一条里，反对称矩阵的特征值是 <span><span>±i\pm i</span><span><span><span></span><span>±</span><span>i</span></span></span></span>，旋转 0.7 弧度的矩阵是 <span><span>cos⁡0.7±isin⁡0.7\cos 0.7 \pm i \sin 0.7</span><span><span><span></span><span>cos</span><span></span><span>0.7</span><span></span><span>±</span><span></span></span><span><span></span><span>i</span><span></span><span>sin</span><span></span><span>0.7</span></span></span></span>，两者的特征向量复内积都是 0，矩阵却都不对称。复内积我用 <code>np.vdot</code> 算，它会自动取共轭，这是<a href="/posts/linear-algebra-26-complex-matrices-fft/">第 26 讲</a>的内容。</p></section>
<section><h2>最小二乘：给答案，找问题<a href="#最小二乘给答案找问题"><span>#</span></a></h2><p><strong>最后一道：</strong> 他说像 Jeopardy 节目，先给答案再问问题。系统是</p><span><span><span>[101112][CD]=[341]\begin{bmatrix} 1 &amp; 0 \\ 1 &amp; 1 \\ 1 &amp; 2 \end{bmatrix}
\begin{bmatrix} C \\ D \end{bmatrix}
= \begin{bmatrix} 3 \\ 4 \\ 1 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>C</span></span></span><span><span></span><span><span>D</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>最小二乘解是 <span><span>(C^,D^)=(11/3,−1)(\hat{C}, \hat{D}) = (11/3, -1)</span><span><span><span></span><span>(</span><span><span><span><span><span><span></span><span>C</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>,</span><span></span><span><span><span><span><span><span></span><span>D</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>11/3</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>。问题有三个：<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在列空间上的投影 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span> 是什么；对应的直线拟合问题画出来是什么样；再找一个不全为零的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，让最小二乘解变成零。</p><p>答案依次是：<span><span>p=113 列一−列二\mathbf{p} = \frac{11}{3}\,\text{列一} - \text{列二}</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>列一</span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>列二</span></span></span></span></span>，最小二乘找的就是“列的最近组合”；直线在 <span><span>t=0,1,2t = 0, 1, 2</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span></span></span></span> 处拟合高度 <span><span>3,4,13, 4, 1</span><span><span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>1</span></span></span></span>，截距 <span><span>11/311/3</span><span><span><span></span><span>11/3</span></span></span></span>、斜率 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>；最后，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 要和两列都正交，比如 <span><span>(1,−2,1)(1, -2, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>3.0</span><span>, </span><span>4.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># 正规方程与 lstsq</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>x_hat </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A.T </span><span>@</span><span> A, A.T </span><span>@</span><span> b)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>x_lstsq, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"x_hat ="</span><span>, x_hat, </span><span>"，11/3 ="</span><span>, </span><span>11</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"两种解法一致："</span><span><span>, np.</span><span>allclose</span><span>(x_hat, x_lstsq))</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># 投影 p = A x_hat：11/3 倍列一，减一倍列二</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> A </span><span>@</span><span> x_hat</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"p ="</span><span>, p)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"p = 11/3 * 列一 - 列二："</span><span><span>, np.</span><span>allclose</span><span>(p, </span></span><span>11</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span><span> </span><span>*</span><span> A[:, </span></span><span>0</span><span><span>] </span><span>-</span><span> A[:, </span></span><span>1</span><span>])</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"误差 b - p 与两列正交："</span><span><span>, np.</span><span>allclose</span><span>(A.T </span><span>@</span><span> (b </span><span>-</span><span> p), </span></span><span>0</span><span>))</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>23</div></div><div><span>        </span><span>"||b|| ="</span><span>,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(b)</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>25</div></div><div><span>        </span><span>"，||p|| ="</span><span>,</span></div></div><div><div><div>26</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(p)</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"，||b - p|| ="</span><span>,</span></div></div><div><div><div>28</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(b </span><span>-</span><span> p)</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span># 最小二乘解是 0 的 b：与两列都正交，比如 (1, -2, 1)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>b0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T b0 ="</span><span><span>, A.T </span><span>@</span><span> b0)</span></span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>x0, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b0, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"b0 的最小二乘解 ="</span><span><span>, x0 </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>x_hat = [ 3.6667 -1.    ] ，11/3 = 3.6666666666666665</span></div></div><div><div><div>2</div></div><div><span>两种解法一致： True</span></div></div><div><div><div>3</div></div><div><span>p = [3.6667 2.6667 1.6667]</span></div></div><div><div><div>4</div></div><div><span>p = 11/3 * 列一 - 列二： True</span></div></div><div><div><div>5</div></div><div><span>误差 b - p 与两列正交： True</span></div></div><div><div><div>6</div></div><div><span>||b|| = 5.0990 ，||p|| = 4.8305 ，||b - p|| = 1.6330</span></div></div><div><div><div>7</div></div><div><span>A^T b0 = [0. 0.]</span></div></div><div><div><div>8</div></div><div><span>b0 的最小二乘解 = [0. 0.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>p=(3.6667,2.6667,1.6667)\mathbf{p} = (3.6667, 2.6667, 1.6667)</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>3.6667</span><span>,</span><span></span><span>2.6667</span><span>,</span><span></span><span>1.6667</span><span>)</span></span></span></span>。勾股定理在这里看得很清楚：<span><span>∥b∥=5.0990\|\mathbf{b}\| = 5.0990</span><span><span><span></span><span>∥</span><span>b</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>5.0990</span></span></span></span>，投影 <span><span>∥p∥=4.8305\|\mathbf{p}\| = 4.8305</span><span><span><span></span><span>∥</span><span>p</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>4.8305</span></span></span></span>，误差 <span><span>∥b−p∥=1.6330\|\mathbf{b} - \mathbf{p}\| = 1.6330</span><span><span><span></span><span>∥</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>p</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>1.6330</span></span></span></span>，<span><span>4.83052+1.63302=26.00=5.099024.8305^2 + 1.6330^2 = 26.00 = 5.0990^2</span><span><span><span></span><span>4.830</span><span><span>5</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>1.633</span><span><span>0</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>26.00</span><span></span><span>=</span><span></span></span><span><span></span><span>5.099</span><span><span>0</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。<span><span>ATb0=(0,0)A^\mathsf{T}\mathbf{b}_0 = (0, 0)</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，所以 <span><span>b0=(1,−2,1)\mathbf{b}_0 = (1, -2, 1)</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 的最小二乘解是零。</p><p>他念完这道题就收了，说一个小时只能做这么多，考试有三个小时。</p><p>如果你是从第 1 讲一路跟下来的，到这里 35 篇就结束了。接下来可以去看 Strang 的另一门课 <a href="https://www.youtube.com/playlist?list=PLUl4u3cNGP63oMNUHXqIUcrkS2PivhN3k" target="_blank">18.065</a>（Matrix Methods in Data Analysis, Signal Processing, and Machine Learning），它默认你已经学过 18.06，SVD、低秩近似和优化会用得更多。以前我看到 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 只会想到“解”，现在想到的是“列的组合”，这个变化，大概是这个系列最大的收获。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-33-pseudoinverse/</id>
      <title type="text">18.06 第 33 讲：左逆、右逆与伪逆</title>
      <published>2026-10-18T11:00:00.000Z</published>
      <updated>2026-10-18T11:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-33-pseudoinverse/"/>
      <summary type="text">MIT 18.06 第 33 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 手写了左逆、右逆和基于 SVD 的伪逆，在一个秩 2 的 4 × 3 矩阵上加了 1e-12 的噪声后，朴素的 (AᵀA)⁻¹Aᵀ 与 A⁺ 差了 9.23e+02，截断小奇异值的伪逆只差 7.10e-13。</summary>
      <content type="html"><![CDATA[<p>矩阵一般没有逆，但几乎每个矩阵都有“最接近逆”的东西：满列秩的有左逆，满行秩的有右逆，什么秩都没有的还有伪逆。Strang 这一讲把它们放进同一幅四个子空间的图里讲清楚。我在一个秩 2 的 4 × 3 矩阵上试了一下，给它加 1e-12 的噪声，朴素的 <span><span>(ATA)−1AT(A^\mathsf{T}A)^{-1}A^\mathsf{T}</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 算出来和真正的 <span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 差 9.23e+02，而截掉小奇异值的伪逆只差 7.10e-13。差了十五个数量级，原因就藏在这一讲里。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=Go2aLo7ZOlU" target="_blank">Lecture 33: Left and right inverses; pseudoinverse</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-33-left-and-right-inverses-pseudoinverse/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §4.5 The Pseudoinverse of a Matrix
前置：<a href="/posts/linear-algebra-32-quiz-3-review/">第 32 讲：第三次测验复习</a>
一句话：一个 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span>、秩为 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 的矩阵，在行空间和列空间之间是一一对应的，把这个对应反过来、再把零空间的部分扔掉，就是伪逆 <span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span>；满列秩时它是左逆，满行秩时它是右逆。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几分钟聊测验成绩和他的生日会议，可以跳过。</li>
<li>他在黑板上把 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 的关系分四种情形过了一遍（两边可逆、只有左逆、只有右逆、什么都没有）。这是对整门课的复习，值得认真看，我在前三节里逐个用代码走了一遍。</li>
<li>他证明“行空间里的两个不同向量被 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 映成两个不同向量”的那几句话很短，用的是零空间与行空间只交于零向量。这一段建议跟着他的思路听一遍，本篇第四节只做数值验证。</li>
<li>最后用 SVD 构造伪逆的部分，他讲得比教材还多。字幕里他说这个内容不会出现在期末考试里，但它是整门课的收束，别快进。</li>
</ul></div></div>
<section><h2>四种可逆性，一张图<a href="#四种可逆性一张图"><span>#</span></a></h2><p>他开场就说这是一堂他从没用这种方式讲过的课。内容是对第一章到第四章的复习：对一个 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span>、秩为 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 的矩阵，四个基本子空间的维数分别是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>。零空间要么不存在（只有零向量），要么存在，这决定了逆的样子。</p><ul>
<li><span><span>r=m=nr = m = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>：方阵，满秩，两边都有逆。四个子空间里两个零空间都只剩零向量。</li>
<li><span><span>r=n&lt;mr = n &lt; m</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span>：满列秩，零空间只有零向量。<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 要么无解要么只有一个解。有左逆。</li>
<li><span><span>r=m&lt;nr = m &lt; n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span>：满行秩，左零空间只有零向量。<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 永远有解，但有 <span><span>n−mn - m</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>m</span></span></span></span> 个自由变量，所以有无穷多解。有右逆。</li>
<li><span><span>r&lt;mr &lt; m</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span> 且 <span><span>r&lt;nr &lt; n</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span>：两个零空间都在，没有左逆也没有右逆，但有伪逆。</li>
</ul><p>他还强调了一件事：矩形矩阵永远不会有两边都成立的逆。<span><span>m≠nm \neq n</span><span><span><span></span><span>m</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>n</span></span></span></span> 时 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 或者 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 必然有一个非零的零空间，被这个空间打成零的向量，没有任何矩阵能把它“复活”。</p></section>
<section><h2>左逆：满列秩，<span><span>(ATA)−1AT(A^\mathsf{T}A)^{-1}A^\mathsf{T}</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><a href="#左逆满列秩ata1atamathsfta-1amathsftata1at"><span>#</span></a></h2><p>先看满列秩。他的论证只有一步：列无关，零空间只有零向量，所以 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>（<span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 对称矩阵）的零空间也只有零向量，因此可逆。于是</p><span><span><span>Aleft−1=(ATA)−1AT,Aleft−1 A=InA^{-1}_\text{left} = (A^\mathsf{T}A)^{-1}A^\mathsf{T}, \qquad A^{-1}_\text{left}\, A = I_n</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>left</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>left</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>这正是最小二乘的正规方程 <span><span>ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 的解 <span><span>x^=Aleft−1b\hat{\mathbf{x}} = A^{-1}_\text{left}\mathbf{b}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>left</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>b</span></span></span></span>。第 16 讲里我们已经见过这个矩阵，这一讲他把它换了一个名字。</p><p>顺序反过来会怎样？他停下来问了一个问题：<span><span>A Aleft−1A\,A^{-1}_\text{left}</span><span><span><span></span><span>A</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>left</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是什么？全班答“<span><span>PP</span><span><span><span></span><span>P</span></span></span></span>”，投影矩阵：它想变成 <span><span>ImI_m</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，可是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 只有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 列，能撑起的只有列空间，列空间之外它只能给出零。</p><p>我用一个 4 × 2 的矩阵验证（两列是常数列和 <span><span>0,1,2,30, 1, 2, 3</span><span><span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span></span></span></span>，也就是拟合直线的设计矩阵）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 满列秩的 4 x 2 矩阵：两列无关，行比列多</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"m, n, r ="</span><span><span>, m, n, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span># 左逆 (A^T A)^{-1} A^T</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(A.T </span><span>@</span><span> A) </span><span>@</span><span> A.T</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"L @ A ="</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span><span>(L </span><span>@</span><span> A)</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"L @ A 等于单位阵："</span><span><span>, np.</span><span>allclose</span><span>(L </span><span>@</span><span> A, np.</span><span>eye</span><span>(n)))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 反过来乘：A @ L 不是单位阵，而是投影到列空间的 P</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> A </span><span>@</span><span> L</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ L 等于单位阵："</span><span><span>, np.</span><span>allclose</span><span>(P, np.</span><span>eye</span><span>(m)))</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ P == P："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P))</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"P.T == P："</span><span><span>, np.</span><span>allclose</span><span>(P.T, P))</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"trace(P) ="</span><span>, </span><span>round</span><span>(</span><span>float</span><span><span>(np.</span><span>trace</span><span>(P)), </span></span><span>4</span><span>), </span><span>"（等于秩）"</span><span>)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 对 b 来说：在列空间里就恢复 x，不在就得到最小二乘解</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>x_true </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>b_in </span><span>=</span><span> A </span><span>@</span><span> x_true</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>b_out </span><span>=</span><span> b_in </span><span>+</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"b 在列空间里，L @ b ="</span><span><span>, L </span><span>@</span><span> b_in)</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"b 在列空间外，L @ b ="</span><span><span>, L </span><span>@</span><span> b_out)</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"和 np.linalg.pinv 一致："</span><span><span>, np.</span><span>allclose</span><span>(L, np.linalg.</span><span>pinv</span><span>(A)))</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m, n, r = 4 2 2</span></div></div><div><div><div>2</div></div><div><span>L @ A =</span></div></div><div><div><div>3</div></div><div><span>[[ 1. -0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>5</div></div><div><span>L @ A 等于单位阵： True</span></div></div><div><div><div>6</div></div><div><span>A @ L 等于单位阵： False</span></div></div><div><div><div>7</div></div><div><span>P @ P == P： True</span></div></div><div><div><div>8</div></div><div><span>P.T == P： True</span></div></div><div><div><div>9</div></div><div><span>trace(P) = 2.0 （等于秩）</span></div></div><div><div><div>10</div></div><div><span>b 在列空间里，L @ b = [ 2. -1.]</span></div></div><div><div><div>11</div></div><div><span>b 在列空间外，L @ b = [ 2.6 -1.4]</span></div></div><div><div><div>12</div></div><div><span>和 np.linalg.pinv 一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>L @ A</code> 是单位阵（那个 <code>-0.</code> 是浮点的负零，不用理它），<code>A @ L</code> 不是；后者是个对称的幂等矩阵，迹等于秩 2，正是投影矩阵的特征。<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在列空间里时左逆精确还原出 <span><span>x=(2,−1)\mathbf{x} = (2, -1)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>；不在时（我加了一个不在列空间里的扰动 <span><span>(1,−1,1,−1)(1, -1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>）它给出的是最小二乘解 <span><span>(2.6,−1.4)(2.6, -1.4)</span><span><span><span></span><span>(</span><span>2.6</span><span>,</span><span></span><span>−</span><span>1.4</span><span>)</span></span></span></span>。最后一行也说明，在满列秩的情形，<code>np.linalg.pinv</code> 给出的就是这个左逆，伪逆在这里没有多出任何新东西。</p></section>
<section><h2>右逆：满行秩，<span><span>AT(AAT)−1A^\mathsf{T}(AA^\mathsf{T})^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span><a href="#右逆满行秩ataat1amathsftaamathsft-1ataat1"><span>#</span></a></h2><p>镜像过来。满行秩时 <span><span>AATAA^\mathsf{T}</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（<span><span>m×mm \times m</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span>）可逆，右逆是</p><span><span><span>Aright−1=AT(AAT)−1,A Aright−1=ImA^{-1}_\text{right} = A^\mathsf{T}(AA^\mathsf{T})^{-1}, \qquad A\,A^{-1}_\text{right} = I_m</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>right</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>A</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>right</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>他说它和上面“完全平行”。顺序反过来，<span><span>Aright−1AA^{-1}_\text{right}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>right</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span></span></span></span> 是一个投影，这一次是投到行空间。</p><p>这里有个上面没有的现象：因为有自由变量，<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 有无穷多个解。他说右逆是“我们最喜欢的那个”右逆，因为它给出的解 <span><span>x=AT(AAT)−1b\mathbf{x} = A^\mathsf{T}(AA^\mathsf{T})^{-1}\mathbf{b}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>b</span></span></span></span> 本身是 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的列的组合，完全落在行空间里，没有零空间分量。零空间分量只会让解变长，所以这是所有解中最短的。字幕里他没有展开这一点，这个结论是教材和后面伪逆的内容，我用 2 × 4 的矩阵验证：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 满行秩的 2 x 4 矩阵：两行无关，列比行多</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"m, n, r ="</span><span><span>, m, n, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span># 右逆 A^T (A A^T)^{-1}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> A.T </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(A </span><span>@</span><span> A.T)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ R ="</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span><span>(A </span><span>@</span><span> R)</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ R 等于单位阵："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> R, np.</span><span>eye</span><span>(m)))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 反过来乘：R @ A 是投影到行空间的矩阵</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> R </span><span>@</span><span> A</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"R @ A 等于单位阵："</span><span><span>, np.</span><span>allclose</span><span>(Q, np.</span><span>eye</span><span>(n)))</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q @ Q == Q："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> Q, Q))</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q.T == Q："</span><span><span>, np.</span><span>allclose</span><span>(Q.T, Q))</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 任何 b 都有解，而且有无穷多个；R @ b 是其中最短的</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>3.0</span><span>, </span><span>5.0</span><span>])</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> R </span><span>@</span><span> b</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"x = R @ b ="</span><span>, x, </span><span>"，A @ x == b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, b))</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 零空间里的两个基向量：加上它们，Ax 不变，x 变长</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>2.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]]).T</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ N 等于零："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> N, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>norms </span><span>=</span><span> []</span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>1000</span><span>):</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> x </span><span>+</span><span> N </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>35</div></div><div><span>        </span><span>assert</span><span><span> np.</span><span>allclose</span><span>(A </span><span>@</span><span> y, b)</span></span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>norms.</span><span>append</span><span>(np.linalg.</span><span>norm</span><span>(y))</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"‖x‖ ="</span><span>, </span><span>round</span><span>(</span><span>float</span><span><span>(np.linalg.</span><span>norm</span><span>(x)), </span></span><span>4</span><span>))</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"1000 个别的解里最短的 ‖y‖ ="</span><span>, </span><span>round</span><span>(</span><span>min</span><span>(norms), </span><span>4</span><span>))</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"x 和零空间正交："</span><span><span>, np.</span><span>allclose</span><span>(N.T </span><span>@</span><span> x, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m, n, r = 2 4 2</span></div></div><div><div><div>2</div></div><div><span>A @ R =</span></div></div><div><div><div>3</div></div><div><span>[[ 1.  0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[-0.  1.]]</span></div></div><div><div><div>5</div></div><div><span>A @ R 等于单位阵： True</span></div></div><div><div><div>6</div></div><div><span>R @ A 等于单位阵： False</span></div></div><div><div><div>7</div></div><div><span>Q @ Q == Q： True</span></div></div><div><div><div>8</div></div><div><span>Q.T == Q： True</span></div></div><div><div><div>9</div></div><div><span>x = R @ b = [0.6 0.7 0.8 0.9] ，A @ x == b： True</span></div></div><div><div><div>10</div></div><div><span>A @ N 等于零： True</span></div></div><div><div><div>11</div></div><div><span>‖x‖ = 1.5166</span></div></div><div><div><div>12</div></div><div><span>1000 个别的解里最短的 ‖y‖ = 1.5182</span></div></div><div><div><div>13</div></div><div><span>x 和零空间正交： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 有无穷多个解，我在零空间里随机抽了 1000 个方向，把它们加到 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 上（每个都仍然满足方程，代码里用 <code>assert</code> 保证），最短的也有 1.5182，比 <span><span>x=Rb\mathbf{x} = R\mathbf{b}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>R</span><span>b</span></span></span></span> 的 1.5166 长。当然抽样不是证明，证明很短：<span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 和零空间正交（最后一行），所以 <span><span>∥x+n∥2=∥x∥2+∥n∥2\|\mathbf{x} + \mathbf{n}\|^2 = \|\mathbf{x}\|^2 + \|\mathbf{n}\|^2</span><span><span><span></span><span>∥</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>n</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>∥</span><span>n</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，加任何非零的 <span><span>n\mathbf{n}</span><span><span><span></span><span>n</span></span></span></span> 都变长。</p></section>
<section><h2>一般情形：A 是行空间到列空间的一座桥<a href="#一般情形a-是行空间到列空间的一座桥"><span>#</span></a></h2><p>四种情形里最后一种才是他真正想讲的：<span><span>r&lt;mr &lt; m</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span> 且 <span><span>r&lt;nr &lt; n</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span>，两个零空间都在，<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 和 <span><span>AATAA^\mathsf{T}</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 都不可逆，上面两个公式都失效。</p><p>他的图是这样的：行空间里的向量 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>，乘上 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 以后落在列空间里。两个空间的维数都是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>。关键的事实是：<strong>如果 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 和 <span><span>y\mathbf{y}</span><span><span><span></span><span>y</span></span></span></span> 是行空间里两个不同的向量，那么 <span><span>Ax≠AyA\mathbf{x} \neq A\mathbf{y}</span><span><span><span></span><span>A</span><span>x</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>A</span><span>y</span></span></span></span>。</strong> 他的证明很短：假设相等，则 <span><span>A(x−y)=0A(\mathbf{x} - \mathbf{y}) = \mathbf{0}</span><span><span><span></span><span>A</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以 <span><span>x−y\mathbf{x} - \mathbf{y}</span><span><span><span></span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span> 在零空间里；但它同时是行空间里两个向量的差，也在行空间里。零空间和行空间只交于零向量，所以 <span><span>x=y\mathbf{x} = \mathbf{y}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>y</span></span></span></span>。</p><p>所以把定义域限制在行空间、值域限制在列空间，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是一座没有岔路的桥，两个方向都能走。往回走的那个方向就是伪逆 <span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span>。零空间是桥外的水域：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 把水域里的东西都打成零，<span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 对列空间之外的东西（左零空间）也同样不管，直接给零。</p><p>我用一个秩 2 的 4 × 3 矩阵走一遍：第三列等于前两列之和，第四行等于第一行。顺便按他最后讲的做法，手写一个基于 SVD 的伪逆，并和 <code>np.linalg.pinv</code> 对比。</p><p>SVD 的做法是：<span><span>A=UΣVTA = U\Sigma V^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，<span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 是正交矩阵，它们的逆就是转置。<span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 是一个 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的“对角”矩阵，对角线上有 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个非零的奇异值，其余都是零。它的伪逆 <span><span>Σ+\Sigma^+</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 是 <span><span>n×mn \times m</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span> 的：非零的对角元取倒数，其余仍然是零。于是</p><span><span><span>A+=V Σ+ UTA^+ = V\,\Sigma^+\,U^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>他说了一句我很喜欢的话：<span><span>Σ+\Sigma^+</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 是“最小的”那个，因为你当然可以在零的位置塞上别的数，它们乘出来的还是零，但那只是平白变大。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>pinv_svd</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""用 SVD 手写伪逆：把非零奇异值取倒数，零的留着零。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>sigma_plus </span><span>=</span><span> np.</span><span>zeros</span><span>((n, m))</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> i, sigma </span><span>in</span><span> </span><span>enumerate</span><span>(s):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> sigma </span><span>&gt;</span><span> tol </span><span>*</span><span> s[</span></span><span>0</span><span>]:</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>sigma_plus[i, i] </span><span>=</span><span> </span><span>1.0</span><span><span> </span><span>/</span><span> sigma</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span><span> Vt.T </span><span>@</span><span> sigma_plus </span><span>@</span><span> U.T</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span># 4 x 3，第三列等于前两列之和，第四行等于第一行：秩 2</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>],</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>],</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"m, n, r ="</span><span>, m, n, r)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A.T </span><span>@</span><span> A), </span></span><span>"，不可逆"</span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"A A^T 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A </span><span>@</span><span> A.T), </span></span><span>"，也不可逆"</span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"奇异值："</span><span>, s)</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>A_plus </span><span>=</span><span> </span><span>pinv_svd</span><span>(A)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^+ ="</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(A_plus)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"和 np.linalg.pinv 一致："</span><span><span>, np.</span><span>allclose</span><span>(A_plus, np.linalg.</span><span>pinv</span><span>(A)))</span></span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span># 两边都乘不出单位阵，得到的是投影</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>P_col </span><span>=</span><span> A </span><span>@</span><span> A_plus</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>P_row </span><span>=</span><span> A_plus </span><span>@</span><span> A</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"A A^+ 是 4 x 4 投影，秩"</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(P_col))</span></span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^+ A 是 3 x 3 投影，秩"</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(P_row))</span></span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"A A^+ 是投影："</span><span><span>, np.</span><span>allclose</span><span>(P_col </span><span>@</span><span> P_col, P_col))</span></span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^+ A 是投影："</span><span><span>, np.</span><span>allclose</span><span>(P_row </span><span>@</span><span> P_row, P_row))</span></span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"A A^+ 对称："</span><span><span>, np.</span><span>allclose</span><span>(P_col, P_col.T))</span></span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^+ A 对称："</span><span><span>, np.</span><span>allclose</span><span>(P_row, P_row.T))</span></span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>    </span><span># 行空间的基 V_r（Vt 的前 r 行）：A 在行空间上一对一</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>V_r </span><span>=</span><span> Vt[:r].T</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> V_r </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(r)</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> V_r </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(r)</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>"x、y 在行空间里，x != y："</span><span>, </span><span>not</span><span><span> np.</span><span>allclose</span><span>(x, y))</span></span></div></div><div><div><div>56</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x != A @ y："</span><span>, </span><span>not</span><span><span> np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, A </span><span>@</span><span> y))</span></span></div></div><div><div><div>57</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^+ @ (A @ x) 把 x 找回来："</span><span><span>, np.</span><span>allclose</span><span>(A_plus </span><span>@</span><span> (A </span><span>@</span><span> x), x))</span></span></div></div><div><div><div>58</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ V_r 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A </span><span>@</span><span> V_r))</span></span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m, n, r = 4 3 2</span></div></div><div><div><div>2</div></div><div><span>A^T A 的秩： 2 ，不可逆</span></div></div><div><div><div>3</div></div><div><span>A A^T 的秩： 2 ，也不可逆</span></div></div><div><div><div>4</div></div><div><span>奇异值： [4.7472 1.2101 0.    ]</span></div></div><div><div><div>5</div></div><div><span>A^+ =</span></div></div><div><div><div>6</div></div><div><span>[[ 0.3939  0.0909 -0.2121  0.3939]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[-0.3333 -0.      0.3333 -0.3333]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[ 0.0606  0.0909  0.1212  0.0606]]</span></div></div><div><div><div>9</div></div><div><span>和 np.linalg.pinv 一致： True</span></div></div><div><div><div>10</div></div><div><span>A A^+ 是 4 x 4 投影，秩 2</span></div></div><div><div><div>11</div></div><div><span>A^+ A 是 3 x 3 投影，秩 2</span></div></div><div><div><div>12</div></div><div><span>A A^+ 是投影： True</span></div></div><div><div><div>13</div></div><div><span>A^+ A 是投影： True</span></div></div><div><div><div>14</div></div><div><span>A A^+ 对称： True</span></div></div><div><div><div>15</div></div><div><span>A^+ A 对称： True</span></div></div><div><div><div>16</div></div><div><span>x、y 在行空间里，x != y： True</span></div></div><div><div><div>17</div></div><div><span>A @ x != A @ y： True</span></div></div><div><div><div>18</div></div><div><span>A^+ @ (A @ x) 把 x 找回来： True</span></div></div><div><div><div>19</div></div><div><span>A @ V_r 的秩： 2</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几处和他说的一一对应：</p><ul>
<li>三个奇异值里只有两个非零，<span><span>Σ+\Sigma^+</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 只对它们取倒数。<code>pinv_svd</code> 里的容差 <code>tol * s[0]</code> 就是在判断“零”：奇异值小于最大奇异值的 <span><span>10−1010^{-10}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 倍就当作零。</li>
<li><span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 和 <span><span>AATAA^\mathsf{T}</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的秩都是 2，一个 3 × 3、一个 4 × 4，都不可逆，所以左逆右逆的公式在这里都不能用。</li>
<li><span><span>AA+AA^+</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span>（4 × 4）和 <span><span>A+AA^+A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span>A</span></span></span></span>（3 × 3）都是对称的幂等矩阵，秩都是 2：前者是投影到列空间，后者是投影到行空间。他的说法是：左乘右乘都得不到单位阵，<span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 能做的是把向量带进行空间和列空间这两个“好空间”，再把零空间抹掉。</li>
<li>在行空间里随便取两个不同的向量，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 映出来的也不同，而且 <span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 能把它们找回来。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 作用在行空间的基 <span><span>VrV_r</span><span><span><span></span><span><span>V</span><span><span><span><span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 上，结果仍然是秩 2，没有丢掉任何维度。</li>
</ul></section>
<section><h2>最小范数解：伪逆给出的那一个答案<a href="#最小范数解伪逆给出的那一个答案"><span>#</span></a></h2><p>有了 <span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span>，<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 不管秩是多少、有没有解，都可以写一个统一的答案：<span><span>x+=A+b\mathbf{x}^+ = A^+\mathbf{b}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span>b</span></span></span></span>。<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在列空间里时它是方程组的解，不在时它是最小二乘解；解不唯一时（零空间非零）它是所有候选解里最短的那一个。</p><p>他在这一讲里特别提到了统计学家：做线性回归的人天天用最小二乘，但前提是 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆，也就是列无关。他开玩笑说，统计学家最怕的事就是“把同一个实验重复了几遍”，那样列就不无关了，<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 奇异，这时伪逆来得正是时候。</p><p>下面这个脚本用的是上一节同一个矩阵，第三列是前两列之和，相当于那个“重复的实验”，并故意给了一个不在列空间里的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>。我看三件事：<span><span>A+bA^+\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 和 <code>np.linalg.lstsq</code> 给的最小范数解是不是同一个；沿着零空间走，残差有没有变；再给 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 加上 1e-12 量级的噪声，朴素左逆、<code>pinv</code> 的默认设置和带截断的 <code>pinv</code> 会怎么样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 同一个 4 x 3 的秩 2 矩阵：第三列等于前两列之和</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>],</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>],</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>A_plus </span><span>=</span><span> np.linalg.</span><span>pinv</span><span>(A)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>x_plus </span><span>=</span><span> A_plus </span><span>@</span><span> b</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"x^+ = A^+ b ="</span><span>, x_plus)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 正规方程 A^T A x = A^T b 有无穷多个解，x^+ 是其中最短的</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>x_lstsq, _, rank, _ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.lstsq 的解："</span><span>, x_lstsq, </span><span>"，秩"</span><span>, rank)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"两者一致："</span><span><span>, np.</span><span>allclose</span><span>(x_plus, x_lstsq))</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 零空间：(1, 1, -1)；沿着它走，残差不变，长度变大</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ z 等于零："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> z, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"x^+ 和 z 正交："</span><span><span>, np.</span><span>isclose</span><span>(x_plus </span><span>@</span><span> z, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> [</span><span>0.0</span><span>, </span><span>0.5</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>]:</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> x_plus </span><span>+</span><span> t </span><span>*</span><span> z</span></div></div><div><div><div>31</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>32</div></div><div><span>            </span><span>f</span><span>"t = </span><span>{</span><span>t</span><span>}</span><span>: 残差 </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> x </span><span>-</span><span> b)</span></span><span>:.4f</span><span>}</span><span>，"</span></div></div><div><div><div>33</div></div><div><span>            </span><span>f</span><span>"长度 </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(x)</span></span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 数据里有 1e-12 量级的噪声时，“秩 2”就不是整数了</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>A_noisy </span><span>=</span><span> A </span><span>+</span><span> </span><span>1e-12</span><span><span> </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(A.shape)</span></span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>naive </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(A_noisy.T </span><span>@</span><span> A_noisy) </span><span>@</span><span> A_noisy.T</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>default </span><span>=</span><span> np.linalg.</span><span>pinv</span><span>(A_noisy)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>cut </span><span>=</span><span> np.linalg.</span><span>pinv</span><span>(A_noisy, </span><span>rcond</span><span>=</span><span>1e-8</span><span>)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>for</span><span> name, M </span><span>in</span><span> [</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>(</span><span>"朴素左逆"</span><span>, naive),</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>(</span><span>"pinv 默认"</span><span>, default),</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>(</span><span>"pinv 截断"</span><span>, cut),</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>]:</span></div></div><div><div><div>47</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：和 A^+ 差 </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(M </span><span>-</span><span> A_plus)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A_noisy, </span><span>compute_uv</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span><span>"加噪后最小的奇异值："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>s[</span><span>-</span></span><span>1</span><span>]</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>x^+ = A^+ b = [ 1.3333 -0.6667  0.6667]</span></div></div><div><div><div>2</div></div><div><span>np.linalg.lstsq 的解： [ 1.3333 -0.6667  0.6667] ，秩 2</span></div></div><div><div><div>3</div></div><div><span>两者一致： True</span></div></div><div><div><div>4</div></div><div><span>A @ z 等于零： True</span></div></div><div><div><div>5</div></div><div><span>x^+ 和 z 正交： True</span></div></div><div><div><div>6</div></div><div><span>t = 0.0: 残差 1.4142，长度 1.6330</span></div></div><div><div><div>7</div></div><div><span>t = 0.5: 残差 1.4142，长度 1.8484</span></div></div><div><div><div>8</div></div><div><span>t = 1.0: 残差 1.4142，长度 2.3805</span></div></div><div><div><div>9</div></div><div><span>t = 2.0: 残差 1.4142，长度 3.8297</span></div></div><div><div><div>10</div></div><div><span>朴素左逆：和 A^+ 差 9.23e+02</span></div></div><div><div><div>11</div></div><div><span>pinv 默认：和 A^+ 差 1.83e+12</span></div></div><div><div><div>12</div></div><div><span>pinv 截断：和 A^+ 差 7.10e-13</span></div></div><div><div><div>13</div></div><div><span>加噪后最小的奇异值： 5.47e-13</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前半段是他讲的东西：<span><span>A+b=(1.3333,−0.6667,0.6667)A^+\mathbf{b} = (1.3333, -0.6667, 0.6667)</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1.3333</span><span>,</span><span></span><span>−</span><span>0.6667</span><span>,</span><span></span><span>0.6667</span><span>)</span></span></span></span>，和 <code>np.linalg.lstsq</code> 一致（后者对秩亏的问题返回的就是最小范数解）。零空间是 <span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 的倍数，沿着它走，残差一直是 <span><span>2=1.4142\sqrt{2} = 1.4142</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.4142</span></span></span></span>，不变；长度从 1.6330 一路涨到 3.8297，<span><span>t=2t = 2</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 时是原来的 2.35 倍。<span><span>x+\mathbf{x}^+</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 和零空间正交，所以它是最短的。</p><p>后半段是他没讲、但我觉得最值得知道的。加了 1e-12 的噪声之后，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 在数值上其实已经是满秩的了：最小的奇异值是 5.47e-13，不是零。朴素的 <span><span>(ATA)−1AT(A^\mathsf{T}A)^{-1}A^\mathsf{T}</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 要对 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 求逆，而 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的最小特征值是这个奇异值的平方，量级在 <span><span>10−2510^{-25}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>25</span></span></span></span></span></span></span></span></span></span></span></span>，比双精度能分辨的下限小得多，结果和 <span><span>A+A^+</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 差了 9.23e+02。<code>np.linalg.pinv</code> 的默认截断很小（据我所知是 1e-15 乘最大奇异值），5.47e-13 比它大，所以它也认为这个方向是“有效”的，把它取倒数，差了 1.83e+12，比朴素左逆还糟。只有我自己把截断设成 <code>rcond=1e-8</code>，把小于最大奇异值 <span><span>10−810^{-8}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span> 倍的都当作零，才得到 7.10e-13 的差：从 1.83e+12 降到 7.10e-13，差了约 24 个数量级。</p><p>这说明伪逆在数学上总是良定义的，在数值上却要你自己告诉它“多小算零”。这个阈值是一个判断，不是定理，SVD 只是把问题摆在了最容易判断的位置。（1e-8 是我随手挑的，它落在噪声 5e-13 和真正的奇异值 1.21 之间很宽的一段里，我没有试别的值。）</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>麦轮底盘有 4 个轮子、3 个自由度（前后、左右、转动），所以从“四个电机电流”到“车体受到的合力和力矩”的映射是一个 3 × 4 的矩阵 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span>，满行秩：给定想要的合力合力矩 <span><span>w\mathbf{w}</span><span><span><span></span><span>w</span></span></span></span>，<span><span>Bi=wB\mathbf{i} = \mathbf{w}</span><span><span><span></span><span>B</span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>w</span></span></span></span> 永远有解，而且有一个维数 <span><span>4−3=14 - 3 = 1</span><span><span><span></span><span>4</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的零空间。这正是这一讲的第三种情形，用右逆 <span><span>BT(BBT)−1B^\mathsf{T}(BB^\mathsf{T})^{-1}</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>B</span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 分配电流。</p><p>下面的 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 是我编的示意矩阵，只借了形状，不是某一台真车的标定值；真正的矩阵和推导在 <a href="/posts/pid-control-08-mimo/">多变量：耦合、RGA、解耦与麦轮底盘</a> 里，那一篇把最小范数电流和 <code>numpy.linalg.pinv</code> 的结果对比，差 5.6e-17。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 示意用的 3 x 4 分配矩阵：四个电流 -&gt; (Fx, Fy, 力矩)</span></div></div><div><div><div>7</div></div><div><span>    </span><span># 不是某一台真车的标定值，只借它的形状：3 个自由度，4 个轮子</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>[</span><span>-</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>[</span><span>-</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>],</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的形状："</span><span>, B.shape, </span><span>"，秩"</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 右逆：给一个想要的合力与力矩，分配出四个电流</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>0.5</span><span>])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> B.T </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(B </span><span>@</span><span> B.T)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>i_min </span><span>=</span><span> R </span><span>@</span><span> w</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"最小范数电流："</span><span>, i_min, </span><span>"，B @ i == w："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> i_min, w))</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"和 pinv 一致："</span><span><span>, np.</span><span>allclose</span><span>(R, np.linalg.</span><span>pinv</span><span>(B)))</span></span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 零空间方向：四个轮子互相较劲，合力合力矩都不变</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"B @ n ="</span><span><span>, B </span><span>@</span><span> n)</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>]:</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>i </span><span>=</span><span> i_min </span><span>+</span><span> t </span><span>*</span><span> n</span></div></div><div><div><div>29</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>30</div></div><div><span>            </span><span>f</span><span>"加 </span><span>{</span><span>t</span><span>}</span><span> 倍零空间电流：B @ i 不变 </span><span>{</span><span><span>np.</span><span>allclose</span><span>(B </span><span>@</span><span> i, w)</span></span><span>}</span><span>，"</span></div></div><div><div><div>31</div></div><div><span>            </span><span>f</span><span>"sum(i^2) = </span><span>{</span><span><span>i </span><span>@</span><span> i</span></span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>B 的形状： (3, 4) ，秩 3</span></div></div><div><div><div>2</div></div><div><span>最小范数电流： [0.125 0.875 0.375 0.625] ，B @ i == w： True</span></div></div><div><div><div>3</div></div><div><span>和 pinv 一致： True</span></div></div><div><div><div>4</div></div><div><span>B @ n = [0. 0. 0.]</span></div></div><div><div><div>5</div></div><div><span>加 0.0 倍零空间电流：B @ i 不变 True，sum(i^2) = 1.3125</span></div></div><div><div><div>6</div></div><div><span>加 1.0 倍零空间电流：B @ i 不变 True，sum(i^2) = 5.3125</span></div></div><div><div><div>7</div></div><div><span>加 2.0 倍零空间电流：B @ i 不变 True，sum(i^2) = 17.3125</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>零空间方向 <span><span>(1,1,−1,−1)(1, 1, -1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 是“两个轮子顺着推，另外两个逆着推”，合力和力矩互相抵消，只剩下发热。电机的发热正比于电流的平方，所以最小范数的分配（<span><span>∑i2=1.3125\sum i^2 = 1.3125</span><span><span><span></span><span>∑</span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.3125</span></span></span></span>）就是同样的合力下最省电的分配：叠一倍零空间电流，<span><span>∑i2\sum i^2</span><span><span><span></span><span>∑</span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 涨到 5.3125，叠两倍涨到 17.3125，是最小值的 13.2 倍，而合力和力矩一点没变。我猜实际的底盘有时会故意利用这个零空间，比如某个轮子饱和了就往零空间里借一点，那是另一回事，这里只说默认的伪逆分配不会白白浪费电流。</p></section>
<section><h2>下一讲<a href="#下一讲"><span>#</span></a></h2><p>他在这一讲结尾说，周三的最后一讲会尽可能把整门课过一遍。下一讲是期末复习：<a href="/posts/linear-algebra-34-final-review/">18.06 第 34 讲：期末复习</a>。这一讲其实已经是预演了，四个子空间、投影、最小二乘、SVD 全都出现了一遍。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-32-quiz-3-review/</id>
      <title type="text">18.06 第 32 讲：第三次测验复习</title>
      <published>2026-10-18T10:40:00.000Z</published>
      <updated>2026-10-18T10:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-32-quiz-3-review/"/>
      <summary type="text">MIT 18.06 第 32 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 Strang 复习课里的反对称矩阵、特征值判断题、SVD 的符号问题和“对称又正交”的真假题逐一验证，SVD 的 U 有 4 种符号选法，只有 1 种能还原 A。</summary>
      <content type="html"><![CDATA[<p>这一讲没有新定理，Strang 拿几道旧考题把第六章的东西串了一遍：特征值、对称与正定、相似矩阵、SVD。我的做法是把每道题都交给 NumPy 验证。其中最有意思的一个数字出在 SVD：从 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 和 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 分别求出特征向量来拼 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>，列的符号有 4 种选法，只有 1 种能还原 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，另外 3 种的误差分别是 8.4853、11.3137 和 14.1421。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=HgC1l_6ySkc" target="_blank">Lecture 32: Quiz 3 review</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-32-quiz-3-review/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版第 6 到 8 章（录像里的测验只到特征值那一章，线性变换留给期末，所以这一讲实际用到的是第 6 章与第 7 章的 SVD）
前置：<a href="/posts/linear-algebra-31-change-of-basis-compression/">第 31 讲：基变换与图像压缩</a>
一句话：做特征值的题先看矩阵属于哪个家族，对称、反对称、正交这三类都满足 <span><span>AAT=ATAAA^T = A^TA</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>，特征向量正交，剩下的全看特征值。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他列了本章要记住的几条：对称矩阵的实特征值与 <span><span>QΛQTQ\Lambda Q^T</span><span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>、正定、相似矩阵特征值相同、SVD。这一段很短，值得完整听一遍当清单用。</li>
<li>第一道微分方程题（反对称矩阵）和后面那道“<span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 取哪些值”的题，我在下面都算过了，可以倍速。其中“<span><span>AAT=ATAAA^T = A^TA</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 是特征向量正交的精确条件”这句他讲得很快，建议回放。</li>
<li>SVD 那一段他承认自己在上一次讲课时符号选错了。这是全篇最值得认真听的地方，本篇第三节用代码复现了这个错误。</li>
<li>最后的“对称又正交”真假题短而密，每一句都是一个结论，暂停着听比较稳。</li>
</ul></div></div>
<section><h2>反对称矩阵：特征值在虚轴上<a href="#反对称矩阵特征值在虚轴上"><span>#</span></a></h2><p>第一道题是一个 3 × 3 的微分方程 <span><span>dudt=Au\frac{d\mathbf{u}}{dt} = A\mathbf{u}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span>t</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>d</span><span>u</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span></span></span></span>，矩阵对角线是 0，上面是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，下面是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>：</p><span><span><span>A=[0−1010−1010]A = \begin{bmatrix} 0 &amp; -1 &amp; 0 \\ 1 &amp; 0 &amp; -1 \\ 0 &amp; 1 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>没有给初值，所以要的是通解 <span><span>c1eλ1tx1+c2eλ2tx2+c3eλ3tx3c_1 e^{\lambda_1 t}\mathbf{x}_1 + c_2 e^{\lambda_2 t}\mathbf{x}_2 + c_3 e^{\lambda_3 t}\mathbf{x}_3</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他先问这个矩阵奇不奇异：第一行和第三行只差一个符号，所以奇异，<span><span>λ1=0\lambda_1 = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。再看 <span><span>AT=−AA^T = -A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>A</span></span></span></span>，反对称矩阵的特征值是纯虚数，特征多项式 <span><span>λ3+2λ=0\lambda^3 + 2\lambda = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 给出 <span><span>λ=0,±2 i\lambda = 0, \pm\sqrt{2}\,i</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span>±</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>i</span></span></span></span>。</p><p>结论有三个。解既不衰减也不爆炸，因为 <span><span>ei2te^{i\sqrt{2}t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 的模永远是 1。解是周期的，周期 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 满足 <span><span>2 T=2π\sqrt{2}\,T = 2\pi</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>π</span></span></span></span>，即 <span><span>T=π2T = \pi\sqrt{2}</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>π</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。特征向量两两正交。下面用 NumPy 一次验证：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 黑板上的反对称矩阵：对角线为 0，上面是 -1，下面是 +1</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span>    </span><span>print</span><span>(</span><span>"A.T == -A："</span><span><span>, np.</span><span>array_equal</span><span>(A.T, </span><span>-</span><span>A))</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>lam, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值的虚部："</span><span><span>, np.</span><span>sort</span><span>(lam.imag))</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值实部的最大绝对值："</span><span><span>, np.</span><span>abs</span><span>(lam.real).</span><span>max</span><span>())</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"sqrt(2) ="</span><span><span>, np.</span><span>sqrt</span><span>(</span></span><span>2.0</span><span>))</span></div></div><div><div><div>14</div></div><div><span>    </span><span># 特征向量两两正交（复向量用共轭转置）</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"S^H S 是单位矩阵："</span><span><span>, np.</span><span>allclose</span><span>(S.</span><span>conj</span><span>().T </span><span>@</span><span> S, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>16</div></div><div><span>    </span><span># 反对称矩阵满足 A A^T = A^T A</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"A A^T == A^T A："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> A.T, A.T </span><span>@</span><span> A))</span></span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># e^{At} = S diag(e^{lambda t}) S^{-1}</span></div></div><div><div><div>20</div></div><div><span>    </span><span>def</span><span> </span><span>expm</span><span>(</span><span>t</span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>21</div></div><div><span>        </span><span>return</span><span><span> (S </span><span>@</span><span> np.</span><span>diag</span><span>(np.</span><span>exp</span><span>(lam </span><span>*</span><span> t)) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S)).real</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>period </span><span>=</span><span> np.pi </span><span>*</span><span> np.</span><span>sqrt</span><span>(</span><span>2.0</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"周期 T = pi * sqrt(2) ="</span><span>, period)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> [</span><span>0.5</span><span><span> </span><span>*</span><span> period, period]:</span></span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(</span><span>expm</span><span>(t) </span><span>-</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>))</span></div></div><div><div><div>27</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"t = </span><span>{</span><span>t</span><span>:.4f</span><span>}</span><span>：||e^(At) - I|| = </span><span>{</span><span>err</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 解既不增长也不衰减：每个时刻的长度都一样</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>u0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>ts </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, period, </span><span>7</span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"||u(t)||："</span><span><span>, np.</span><span>array</span><span>([np.linalg.</span><span>norm</span><span>(</span><span>expm</span><span>(t) </span><span>@</span><span> u0) </span></span><span>for</span><span> t </span><span>in</span><span> ts]))</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A.T == -A： True</span></div></div><div><div><div>2</div></div><div><span>特征值的虚部： [-1.4142  0.      1.4142]</span></div></div><div><div><div>3</div></div><div><span>特征值实部的最大绝对值： 5.4814816543375465e-17</span></div></div><div><div><div>4</div></div><div><span>sqrt(2) = 1.4142135623730951</span></div></div><div><div><div>5</div></div><div><span>S^H S 是单位矩阵： True</span></div></div><div><div><div>6</div></div><div><span>A A^T == A^T A： True</span></div></div><div><div><div>7</div></div><div><span>周期 T = pi * sqrt(2) = 4.442882938158366</span></div></div><div><div><div>8</div></div><div><span>t = 2.2214：||e^(At) - I|| = 2.83e+00</span></div></div><div><div><div>9</div></div><div><span>t = 4.4429：||e^(At) - I|| = 8.45e-15</span></div></div><div><div><div>10</div></div><div><span>||u(t)||： [3.7417 3.7417 3.7417 3.7417 3.7417 3.7417 3.7417]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个脚本有三处值得看。特征值的实部最大是 <span><span>5.5×10−175.5 \times 10^{-17}</span><span><span><span></span><span>5.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>，也就是 0；虚部是 <span><span>0,±1.41420, \pm 1.4142</span><span><span><span></span><span>0</span><span>,</span><span></span><span>±</span><span>1.4142</span></span></span></span>。<span><span>eATe^{AT}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>T</span></span></span></span></span></span></span></span></span></span></span></span> 在半个周期处离 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 有 2.83，到整个周期 <span><span>T=4.4429T = 4.4429</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>4.4429</span></span></span></span> 处只剩 <span><span>8.45×10−158.45 \times 10^{-15}</span><span><span><span></span><span>8.45</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，所以周期确实是 <span><span>π2\pi\sqrt{2}</span><span><span><span></span><span>π</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。而 <span><span>∥u(t)∥\|\mathbf{u}(t)\|</span><span><span><span></span><span>∥</span><span>u</span><span>(</span><span>t</span><span>)</span><span>∥</span></span></span></span> 在七个等间隔的时刻都是 3.7417（也就是 <span><span>14\sqrt{14}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>14</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，<span><span>u0\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的长度），解一直在一个球面上绕。</p><p>他顺带补了一句我觉得值得抄下来的话：特征向量正交的精确条件是 <span><span>AAT=ATAAA^T = A^TA</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>。对称矩阵（两边都是 <span><span>A2A^2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>）、反对称矩阵（两边都是 <span><span>−A2-A^2</span><span><span><span></span><span>−</span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>）、正交矩阵（两边都是 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>）都满足，这就是第六章里三个最重要的家族。输出里的 <code>A A^T == A^T A： True</code> 是反对称这一项。（用复向量就要用共轭转置，所以脚本里写的是 <code>S.conj().T</code>。）</p><p>最后他提到 <span><span>eAte^{At}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>：矩阵可对角化时它是 <span><span>SeΛtS−1Se^{\Lambda t}S^{-1}</span><span><span><span></span><span>S</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>Λ</span><span>t</span></span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，脚本里的 <code>expm</code> 就是这么写的，没有调用任何现成的矩阵指数函数。</p></section>
<section><h2>只知道特征值和特征向量，能说什么<a href="#只知道特征值和特征向量能说什么"><span>#</span></a></h2><p>第二道题给了一个 3 × 3 矩阵的三个特征向量（两两正交）和特征值（一个是 0，另一个是 2，还有一个未知的 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span>），然后连续问：对哪些 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 可对角化、对称、正定、是 Markov 矩阵、<span><span>A/2A/2</span><span><span><span></span><span>A</span><span>/2</span></span></span></span> 是投影矩阵。字幕没有念出黑板上的向量，所以下面的向量是我自己选的一组正交基，不是原题的数字，结论只依赖“正交”这一点。</p><p>他的回答顺序是：可对角化只看特征向量够不够，三个正交向量一定无关，所以任何 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 都行；特征向量正交加上特征值为实数就是对称，所以所有实数 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span>；正定不可能，因为有特征值 0，<span><span>c≥0c \ge 0</span><span><span><span></span><span>c</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span> 时是半正定（“安慰奖”）；Markov 不可能，因为特征值不能超过 1，而这里有 2；<span><span>A/2A/2</span><span><span><span></span><span>A</span><span>/2</span></span></span></span> 是投影矩阵要求特征值只能是 0 或 1，所以 <span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 或 <span><span>c=2c = 2</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 字幕没有念出板上的向量，我自己选了一组两两正交的特征向量</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> S </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(S, </span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"三个特征向量两两正交："</span><span><span>, np.</span><span>allclose</span><span>(Q.T </span><span>@</span><span> Q, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> c </span><span>in</span><span><span> [</span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>]:</span></div></div><div><div><div>12</div></div><div><span>        </span><span># 特征值 0、2、c，特征向量是 Q 的三列：A = Q diag(0, 2, c) Q^T</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> Q </span><span>@</span><span> np.</span><span>diag</span><span>([</span><span>0.0</span><span>, </span><span>2.0</span><span><span>, c]) </span><span>@</span><span> Q.T</span></span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>lam </span><span>=</span><span> np.linalg.</span><span>eigvalsh</span><span>(A)</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>P </span><span>=</span><span> A </span><span>/</span><span> </span><span>2.0</span></div></div><div><div><div>16</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"c = </span><span>{</span><span>c</span><span>:4.1f</span><span>}</span><span>，特征值 </span><span>{</span><span><span>np.</span><span>round</span><span>(lam, </span></span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0}</span><span>"</span><span>)</span></div></div><div><div><div>17</div></div><div><span>        </span><span>print</span><span>(</span><span>"  对称："</span><span><span>, np.</span><span>allclose</span><span>(A, A.T))</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>print</span><span>(</span><span>"  半正定："</span><span>, </span><span>bool</span><span><span>(lam.</span><span>min</span><span>() </span><span>&gt;</span><span> </span><span>-</span></span><span>1e-12</span><span>))</span></div></div><div><div><div>19</div></div><div><span>        </span><span>print</span><span>(</span><span>"  正定："</span><span>, </span><span>bool</span><span><span>(lam.</span><span>min</span><span>() </span><span>&gt;</span><span> </span></span><span>1e-12</span><span>))</span></div></div><div><div><div>20</div></div><div><span>        </span><span>print</span><span>(</span><span>"  有特征值大于 1（Markov 不允许）："</span><span>, </span><span>bool</span><span><span>(lam.</span><span>max</span><span>() </span><span>&gt;</span><span> </span></span><span>1.0</span><span>))</span></div></div><div><div><div>21</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>22</div></div><div><span>            </span><span>"  A / 2 是投影矩阵："</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P) </span><span>and</span><span><span> np.</span><span>allclose</span><span>(P, P.T),</span></span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>三个特征向量两两正交： True</span></div></div><div><div><div>2</div></div><div><span>c = -1.0，特征值 [-1.  0.  2.]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>对称： True</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>半正定： False</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>正定： False</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>有特征值大于 1（Markov 不允许）： True</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>A / 2 是投影矩阵： False</span></div></div><div><div><div>8</div></div><div><span>c =  0.0，特征值 [0. 0. 2.]</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>对称： True</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>半正定： True</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>正定： False</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>有特征值大于 1（Markov 不允许）： True</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>A / 2 是投影矩阵： True</span></div></div><div><div><div>14</div></div><div><span>c =  1.0，特征值 [0. 1. 2.]</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>对称： True</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>半正定： True</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>正定： False</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>有特征值大于 1（Markov 不允许）： True</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>A / 2 是投影矩阵： False</span></div></div><div><div><div>20</div></div><div><span>c =  2.0，特征值 [0. 2. 2.]</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>对称： True</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>半正定： True</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>正定： False</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>有特征值大于 1（Markov 不允许）： True</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>A / 2 是投影矩阵： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四个 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 值的结果和他的口头回答一一对应：<span><span>c=−1c = -1</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span> 连半正定都不是，<span><span>c=0,1,2c = 0, 1, 2</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span></span></span></span> 是半正定但没有一个正定，所有行的“有特征值大于 1”都是 <code>True</code>，只有 <span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 和 <span><span>c=2c = 2</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 让 <span><span>A/2A/2</span><span><span><span></span><span>A</span><span>/2</span></span></span></span> 通过了 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span> 与 <span><span>PT=PP^T = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>。我只试了四个点，并不能证明“只有这两个”，但 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span> 推出 <span><span>λ2=λ\lambda^2 = \lambda</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span></span></span></span>，证明是他那一句话里的。</p></section>
<section><h2>SVD：符号是唯一会出错的地方<a href="#svd符号是唯一会出错的地方"><span>#</span></a></h2><p>第三个话题是 SVD。<span><span>A=UΣVTA = U\Sigma V^T</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 对任何矩阵成立，关键是 <span><span>ATA=VΣTΣVTA^TA = V\Sigma^T\Sigma V^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：中间是对角，两边是同一个正交矩阵，所以 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 是 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的特征向量矩阵，<span><span>σi2\sigma_i^2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是它的特征值。同样 <span><span>AAT=UΣΣTUTAA^T = U\Sigma\Sigma^T U^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 给出 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>。</p><p>然后是他承认的那个错误。上次上课他用 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的特征向量当 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>，符号撞上了错的那一半：特征向量乘以 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 还是特征向量，光凭 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 决定不了符号。正确的做法是定了 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 之后，用 <span><span>Avi=σiuiA\mathbf{v}_i = \sigma_i\mathbf{u}_i</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 反推 <span><span>ui\mathbf{u}_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，这样符号被 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 绑定。他说自己有 50% 的概率选对，结果没选对。</p><p>我用 <span><span>A=[44−33]A = \begin{bmatrix} 4 &amp; 4 \\ -3 &amp; 3 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 复现（这是我挑的矩阵，不是黑板上的）。把 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的两列符号四种组合全试一遍：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>4.0</span><span><span>], [</span><span>-</span></span><span>3.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 第一步：A^T A 的特征向量给出 V，特征值开方给出 sigma</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>w, V </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A.T </span><span>@</span><span> A)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>order </span><span>=</span><span> np.</span><span>argsort</span><span>(w)[::</span><span>-</span><span>1</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>w, V </span><span>=</span><span> w[order], V[:, order]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>sigma </span><span>=</span><span> np.</span><span>sqrt</span><span>(w)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"sigma ="</span><span>, sigma)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># 第二步（对的）：由 A v = sigma u 得到 u，符号随 V 定下来</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>U_good </span><span>=</span><span> (A </span><span>@</span><span> V) </span><span>/</span><span> sigma</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>18</div></div><div><span>        </span><span>"A v = sigma u 得到的 U 满足 A = U Sigma V^T："</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(U_good </span><span>@</span><span> np.</span><span>diag</span><span>(sigma) </span><span>@</span><span> V.T, A),</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 第二步（Strang 当年出错的做法）：独立求 A A^T 的特征向量，</span></div></div><div><div><div>23</div></div><div><span>    </span><span># 但每一列的符号是任意的，一共 2 * 2 = 4 种选法</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>w2, U_eig </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A </span><span>@</span><span> A.T)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>U_eig </span><span>=</span><span> U_eig[:, np.</span><span>argsort</span><span>(w2)[::</span><span>-</span><span>1</span><span>]]</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>good </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> s1 </span><span>in</span><span> [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]:</span></div></div><div><div><div>28</div></div><div><span>        </span><span>for</span><span> s2 </span><span>in</span><span> [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]:</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>U_try </span><span>=</span><span> U_eig </span><span>*</span><span> np.</span><span>array</span><span>([s1, s2])</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(U_try </span><span>@</span><span> np.</span><span>diag</span><span>(sigma) </span><span>@</span><span> V.T </span><span>-</span><span> A)</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>good </span><span>+=</span><span> </span><span>int</span><span><span>(err </span><span>&lt;</span><span> </span></span><span>1e-10</span><span>)</span></div></div><div><div><div>32</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"符号 (</span><span>{</span><span>s1</span><span>:4.1f</span><span>}</span><span>, </span><span>{</span><span>s2</span><span>:4.1f</span><span>}</span><span>)：误差 </span><span>{</span><span>err</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"4 种符号里能还原 A 的有"</span><span>, good, </span><span>"种"</span><span>)</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 和 NumPy 对照</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.svd 的奇异值："</span><span>, s)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sigma = [5.6569 4.2426]</span></div></div><div><div><div>2</div></div><div><span>A v = sigma u 得到的 U 满足 A = U Sigma V^T： True</span></div></div><div><div><div>3</div></div><div><span>符号 ( 1.0,  1.0)：误差 0.0000</span></div></div><div><div><div>4</div></div><div><span>符号 ( 1.0, -1.0)：误差 8.4853</span></div></div><div><div><div>5</div></div><div><span>符号 (-1.0,  1.0)：误差 11.3137</span></div></div><div><div><div>6</div></div><div><span>符号 (-1.0, -1.0)：误差 14.1421</span></div></div><div><div><div>7</div></div><div><span>4 种符号里能还原 A 的有 1 种</span></div></div><div><div><div>8</div></div><div><span>np.linalg.svd 的奇异值： [5.6569 4.2426]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行的“对的”做法用 <span><span>Av=σuA\mathbf{v} = \sigma\mathbf{u}</span><span><span><span></span><span>A</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>σ</span><span>u</span></span></span></span>，通过了。独立求 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的特征向量后，4 种符号只有 <span><span>(+,+)(+, +)</span><span><span><span></span><span>(</span><span>+</span><span>,</span><span></span><span>+</span><span>)</span></span></span></span> 这一种能还原 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>（这次 <code>eigh</code> 恰好吐出了对的符号，但没有任何东西保证它会这样），另外三种的误差是 8.4853、11.3137 和 14.1421。表里是 4 选 1，也就是 25%；Strang 当时只有一个符号没定，所以他说 50% 的运气。最后一行用 <code>np.linalg.svd</code> 对照，两个奇异值 5.6569 与 4.2426 和手算的 <span><span>32\sqrt{32}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>32</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>、<span><span>18\sqrt{18}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>18</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 一致。</p><p>他接着出了几个问 SVD 的“读图”题。<span><span>Σ=diag⁡(3,2)\Sigma = \operatorname{diag}(3, 2)</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 是 2 × 2 的可逆矩阵；把 2 换成 <span><span>−5-5</span><span><span><span></span><span>−</span><span>5</span></span></span></span> 就不是 SVD 了，因为奇异值不能为负；<span><span>Σ=diag⁡(3,0)\Sigma = \operatorname{diag}(3, 0)</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>3</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 的话秩为 1，零空间维数为 1，<span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在零空间里，<span><span>u2\mathbf{u}_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 <span><span>ATA^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间里。他说得很直白：SVD 多花一点时间，但把所有基本子空间一次摆了出来。</p></section>
<section><h2>对称又正交的矩阵<a href="#对称又正交的矩阵"><span>#</span></a></h2><p>最后一道是真假题。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 既对称又正交，问：</p><ul>
<li>特征值是什么？实数（对称）且模为 1（正交：<span><span>∣λ∣ ∥x∥=∥Qx∥=∥x∥|\lambda|\,\|\mathbf{x}\| = \|Q\mathbf{x}\| = \|\mathbf{x}\|</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span><span></span><span>∥</span><span>x</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>Q</span><span>x</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>x</span><span>∥</span></span></span></span>），所以只能是 <span><span>11</span><span><span><span></span><span>1</span></span></span></span> 或 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。</li>
<li>一定正定吗？不，可以有 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。</li>
<li>没有重复特征值吗？假，只要够大，<span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> 里总有一个重复。</li>
<li>可对角化吗？可以，而且能用正交的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>。</li>
<li>非奇异吗？是，正交矩阵都可逆。</li>
</ul><p>最后一步：证明 <span><span>12(A+I)\frac12(A + I)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span>)</span></span></span></span> 是投影矩阵。对称是显然的；平方得 <span><span>14(A2+2A+I)\frac14(A^2 + 2A + I)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>4</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span>)</span></span></span></span>，而 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 对称正交意味着 <span><span>AT=A=A−1A^T = A = A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，所以 <span><span>A2=IA^2 = I</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，结果是 <span><span>12(A+I)\frac12(A + I)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span>)</span></span></span></span> 自己。换个角度：特征值是 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span>，加 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 后是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span> 和 <span><span>22</span><span><span><span></span><span>2</span></span></span></span>，除以 2 就是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span> 和 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>。</p><p>“对称又正交”的最典型例子是 Householder 反射 <span><span>H=I−2uuTH = I - 2\mathbf{u}\mathbf{u}^T</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>u</span><span><span>u</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。我用它验证上面的每一条，并且顺手验证他开头说的“相似矩阵特征值相同，幂也只差一个 <span><span>M−1M^{-1}</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 和 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># Sigma = diag(3, 2) 与 diag(3, 0)：U、V 用随机正交矩阵</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>U, _ </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>2</span><span>)))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>V, _ </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>2</span><span>)))</span></div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> s </span><span>in</span><span> [(</span><span>3.0</span><span>, </span><span>2.0</span><span>), (</span><span>3.0</span><span>, </span><span>0.0</span><span>)]:</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> U </span><span>@</span><span> np.</span><span>diag</span><span>(s) </span><span>@</span><span> V.T</span></div></div><div><div><div>13</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>14</div></div><div><span>            </span><span>f</span><span>"Sigma = diag</span><span>{</span><span>s</span><span>}</span><span>：秩 </span><span>{</span><span><span>np.linalg.</span><span>matrix_rank</span><span>(A)</span></span><span>}</span><span>，"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>            </span><span>f</span><span>"det = </span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(A)</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span># 第二个奇异值为 0 时，v2 在零空间里，u2 在 A^T 的零空间里</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"A v2 ="</span><span><span>, np.</span><span>round</span><span>(A </span><span>@</span><span> V[:, </span></span><span>1</span><span>], </span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T u2 ="</span><span><span>, np.</span><span>round</span><span>(A.T </span><span>@</span><span> U[:, </span></span><span>1</span><span>], </span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 对称又正交的矩阵：Householder 反射 A = I - 2 u u^T</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>5</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((n, </span><span>1</span><span>))</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>u </span><span>/=</span><span> np.linalg.</span><span>norm</span><span>(u)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>H </span><span>=</span><span> np.</span><span>eye</span><span>(n) </span><span>-</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> u </span><span>@</span><span> u.T</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"H 对称且正交："</span><span><span>, np.</span><span>allclose</span><span>(H, H.T), np.</span><span>allclose</span><span>(H </span><span>@</span><span> H.T, np.</span><span>eye</span><span>(n))</span></span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(H))</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"H @ H == I："</span><span><span>, np.</span><span>allclose</span><span>(H </span><span>@</span><span> H, np.</span><span>eye</span><span>(n)))</span></span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> (H </span><span>+</span><span> np.</span><span>eye</span><span>(n))</span></span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>33</div></div><div><span>        </span><span>"(H + I) / 2 是投影矩阵："</span><span>,</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P) </span><span>and</span><span><span> np.</span><span>allclose</span><span>(P, P.T),</span></span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"P 的特征值："</span><span><span>, np.</span><span>round</span><span>(np.linalg.</span><span>eigvalsh</span><span>(P), </span></span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"trace(P) ="</span><span><span>, np.</span><span>trace</span><span>(P))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 相似矩阵：B = M^{-1} A M 与 A 的特征值相同，B^k = M^{-1} A^k M</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((n, n))</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> H </span><span>@</span><span> M</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>43</div></div><div><span>        </span><span>"相似矩阵特征值相同："</span><span>,</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(B).real), np.linalg.</span><span>eigvalsh</span><span>(H)),</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>k </span><span>=</span><span> </span><span>7</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>lhs </span><span>=</span><span> np.linalg.</span><span>matrix_power</span><span>(B, k)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>rhs </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> np.linalg.</span><span>matrix_power</span><span>(H, k) </span><span>@</span><span> M</span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"B^</span><span>{</span><span>k</span><span>}</span><span> == M^-1 H^</span><span>{</span><span>k</span><span>}</span><span> M："</span><span><span>, np.</span><span>allclose</span><span>(lhs, rhs))</span></span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Sigma = diag(3.0, 2.0)：秩 2， det = 6.0000</span></div></div><div><div><div>2</div></div><div><span>Sigma = diag(3.0, 0.0)：秩 1， det = 0.0000</span></div></div><div><div><div>3</div></div><div><span>A v2 = [0. 0.]</span></div></div><div><div><div>4</div></div><div><span>A^T u2 = [0. 0.]</span></div></div><div><div><div>5</div></div><div><span>H 对称且正交： True True</span></div></div><div><div><div>6</div></div><div><span>特征值： [-1.  1.  1.  1.  1.]</span></div></div><div><div><div>7</div></div><div><span>H @ H == I： True</span></div></div><div><div><div>8</div></div><div><span>(H + I) / 2 是投影矩阵： True</span></div></div><div><div><div>9</div></div><div><span>P 的特征值： [0. 1. 1. 1. 1.]</span></div></div><div><div><div>10</div></div><div><span>trace(P) = 4.0</span></div></div><div><div><div>11</div></div><div><span>相似矩阵特征值相同： True</span></div></div><div><div><div>12</div></div><div><span>B^7 == M^-1 H^7 M： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的特征值是一个 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 加四个 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>（<span><span>u\mathbf{u}</span><span><span><span></span><span>u</span></span></span></span> 方向被翻转，其余方向不动），<span><span>H2=IH^2 = I</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 通过了，<span><span>12(H+I)\frac12(H + I)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>H</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span>)</span></span></span></span> 的特征值是一个 0 加四个 1，迹为 4，正是投影到 4 维子空间。随机的 5 × 5 矩阵 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 做出来的相似矩阵 <span><span>B=M−1HMB = M^{-1}HM</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>H</span><span>M</span></span></span></span> 与 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的特征值一致，<span><span>B7=M−1H7MB^7 = M^{-1}H^7M</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span><span>M</span></span></span></span> 也成立。再强调一遍他的原话：<span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 改变特征向量，但不改变特征值。（另外两行关于秩 1 的输出，<code>A v2</code> 与 <code>A^T u2</code> 都是零向量，是上一节“读图”题的验证。）</p><p>下一讲回到一个我觉得更有趣的话题（我对“有趣”的标准很低：只要某个误差能从 0 跳到 14）：矩阵不可逆、方程组无解或有无穷多解的时候，到底该用什么“逆”。Strang 管这个叫<a href="/posts/linear-algebra-33-pseudoinverse/">左逆、右逆与伪逆</a>，而它恰好是 SVD 的又一个应用。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-31-change-of-basis-compression/</id>
      <title type="text">18.06 第 31 讲：基变换与图像压缩</title>
      <published>2026-10-18T10:20:00.000Z</published>
      <updated>2026-10-18T10:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-31-change-of-basis-compression/"/>
      <summary type="text">MIT 18.06 第 31 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 在 Haar 小波基下压缩一张合成的 64 × 64 灰度图，只存 516 个数，相对误差 0.0142，同样存储量的 SVD 低秩近似是 0.0795。</summary>
      <content type="html"><![CDATA[<p>压缩图像在线性代数里只是一件事：把像素向量从标准基换到另一组基，扔掉绝对值小的系数，再换回来。我合成了一张 64 × 64 的灰度图（渐变背景、一个圆盘、一个方块、一点噪声），在 Haar 小波基下只保留 516 个最大的系数，相对误差是 0.0142。同样存 516 个数，SVD 低秩近似（秩 4）的误差是 0.0795，大了 5.6 倍；在标准基里只留 516 个最大的像素，误差是 0.7760，基本等于图被毁了。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=0h43aV4aH7I" target="_blank">Lecture 31: Change of basis; image compression</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-31-change-of-basis-image-compression/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §8.3 The Search for a Good Basis；§7.2 Image Processing by Linear Algebra
前置：<a href="/posts/linear-algebra-30-linear-transformations/">第 30 讲：线性变换及其矩阵</a>
一句话：换基就是解 <span><span>x=Wc\mathbf{x} = W\mathbf{c}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>W</span><span>c</span></span></span></span>，好基要又快（<span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 和 <span><span>W−1W^{-1}</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 都容易乘）又省（几个基向量就够），而同一个线性变换在两组基下的矩阵是相似的：<span><span>B=M−1AMB = M^{-1}AM</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span></span></span></span>。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>前半讲是应用：JPEG、FBI 指纹、视频压缩。他讲得很随意，也很有意思，值得正常速度看，本篇只用代码重做了其中的数学。</li>
<li>他在黑板上写 8 个小波向量（全 1、四个 1 四个 -1、两个 1 两个 -1……）的那一段要看，我的第一个代码块就是照着它建矩阵。</li>
<li>讲到 <span><span>P=Wc\mathbf{P} = W\mathbf{c}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>W</span><span>c</span></span></span></span>、“好基要有快速的逆”那几分钟是全讲的核心，不要倍速。</li>
<li>后半讲他自己说“这一部分是压缩过的”：用相似矩阵回顾上一讲。如果上一讲已经熟，可以倍速，但最后一分钟的“特征向量基下矩阵是对角阵”要听。</li>
</ul></div></div>
<section><h2>压缩从哪里来：标准基是个糟糕的基<a href="#压缩从哪里来标准基是个糟糕的基"><span>#</span></a></h2><p>Strang 一开口就拿这门课的录像说事：你看到的他的动作有点跳，是因为视频被压缩过了。静止的板书压得很干净，他自己走来走去才吃比特。</p><p>静止图像是什么？一张 <span><span>512×512512 \times 512</span><span><span><span></span><span>512</span><span></span><span>×</span><span></span></span><span><span></span><span>512</span></span></span></span> 的灰度图，每个像素是 0 到 255 之间的一个数（8 位）。把所有像素排成一列就是向量 <span><span>x∈Rn\mathbf{x} \in \mathbb{R}^n</span><span><span><span></span><span>x</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>，<span><span>n=5122n = 512^2</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>51</span><span><span>2</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>；彩色图再乘 3。这个向量用的是标准基：第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个坐标就是第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个像素的灰度。</p><p>问题出在这里：相邻像素的灰度几乎一样。一块蓝衬衫上的像素是 121、124、124、126……领带才会换一种颜色。一块干净的黑板更极端，所有像素都一样，你却要一个一个把它们报出来。他的话是：标准基完全没有利用相邻像素相关这个事实。</p><p>所以他的想法是换基。有一个向量几乎是必须在基里的：全 1 向量。一个全 1 向量加一个系数，就能完整描述纯色图。交替的 <span><span>(1,−1,1,−1,… )(1, -1, 1, -1, \dots)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>)</span></span></span></span> 是另一个极端，是棋盘图案。再往中间走，是一半亮一半暗的 <span><span>(1,…,1,−1,…,−1)(1, \dots, 1, -1, \dots, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>。基是我们可以自由选的，他说这是“十亿美元的决定”，电视台和电影界各有偏好，吵得很凶，而吵架的内容归根到底是一个线性代数问题：选哪组基。</p><p>JPEG 的做法是：</p><ol>
<li>把大图切成 <span><span>8×88 \times 8</span><span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span></span></span></span> 的小块，每块有 64 个像素。</li>
<li>在每块的 64 维空间里换成 Fourier 基，得到 64 个系数 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span>。这一步无损，只是换了一组坐标。</li>
<li>阈值：把小的系数设成 0，得到有很多零的 <span><span>c^\hat{\mathbf{c}}</span><span><span><span></span><span><span><span><span><span><span></span><span>c</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>。这一步有损，是压缩真正发生的地方。</li>
<li>用留下的几项乘它们的基向量，重建图像。</li>
</ol><p>他估计光滑的块里 64 项只会留下两三项，“64 到 3，压缩 21 比 1”。（据我所知，实际的 JPEG 用的是离散余弦变换，和 Fourier 基是近亲，不过我没有去核对标准文本，这里按他的说法。）</p></section>
<section><h2>小波：他写在黑板上的 8 个向量<a href="#小波他写在黑板上的-8-个向量"><span>#</span></a></h2><p>Fourier 基的对手是小波。他在 <span><span>R8\mathbb{R}^8</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span></span></span></span></span></span></span></span> 里写了最简单的那一组：</p><ul>
<li>全 1：<span><span>(1,1,1,1,1,1,1,1)(1, 1, 1, 1, 1, 1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span></li>
<li>半正半负：<span><span>(1,1,1,1,−1,−1,−1,−1)(1, 1, 1, 1, -1, -1, -1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span></li>
<li>前四个里的半正半负：<span><span>(1,1,−1,−1,0,0,0,0)(1, 1, -1, -1, 0, 0, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，后四个里的：<span><span>(0,0,0,0,1,1,−1,−1)(0, 0, 0, 0, 1, 1, -1, -1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span></li>
<li>最后四个：<span><span>(1,−1,0,… )(1, -1, 0, \dots)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>…</span><span></span><span>)</span></span></span></span>、<span><span>(0,0,1,−1,0,… )(0, 0, 1, -1, 0, \dots)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>…</span><span></span><span>)</span></span></span></span> 等，每个只占两个位置</li>
</ul><p>把它们放在矩阵 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 的列里。他问这组向量有什么好性质：首先每个元素都是 1、-1、0，乘起来只有加减。更重要的是它们两两正交：全 1 和半正半负的点积是四个 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span> 加四个 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，等于 0。但它们长度不同（<span><span>8\sqrt{8}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>8</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>、<span><span>4\sqrt{4}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>、<span><span>2\sqrt{2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>），所以是正交基，不是标准正交基；每列除以自己的长度就是了。</p><p>这个矩阵我不想手敲。观察一下规律：全部小波可以递归地造出来，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶矩阵的前一半列是 <span><span>n/2n/2</span><span><span><span></span><span>n</span><span>/2</span></span></span></span> 阶矩阵的每一行抄两遍，后一半列是 <span><span>(1,−1)(1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 一对一对地沿对角线排。用 <code>np.kron</code> 两行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>haar</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 n x n 的 Haar 小波矩阵，列是未归一化的基向量。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>top </span><span>=</span><span> np.</span><span>kron</span><span>(</span><span>haar</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>], [</span><span>1.0</span><span>]]))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>bottom </span><span>=</span><span> np.</span><span>kron</span><span>(np.</span><span>eye</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>]]))</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>hstack</span><span>([top, bottom])</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>, </span><span>linewidth</span><span>=</span><span>79</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> </span><span>haar</span><span>(</span><span>8</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span><span>(W.</span><span>astype</span><span>(</span></span><span>int</span><span>))</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 列两两正交，但长度不同：根号 8、根号 8、根号 4、根号 2</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> W.T </span><span>@</span><span> W</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"W^T W 是对角阵："</span><span><span>, np.</span><span>allclose</span><span>(G, np.</span><span>diag</span><span>(np.</span><span>diag</span><span>(G))))</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"列长的平方："</span><span><span>, np.</span><span>diag</span><span>(G).</span><span>astype</span><span>(</span></span><span>int</span><span>))</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 每列除以自己的长度，W^{-1} 就是 W^T</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> W </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(W, </span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"归一化后 W^T W = I："</span><span><span>, np.</span><span>allclose</span><span>(W.T </span><span>@</span><span> W, np.</span><span>eye</span><span>(</span></span><span>8</span><span>)))</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"inv(W) 等于 W^T："</span><span><span>, np.</span><span>allclose</span><span>(np.linalg.</span><span>inv</span><span>(W), W.T))</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 最快的向量 (1, -1, 1, -1, ...) 是最后四个小波之和</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>fast </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>] </span><span>*</span><span> </span></span><span>4</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> W.T </span><span>@</span><span> fast</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"最快向量的系数 c ="</span><span>, c)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"W @ c 还原出原向量："</span><span><span>, np.</span><span>allclose</span><span>(W </span><span>@</span><span> c, fast))</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[[ 1  1  1  0  1  0  0  0]</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[ 1  1  1  0 -1  0  0  0]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 1  1 -1  0  0  1  0  0]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 1  1 -1  0  0 -1  0  0]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 1 -1  0  1  0  0  1  0]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[ 1 -1  0  1  0  0 -1  0]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ 1 -1  0 -1  0  0  0  1]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[ 1 -1  0 -1  0  0  0 -1]]</span></div></div><div><div><div>9</div></div><div><span>W^T W 是对角阵： True</span></div></div><div><div><div>10</div></div><div><span>列长的平方： [8 8 4 4 2 2 2 2]</span></div></div><div><div><div>11</div></div><div><span>归一化后 W^T W = I： True</span></div></div><div><div><div>12</div></div><div><span>inv(W) 等于 W^T： True</span></div></div><div><div><div>13</div></div><div><span>最快向量的系数 c = [0.     0.     0.     0.     1.4142 1.4142 1.4142 1.4142]</span></div></div><div><div><div>14</div></div><div><span>W @ c 还原出原向量： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>打印出来的 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 和他黑板上的 8 列一模一样，包括列的顺序。<span><span>WTWW^\mathsf{T}W</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>W</span></span></span></span> 是对角阵，列长的平方是 <span><span>(8,8,4,4,2,2,2,2)(8, 8, 4, 4, 2, 2, 2, 2)</span><span><span><span></span><span>(</span><span>8</span><span>,</span><span></span><span>8</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，所以除以长度之后 <span><span>WTW=IW^\mathsf{T}W = I</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>W</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，<code>inv(W)</code> 和 <code>W.T</code> 相等。这是第 17 讲的结论：标准正交列的矩阵，逆就是转置，变基不用解方程。</p><p>他还问了一个小问题：交替向量 <span><span>(1,−1,1,−1,… )(1, -1, 1, -1, \dots)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>)</span></span></span></span>，用小波基怎么写？答案是最后四个小波之和。最后一行的系数前四个是 0，后四个都是 <span><span>2=1.4142\sqrt{2} = 1.4142</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.4142</span></span></span></span>（因为归一化之后每个小波的长度从 <span><span>2\sqrt{2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 变成了 1），<code>W @ c</code> 确实还原出了原向量。</p></section>
<section><h2>变基就是解 <span><span>P=Wc\mathbf{P} = W\mathbf{c}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>W</span><span>c</span></span></span></span><a href="#变基就是解-pwcmathbfp--wmathbfcpwc"><span>#</span></a></h2><p>现在可以说线性代数在哪里了。他取一行 8 个像素 <span><span>P=(P1,…,P8)\mathbf{P} = (P_1, \dots, P_8)</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，想把它写成小波的组合：</p><span><span><span>P=c1w1+c2w2+⋯+c8w8=Wc\mathbf{P} = c_1 \mathbf{w}_1 + c_2 \mathbf{w}_2 + \cdots + c_8 \mathbf{w}_8 = W\mathbf{c}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>W</span><span>c</span></span></span></span></span><p>这是这门课里一直在做的事：基向量放在列里，列的组合给出向量。系数是 <span><span>c=W−1P\mathbf{c} = W^{-1}\mathbf{P}</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>P</span></span></span></span>。他在这里停下来强调：<strong>好基的标准有两条</strong>。</p><ul>
<li><strong>要快。</strong> <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 和 <span><span>W−1W^{-1}</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 乘向量都得很快，否则变基本身比省下来的还贵。Fourier 基有 FFT（第 26 讲），小波也有快速算法；对我们的 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span>，因为列标准正交，<span><span>W−1=WTW^{-1} = W^\mathsf{T}</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，逆和正变换一样便宜。</li>
<li><strong>要省。</strong> 少数几个基向量就应该接近原信号。最快的变换是不换基，可是标准基里一扔就是丢像素，他说如果扔掉 90% 的像素，图就“黑”了。</li>
</ul><p>下面的块把两条都验一遍。像素行我自己编的：左边四个是蓝衬衫（121 到 126），右边四个是领带（60 到 63）。快速算法不造矩阵，只是反复做平均和差分，每次把长度折半：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>haar</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 n x n 的归一化 Haar 矩阵，列是基向量。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>top </span><span>=</span><span> np.</span><span>kron</span><span>(</span><span>haar</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>], [</span><span>1.0</span><span>]]))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>bottom </span><span>=</span><span> np.</span><span>kron</span><span>(np.</span><span>eye</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>]]))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> np.</span><span>hstack</span><span>([top, bottom])</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span><span> W </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(W, </span></span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>fast_haar</span><span>(</span><span>p</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>"""不构造矩阵，用反复的平均与差分算 W^T p。"""</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>details </span><span>=</span><span> []</span></div></div><div><div><div>17</div></div><div><span>    </span><span>while</span><span> </span><span>len</span><span><span>(p) </span><span>&gt;</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>average </span><span>=</span><span> (p[</span><span>0</span><span>::</span><span>2</span><span><span>] </span><span>+</span><span> p[</span></span><span>1</span><span>::</span><span>2</span><span><span>]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>detail </span><span>=</span><span> (p[</span><span>0</span><span>::</span><span>2</span><span><span>] </span><span>-</span><span> p[</span></span><span>1</span><span>::</span><span>2</span><span><span>]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>details </span><span>=</span><span> [detail] </span><span>+</span><span> details</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> average</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>concatenate</span><span>([p] </span><span>+</span><span> details)</span></span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>def</span><span> </span><span>keep_largest</span><span>(</span><span>c</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>k</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>26</div></div><div><span>    </span><span>"""只保留绝对值最大的 k 个系数，其余置零。"""</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>out </span><span>=</span><span> np.</span><span>zeros_like</span><span>(c)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>idx </span><span>=</span><span> np.</span><span>argsort</span><span>(</span><span>-</span><span>np.</span><span>abs</span><span>(c))[:k]</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>out[idx] </span><span>=</span><span> c[idx]</span></div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span> out</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>2</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>, </span><span>linewidth</span><span>=</span><span>79</span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> </span><span>haar</span><span>(</span><span>8</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span># 一行 8 个像素：左边是蓝衬衫，右边是领带</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>121.0</span><span>, </span><span>124.0</span><span>, </span><span>124.0</span><span>, </span><span>126.0</span><span>, </span><span>60.0</span><span>, </span><span>62.0</span><span>, </span><span>63.0</span><span>, </span><span>61.0</span><span>])</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> W.T </span><span>@</span><span> P</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>, P)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"c ="</span><span>, c)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"快速算法与 W^T P 一致："</span><span><span>, np.</span><span>allclose</span><span>(</span><span>fast_haar</span><span>(P), c))</span></span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"乘法次数：矩阵"</span><span>, </span><span>8</span><span><span> </span><span>*</span><span> </span></span><span>8</span><span>, </span><span>"，快速算法的加减次数"</span><span>, </span><span>2</span><span><span> </span><span>*</span><span> (</span></span><span>8</span><span><span> </span><span>-</span><span> </span></span><span>1</span><span>))</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span># 同样只留 k 个数：标准基里留最大的像素，小波基里留最大的系数</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"k  标准基相对误差  小波基相对误差"</span><span>)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> [</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>]:</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>std </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(P </span><span>-</span><span> </span><span>keep_largest</span><span>(P, k)) </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(P)</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>wav </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(P </span><span>-</span><span> W </span><span>@</span><span> </span><span>keep_largest</span><span>(c, k)) </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(P)</span></div></div><div><div><div>50</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>k</span><span>}</span><span>  </span><span>{</span><span>std</span><span>:14.4f</span><span>}</span><span>  </span><span>{</span><span>wav</span><span>:14.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P = [121. 124. 124. 126.  60.  62.  63.  61.]</span></div></div><div><div><div>2</div></div><div><span>c = [261.98  88.03  -2.5   -1.    -2.12  -1.41  -1.41   1.41]</span></div></div><div><div><div>3</div></div><div><span>快速算法与 W^T P 一致： True</span></div></div><div><div><div>4</div></div><div><span>乘法次数：矩阵 64 ，快速算法的加减次数 14</span></div></div><div><div><div>5</div></div><div><span>k  标准基相对误差  小波基相对误差</span></div></div><div><div><div>6</div></div><div><span>1          0.8901          0.3189</span></div></div><div><div><div>7</div></div><div><span>2          0.7687          0.0152</span></div></div><div><div><div>8</div></div><div><span>3          0.6243          0.0123</span></div></div><div><div><div>9</div></div><div><span>4          0.4451          0.0096</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先看系数 <span><span>c=(261.98,88.03,−2.5,… )\mathbf{c} = (261.98, 88.03, -2.5, \dots)</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>261.98</span><span>,</span><span></span><span>88.03</span><span>,</span><span></span><span>−</span><span>2.5</span><span>,</span><span></span><span>…</span><span></span><span>)</span></span></span></span>。第一个是整行的平均（乘了 <span><span>8\sqrt{8}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>8</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>），第二个 88.03 是“左半边比右半边亮多少”，两个就带走了几乎全部信息，后面六个都在 <span><span>±3\pm 3</span><span><span><span></span><span>±</span><span>3</span></span></span></span> 以内。</p><p>比较是在同样只留 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个数的前提下做的。<span><span>k=2k = 2</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 时，小波基的相对误差是 0.0152，而标准基里只留最大的两个像素，误差是 0.7687，差了 50 倍。到 <span><span>k=4k = 4</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span> 小波基是 0.0096，标准基还有 0.4451，一半的向量没了。</p><p>快速算法和 <code>W.T @ P</code> 的结果一致：矩阵乘需要 <span><span>8×8=648 \times 8 = 64</span><span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span> 次乘法，快速算法只要 14 次加减（再统一乘一个 <span><span>1/21/\sqrt{2}</span><span><span><span></span><span>1/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>）。这是 8 个像素，<span><span>n=64n = 64</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span> 时是 4096 比 126，差距随 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 增长：矩阵乘是 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，快速算法是 <span><span>O(n)O(n)</span><span><span><span></span><span>O</span><span>(</span><span>n</span><span>)</span></span></span></span>。（所以小波在这里的卖点是“又省又便宜”，不是“又准又贵”。）</p></section>
<section><h2>整张图：Haar 对 SVD<a href="#整张图haar-对-svd"><span>#</span></a></h2><p>接下来把一维换成二维。图像 <span><span>XX</span><span><span><span></span><span>X</span></span></span></span> 是 <span><span>64×6464 \times 64</span><span><span><span></span><span>64</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span></span></span></span> 的矩阵，在列方向换一次基，在行方向再换一次：<span><span>C=WTXWC = W^\mathsf{T} X W</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>X</span><span>W</span></span></span></span>，反过来 <span><span>X=WCWTX = W C W^\mathsf{T}</span><span><span><span></span><span>X</span><span></span><span>=</span><span></span></span><span><span></span><span>W</span><span>C</span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（因为 <span><span>W−1=WTW^{-1} = W^\mathsf{T}</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>）。<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 里每个元素是 <span><span>XX</span><span><span><span></span><span>X</span></span></span></span> 在一个二维小波“砖块”上的系数。图是合成的，没有读外部文件：灰度从左到右渐变，加一个亮圆盘、一个暗方块，再加标准差为 2 的高斯噪声。</p><p>压缩比较什么？我拿三种东西比：</p><ul>
<li>Haar：保留 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 里最大的 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个系数。</li>
<li>标准基：保留 <span><span>XX</span><span><span><span></span><span>X</span></span></span></span> 里最大的 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个像素。</li>
<li>SVD：保留秩 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 的近似，需要存 <span><span>r(64+64+1)=129rr(64 + 64 + 1) = 129r</span><span><span><span></span><span>r</span><span>(</span><span>64</span><span></span><span>+</span><span></span></span><span><span></span><span>64</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>129</span><span>r</span></span></span></span> 个数，所以取 <span><span>k=129rk = 129r</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>129</span><span>r</span></span></span></span>。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>haar</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 n x n 的归一化 Haar 矩阵，列是基向量。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>top </span><span>=</span><span> np.</span><span>kron</span><span>(</span><span>haar</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>], [</span><span>1.0</span><span>]]))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>bottom </span><span>=</span><span> np.</span><span>kron</span><span>(np.</span><span>eye</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>]]))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> np.</span><span>hstack</span><span>([top, bottom])</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span><span> W </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(W, </span></span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>make_image</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>64</span></span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>"""合成灰度图：渐变背景、一个圆盘、一个方块、一点噪声。"""</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>r, c </span><span>=</span><span> np.mgrid[</span><span>0</span><span>:n, </span><span>0</span><span>:n]</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> </span><span>60.0</span><span><span> </span><span>+</span><span> </span></span><span>80.0</span><span><span> </span><span>*</span><span> c </span><span>/</span><span> (n </span><span>-</span><span> </span></span><span>1</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>X[(r </span><span>-</span><span> </span><span>24</span><span><span>) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>+</span><span> (c </span><span>-</span><span> </span></span><span>22</span><span><span>) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>&lt;</span><span> </span></span><span>14</span><span>**</span><span>2</span><span><span>] </span><span>=</span><span> </span></span><span>220.0</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>X[</span><span>40</span><span>:</span><span>56</span><span>, </span><span>36</span><span>:</span><span>58</span><span><span>] </span><span>=</span><span> </span></span><span>20.0</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>X </span><span>+=</span><span> rng.</span><span>normal</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>size</span><span><span>=</span><span>(n, n))</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>clip</span><span>(X, </span></span><span>0.0</span><span>, </span><span>255.0</span><span>)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>def</span><span> </span><span>keep_largest</span><span>(</span><span>C</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>k</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>26</div></div><div><span>    </span><span>"""只保留绝对值最大的 k 个元素，其余置零。"""</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>flat </span><span>=</span><span> np.</span><span>zeros</span><span>(C.size)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>idx </span><span>=</span><span> np.</span><span>argsort</span><span>(</span><span>-</span><span>np.</span><span>abs</span><span>(C).</span><span>ravel</span><span>())[:k]</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>flat[idx] </span><span>=</span><span> C.</span><span>ravel</span><span>()[idx]</span></div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span><span> flat.</span><span>reshape</span><span>(C.shape)</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>def</span><span> </span><span>rel_err</span><span>(</span><span>X</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>Y</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>34</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.linalg.</span><span>norm</span><span>(X </span><span>-</span><span> Y) </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(X))</span></span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> </span><span>haar</span><span>(</span><span>64</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> </span><span>make_image</span><span>()</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span># 二维变换：先对每一列做 W^T，再对每一行做 W^T</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> W.T </span><span>@</span><span> X </span><span>@</span><span> W</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"W^T W = I："</span><span><span>, np.</span><span>allclose</span><span>(W.T </span><span>@</span><span> W, np.</span><span>eye</span><span>(</span></span><span>64</span><span>)))</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"无损往返的相对误差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>rel_err</span><span>(X, W </span><span>@</span><span> C </span><span>@</span><span> W.T)</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"能量不变："</span><span><span>, np.</span><span>isclose</span><span>(np.linalg.</span><span>norm</span><span>(X), np.linalg.</span><span>norm</span><span>(C)))</span></span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 公平一点的比较：SVD 秩 r 要存 r * (64 + 64 + 1) = 129 r 个数</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(X)</span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span><span>"存的数  Haar    像素    SVD"</span><span>)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>for</span><span> r </span><span>in</span><span> [</span><span>1</span><span>, </span><span>2</span><span>, </span><span>4</span><span>, </span><span>8</span><span>]:</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> </span><span>129</span><span><span> </span><span>*</span><span> r</span></span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>e_haar </span><span>=</span><span> </span><span>rel_err</span><span>(X, W </span><span>@</span><span> </span><span>keep_largest</span><span>(C, k) </span><span>@</span><span> W.T)</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>e_pixel </span><span>=</span><span> </span><span>rel_err</span><span>(X, </span><span>keep_largest</span><span>(X, k))</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>e_svd </span><span>=</span><span> </span><span>rel_err</span><span>(X, (U[:, :r] </span><span>*</span><span> s[:r]) </span><span>@</span><span> Vt[:r])</span></div></div><div><div><div>55</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>k</span><span>:6d</span><span>}</span><span>  </span><span>{</span><span>e_haar</span><span>:.4f</span><span>}</span><span>  </span><span>{</span><span>e_pixel</span><span>:.4f</span><span>}</span><span>  </span><span>{</span><span>e_svd</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>W^T W = I： True</span></div></div><div><div><div>2</div></div><div><span>无损往返的相对误差： 3.2e-16</span></div></div><div><div><div>3</div></div><div><span>能量不变： True</span></div></div><div><div><div>4</div></div><div><span>存的数  Haar    像素    SVD</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>129  0.0708  0.9479  0.2506</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>258  0.0336  0.8937  0.1798</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>516  0.0142  0.7760  0.0795</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>1032  0.0100  0.6386  0.0295</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前三行是检查：<span><span>WTW=IW^\mathsf{T}W = I</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>W</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，不压缩的往返误差是 <span><span>3.2×10−163.2 \times 10^{-16}</span><span><span><span></span><span>3.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>∥X∥F=∥C∥F\|X\|_F = \|C\|_F</span><span><span><span></span><span>∥</span><span>X</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>F</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>C</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>F</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（标准正交基不改变长度，第 17 讲的事实）。</p><p>然后是表。存 516 个数，Haar 的相对误差 0.0142，SVD 0.0795，像素 0.7760。这时 Haar 比 SVD 好 5.6 倍，比像素好 55 倍。存 129 个数时，Haar 是 0.0708，SVD 是 0.2506。再往后到 1032 个数，Haar 误差只降到 0.0100：它碰到了噪声的地板，剩下的系数都是噪声，没必要再存。</p><p></p><figure><img src="/assets/images/linear-algebra/l31-original-haar-svd.svg" alt="左：原图。中：Haar 基下保留 516 个最大系数，相对误差 0.0142。右：秩 4 的 SVD 近似，同样 516 个数，相对误差 0.0795" /><figcaption>左：原图。中：Haar 基下保留 516 个最大系数，相对误差 0.0142。右：秩 4 的 SVD 近似，同样 516 个数，相对误差 0.0795</figcaption></figure><p></p><p>图的右边解释了为什么 SVD 在这里输：秩 4 的近似把圆盘糊成了一团带阴影的方块。SVD 的基是整张图的行和列方向的一维向量，它们必须在整张图上起作用；而圆盘和方块的边缘是局部的、不规则的，需要很多个秩 1 项才能凑出来。小波基是“局部”的，一个边缘只牵连少数几个小波，其余系数都是 0。</p><p></p><figure><img src="/assets/images/linear-algebra/l31-error-vs-storage.svg" alt="三种方法的相对误差随存储的数的个数 k 变化，纵轴是对数坐标" /><figcaption>三种方法的相对误差随存储的数的个数 k 变化，纵轴是对数坐标</figcaption></figure><p></p><p>上面两张图由下面这个脚本生成（需要 matplotlib，在当前目录写出两个 SVG；图中的数据与前面的代码块完全相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>haar</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"""返回 n x n 的归一化 Haar 矩阵，列是基向量。"""</span></div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>top </span><span>=</span><span> np.</span><span>kron</span><span>(</span><span>haar</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>], [</span><span>1.0</span><span>]]))</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>bottom </span><span>=</span><span> np.</span><span>kron</span><span>(np.</span><span>eye</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>]]))</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> np.</span><span>hstack</span><span>([top, bottom])</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> W </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(W, </span></span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>make_image</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>64</span></span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>17</div></div><div><span>    </span><span>"""合成灰度图：渐变背景、一个圆盘、一个方块、一点噪声。"""</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>r, c </span><span>=</span><span> np.mgrid[</span><span>0</span><span>:n, </span><span>0</span><span>:n]</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> </span><span>60.0</span><span><span> </span><span>+</span><span> </span></span><span>80.0</span><span><span> </span><span>*</span><span> c </span><span>/</span><span> (n </span><span>-</span><span> </span></span><span>1</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>X[(r </span><span>-</span><span> </span><span>24</span><span><span>) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>+</span><span> (c </span><span>-</span><span> </span></span><span>22</span><span><span>) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>&lt;</span><span> </span></span><span>14</span><span>**</span><span>2</span><span><span>] </span><span>=</span><span> </span></span><span>220.0</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>X[</span><span>40</span><span>:</span><span>56</span><span>, </span><span>36</span><span>:</span><span>58</span><span><span>] </span><span>=</span><span> </span></span><span>20.0</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>X </span><span>+=</span><span> rng.</span><span>normal</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>size</span><span><span>=</span><span>(n, n))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>clip</span><span>(X, </span></span><span>0.0</span><span>, </span><span>255.0</span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>def</span><span> </span><span>keep_largest</span><span>(</span><span>C</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>k</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>28</div></div><div><span>    </span><span>"""只保留绝对值最大的 k 个元素，其余置零。"""</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>flat </span><span>=</span><span> np.</span><span>zeros</span><span>(C.size)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>idx </span><span>=</span><span> np.</span><span>argsort</span><span>(</span><span>-</span><span>np.</span><span>abs</span><span>(C).</span><span>ravel</span><span>())[:k]</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>flat[idx] </span><span>=</span><span> C.</span><span>ravel</span><span>()[idx]</span></div></div><div><div><div>32</div></div><div><span>    </span><span>return</span><span><span> flat.</span><span>reshape</span><span>(C.shape)</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>def</span><span> </span><span>rel_err</span><span>(</span><span>X</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>Y</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>36</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.linalg.</span><span>norm</span><span>(X </span><span>-</span><span> Y) </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(X))</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> </span><span>haar</span><span>(</span><span>64</span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> </span><span>make_image</span><span>()</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> W.T </span><span>@</span><span> X </span><span>@</span><span> W</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(X)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>k, r </span><span>=</span><span> </span><span>516</span><span>, </span><span>4</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>Xh </span><span>=</span><span> W </span><span>@</span><span> </span><span>keep_largest</span><span>(C, k) </span><span>@</span><span> W.T</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>Xs </span><span>=</span><span> (U[:, :r] </span><span>*</span><span> s[:r]) </span><span>@</span><span> Vt[:r]</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>fig, axes </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>3</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>panels </span><span>=</span><span> [</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>(X, </span><span>"original, 4096 numbers"</span><span>),</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>(Xh, </span><span>f</span><span>"Haar, top </span><span>{</span><span>k</span><span>}</span><span> coefficients</span><span>\n</span><span>error </span><span>{</span><span><span>rel_err</span><span>(X, Xh)</span></span><span>:.4f</span><span>}</span><span>"</span><span>),</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>(Xs, </span><span>f</span><span>"SVD, rank </span><span>{</span><span>r</span><span>}</span><span> (</span><span>{129</span><span><span> </span><span>*</span><span> r</span></span><span>}</span><span> numbers)</span><span>\n</span><span>error </span><span>{</span><span><span>rel_err</span><span>(X, Xs)</span></span><span>:.4f</span><span>}</span><span>"</span><span>),</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>57</div></div><div><span>    </span><span>for</span><span> ax, (img, title) </span><span>in</span><span> </span><span>zip</span><span>(axes, panels):</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>ax.</span><span>imshow</span><span>(img, </span><span>cmap</span><span>=</span><span>"gray"</span><span>, </span><span>vmin</span><span>=</span><span>0</span><span>, </span><span>vmax</span><span>=</span><span>255</span><span>)</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>ax.</span><span>set_title</span><span>(title, </span><span>fontsize</span><span>=</span><span>10</span><span>)</span></div></div><div><div><div>60</div></div><div><span><span>        </span></span><span>ax.</span><span>axis</span><span>(</span><span>"off"</span><span>)</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span><span>"l31-original-haar-svd.svg"</span><span>, </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>})</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>ks </span><span>=</span><span> [</span><span>129</span><span><span> </span><span>*</span><span> r </span></span><span>for</span><span> r </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>17</span><span>)]</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>fig, ax </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>figsize</span><span><span>=</span><span>(</span></span><span>6</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>ax.</span><span>semilogy</span><span>(</span></div></div><div><div><div>67</div></div><div><span><span>        </span></span><span>ks,</span></div></div><div><div><div>68</div></div><div><span><span>        </span></span><span>[</span><span>rel_err</span><span>(X, W </span><span>@</span><span> </span><span>keep_largest</span><span>(C, k) </span><span>@</span><span> W.T) </span><span>for</span><span> k </span><span>in</span><span> ks],</span></div></div><div><div><div>69</div></div><div><span>        </span><span>"o-"</span><span>,</span></div></div><div><div><div>70</div></div><div><span>        </span><span>label</span><span>=</span><span>"Haar, top k coefficients"</span><span>,</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>    </span></span><span>ax.</span><span>semilogy</span><span>(</span></div></div><div><div><div>73</div></div><div><span><span>        </span></span><span>ks,</span></div></div><div><div><div>74</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>75</div></div><div><span><span>            </span></span><span>rel_err</span><span>(X, (U[:, : k </span><span>//</span><span> </span><span>129</span><span><span>] </span><span>*</span><span> s[: k </span><span>//</span><span> </span></span><span>129</span><span><span>]) </span><span>@</span><span> Vt[: k </span><span>//</span><span> </span></span><span>129</span><span>])</span></div></div><div><div><div>76</div></div><div><span>            </span><span>for</span><span> k </span><span>in</span><span> ks</span></div></div><div><div><div>77</div></div><div><span><span>        </span></span><span>],</span></div></div><div><div><div>78</div></div><div><span>        </span><span>"s-"</span><span>,</span></div></div><div><div><div>79</div></div><div><span>        </span><span>label</span><span>=</span><span>"SVD, rank k/129"</span><span>,</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>ax.</span><span>semilogy</span><span>(</span></div></div><div><div><div>82</div></div><div><span><span>        </span></span><span>ks,</span></div></div><div><div><div>83</div></div><div><span><span>        </span></span><span>[</span><span>rel_err</span><span>(X, </span><span>keep_largest</span><span>(X, k)) </span><span>for</span><span> k </span><span>in</span><span> ks],</span></div></div><div><div><div>84</div></div><div><span>        </span><span>"^-"</span><span>,</span></div></div><div><div><div>85</div></div><div><span>        </span><span>label</span><span>=</span><span>"standard basis, top k pixels"</span><span>,</span></div></div><div><div><div>86</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>87</div></div><div><span><span>    </span></span><span>ax.</span><span>set_xlabel</span><span>(</span><span>"numbers stored k"</span><span>)</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>ax.</span><span>set_ylabel</span><span>(</span><span>"relative error"</span><span>)</span></div></div><div><div><div>89</div></div><div><span><span>    </span></span><span>ax.</span><span>legend</span><span>()</span></div></div><div><div><div>90</div></div><div><span><span>    </span></span><span>ax.</span><span>grid</span><span>(</span><span>True</span><span>, </span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>91</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span><span>"l31-error-vs-storage.svg"</span><span>, </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>})</span></div></div><div><div><div>93</div></div><div>
</div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>96</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我得对这个比较做三点保留，不然就是不公平的广告。一，Haar 的 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个系数还要加上它们的位置（索引），我没把索引算进存储，所以 5.6 倍是上界，不是实际压缩比。二，这张图是我挑的：分段平滑加锐利边缘，正好是 Haar 的主场；换一张纹理很多的真实照片，我猜差距会小得多。三，Strang 说的是 <span><span>8×88 \times 8</span><span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span></span></span></span> 分块，我这里是整张 <span><span>64×6464 \times 64</span><span><span><span></span><span>64</span><span></span><span>×</span><span></span></span><span><span></span><span>64</span></span></span></span> 一起变，分块的好处是局部性和便宜，坏处是块边界可能露出来。</p></section>
<section><h2>同一个变换，两个矩阵：相似矩阵<a href="#同一个变换两个矩阵相似矩阵"><span>#</span></a></h2><p>讲完应用，他回到数学，而且开头就说“这一部分是压缩过的”（大概是时间不够了）。结论是这样的：设 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 是一个线性变换，在基 <span><span>v1,…,vn\mathbf{v}_1, \dots, \mathbf{v}_n</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 下的矩阵是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，在基 <span><span>w1,…,wn\mathbf{w}_1, \dots, \mathbf{w}_n</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 下的矩阵是 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span>，那么 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 相似：</p><span><span><span>B=M−1AMB = M^{-1} A M</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span></span></span></span></span><p><span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 是换基矩阵，它的列就是新基向量（在旧基下的坐标），和前面的 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 是同一类东西。换基之后两件事同时发生：每个向量有了新坐标（<span><span>x=Mc\mathbf{x} = M\mathbf{c}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>M</span><span>c</span></span></span></span>），每个线性变换有了新矩阵。</p><p>这里有一个类比，我只用这一次：同一句话用两种语言写出来，字数、词序都变了，意思没变。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 是同一个变换的两种“写法”，所以凡是和写法无关的量（特征值、迹、行列式）两边必须相等。</p><p>怎么得到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>？他把上一讲的配方重复了一遍：把 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 作用在每个基向量上，把结果写成基向量的组合，系数放进矩阵的一列。我用一个上三角矩阵验证上面的关系：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>, </span><span>linewidth</span><span>=</span><span>79</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 同一个线性变换：在标准基下的矩阵是 A</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 新基的三个向量放在 M 的列里</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(M, x)  </span><span># x 在新基下的坐标，c = M^{-1} x</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"M c 还原出 x："</span><span><span>, np.</span><span>allclose</span><span>(M </span><span>@</span><span> c, x))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># T(x) 的新坐标：先变回旧坐标，用 A，再变回新坐标</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> A </span><span>@</span><span> M</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>20</div></div><div><span>        </span><span>"B @ c = M^{-1} (A x)："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> c, np.linalg.</span><span>solve</span><span>(M, A </span><span>@</span><span> x))</span></span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A)))</span></span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(B)))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的迹与行列式："</span><span><span>, np.</span><span>trace</span><span>(A), np.linalg.</span><span>det</span><span>(A))</span></span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的迹与行列式："</span><span><span>, np.</span><span>trace</span><span>(B), np.linalg.</span><span>det</span><span>(B))</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 新基取特征向量：矩阵变成对角阵</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>lam, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>Lambda </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(S) </span><span>@</span><span> A </span><span>@</span><span> S</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量基下的矩阵："</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(Lambda)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"它是对角阵："</span><span><span>, np.</span><span>allclose</span><span>(Lambda, np.</span><span>diag</span><span>(lam)))</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>M c 还原出 x： True</span></div></div><div><div><div>2</div></div><div><span>B @ c = M^{-1} (A x)： True</span></div></div><div><div><div>3</div></div><div><span>A 的特征值： [1. 2. 3.]</span></div></div><div><div><div>4</div></div><div><span>B 的特征值： [1. 2. 3.]</span></div></div><div><div><div>5</div></div><div><span>A 的迹与行列式： 6.0 6.0</span></div></div><div><div><div>6</div></div><div><span>B 的迹与行列式： 6.0 6.000000000000001</span></div></div><div><div><div>7</div></div><div><span>特征向量基下的矩阵：</span></div></div><div><div><div>8</div></div><div><span>[[2. 0. 0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[0. 3. 0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0. 0. 1.]]</span></div></div><div><div><div>11</div></div><div><span>它是对角阵： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 是用随机的 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>（固定种子）得到的，看起来和 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 毫无关系，但特征值都是 <span><span>(1,2,3)(1, 2, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，迹都是 6，行列式都是 6（<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的行列式那一个末位是 <span><span>10−1510^{-15}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 量级的浮点噪声）。向量的坐标也对得上：<code>B @ c</code> 等于 <span><span>M−1(Ax)M^{-1}(A\mathbf{x})</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>A</span><span>x</span><span>)</span></span></span></span>，也就是先在旧坐标里作用 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，再换成新坐标。</p><p>最后一个例子是他留到最后一分钟的：如果基向量 <span><span>vi\mathbf{v}_i</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是特征向量，<span><span>T(vi)=λiviT(\mathbf{v}_i) = \lambda_i \mathbf{v}_i</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 列的描述就是“<span><span>λi\lambda_i</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 份 <span><span>vi\mathbf{v}_i</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，其他基向量 0 份”，所以矩阵是对角阵。代码里最后几行就是这个：用 <code>np.linalg.eig</code> 的特征向量当 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>，<span><span>M−1AMM^{-1}AM</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span></span></span></span> 是对角阵 <span><span>diag(2,3,1)\mathrm{diag}(2, 3, 1)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。这是第 22 讲的 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 换一种说法。</p><p>他说这是“最好的基”，但求图像矩阵的特征向量太贵，所以工程里退一步，用小波或 Fourier 这种便宜、接近的基。“接近”能不能量化？拿一个 64 阶的平滑算子（每个像素换成它和左右邻居的 <span><span>(1,2,1)/4(1, 2, 1)/4</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span><span>/4</span></span></span></span> 加权平均）来试：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>haar</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 n x n 的归一化 Haar 矩阵，列是基向量。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>top </span><span>=</span><span> np.</span><span>kron</span><span>(</span><span>haar</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>], [</span><span>1.0</span><span>]]))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>bottom </span><span>=</span><span> np.</span><span>kron</span><span>(np.</span><span>eye</span><span>(n </span><span>//</span><span> </span><span>2</span><span><span>), np.</span><span>array</span><span>([[</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>]]))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> np.</span><span>hstack</span><span>([top, bottom])</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span><span> W </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(W, </span></span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>off_diagonal_share</span><span>(</span><span>B</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>"""非对角元素的能量占全部能量的比例。"""</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>off </span><span>=</span><span> B </span><span>-</span><span> np.</span><span>diag</span><span>(np.</span><span>diag</span><span>(B))</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.linalg.</span><span>norm</span><span>(off) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(B) </span><span>**</span><span> </span></span><span>2</span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>, </span><span>linewidth</span><span>=</span><span>79</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>64</span></div></div><div><div><div>23</div></div><div><span>    </span><span># 一个平滑算子：每个像素换成自己与左右邻居的加权平均 (1, 2, 1) / 4</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(n) </span><span>+</span><span> </span></span><span>0.25</span><span><span> </span><span>*</span><span> (np.</span><span>eye</span><span>(n, </span></span><span>k</span><span>=</span><span>1</span><span><span>) </span><span>+</span><span> np.</span><span>eye</span><span>(n, </span></span><span>k</span><span>=-</span><span>1</span><span>))</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>A[</span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> A[</span><span>-</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>0.75</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> </span><span>haar</span><span>(n)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>_, S </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A)  </span><span># 对称矩阵，特征向量正交，S^{-1} = S^T</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"标准基下非对角能量占比："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>off_diagonal_share</span><span>(A)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>32</div></div><div><span>        </span><span>"Haar 基下非对角能量占比："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>off_diagonal_share</span><span>(W.T </span><span>@</span><span> A </span><span>@</span><span> W)</span></span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>35</div></div><div><span>        </span><span>"特征向量基下非对角能量占比："</span><span>,</span></div></div><div><div><div>36</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>off_diagonal_share</span><span>(S.T </span><span>@</span><span> A </span><span>@</span><span> S)</span></span><span>:.2e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>标准基下非对角能量占比： 0.3214</span></div></div><div><div><div>2</div></div><div><span>Haar 基下非对角能量占比： 0.1382</span></div></div><div><div><div>3</div></div><div><span>特征向量基下非对角能量占比： 1.32e-30</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用非对角元素的能量占比来衡量离对角阵有多远。标准基下是 0.3214，Haar 基下降到 0.1382，不到一半；特征向量基是 <span><span>10−3010^{-30}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>30</span></span></span></span></span></span></span></span></span></span></span></span>，也就是对角阵。Haar 没有做到对角，但把约 57% 的非对角能量搬回了对角线，我觉得这个“便宜又接近”的取舍，才是 Strang 说的“十亿美元的决定”的实质。（对称矩阵用 <code>np.linalg.eigh</code>，特征向量标准正交，所以逆也是转置，和 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 一样。）</p><p>下一讲是第三次测验的复习，用 NumPy 把字幕里的题再过一遍，见 <a href="/posts/linear-algebra-32-quiz-3-review/">18.06 第 32 讲：第三次测验复习</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-30-linear-transformations/</id>
      <title type="text">18.06 第 30 讲：线性变换及其矩阵</title>
      <published>2026-10-18T10:00:00.000Z</published>
      <updated>2026-10-18T10:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-30-linear-transformations/"/>
      <summary type="text">MIT 18.06 第 30 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把一座房子的六个顶点交给翻转、旋转和投影三个矩阵，又把多项式求导写成 2 × 3 矩阵，平移的线性性误差是 3.1623，而投影把房子的面积压成了 0 倍。</summary>
      <content type="html"><![CDATA[<p>Strang 说，线性代数课以前就是从这一讲开始的：先有变换，后有矩阵。变换是“把每个输入变成一个输出”的规则，矩阵只是你选定了坐标之后，这条规则写在纸上的样子。我把他黑板上的投影、旋转和那座“房子”都交给 NumPy：对“平移整个平面”这个看上去很无辜的变换，线性性的误差是 3.1623，对投影是 0.0000；而同一个投影，在标准基下是满的矩阵，在特征向量基下就变成了 <span><span>diag(1,0)\mathrm{diag}(1, 0)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=Ts3o2I8_Mxc" target="_blank">Lecture 30: Linear transformations and their matrices</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-30-linear-transformations-and-their-matrices/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §8.1 The Idea of a Linear Transformation；§8.2 The Matrix of a Linear Transformation
前置：<a href="/posts/linear-algebra-29-svd/">第 29 讲：奇异值分解</a>
一句话：选定输入基和输出基之后，线性变换对应唯一的矩阵，它的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列就是第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个输入基向量被变换之后，在输出基下的坐标。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先不画坐标轴，用投影和旋转讲“什么是线性变换”，再举平移和取长度两个反例。这一段是整讲的概念核心，值得认真听，本篇第一节只是把它们数值化。</li>
<li>“房子”那段很短，他把矩阵 <span><span>diag(1,−1)\mathrm{diag}(1, -1)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 作用在房子上。本篇第二节算过，可以倍速。</li>
<li>他在黑板上写“造矩阵的规则”（先变换第一个基向量，再把结果写成输出基的组合，系数放进第一列）的那几分钟要停下来跟着写一遍，它是后面所有内容的依据。</li>
<li>最后的求导例子很快，本篇最后一节用代码重做了一遍。结尾两句（逆矩阵对应逆变换、乘积对应复合）是下一讲的伏笔。</li>
</ul></div></div>
<section><h2>什么样的变换算线性<a href="#什么样的变换算线性"><span>#</span></a></h2><p><strong>先说变换是什么。</strong> 它就是一个函数：你给一个输入向量 <span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span>，它给一个输出向量 <span><span>T(v)T(\mathbf{v})</span><span><span><span></span><span>T</span><span>(</span><span>v</span><span>)</span></span></span></span>。Strang 的第一个例子是投影：平面上画一条直线，把每个向量投影到这条直线上。他特意说他不想画坐标轴，规则里没有矩阵，只有“垂直落到直线上”这个几何动作。</p><p>线性的要求只有两条：<span><span>T(v+w)=T(v)+T(w)T(\mathbf{v} + \mathbf{w}) = T(\mathbf{v}) + T(\mathbf{w})</span><span><span><span></span><span>T</span><span>(</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>T</span><span>(</span><span>v</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>T</span><span>(</span><span>w</span><span>)</span></span></span></span> 和 <span><span>T(cv)=cT(v)T(c\mathbf{v}) = cT(\mathbf{v})</span><span><span><span></span><span>T</span><span>(</span><span>c</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span><span>T</span><span>(</span><span>v</span><span>)</span></span></span></span>。他把两条合成一条：对任意线性组合，<span><span>T(cv+dw)=cT(v)+dT(w)T(c\mathbf{v} + d\mathbf{w}) = cT(\mathbf{v}) + dT(\mathbf{w})</span><span><span><span></span><span>T</span><span>(</span><span>c</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span><span>w</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span><span>T</span><span>(</span><span>v</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span><span>T</span><span>(</span><span>w</span><span>)</span></span></span></span>。向量加法和数乘是变换必须“尊重”的两种运算，别的什么都不用管。</p><p>他给了两个例子和两个反例：</p><ul>
<li>投影：线性。向量变成两倍，投影也是两倍；<span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 变成 <span><span>−v-\mathbf{v}</span><span><span><span></span><span>−</span><span>v</span></span></span></span>，投影也变号。</li>
<li>旋转 45 度：线性。先加再转，和先转再加，结果一样。</li>
<li>平移整个平面，<span><span>T(v)=v+v0T(\mathbf{v}) = \mathbf{v} + \mathbf{v}_0</span><span><span><span></span><span>T</span><span>(</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：不线性。向量变成两倍，它只多加了一个 <span><span>v0\mathbf{v}_0</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，而不是两个。</li>
<li>取长度，<span><span>T(v)=∥v∥T(\mathbf{v}) = \|\mathbf{v}\|</span><span><span><span></span><span>T</span><span>(</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>v</span><span>∥</span></span></span></span>：不线性。<span><span>T(0)=0T(\mathbf{0}) = 0</span><span><span><span></span><span>T</span><span>(</span><span>0</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 成立，<span><span>c=2c = 2</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 也成立，但 <span><span>c=−2c = -2</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span> 的时候长度不会乘 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span>，只会翻倍。</li>
</ul><p>他还顺手指出一个很便宜的检查：取 <span><span>c=3c = 3</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>、<span><span>v=0\mathbf{v} = \mathbf{0}</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，线性要求 <span><span>T(0)=3T(0)T(\mathbf{0}) = 3T(\mathbf{0})</span><span><span><span></span><span>T</span><span>(</span><span>0</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>T</span><span>(</span><span>0</span><span>)</span></span></span></span>，所以线性变换必须把零向量送到零向量。平移一上来就没过这关。</p><p>下面用固定种子的随机向量把四个变换逐一测一遍。四个变换都是用几何或函数规则直接写的，没有矩阵，和黑板上一致：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>project</span><span>(</span><span>v</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span># 投影到 45 度的直线上，不涉及任何矩阵，只用几何规则</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>2.0</span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> (u </span><span>@</span><span> v) </span><span>*</span><span> u</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>rotate</span><span>(</span><span>v</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>11</div></div><div><span>    </span><span># 逆时针转 45 度，同样直接用三角函数描述</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.pi </span><span>/</span><span> </span><span>4</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>(</span></span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>np.</span><span>cos</span><span>(t) </span><span>*</span><span> v[</span><span>0</span><span><span>] </span><span>-</span><span> np.</span><span>sin</span><span>(t) </span><span>*</span><span> v[</span></span><span>1</span><span>],</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>np.</span><span>sin</span><span>(t) </span><span>*</span><span> v[</span><span>0</span><span><span>] </span><span>+</span><span> np.</span><span>cos</span><span>(t) </span><span>*</span><span> v[</span></span><span>1</span><span>],</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>def</span><span> </span><span>shift</span><span>(</span><span>v</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span><span> v </span><span>+</span><span> np.</span><span>array</span><span>([</span></span><span>3.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>def</span><span> </span><span>length</span><span>(</span><span>v</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([np.linalg.</span><span>norm</span><span>(v)])</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>c, d </span><span>=</span><span> </span><span>2.0</span><span><span>, </span><span>-</span></span><span>2.0</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> name, T </span><span>in</span><span> [</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>(</span><span>"投影"</span><span>, project),</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>(</span><span>"旋转"</span><span>, rotate),</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>(</span><span>"平移"</span><span>, shift),</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>(</span><span>"取长度"</span><span>, length),</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>]:</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>zero </span><span>=</span><span> np.</span><span>allclose</span><span>(</span><span>T</span><span>(np.</span><span>zeros</span><span>(</span><span>2</span><span>)), </span><span>0.0</span><span>)</span></div></div><div><div><div>43</div></div><div><span>        </span><span># 线性的两条要求合成一条：T(c v + d w) = c T(v) + d T(w)</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(</span><span>T</span><span>(c </span><span>*</span><span> v </span><span>+</span><span> d </span><span>*</span><span> w) </span><span>-</span><span> (c </span><span>*</span><span> </span><span>T</span><span>(v) </span><span>+</span><span> d </span><span>*</span><span> </span><span>T</span><span>(w)))</span></div></div><div><div><div>45</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：T(0) = 0 吗？</span><span>{</span><span>zero</span><span>}</span><span>，线性性的误差 = </span><span>{</span><span>err</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 取长度：乘 -2 之后长度还是正的，不会乘 -2</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span><span>"|v| ="</span><span><span>, </span><span>length</span><span>(v), </span></span><span>"，|-2v| ="</span><span><span>, </span><span>length</span><span>(</span><span>-</span></span><span>2.0</span><span><span> </span><span>*</span><span> v))</span></span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>投影：T(0) = 0 吗？True，线性性的误差 = 0.0000</span></div></div><div><div><div>2</div></div><div><span>旋转：T(0) = 0 吗？True，线性性的误差 = 0.0000</span></div></div><div><div><div>3</div></div><div><span>平移：T(0) = 0 吗？False，线性性的误差 = 3.1623</span></div></div><div><div><div>4</div></div><div><span>取长度：T(0) = 0 吗？True，线性性的误差 = 1.7145</span></div></div><div><div><div>5</div></div><div><span>|v| = [0.7072] ，|-2v| = [1.4144]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>投影和旋转的误差是 0.0000，平移是 3.1623（恰好是 <span><span>∥v0∥=10\|\mathbf{v}_0\| = \sqrt{10}</span><span><span><span></span><span>∥</span><span><span>v</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>10</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>：我取的 <span><span>c+d=0c + d = 0</span><span><span><span></span><span>c</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，线性组合里的 <span><span>v0\mathbf{v}_0</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 全部抵消，变换自己却多添了一个）。取长度的误差是 1.7145，而它的 <span><span>T(0)=0T(\mathbf{0}) = 0</span><span><span><span></span><span>T</span><span>(</span><span>0</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 是通过的：我故意把这一栏打印出来，是想让你看到“<span><span>T(0)=0T(\mathbf{0}) = 0</span><span><span><span></span><span>T</span><span>(</span><span>0</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>”只是必要条件，不是充分条件。最后一行把 <span><span>c=−2c = -2</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span> 的问题摆出来：<span><span>∥v∥=0.7072\|\mathbf{v}\| = 0.7072</span><span><span><span></span><span>∥</span><span>v</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7072</span></span></span></span>，<span><span>∥−2v∥=1.4144\|-2\mathbf{v}\| = 1.4144</span><span><span><span></span><span>∥</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>v</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>1.4144</span></span></span></span>，是 <span><span>+2+2</span><span><span><span></span><span>+</span><span>2</span></span></span></span> 倍，不是 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 倍。</p></section>
<section><h2>矩阵乘法给出一大族线性变换<a href="#矩阵乘法给出一大族线性变换"><span>#</span></a></h2><p><strong>怎么造一个一定线性的变换？</strong> 取一个矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，规定 <span><span>T(v)=AvT(\mathbf{v}) = A\mathbf{v}</span><span><span><span></span><span>T</span><span>(</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>v</span></span></span></span>。<span><span>A(v+w)=Av+AwA(\mathbf{v} + \mathbf{w}) = A\mathbf{v} + A\mathbf{w}</span><span><span><span></span><span>A</span><span>(</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>A</span><span>w</span></span></span></span> 和 <span><span>A(cv)=cAvA(c\mathbf{v}) = cA\mathbf{v}</span><span><span><span></span><span>A</span><span>(</span><span>c</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span><span>A</span><span>v</span></span></span></span> 就是矩阵乘法的基本性质，不用再检查。他说：每个矩阵都是一个线性变换，所以这是一大族例子。</p><p>他想让你看的是整个平面一起动。画一座房子，轮廓是一圈顶点，对每个顶点做同一个变换，就看到整个房子怎么变。他取了 <span><span>A=[100−1]A = \begin{bmatrix} 1 &amp; 0 \\ 0 &amp; -1 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>：<span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 分量不变，<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 分量变号，房子倒过来，门也跟着到了下面。</p><p>我用同样的思路，给房子定了六个顶点（最后一个回到起点，轮廓是个五边形，屋顶尖在 <span><span>(1,3)(1, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>），然后依次交给翻转、旋转 45 度和投影到 45 度线上这三个矩阵。一次矩阵乘法就把六个顶点全部变了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 房子的轮廓：每一列是一个顶点，最后回到起点</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>house </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>2.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.pi </span><span>/</span><span> </span><span>4</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>flip </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>rot </span><span>=</span><span> np.</span><span>array</span><span>([[np.</span><span>cos</span><span>(t), </span><span>-</span><span>np.</span><span>sin</span><span>(t)], [np.</span><span>sin</span><span>(t), np.</span><span>cos</span><span>(t)]])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>proj </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.5</span><span>, </span><span>0.5</span><span>], [</span><span>0.5</span><span>, </span><span>0.5</span><span>]])</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 一次矩阵乘法，整座房子的 6 个顶点同时变换</span></div></div><div><div><div>19</div></div><div><span>    </span><span>for</span><span> name, A </span><span>in</span><span> [(</span><span>"翻转"</span><span>, flip), (</span><span>"旋转 45 度"</span><span>, rot), (</span><span>"投影"</span><span>, proj)]:</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>out </span><span>=</span><span> A </span><span>@</span><span> house</span></div></div><div><div><div>21</div></div><div><span>        </span><span>print</span><span>(name, </span><span>"之后的顶点："</span><span>)</span></div></div><div><div><div>22</div></div><div><span>        </span><span>print</span><span>(out)</span></div></div><div><div><div>23</div></div><div><span>        </span><span>print</span><span>(</span><span>"  房顶尖 (1, 3) 变成："</span><span><span>, A </span><span>@</span><span> house[:, </span></span><span>3</span><span>])</span></div></div><div><div><div>24</div></div><div><span>        </span><span>print</span><span>(</span><span>"  房子的“面积”变成原来的"</span><span>, </span><span>f</span><span>"</span><span>{</span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(A))</span></span><span>:.4f</span><span>}</span><span>"</span><span>, </span><span>"倍"</span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>翻转 之后的顶点：</span></div></div><div><div><div>2</div></div><div><span>[[ 0.  2.  2.  1.  0.  0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.  0. -2. -3. -2.  0.]]</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>房顶尖 (1, 3) 变成： [ 1. -3.]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>房子的“面积”变成原来的 1.0000 倍</span></div></div><div><div><div>6</div></div><div><span>旋转 45 度 之后的顶点：</span></div></div><div><div><div>7</div></div><div><span>[[ 0.      1.4142  0.     -1.4142 -1.4142  0.    ]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[ 0.      1.4142  2.8284  2.8284  1.4142  0.    ]]</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>房顶尖 (1, 3) 变成： [-1.4142  2.8284]</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>房子的“面积”变成原来的 1.0000 倍</span></div></div><div><div><div>11</div></div><div><span>投影 之后的顶点：</span></div></div><div><div><div>12</div></div><div><span>[[0. 1. 2. 2. 1. 0.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[0. 1. 2. 2. 1. 0.]]</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>房顶尖 (1, 3) 变成： [2. 2.]</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>房子的“面积”变成原来的 0.0000 倍</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>翻转之后屋顶尖 <span><span>(1,3)(1, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 变成 <span><span>(1,−3)(1, -3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>3</span><span>)</span></span></span></span>，和他说的一样。旋转之后变成 <span><span>(−1.4142,2.8284)(-1.4142, 2.8284)</span><span><span><span></span><span>(</span><span>−</span><span>1.4142</span><span>,</span><span></span><span>2.8284</span><span>)</span></span></span></span>：长度没变（都是 <span><span>10\sqrt{10}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>10</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>），方向转了 45 度。最后一行的“面积倍数”是 <span><span>∣det⁡A∣|\det A|</span><span><span><span></span><span>∣</span><span></span><span>det</span><span></span><span>A</span><span>∣</span></span></span></span>：翻转和旋转都是 1.0000，房子没有变大变小；投影是 0.0000，整座房子被压成了一条线段，输出里两行数字一模一样，所有顶点都落在 <span><span>y=xy = x</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span></span></span></span> 上。</p><p>翻转和旋转放在一起看更直观：</p><p></p><figure><img src="/assets/images/linear-algebra/l30-house.svg" alt="左：矩阵 diag(1, -1) 把蓝色的房子变成红色的倒房子。右：旋转 45 度的矩阵把房子绕原点转了一个角度，虚线连着屋顶尖的输入与输出" /><figcaption>左：矩阵 diag(1, -1) 把蓝色的房子变成红色的倒房子。右：旋转 45 度的矩阵把房子绕原点转了一个角度，虚线连着屋顶尖的输入与输出</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>house </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>2.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.pi </span><span>/</span><span> </span><span>4</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>flip </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>rot </span><span>=</span><span> np.</span><span>array</span><span>([[np.</span><span>cos</span><span>(t), </span><span>-</span><span>np.</span><span>sin</span><span>(t)], [np.</span><span>sin</span><span>(t), np.</span><span>cos</span><span>(t)]])</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>fig, axes </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> ax, A, title </span><span>in</span><span> [</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>(axes[</span><span>0</span><span>], flip, </span><span>"A = diag(1, -1)"</span><span>),</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>(axes[</span><span>1</span><span>], rot, </span><span>"A = rotation by 45 degrees"</span><span>),</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>]:</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>out </span><span>=</span><span> A </span><span>@</span><span> house</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>ax.</span><span>plot</span><span>(house[</span><span>0</span><span>], house[</span><span>1</span><span>], </span><span>"o-"</span><span>, </span><span>color</span><span>=</span><span>"tab:blue"</span><span>, </span><span>label</span><span>=</span><span>"input"</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>ax.</span><span>plot</span><span>(out[</span><span>0</span><span>], out[</span><span>1</span><span>], </span><span>"o-"</span><span>, </span><span>color</span><span>=</span><span>"tab:red"</span><span>, </span><span>label</span><span>=</span><span>"output"</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>ax.</span><span>plot</span><span>(</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>[house[</span><span>0</span><span>, </span><span>3</span><span>], out[</span><span>0</span><span>, </span><span>3</span><span>]],</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>[house[</span><span>1</span><span>, </span><span>3</span><span>], out[</span><span>1</span><span>, </span><span>3</span><span>]],</span></div></div><div><div><div>31</div></div><div><span>            </span><span>":"</span><span>,</span></div></div><div><div><div>32</div></div><div><span>            </span><span>color</span><span>=</span><span>"gray"</span><span>,</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>ax.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>ax.</span><span>grid</span><span>(</span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>ax.</span><span>axhline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"black"</span><span>, </span><span>lw</span><span>=</span><span>0.6</span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>ax.</span><span>axvline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"black"</span><span>, </span><span>lw</span><span>=</span><span>0.6</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>ax.</span><span>set_title</span><span>(title)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>ax.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"lower right"</span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>ax.</span><span>set_xlim</span><span>(</span><span>-</span><span>3.5</span><span>, </span><span>3.5</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>ax.</span><span>set_ylim</span><span>(</span><span>-</span><span>3.5</span><span>, </span><span>3.5</span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>44</div></div><div><span>        </span><span>"l30-house.svg"</span><span>,</span></div></div><div><div><div>45</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>他在这里说的一句话我觉得是全讲的主旨：线性变换是矩阵乘法的抽象描述，我们的目标是“找出它背后的矩阵”。要做到这一点，就得引入坐标，也就是选基。</p></section>
<section><h2>只需要知道变换对基做了什么<a href="#只需要知道变换对基做了什么"><span>#</span></a></h2><p><strong>要完全确定一个线性变换，要知道多少信息？</strong> 他一步一步地问。只告诉你 <span><span>T(v1)T(\mathbf{v}_1)</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，你只知道一个向量的去向。再告诉你 <span><span>T(v2)T(\mathbf{v}_2)</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，你就知道了 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 张成的整个平面里每个向量的去向，因为线性：<span><span>T(c1v1+c2v2)=c1T(v1)+c2T(v2)T(c_1\mathbf{v}_1 + c_2\mathbf{v}_2) = c_1T(\mathbf{v}_1) + c_2T(\mathbf{v}_2)</span><span><span><span></span><span>T</span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，没有别的选择。前提是 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 无关：如果 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 6 倍，<span><span>T(v2)T(\mathbf{v}_2)</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 里没有新信息。</p><p>所以答案是：只要输入空间的一组基 <span><span>v1,…,vn\mathbf{v}_1, \dots, \mathbf{v}_n</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和每个基向量的像 <span><span>T(v1),…,T(vn)T(\mathbf{v}_1), \dots, T(\mathbf{v}_n)</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，就知道了所有输入的像。无限多个输入，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个输出就够了，我觉得这相当省事。</p><p>坐标又是什么？他说坐标来自基：向量 <span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 在基 <span><span>v1,…,vn\mathbf{v}_1, \dots, \mathbf{v}_n</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 下的坐标，就是 <span><span>v=c1v1+⋯+cnvn\mathbf{v} = c_1\mathbf{v}_1 + \cdots + c_n\mathbf{v}_n</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里的那些 <span><span>cic_i</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，在一组基下这种表示是唯一的。我们平时写 <span><span>(3,2,4)(3, 2, 4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，默认的是标准基。换一组基（比如某个矩阵的特征向量），坐标就变了，向量本身没变。这有点像同一栋房子在不同城市的地址系统里有不同的门牌号，房子没动，动的是编号方式。</p><p>于是，从线性变换 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span>（输入在 <span><span>Rn\mathbf{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>，输出在 <span><span>Rm\mathbf{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span>）造矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，他的规则是：</p><ul>
<li>选输入基 <span><span>v1,…,vn\mathbf{v}_1, \dots, \mathbf{v}_n</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和输出基 <span><span>w1,…,wm\mathbf{w}_1, \dots, \mathbf{w}_m</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。（要选两组，因为输入和输出可以不在同一个空间里。）</li>
<li>第 1 列：对 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做变换，得到 <span><span>T(v1)T(\mathbf{v}_1)</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，把它写成输出基的组合 <span><span>T(v1)=a11w1+⋯+am1wmT(\mathbf{v}_1) = a_{11}\mathbf{w}_1 + \cdots + a_{m1}\mathbf{w}_m</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>m</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，系数 <span><span>a11,…,am1a_{11}, \dots, a_{m1}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>m</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 1 列。</li>
<li>第 2 列同理，对 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做同样的事。依此类推。</li>
</ul><p>为什么这样造的矩阵一定对？输入坐标是 <span><span>(1,0,…,0)(1, 0, \dots, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 的时候，输入就是 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，矩阵乘出来恰好是第 1 列，也就是 <span><span>T(v1)T(\mathbf{v}_1)</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 的坐标。第 2 列、第 3 列……都对，所以对所有基向量都对，所以对所有向量都对。结论：输入坐标乘 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，得到输出坐标。</p></section>
<section><h2>同一个投影，不同的基，不同的矩阵<a href="#同一个投影不同的基不同的矩阵"><span>#</span></a></h2><p>他用投影当例子，输入输出用同一组基。第一次选的基很讲究：第一个基向量 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 落在要投影的直线上（单位向量），第二个 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 垂直于直线。看规则：</p><ul>
<li><span><span>T(v1)=v1T(\mathbf{v}_1) = \mathbf{v}_1</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：投影不动直线上的向量，所以第 1 列是 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。</li>
<li><span><span>T(v2)=0T(\mathbf{v}_2) = \mathbf{0}</span><span><span><span></span><span>T</span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：投影把垂直方向消灭，第 2 列是 <span><span>(0,0)(0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。</li>
</ul><p>矩阵是 <span><span>[1000]\begin{bmatrix} 1 &amp; 0 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，对角的。他提醒我们，这组基其实就是投影的特征向量，特征值是 1 和 0，所以矩阵就是特征值矩阵 <span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span>。这才是“好基”：他说，物理学家不情愿地引入坐标时，选的就是这种。</p><p>如果偏要用标准基（直线取 45 度），就是第 15 讲里的 <span><span>P=aaTaTaP = \dfrac{\mathbf{a}\mathbf{a}^\mathsf{T}}{\mathbf{a}^\mathsf{T}\mathbf{a}}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，<span><span>a=(1,1)\mathbf{a} = (1, 1)</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，他说算出来是四个 <span><span>12\frac12</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。输入 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，输出 <span><span>(12,12)(\frac12, \frac12)</span><span><span><span></span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span>，是对的，只是不对角。我把造矩阵的规则写成函数 <code>matrix_in_bases</code>，用它在三组基下造矩阵，再验证“输入坐标乘矩阵得输出坐标”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>matrix_in_bases</span><span>(</span><span>T</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>V</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>W</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""按 Strang 的规则造矩阵：第 j 列是 T(v_j) 在输出基 W 下的坐标。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>cols </span><span>=</span><span> []</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(V.shape[</span><span>1</span><span>]):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>out </span><span>=</span><span> T </span><span>@</span><span> V[:, j]  </span><span># 先对第 j 个输入基向量施加变换</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>cols.</span><span>append</span><span>(np.linalg.</span><span>solve</span><span>(W, out))  </span><span># 再求它在输出基下的坐标</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>column_stack</span><span>(cols)</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span># 投影到 45 度直线上，用标准基时，这就是第 15 讲的 P</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>], [</span><span>1.0</span><span>]])</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> a </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(a.T </span><span>@</span><span> a) </span><span>@</span><span> a.T</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"标准基下的矩阵 P："</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ (1, 0) ="</span><span><span>, P </span><span>@</span><span> np.</span><span>array</span><span>([</span></span><span>1.0</span><span>, </span><span>0.0</span><span>]))</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 换成特征向量基：一个在直线上，一个垂直于直线</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>q1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>2.0</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>q2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>-</span><span>1.0</span><span>, </span><span>1.0</span><span><span>]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>2.0</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>column_stack</span><span>([q1, q2])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>Lam </span><span>=</span><span> </span><span>matrix_in_bases</span><span>(P, Q, Q)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量基下的矩阵："</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(Lam)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 输入基和输出基不必相同，换一组斜着的基，矩阵就不再对角</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>V </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> </span><span>matrix_in_bases</span><span>(P, V, V)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"斜基下的矩阵："</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(M)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 验证：输入坐标乘矩阵，得到输出坐标</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>3.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>c_in </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(Q, v)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>c_out </span><span>=</span><span> Lam </span><span>@</span><span> c_in</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"输入坐标"</span><span>, c_in, </span><span>"，输出坐标"</span><span>, c_out)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"还原成向量后与 P @ v 一致："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> c_out, P </span><span>@</span><span> v))</span></span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(P))</span></span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>标准基下的矩阵 P：</span></div></div><div><div><div>2</div></div><div><span>[[0.5 0.5]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0.5 0.5]]</span></div></div><div><div><div>4</div></div><div><span>P @ (1, 0) = [0.5 0.5]</span></div></div><div><div><div>5</div></div><div><span>特征向量基下的矩阵：</span></div></div><div><div><div>6</div></div><div><span>[[1. 0.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[0. 0.]]</span></div></div><div><div><div>8</div></div><div><span>斜基下的矩阵：</span></div></div><div><div><div>9</div></div><div><span>[[0.  0. ]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0.5 1. ]]</span></div></div><div><div><div>11</div></div><div><span>输入坐标 [ 1.4142 -2.8284] ，输出坐标 [1.4142 0.    ]</span></div></div><div><div><div>12</div></div><div><span>还原成向量后与 P @ v 一致： True</span></div></div><div><div><div>13</div></div><div><span>特征值： [0. 1.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>标准基下是四个 0.5，特征向量基下是 <span><span>diag(1,0)\mathrm{diag}(1, 0)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，<code>eigvalsh</code> 打印出来的特征值也是 0 和 1，对得上。第三个矩阵是我自己加的：用一组斜着的基 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，造出来的矩阵是 <span><span>[000.51]\begin{bmatrix} 0 &amp; 0 \\ 0.5 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0.5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，既不对称也不对角。这三个矩阵描述的是同一个变换，差别全在基上。</p><p>倒数第二行验证规则本身：取 <span><span>v=(3,−1)\mathbf{v} = (3, -1)</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，在特征向量基下的坐标是 <span><span>(1.4142,−2.8284)(1.4142, -2.8284)</span><span><span><span></span><span>(</span><span>1.4142</span><span>,</span><span></span><span>−</span><span>2.8284</span><span>)</span></span></span></span>，乘上 <span><span>diag(1,0)\mathrm{diag}(1, 0)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 得到 <span><span>(1.4142,0)(1.4142, 0)</span><span><span><span></span><span>(</span><span>1.4142</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，再用 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 还原回标准坐标，和直接算 <span><span>PvP\mathbf{v}</span><span><span><span></span><span>P</span><span>v</span></span></span></span> 一致。（这就是第 22 讲对角化的回顾：<span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，不过是同一件事换了说法。）</p><p>他还说了一句我觉得很对的话：整门课其实一直在用“最顺手的基”，也就是标准基，然后对付得到的矩阵。这个矩阵不坏：对称，<span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>。但在最好的基里，对角、对称、<span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span> 这三件事一眼都能看出来。</p></section>
<section><h2>求导是线性的，它的矩阵是 2 × 3<a href="#求导是线性的它的矩阵是-2--3"><span>#</span></a></h2><p>最后一个例子不再是平面上的箭头。输入空间是所有形如 <span><span>c1+c2x+c3x2c_1 + c_2x + c_3x^2</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的多项式，基是 <span><span>1,x,x21, x, x^2</span><span><span><span></span><span>1</span><span>,</span><span></span><span>x</span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>；变换是求导，输出是 <span><span>c2+2c3xc_2 + 2c_3x</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>c</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>x</span></span></span></span>，输出基取 <span><span>1,x1, x</span><span><span><span></span><span>1</span><span>,</span><span></span><span>x</span></span></span></span>。三维到二维，所以矩阵是 2 × 3。</p><p>他的话很实在：你大概没想过求导是线性的，但如果它不是，求导会花上很长时间。正是因为线性，我们只要知道 <span><span>sin⁡x\sin x</span><span><span><span></span><span>sin</span><span></span><span>x</span></span></span></span>、<span><span>cos⁡x\cos x</span><span><span><span></span><span>cos</span><span></span><span>x</span></span></span></span>、<span><span>exe^x</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span></span></span></span></span></span></span></span> 这几个小清单上的导数，就能对它们的所有组合求导。</p><p>套上造矩阵的规则：<span><span>11</span><span><span><span></span><span>1</span></span></span></span> 求导是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>，坐标 <span><span>(0,0)(0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>；<span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 求导是 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>，坐标 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>；<span><span>x2x^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 求导是 <span><span>2x2x</span><span><span><span></span><span>2</span><span>x</span></span></span></span>，坐标 <span><span>(0,2)(0, 2)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>。三列并起来：</p><span><span><span>D=[010002]D = \begin{bmatrix} 0 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 2 \end{bmatrix}</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>乘上输入坐标 <span><span>(c1,c2,c3)(c_1, c_2, c_3)</span><span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，得 <span><span>(c2,2c3)(c_2, 2c_3)</span><span><span><span></span><span>(</span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>2</span><span><span>c</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，正是他黑板上要的。我让代码也走一遍规则：对三个基向量分别用 <code>numpy.polynomial</code> 求导，把结果当作列拼起来，再和直接求导对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div><span>from</span><span> numpy.polynomial </span><span>import</span><span> polynomial </span><span>as</span><span> poly</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>def</span><span> </span><span>derivative_coords</span><span>(</span><span>c</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"""多项式系数 (c1, c2, c3) 求导，输出系数补成长度 2。"""</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>pad</span><span>(poly.</span><span>polyder</span><span>(c), (</span></span><span>0</span><span>, </span><span>2</span><span><span> </span><span>-</span><span> (</span></span><span>len</span><span><span>(c) </span><span>-</span><span> </span></span><span>1</span><span>)))</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span># 输入基 1, x, x^2；输出基 1, x。按规则造矩阵：</span></div></div><div><div><div>13</div></div><div><span>    </span><span># 第 j 列 = 求导作用在第 j 个输入基向量上的输出坐标</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>basis </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>column_stack</span><span>([</span><span>derivative_coords</span><span>(basis[:, j]) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)])</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"D ="</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(D)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"D 的形状："</span><span>, D.shape)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>5.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>, </span><span>4.0</span><span>])  </span><span># 5 - 3x + 4x^2</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"D @ c ="</span><span><span>, D </span><span>@</span><span> c, </span></span><span>"，直接求导："</span><span><span>, </span><span>derivative_coords</span><span>(c))</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 线性性：对和求导等于对导数求和</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>q </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"D(p + q) = D(p) + D(q)："</span><span><span>, np.</span><span>allclose</span><span>(D </span><span>@</span><span> (p </span><span>+</span><span> q), D </span><span>@</span><span> p </span><span>+</span><span> D </span><span>@</span><span> q))</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 零空间：导数为 0 的多项式只有常数</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"D 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(D))</span></span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"D @ (7, 0, 0) ="</span><span><span>, D </span><span>@</span><span> np.</span><span>array</span><span>([</span></span><span>7.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>]))</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>D =</span></div></div><div><div><div>2</div></div><div><span>[[0. 1. 0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 0. 2.]]</span></div></div><div><div><div>4</div></div><div><span>D 的形状： (2, 3)</span></div></div><div><div><div>5</div></div><div><span>D @ c = [-3.  8.] ，直接求导： [-3.  8.]</span></div></div><div><div><div>6</div></div><div><span>D(p + q) = D(p) + D(q)： True</span></div></div><div><div><div>7</div></div><div><span>D 的秩： 2</span></div></div><div><div><div>8</div></div><div><span>D @ (7, 0, 0) = [0. 0.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>D</code> 就是上面那个矩阵，<span><span>5−3x+4x25 - 3x + 4x^2</span><span><span><span></span><span>5</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的导数 <span><span>−3+8x-3 + 8x</span><span><span><span></span><span>−</span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>8</span><span>x</span></span></span></span>，两种算法都是 <span><span>(−3,8)(-3, 8)</span><span><span><span></span><span>(</span><span>−</span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span>。零空间也很好看：<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 的秩是 2，<span><span>(7,0,0)(7, 0, 0)</span><span><span><span></span><span>(</span><span>7</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>（也就是常数 7）被送到 <span><span>(0,0)(0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，所以求导的零空间正好是常数函数，这是“导数为零的函数是常数”的线性代数版本。</p><p>他最后留了两句：逆矩阵给出逆变换，矩阵的乘积给出变换复合的矩阵。矩阵乘法本来就是从线性变换来的，不是先有一个奇怪的乘法规则再去凑。这个话题他说要等感恩节假期之后再讲，我猜下一讲会从“换一组基，同一个变换的矩阵怎么变”切入，见 <a href="/posts/linear-algebra-31-change-of-basis-compression/">18.06 第 31 讲：基变换与图像压缩</a>。到时候，今天那个斜基下的矩阵 <span><span>[000.51]\begin{bmatrix} 0 &amp; 0 \\ 0.5 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0.5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 和 <span><span>diag(1,0)\mathrm{diag}(1, 0)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 之间的关系就有名字了。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-29-svd/</id>
      <title type="text">18.06 第 29 讲：奇异值分解</title>
      <published>2026-10-18T09:40:00.000Z</published>
      <updated>2026-10-18T09:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-29-svd/"/>
      <summary type="text">MIT 18.06 第 29 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 Strang 黑板上的矩阵 [[4, 4], [-3, 3]] 做了奇异值分解，σ 是 5.6569 和 4.2426，单位圆被映成面积 75.3981 的椭圆，再用同一套办法算出 Wood–Berry 精馏塔增益矩阵的条件数是 7.48。</summary>
      <content type="html"><![CDATA[<p>Strang 开场就说 SVD 是矩阵“最终的、最好的”分解：任何矩阵（不必方，不必可逆，不必对称）都能写成 <span><span>A=UΣVTA = U\Sigma V^T</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，正交矩阵乘对角矩阵再乘正交矩阵。我用他黑板上的 <span><span>[44−33]\begin{bmatrix} 4 &amp; 4 \\ -3 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 算了一遍：奇异值是 <span><span>32=5.6569\sqrt{32} = 5.6569</span><span><span><span></span><span><span><span><span><span><span></span><span><span>32</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5.6569</span></span></span></span> 和 <span><span>18=4.2426\sqrt{18} = 4.2426</span><span><span><span></span><span><span><span><span><span><span></span><span><span>18</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.2426</span></span></span></span>，单位圆上 2000 个点乘上 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 以后，最长的半径是 5.6569，最短的是 4.2426，像是一个面积 75.3981 的椭圆。他在黑板上还算出了一个符号错误，当堂没有解决，我在下面给出原因。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=TX_vooSnhm8" target="_blank">Lecture 29: Singular value decomposition</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-29-singular-value-decomposition/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §7.1 Singular Values and Singular Vectors
前置：<a href="/posts/linear-algebra-28-similar-matrices-jordan/">第 28 讲：相似矩阵与 Jordan 标准形</a>
一句话：行空间里有一组正交基，被 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 送进列空间里另一组正交基，只是各自拉伸了 <span><span>σi\sigma_i</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 倍；这组基由 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的特征向量给出。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几分钟他先说明对称正定矩阵是 SVD 的特例（<span><span>QΛQTQ\Lambda Q^T</span><span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，一个 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 就够了），再画“行空间的正交基被送进列空间的正交基”那张图。这两段要认真看，整个 SVD 的想法都在这里。</li>
<li>把 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 里的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 消掉、只剩 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 的那几行推导很短，下面我会用代码再算一遍，可以倍速。</li>
<li>第一个例子（<span><span>4,4,−3,34, 4, -3, 3</span><span><span><span></span><span>4</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>−</span><span>3</span><span>,</span><span></span><span>3</span></span></span></span>）算到最后，他得到了 <span><span>−3,3-3, 3</span><span><span><span></span><span>−</span><span>3</span><span>,</span><span></span><span>3</span></span></span></span> 和 <span><span>3,−33, -3</span><span><span><span></span><span>3</span><span>,</span><span></span><span>−</span><span>3</span></span></span></span> 对不上的符号，说周三再补。这一段值得看他怎么面对错误，我在“先算 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>”一节里给出答案。</li>
<li>秩 1 的第二个例子、四个子空间的基（视频最后几分钟）本篇都算过了，倍速即可。</li>
</ul></div></div>
<section><h2>目标：行空间的一组正交基，送到列空间的一组正交基<a href="#目标行空间的一组正交基送到列空间的一组正交基"><span>#</span></a></h2><p>先说他想要什么。回忆那张“最重要的图”：左边是行空间，右边是列空间，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 把行空间里的每个向量送到列空间里。他问了一个很具体的问题：能不能在行空间里找一组 <strong>正交</strong> 基 <span><span>v1,…,vr\mathbf{v}_1, \dots, \mathbf{v}_r</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，使得它们被 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 送过去以后，在列空间里 <strong>仍然是正交的</strong>？</p><p>随便取一组正交基（Gram-Schmidt 能给你一组）通常做不到，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘完直角就没了。他要的是一组特殊的基，使得</p><span><span><span>Avi=σiui,i=1,…,rA\mathbf{v}_i = \sigma_i \mathbf{u}_i, \qquad i = 1, \dots, r</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>r</span></span></span></span></span><p>其中 <span><span>ui\mathbf{u}_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是列空间里的单位向量，<span><span>σi&gt;0\sigma_i &gt; 0</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 是“拉伸的倍数”。他说这个倍数不再叫 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 了，叫 <span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span>。零空间不用操心：把 <span><span>vr+1,…,vn\mathbf{v}_{r+1}, \dots, \mathbf{v}_n</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>r</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 取成零空间的正交基，把 <span><span>ur+1,…,um\mathbf{u}_{r+1}, \dots, \mathbf{u}_m</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>r</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 取成 <span><span>ATA^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间的正交基，对应的 <span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span> 全是 0，对角线上多几个 0 而已。</p><p>把这些式子并排写成矩阵，就是</p><span><span><span>A[v1⋯vn]=[u1⋯um][σ1⋱σr],AV=UΣA \begin{bmatrix} \mathbf{v}_1 &amp; \cdots &amp; \mathbf{v}_n \end{bmatrix}
= \begin{bmatrix} \mathbf{u}_1 &amp; \cdots &amp; \mathbf{u}_m \end{bmatrix}
\begin{bmatrix} \sigma_1 &amp; &amp; \\ &amp; \ddots &amp; \\ &amp; &amp; \sigma_r \end{bmatrix},
\qquad AV = U\Sigma</span><span><span><span></span><span>A</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span><span>⋱</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span>A</span><span>V</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span></span></span></span></span><p><span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 和 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 都是方阵，列向量标准正交，所以逆就是转置，<span><span>A=UΣV−1=UΣVTA = U\Sigma V^{-1} = U\Sigma V^T</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。和对角化 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 比，这里 <strong>要两个不同的正交矩阵</strong>（他的原话是“probably different”），好处是任何矩阵都做得到，而且两边都是正交矩阵，没有 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 那种数值上的烦恼。对称正定的 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是唯一的例外：特征向量本来就正交，<span><span>U=V=QU = V = Q</span><span><span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span></span></span></span>，<span><span>Σ=Λ\Sigma = \Lambda</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>Λ</span></span></span></span>。</p><p>怎么找 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 和 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 呢？他的办法是造一个让 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 消失的组合：</p><span><span><span>ATA=(VΣTUT)(UΣVT)=V ΣTΣ VT=V[σ12⋱σn2]VTA^TA = (V\Sigma^T U^T)(U\Sigma V^T) = V\,\Sigma^T\Sigma\,V^T
= V \begin{bmatrix} \sigma_1^2 &amp; &amp; \\ &amp; \ddots &amp; \\ &amp; &amp; \sigma_n^2 \end{bmatrix} V^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>V</span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span>(</span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Σ</span><span></span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span><span>⋱</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>UTU=IU^TU = I</span><span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，所以 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 不见了。右边是对称矩阵 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的 <span><span>QΛQTQ\Lambda Q^T</span><span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：<span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 是 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的特征向量，<span><span>σi2\sigma_i^2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是它的特征值（<span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 半正定，特征值不会是负的，所以有实的正平方根）。反过来乘 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，<span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 消失，<span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 是 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的特征向量。</p><p><strong>这样造出来的 <span><span>ui\mathbf{u}_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为什么正好是标准正交的？</strong> 视频里没有展开，我补一下。<span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 对称，<span><span>xTATAx=∥Ax∥2≥0\mathbf{x}^TA^TA\mathbf{x} = \|A\mathbf{x}\|^2 \ge 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以它半正定，特征向量 <span><span>vi\mathbf{v}_i</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 可以取成标准正交，特征值 <span><span>σi2≥0\sigma_i^2 \ge 0</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>。再看 <span><span>AviA\mathbf{v}_i</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 们：</p><span><span><span>(Avi)T(Avj)=viTATA vj=σj2 viTvj={σi2i=j0i≠j(A\mathbf{v}_i)^T(A\mathbf{v}_j) = \mathbf{v}_i^T A^TA\,\mathbf{v}_j = \sigma_j^2\,\mathbf{v}_i^T\mathbf{v}_j =
\begin{cases} \sigma_i^2 &amp; i = j \\ 0 &amp; i \ne j \end{cases}</span><span><span><span></span><span>(</span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>j</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>{</span></span><span><span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span>i</span><span></span><span>=</span><span></span><span>j</span></span></span><span><span></span><span><span>i</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span></span></span></span></span></span><p><span><span>i≠ji \ne j</span><span><span><span></span><span>i</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>j</span></span></span></span> 时内积是 0，说明 <span><span>AviA\mathbf{v}_i</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 两两正交；<span><span>i=ji = j</span><span><span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>j</span></span></span></span> 时 <span><span>∥Avi∥=σi\|A\mathbf{v}_i\| = \sigma_i</span><span><span><span></span><span>∥</span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，除以 <span><span>σi\sigma_i</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就是单位向量。所以 <span><span>ui=Avi/σi\mathbf{u}_i = A\mathbf{v}_i / \sigma_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 天然是标准正交的，不需要再对 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 做一次特征分解。他讲 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是为了让大家看见 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 是它的特征向量，但真要算的时候，这个公式更省事，也不会出符号问题（下一节会撞上）。</p></section>
<section><h2>先算 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>：黑板上的第一个例子<a href="#先算-ataataata黑板上的第一个例子"><span>#</span></a></h2><p>他选的矩阵是</p><span><span><span>A=[44−33]A = \begin{bmatrix} 4 &amp; 4 \\ -3 &amp; 3 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>可逆，秩 2。他的步骤是：算 <span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>，求特征向量和特征值，特征值开方得 <span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span>，再算 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 找 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>。我照着做，然后复现他在最后一步算错的地方：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>4.0</span><span><span>], [</span><span>-</span></span><span>3.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 第一步：A^T A 的特征分解给出 V 和 sigma^2</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A ="</span><span>)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span><span>(A.T </span><span>@</span><span> A)</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>w, V </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A.T </span><span>@</span><span> A)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>order </span><span>=</span><span> np.</span><span>argsort</span><span>(w)[::</span><span>-</span><span>1</span><span>]  </span><span># 从大到小排</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>w, V </span><span>=</span><span> w[order], V[:, order]</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>sigma </span><span>=</span><span> np.</span><span>sqrt</span><span>(w)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A 的特征值："</span><span>, w)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"sigma ="</span><span>, sigma)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"v1 ="</span><span>, V[:, </span><span>0</span><span>], </span><span>" v2 ="</span><span>, V[:, </span><span>1</span><span>])</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 第二步：A A^T 恰好是对角阵，特征向量是单位矩阵的列</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"A A^T ="</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span><span>(A </span><span>@</span><span> A.T)</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># Strang 在黑板上的选择：U = 单位矩阵，V 取 (1, 1)/sqrt(2) 与</span></div></div><div><div><div>24</div></div><div><span>    </span><span># (1, -1)/sqrt(2)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>V_board </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>]]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>2.0</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>U_board </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>Sigma </span><span>=</span><span> np.</span><span>diag</span><span>(np.</span><span>sqrt</span><span>([</span><span>32.0</span><span>, </span><span>18.0</span><span>]))</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"U Sigma V^T（黑板上的选择）="</span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span><span>(U_board </span><span>@</span><span> Sigma </span><span>@</span><span> V_board.T)</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span># 修正：U 的列不能独立挑，必须取 u_i = A v_i / sigma_i</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> (A </span><span>@</span><span> V_board) </span><span>/</span><span> np.</span><span>sqrt</span><span>([</span><span>32.0</span><span>, </span><span>18.0</span><span>])</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"U = A V / sigma ="</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(U)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"U Sigma V^T ="</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span><span>(U </span><span>@</span><span> Sigma </span><span>@</span><span> V_board.T)</span></span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"还原出 A："</span><span><span>, np.</span><span>allclose</span><span>(U </span><span>@</span><span> Sigma </span><span>@</span><span> V_board.T, A))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 对照 NumPy</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>U_np, s_np, Vt_np </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.svd 的 sigma："</span><span>, s_np)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"np 的 U："</span><span>)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(U_np)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"np 的 V^T："</span><span>)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(Vt_np)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A^T A =</span></div></div><div><div><div>2</div></div><div><span>[[25.  7.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 7. 25.]]</span></div></div><div><div><div>4</div></div><div><span>A^T A 的特征值： [32. 18.]</span></div></div><div><div><div>5</div></div><div><span>sigma = [5.6569 4.2426]</span></div></div><div><div><div>6</div></div><div><span>v1 = [0.7071 0.7071]  v2 = [-0.7071  0.7071]</span></div></div><div><div><div>7</div></div><div><span>A A^T =</span></div></div><div><div><div>8</div></div><div><span>[[32.  0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[ 0. 18.]]</span></div></div><div><div><div>10</div></div><div><span>U Sigma V^T（黑板上的选择）=</span></div></div><div><div><div>11</div></div><div><span>[[ 4.  4.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[ 3. -3.]]</span></div></div><div><div><div>13</div></div><div><span>U = A V / sigma =</span></div></div><div><div><div>14</div></div><div><span>[[ 1.  0.]</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[ 0. -1.]]</span></div></div><div><div><div>16</div></div><div><span>U Sigma V^T =</span></div></div><div><div><div>17</div></div><div><span>[[ 4.  4.]</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>[-3.  3.]]</span></div></div><div><div><div>19</div></div><div><span>还原出 A： True</span></div></div><div><div><div>20</div></div><div><span>np.linalg.svd 的 sigma： [5.6569 4.2426]</span></div></div><div><div><div>21</div></div><div><span>np 的 U：</span></div></div><div><div><div>22</div></div><div><span>[[-1.  0.]</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>24</div></div><div><span>np 的 V^T：</span></div></div><div><div><div>25</div></div><div><span>[[-0.7071 -0.7071]</span></div></div><div><div><div>26</div></div><div><span><span> </span></span><span>[-0.7071  0.7071]]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读一下输出。<span><span>ATA=[257725]A^TA = \begin{bmatrix} 25 &amp; 7 \\ 7 &amp; 25 \end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>25</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>25</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，和他口算的一样（他还特意说“一定要对称”）。特征向量是 <span><span>(1,1)/2(1, 1)/\sqrt{2}</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span><span>/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 和 <span><span>(1,−1)/2(1, -1)/\sqrt{2}</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span><span>/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，特征值是 32 和 18，<span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span> 是 5.6569 和 4.2426。<code>eigh</code> 给出的 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>(−0.7071,0.7071)(-0.7071, 0.7071)</span><span><span><span></span><span>(</span><span>−</span><span>0.7071</span><span>,</span><span></span><span>0.7071</span><span>)</span></span></span></span>，和他的差一个符号，这是特征向量天然的自由度，后面马上用得上。</p><p><span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 恰好是对角阵 <span><span>diag(32,18)\text{diag}(32, 18)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>32</span><span>,</span><span></span><span>18</span><span>)</span></span></span></span>，他说这是个“lucky case”：特征向量就是单位矩阵的两列，所以 <span><span>U=IU = I</span><span><span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。特征值也还是 32 和 18，他顺便提了一句：<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 和 <span><span>BABA</span><span><span><span></span><span>B</span><span>A</span></span></span></span> 的特征值一样。</p><p>问题出在乘回去的时候。用他的 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 和 <span><span>U=IU = I</span><span><span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 乘出来的是 <span><span>[443−3]\begin{bmatrix} 4 &amp; 4 \\ 3 &amp; -3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，第二行的符号反了。他当堂的反应是“Phooey”，说想想再讲。（我挺喜欢这种场面：一个 1999 年的教授在讲台上被自己的矩阵打了脸，然后说周三再来。）</p><p>答案是：<strong><span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的列不能独立挑</strong>。<span><span>u2\mathbf{u}_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 作为 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的特征向量，<span><span>+e2+\mathbf{e}_2</span><span><span><span></span><span>+</span><span><span>e</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>−e2-\mathbf{e}_2</span><span><span><span></span><span>−</span><span><span>e</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都合格，但一旦 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 固定了，<span><span>Av2=σ2u2A\mathbf{v}_2 = \sigma_2\mathbf{u}_2</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就把符号定死了。正确的做法是先求 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 和 <span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span>，再令 <span><span>ui=Avi/σi\mathbf{u}_i = A\mathbf{v}_i / \sigma_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。代码里的 <code>U = (A @ V_board) / ...</code> 就是这样做的，得到 <span><span>u2=−e2\mathbf{u}_2 = -\mathbf{e}_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>e</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，乘回去恰好是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>。<code>np.linalg.svd</code> 给出的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>VTV^T</span><span><span><span></span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的符号和我的不同（它把两个 <span><span>u\mathbf{u}</span><span><span><span></span><span>u</span></span></span></span> 和 <span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 的符号各翻了一下），但每一对 <span><span>(ui,vi)(\mathbf{u}_i, \mathbf{v}_i)</span><span><span><span></span><span>(</span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 的符号是同步的，所以乘积一样。SVD 的唯一性只到每一对 <span><span>(ui,vi)(\mathbf{u}_i, \mathbf{v}_i)</span><span><span><span></span><span>(</span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 同时变号为止。</p><p>据我所知，视频里这个符号问题当堂没有解决，我这里给的是自己推的解释，用上面的输出验证过了。</p></section>
<section><h2>秩 1 的例子与四个子空间<a href="#秩-1-的例子与四个子空间"><span>#</span></a></h2><p>他的第二个例子是奇异矩阵</p><span><span><span>A=[4386]A = \begin{bmatrix} 4 &amp; 3 \\ 8 &amp; 6 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>秩 1：行空间是 <span><span>(4,3)(4, 3)</span><span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 所在的直线，列空间是 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 所在的直线，另外两个子空间（零空间和左零空间）是它们各自的垂线。他的答案是 <span><span>v1=(0.8,0.6)\mathbf{v}_1 = (0.8, 0.6)</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0.8</span><span>,</span><span></span><span>0.6</span><span>)</span></span></span></span>，<span><span>u1=(1,2)/5\mathbf{u}_1 = (1, 2)/\sqrt{5}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span><span>/</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，<span><span>σ1=125\sigma_1 = \sqrt{125}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>125</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>（来自 <span><span>ATA=[80606045]A^TA = \begin{bmatrix} 80 &amp; 60 \\ 60 &amp; 45 \end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>80</span></span></span><span><span></span><span><span>60</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>60</span></span></span><span><span></span><span><span>45</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，秩 1 所以另一个特征值是 0，迹是 125）。<span><span>v2=(0.6,−0.8)\mathbf{v}_2 = (0.6, -0.8)</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0.6</span><span>,</span><span></span><span>−</span><span>0.8</span><span>)</span></span></span></span> 和 <span><span>u2=(2,−1)/5\mathbf{u}_2 = (2, -1)/\sqrt{5}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span><span>/</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 只需要补成标准正交基。他还说乘回去时 <span><span>5\sqrt{5}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 和 <span><span>125\sqrt{125}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>125</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 约掉得到 5，整数就出来了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>3.0</span><span>], [</span><span>8.0</span><span>, </span><span>6.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span>    </span><span>print</span><span>(</span><span>"rank ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A ="</span><span>)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span><span>(A.T </span><span>@</span><span> A)</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A 的特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(A.T </span><span>@</span><span> A))</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 黑板上的答案：u1 = (1, 2)/sqrt(5)，v1 = (0.8, 0.6)，sigma1 = sqrt(125)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>]]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>5.0</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>Sigma </span><span>=</span><span> np.</span><span>array</span><span>([[np.</span><span>sqrt</span><span>(</span><span>125.0</span><span>), </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>Vt </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.8</span><span>, </span><span>0.6</span><span>], [</span><span>0.6</span><span><span>, </span><span>-</span></span><span>0.8</span><span>]])</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"U Sigma V^T ="</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span><span>(U </span><span>@</span><span> Sigma </span><span>@</span><span> Vt)</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"还原出 A："</span><span><span>, np.</span><span>allclose</span><span>(U </span><span>@</span><span> Sigma </span><span>@</span><span> Vt, A))</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 多出来的那一对向量被 sigma = 0 吃掉</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>v2 </span><span>=</span><span> Vt[</span><span>1</span><span>]</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>u2 </span><span>=</span><span> U[:, </span><span>1</span><span>]</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"|A v2| = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> v2)</span></span><span>:.1e</span><span>}</span><span>（v2 在 A 的零空间里）"</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"|A^T u2| = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A.T </span><span>@</span><span> u2)</span></span><span>:.1e</span><span>}</span><span>（u2 在 A^T 的零空间里）"</span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 对照 NumPy</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>U_np, s_np, Vt_np </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.svd 的 sigma："</span><span>, s_np)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"sqrt(125) = </span><span>{</span><span><span>np.</span><span>sqrt</span><span>(</span></span><span>125.0</span><span>)</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>rank_one </span><span>=</span><span> s_np[</span><span>0</span><span><span>] </span><span>*</span><span> np.</span><span>outer</span><span>(U_np[:, </span></span><span>0</span><span>], Vt_np[</span><span>0</span><span>])</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"sigma1 * u1 v1^T 等于 A："</span><span><span>, np.</span><span>allclose</span><span>(rank_one, A))</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>rank = 1</span></div></div><div><div><div>2</div></div><div><span>A^T A =</span></div></div><div><div><div>3</div></div><div><span>[[80. 60.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[60. 45.]]</span></div></div><div><div><div>5</div></div><div><span>A^T A 的特征值： [  0. 125.]</span></div></div><div><div><div>6</div></div><div><span>U Sigma V^T =</span></div></div><div><div><div>7</div></div><div><span>[[4. 3.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[8. 6.]]</span></div></div><div><div><div>9</div></div><div><span>还原出 A： True</span></div></div><div><div><div>10</div></div><div><span>|A v2| = 9.9e-16（v2 在 A 的零空间里）</span></div></div><div><div><div>11</div></div><div><span>|A^T u2| = 0.0e+00（u2 在 A^T 的零空间里）</span></div></div><div><div><div>12</div></div><div><span>np.linalg.svd 的 sigma： [11.1803  0.    ]</span></div></div><div><div><div>13</div></div><div><span>sqrt(125) = 11.1803</span></div></div><div><div><div>14</div></div><div><span>sigma1 * u1 v1^T 等于 A： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>σ2=0\sigma_2 = 0</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 把 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>u2\mathbf{u}_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 这一对吃掉了：<span><span>Av2A\mathbf{v}_2</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是零向量（浮点误差 <span><span>9.9×10−169.9 \times 10^{-16}</span><span><span><span></span><span>9.9</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>），<span><span>ATu2A^T\mathbf{u}_2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 也是零向量。所以 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在零空间里，<span><span>u2\mathbf{u}_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在左零空间里。对角阵里这个 0，就是“零空间不用操心”的意思。</p><p>这正是视频最后几分钟的结论：SVD 给四个基本子空间同时选好了标准正交基。</p><ul>
<li><span><span>v1,…,vr\mathbf{v}_1, \dots, \mathbf{v}_r</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：行空间。</li>
<li><span><span>u1,…,ur\mathbf{u}_1, \dots, \mathbf{u}_r</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：列空间。</li>
<li><span><span>vr+1,…,vn\mathbf{v}_{r+1}, \dots, \mathbf{v}_n</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>r</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：零空间，共 <span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 个。</li>
<li><span><span>ur+1,…,um\mathbf{u}_{r+1}, \dots, \mathbf{u}_m</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>r</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：左零空间，共 <span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 个。</li>
</ul><p>Gram-Schmidt（第 17 讲）也能给出标准正交基，但他说它给不出“让矩阵对角”的基，这里需要特征值。我用一个非方的矩阵验证维数：随机造一个 <span><span>4×34 \times 3</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span>、秩为 2 的矩阵（<span><span>4×24 \times 2</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 乘 <span><span>2×32 \times 3</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span>），取完整的 SVD：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 一个 4 x 3、秩为 2 的矩阵：m = 4，n = 3，r = 2</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>2</span><span><span>)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((</span></span><span>2</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)  </span><span># 完整的 SVD：U 是 4 x 4，V^T 是 3 x 3</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>V </span><span>=</span><span> Vt.T</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>int</span><span><span>(np.</span><span>sum</span><span>(s </span><span>&gt;</span><span> </span></span><span>1e-10</span><span>))</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"sigma ="</span><span>, s)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"m, n, r ="</span><span>, m, n, r)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 四个子空间的正交归一基</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"A v_i = sigma_i u_i（i &lt; r）："</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(A </span><span>@</span><span> V[:, :r], U[:, :r] </span><span>*</span><span> s[:r]),</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>23</div></div><div><span>        </span><span>"|A V_null| ="</span><span>,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> V[:, r:])</span></span><span>:.1e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>25</div></div><div><span>        </span><span>"，列数 n - r ="</span><span>,</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>n </span><span>-</span><span> r,</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>29</div></div><div><span>        </span><span>"|A^T U_null| ="</span><span>,</span></div></div><div><div><div>30</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A.T </span><span>@</span><span> U[:, r:])</span></span><span>:.1e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>31</div></div><div><span>        </span><span>"，列数 m - r ="</span><span>,</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>m </span><span>-</span><span> r,</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>35</div></div><div><span>        </span><span>"U 与 V 都是正交矩阵："</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(U.T </span><span>@</span><span> U, np.</span><span>eye</span><span>(m)),</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(V.T </span><span>@</span><span> V, np.</span><span>eye</span><span>(n)),</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span># A^T A 与 A A^T 的非零特征值都是 sigma^2，其余的是 0</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>w_right </span><span>=</span><span> np.linalg.</span><span>eigvalsh</span><span>(A.T </span><span>@</span><span> A)[::</span><span>-</span><span>1</span><span>][:r]</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>w_left </span><span>=</span><span> np.linalg.</span><span>eigvalsh</span><span>(A </span><span>@</span><span> A.T)[::</span><span>-</span><span>1</span><span>][:r]</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A 的前 r 个特征值："</span><span>, w_right)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"A A^T 的前 r 个特征值："</span><span>, w_left)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"sigma^2（前 r 个）="</span><span><span>, s[:r] </span><span>**</span><span> </span></span><span>2</span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 对称正定的特殊情形：奇异值就是特征值</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> np.linalg.</span><span>eigvalsh</span><span>(S)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>s_spd </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(S, </span><span>compute_uv</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"S 的特征值："</span><span><span>, lam[::</span><span>-</span></span><span>1</span><span>], </span><span>"，奇异值："</span><span>, s_spd)</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sigma = [1.8858 0.4917 0.    ]</span></div></div><div><div><div>2</div></div><div><span>m, n, r = 4 3 2</span></div></div><div><div><div>3</div></div><div><span>A v_i = sigma_i u_i（i &lt; r）： True</span></div></div><div><div><div>4</div></div><div><span>|A V_null| = 1.5e-16 ，列数 n - r = 1</span></div></div><div><div><div>5</div></div><div><span>|A^T U_null| = 3.8e-16 ，列数 m - r = 2</span></div></div><div><div><div>6</div></div><div><span>U 与 V 都是正交矩阵： True True</span></div></div><div><div><div>7</div></div><div><span>A^T A 的前 r 个特征值： [3.5561 0.2418]</span></div></div><div><div><div>8</div></div><div><span>A A^T 的前 r 个特征值： [3.5561 0.2418]</span></div></div><div><div><div>9</div></div><div><span>sigma^2（前 r 个）= [3.5561 0.2418]</span></div></div><div><div><div>10</div></div><div><span>S 的特征值： [3. 1.] ，奇异值： [3. 1.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第三个奇异值是 0，所以 <span><span>r=2r = 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>。<span><span>AVnullAV_{\text{null}}</span><span><span><span></span><span>A</span><span><span>V</span><span><span><span><span><span><span></span><span><span><span><span>null</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的范数是 <span><span>1.5×10−161.5 \times 10^{-16}</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，只有 <span><span>n−r=1n - r = 1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 列；<span><span>ATUnullA^TU_{\text{null}}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>U</span><span><span><span><span><span><span></span><span><span><span><span>null</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>3.8×10−163.8 \times 10^{-16}</span><span><span><span></span><span>3.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，有 <span><span>m−r=2m - r = 2</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 列，维数和第 10 讲的四个基本子空间完全对得上。<span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>（<span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span>）和 <span><span>AATAA^T</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（<span><span>4×44 \times 4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span>）的形状不同，但非零特征值都是 3.5561 和 0.2418，等于 <span><span>σi2\sigma_i^2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：它们是同一组数，只是放进了不同大小的矩阵。最后一行是对称正定的特例：<span><span>[2−1−12]\begin{bmatrix} 2 &amp; -1 \\ -1 &amp; 2 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的特征值和奇异值都是 3 和 1。（如果有负特征值，奇异值是它的绝对值，<span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 的对应列差一个符号，这一条我没有在代码里验证。）</p></section>
<section><h2>单位圆变成椭圆<a href="#单位圆变成椭圆"><span>#</span></a></h2><p>回到第一个例子，换成几何的看法。<span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 是对 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 做了三件事：先用 <span><span>VTV^T</span><span><span><span></span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 转一下（正交，不改变长度），再用 <span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 沿两个坐标轴分别拉伸 <span><span>σ1\sigma_1</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>σ2\sigma_2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 倍，最后用 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 再转一下。所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 把单位圆映成一个椭圆，半轴长就是 <span><span>σi\sigma_i</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，半轴方向就是 <span><span>ui\mathbf{u}_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，而单位圆上被映成长轴和短轴的点，正好是 <span><span>vi\mathbf{v}_i</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。把一团圆形的面团沿两个垂直的方向擀开，擀面杖的方向是 <span><span>vi\mathbf{v}_i</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，擀出来的长短轴是 <span><span>ui\mathbf{u}_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。这个类比就用这一次。</p><p>数一下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>4.0</span><span><span>], [</span><span>-</span></span><span>3.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># 单位圆上取 2000 个点，整体乘 A</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span><span> </span><span>*</span><span> np.pi, </span></span><span>2000</span><span>, </span><span>endpoint</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>circle </span><span>=</span><span> np.</span><span>vstack</span><span>([np.</span><span>cos</span><span>(t), np.</span><span>sin</span><span>(t)])  </span><span># 2 x 2000</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>ellipse </span><span>=</span><span> A </span><span>@</span><span> circle</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>lengths </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(ellipse, </span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"sigma ="</span><span>, s)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"像的最长半径："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>lengths.</span><span>max</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"像的最短半径："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>lengths.</span><span>min</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 椭圆的半轴方向就是 u_i：把像转到 U 的坐标系，再除以 sigma</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>canonical </span><span>=</span><span> (U.T </span><span>@</span><span> ellipse) </span><span>/</span><span> s[:, </span><span>None</span><span>]</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>on_unit_circle </span><span>=</span><span> np.</span><span>allclose</span><span>(np.linalg.</span><span>norm</span><span>(canonical, </span><span>axis</span><span>=</span><span>0</span><span>), </span><span>1.0</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"U^T y / sigma 回到单位圆："</span><span>, on_unit_circle)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 面积：鞋带公式，与 pi * sigma1 * sigma2 = pi * |det A| 对照</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>x, y </span><span>=</span><span> ellipse</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>area </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> </span></span><span>abs</span><span><span>(np.</span><span>sum</span><span>(x </span><span>*</span><span> np.</span><span>roll</span><span>(y, </span><span>-</span></span><span>1</span><span><span>) </span><span>-</span><span> np.</span><span>roll</span><span>(x, </span><span>-</span></span><span>1</span><span><span>) </span><span>*</span><span> y))</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"椭圆面积（鞋带公式）："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>area</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"pi * sigma1 * sigma2 ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.pi </span><span>*</span><span> s[</span></span><span>0</span><span><span>] </span><span>*</span><span> s[</span></span><span>1</span><span>]</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"pi * |det A| ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.pi </span><span>*</span><span> </span></span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(A))</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 把 v1 和 v2 送进去：长度被拉成 sigma，方向落在 u1 和 u2 上</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>):</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>image </span><span>=</span><span> A </span><span>@</span><span> Vt[i]</span></div></div><div><div><div>33</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>34</div></div><div><span>            </span><span>f</span><span>"A v</span><span>{</span><span><span>i </span><span>+</span><span> </span></span><span>1}</span><span> ="</span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>            </span></span><span>image,</span></div></div><div><div><div>36</div></div><div><span>            </span><span>"，等于 sigma * u："</span><span>,</span></div></div><div><div><div>37</div></div><div><span><span>            </span></span><span>np.</span><span>allclose</span><span>(image, s[i] </span><span>*</span><span> U[:, i]),</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sigma = [5.6569 4.2426]</span></div></div><div><div><div>2</div></div><div><span>像的最长半径： 5.6569</span></div></div><div><div><div>3</div></div><div><span>像的最短半径： 4.2426</span></div></div><div><div><div>4</div></div><div><span>U^T y / sigma 回到单位圆： True</span></div></div><div><div><div>5</div></div><div><span>椭圆面积（鞋带公式）： 75.3981</span></div></div><div><div><div>6</div></div><div><span>pi * sigma1 * sigma2 = 75.3982</span></div></div><div><div><div>7</div></div><div><span>pi * |det A| = 75.3982</span></div></div><div><div><div>8</div></div><div><span>A v1 = [-5.6569  0.    ] ，等于 sigma * u： True</span></div></div><div><div><div>9</div></div><div><span>A v2 = [0.     4.2426] ，等于 sigma * u： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>2000 个点的像里，最长的半径是 5.6569，最短的是 4.2426，和 <span><span>σ1\sigma_1</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>σ2\sigma_2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在四位小数里一致。把像用 <span><span>UTU^T</span><span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 转到椭圆自己的坐标系再除以 <span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span>，所有点都回到单位圆上，说明这确实是一个以 <span><span>ui\mathbf{u}_i</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为轴、以 <span><span>σi\sigma_i</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为半轴的椭圆。面积由鞋带公式算得 75.3981（离散化的误差），<span><span>πσ1σ2=π∣det⁡A∣=75.3982\pi\sigma_1\sigma_2 = \pi|\det A| = 75.3982</span><span><span><span></span><span>π</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>π</span><span>∣</span><span></span><span>det</span><span></span><span>A</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>75.3982</span></span></span></span>，<span><span>det⁡A=24\det A = 24</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>24</span></span></span></span> 就是 <span><span>σ1σ2\sigma_1\sigma_2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。最后两行直接验证了 <span><span>Avi=σiuiA\mathbf{v}_i = \sigma_i\mathbf{u}_i</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：<span><span>Av1=(−5.6569,0)A\mathbf{v}_1 = (-5.6569, 0)</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>5.6569</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，<span><span>Av2=(0,4.2426)A\mathbf{v}_2 = (0, 4.2426)</span><span><span><span></span><span>A</span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>4.2426</span><span>)</span></span></span></span>，两个像互相垂直，这就是 Strang 在那张图上画的“两个直角”。</p><p></p><figure><img src="/assets/images/linear-algebra/l29-circle-ellipse.svg" alt="左：单位圆和它的右奇异向量 v1、v2。右：像是椭圆，半轴是 σ1 u1 和 σ2 u2" /><figcaption>左：单位圆和它的右奇异向量 v1、v2。右：像是椭圆，半轴是 σ1 u1 和 σ2 u2</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>4.0</span><span><span>], [</span><span>-</span></span><span>3.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span><span> </span><span>*</span><span> np.pi, </span></span><span>400</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>circle </span><span>=</span><span> np.</span><span>vstack</span><span>([np.</span><span>cos</span><span>(t), np.</span><span>sin</span><span>(t)])</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>ellipse </span><span>=</span><span> A </span><span>@</span><span> circle</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>fig, (ax1, ax2) </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(*circle, </span><span>color</span><span>=</span><span>"tab:blue"</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(*ellipse, </span><span>color</span><span>=</span><span>"tab:blue"</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>colors </span><span>=</span><span> [</span><span>"tab:red"</span><span>, </span><span>"tab:green"</span><span>]</span></div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>):</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> Vt[i]</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> s[i] </span><span>*</span><span> U[:, i]</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>ax1.</span><span>annotate</span><span>(</span></div></div><div><div><div>24</div></div><div><span>            </span><span>""</span><span>,</span></div></div><div><div><div>25</div></div><div><span>            </span><span>xy</span><span><span>=</span><span>v,</span></span></div></div><div><div><div>26</div></div><div><span>            </span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>27</div></div><div><span>            </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>arrowstyle</span><span>=</span><span>"-&gt;"</span><span>, </span><span>color</span><span><span>=</span><span>colors[i], </span></span><span>lw</span><span>=</span><span>2</span><span>),</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>ax1.</span><span>text</span><span>(v[</span><span>0</span><span><span>] </span><span>*</span><span> </span></span><span>1.15</span><span>, v[</span><span>1</span><span><span>] </span><span>*</span><span> </span></span><span>1.15</span><span>, </span><span>f</span><span>"$v_</span><span>{</span><span><span>i </span><span>+</span><span> </span></span><span>1}</span><span>$"</span><span>, </span><span>color</span><span><span>=</span><span>colors[i])</span></span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>ax2.</span><span>annotate</span><span>(</span></div></div><div><div><div>31</div></div><div><span>            </span><span>""</span><span>,</span></div></div><div><div><div>32</div></div><div><span>            </span><span>xy</span><span><span>=</span><span>u,</span></span></div></div><div><div><div>33</div></div><div><span>            </span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>34</div></div><div><span>            </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>arrowstyle</span><span>=</span><span>"-&gt;"</span><span>, </span><span>color</span><span><span>=</span><span>colors[i], </span></span><span>lw</span><span>=</span><span>2</span><span>),</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>pos </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> u </span><span>+</span><span> np.</span><span>array</span><span>([</span></span><span>0.0</span><span>, </span><span>0.35</span><span>]) </span><span>if</span><span><span> i </span><span>==</span><span> </span></span><span>0</span><span> </span><span>else</span><span><span> u </span><span>*</span><span> </span></span><span>1.1</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>ax2.</span><span>text</span><span>(</span></div></div><div><div><div>38</div></div><div><span><span>            </span></span><span>pos[</span><span>0</span><span>], pos[</span><span>1</span><span>], </span><span>f</span><span>"$</span><span>\\</span><span>sigma_</span><span>{</span><span><span>i </span><span>+</span><span> </span></span><span>1}</span><span> u_</span><span>{</span><span><span>i </span><span>+</span><span> </span></span><span>1}</span><span>$"</span><span>, </span><span>color</span><span><span>=</span><span>colors[i]</span></span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_title</span><span>(</span><span>"unit circle, right singular vectors $v_i$"</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_title</span><span>(</span><span>f</span><span>"image: $</span><span>\\</span><span>sigma_1$ = </span><span>{</span><span>s[</span><span>0</span><span>]</span><span>:.4f</span><span>}</span><span>, $</span><span>\\</span><span>sigma_2$ = </span><span>{</span><span>s[</span><span>1</span><span>]</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlim</span><span>(</span><span>-</span><span>1.6</span><span>, </span><span>1.6</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylim</span><span>(</span><span>-</span><span>1.6</span><span>, </span><span>1.6</span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlim</span><span>(</span><span>-</span><span>7</span><span>, </span><span>7</span><span>)</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylim</span><span>(</span><span>-</span><span>7</span><span>, </span><span>7</span><span>)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>for</span><span> ax </span><span>in</span><span> (ax1, ax2):</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>ax.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>ax.</span><span>axhline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"gray"</span><span>, </span><span>lw</span><span>=</span><span>0.5</span><span>)</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>ax.</span><span>axvline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"gray"</span><span>, </span><span>lw</span><span>=</span><span>0.5</span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>ax.</span><span>grid</span><span>(</span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>53</div></div><div><span>        </span><span>"l29-circle-ellipse.svg"</span><span>,</span></div></div><div><div><div>54</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意图里 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 指向左下，<span><span>σ1u1\sigma_1\mathbf{u}_1</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 指向左：这是 <code>np.linalg.svd</code> 选的符号。换成黑板上的符号只会让箭头翻到另一边，椭圆本身不变。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>多变量控制里的“增益”不是一个数，而是一个矩阵 <span><span>GG</span><span><span><span></span><span>G</span></span></span></span>：输入 <span><span>u\mathbf{u}</span><span><span><span></span><span>u</span></span></span></span> 是几个执行器，输出 <span><span>y=Gu\mathbf{y} = G\mathbf{u}</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>G</span><span>u</span></span></span></span> 是几个被控量。这时 SVD 回答的问题是：<strong>哪个输入方向增益大，哪个增益小？</strong> 奇异值 <span><span>σ1\sigma_1</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是任何单位输入能得到的最大输出，<span><span>σn\sigma_n</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是最小的，<span><span>σ1/σn\sigma_1/\sigma_n</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就是条件数。<a href="/posts/pid-control-08-mimo/">PID 控制系列第 8 篇</a>里 Wood–Berry 精馏塔的稳态增益矩阵是 <span><span>G(0)=[12.8−18.96.6−19.4]G(0) = \begin{bmatrix} 12.8 &amp; -18.9 \\ 6.6 &amp; -19.4 \end{bmatrix}</span><span><span><span></span><span>G</span><span>(</span><span>0</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>12.8</span></span></span><span><span></span><span><span>6.6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>18.9</span></span></span><span><span></span><span><span>−</span><span>19.4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，那篇用相对增益阵（RGA）判断了输入输出怎么配对，这里用 SVD 看同一个矩阵：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># Wood-Berry 精馏塔的稳态增益矩阵 G(0)，y = G u</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>12.8</span><span><span>, </span><span>-</span></span><span>18.9</span><span>], [</span><span>6.6</span><span><span>, </span><span>-</span></span><span>19.4</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(G)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"sigma ="</span><span>, s)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"条件数 sigma1 / sigma2 ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span>s[</span><span>0</span><span><span>] </span><span>/</span><span> s[</span></span><span>1</span><span>]</span><span>:.2f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.cond(G) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(G)</span></span><span>:.2f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"高增益的输入方向 v1 ="</span><span>, Vt[</span><span>0</span><span>], </span><span>"，输出方向 u1 ="</span><span>, U[:, </span><span>0</span><span>])</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"低增益的输入方向 v2 ="</span><span>, Vt[</span><span>1</span><span>], </span><span>"，输出方向 u2 ="</span><span>, U[:, </span><span>1</span><span>])</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># 单位长度的输入，沿不同方向推，输出有多长</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, np.pi, </span><span>1000</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>inputs </span><span>=</span><span> np.</span><span>vstack</span><span>([np.</span><span>cos</span><span>(theta), np.</span><span>sin</span><span>(theta)])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>gains </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(G </span><span>@</span><span> inputs, </span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>21</div></div><div><span>        </span><span>"1000 个方向上的增益："</span><span>,</span></div></div><div><div><div>22</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>gains.</span><span>min</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>23</div></div><div><span>        </span><span>"到"</span><span>,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>gains.</span><span>max</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>for</span><span> name, u </span><span>in</span><span> [(</span><span>"u = (1, 0)"</span><span>, [</span><span>1.0</span><span>, </span><span>0.0</span><span>]), (</span><span>"u = (0, 1)"</span><span>, [</span><span>0.0</span><span>, </span><span>1.0</span><span>])]:</span></div></div><div><div><div>27</div></div><div><span>        </span><span>print</span><span>(name, </span><span>"的增益："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(G </span><span>@</span><span> np.</span><span>array</span><span>(u))</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 要让输出沿 u1、u2 各长 1，输入要多长</span></div></div><div><div><div>30</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>):</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>u_need </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(G, U[:, i])</span></div></div><div><div><div>32</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>33</div></div><div><span>            </span><span>f</span><span>"输出沿 u</span><span>{</span><span><span>i </span><span>+</span><span> </span></span><span>1}</span><span> 长为 1，所需输入的长度："</span><span>,</span></div></div><div><div><div>34</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(u_need)</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sigma = [30.4048  4.0645]</span></div></div><div><div><div>2</div></div><div><span>条件数 sigma1 / sigma2 = 7.48</span></div></div><div><div><div>3</div></div><div><span>np.linalg.cond(G) = 7.48</span></div></div><div><div><div>4</div></div><div><span>高增益的输入方向 v1 = [-0.4585  0.8887] ，输出方向 u1 = [-0.7454 -0.6666]</span></div></div><div><div><div>5</div></div><div><span>低增益的输入方向 v2 = [-0.8887 -0.4585] ，输出方向 u2 = [-0.6666  0.7454]</span></div></div><div><div><div>6</div></div><div><span>1000 个方向上的增益： 4.0647 到 30.4048</span></div></div><div><div><div>7</div></div><div><span>u = (1, 0) 的增益： 14.4014</span></div></div><div><div><div>8</div></div><div><span>u = (0, 1) 的增益： 27.0845</span></div></div><div><div><div>9</div></div><div><span>输出沿 u1 长为 1，所需输入的长度： 0.0329</span></div></div><div><div><div>10</div></div><div><span>输出沿 u2 长为 1，所需输入的长度： 0.2460</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个奇异值是 30.4048 和 4.0645，条件数 7.48。单位长度的输入沿不同方向推，增益从 4.0647 到 30.4048（和 <span><span>σ2\sigma_2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>σ1\sigma_1</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在网格精度内一致）。只推单个输入时，增益取决于哪个输入：<span><span>u=(1,0)\mathbf{u} = (1, 0)</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 的增益是 14.4014，<span><span>u=(0,1)\mathbf{u} = (0, 1)</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 是 27.0845，都夹在两个极值中间。要把输出沿 <span><span>u1\mathbf{u}_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 推 1 个单位，输入长度是 0.0329；沿 <span><span>u2\mathbf{u}_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 推 1 个单位，输入长度是 0.2460，比值恰好是条件数 7.48。</p><p>读法是：这个塔的输出沿 <span><span>u2=(−0.6666,0.7454)\mathbf{u}_2 = (-0.6666, 0.7454)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.6666</span><span>,</span><span></span><span>0.7454</span><span>)</span></span></span></span> 的方向最“难推”，需要的输入是另一个方向的 7.48 倍，如果输入有限幅，我猜这个方向会先撞上限幅。我也猜这就是 MIMO 控制里“条件数大，对模型误差敏感、要小心解耦”的由来；这一篇我只验证了稳态增益的方向性，没有验证它对闭环性能的影响。</p></section>
<section><h2>下一讲<a href="#下一讲"><span>#</span></a></h2><p>这一讲的 SVD 把行空间和列空间配成了一对一对的正交方向。下一讲 Strang 要问的是：任何一个线性变换，换一组基，矩阵长什么样？SVD 正是“选两组好基让矩阵最简单”的一个答案，细节见 <a href="/posts/linear-algebra-30-linear-transformations/">18.06 第 30 讲：线性变换及其矩阵</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-28-similar-matrices-jordan/</id>
      <title type="text">18.06 第 28 讲：相似矩阵与 Jordan 标准形</title>
      <published>2026-10-18T09:20:00.000Z</published>
      <updated>2026-10-18T09:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-28-similar-matrices-jordan/"/>
      <summary type="text">MIT 18.06 第 28 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 验证了相似矩阵的特征值相同，再给 4 阶 Jordan 块加了 1e-10 的扰动，特征值就跑到了模长 3.16e-03 的位置。</summary>
      <content type="html"><![CDATA[<p>这一讲的主角是 <span><span>B=M−1AMB = M^{-1}AM</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span></span></span></span>：同一个线性变换换一套基写下来，矩阵就变成了 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span>，但特征值一个都没变。Strang 把所有这样连起来的矩阵叫作一个“家族”，每个家族里挑一个最规整的代表，这个代表就是 Jordan 标准形。可是我把一个 4 阶 Jordan 块的左下角改了 <span><span>10−1010^{-10}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，特征值立刻从 0 跑到了模长 <span><span>3.16×10−33.16 \times 10^{-3}</span><span><span><span></span><span>3.16</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，放大了 3000 多万倍。他在课上说 Jordan 形在数值上不是好东西，我一开始以为是客气话。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=TSdXJw83kyA" target="_blank">Lecture 28: Similar matrices and Jordan form</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-28-similar-matrices-and-jordan-form/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §6.2 Diagonalizing a Matrix；Appendix 5 The Jordan Form of a Square Matrix
前置：<a href="/posts/linear-algebra-27-positive-definite-minima/">第 27 讲：正定矩阵与极小值</a>
一句话：相似矩阵有相同的特征值；每个方阵都相似于一个 Jordan 矩阵，对角化只是它不缺特征向量时的特例。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>前十分钟还在讲正定矩阵：逆矩阵、<span><span>A+BA + B</span><span><span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span></span></span></span>、<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 是不是正定。这段很短，我在第一节用代码过了一遍，可以倍速。</li>
<li>他证明“相似矩阵特征值相同”的那几行（在 <span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 中间塞一个 <span><span>MM−1MM^{-1}</span><span><span><span></span><span>M</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>）值得认真看，整讲只有这一处是证明。</li>
<li>他当场“编”一个 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 去算 <span><span>M−1AMM^{-1}AM</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span></span></span></span>，一路算一路惊呼怎么会是 <span><span>−15-15</span><span><span><span></span><span>−</span><span>15</span></span></span></span>，随后用迹和行列式验算。这一段有人味，不必倍速。</li>
<li>后半段讲 <span><span>4×44 \times 4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的 Jordan 块那几分钟信息密度最高：两个矩阵特征值相同、特征向量个数也相同，却不相似。暂停看一遍黑板上的两个矩阵。</li>
</ul></div></div>
<section><h2>开头十分钟：正定矩阵从哪里来<a href="#开头十分钟正定矩阵从哪里来"><span>#</span></a></h2><p>这是上一讲的尾巴。他想补一句：正定矩阵不是凭空冒出来的，最常见的出处是最小二乘里那个 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>。他顺手问了三个问题，我用 NumPy 各验证一次。</p><p><strong>对称正定矩阵的逆还正定吗？</strong> 还正定，因为逆矩阵的特征值是原来的倒数，正数的倒数还是正数。（他的原话大意是：逆矩阵的主元我们知道得不多，特征值我们什么都知道。）</p><p><strong>两个正定矩阵相加呢？</strong> 主元和特征值都帮不上忙，但二次型可以：<span><span>xT(A+B)x=xTAx+xTBx\mathbf{x}^\mathsf{T}(A + B)\mathbf{x} = \mathbf{x}^\mathsf{T}A\mathbf{x} + \mathbf{x}^\mathsf{T}B\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>)</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span><span>x</span></span></span></span>，两项都大于 0，和当然大于 0。</p><p><strong>长方形的 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 呢？</strong> <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 本身不对称，甚至不是方阵，无从谈起正定。但 <span><span>xTATAx=(Ax)T(Ax)=∥Ax∥2≥0\mathbf{x}^\mathsf{T}A^\mathsf{T}A\mathbf{x} = (A\mathbf{x})^\mathsf{T}(A\mathbf{x}) = \|A\mathbf{x}\|^2 \ge 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>A</span><span>x</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>。想让它严格大于 0，只需要 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 除了 <span><span>x=0\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 之外都不为 0，也就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的零空间里只有零向量，也就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列线性无关。他的例子是 11 × 5 的矩阵，秩要等于 5。</p><p>下面的脚本里，<span><span>GG</span><span><span><span></span><span>G</span></span></span></span> 是随机的 11 × 5 矩阵，<span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 是把第五列抄成第四列的版本：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>is_pd</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>bool</span><span><span>(np.</span><span>all</span><span>(np.linalg.</span><span>eigvalsh</span><span>(M) </span><span>&gt;</span><span> </span></span><span>1e-12</span><span>))</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 对称正定矩阵的逆：特征值取倒数</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(A))</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^-1 的特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(np.linalg.</span><span>inv</span><span>(A)))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 两个正定矩阵之和：x^T (A + B) x = x^T A x + x^T B x</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span><span>], [</span><span>-</span></span><span>2.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A、B、A + B 都正定："</span><span><span>, </span><span>is_pd</span><span>(A), </span><span>is_pd</span><span>(B), </span><span>is_pd</span><span>(A </span><span>+</span><span> B))</span></span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>22</div></div><div><span>        </span><span>"二次型可加："</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>np.</span><span>isclose</span><span>(x </span><span>@</span><span> (A </span><span>+</span><span> B) </span><span>@</span><span> x, x </span><span>@</span><span> A </span><span>@</span><span> x </span><span>+</span><span> x </span><span>@</span><span> B </span><span>@</span><span> x),</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 11 x 5 的长方形矩阵：G^T G 是方阵、对称，而且正定</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>11</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>K </span><span>=</span><span> G.T </span><span>@</span><span> G</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>5</span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>31</div></div><div><span>        </span><span>f</span><span>"x^T K x = </span><span>{</span><span><span>x </span><span>@</span><span> K </span><span>@</span><span> x</span></span><span>:.6f</span><span>}</span><span>，|Gx|^2 = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(G </span><span>@</span><span> x) </span><span>**</span><span> </span></span><span>2</span><span>:.6f</span><span>}</span><span>"</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"K 的最小特征值：</span><span>{</span><span><span>np.linalg.</span><span>eigvalsh</span><span>(K).</span><span>min</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 第五列抄第四列：零空间不再只有零向量，只剩半正定</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>H </span><span>=</span><span> G.</span><span>copy</span><span>()</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>H[:, </span><span>4</span><span><span>] </span><span>=</span><span> H[:, </span></span><span>3</span><span>]</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>K2 </span><span>=</span><span> H.T </span><span>@</span><span> H</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"H 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(H))</span></span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"n^T K2 n = </span><span>{</span><span><span>n </span><span>@</span><span> K2 </span><span>@</span><span> n</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"K2 的最小特征值：</span><span>{</span><span><span>np.linalg.</span><span>eigvalsh</span><span>(K2).</span><span>min</span><span>()</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 的特征值： [1. 3.]</span></div></div><div><div><div>2</div></div><div><span>A^-1 的特征值： [0.3333 1.    ]</span></div></div><div><div><div>3</div></div><div><span>A、B、A + B 都正定： True True True</span></div></div><div><div><div>4</div></div><div><span>二次型可加： True</span></div></div><div><div><div>5</div></div><div><span>x^T K x = 16.278015，|Gx|^2 = 16.278015</span></div></div><div><div><div>6</div></div><div><span>K 的最小特征值：2.7552</span></div></div><div><div><div>7</div></div><div><span>H 的秩： 4</span></div></div><div><div><div>8</div></div><div><span>n^T K2 n = 0.0e+00</span></div></div><div><div><div>9</div></div><div><span>K2 的最小特征值：4.4e-16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>A+BA + B</span><span><span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span></span></span></span> 的三个“正定”都是 <code>True</code>。<span><span>GG</span><span><span><span></span><span>G</span></span></span></span> 的 <span><span>K=GTGK = G^\mathsf{T}G</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>G</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>G</span></span></span></span> 最小特征值是 2.7552，完全正定；<span><span>xTKx\mathbf{x}^\mathsf{T}K\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>K</span><span>x</span></span></span></span> 和 <span><span>∥Gx∥2\|G\mathbf{x}\|^2</span><span><span><span></span><span>∥</span><span>G</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 在六位小数内相同，因为它们本来就是同一个数。<span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的秩掉到 4，零空间里多了 <span><span>(0,0,0,1,−1)(0, 0, 0, 1, -1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>：沿着这个方向二次型是 0，最小特征值只剩 <span><span>4.4×10−164.4 \times 10^{-16}</span><span><span><span></span><span>4.4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，也就是舍入误差，正定退化成半正定。</p></section>
<section><h2>相似矩阵：同一个家族，同一组特征值<a href="#相似矩阵同一个家族同一组特征值"><span>#</span></a></h2><p><strong>“相似”是什么意思？</strong> 两个 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 相似，指的是存在可逆矩阵 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>，使得</p><span><span><span>B=M−1AMB = M^{-1}AM</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span></span></span></span></span><p>上一章的对角化是它的特例：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 有一整套特征向量时，取 <span><span>M=SM = S</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span></span></span></span>（特征向量矩阵），<span><span>S−1AS=ΛS^{-1}AS = \Lambda</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>Λ</span></span></span></span>。所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 相似于 <span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span>。换一个别的 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>，得到的 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 不是对角的，但仍然和 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 相似。Strang 把相似的矩阵看成一个“家族”，对角的 <span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span> 是家族里最体面的成员。</p><p>他在黑板上的例子是 <span><span>A=[2112]A = \begin{bmatrix} 2 &amp; 1 \\ 1 &amp; 2 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，特征值 3 和 1。他随手编了 <span><span>M=[1401]M = \begin{bmatrix} 1 &amp; 4 \\ 0 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>（上三角，逆矩阵好写），算出</p><span><span><span>B=M−1AM=[−2−1516]B = M^{-1}AM = \begin{bmatrix} -2 &amp; -15 \\ 1 &amp; 6 \end{bmatrix}</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>15</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>这个矩阵看上去什么特点也没有，但迹是 <span><span>−2+6=4-2 + 6 = 4</span><span><span><span></span><span>−</span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>，行列式是 <span><span>−12+15=3-12 + 15 = 3</span><span><span><span></span><span>−</span><span>12</span><span></span><span>+</span><span></span></span><span><span></span><span>15</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，和 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的迹 4、行列式 3 一样，所以特征值也是 3 和 1。（他自己心算 <span><span>M−1AMM^{-1}AM</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span></span></span></span> 的时候也算得磕磕绊绊，我这里让 NumPy 来。）</p><p><strong>为什么特征值不变？</strong> 从 <span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 出发，在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 之间塞一个 <span><span>MM−1MM^{-1}</span><span><span><span></span><span>M</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，两边左乘 <span><span>M−1M^{-1}</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>M−1AMM−1x=λM−1x⟹B (M−1x)=λ (M−1x)M^{-1}AMM^{-1}\mathbf{x} = \lambda M^{-1}\mathbf{x}
\quad\Longrightarrow\quad
B\,(M^{-1}\mathbf{x}) = \lambda\,(M^{-1}\mathbf{x})</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>M</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>x</span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span>B</span><span></span><span>(</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span></span><span>(</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>x</span><span>)</span></span></span></span></span><p><span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 也是 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的特征值，对应的特征向量是 <span><span>M−1xM^{-1}\mathbf{x}</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>x</span></span></span></span>。特征值留下了，特征向量被 <span><span>M−1M^{-1}</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 挪了个位置。（要是连特征向量也不动，<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 大概就等于 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 了。）我把这几句都跑了一遍，同时在家族里另找两个三角矩阵：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>4.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> A </span><span>@</span><span> M</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"B = M^-1 A M ="</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(B)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A)))</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(B)))</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹："</span><span><span>, np.</span><span>trace</span><span>(A), np.</span><span>trace</span><span>(B))</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"行列式：</span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(A)</span></span><span>:.4f</span><span>}</span><span> </span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(B)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 特征向量被 M^-1 挪了个位置</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>lam, V </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> V</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"M^-1 x 是 B 的特征向量："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> W, W </span><span>*</span><span> lam))</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 对角化是最特殊的一次相似：M 取特征向量矩阵 S</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> V</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"S^-1 A S ="</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span><span>(np.linalg.</span><span>inv</span><span>(S) </span><span>@</span><span> A </span><span>@</span><span> S)</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 家族里再取几个成员：迹是 4，行列式是 3</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>5.0</span><span>], [</span><span>0.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>30</div></div><div><span>    </span><span>for</span><span> name, X </span><span>in</span><span> [(</span><span>"三角 C"</span><span>, C), (</span><span>"三角 D"</span><span>, D)]:</span></div></div><div><div><div>31</div></div><div><span>        </span><span>print</span><span>(name, </span><span>"的特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(X)))</span></span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 和 B 的特征向量相同吗："</span><span><span>, np.</span><span>allclose</span><span>(np.</span><span>abs</span><span>(V), np.</span><span>abs</span><span>(W)))</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>B = M^-1 A M =</span></div></div><div><div><div>2</div></div><div><span>[[ -2. -15.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[  1.   6.]]</span></div></div><div><div><div>4</div></div><div><span>A 的特征值： [1. 3.]</span></div></div><div><div><div>5</div></div><div><span>B 的特征值： [1. 3.]</span></div></div><div><div><div>6</div></div><div><span>迹： 4.0 4.0</span></div></div><div><div><div>7</div></div><div><span>行列式：3.0000 3.0000</span></div></div><div><div><div>8</div></div><div><span>M^-1 x 是 B 的特征向量： True</span></div></div><div><div><div>9</div></div><div><span>S^-1 A S =</span></div></div><div><div><div>10</div></div><div><span>[[ 3.  0.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[-0.  1.]]</span></div></div><div><div><div>12</div></div><div><span>三角 C 的特征值： [1. 3.]</span></div></div><div><div><div>13</div></div><div><span>三角 D 的特征值： [1. 3.]</span></div></div><div><div><div>14</div></div><div><span>A 和 B 的特征向量相同吗： False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>B</code> 和 <code>A</code> 的特征值都是 <span><span>[1,3][1, 3]</span><span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span>3</span><span>]</span></span></span></span>，迹都是 4，行列式都是 3.0000。<code>M^-1 x 是 B 的特征向量</code> 为 <code>True</code>，而 <code>A 和 B 的特征向量相同吗</code> 为 <code>False</code>，对应他说的“特征向量被挪了位置”。最后两个三角矩阵 <span><span>[1503]\begin{bmatrix} 1 &amp; 5 \\ 0 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 和 <span><span>[3101]\begin{bmatrix} 3 &amp; 1 \\ 0 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的特征值也是 1 和 3：迹为 4、行列式为 3 的 2 × 2 矩阵，特征多项式都是 <span><span>λ2−4λ+3\lambda^2 - 4\lambda + 3</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>4</span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span></span></span></span>，特征值都是 3 和 1，互不相同，所以它们都和 <span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span> 相似，也就彼此相似。</p><p>如果硬要找个类比，我觉得它像同一座城市用不同的坐标系画地图：街道的连通方式（特征值）是地图的内在性质，地图怎么摆（特征向量）取决于你选哪个原点和朝向。这个类比到这里就收。</p></section>
<section><h2>孤家寡人 4I 和只有一个特征向量的 Jordan 块<a href="#孤家寡人-4i-和只有一个特征向量的-jordan-块"><span>#</span></a></h2><p><strong>特征值重复了会怎样？</strong> 刚才的家族之所以好，是因为两个特征值 3 和 1 不同。如果两个特征值相等，就可能缺特征向量，对角化不再保证。Strang 取了特征值 4 和 4：</p><ul>
<li><span><span>4I4I</span><span><span><span></span><span>4</span><span>I</span></span></span></span> 自己一个家族。（他说到这里承认，学到 18.06 的这个阶段，他已经把矩阵当人看了。）</li>
<li>任何 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 都有 <span><span>M−1(4I)M=4M−1M=4IM^{-1}(4I)M = 4M^{-1}M = 4I</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>4</span><span>I</span><span>)</span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span>I</span></span></span></span>，挪不出第二个成员。</li>
<li>其余所有迹为 8、行列式为 16 的矩阵（他举了 <span><span>[51−13]\begin{bmatrix} 5 &amp; 1 \\ -1 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 和 <span><span>[41704]\begin{bmatrix} 4 &amp; 17 \\ 0 &amp; 4 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>17</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>）都在另一个家族里。这个家族里不可能有对角矩阵，因为对角的只能是 <span><span>4I4I</span><span><span><span></span><span>4</span><span>I</span></span></span></span>，而 <span><span>4I4I</span><span><span><span></span><span>4</span><span>I</span></span></span></span> 不相似于其中任何一个。</li>
</ul><p>这个大家族里最规整的是 Jordan 块 <span><span>J=[4104]J = \begin{bmatrix} 4 &amp; 1 \\ 0 &amp; 4 \end{bmatrix}</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。他说他把 1 改成 10 或者 100 万，也能找到 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 连回来，所以都是亲戚，Jordan 挑的是最简洁的那个，对角线上方保留一个 1。</p><p>下面用特征向量个数（<span><span>2−rank⁡(X−4I)2 - \operatorname{rank}(X - 4I)</span><span><span><span></span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span><span>rank</span></span><span>(</span><span>X</span><span></span><span>−</span><span></span></span><span><span></span><span>4</span><span>I</span><span>)</span></span></span></span>）当“指纹”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 4I 是孤家寡人：任何 M 都挪不动它</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>four </span><span>=</span><span> </span><span>4.0</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>moved </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> four </span><span>@</span><span> M</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"M^-1 (4I) M = 4I："</span><span><span>, np.</span><span>allclose</span><span>(moved, four))</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 特征值 4、4 的另一族：迹 8、行列式 16，只有一个特征向量</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>J </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>]])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>family </span><span>=</span><span> {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>"J"</span><span>: J,</span></div></div><div><div><div>18</div></div><div><span>        </span><span>"[[5, 1], [-1, 3]]"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>5.0</span><span>, </span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>3.0</span><span>]]),</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"[[4, 17], [0, 4]]"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>4.0</span><span>, </span><span>17.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>]]),</span></div></div><div><div><div>20</div></div><div><span>        </span><span>"M^-1 J M"</span><span><span>: np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> J </span><span>@</span><span> M,</span></span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> name, X </span><span>in</span><span><span> family.</span><span>items</span><span>():</span></span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>rank </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(X </span><span>-</span><span> </span><span>4.0</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>), </span><span>tol</span><span>=</span><span>1e-8</span><span>)</span></div></div><div><div><div>24</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>25</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>name</span><span>:&gt;18</span><span>}</span><span>：迹 </span><span>{</span><span><span>np.</span><span>trace</span><span>(X)</span></span><span>:.1f</span><span>}</span><span>，det </span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(X)</span></span><span>:.1f</span><span>}</span><span>，"</span></div></div><div><div><div>26</div></div><div><span>            </span><span>f</span><span>"特征向量个数 </span><span>{2</span><span><span> </span><span>-</span><span> rank</span></span><span>}</span><span>"</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 4I 有两个特征向量，J 只有一个：所以不可能相似</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>rank </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(four </span><span>-</span><span> </span><span>4.0</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>))</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"4I 的特征向量个数："</span><span>, </span><span>2</span><span><span> </span><span>-</span><span> rank)</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 对 J 求特征向量矩阵：两列平行，S 奇异（容差内），凑不出对角化</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>_, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(J)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"eig(J) 的特征向量矩阵："</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(S)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"S 的秩（容差 1e-8）："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(S, </span></span><span>tol</span><span>=</span><span>1e-8</span><span>))</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>M^-1 (4I) M = 4I： True</span></div></div><div><div><div>2</div></div><div><span><span>                 </span></span><span>J：迹 8.0，det 16.0，特征向量个数 1</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[[5, 1], [-1, 3]]：迹 8.0，det 16.0，特征向量个数 1</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[[4, 17], [0, 4]]：迹 8.0，det 16.0，特征向量个数 1</span></div></div><div><div><div>5</div></div><div><span><span>          </span></span><span>M^-1 J M：迹 8.0，det 16.0，特征向量个数 1</span></div></div><div><div><div>6</div></div><div><span>4I 的特征向量个数： 2</span></div></div><div><div><div>7</div></div><div><span>eig(J) 的特征向量矩阵：</span></div></div><div><div><div>8</div></div><div><span>[[ 1. -1.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[ 0.  0.]]</span></div></div><div><div><div>10</div></div><div><span>S 的秩（容差 1e-8）： 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个成员的特征向量个数都是 1，而 <span><span>4I4I</span><span><span><span></span><span>4</span><span>I</span></span></span></span> 是 2，所以 <span><span>4I4I</span><span><span><span></span><span>4</span><span>I</span></span></span></span> 不可能和它们相似，和 Strang 说的一致。<code>np.linalg.eig(J)</code> 给出的特征向量矩阵是 <span><span>[1−100]\begin{bmatrix} 1 &amp; -1 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，两列平行，秩（容差 <span><span>10−810^{-8}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>）是 1，没法当 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 用：Jordan 块只有一个特征向量，就是对角化失败的原因。（<code>M^-1 J M</code> 那一行我最初没加 <code>tol</code>，默认容差把噪声当成了秩，算出 0 个特征向量，我才发现应该显式给容差。）</p></section>
<section><h2>光数特征向量还不够<a href="#光数特征向量还不够"><span>#</span></a></h2><p><strong>特征值相同、特征向量个数也相同，就相似了吗？</strong> 他接着给了一对 4 × 4 矩阵，让人做噩梦：特征值都是 4 个 0。</p><ul>
<li>第一个：<span><span>J1J_1</span><span><span><span></span><span><span>J</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 和 <span><span>(3,4)(3, 4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 位置是 1，其余是 0。它是两个 2 × 2 的 Jordan 块。</li>
<li>第二个：<span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 和 <span><span>(2,3)(2, 3)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 位置是 1，其余是 0。它是一个 3 × 3 块加一个 1 × 1 块。</li>
</ul><p>两个矩阵的秩都是 2，所以零空间维数都是 <span><span>4−2=24 - 2 = 2</span><span><span><span></span><span>4</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，各有两个特征向量，缺两个。他说光数特征向量，它们看上去像是相似的，其实不相似。区别在块的大小：每个 Jordan 块贡献一个特征向量，块数就是特征向量个数，但块大小不同，家族就不同。这里的判别方法他没讲，我用的是 <span><span>NkN^k</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 的秩：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>nilpotent</span><span>(</span><span>blocks</span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""按块大小构造特征值全为 0 的 Jordan 矩阵。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>sum</span><span>(blocks)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, n))</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>start </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> size </span><span>in</span><span> blocks:</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(start, start </span><span>+</span><span> size </span><span>-</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>N[i, i </span><span>+</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>start </span><span>+=</span><span> size</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> N</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>rank </span><span>=</span><span> np.linalg.matrix_rank</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>two_two </span><span>=</span><span> </span><span>nilpotent</span><span>([</span><span>2</span><span>, </span><span>2</span><span>])</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>three_one </span><span>=</span><span> </span><span>nilpotent</span><span>([</span><span>3</span><span>, </span><span>1</span><span>])</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"2 + 2 块："</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(two_two)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"3 + 1 块："</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(three_one)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>for</span><span> name, N </span><span>in</span><span> [(</span><span>"2 + 2"</span><span>, two_two), (</span><span>"3 + 1"</span><span>, three_one)]:</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>ranks </span><span>=</span><span> [</span><span>int</span><span><span>(</span><span>rank</span><span>(np.linalg.</span><span>matrix_power</span><span>(N, k))) </span></span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)]</span></div></div><div><div><div>28</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：特征值 </span><span>{</span><span><span>np.linalg.</span><span>eigvals</span><span>(N)</span></span><span>}</span><span>，N^k 的秩 </span><span>{</span><span>ranks</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 把 2 + 2 里的一个 0 换成 7：Strang 没说是哪个 0，两种选法差别很大</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> pos </span><span>in</span><span> [(</span><span>0</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>2</span><span>)]:</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>X </span><span>=</span><span> two_two.</span><span>copy</span><span>()</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>X[pos] </span><span>=</span><span> </span><span>7.0</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>ranks </span><span>=</span><span> [</span><span>int</span><span><span>(</span><span>rank</span><span>(np.linalg.</span><span>matrix_power</span><span>(X, k))) </span></span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)]</span></div></div><div><div><div>35</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"位置 </span><span>{</span><span>pos</span><span>}</span><span> 换成 7：N^k 的秩 </span><span>{</span><span>ranks</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span># 用 M^-1 X M 随便挪一下，N^k 的秩序列不变</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>Y </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(M) </span><span>@</span><span> two_two </span><span>@</span><span> M</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>ranks </span><span>=</span><span> [</span></div></div><div><div><div>42</div></div><div><span>        </span><span>int</span><span><span>(</span><span>rank</span><span>(np.linalg.</span><span>matrix_power</span><span>(Y, k), </span></span><span>tol</span><span>=</span><span>1e-8</span><span>)) </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"相似挪动后的 2 + 2：N^k 的秩"</span><span>, ranks)</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>2 + 2 块：</span></div></div><div><div><div>2</div></div><div><span>[[0. 1. 0. 0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 0. 0. 0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[0. 0. 0. 1.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[0. 0. 0. 0.]]</span></div></div><div><div><div>6</div></div><div><span>3 + 1 块：</span></div></div><div><div><div>7</div></div><div><span>[[0. 1. 0. 0.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[0. 0. 1. 0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[0. 0. 0. 0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0. 0. 0. 0.]]</span></div></div><div><div><div>11</div></div><div><span>2 + 2：特征值 [0. 0. 0. 0.]，N^k 的秩 [4, 2, 0, 0]</span></div></div><div><div><div>12</div></div><div><span>3 + 1：特征值 [0. 0. 0. 0.]，N^k 的秩 [4, 2, 1, 0]</span></div></div><div><div><div>13</div></div><div><span>位置 (0, 3) 换成 7：N^k 的秩 [4, 2, 0, 0]</span></div></div><div><div><div>14</div></div><div><span>位置 (1, 2) 换成 7：N^k 的秩 [4, 3, 2, 1]</span></div></div><div><div><div>15</div></div><div><span>相似挪动后的 2 + 2：N^k 的秩 [4, 2, 0, 0]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的秩两个矩阵都是 2，<span><span>N2N^2</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的秩才分出高下：2 + 2 是 0，3 + 1 是 1。这是 Jordan 结构的指纹，经过相似变换不变（最后一行，用随机的 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 挪过以后还是 <span><span>[4,2,0,0][4, 2, 0, 0]</span><span><span><span></span><span>[</span><span>4</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span>）。</p><p>他还说，把 2 + 2 里的“一个 0”改成 7，秩仍然是 2，还是同一个家族，只是不如原来那个好看。转写里没说清是哪个 0，所以我两种都试了：改 <span><span>(1,4)(1, 4)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 位置，秩序列还是 <span><span>[4,2,0,0][4, 2, 0, 0]</span><span><span><span></span><span>[</span><span>4</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span>，确实同族；改 <span><span>(2,3)(2, 3)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 位置（两个块之间那一格），秩变成 <span><span>[4,3,2,1][4, 3, 2, 1]</span><span><span><span></span><span>[</span><span>4</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>]</span></span></span></span>，已经变成一个 4 阶块了。我猜他指的是前者，毕竟他是在强调“秩没变”。</p><p>于是 Jordan 定理的完整表述就清楚了：每个方阵相似于一个 Jordan 矩阵 <span><span>JJ</span><span><span><span></span><span>J</span></span></span></span>，由若干 Jordan 块拼成，对角线上是特征值，块内对角线上方是 1。块数等于特征向量个数。特征值互不相同时，每块是 1 × 1，<span><span>J=ΛJ = \Lambda</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>Λ</span></span></span></span>，这是“好情形”，整门课主要讲这个。</p></section>
<section><h2>Jordan 形为什么在数值上不好用<a href="#jordan-形为什么在数值上不好用"><span>#</span></a></h2><p><strong>他自己对 Jordan 形是什么态度？</strong> 他说得很直白：二十年前上 18.06，这是课程的高潮；现在不是了，因为它“依赖特征值恰好相同”，你得精确知道特征值，还得精确知道秩，数字稍微变一点，特征值变了、秩变了，整个结构就变了。他最后说，期末考试大概不会让你算一个四重特征值的 Jordan 形，他自己不太喜欢 Jordan 形，对正定矩阵和下一讲的 SVD 倒是很积极。</p><p>我想把他这个说法量化。取扰动 <span><span>ε=10−10\varepsilon = 10^{-10}</span><span><span><span></span><span>ε</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>6</span><span>, </span><span>suppress</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>eps </span><span>=</span><span> </span><span>1e-10</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 2 x 2：4I 加扰动，对比 Jordan 块加扰动</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>diag </span><span>=</span><span> </span><span>4.0</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>J </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>]])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>bump </span><span>=</span><span> eps </span><span>*</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> name, X </span><span>in</span><span> [(</span><span>"4I"</span><span>, diag), (</span><span>"J"</span><span>, J)]:</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>lam </span><span>=</span><span> np.linalg.</span><span>eigvals</span><span>(X </span><span>+</span><span> bump)</span></div></div><div><div><div>14</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span> + 1e-10：特征值 </span><span>{</span><span>lam</span><span>}</span><span>，偏离 </span><span>{</span><span>abs</span><span><span>(lam </span><span>-</span><span> </span></span><span>4</span><span><span>).</span><span>max</span><span>()</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># 4 x 4：一个 4 阶 Jordan 块，扰动落在左下角</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>diag</span><span>(np.</span><span>ones</span><span>(</span><span>3</span><span>), </span><span>k</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>N[</span><span>3</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> eps</span></span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> np.linalg.</span><span>eigvals</span><span>(N)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"4 阶块 + 1e-10：|lambda| = </span><span>{</span><span>abs</span><span><span>(lam).</span><span>max</span><span>()</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"理论值 eps^(1/4) = </span><span>{</span><span><span>eps</span><span>**</span></span><span>0.25</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 秩：Jordan 结构靠“恰好为零”，扰动之后秩立刻变了</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>two_two </span><span>=</span><span> np.</span><span>diag</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>], </span><span>k</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>noisy </span><span>=</span><span> two_two </span><span>+</span><span> eps </span><span>*</span><span> np.</span><span>diag</span><span>(np.</span><span>ones</span><span>(</span><span>3</span><span>), </span><span>k</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"N^2 的秩（精确）："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(two_two </span><span>@</span><span> two_two))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>28</div></div><div><span>        </span><span>"N^2 的秩（加 1e-10 后，默认容差）："</span><span>,</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(noisy </span><span>@</span><span> noisy),</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 对 J 加 1e-10 的扰动，特征向量矩阵有多病态</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>_, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(J </span><span>+</span><span> bump)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量矩阵的条件数："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(S)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>4I + 1e-10：特征值 [4. 4.]，偏离 1.00e-10</span></div></div><div><div><div>2</div></div><div><span>J + 1e-10：特征值 [4.00001 3.99999]，偏离 1.00e-05</span></div></div><div><div><div>3</div></div><div><span>4 阶块 + 1e-10：|lambda| = 3.16e-03</span></div></div><div><div><div>4</div></div><div><span>理论值 eps^(1/4) = 3.16e-03</span></div></div><div><div><div>5</div></div><div><span>N^2 的秩（精确）： 0</span></div></div><div><div><div>6</div></div><div><span>N^2 的秩（加 1e-10 后，默认容差）： 4</span></div></div><div><div><div>7</div></div><div><span>特征向量矩阵的条件数： 1.00e+05</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个数字：</p><ul>
<li>对角化的 <span><span>4I4I</span><span><span><span></span><span>4</span><span>I</span></span></span></span> 加一个 <span><span>10−1010^{-10}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 的对称扰动，特征值偏离 <span><span>1.00×10−101.00 \times 10^{-10}</span><span><span><span></span><span>1.00</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，和扰动同量级。</li>
<li>2 × 2 的 Jordan 块加同样的扰动，偏离 <span><span>1.00×10−51.00 \times 10^{-5}</span><span><span><span></span><span>1.00</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span>，放大 <span><span>10510^5</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span></span></span></span></span></span></span></span> 倍。理由是特征多项式变成 <span><span>(4−λ)2=ε(1+ε)(4 - \lambda)^2 = \varepsilon(1 + \varepsilon)</span><span><span><span></span><span>(</span><span>4</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>ε</span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>ε</span><span>)</span></span></span></span>，特征值跑去 <span><span>4±ε4 \pm \sqrt{\varepsilon}</span><span><span><span></span><span>4</span><span></span><span>±</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>ε</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。</li>
<li>4 阶 Jordan 块的左下角放 <span><span>10−1010^{-10}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，特征值模长是 <span><span>3.16×10−33.16 \times 10^{-3}</span><span><span><span></span><span>3.16</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，正好是 <span><span>ε1/4\varepsilon^{1/4}</span><span><span><span></span><span><span>ε</span><span><span><span><span><span><span></span><span><span><span>1/4</span></span></span></span></span></span></span></span></span></span></span></span>：放大 3000 多万倍。</li>
<li>2 + 2 那个矩阵的次对角线加上 <span><span>10−1010^{-10}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 以后，<span><span>N2N^2</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的秩从精确的 0 变成 4（默认容差）。而他强调的正是“靠秩来区分 Jordan 结构”。</li>
</ul><p>另外加了扰动的 <span><span>JJ</span><span><span><span></span><span>J</span></span></span></span> 其实有两个不同的特征值，可以对角化，但特征向量矩阵的条件数是 <span><span>1.00×1051.00 \times 10^5</span><span><span><span></span><span>1.00</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span></span></span></span></span></span></span></span>：用它去做相似变换会放大误差，没什么好处。据我所知，实际的数值软件不去算 Jordan 形，而是用 Schur 分解这类稳定的三角化，不过这一点我没有在本讲里验证。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>重极点正是 Jordan 块的用武之地。状态矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 有一个二重极点 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 又缺一个特征向量时，<span><span>x˙=Ax\dot{\mathbf{x}} = A\mathbf{x}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span></span></span></span> 的解里不只有 <span><span>eλte^{\lambda t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>，还有 <span><span>t eλtt\,e^{\lambda t}</span><span><span><span></span><span>t</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>：对 <span><span>J=[λ10λ]J = \begin{bmatrix} \lambda &amp; 1 \\ 0 &amp; \lambda \end{bmatrix}</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>λ</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>λ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，有 <span><span>eJt=eλt[1t01]e^{Jt} = e^{\lambda t}\begin{bmatrix} 1 &amp; t \\ 0 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>J</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>t</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。我用 Taylor 级数验证了 <span><span>λ=−1\lambda = -1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span> 的情形：<span><span>t=1t = 1</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时右上角是 0.3679（即 <span><span>1/e1/e</span><span><span><span></span><span>1/</span><span>e</span></span></span></span>，也是 <span><span>te−tte^{-t}</span><span><span><span></span><span>t</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 的峰值），<span><span>t=10t = 10</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时降到 <span><span>4.5×10−44.5 \times 10^{-4}</span><span><span><span></span><span>4.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>。这个响应先升后降，稳定，但不是单纯的指数衰减。据我所知，临界阻尼系统的响应里就有这样的 <span><span>t e−tt\,e^{-t}</span><span><span><span></span><span>t</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 项。</p><p>模态坐标是一个相似变换：取 <span><span>z=S−1x\mathbf{z} = S^{-1}\mathbf{x}</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>x</span></span></span></span>，<span><span>z˙=Λz\dot{\mathbf{z}} = \Lambda\mathbf{z}</span><span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Λ</span><span>z</span></span></span></span>，每个模态各走各的，<span><span>zi(t)=eλitzi(0)z_i(t) = e^{\lambda_i t}z_i(0)</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>0</span><span>)</span></span></span></span>。脚本里 <span><span>A=[−120−3]A = \begin{bmatrix} -1 &amp; 2 \\ 0 &amp; -3 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的两个极点是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 和 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span>，模态初值是 <span><span>(2,1.4142)(2, 1.4142)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1.4142</span><span>)</span></span></span></span>，用模态坐标算出的 <span><span>x(2)\mathbf{x}(2)</span><span><span><span></span><span>x</span><span>(</span><span>2</span><span>)</span></span></span></span> 与直接用级数算出的一致。但是 Jordan 块的 <code>eig</code> 给出的特征向量矩阵秩为 1，没有 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，模态坐标这条路走不通，只能退到 Jordan 形，保留那个 1 带来的 <span><span>t eλtt\,e^{\lambda t}</span><span><span><span></span><span>t</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 项。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>taylor_exp</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>terms</span></span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>60</span></span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""用 Taylor 级数求 exp(A)。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>result </span><span>=</span><span> np.</span><span>eye</span><span>(A.shape[</span><span>0</span><span>])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>term </span><span>=</span><span> np.</span><span>eye</span><span>(A.shape[</span><span>0</span><span>])</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, terms):</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>term </span><span>=</span><span> term </span><span>@</span><span> A </span><span>/</span><span> k</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>result </span><span>=</span><span> result </span><span>+</span><span> term</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> result</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 重极点 -1：x' = J x，J 是 2 x 2 的 Jordan 块</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> </span><span>-</span><span>1.0</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>J </span><span>=</span><span> np.</span><span>array</span><span>([[lam, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, lam]])</span></div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> [</span><span>1.0</span><span>, </span><span>5.0</span><span>, </span><span>10.0</span><span>]:</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>E </span><span>=</span><span> </span><span>taylor_exp</span><span>(J </span><span>*</span><span> t)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>closed </span><span>=</span><span> np.</span><span>exp</span><span>(lam </span><span>*</span><span> t) </span><span>*</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, t], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>23</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>24</div></div><div><span>            </span><span>f</span><span>"t = </span><span>{</span><span>t</span><span>:&gt;4</span><span>}</span><span>：Taylor 与闭式一致 </span><span>{</span><span><span>np.</span><span>allclose</span><span>(E, closed)</span></span><span>}</span><span>，"</span></div></div><div><div><div>25</div></div><div><span>            </span><span>f</span><span>"x2 的响应 t e^(lt) = </span><span>{</span><span>E[</span><span>0</span><span>, </span><span>1</span><span>]</span><span>:.4e</span><span>}</span><span>"</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 模态坐标：对角化后 z' = Lambda z，每个模态各走各的</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>]])</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>eigvals, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>x0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>z0 </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(S, x0)</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> </span><span>2.0</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>x_direct </span><span>=</span><span> </span><span>taylor_exp</span><span>(A </span><span>*</span><span> t) </span><span>@</span><span> x0</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>x_modal </span><span>=</span><span> S </span><span>@</span><span> (np.</span><span>exp</span><span>(eigvals </span><span>*</span><span> t) </span><span>*</span><span> z0)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"极点："</span><span>, eigvals, </span><span>"，模态初值 z0 ="</span><span>, z0)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"直接算与模态坐标一致："</span><span><span>, np.</span><span>allclose</span><span>(x_direct, x_modal))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># Jordan 块没有足够的特征向量：S 不可逆，模态坐标这条路走不通</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>_, </span><span>SJ</span><span> </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(J)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"Jordan 块的特征向量矩阵秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(</span><span>SJ</span><span>))</span></span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>t =  1.0：Taylor 与闭式一致 True，x2 的响应 t e^(lt) = 3.6788e-01</span></div></div><div><div><div>2</div></div><div><span>t =  5.0：Taylor 与闭式一致 True，x2 的响应 t e^(lt) = 3.3690e-02</span></div></div><div><div><div>3</div></div><div><span>t = 10.0：Taylor 与闭式一致 True，x2 的响应 t e^(lt) = 4.5400e-04</span></div></div><div><div><div>4</div></div><div><span>极点： [-1. -3.] ，模态初值 z0 = [2.     1.4142]</span></div></div><div><div><div>5</div></div><div><span>直接算与模态坐标一致： True</span></div></div><div><div><div>6</div></div><div><span>Jordan 块的特征向量矩阵秩： 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上一节的结论在这里换个说法：真实系统的极点不会恰好重合，参数一变，二重极点就裂成两个靠得很近的单极点，Jordan 块变成一个病态的对角化。工程上除非有意要临界阻尼，多半会避免把极点放到完全相同的位置（我猜的，这不是本讲的内容）。</p><p>下一讲是奇异值分解，Strang 说它十年前还不在课里，现在不讲不行了：任何矩阵（不必是方阵）都能写成 <span><span>A=UΣVTA = U\Sigma V^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，而且数值上很稳。接着看 <a href="/posts/linear-algebra-29-svd/">18.06 第 29 讲：奇异值分解</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-27-positive-definite-minima/</id>
      <title type="text">18.06 第 27 讲：正定矩阵与极小值</title>
      <published>2026-10-18T09:00:00.000Z</published>
      <updated>2026-10-18T09:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-27-positive-definite-minima/"/>
      <summary type="text">MIT 18.06 第 27 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 Strang 的 [[2, 6], [6, c]] 换了三个 c，逐一验证正定的四种判据，c = 20 时单位向量上 xᵀAx 的最小值是 0.1833，换成 c = 7 就掉到 -2.0000。</summary>
      <content type="html"><![CDATA[<p>Strang 说这一讲会把整门课串在一起：主元、行列式、特征值，再加一个新东西 <span><span>xTAx\mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span>，“这才是这一讲要盯着的那个人”。我拿他黑板上的 <span><span>[266c]\begin{bmatrix} 2 &amp; 6 \\ 6 &amp; c \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 做实验：<span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 的时候，在 10 万个随机单位向量上 <span><span>xTAx\mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 的最小值是 0.1833，恰好等于最小特征值；把 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 改成 7，最小值变成 <span><span>−2.0000-2.0000</span><span><span><span></span><span>−</span><span>2.0000</span></span></span></span>，也恰好是最小特征值（负的）。四种判据说的是同一件事，而这件事就是“向任何方向走，函数都往上翘”。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=vF7eyJ2g3kU" target="_blank">Lecture 27: Positive definite matrices and minima</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-27-positive-definite-matrices-and-minima/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §6.3 Symmetric Positive Definite Matrices；§9.1 Minimizing a Multivariable Function
前置：<a href="/posts/linear-algebra-26-complex-matrices-fft/">第 26 讲：复矩阵与快速 Fourier 变换</a>
一句话：对称矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 正定，等价于特征值全正、顺序主子式全正、主元全正、<span><span>xTAx&gt;0\mathbf{x}^TA\mathbf{x} &gt; 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 对所有 <span><span>x≠0\mathbf{x} \neq \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> 成立；二阶导数矩阵正定，就是多元函数的极小值条件。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先列目标：怎么判断、为什么在乎、最后有几何。这一小段听一遍就够了，后面每一步我都在代码里对应上了。</li>
<li>他口算 <span><span>[266c]\begin{bmatrix} 2 &amp; 6 \\ 6 &amp; c \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，一路问“<span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 要多大”，从 18 的临界情形讲到 7 的鞍点。这一段是整讲的核心，要认真看，本篇第一节只是重算了一遍。</li>
<li>他用“配方”解释为什么主元是括号外面的系数，这是最漂亮的一步，值得停下来自己配一次方。</li>
<li>讲 <span><span>fxxf_{xx}</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>xx</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>fyyf_{yy}</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>y</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>fxyf_{xy}</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>x</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的那段和最后的椭球、橄榄球可以倍速，我在第三、四节用代码算过了。他在最后一句留的主轴定理，就是第 25 讲的谱分解 <span><span>A=QΛQTA = Q\Lambda Q^T</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。</li>
</ul></div></div>
<section><h2>四种判据，同一个 2 × 2<a href="#四种判据同一个-2--2"><span>#</span></a></h2><p>Strang 先只考虑对称矩阵。他给出的前三个判据都是“不用看 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 就能判”的：</p><ul>
<li>特征值：全部大于 0。</li>
<li>行列式：左上角 <span><span>1×11 \times 1</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span>、……的顺序主子式（他叫它们“西北角”的子矩阵）全部大于 0。</li>
<li>主元：消元之后的主元全部大于 0。</li>
</ul><p>第四个才是他最想强调的：对任意非零 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>，<span><span>xTAx&gt;0\mathbf{x}^TA\mathbf{x} &gt; 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>。他说多数教材拿它当定义，前面三条当检验。</p><p><strong>那个 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 到底要多大？</strong> 他取 <span><span>A=[266c]A = \begin{bmatrix} 2 &amp; 6 \\ 6 &amp; c \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，问 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 至少要多大才正定。第一个主元是 2，没问题；行列式是 <span><span>2c−362c - 36</span><span><span><span></span><span>2</span><span>c</span><span></span><span>−</span><span></span></span><span><span></span><span>36</span></span></span></span>，所以 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 必须大于 18。他还专门拿 18 本身做了一次示范：此时矩阵奇异，只有一个主元，特征值是 0 和 20（迹是 20），这叫半正定。他再换成 <span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span>，说这个矩阵“肯定不正定”：行列式是 <span><span>14−36=−2214 - 36 = -22</span><span><span><span></span><span>14</span><span></span><span>−</span><span></span></span><span><span></span><span>36</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>22</span></span></span></span>，主元是 2 和 <span><span>−11-11</span><span><span><span></span><span>−</span><span>11</span></span></span></span>。</p><p>下面的脚本对 <span><span>c=7,18,20c = 7, 18, 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span><span>,</span><span></span><span>18</span><span>,</span><span></span><span>20</span></span></span></span> 各算一遍特征值、顺序主子式、主元，并且在 10 万个随机单位向量上看 <span><span>xTAx\mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 的最小值。最后一行是他的反例：<span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span> 时取 <span><span>x=(1,−1)\mathbf{x} = (1, -1)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>pivots_no_exchange</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""不换行的消元，返回主元。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> U.shape[</span><span>0</span><span>]</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>9</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(j </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> U[i, j] </span><span>/</span><span> U[j, j] </span><span>*</span><span> U[j]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>diag</span><span>(U)</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>100000</span><span>))</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>X </span><span>/=</span><span> np.linalg.</span><span>norm</span><span>(X, </span><span>axis</span><span>=</span><span>0</span><span>)  </span><span># 单位向量，只看方向</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> c </span><span>in</span><span> (</span><span>7.0</span><span>, </span><span>18.0</span><span>, </span><span>20.0</span><span>):</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, c]])</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>eigs </span><span>=</span><span> np.linalg.</span><span>eigvalsh</span><span>(A)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>dets </span><span>=</span><span> [A[</span><span>0</span><span>, </span><span>0</span><span><span>], np.linalg.</span><span>det</span><span>(A)]</span></span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>pivots </span><span>=</span><span> </span><span>pivots_no_exchange</span><span>(A)</span></div></div><div><div><div>25</div></div><div><span>        </span><span># 对 10 万个随机单位向量，看 x^T A x 的最小值</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>f </span><span>=</span><span> np.</span><span>einsum</span><span>(</span><span>"ik,ij,jk-&gt;k"</span><span>, X, A, X)</span></div></div><div><div><div>27</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"c = </span><span>{</span><span>c</span><span>:.0f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div><span>        </span><span>print</span><span>(</span><span>"  特征值："</span><span>, eigs)</span></div></div><div><div><div>29</div></div><div><span>        </span><span>print</span><span>(</span><span>"  顺序主子式："</span><span><span>, np.</span><span>array</span><span>(dets))</span></span></div></div><div><div><div>30</div></div><div><span>        </span><span>print</span><span>(</span><span>"  主元："</span><span>, pivots)</span></div></div><div><div><div>31</div></div><div><span>        </span><span>print</span><span>(</span><span>"  单位向量上 x^T A x 的最小值："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>f.</span><span>min</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># Strang 的反例：c = 7 时取 x = (1, -1)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, </span><span>7.0</span><span>]])</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"c = 7，x = (1, -1)：x^T A x ="</span><span><span>, x </span><span>@</span><span> A </span><span>@</span><span> x)</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>c = 7</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>特征值： [-2. 11.]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>顺序主子式： [  2. -22.]</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>主元： [  2. -11.]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>单位向量上 x^T A x 的最小值： -2.0000</span></div></div><div><div><div>6</div></div><div><span>c = 18</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>特征值： [ 0. 20.]</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>顺序主子式： [2. 0.]</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>主元： [2. 0.]</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>单位向量上 x^T A x 的最小值： 0.0000</span></div></div><div><div><div>11</div></div><div><span>c = 20</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>特征值： [ 0.1833 21.8167]</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>顺序主子式： [2. 4.]</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>主元： [2. 2.]</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>单位向量上 x^T A x 的最小值： 0.1833</span></div></div><div><div><div>16</div></div><div><span>c = 7，x = (1, -1)：x^T A x = -3.0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三种 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 的输出排在一起看，规律很整齐。</p><ul>
<li><span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span>：特征值 0.1833 和 21.8167 都是正的，顺序主子式是 2 和 4，主元是 2 和 2，随机单位向量上的最小值是 0.1833。</li>
<li><span><span>c=18c = 18</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>18</span></span></span></span>：特征值是 0 和 20，行列式是 0，第二个主元是 0，最小值是 0.0000：向某个方向走，函数一路平着没有上翘，这就是半正定。</li>
<li><span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span>：特征值 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 和 11，行列式 <span><span>−22-22</span><span><span><span></span><span>−</span><span>22</span></span></span></span>，主元 2 和 <span><span>−11-11</span><span><span><span></span><span>−</span><span>11</span></span></span></span>（和他在黑板上说的完全一致），最小值 <span><span>−2.0000-2.0000</span><span><span><span></span><span>−</span><span>2.0000</span></span></span></span>。他取的 <span><span>(1,−1)(1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 给出 <span><span>2−12+7=−32 - 12 + 7 = -3</span><span><span><span></span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span>12</span><span></span><span>+</span><span></span></span><span><span></span><span>7</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3</span></span></span></span>，输出里是 <span><span>−3.0-3.0</span><span><span><span></span><span>−</span><span>3.0</span></span></span></span>。</li>
</ul><p>最小值那一行是我顺手加的，不是他讲的。我先用的是不归一化的随机向量，结果 <span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 也给出一个贴着 0 的最小值，因为向量本身可以任意短。归一化之后，最小值就是最小特征值，这是 Rayleigh 商的结论（我是在代码里看到它的，视频里没有提）。</p><p>他还有一个口算特征值符号的技巧：<span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 的时候，行列式是 <span><span>40−36=440 - 36 = 4</span><span><span><span></span><span>40</span><span></span><span>−</span><span></span></span><span><span></span><span>36</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>，迹是 22。两个特征值乘起来是 4，加起来是 22，所以不可能一正一负，也不可能都负（都负的话迹就是负的），只能都正。输出里的 0.1833 和 21.8167，乘积是 4，和是 22，对得上。（我一直觉得这类“不用算特征值就知道符号”的技巧很像考试专用，但它确实比解二次方程快。）</p></section>
<section><h2>配方就是消元<a href="#配方就是消元"><span>#</span></a></h2><p><strong>第四个判据怎么检验？</strong> 光靠试向量是不行的，试多少个都不算数。Strang 的办法是写成平方和。<span><span>xTAx\mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 对 <span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 展开是</p><span><span><span>2x2+12xy+20y22x^2 + 12xy + 20y^2</span><span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>12</span><span>x</span><span>y</span><span></span><span>+</span><span></span></span><span><span></span><span>20</span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span></span><p>所有麻烦都来自交叉项 <span><span>12xy12xy</span><span><span><span></span><span>12</span><span>x</span><span>y</span></span></span></span>。他让大家配方：先写 <span><span>2(x+3y)22(x + 3y)^2</span><span><span><span></span><span>2</span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>y</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，展开后 <span><span>x2x^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 项和 <span><span>xyxy</span><span><span><span></span><span>x</span><span>y</span></span></span></span> 项都对了，但 <span><span>y2y^2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 项是 18，比 20 少 2，补上 <span><span>2y22y^2</span><span><span><span></span><span>2</span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>2x2+12xy+20y2=2(x+3y)2+2y22x^2 + 12xy + 20y^2 = 2(x + 3y)^2 + 2y^2</span><span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>12</span><span>x</span><span>y</span><span></span><span>+</span><span></span></span><span><span></span><span>20</span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>y</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span></span><p>两个平方加起来，不可能是负的。如果 <span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span>，剩下的就是 <span><span>7−18=−117 - 18 = -11</span><span><span><span></span><span>7</span><span></span><span>−</span><span></span></span><span><span></span><span>18</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>11</span></span></span></span>，是 <span><span>−11y2-11y^2</span><span><span><span></span><span>−</span><span>11</span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，取 <span><span>x=−3yx = -3y</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3</span><span>y</span></span></span></span> 让第一个平方为 0，函数就是负的。<span><span>c=18c = 18</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>18</span></span></span></span> 时补不上任何东西，正是临界。</p><p>然后他指出这些数字不是巧合：括号外面的 2 和 2 是主元，括号里的 3 是消元的乘数。对 <span><span>[26620]\begin{bmatrix} 2 &amp; 6 \\ 6 &amp; 20 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>20</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 做消元，从第二行减去 3 倍第一行，得到 <span><span>U=[2602]U = \begin{bmatrix} 2 &amp; 6 \\ 0 &amp; 2 \end{bmatrix}</span><span><span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，<span><span>L=[1031]L = \begin{bmatrix} 1 &amp; 0 \\ 3 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。所以配方就是消元，主元为正就是平方和，平方和就是函数为正。</p><p>我用一个脚本把这件事验了一遍，顺便做了他最后一个例子的 3 × 3 版本。他说“用我最喜欢的那个矩阵”，也就是对角线为 2、两侧为 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 的三对角矩阵。他口算的结果是：行列式 2、3、4，主元 <span><span>2,32,432, \frac32, \frac43</span><span><span><span></span><span>2</span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，特征值猜是 <span><span>2−2,2,2+22 - \sqrt2, 2, 2 + \sqrt2</span><span><span><span></span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>（他用迹 6 和行列式 4 来核对）。脚本里还加了 <code>np.linalg.cholesky</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>ldu</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; tuple[np.ndarray, np.ndarray]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""不换行的消元，返回单位下三角 L 和主元向量 d。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> U.shape[</span><span>0</span><span>]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(j </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>L[i, j] </span><span>=</span><span> U[i, j] </span><span>/</span><span> U[j, j]</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> L[i, j] </span><span>*</span><span> U[j]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> L, np.</span><span>diag</span><span>(U)</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 2 x 2：配方就是消元，括号外是主元，括号里是乘数</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, </span><span>20.0</span><span>]])</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>L, d </span><span>=</span><span> </span><span>ldu</span><span>(A)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"乘数 l21 ="</span><span>, L[</span><span>1</span><span>, </span><span>0</span><span>], </span><span>"，主元 ="</span><span>, d)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>x, y </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> np.</span><span>array</span><span>([x, y]) </span><span>@</span><span> A </span><span>@</span><span> np.</span><span>array</span><span>([x, y])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>square </span><span>=</span><span> d[</span><span>0</span><span><span>] </span><span>*</span><span> (x </span><span>+</span><span> L[</span></span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>*</span><span> y) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>+</span><span> d[</span></span><span>1</span><span><span>] </span><span>*</span><span> y</span><span>**</span></span><span>2</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"2(x + 3y)^2 + 2y^2 与 x^T A x 相等："</span><span><span>, np.</span><span>isclose</span><span>(f, square))</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 3 x 3：Strang 的“好矩阵”</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>L, d </span><span>=</span><span> </span><span>ldu</span><span>(B)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"主元："</span><span>, d, </span><span>"，即 2, 3/2, 4/3："</span><span><span>, np.</span><span>allclose</span><span>(d, [</span></span><span>2</span><span>, </span><span>1.5</span><span>, </span><span>4</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>]))</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(B))</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>35</div></div><div><span>        </span><span>"2 - sqrt(2), 2, 2 + sqrt(2)："</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>np.</span><span>array</span><span>([</span><span>2</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span>**</span><span>0.5</span><span>, </span><span>2.0</span><span>, </span><span>2</span><span><span> </span><span>+</span><span> </span></span><span>2</span><span>**</span><span>0.5</span><span>]),</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹 ="</span><span><span>, np.</span><span>trace</span><span>(B), </span></span><span>"，det ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(B), </span></span><span>4</span><span>))</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span># Cholesky：B = C C^T，C = L sqrt(D)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.linalg.</span><span>cholesky</span><span>(B)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"cholesky(B) ="</span><span>)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(C)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>45</div></div><div><span>        </span><span>"L sqrt(D) 与 cholesky 相同："</span><span><span>, np.</span><span>allclose</span><span>(C, L </span><span>@</span><span> np.</span><span>diag</span><span>(np.</span><span>sqrt</span><span>(d)))</span></span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"C @ C.T 等于 B："</span><span><span>, np.</span><span>allclose</span><span>(C </span><span>@</span><span> C.T, B))</span></span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span># 不正定的矩阵，cholesky 直接报错</span></div></div><div><div><div>50</div></div><div><span>    </span><span>for</span><span> c </span><span>in</span><span> (</span><span>18.0</span><span>, </span><span>7.0</span><span>):</span></div></div><div><div><div>51</div></div><div><span>        </span><span>try</span><span>:</span></div></div><div><div><div>52</div></div><div><span><span>            </span></span><span>C2 </span><span>=</span><span> np.linalg.</span><span>cholesky</span><span>(np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, c]]))</span></div></div><div><div><div>53</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"c = </span><span>{</span><span>c</span><span>:.0f</span><span>}</span><span>：分解成功，C[1, 1] = </span><span>{</span><span>C2[</span><span>1</span><span>, </span><span>1</span><span>]</span><span>:.3e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>54</div></div><div><span>        </span><span>except</span><span> np.linalg.LinAlgError </span><span>as</span><span> err:</span></div></div><div><div><div>55</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"c = </span><span>{</span><span>c</span><span>:.0f</span><span>}</span><span>：</span><span>{</span><span>err</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>乘数 l21 = 3.0 ，主元 = [2. 2.]</span></div></div><div><div><div>2</div></div><div><span>2(x + 3y)^2 + 2y^2 与 x^T A x 相等： True</span></div></div><div><div><div>3</div></div><div><span>主元： [2.     1.5    1.3333] ，即 2, 3/2, 4/3： True</span></div></div><div><div><div>4</div></div><div><span>特征值： [0.5858 2.     3.4142]</span></div></div><div><div><div>5</div></div><div><span>2 - sqrt(2), 2, 2 + sqrt(2)： [0.5858 2.     3.4142]</span></div></div><div><div><div>6</div></div><div><span>迹 = 6.0 ，det = 4.0</span></div></div><div><div><div>7</div></div><div><span>cholesky(B) =</span></div></div><div><div><div>8</div></div><div><span>[[ 1.4142  0.      0.    ]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[-0.7071  1.2247  0.    ]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 0.     -0.8165  1.1547]]</span></div></div><div><div><div>11</div></div><div><span>L sqrt(D) 与 cholesky 相同： True</span></div></div><div><div><div>12</div></div><div><span>C @ C.T 等于 B： True</span></div></div><div><div><div>13</div></div><div><span>c = 18：分解成功，C[1, 1] = 5.960e-08</span></div></div><div><div><div>14</div></div><div><span>c = 7：Matrix is not positive definite</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前几行就是他口算的所有数字：乘数 3、主元 2 和 2、主元 <span><span>2,1.5,1.33332, 1.5, 1.3333</span><span><span><span></span><span>2</span><span>,</span><span></span><span>1.5</span><span>,</span><span></span><span>1.3333</span></span></span></span>、特征值 0.5858、2、3.4142，迹 6、行列式 4，都对上了。</p><p>Cholesky 分解 <span><span>A=CCTA = CC^T</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是视频里没有出现的东西，我加它是因为它正好是上面这件事的矩阵形式：主元开平方，<span><span>C=LDC = L\sqrt{D}</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span><span><span><span><span><span></span><span><span>D</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。输出里“<code>L sqrt(D) 与 cholesky 相同：True</code>”就是这个意思。分解没有主元为负的余地（负数没法开方），所以 <code>np.linalg.cholesky</code> 也成了我手边最顺手的“是不是正定”的检验，比求特征值便宜。</p><p><strong>但临界情形有个坑。</strong> <span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span> 的时候，<code>cholesky</code> 抛出 <code>Matrix is not positive definite</code>，符合预期。<span><span>c=18c = 18</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>18</span></span></span></span> 的时候，真实的第二个主元是 0，分解本该失败，它却“成功”了，算出 <span><span>C22=5.960×10−8C_{22} = 5.960 \times 10^{-8}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5.960</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>。我猜是舍入：<span><span>6/26/\sqrt2</span><span><span><span></span><span>6/</span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 的平方和 18 差了一个浮点误差，让本该是 0 的主元变成了 <span><span>3.6×10−153.6 \times 10^{-15}</span><span><span><span></span><span>3.6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，开方后就是这个 <span><span>10−810^{-8}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span> 量级的数。结论是：浮点数上判断半正定和正定之间的边界，本来就靠不住，要么留容差，要么看主元的大小，不要看“成不成功”。</p></section>
<section><h2>碗、鞍与椭圆<a href="#碗鞍与椭圆"><span>#</span></a></h2><p><strong><span><span>xTAx\mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 画出来是什么样？</strong> Strang 先把乘法做开：<span><span>xTAx=ax2+2bxy+cy2\mathbf{x}^TA\mathbf{x} = ax^2 + 2bxy + cy^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>b</span><span>x</span><span>y</span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。它不是线性的（<span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 才是），而是纯二次的：没有一次项，没有常数项，是“二次型”。</p><p>函数 <span><span>f(x,y)=xTAxf(x, y) = \mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>,</span><span></span><span>y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 的图像在原点过零点，所以原点是它的“可疑点”。<span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 的时候，沿每个方向都往上，图像是一个碗，原点是最低点。<span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span> 的时候沿 <span><span>x=−yx = -y</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>y</span></span></span></span> 往下，沿 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 轴和 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 轴往上，原点是鞍点。他承认自己画不好鞍，只好把“马鞍”这个词念出来。（图上他画的那条抛物线，他自己都说是抛物线，我就信了。）</p><p>把碗在高度 1 处切开，<span><span>xTAx=1\mathbf{x}^TA\mathbf{x} = 1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，得到一个椭圆；把鞍在 1 处切开，得到双曲线。这就是他说的“椭圆和正定有关，双曲线没有”。</p><p></p><figure><img src="/assets/images/linear-algebra/l27-ellipse-saddle.svg" alt="左：c = 20 时 xᵀAx = 1 是一个椭圆，两条主轴沿特征向量。右：c = 7 时的等高线是双曲线，红色是负的水平，蓝色是正的" /><figcaption>左：c = 20 时 xᵀAx = 1 是一个椭圆，两条主轴沿特征向量。右：c = 7 时的等高线是双曲线，红色是负的水平，蓝色是正的</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>OUT</span><span> </span><span>=</span><span> </span><span>"l27-ellipse-saddle.svg"</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>fig, (ax1, ax2) </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, </span><span>20.0</span><span>]])</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>lam, Q </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>half </span><span>=</span><span> </span><span>1</span><span><span> </span><span>/</span><span> np.</span><span>sqrt</span><span>(lam)</span></span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0</span><span>, </span><span>2</span><span><span> </span><span>*</span><span> np.pi, </span></span><span>400</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>circle </span><span>=</span><span> np.</span><span>vstack</span><span>([np.</span><span>cos</span><span>(t), np.</span><span>sin</span><span>(t)])</span></div></div><div><div><div>18</div></div><div><span>    </span><span># x = Q diag(1/sqrt(lam)) u，u 在单位圆上</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>pts </span><span>=</span><span> Q </span><span>@</span><span> np.</span><span>diag</span><span>(half) </span><span>@</span><span> circle</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(pts[</span><span>0</span><span>], pts[</span><span>1</span><span>], </span><span>color</span><span>=</span><span>"C0"</span><span>, </span><span>lw</span><span>=</span><span>2</span><span>, </span><span>label</span><span>=</span><span>r</span><span>"$x^TAx=1$"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>):</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>tip </span><span>=</span><span> half[k] </span><span>*</span><span> Q[:, k]</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>ax1.</span><span>plot</span><span>([</span><span>-</span><span>tip[</span><span>0</span><span>], tip[</span><span>0</span><span><span>]], [</span><span>-</span><span>tip[</span></span><span>1</span><span>], tip[</span><span>1</span><span>]], </span><span>"--"</span><span>, </span><span>color</span><span>=</span><span>"C3"</span><span>, </span><span>lw</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>ax1.</span><span>plot</span><span>(*tip, </span><span>"o"</span><span>, </span><span>color</span><span>=</span><span>"C3"</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span></div></div><div><div><div>26</div></div><div><span>        </span><span>r</span><span><span>"$1/\sqrt{</span><span>\l</span><span>ambda_1}=2.3354$"</span></span><span>,</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>(</span><span>-</span><span>2.2352</span><span>, </span><span>0.6768</span><span>),</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>(</span><span>-</span><span>2.2</span><span>, </span><span>1.3</span><span>),</span></div></div><div><div><div>29</div></div><div><span>        </span><span>fontsize</span><span>=</span><span>9</span><span>,</span></div></div><div><div><div>30</div></div><div><span>        </span><span>arrowprops</span><span><span>=</span><span>{</span></span><span>"arrowstyle"</span><span>: </span><span>"-"</span><span>, </span><span>"color"</span><span>: </span><span>"gray"</span><span>},</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span></div></div><div><div><div>33</div></div><div><span>        </span><span>r</span><span><span>"$1/\sqrt{</span><span>\l</span><span>ambda_2}=0.2141$"</span></span><span>,</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>(</span><span>0.062</span><span>, </span><span>0.2049</span><span>),</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>(</span><span>0.6</span><span>, </span><span>0.9</span><span>),</span></div></div><div><div><div>36</div></div><div><span>        </span><span>fontsize</span><span>=</span><span>9</span><span>,</span></div></div><div><div><div>37</div></div><div><span>        </span><span>arrowprops</span><span><span>=</span><span>{</span></span><span>"arrowstyle"</span><span>: </span><span>"-"</span><span>, </span><span>"color"</span><span>: </span><span>"gray"</span><span>},</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlim</span><span>(</span><span>-</span><span>2.8</span><span>, </span><span>2.8</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylim</span><span>(</span><span>-</span><span>1.8</span><span>, </span><span>1.8</span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>ax1.</span><span>axhline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"gray"</span><span>, </span><span>lw</span><span>=</span><span>0.5</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>ax1.</span><span>axvline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"gray"</span><span>, </span><span>lw</span><span>=</span><span>0.5</span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_title</span><span>(</span><span>r</span><span>"$c=20$: ellipse, axes along eigenvectors"</span><span>)</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>ax1.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"lower right"</span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, </span><span>7.0</span><span>]])</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>-</span><span>2</span><span>, </span><span>2</span><span>, </span><span>400</span><span>)</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>X, Y </span><span>=</span><span> np.</span><span>meshgrid</span><span>(g, g)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>F </span><span>=</span><span> S[</span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>*</span><span> X</span><span>**</span></span><span>2</span><span><span> </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> S[</span></span><span>0</span><span>, </span><span>1</span><span><span>] </span><span>*</span><span> X </span><span>*</span><span> Y </span><span>+</span><span> S[</span></span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>*</span><span> Y</span><span>**</span></span><span>2</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>cs </span><span>=</span><span> ax2.</span><span>contour</span><span>(</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>X, Y, F, </span><span>levels</span><span><span>=</span><span>[</span><span>-</span></span><span>3</span><span><span>, </span><span>-</span></span><span>1</span><span>, </span><span>1</span><span>, </span><span>3</span><span>], </span><span>colors</span><span><span>=</span><span>[</span></span><span>"C3"</span><span>, </span><span>"C3"</span><span>, </span><span>"C0"</span><span>, </span><span>"C0"</span><span>]</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>ax2.</span><span>clabel</span><span>(cs, </span><span>fmt</span><span>=</span><span>"</span><span>%d</span><span>"</span><span>, </span><span>fontsize</span><span>=</span><span>8</span><span>)</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>([</span><span>1</span><span><span>], [</span><span>-</span></span><span>1</span><span>], </span><span>"ko"</span><span>)</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>ax2.</span><span>annotate</span><span>(</span></div></div><div><div><div>57</div></div><div><span>        </span><span>r</span><span><span>"$</span><span>(</span><span>1,-1</span><span>)</span><span>$: $f=-3$"</span></span><span>,</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>(</span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span>),</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>(</span><span>-</span><span>1.9</span><span><span>, </span><span>-</span></span><span>1.6</span><span>),</span></div></div><div><div><div>60</div></div><div><span>        </span><span>fontsize</span><span>=</span><span>9</span><span>,</span></div></div><div><div><div>61</div></div><div><span>        </span><span>arrowprops</span><span><span>=</span><span>{</span></span><span>"arrowstyle"</span><span>: </span><span>"-"</span><span>, </span><span>"color"</span><span>: </span><span>"gray"</span><span>},</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>ax2.</span><span>axhline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"gray"</span><span>, </span><span>lw</span><span>=</span><span>0.5</span><span>)</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>ax2.</span><span>axvline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"gray"</span><span>, </span><span>lw</span><span>=</span><span>0.5</span><span>)</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_title</span><span>(</span><span>r</span><span>"$c=7$: saddle, level sets are hyperbolas"</span><span>)</span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span><span>OUT</span><span>, </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>})</span></div></div><div><div><div>70</div></div><div>
</div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>73</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>椭圆的形状由特征分解决定。他在最后说，把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 写成 <span><span>QΛQTQ\Lambda Q^T</span><span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（主轴定理），特征向量是主轴方向，特征值决定轴的长度。他当时对“长度和特征值的关系”含糊其辞（“长度，或者半长度，或者特征值的倒数”）。我自己推一下：沿特征向量 <span><span>qk\mathbf{q}_k</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 走 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span>，<span><span>xTAx=λkt2\mathbf{x}^TA\mathbf{x} = \lambda_k t^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，等于 1 的时候 <span><span>t=1/λkt = 1/\sqrt{\lambda_k}</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span><span><span><span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，所以半轴长是 <span><span>1/λk1/\sqrt{\lambda_k}</span><span><span><span></span><span>1/</span><span><span><span><span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，特征值越大轴越短。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 椭圆 x^T A x = 1：A = Q Lambda Q^T，特征向量是主轴方向</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, </span><span>20.0</span><span>]])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>lam, Q </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, lam)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量（列）："</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(Q)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 等于 Q Lambda Q^T："</span><span><span>, np.</span><span>allclose</span><span>(A, Q </span><span>@</span><span> np.</span><span>diag</span><span>(lam) </span><span>@</span><span> Q.T))</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>half </span><span>=</span><span> </span><span>1</span><span><span> </span><span>/</span><span> np.</span><span>sqrt</span><span>(lam)</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"半轴长 1/sqrt(lambda)："</span><span>, half)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>):</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>tip </span><span>=</span><span> half[k] </span><span>*</span><span> Q[:, k]</span></div></div><div><div><div>20</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"  主轴 </span><span>{</span><span><span>k </span><span>+</span><span> </span></span><span>1}</span><span> 的端点 </span><span>{</span><span>tip</span><span>}</span><span>，x^T A x = </span><span>{</span><span><span>tip </span><span>@</span><span> A </span><span>@</span><span> tip</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 任取一个方向，缩放到椭圆上</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> d </span><span>/</span><span> np.</span><span>sqrt</span><span>(d </span><span>@</span><span> A </span><span>@</span><span> d)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"随机方向缩放后：x^T A x = </span><span>{</span><span><span>p </span><span>@</span><span> A </span><span>@</span><span> p</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"长半轴 / 短半轴 = </span><span>{</span><span>half[</span><span>0</span><span><span>] </span><span>/</span><span> half[</span></span><span>1</span><span>]</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"sqrt(特征值之比) = </span><span>{</span><span><span>np.</span><span>sqrt</span><span>(lam[</span></span><span>1</span><span><span>] </span><span>/</span><span> lam[</span></span><span>0</span><span>])</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 鞍点：c = 7 时沿两个特征向量走同样的距离</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, </span><span>7.0</span><span>]])</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>lam_s, Q_s </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(S)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>):</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>f </span><span>=</span><span> (Q_s[:, k] </span><span>@</span><span> S </span><span>@</span><span> Q_s[:, k]) </span><span>*</span><span> </span><span>0.5</span><span>**</span><span>2</span></div></div><div><div><div>34</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"鞍点矩阵：沿特征向量 </span><span>{</span><span><span>k </span><span>+</span><span> </span></span><span>1}</span><span> 走 0.5，f = </span><span>{</span><span>f</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 3 x 3 的“橄榄球”：三个半轴，各自对应一个特征值</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>lam_b, Q_b </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(B)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"3 x 3 半轴长："</span><span>, </span><span>1</span><span><span> </span><span>/</span><span> np.</span><span>sqrt</span><span>(lam_b))</span></span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"主轴两两正交："</span><span><span>, np.</span><span>allclose</span><span>(Q_b.T </span><span>@</span><span> Q_b, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>特征值： [ 0.1833 21.8167]</span></div></div><div><div><div>2</div></div><div><span>特征向量（列）：</span></div></div><div><div><div>3</div></div><div><span>[[-0.9571  0.2898]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.2898  0.9571]]</span></div></div><div><div><div>5</div></div><div><span>A 等于 Q Lambda Q^T： True</span></div></div><div><div><div>6</div></div><div><span>半轴长 1/sqrt(lambda)： [2.3354 0.2141]</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>主轴 1 的端点 [-2.2352  0.6768]，x^T A x = 1.0000</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>主轴 2 的端点 [0.062  0.2049]，x^T A x = 1.0000</span></div></div><div><div><div>9</div></div><div><span>随机方向缩放后：x^T A x = 1.0000</span></div></div><div><div><div>10</div></div><div><span>长半轴 / 短半轴 = 10.9083</span></div></div><div><div><div>11</div></div><div><span>sqrt(特征值之比) = 10.9083</span></div></div><div><div><div>12</div></div><div><span>鞍点矩阵：沿特征向量 1 走 0.5，f = -0.5000</span></div></div><div><div><div>13</div></div><div><span>鞍点矩阵：沿特征向量 2 走 0.5，f = 2.7500</span></div></div><div><div><div>14</div></div><div><span>3 x 3 半轴长： [1.3066 0.7071 0.5412]</span></div></div><div><div><div>15</div></div><div><span>主轴两两正交： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对 <span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span>，<span><span>λ=0.1833,21.8167\lambda = 0.1833, 21.8167</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1833</span><span>,</span><span></span><span>21.8167</span></span></span></span>，半轴长 2.3354 和 0.2141，两个端点 <span><span>(−2.2352,0.6768)(-2.2352, 0.6768)</span><span><span><span></span><span>(</span><span>−</span><span>2.2352</span><span>,</span><span></span><span>0.6768</span><span>)</span></span></span></span> 和 <span><span>(0.062,0.2049)(0.062, 0.2049)</span><span><span><span></span><span>(</span><span>0.062</span><span>,</span><span></span><span>0.2049</span><span>)</span></span></span></span> 代进 <span><span>xTAx\mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 都得 1.0000。长半轴与短半轴之比是 10.9083，恰好是 <span><span>λ2/λ1\sqrt{\lambda_2/\lambda_1}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，特征值之比是 119 倍，开方之后是 10.9 倍。这就是为什么图里那个椭圆这么细长：两个特征值差了两个数量级。</p><p>对 <span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span>，沿两个特征向量各走 0.5：一个方向 <span><span>f=−0.5000f = -0.5000</span><span><span><span></span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.5000</span></span></span></span>，另一个 <span><span>f=2.7500f = 2.7500</span><span><span><span></span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span>2.7500</span></span></span></span>。同样的步长，一边下坡一边上坡，鞍的定义就是这个。他说“完美的方向就是特征向量方向”，我验证的就是这句。把一颗弹珠放在原点，碗里它会停住，鞍上轻轻一推，它会顺着 <span><span>−0.5-0.5</span><span><span><span></span><span>−</span><span>0.5</span></span></span></span> 那个特征向量的方向滑走。（这是我这篇唯一的类比，用完就收。）</p><p>最后是他的 3 × 3 例子。四维空间里没法画图，但 <span><span>xTAx=1\mathbf{x}^TA\mathbf{x} = 1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 是一个椭球，他叫它“橄榄球”，有三个主轴，长度由特征值决定。输出里 3 × 3 的半轴长是 1.3066、0.7071、0.5412，主轴两两正交。如果特征值全相等，就是球（单位矩阵）；有两个相等，就是他说的“更接近橄榄球”的旋转椭球。</p></section>
<section><h2>极小值：二阶导数矩阵<a href="#极小值二阶导数矩阵"><span>#</span></a></h2><p><strong>这些和极小值有什么关系？</strong> Strang 回到微积分：一元函数 <span><span>y(x)y(x)</span><span><span><span></span><span>y</span><span>(</span><span>x</span><span>)</span></span></span></span> 在一个点取极小，要一阶导数为 0，再要二阶导数为正，也就是斜率在增加、曲线向上弯。多元函数完全平行：一阶导数（梯度）为 0，二阶导数矩阵</p><span><span><span>[fxxfxyfyxfyy]\begin{bmatrix} f_{xx} &amp; f_{xy} \\ f_{yx} &amp; f_{yy} \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>xx</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>y</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>x</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>y</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>正定。它为什么对称？因为混合偏导与求导次序无关，<span><span>fxy=fyxf_{xy} = f_{yx}</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>x</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>y</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他提到微积分课本里写的条件是 <span><span>fxxfyy&gt;fxy2f_{xx}f_{yy} &gt; f_{xy}^2</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>xx</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>y</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span><span>x</span><span>y</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，那只是 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的行列式条件；现在我们有了 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的答案，不管维数多高，都是同一句话：“数为正”变成“矩阵正定”。</p><p>我用中心差分算了前面两个函数在原点处的二阶导数矩阵，再用梯度下降看原点是不是真的吸引：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>hessian</span><span>(</span><span>f</span><span>,</span><span><span> </span><span>p</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>h</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-3</span></span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""中心差分求二阶导数矩阵。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> p.size</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>H </span><span>=</span><span> np.</span><span>zeros</span><span>((n, n))</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> np.</span><span>eye</span><span>(n) </span><span>*</span><span> h</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>H[i, j] </span><span>=</span><span> (</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>f</span><span>(p </span><span>+</span><span> E[i] </span><span>+</span><span> E[j])</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>-</span><span> </span><span>f</span><span>(p </span><span>+</span><span> E[i] </span><span>-</span><span> E[j])</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>-</span><span> </span><span>f</span><span>(p </span><span>-</span><span> E[i] </span><span>+</span><span> E[j])</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>+</span><span> </span><span>f</span><span>(p </span><span>-</span><span> E[i] </span><span>-</span><span> E[j])</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>) </span><span>/</span><span> (</span><span>4</span><span><span> </span><span>*</span><span> h </span><span>*</span><span> h)</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> H</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>def</span><span> </span><span>descend</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>p</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>steps</span></span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>lr</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>21</div></div><div><span>    </span><span>"""对 f = x^T A x 做梯度下降，返回最后的 |x|。"""</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(steps):</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> p </span><span>-</span><span> lr </span><span>*</span><span> </span><span>2</span><span><span> </span><span>*</span><span> A </span><span>@</span><span> p</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.linalg.</span><span>norm</span><span>(p))</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>origin </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>for</span><span> c </span><span>in</span><span> (</span><span>20.0</span><span>, </span><span>7.0</span><span>):</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>6.0</span><span>], [</span><span>6.0</span><span>, c]])</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>        </span><span>def</span><span> </span><span>f</span><span>(</span><span>p</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>A</span></span><span>:</span><span> np.ndarray </span><span>=</span><span> A</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>36</div></div><div><span>            </span><span>return</span><span> </span><span>float</span><span><span>(p </span><span>@</span><span> A </span><span>@</span><span> p)</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>H </span><span>=</span><span> </span><span>hessian</span><span>(f, origin)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>grad </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>[(</span><span>f</span><span>(e </span><span>*</span><span> </span><span>1e-3</span><span><span>) </span><span>-</span><span> </span><span>f</span><span>(</span><span>-</span><span>e </span><span>*</span><span> </span></span><span>1e-3</span><span><span>)) </span><span>/</span><span> </span></span><span>2e-3</span><span> </span><span>for</span><span> e </span><span>in</span><span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>)]</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>42</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"c = </span><span>{</span><span>c</span><span>:.0f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>43</div></div><div><span>        </span><span>print</span><span>(</span><span>"  原点处的梯度："</span><span>, grad)</span></div></div><div><div><div>44</div></div><div><span>        </span><span>print</span><span>(</span><span>"  二阶导数矩阵："</span><span>)</span></div></div><div><div><div>45</div></div><div><span>        </span><span>print</span><span>(H)</span></div></div><div><div><div>46</div></div><div><span>        </span><span>print</span><span>(</span><span>"  等于 2A："</span><span><span>, np.</span><span>allclose</span><span>(H, </span></span><span>2</span><span><span> </span><span>*</span><span> A, </span></span><span>atol</span><span>=</span><span>1e-4</span><span>))</span></div></div><div><div><div>47</div></div><div><span>        </span><span>print</span><span>(</span><span>"  二阶导数矩阵的特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(H))</span></span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>p0 </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>50</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>51</div></div><div><span>            </span><span>"  从随机点出发，梯度下降 5000 步后 |x| ="</span><span>,</span></div></div><div><div><div>52</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span><span>descend</span><span>(A, p0, </span></span><span>5000</span><span>, </span><span>0.01</span><span>)</span><span>:.3e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>53</div></div><div><span>            </span><span>"，起点 |x| ="</span><span>,</span></div></div><div><div><div>54</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(p0)</span></span><span>:.3f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>c = 20</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>原点处的梯度： [0. 0.]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>二阶导数矩阵：</span></div></div><div><div><div>4</div></div><div><span>[[ 4. 12.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[12. 40.]]</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>等于 2A： True</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>二阶导数矩阵的特征值： [ 0.3667 43.6333]</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>从随机点出发，梯度下降 5000 步后 |x| = 7.446e-09 ，起点 |x| = 0.707</span></div></div><div><div><div>9</div></div><div><span>c = 7</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>原点处的梯度： [0. 0.]</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>二阶导数矩阵：</span></div></div><div><div><div>12</div></div><div><span>[[ 4. 12.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[12. 14.]]</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>等于 2A： True</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>二阶导数矩阵的特征值： [-4. 22.]</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>从随机点出发，梯度下降 5000 步后 |x| = 6.057e+83 ，起点 |x| = 0.575</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><span><span>f=xTAxf = \mathbf{x}^TA\mathbf{x}</span><span><span><span></span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 的二阶导数矩阵是 <span><span>2A2A</span><span><span><span></span><span>2</span><span>A</span></span></span></span>（二次函数的二阶导数里，平方项会多出一个 2），所以输出里是 <span><span>4,12;12,404, 12; 12, 40</span><span><span><span></span><span>4</span><span>,</span><span></span><span>12</span><span>;</span><span></span><span>12</span><span>,</span><span></span><span>40</span></span></span></span>。Strang 在黑板上写的是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 本身，差一个因子 2 不影响正定与否。</li>
<li>原点处梯度是 0，这对碗和鞍都成立。一阶条件只给“候选点”，是二阶导数矩阵的特征值（<span><span>c=20c = 20</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 时 0.3667 与 43.6333，<span><span>c=7c = 7</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span> 时 <span><span>−4-4</span><span><span><span></span><span>−</span><span>4</span></span></span></span> 与 22）说了算。</li>
<li>梯度下降 5000 步：碗里 <span><span>∣x∣|\mathbf{x}|</span><span><span><span></span><span>∣</span><span>x</span><span>∣</span></span></span></span> 从 0.707 降到 <span><span>7.446×10−97.446 \times 10^{-9}</span><span><span><span></span><span>7.446</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，鞍上从 0.575 涨到 <span><span>6.057×10+836.057 \times 10^{+83}</span><span><span><span></span><span>6.057</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>+</span><span>83</span></span></span></span></span></span></span></span></span></span></span></span>。第二个数字大得有点滑稽，但它说的只是：沿着 <span><span>−4-4</span><span><span><span></span><span>−</span><span>4</span></span></span></span> 对应的方向，每一步都把偏离放大。</li>
</ul><p>注意碗里收敛得并不快：5000 步才降到 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>。二阶导数矩阵两个特征值差了 119 倍，步长要听最大的特征值的，而最平的方向就只能慢慢走。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>LQR 的代价每一步是 <span><span>xTQx+uTRu\mathbf{x}^TQ\mathbf{x} + \mathbf{u}^TR\mathbf{u}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span><span>u</span></span></span></span>，正是两个二次型相加。把 <span><span>(x,u)(\mathbf{x}, \mathbf{u})</span><span><span><span></span><span>(</span><span>x</span><span>,</span><span></span><span>u</span><span>)</span></span></span></span> 拼成一个向量，它就是 <span><span>zTdiag⁡(Q,R) z\mathbf{z}^T\operatorname{diag}(Q, R)\,\mathbf{z}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span><span>diag</span></span><span>(</span><span>Q</span><span>,</span><span></span><span>R</span><span>)</span><span></span><span>z</span></span></span></span>。这一讲的结论拿来用就是：<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的正（半）定性决定了“代价函数是不是一个碗”。我在 <a href="/posts/pid-control-09-lqr/">平衡步兵的 LQR</a> 里用 Bryson 法则取的 <span><span>Q=diag⁡(4,1,100,1)Q = \operatorname{diag}(4, 1, 100, 1)</span><span><span><span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>4</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>100</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>R=0.01R = 0.01</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span>，下面检查它们：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>is_pd</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""用 Cholesky 判断是否正定。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>try</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>cholesky</span><span>(M)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>except</span><span> np.linalg.LinAlgError:</span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>True</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># Bryson 权重：允许的最大值的平方的倒数</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>x_max </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.5</span><span>, </span><span>1.0</span><span>, </span><span>0.1</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>u_max </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>10.0</span><span>])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>diag</span><span>(</span><span>1</span><span><span> </span><span>/</span><span> x_max</span><span>**</span></span><span>2</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> np.</span><span>diag</span><span>(</span><span>1</span><span><span> </span><span>/</span><span> u_max</span><span>**</span></span><span>2</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q 的对角线："</span><span><span>, np.</span><span>diag</span><span>(Q), </span></span><span>"，R ="</span><span>, R[</span><span>0</span><span>, </span><span>0</span><span>])</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q 正定："</span><span><span>, </span><span>is_pd</span><span>(Q), </span></span><span>"，R 正定："</span><span><span>, </span><span>is_pd</span><span>(R))</span></span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 每一步的代价 x^T Q x + u^T R u 是 (x, u) 上的一个二次型</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>block</span><span>([[Q, np.</span><span>zeros</span><span>((</span><span>4</span><span>, </span><span>1</span><span><span>))], [np.</span><span>zeros</span><span>((</span></span><span>1</span><span>, </span><span>4</span><span>)), R]])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.5</span><span>, </span><span>1.0</span><span>, </span><span>0.1</span><span>, </span><span>1.0</span><span>, </span><span>10.0</span><span>])  </span><span># 每个量都取到允许的最大值</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"全部取最大允许值时的代价："</span><span><span>, z </span><span>@</span><span> M </span><span>@</span><span> z)</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 把 Q 的第二个权重设成 0：半正定，有一个方向不花钱</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>Q0 </span><span>=</span><span> Q.</span><span>copy</span><span>()</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>Q0[</span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>x_free </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q0 正定："</span><span><span>, </span><span>is_pd</span><span>(Q0), </span></span><span>"，Q0 的特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(Q0))</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"速度 1 m/s、其余为 0 的代价："</span><span><span>, x_free </span><span>@</span><span> Q0 </span><span>@</span><span> x_free)</span></span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 非对角的 Q：同时惩罚位置和倾角的“耦合”，要看是否仍然正定</span></div></div><div><div><div>37</div></div><div><span>    </span><span>for</span><span> coupling </span><span>in</span><span> (</span><span>1.0</span><span>, </span><span>25.0</span><span>):</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>Qc </span><span>=</span><span> Q.</span><span>copy</span><span>()</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>Qc[</span><span>0</span><span>, </span><span>2</span><span><span>] </span><span>=</span><span> Qc[</span></span><span>2</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> coupling</span></span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>eigs </span><span>=</span><span> np.linalg.</span><span>eigvalsh</span><span>(Qc)</span></div></div><div><div><div>41</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>42</div></div><div><span>            </span><span>f</span><span>"耦合项 </span><span>{</span><span>coupling</span><span>:&gt;4</span><span>}</span><span>：最小特征值 </span><span>{</span><span>eigs[</span><span>0</span><span>]</span><span>:.4f</span><span>}</span><span>，正定："</span><span>,</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>is_pd</span><span>(Qc),</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Q 的对角线： [  4.   1. 100.   1.] ，R = 0.01</span></div></div><div><div><div>2</div></div><div><span>Q 正定： True ，R 正定： True</span></div></div><div><div><div>3</div></div><div><span>全部取最大允许值时的代价： 5.0</span></div></div><div><div><div>4</div></div><div><span>Q0 正定： False ，Q0 的特征值： [  0.   1.   4. 100.]</span></div></div><div><div><div>5</div></div><div><span>速度 1 m/s、其余为 0 的代价： 0.0</span></div></div><div><div><div>6</div></div><div><span>耦合项  1.0：最小特征值 1.0000，正定： True</span></div></div><div><div><div>7</div></div><div><span>耦合项 25.0：最小特征值 -2.1202，正定： False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>据我所知，标准的 LQR 要求 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 半正定、<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 正定。原因是这样的（我没有在代码里验证 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 半正定的情形）：如果 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 只是半正定，就会有某个方向的输入不花代价，优化器没有理由不在那个方向上无限加大；<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 正定就保证每一个非零输入都有正的代价，也保证求增益时要求逆的 <span><span>R+BTPBR + B^TPB</span><span><span><span></span><span>R</span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>P</span><span>B</span></span></span></span> 可逆。<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 可以是半正定的，只是意味着某些状态“不花钱”，输出里把速度权重设成 0，速度 1 m/s 的代价是 0.0（这时 LQR 是不是还有解，要看系统可检测不可检测，我没有在这里验证，是控制课的内容）。</p><p>最后一个检查是容易踩的坑：一旦写非对角的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 去表达“位置和倾角的耦合”，就不能随手填数。耦合项 1.0 时最小特征值是 1.0000，仍然正定；耦合项 25.0 时最小特征值是 <span><span>−2.1202-2.1202</span><span><span><span></span><span>−</span><span>2.1202</span></span></span></span>，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 不正定，代价函数在某个方向上可以是负的，LQR 就没有意义了。写非对角权重前先用 <code>cholesky</code> 跑一遍，几乎不费事。</p></section>
<section><h2>下一讲<a href="#下一讲"><span>#</span></a></h2><p>今天一直在用“特征值全正”，但特征值有它不好用的地方：两个不同的矩阵可以有同样的特征值，而且有的矩阵根本对角化不了。下一讲 Strang 要讲相似矩阵与 Jordan 标准形，我把没法对角化的那种矩阵拿出来，对它加一个 <span><span>10−1010^{-10}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 的扰动，看看特征值会跑多远，见 <a href="/posts/linear-algebra-28-similar-matrices-jordan/">18.06 第 28 讲：相似矩阵与 Jordan 标准形</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-26-complex-matrices-fft/</id>
      <title type="text">18.06 第 26 讲：复矩阵与快速 Fourier 变换</title>
      <published>2026-10-18T08:40:00.000Z</published>
      <updated>2026-10-18T08:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-26-complex-matrices-fft/"/>
      <summary type="text">MIT 18.06 第 26 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 验证了归一化的 Fourier 矩阵是酉矩阵，又手写了递归 FFT，n = 1024 时乘法次数从 1048576 降到 5120，差 204.8 倍。</summary>
      <content type="html"><![CDATA[<p>这一讲有两个想法。第一，向量一旦有了复数分量，凡是“转置”都要顺手取共轭，<span><span>zTz\mathbf{z}^T\mathbf{z}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>z</span></span></span></span> 就换成 <span><span>zHz\mathbf{z}^H\mathbf{z}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>z</span></span></span></span>。第二，Fourier 矩阵 <span><span>FnF_n</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是满矩阵，乘一次要 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 次乘法，但它能拆成两个一半大小的 <span><span>Fn/2F_{n/2}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 加几个几乎全是零的因子。我手写了这个递归：<span><span>n=1024n = 1024</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>1024</span></span></span></span> 时，<span><span>n2=1048576n^2 = 1048576</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1048576</span></span></span></span> 次乘法被压到 5120 次，比值 204.8。Strang 在黑板上算 <span><span>2102^{10}</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 的时候说“大约 200 倍”，代码给的数是 204.8。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=M0Sa8fLOajA" target="_blank">Lecture 26: Complex matrices; fast fourier transform</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-26-complex-matrices-fast-fourier-transform/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §6.4 Complex Numbers and Vectors and Matrices（FFT 在第 6 版目录里没有独立小节，我没有找到对应的页码，所以不标）
前置：<a href="/posts/linear-algebra-25-symmetric-positive-definite/">第 25 讲：对称矩阵与正定性</a>
一句话：复数世界里把“转置”换成“共轭转置”，Fourier 矩阵就成了酉矩阵，逆矩阵就是它的共轭转置；而把它拆成偶数项和奇数项两半，乘法次数从 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 降到 <span><span>12nlog⁡2n\frac{1}{2}n\log_2 n</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>n</span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>n</span></span></span></span>。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几分钟是动机：特征值可能是复数，所以复数迟早要进来。这一段字幕里讲得很直白，可以倍速。</li>
<li>他讲 <span><span>zTz\mathbf{z}^T\mathbf{z}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>z</span></span></span></span> 为什么不行的那段（向量 <span><span>(1,i)(1, i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span> 的“长度”居然是 0）很短，值得认真听；Hermitian 矩阵和酉矩阵只是改了两个词，本篇第一节已经用代码算过。</li>
<li>他在黑板上写 <span><span>F4F_4</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、检查两列正交时当场卡了一下，然后想起要取共轭。这段看着有点乱，但它恰好是整讲最容易犯错的地方，别跳。</li>
<li>最后一段的 <span><span>F64F_{64}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>64</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 到 <span><span>F32F_{32}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的分解是重点，建议先看完再回来读本篇第三、第四节；他最后算 <span><span>2102^{10}</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 的那几句，我在第四节用代码重算了一遍。</li>
</ul></div></div>
<section><h2>复向量：长度、内积与 Hermitian 矩阵<a href="#复向量长度内积与-hermitian-矩阵"><span>#</span></a></h2><p>复数是被特征值带进这门课的：即使矩阵的元素全是实数，特征值也可能是复数，比如把平面旋转 <span><span>90°90°</span><span><span><span></span><span>90°</span></span></span></span> 的矩阵，特征值是 <span><span>±i\pm i</span><span><span><span></span><span>±</span><span>i</span></span></span></span>，对应的特征向量也是复向量。他说迟早要正面看一次：向量和矩阵的元素变成复数以后，哪些定义要改，哪些不用改。答案是只改一件事，下面从长度开始。</p><p><strong>复向量的长度怎么算？</strong> 他的做法是先给一个反例。取 <span><span>z=(1,i)\mathbf{z} = (1, i)</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span>，如果照老办法算 <span><span>zTz\mathbf{z}^T\mathbf{z}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>z</span></span></span></span>，得到 <span><span>1⋅1+i⋅i=01 \cdot 1 + i \cdot i = 0</span><span><span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>i</span><span></span><span>⋅</span><span></span></span><span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，一个非零向量的长度平方居然是零。问题出在 <span><span>i2=−1i^2 = -1</span><span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>。他的修正是：每个分量先取共轭再相乘，<span><span>zˉ1z1+⋯+zˉnzn=∣z1∣2+⋯+∣zn∣2\bar{z}_1 z_1 + \cdots + \bar{z}_n z_n = |z_1|^2 + \cdots + |z_n|^2</span><span><span><span></span><span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>∣</span><span><span>z</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span>∣</span><span><span>z</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，这样每一项都不是负数。</p><p>所以规则只有一条：只要转置，就同时取共轭。他用一个符号 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 同时表示这两件事（取自 Hermite，他顺便说这位先生自己不发 H 的音）：</p><span><span><span>zH=zˉT,∥z∥2=zHz,内积 yHx\mathbf{z}^H = \bar{\mathbf{z}}^T,\qquad
\|\mathbf{z}\|^2 = \mathbf{z}^H\mathbf{z},\qquad
\text{内积 } \mathbf{y}^H\mathbf{x}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>∥</span><span>z</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>z</span><span>,</span><span></span><span></span><span><span>内积</span><span> </span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>x</span></span></span></span></span><p>内积一般是复数，交换两个向量的位置得到的是共轭：<span><span>xHy=yHx‾\mathbf{x}^H\mathbf{y} = \overline{\mathbf{y}^H\mathbf{x}}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>x</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。</p><p>同样的规则改写了另外两个词。“对称”（<span><span>AT=AA^T = A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span></span></span></span>）对复矩阵不再合适，要换成 <span><span>AH=AA^H = A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span></span></span></span>，叫 Hermitian 矩阵。他的例子是</p><span><span><span>A=[23+i3−i5]A = \begin{bmatrix} 2 &amp; 3 + i \\ 3 - i &amp; 5 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span><span></span><span>−</span><span></span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span><span></span><span>+</span><span></span><span>i</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>对角线必须是实数（翻过去再取共轭还得回到原位），非对角线上的 <span><span>3+i3 + i</span><span><span><span></span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>i</span></span></span></span> 和 <span><span>3−i3 - i</span><span><span><span></span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>i</span></span></span></span> 互为共轭。“正交矩阵”（<span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>）换成 <span><span>QHQ=IQ^HQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，叫酉矩阵（unitary）。Hermitian 矩阵的特征值是实数，特征向量互相垂直，这是上一讲实对称矩阵那套结论的复数版本。</p><p>下面的脚本把这几件事都算一遍。NumPy 里 <code>np.vdot(y, x)</code> 正好是 <span><span>yHx\mathbf{y}^H\mathbf{x}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>x</span></span></span></span>（第一个参数取共轭），<code>np.linalg.eigh</code> 是专门给 Hermitian 矩阵用的特征值求解器：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 复向量的长度：z^T z 不行，要用 z^H z</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>j</span><span>])</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"z^T z ="</span><span><span>, z </span><span>@</span><span> z)</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"z^H z ="</span><span><span>, z.</span><span>conj</span><span>() </span><span>@</span><span> z, </span></span><span>"，长度 ="</span><span><span>, np.linalg.</span><span>norm</span><span>(z))</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 内积 y^H x 一般是复数，np.vdot 会对第一个参数取共轭</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span> </span><span>+</span><span> </span></span><span>2.0</span><span>j</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span><span> </span><span>-</span><span> </span></span><span>1.0</span><span>j</span><span>])</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"y^H x ="</span><span><span>, np.</span><span>vdot</span><span>(y, x), </span></span><span>"，x^H y ="</span><span><span>, np.</span><span>vdot</span><span>(x, y))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># Hermitian 矩阵：对角线是实数，转置后取共轭等于自己</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>3.0</span><span><span> </span><span>+</span><span> </span></span><span>1.0</span><span>j</span><span>], [</span><span>3.0</span><span><span> </span><span>-</span><span> </span></span><span>1.0</span><span>j</span><span>, </span><span>5.0</span><span>]])</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^H = A："</span><span><span>, np.</span><span>allclose</span><span>(A.</span><span>conj</span><span>().T, A))</span></span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>lam, Q </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值（实数）："</span><span><span>, np.</span><span>round</span><span>(lam, </span></span><span>6</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q^H Q = I："</span><span><span>, np.</span><span>allclose</span><span>(Q.</span><span>conj</span><span>().T </span><span>@</span><span> Q, np.</span><span>eye</span><span>(</span></span><span>2</span><span>)))</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"A = Q Λ Q^H："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> np.</span><span>diag</span><span>(lam) </span><span>@</span><span> Q.</span><span>conj</span><span>().T, A))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"两个特征向量的内积："</span><span><span>, np.</span><span>abs</span><span>(np.</span><span>vdot</span><span>(Q[:, </span></span><span>0</span><span>], Q[:, </span><span>1</span><span>])))</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>z^T z = 0j</span></div></div><div><div><div>2</div></div><div><span>z^H z = (2+0j) ，长度 = 1.4142135623730951</span></div></div><div><div><div>3</div></div><div><span>y^H x = (5-7j) ，x^H y = (5+7j)</span></div></div><div><div><div>4</div></div><div><span>A^H = A： True</span></div></div><div><div><div>5</div></div><div><span>特征值（实数）： [0. 7.]</span></div></div><div><div><div>6</div></div><div><span>Q^H Q = I： True</span></div></div><div><div><div>7</div></div><div><span>A = Q Λ Q^H： True</span></div></div><div><div><div>8</div></div><div><span>两个特征向量的内积： 1.457167719820518e-16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三处输出对应三个结论。<code>z^T z = 0j</code>，而 <code>z^H z = 2</code>，长度是 <span><span>2\sqrt{2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，和他在黑板上写的完全一致。<span><span>yHx=5−7i\mathbf{y}^H\mathbf{x} = 5 - 7i</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span><span></span><span>−</span><span></span></span><span><span></span><span>7</span><span>i</span></span></span></span>，反过来 <span><span>xHy=5+7i\mathbf{x}^H\mathbf{y} = 5 + 7i</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span><span></span><span>+</span><span></span></span><span><span></span><span>7</span><span>i</span></span></span></span>，互为共轭。他的 Hermitian 矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值是 0 和 7：迹是 <span><span>2+5=72 + 5 = 7</span><span><span><span></span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span>，行列式是 <span><span>10−(3+i)(3−i)=10−10=010 - (3 + i)(3 - i) = 10 - 10 = 0</span><span><span><span></span><span>10</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>i</span><span>)</span><span>(</span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>i</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span></span><span>−</span><span></span></span><span><span></span><span>10</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，两个数都对得上；<code>eigh</code> 给出的两个特征向量内积的模是 <span><span>1.5×10−161.5 \times 10^{-16}</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，就是舍入误差。</p><p>这里有个我自己踩过的坑：写 <code>a @ b</code> 的时候，NumPy 不会替你取共轭。复向量的内积忘了 <code>.conj()</code>，程序不报错，只是答案不对。</p></section>
<section><h2>Fourier 矩阵：满矩阵，却有正交的列<a href="#fourier-矩阵满矩阵却有正交的列"><span>#</span></a></h2><p><strong>Fourier 矩阵是什么样的？</strong> 他先宣布一个约定：下标从 0 数起，因为电气系这么数（数学系从 1 开始；我是写代码的，毫无悬念站电气系）。<span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的 Fourier 矩阵第 <span><span>(j,k)(j, k)</span><span><span><span></span><span>(</span><span>j</span><span>,</span><span></span><span>k</span><span>)</span></span></span></span> 项是 <span><span>wjkw^{jk}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>k</span></span></span></span></span></span></span></span></span></span></span></span>，其中</p><span><span><span>w=e2πi/n,wn=1w = e^{2\pi i/n},\qquad w^n = 1</span><span><span><span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>2</span><span>π</span><span>i</span><span>/</span><span>n</span></span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></span><p><span><span>ww</span><span><span><span></span><span>w</span></span></span></span> 落在复平面的单位圆上，是圆周的 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 分之一。他取 <span><span>n=6n = 6</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span> 画了六个点，每个是上一个转 <span><span>60°60°</span><span><span><span></span><span>60°</span></span></span></span>，<span><span>w6w^6</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 回到 1，它们是 1 的六个六次方根，<span><span>ww</span><span><span><span></span><span>w</span></span></span></span> 是第一个，叫本原的。取 <span><span>n=4n = 4</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span> 更方便：<span><span>w=eiπ/2=iw = e^{i\pi/2} = i</span><span><span><span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>iπ</span><span>/2</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>i</span></span></span></span>，各次幂是 <span><span>i,−1,−i,1i, -1, -i, 1</span><span><span><span></span><span>i</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>i</span><span>,</span><span></span><span>1</span></span></span></span>，于是</p><span><span><span>F4=[11111ii2i31i2i4i61i3i6i9]=[11111i−1−i1−11−11−i−1i]F_4 = \begin{bmatrix}
1 &amp; 1 &amp; 1 &amp; 1 \\
1 &amp; i &amp; i^2 &amp; i^3 \\
1 &amp; i^2 &amp; i^4 &amp; i^6 \\
1 &amp; i^3 &amp; i^6 &amp; i^9
\end{bmatrix}
= \begin{bmatrix}
1 &amp; 1 &amp; 1 &amp; 1 \\
1 &amp; i &amp; -1 &amp; -i \\
1 &amp; -1 &amp; 1 &amp; -1 \\
1 &amp; -i &amp; -1 &amp; i
\end{bmatrix}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>i</span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>i</span><span><span><span><span><span><span></span><span><span>9</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>−</span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>i</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>指数就是行号乘列号。它是满矩阵，一个零都没有，乘一次向量要 16 次乘法；一般地是 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 次。</p><p><strong>它的列为什么正交？</strong> 看第 1 列 <span><span>(1,i,−1,−i)(1, i, -1, -i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>i</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>i</span><span>)</span></span></span></span> 与第 3 列 <span><span>(1,−i,−1,i)(1, -i, -1, i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>i</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span>。他的第一反应是直接逐项相乘：<span><span>1⋅1+i(−i)+(−1)(−1)+(−i)(i)=1+1+1+1=41 \cdot 1 + i(-i) + (-1)(-1) + (-i)(i) = 1 + 1 + 1 + 1 = 4</span><span><span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>i</span><span>(</span><span>−</span><span>i</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span>)</span><span>(</span><span>−</span><span>1</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>i</span><span>)</span><span>(</span><span>i</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>，看上去不是零。然后他想起来要给其中一列取共轭，第 1 列变成 <span><span>(1,−i,−1,i)(1, -i, -1, i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>i</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span>，再逐项相乘：<span><span>1+(−i)(−i)+(−1)(−1)+i⋅i=1−1+1−1=01 + (-i)(-i) + (-1)(-1) + i \cdot i = 1 - 1 + 1 - 1 = 0</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>i</span><span>)</span><span>(</span><span>−</span><span>i</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span>)</span><span>(</span><span>−</span><span>1</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>i</span><span></span><span>⋅</span><span></span></span><span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。这个“看上去不是零，其实是零”的瞬间，就是复内积和实内积的差别。每一列的长度平方是 4，所以除以 2 就是标准正交列：<span><span>12F4\frac{1}{2}F_4</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是酉矩阵，<span><span>F4HF4=4IF_4^HF_4 = 4I</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>H</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span>I</span></span></span></span>，逆矩阵就是共轭转置除以 4。</p><p>我把这些全部交给 NumPy，同时也检查 <span><span>n=4,16,64,256n = 4, 16, 64, 256</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span>,</span><span></span><span>16</span><span>,</span><span></span><span>64</span><span>,</span><span></span><span>256</span></span></span></span> 的归一化版本 <span><span>Fn/nF_n/\sqrt{n}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，并确认它和 <code>np.fft</code> 的关系。后一点很实用：NumPy 的 <code>fft</code> 用的是 <span><span>e−2πijk/ne^{-2\pi i jk/n}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>2</span><span>π</span><span>ij</span><span>k</span><span>/</span><span>n</span></span></span></span></span></span></span></span></span></span></span></span>（负号），Strang 用的是正号，所以他的 <span><span>FnxF_n \mathbf{x}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>x</span></span></span></span> 等于 NumPy 的 <code>n * ifft(x)</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>fourier</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 Strang 约定的 Fourier 矩阵：F[j, k] = w^(j k)，w = e^(2 pi i/n)。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>exp</span><span>(</span><span>2</span><span>j</span><span><span> </span><span>*</span><span> np.pi </span><span>/</span><span> n)</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>j </span><span>=</span><span> np.</span><span>arange</span><span>(n)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span><span> w </span><span>**</span><span> (np.</span><span>outer</span><span>(j, j) </span><span>%</span><span> n)</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>F </span><span>=</span><span> </span><span>fourier</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"F4 ="</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span><span>(np.</span><span>round</span><span>(F, </span></span><span>4</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 第 1 列和第 3 列：直接相乘不是 0，取共轭之后是 0</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"列 1 与列 3，不取共轭："</span><span><span>, np.</span><span>round</span><span>(F[:, </span></span><span>1</span><span><span>] </span><span>@</span><span> F[:, </span></span><span>3</span><span>], </span><span>10</span><span>))</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"列 1 与列 3，取共轭："</span><span><span>, np.</span><span>round</span><span>(np.</span><span>vdot</span><span>(F[:, </span></span><span>1</span><span>], F[:, </span><span>3</span><span>]), </span><span>10</span><span>))</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> F.</span><span>conj</span><span>().T </span><span>@</span><span> F</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"F^H F ="</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span><span>(np.</span><span>round</span><span>(G, </span></span><span>4</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> F </span><span>/</span><span> </span><span>2.0</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"F4 / 2 是酉矩阵："</span><span><span>, np.</span><span>allclose</span><span>(U.</span><span>conj</span><span>().T </span><span>@</span><span> U, np.</span><span>eye</span><span>(</span></span><span>4</span><span>)))</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"逆 = 共轭转置："</span><span><span>, np.</span><span>allclose</span><span>(np.linalg.</span><span>inv</span><span>(F), F.</span><span>conj</span><span>().T </span><span>/</span><span> </span></span><span>4.0</span><span>))</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> [</span><span>4</span><span>, </span><span>16</span><span>, </span><span>64</span><span>, </span><span>256</span><span>]:</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>Un </span><span>=</span><span> </span><span>fourier</span><span>(n) </span><span>/</span><span> np.</span><span>sqrt</span><span>(n)</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(Un.</span><span>conj</span><span>().T </span><span>@</span><span> Un </span><span>-</span><span> np.</span><span>eye</span><span>(n))</span></div></div><div><div><div>32</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"n = </span><span>{</span><span>n</span><span>:3d</span><span>}</span><span>，||U^H U - I|| = </span><span>{</span><span>err</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span># 与 np.fft 的关系：NumPy 的指数是负号，Strang 的是正号</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>8</span><span><span>) </span><span>+</span><span> </span></span><span>1</span><span>j</span><span><span> </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(</span></span><span>8</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>38</div></div><div><span>        </span><span>"F8 @ x = n * ifft(x)："</span><span>,</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(</span><span>fourier</span><span>(</span><span>8</span><span><span>) </span><span>@</span><span> x, </span></span><span>8</span><span><span> </span><span>*</span><span> np.fft.</span><span>ifft</span><span>(x)),</span></span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>42</div></div><div><span>        </span><span>"conj(F8) @ x = fft(x)："</span><span>,</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(</span><span>fourier</span><span>(</span><span>8</span><span><span>).</span><span>conj</span><span>() </span><span>@</span><span> x, np.fft.</span><span>fft</span><span>(x)),</span></span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>F4 =</span></div></div><div><div><div>2</div></div><div><span>[[ 1.+0.j  1.+0.j  1.+0.j  1.+0.j]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 1.+0.j  0.+1.j -1.+0.j  0.-1.j]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 1.+0.j -1.+0.j  1.+0.j -1.+0.j]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 1.+0.j  0.-1.j -1.+0.j  0.+1.j]]</span></div></div><div><div><div>6</div></div><div><span>列 1 与列 3，不取共轭： (4-0j)</span></div></div><div><div><div>7</div></div><div><span>列 1 与列 3，取共轭： 0j</span></div></div><div><div><div>8</div></div><div><span>F^H F =</span></div></div><div><div><div>9</div></div><div><span>[[4.+0.j 0.+0.j 0.+0.j 0.+0.j]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0.+0.j 4.+0.j 0.+0.j 0.+0.j]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[0.+0.j 0.+0.j 4.+0.j 0.+0.j]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[0.+0.j 0.+0.j 0.+0.j 4.+0.j]]</span></div></div><div><div><div>13</div></div><div><span>F4 / 2 是酉矩阵： True</span></div></div><div><div><div>14</div></div><div><span>逆 = 共轭转置： True</span></div></div><div><div><div>15</div></div><div><span>n =   4，||U^H U - I|| = 1.85e-16</span></div></div><div><div><div>16</div></div><div><span>n =  16，||U^H U - I|| = 1.44e-15</span></div></div><div><div><div>17</div></div><div><span>n =  64，||U^H U - I|| = 2.90e-14</span></div></div><div><div><div>18</div></div><div><span>n = 256，||U^H U - I|| = 1.24e-13</span></div></div><div><div><div>19</div></div><div><span>F8 @ x = n * ifft(x)： True</span></div></div><div><div><div>20</div></div><div><span>conj(F8) @ x = fft(x)： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读输出：第 1 列与第 3 列不取共轭的内积是 4，取共轭之后是 0。<span><span>FHFF^HF</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>F</span></span></span></span> 是 <span><span>4I4I</span><span><span><span></span><span>4</span><span>I</span></span></span></span>。四个尺寸的 <span><span>∥UHU−I∥\|U^HU - I\|</span><span><span><span></span><span>∥</span><span><span>U</span><span><span><span><span><span><span></span><span><span>H</span></span></span></span></span></span></span></span><span>U</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>∥</span></span></span></span> 从 <span><span>1.85×10−161.85 \times 10^{-16}</span><span><span><span></span><span>1.85</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 慢慢涨到 <span><span>1.24×10−131.24 \times 10^{-13}</span><span><span><span></span><span>1.24</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，矩阵越大累积的舍入误差越多，但始终在机器精度的几百倍以内。最后两行证实了符号约定：<span><span>F8x=8 ifft(x)F_8\mathbf{x} = 8\,\text{ifft}(\mathbf{x})</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>8</span><span></span><span><span>ifft</span></span><span>(</span><span>x</span><span>)</span></span></span></span>，<span><span>F8‾x=fft(x)\overline{F_8}\mathbf{x} = \text{fft}(\mathbf{x})</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>fft</span></span><span>(</span><span>x</span><span>)</span></span></span></span>。</p><p>（顺带一个我实际碰到的数字：我第一次写 <code>w ** np.outer(j, j)</code>，没有对 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 取模，<span><span>n=256n = 256</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>256</span></span></span></span> 时误差是 <span><span>2×10−112 \times 10^{-11}</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>11</span></span></span></span></span></span></span></span></span></span></span></span>，比现在大两个数量级。指数大到几万的时候，<code>w</code> 的乘方在浮点里已经不是精确的单位复数了。代码里的 <code>% n</code> 就是为这个加的。）</p><p>这就是为什么逆变换这么便宜：求逆矩阵通常要 <span><span>O(n3)O(n^3)</span><span><span><span></span><span>O</span><span>(</span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，这里什么都不用算，转置再取共轭就是逆。正变换用 <span><span>FnF_n</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，逆变换用 <span><span>Fn‾/n\overline{F_n}/n</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>/</span><span>n</span></span></span></span>，两者几乎一样，他说它们很近，近到容易搞混。</p></section>
<section><h2>把 F64 拆成两个 F32<a href="#把-f64-拆成两个-f32"><span>#</span></a></h2><p><strong>快在哪里？</strong> 他从一个看似不相干的观察开始：把 <span><span>w64w_{64}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>64</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 平方，角度翻倍，正好得到 <span><span>w32w_{32}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。所以 <span><span>F64F_{64}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>64</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>F32F_{32}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 用的是同一族数，应该能连起来。连接的公式是</p><span><span><span>F64=[IDI−D][F3200F32]PF_{64} =
\begin{bmatrix} I &amp; D \\ I &amp; -D \end{bmatrix}
\begin{bmatrix} F_{32} &amp; 0 \\ 0 &amp; F_{32} \end{bmatrix}
P</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>64</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>D</span></span></span><span><span></span><span><span>−</span><span>D</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>P</span></span></span></span></span><p>三个因子各管一件事。最右边的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 是置换矩阵：把向量的偶数下标分量 <span><span>x0,x2,x4,…x_0, x_2, x_4, \ldots</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span></span></span></span> 排在前面，奇数下标分量 <span><span>x1,x3,x5,…x_1, x_3, x_5, \ldots</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span></span></span></span> 排在后面。中间是两份 <span><span>F32F_{32}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，分别处理偶数部分和奇数部分，中间两块零是关键。最左边是“修补”：<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是对角矩阵，对角线是 <span><span>1,w,w2,…,w311, w, w^2, \ldots, w^{31}</span><span><span><span></span><span>1</span><span>,</span><span></span><span>w</span><span>,</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span></span></span></span></span></span></span></span>，上半部分用 <span><span>+D+D</span><span><span><span></span><span>+</span><span>D</span></span></span></span> 合并，下半部分用 <span><span>−D-D</span><span><span><span></span><span>−</span><span>D</span></span></span></span> 合并。</p><p>他说这个公式要检查，不过只是核对一堆求和，没有当场推。我补一下推导，只要三行。输出的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 项是 <span><span>∑kwjkxk\sum_k w^{jk}x_k</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span><span>k</span></span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，把 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 拆成偶数 <span><span>2k′2k'</span><span><span><span></span><span>2</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span> 和奇数 <span><span>2k′+12k'+1</span><span><span><span></span><span>2</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span>：</p><span><span><span>yj=∑k′wn/2jk′ x2k′⏟偶数部分的 Fn/2+wj∑k′wn/2jk′ x2k′+1⏟奇数部分的 Fn/2y_j = \underbrace{\sum_{k'} w_{n/2}^{jk'}\, x_{2k'}}_{\text{偶数部分的 } F_{n/2}} +
w^{j} \underbrace{\sum_{k'} w_{n/2}^{jk'}\, x_{2k'+1}}_{\text{奇数部分的 } F_{n/2}}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>偶数部分的</span><span> </span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span><span><span><span></span><span><span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span><span><span></span><span><span><span>j</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>2</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>j</span></span></span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span><span>奇数部分的</span><span> </span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span><span><span><span></span><span><span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span><span><span></span><span><span><span>j</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>2</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p>这里用了 <span><span>wn2=wn/2w_n^2 = w_{n/2}</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。而对 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 与 <span><span>j+n/2j + n/2</span><span><span><span></span><span>j</span><span></span><span>+</span><span></span></span><span><span></span><span>n</span><span>/2</span></span></span></span> 来说，两个 <span><span>Fn/2F_{n/2}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的结果相同（因为 <span><span>wn/2n/2=1w_{n/2}^{n/2} = 1</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>），<span><span>wn/2=−1w^{n/2} = -1</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>n</span><span>/2</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span> 只让奇数部分变号。所以前一半输出是“偶数 <span><span>+D⋅+ D \cdot</span><span><span><span></span><span>+</span><span>D</span><span>⋅</span></span></span></span> 奇数”，后一半是“偶数 <span><span>−D⋅- D \cdot</span><span><span><span></span><span>−</span><span>D</span><span>⋅</span></span></span></span> 奇数”，这正是 <span><span>[IDI−D]\begin{bmatrix} I &amp; D \\ I &amp; -D \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>D</span></span></span><span><span></span><span><span>−</span><span>D</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 做的事。</p><p>下面的脚本把三个因子真的造出来，对 <span><span>n=4,8,64n = 4, 8, 64</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span>,</span><span></span><span>8</span><span>,</span><span></span><span>64</span></span></span></span> 验证乘积等于 <span><span>FnF_n</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，并数一数每个因子里有多少个非零元：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>fourier</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 F[j, k] = w^(j k)，w = e^(2 pi i/n)。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>exp</span><span>(</span><span>2</span><span>j</span><span><span> </span><span>*</span><span> np.pi </span><span>/</span><span> n)</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>j </span><span>=</span><span> np.</span><span>arange</span><span>(n)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span><span> w </span><span>**</span><span> (np.</span><span>outer</span><span>(j, j) </span><span>%</span><span> n)</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>split_factors</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; tuple[np.ndarray, np.ndarray, np.ndarray]:</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"""返回 F_n = B @ blockdiag(F_m, F_m) @ P 中的 B、blockdiag 和 P。"""</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> n </span><span>//</span><span> </span><span>2</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>exp</span><span>(</span><span>2</span><span>j</span><span><span> </span><span>*</span><span> np.pi </span><span>/</span><span> n)</span></span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>diag</span><span>(w </span><span>**</span><span> np.</span><span>arange</span><span>(m))</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(m)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>block</span><span>([[identity, D], [identity, </span><span>-</span><span>D]])</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>Fm </span><span>=</span><span> </span><span>fourier</span><span>(m)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>zero </span><span>=</span><span> np.</span><span>zeros</span><span>((m, m))</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>blocks </span><span>=</span><span> np.</span><span>block</span><span>([[Fm, zero], [zero, Fm]])</span></div></div><div><div><div>21</div></div><div><span>    </span><span># 置换：先取偶数下标，再取奇数下标</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>zeros</span><span>((n, n))</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>P[np.</span><span>arange</span><span>(m), </span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>arange</span><span>(m)] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>P[m </span><span>+</span><span> np.</span><span>arange</span><span>(m), </span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>arange</span><span>(m) </span><span>+</span><span> </span></span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span> B, blocks, P</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>B, blocks, P </span><span>=</span><span> </span><span>split_factors</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"P4 ="</span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"D = diag"</span><span><span>, np.</span><span>round</span><span>(np.</span><span>diag</span><span>(B[:</span></span><span>2</span><span>, </span><span>2</span><span>:]), </span><span>4</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"F4 = B @ blocks @ P："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> blocks </span><span>@</span><span> P, </span><span>fourier</span><span>(</span></span><span>4</span><span>)))</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> [</span><span>8</span><span>, </span><span>64</span><span>]:</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>B, blocks, P </span><span>=</span><span> </span><span>split_factors</span><span>(n)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>ok </span><span>=</span><span> np.</span><span>allclose</span><span>(B </span><span>@</span><span> blocks </span><span>@</span><span> P, </span><span>fourier</span><span>(n))</span></div></div><div><div><div>40</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"F</span><span>{</span><span>n</span><span>}</span><span> = B @ blocks @ P："</span><span>, ok)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span># 零的个数：B 和 P 几乎全是零，blocks 一半是零</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>64</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>B, blocks, P </span><span>=</span><span> </span><span>split_factors</span><span>(n)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>for</span><span> name, M </span><span>in</span><span> [(</span><span>"B"</span><span>, B), (</span><span>"blocks"</span><span>, blocks), (</span><span>"P"</span><span>, P)]:</span></div></div><div><div><div>46</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span> 的非零元个数：</span><span>{</span><span><span>np.</span><span>count_nonzero</span><span>(M)</span></span><span>}</span><span> / </span><span>{</span><span><span>n </span><span>*</span><span> n</span></span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P4 =</span></div></div><div><div><div>2</div></div><div><span>[[1. 0. 0. 0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 0. 1. 0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[0. 1. 0. 0.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[0. 0. 0. 1.]]</span></div></div><div><div><div>6</div></div><div><span>D = diag [1.+0.j 0.+1.j]</span></div></div><div><div><div>7</div></div><div><span>F4 = B @ blocks @ P： True</span></div></div><div><div><div>8</div></div><div><span>F8 = B @ blocks @ P： True</span></div></div><div><div><div>9</div></div><div><span>F64 = B @ blocks @ P： True</span></div></div><div><div><div>10</div></div><div><span>B 的非零元个数：128 / 4096</span></div></div><div><div><div>11</div></div><div><span>blocks 的非零元个数：2048 / 4096</span></div></div><div><div><div>12</div></div><div><span>P 的非零元个数：64 / 4096</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>n=4n = 4</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span> 时 <span><span>D=diag(1,i)D = \text{diag}(1, i)</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span>，<span><span>P4P_4</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 把下标顺序 <span><span>0,1,2,30, 1, 2, 3</span><span><span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span></span></span></span> 变成 <span><span>0,2,1,30, 2, 1, 3</span><span><span><span></span><span>0</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>3</span></span></span></span>。三个尺寸的分解都对得上。再看 <span><span>n=64n = 64</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span> 的非零元：<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 只有 128 个，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 只有 64 个，中间的两份 <span><span>F32F_{32}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 有 <span><span>2×322=20482 \times 32^2 = 2048</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span><span><span>2</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2048</span></span></span></span> 个，正好是 <span><span>642=409664^2 = 4096</span><span><span><span></span><span>6</span><span><span>4</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4096</span></span></span></span> 的一半。置换不用乘法，<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 里真正要乘的只有 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 的 32 个对角元，所以乘一次向量的代价从 4096 次降到 <span><span>2048+32=20802048 + 32 = 2080</span><span><span><span></span><span>2048</span><span></span><span>+</span><span></span></span><span><span></span><span>32</span><span></span><span>=</span><span></span></span><span><span></span><span>2080</span></span></span></span> 次，大约少了一半（<span><span>4096/2080≈1.974096/2080 \approx 1.97</span><span><span><span></span><span>4096/2080</span><span></span><span>≈</span><span></span></span><span><span></span><span>1.97</span></span></span></span>）。</p><p>一半还不算惊人。真正的收益在下一步。</p></section>
<section><h2>一直拆下去：递归与 n log n<a href="#一直拆下去递归与-n-log-n"><span>#</span></a></h2><p><strong>为什么不在这里停下？</strong> 因为中间那两个 <span><span>F32F_{32}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 也可以用同一个公式各自拆成两个 <span><span>F16F_{16}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>16</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，再拆成 <span><span>F8F_8</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>F4F_4</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>F2F_2</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>F1F_1</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他的原话大意是：递归是关键词。每拆一层，中间的块越来越小，而左边多出来的修补因子每层都只花 <span><span>n/2n/2</span><span><span><span></span><span>n</span><span>/2</span></span></span></span> 次乘法，右边多出来的只是置换。从 64 到 1 要拆 <span><span>log⁡264=6\log_2 64 = 6</span><span><span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>64</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span> 层，每层 32 次乘法，总数是 <span><span>6×32=1926 \times 32 = 192</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>32</span><span></span><span>=</span><span></span></span><span><span></span><span>192</span></span></span></span>，也就是 <span><span>12nlog⁡2n\frac{1}{2}n\log_2 n</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>n</span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>n</span></span></span></span>。</p><p>我在这一类问题里总会想起归并排序：一份工作切成两半，各自递归，再花线性的代价合并，<span><span>nlog⁡nn \log n</span><span><span><span></span><span>n</span><span></span><span>lo<span>g</span></span><span></span><span>n</span></span></span></span> 就是这么来的。（类比只用这一次，FFT 的合并步骤是乘 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 再加减，不是比大小。）</p><p>手写递归版本只要十来行。每层把偶数下标和奇数下标分别交给自己，拿回两半结果，乘上 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 的对角元再加减拼起来。我还在里面放了一个计数器，每次乘 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 就加 <span><span>n/2n/2</span><span><span><span></span><span>n</span><span>/2</span></span></span></span>，然后把 <span><span>n=2n = 2</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 到 <span><span>10241024</span><span><span><span></span><span>1024</span></span></span></span> 的计数列成表，和公式 <span><span>n2log⁡2n\frac{n}{2}\log_2 n</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>n</span></span></span></span> 对比：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>fft</span><span>(</span><span>x</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>counter</span></span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""递归 FFT（Strang 约定，指数为正号），counter[0] 累计对 D 的乘法。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(x)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span><span> x.</span><span>astype</span><span>(</span></span><span>complex</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>even </span><span>=</span><span> </span><span>fft</span><span>(x[</span><span>0</span><span>::</span><span>2</span><span>], counter)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>odd </span><span>=</span><span> </span><span>fft</span><span>(x[</span><span>1</span><span>::</span><span>2</span><span>], counter)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> np.</span><span>exp</span><span>(</span><span>2</span><span>j</span><span><span> </span><span>*</span><span> np.pi </span><span>/</span><span> n) </span><span>**</span><span> np.</span><span>arange</span><span>(n </span><span>//</span><span> </span></span><span>2</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>counter[</span><span>0</span><span><span>] </span><span>+=</span><span> n </span><span>//</span><span> </span></span><span>2</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>concatenate</span><span>([even </span><span>+</span><span> d </span><span>*</span><span> odd, even </span><span>-</span><span> d </span><span>*</span><span> odd])</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>64</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>counter </span><span>=</span><span> [</span><span>0</span><span>]</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> </span><span>fft</span><span>(x, counter)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"与 n * ifft 一致："</span><span><span>, np.</span><span>allclose</span><span>(y, </span></span><span>64</span><span><span> </span><span>*</span><span> np.fft.</span><span>ifft</span><span>(x)))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"最大误差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.</span><span>max</span><span>(np.</span><span>abs</span><span>(y </span><span>-</span><span> </span></span><span>64</span><span><span> </span><span>*</span><span> np.fft.</span><span>ifft</span><span>(x)))</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>27</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span>'n'</span><span>:&gt;6</span><span>}</span><span> </span><span>{</span><span>'n^2'</span><span>:&gt;9</span><span>}</span><span> </span><span>{</span><span>'递归计数'</span><span>:&gt;8</span><span>}</span><span> </span><span>{</span><span>'n/2*log2(n)'</span><span>:&gt;12</span><span>}</span><span> </span><span>{</span><span>'比值'</span><span>:&gt;8</span><span>}</span><span>"</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>11</span><span>):</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>n </span><span>=</span><span> </span><span>2</span><span><span>**</span><span>k</span></span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>counter </span><span>=</span><span> [</span><span>0</span><span>]</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>fft</span><span>(rng.</span><span>standard_normal</span><span>(n), counter)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>formula </span><span>=</span><span> n </span><span>//</span><span> </span><span>2</span><span><span> </span><span>*</span><span> k</span></span></div></div><div><div><div>34</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>35</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>n</span><span>:6d</span><span>}</span><span> </span><span>{</span><span><span>n </span><span>*</span><span> n</span></span><span>:9d</span><span>}</span><span> </span><span>{</span><span>counter[</span><span>0</span><span>]</span><span>:8d</span><span>}</span><span> </span><span>{</span><span>formula</span><span>:12d</span><span>}</span><span> "</span></div></div><div><div><div>36</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span><span>n </span><span>*</span><span> n </span><span>/</span><span> counter[</span></span><span>0</span><span>]</span><span>:8.1f</span><span>}</span><span>"</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>与 n * ifft 一致： True</span></div></div><div><div><div>2</div></div><div><span>最大误差： 2.44e-14</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>n       n^2     递归计数  n/2*log2(n)       比值</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>2         4        1            1      4.0</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>4        16        4            4      4.0</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>8        64       12           12      5.3</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>16       256       32           32      8.0</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>32      1024       80           80     12.8</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>64      4096      192          192     21.3</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>128     16384      448          448     36.6</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>256     65536     1024         1024     64.0</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>512    262144     2304         2304    113.8</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>1024   1048576     5120         5120    204.8</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行：手写的 FFT 与 <code>64 * np.fft.ifft(x)</code> 最大差 <span><span>2.44×10−142.44 \times 10^{-14}</span><span><span><span></span><span>2.44</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>，是同一个变换（符号约定同上）。表里“递归计数”与“<span><span>n/2⋅log⁡2nn/2 \cdot \log_2 n</span><span><span><span></span><span>n</span><span>/2</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>n</span></span></span></span>”两列在十个尺寸上逐行相等，包括 <span><span>n=64n = 64</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span> 时的 192。最后一列的比值 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 除以计数，是他在黑板上算的那个数：<span><span>n=1024n = 1024</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>1024</span></span></span></span> 时是 <span><span>1048576/5120=204.81048576 / 5120 = 204.8</span><span><span><span></span><span>1048576/5120</span><span></span><span>=</span><span></span></span><span><span></span><span>204.8</span></span></span></span>。他说的“减少约 200 倍”、“原来做一次变换的时间现在能做 200 次”，就是这行。比值还随 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 增长，<span><span>n=64n = 64</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>64</span></span></span></span> 时 21.3，<span><span>n=1024n = 1024</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>1024</span></span></span></span> 时 204.8，尺寸每翻一倍，收益接近翻一倍。</p><p>需要说明两点，免得读者高估。第一，我数的是“乘 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 的次数”，沿用了他的口径：<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 的第一个元素是 1，其实不必乘；加减法也没算；真实库的常数因子（还有 radix-4、split-radix 这些变种）比这复杂。我只计数，没有计时，所以不敢说“实际快了 200 倍”。第二，这个递归要求 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 是 2 的幂；<span><span>n=6n = 6</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span> 这样的尺寸他提到过 <span><span>F6F_6</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与 <span><span>F3F_3</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的联系，但拆到 <span><span>F3F_3</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就拆不动了；据我所知，实际的库对这种尺寸另有办法，那是 FFT 实现的话题，这一讲没有展开。</p><p>回头看，整件事靠的是 <span><span>FnF_n</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的结构：它的元素全是同一个数的幂，<span><span>w2w^2</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 恰好是更小的 Fourier 矩阵里的数。他在最后一句说得很对，整个想法取决于一个“特别的矩阵”，而不是什么通用技巧。我猜这就是 FFT 能到处用的原因：它不改变任何结果，只是把同一个矩阵换了一种乘法。他顺便提到，据他所知高斯早就注意到了这个分解，只是没有强调（这是他的说法，我没有去查文献）。</p><p>下一讲回到实数世界：什么是正定矩阵，为什么应用里遇到的矩阵多半是它。接着往下读：<a href="/posts/linear-algebra-27-positive-definite-minima/">18.06 第 27 讲：正定矩阵与极小值</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-25-symmetric-positive-definite/</id>
      <title type="text">18.06 第 25 讲：对称矩阵与正定性</title>
      <published>2026-10-18T08:20:00.000Z</published>
      <updated>2026-10-18T08:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-25-symmetric-positive-definite/"/>
      <summary type="text">MIT 18.06 第 25 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 在一个 50 阶随机对称矩阵上只做消元数主元的正负，得到 24 正 26 负，和 24 个正特征值、26 个负特征值完全对得上。</summary>
      <content type="html"><![CDATA[<p>这一讲 Strang 只想让你记住一句话：实对称矩阵的特征值是实数，特征向量可以选成互相垂直，所以 <span><span>A=QΛQTA = Q\Lambda Q^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。顺着这句话他还给了一个很实用的推论：不用解特征值，只做消元，主元的正负个数就等于特征值的正负个数。我在一个 50 阶的随机对称矩阵上试了一下，24 个正主元、26 个负主元，和 24 个正特征值、26 个负特征值一个不差；而用特征多项式求根去算一个 20 阶矩阵的特征值，误差是 <code>eigvalsh</code> 的 <span><span>4.3×10124.3 \times 10^{12}</span><span><span><span></span><span>4.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 倍。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=UCc9q_cAhho" target="_blank">Lecture 25: Symmetric matrices and positive definiteness</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-25-symmetric-matrices-and-positive-definiteness/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §6.3 Symmetric Positive Definite Matrices
前置：<a href="/posts/linear-algebra-24b-quiz-2-review/">第 24b 讲：第二次测验复习</a>
一句话：对称矩阵 <span><span>A=ATA = A^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 有实特征值和一组标准正交的特征向量，<span><span>A=QΛQTA = Q\Lambda Q^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 就是一堆互相垂直的投影矩阵的加权和；主元与特征值的正负个数相同，正定矩阵就是两者全为正的那一类。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几分钟他直接把结论摆出来：特征值是实数，特征向量垂直。接着他在“可以选成垂直”那里停了一下，讲重特征值的情形，值得听。</li>
<li>中间一大段是证明特征值为什么是实数，要在黑板上做复共轭。字幕里这一段很碎（他自己也在中途改口），本篇用文字重述了一遍，建议看视频时只抓住最关键的一步：<span><span>xˉTx\bar{\mathbf{x}}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span> 一定是正数，所以可以约掉。</li>
<li>他把 <span><span>QΛQTQ\Lambda Q^\mathsf{T}</span><span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 按“列乘行”拆开、说每一项是投影矩阵的那几句，和第 15、16 讲连起来了，值得认真听。</li>
<li>主元的正负等于特征值的正负，那一段最实用，也是本篇做得最多的实验。快到结尾时他才给出第一个具体数字例子，<span><span>[5223]\begin{bmatrix}5&amp;2\\2&amp;3\end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，之后是“子行列式全为正”的判据，最后一句说的是整门课在这里汇合。</li>
</ul></div></div>
<section><h2>先看事实：实特征值，垂直的特征向量<a href="#先看事实实特征值垂直的特征向量"><span>#</span></a></h2><p>Strang 开场就先说结论，再回头证明。对称矩阵（real symmetric matrix，<span><span>A=ATA = A^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>）有两条性质：</p><ul>
<li>特征值全是实数。前面几讲里旋转矩阵给出过复特征值，对称矩阵不会这样。</li>
<li>特征向量互相垂直。准确地说是“可以选成互相垂直”：特征值互不相同时，每个特征值对应一条直线，这些直线自然垂直；特征值有重复时，对应的是一整个平面，他的意思是你可以在这个平面里选出垂直的一组。</li>
</ul><p>他举的例子是单位矩阵：它当然对称，特征值全是 1，但每一个向量都是特征向量，所以“垂直”只能是“可以选成垂直”。</p><p>先拿一个随机对称矩阵试一下，看看 NumPy 给出的是什么。<code>np.linalg.eigh</code> 是专为对称矩阵写的：它只读一半矩阵，返回升序排列的实特征值和标准正交的特征向量。我把它和通用的 <code>np.linalg.eig</code> 并排放，再拿一个不对称矩阵当反例：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 实对称矩阵：B + B^T 一定对称</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> B </span><span>+</span><span> B.T</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 对称："</span><span><span>, np.</span><span>allclose</span><span>(A, A.T))</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 一般的 eig：不知道对称，也不保证特征向量正交</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>lam_gen, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"eig 的特征值："</span><span>, lam_gen)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值全是实数："</span><span><span>, np.</span><span>isrealobj</span><span>(lam_gen))</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"||S^T S - I|| ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(S.T </span><span>@</span><span> S </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>4</span><span>))</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># eigh：专为对称矩阵写的，返回升序的实特征值和标准正交的 Q</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>lam, Q </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"eigh 的特征值："</span><span>, lam)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"||Q^T Q - I|| ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q.T </span><span>@</span><span> Q </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>4</span><span>))</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"A = Q Lambda Q^T："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> np.</span><span>diag</span><span>(lam) </span><span>@</span><span> Q.T, A))</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 对照：不对称的矩阵，特征向量一般不正交，特征值还可能是复数</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> B</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>lam_n, S_n </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(N)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"不对称矩阵的特征值："</span><span>, lam_n)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>gram </span><span>=</span><span> S_n.</span><span>conj</span><span>().T </span><span>@</span><span> S_n</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>off </span><span>=</span><span> gram </span><span>-</span><span> np.</span><span>diag</span><span>(np.</span><span>diag</span><span>(gram))</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>32</div></div><div><span>        </span><span>"不对称矩阵的特征向量，最大的非对角内积："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.</span><span>abs</span><span>(off).</span><span>max</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 重特征值：单位矩阵的每个向量都是特征向量，eigh 替我们选了一组正交的</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>lam_i, Q_i </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(np.</span><span>eye</span><span>(</span><span>3</span><span>))</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"单位矩阵的特征值："</span><span>, lam_i)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"单位矩阵 eigh 给出的 Q："</span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(Q_i)</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 对称： True</span></div></div><div><div><div>2</div></div><div><span>eig 的特征值： [ 2.9951  0.2263 -1.9557 -3.5211]</span></div></div><div><div><div>3</div></div><div><span>特征值全是实数： True</span></div></div><div><div><div>4</div></div><div><span>||S^T S - I|| = 6.67e-16</span></div></div><div><div><div>5</div></div><div><span>eigh 的特征值： [-3.5211 -1.9557  0.2263  2.9951]</span></div></div><div><div><div>6</div></div><div><span>||Q^T Q - I|| = 9.19e-16</span></div></div><div><div><div>7</div></div><div><span>A = Q Lambda Q^T： True</span></div></div><div><div><div>8</div></div><div><span>不对称矩阵的特征值： [ 1.3502 -0.1065 -0.8154 -1.5559]</span></div></div><div><div><div>9</div></div><div><span>不对称矩阵的特征向量，最大的非对角内积： 0.4836</span></div></div><div><div><div>10</div></div><div><span>单位矩阵的特征值： [1. 1. 1.]</span></div></div><div><div><div>11</div></div><div><span>单位矩阵 eigh 给出的 Q：</span></div></div><div><div><div>12</div></div><div><span>[[1. 0. 0.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[0. 1. 0.]</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[0. 0. 1.]]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>eig</code> 和 <code>eigh</code> 给出同样的四个特征值，只是顺序不同。两者的特征向量都满足 <span><span>∥QTQ−I∥≈10−16\lVert Q^\mathsf{T}Q - I \rVert \approx 10^{-16}</span><span><span><span></span><span>∥</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>∥</span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，也就是列向量两两正交、长度为 1。这里 <code>eig</code> 恰好也给出了正交的特征向量，是因为四个特征值互不相同，理论上本来就垂直；<code>eigh</code> 的好处是不管有没有重复特征值，它都保证返回正交的一组。</p><p>反例是最后一组：同一个 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 不加 <span><span>BTB^\mathsf{T}</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，不再对称，特征向量之间最大的非对角内积是 0.4836，离 0 差得很远（这次它的特征值碰巧也是实数，只是没有垂直的保证）。重特征值也验证了他的说法：单位矩阵三个特征值都是 1，<code>eigh</code> 返回的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 就是单位矩阵本身，也就是坐标轴。这只是无穷多种标准正交选法里的一种，任何一组标准正交基对单位矩阵都同样成立，所以他才一定要写“可以选成”。</p></section>
<section><h2>为什么特征值是实数<a href="#为什么特征值是实数"><span>#</span></a></h2><p>他花了相当长的时间证明这一点，字幕里他中途多次改口，看得出是边写边想。我把那条线捋成四步。</p><ol>
<li>从 <span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 出发。此刻 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 和 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 都可能是复数。两边取复共轭，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是实矩阵所以没变：<span><span>Axˉ=λˉxˉA\bar{\mathbf{x}} = \bar\lambda\bar{\mathbf{x}}</span><span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>λ</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span></span></span></span>。</li>
<li>对这个式子两边转置，用上 <span><span>AT=AA^\mathsf{T} = A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span></span></span></span>（对称性只用了这一次）：<span><span>xˉTA=λˉxˉT\bar{\mathbf{x}}^\mathsf{T}A = \bar\lambda\bar{\mathbf{x}}^\mathsf{T}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>λ</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。</li>
<li>把这行右乘 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>，得到 <span><span>xˉTAx=λˉ xˉTx\bar{\mathbf{x}}^\mathsf{T}A\mathbf{x} = \bar\lambda\,\bar{\mathbf{x}}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>λ</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span>。另一边，把 <span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 左乘 <span><span>xˉT\bar{\mathbf{x}}^\mathsf{T}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，得到 <span><span>xˉTAx=λ xˉTx\bar{\mathbf{x}}^\mathsf{T}A\mathbf{x} = \lambda\,\bar{\mathbf{x}}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span>。两个式子左边相同，所以 <span><span>λ xˉTx=λˉ xˉTx\lambda\,\bar{\mathbf{x}}^\mathsf{T}\mathbf{x} = \bar\lambda\,\bar{\mathbf{x}}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span>λ</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>λ</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span>。</li>
<li><span><span>xˉTx=∑xˉixi=∑(ai2+bi2)\bar{\mathbf{x}}^\mathsf{T}\mathbf{x} = \sum \bar{x}_i x_i = \sum (a_i^2 + b_i^2)</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>∑</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>∑</span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>：每个 <span><span>xi=ai+biix_i = a_i + b_i\mathrm{i}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span></span></span></span> 乘上自己的共轭，虚部抵消，剩下 <span><span>ai2+bi2a_i^2 + b_i^2</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。只要 <span><span>x≠0\mathbf{x} \ne \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span>，这个数就严格大于 0，可以放心约掉，得到 <span><span>λ=λˉ\lambda = \bar\lambda</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>λ</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span></span></span></span>，所以 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 是实数。</li>
</ol><p><strong>对复向量，正确的“长度平方”是什么？</strong> 是 <span><span>xˉTx\bar{\mathbf{x}}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span>，而不是 <span><span>xTx\mathbf{x}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span>。用后者的话，<span><span>(1,i)(1, \mathrm{i})</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span> 的“长度平方”是 <span><span>1+i2=01 + \mathrm{i}^2 = 0</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，一个非零向量长度为 0，显然不对。这是下一讲（复矩阵）的预告，他在这里顺带点了一句。</p><p>他还顺手把条件推广了一下：如果 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是复矩阵，第二步转置时就应该同时取共轭，对称性要换成 <span><span>AˉT=A\bar A^\mathsf{T} = A</span><span><span><span></span><span><span><span><span><span><span><span></span><span>A</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span></span></span></span>（Hermitian 矩阵），证明照样成立。实矩阵的情形下共轭不起作用，这就退回了 <span><span>A=ATA = A^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。他说 99% 的应用里矩阵都是实的，所以“对称”是关键词。</p></section>
<section><h2>A = QΛQᵀ 与谱分解<a href="#a--qλqᵀ-与谱分解"><span>#</span></a></h2><p>特征向量垂直，还可以把长度缩放成 1，于是特征向量矩阵的列是标准正交的。这时 Strang 说，字母 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 该换成 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>。一般情形的 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 就变成</p><span><span><span>A=QΛQ−1=QΛQTA = Q\Lambda Q^{-1} = Q\Lambda Q^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>因为标准正交列的矩阵满足 <span><span>Q−1=QTQ^{-1} = Q^\mathsf{T}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。他说这是线性代数里最著名的定理之一，叫谱定理（spectral theorem；力学里叫主轴定理，principal axis theorem）。反方向也很显然：任何形如 <span><span>QΛQTQ\Lambda Q^\mathsf{T}</span><span><span><span></span><span>Q</span><span>Λ</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的矩阵，转置之后还是它自己，所以一定对称。这个式子同时把特征值（<span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span> 里的实数）和对称性都摆出来了。</p><p>接下来他用“列乘行”把它拆开，这就是第 3 讲里矩阵乘法的第四种看法：</p><span><span><span>A=λ1q1q1T+λ2q2q2T+⋯+λnqnqnTA = \lambda_1\mathbf{q}_1\mathbf{q}_1^\mathsf{T} + \lambda_2\mathbf{q}_2\mathbf{q}_2^\mathsf{T} + \cdots + \lambda_n\mathbf{q}_n\mathbf{q}_n^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>每一项里的 <span><span>qkqkT\mathbf{q}_k\mathbf{q}_k^\mathsf{T}</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是单位向量乘它自己的转置，也就是第 15 讲里投影到直线的矩阵 <span><span>aaTaTa\dfrac{\mathbf{a}\mathbf{a}^\mathsf{T}}{\mathbf{a}^\mathsf{T}\mathbf{a}}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 在 <span><span>aTa=1\mathbf{a}^\mathsf{T}\mathbf{a} = 1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时的样子。所以每个对称矩阵都是若干个互相垂直的投影矩阵，按特征值加权求和。我就用他后面要用的 <span><span>[5223]\begin{bmatrix}5&amp;2\\2&amp;3\end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 把这件事数值地做一遍。随后再用一个 4 阶随机对称矩阵，按特征值绝对值从大到小一项一项累加，看每加一项误差降多少：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># Strang 后面用的 2 x 2 例子</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>5.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>lam, Q </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, lam, </span><span>"，理论值 4 -+ sqrt(5)："</span><span>, </span><span>4</span><span><span> </span><span>-</span><span> </span></span><span>5</span><span>**</span><span>0.5</span><span>, </span><span>4</span><span><span> </span><span>+</span><span> </span></span><span>5</span><span>**</span><span>0.5</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q ="</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(Q)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 谱分解：A = lambda_1 q1 q1^T + lambda_2 q2 q2^T</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>total </span><span>=</span><span> np.</span><span>zeros_like</span><span>(A)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>):</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> Q[:, k]</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>P </span><span>=</span><span> np.</span><span>outer</span><span>(q, q)</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>total </span><span>+=</span><span> lam[k] </span><span>*</span><span> P</span></div></div><div><div><div>20</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"P</span><span>{</span><span><span>k </span><span>+</span><span> </span></span><span>1}</span><span> = q</span><span>{</span><span><span>k </span><span>+</span><span> </span></span><span>1}</span><span> q</span><span>{</span><span><span>k </span><span>+</span><span> </span></span><span>1}</span><span>^T ="</span><span>)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>print</span><span>(P)</span></div></div><div><div><div>22</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>23</div></div><div><span>            </span><span>"  对称："</span><span>,</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>np.</span><span>allclose</span><span>(P, P.T),</span></div></div><div><div><div>25</div></div><div><span>            </span><span>"，P @ P = P："</span><span>,</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P),</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span>        </span><span>print</span><span>(</span><span>"  迹（也就是秩）："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.</span><span>trace</span><span>(P)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"两项加起来等于 A："</span><span><span>, np.</span><span>allclose</span><span>(total, A))</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"P1 + P2 = 单位矩阵："</span><span>, </span><span>end</span><span>=</span><span>" "</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>np.</span><span>outer</span><span>(Q[:, </span><span>0</span><span>], Q[:, </span><span>0</span><span><span>]) </span><span>+</span><span> np.</span><span>outer</span><span>(Q[:, </span></span><span>1</span><span>], Q[:, </span><span>1</span><span><span>]), np.</span><span>eye</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span># 4 x 4 随机对称矩阵，按特征值绝对值从大到小累加各项，看误差</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> B </span><span>+</span><span> B.T</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>lam4, Q4 </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(M)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>order </span><span>=</span><span> np.</span><span>argsort</span><span>(</span><span>-</span><span>np.</span><span>abs</span><span>(lam4))</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"按绝对值排序的特征值："</span><span>, lam4[order])</span></div></div><div><div><div>44</div></div><div><span>    </span><span>for</span><span> r </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>5</span><span>):</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>approx </span><span>=</span><span> </span><span>sum</span><span><span>(lam4[k] </span><span>*</span><span> np.</span><span>outer</span><span>(Q4[:, k], Q4[:, k]) </span></span><span>for</span><span> k </span><span>in</span><span> order[:r])</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(M </span><span>-</span><span> approx)</span></div></div><div><div><div>47</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"只留 </span><span>{</span><span>r</span><span>}</span><span> 项，||M - 近似|| = </span><span>{</span><span>err</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>特征值： [1.7639 6.2361] ，理论值 4 -+ sqrt(5)： 1.7639320225002102 6.23606797749979</span></div></div><div><div><div>2</div></div><div><span>Q =</span></div></div><div><div><div>3</div></div><div><span>[[ 0.5257 -0.8507]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[-0.8507 -0.5257]]</span></div></div><div><div><div>5</div></div><div><span>P1 = q1 q1^T =</span></div></div><div><div><div>6</div></div><div><span>[[ 0.2764 -0.4472]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[-0.4472  0.7236]]</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>对称： True ，P @ P = P： True</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>迹（也就是秩）： 1.0000</span></div></div><div><div><div>10</div></div><div><span>P2 = q2 q2^T =</span></div></div><div><div><div>11</div></div><div><span>[[0.7236 0.4472]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[0.4472 0.2764]]</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>对称： True ，P @ P = P： True</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>迹（也就是秩）： 1.0000</span></div></div><div><div><div>15</div></div><div><span>两项加起来等于 A： True</span></div></div><div><div><div>16</div></div><div><span>P1 + P2 = 单位矩阵： True</span></div></div><div><div><div>17</div></div><div><span>按绝对值排序的特征值： [-3.5211  2.9951 -1.9557  0.2263]</span></div></div><div><div><div>18</div></div><div><span>只留 1 项，||M - 近似|| = 3.5842</span></div></div><div><div><div>19</div></div><div><span>只留 2 项，||M - 近似|| = 1.9688</span></div></div><div><div><div>20</div></div><div><span>只留 3 项，||M - 近似|| = 0.2263</span></div></div><div><div><div>21</div></div><div><span>只留 4 项，||M - 近似|| = 0.0000</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>特征值 <span><span>4∓5=1.7639,6.23614 \mp \sqrt5 = 1.7639, 6.2361</span><span><span><span></span><span>4</span><span></span><span>∓</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>5</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.7639</span><span>,</span><span></span><span>6.2361</span></span></span></span> 与解析值吻合（等一下会看到这是怎么来的）。两个 <span><span>Pk=qkqkTP_k = \mathbf{q}_k\mathbf{q}_k^\mathsf{T}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都对称，且 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>，迹都是 1，所以都是秩 1 的投影矩阵；两个加起来是单位矩阵，因为 <span><span>QQT=IQ Q^\mathsf{T} = I</span><span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。</p><p>后半段的数很有意思：4 项加完误差是 0；只留 3 项，误差是 0.2263，恰好等于被丢掉的那个特征值的绝对值（最小的特征值 0.2263）。这里的 <span><span>∥⋅∥\lVert\cdot\rVert</span><span><span><span></span><span>∥</span><span>⋅</span><span>∥</span></span></span></span> 是 Frobenius 范数，丢掉一项后剩下的就是 <span><span>λ qqT\lambda\,\mathbf{q}\mathbf{q}^\mathsf{T}</span><span><span><span></span><span>λ</span><span></span><span>q</span><span><span>q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，它的范数就是 <span><span>∣λ∣\lvert\lambda\rvert</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span></span></span></span>。再往前看，留 2 项误差 1.9688，留 1 项 3.5842，每一项都在“拆掉”一个方向。这是低秩近似的最朴素版本，等到第 29 讲 SVD 会再碰到它。</p></section>
<section><h2>主元的正负就是特征值的正负<a href="#主元的正负就是特征值的正负"><span>#</span></a></h2><p>接下来他提了一个很具体的问题：知道特征值是实数之后，下一步就是问它们是正还是负。比如微分方程里，正负决定了稳定还是发散。</p><p><strong>怎么知道正负，又不必真的把特征值算出来？</strong> 他先把“算特征值”这件事说得很不客气：一个 50 阶的矩阵，手算要花一辈子；让 MATLAB 去算 <span><span>det⁡(A−λI)\det(A - \lambda I)</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span></span></span></span> 这个 50 次多项式再求根，它虽然会算，但会抱怨，因为这办法数值上很不稳定。他还承认，这正是他教过的做法，其实只适合二阶和三阶。他说的不稳定，我真的想看看到底有多糟，所以做了一个实验：构造一个特征值恰好是 <span><span>1,2,…,201, 2, \dots, 20</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>20</span></span></span></span> 的 20 阶对称矩阵，一种做法是先算特征多项式的系数再求根，另一种是直接 <code>eigvalsh</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 20 阶对称矩阵，特征值取 1, 2, ..., 20，特征向量是一个固定的正交矩阵</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>Q, _ </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(rng.</span><span>standard_normal</span><span>((</span><span>20</span><span>, </span><span>20</span><span>)))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>true </span><span>=</span><span> np.</span><span>arange</span><span>(</span><span>1.0</span><span>, </span><span>21.0</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> Q </span><span>@</span><span> np.</span><span>diag</span><span>(true) </span><span>@</span><span> Q.T</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> (A </span><span>+</span><span> A.T) </span><span>/</span><span> </span><span>2</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 老办法：先算 det(A - lambda I) 的系数，再解多项式的根</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>coeffs </span><span>=</span><span> np.</span><span>poly</span><span>(A)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>roots </span><span>=</span><span> np.</span><span>sort</span><span>(np.</span><span>roots</span><span>(coeffs))</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>err_roots </span><span>=</span><span> np.</span><span>max</span><span>(np.</span><span>abs</span><span>(roots </span><span>-</span><span> true))</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 新办法：直接做对称特征值分解</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>err_eigh </span><span>=</span><span> np.</span><span>max</span><span>(np.</span><span>abs</span><span>(np.linalg.</span><span>eigvalsh</span><span>(A) </span><span>-</span><span> true))</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"多项式系数的量级："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.</span><span>abs</span><span>(coeffs).</span><span>max</span><span>()</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"求根得到的特征值，最大误差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>err_roots</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"eigvalsh 得到的特征值，最大误差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>err_eigh</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"两者误差之比："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>err_roots </span><span>/</span><span> err_eigh</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>多项式系数的量级： 1.38e+19</span></div></div><div><div><div>2</div></div><div><span>求根得到的特征值，最大误差： 5.40e-02</span></div></div><div><div><div>3</div></div><div><span>eigvalsh 得到的特征值，最大误差： 1.24e-14</span></div></div><div><div><div>4</div></div><div><span>两者误差之比： 4.3e+12</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>多项式系数最大有 <span><span>1.38×10191.38 \times 10^{19}</span><span><span><span></span><span>1.38</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>19</span></span></span></span></span></span></span></span></span></span></span></span>，已经大到双精度的 16 位有效数字不够用了；求根得到的特征值最大误差是 0.054，而 <code>eigvalsh</code> 的误差是 <span><span>1.24×10−141.24 \times 10^{-14}</span><span><span><span></span><span>1.24</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>，相差 <span><span>4.3×10124.3 \times 10^{12}</span><span><span><span></span><span>4.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 倍。这还是 20 阶，不是他说的 50 阶，我猜 50 阶的结果会更夸张，但我没有跑，所以不下结论。（教材里很少见作者当面承认自己教过的方法不能用，我挺喜欢这一句。）</p><p>那“稳定又快”的办法是什么？他说是消元。<strong>对称矩阵的主元的正负个数，等于特征值的正负个数。</strong> 主元不是特征值，数值也完全不一样，但符号数一致。他在这里还补了一个很自然的连接：主元之积等于行列式（没有换行的话），特征值之积也等于行列式，所以两者的乘积相等，但这对 50 个数各自的正负没有告诉你什么，上面那条才是真正有信息的。</p><p>他还想到了一个办法利用这点：想知道有多少特征值大于 7，就把矩阵平移，<span><span>A−7IA - 7I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>7</span><span>I</span></span></span></span>，特征值整体减 7，再数正主元（数负主元得到的是小于 7 的个数）。我写了一个不换行的消元函数，在一个 50 阶随机对称矩阵上，对 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>、<span><span>A−7IA - 7I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>7</span><span>I</span></span></span></span>、<span><span>A−10IA - 10I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>10</span><span>I</span></span></span></span> 分别数负主元，对比 <code>eigvalsh</code> 算出的特征值：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>pivots</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""不换行的消元，返回主元。遇到 0 主元就报错。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> U.shape[</span><span>0</span><span>]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>out </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> </span><span>abs</span><span><span>(U[k, k]) </span><span>&lt;</span><span> </span></span><span>1e-12</span><span>:</span></div></div><div><div><div>11</div></div><div><span>            </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>f</span><span>"第 </span><span>{</span><span>k</span><span>}</span><span> 个主元是 0，需要换行"</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>out[k] </span><span>=</span><span> U[k, k]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(k </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>U[i, k:] </span><span>-=</span><span> U[i, k] </span><span>/</span><span> U[k, k] </span><span>*</span><span> U[k, k:]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span> out</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># Strang 说的 50 阶：随机对称矩阵，数主元的正负和特征值的正负</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>50</span><span>, </span><span>50</span><span>))</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> B </span><span>+</span><span> B.T</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> </span><span>pivots</span><span>(A)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> np.linalg.</span><span>eigvalsh</span><span>(A)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"正主元个数："</span><span><span>, np.</span><span>sum</span><span>(d </span><span>&gt;</span><span> </span></span><span>0</span><span>), </span><span>"，负主元个数："</span><span><span>, np.</span><span>sum</span><span>(d </span><span>&lt;</span><span> </span></span><span>0</span><span>))</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>29</div></div><div><span>        </span><span>"正特征值个数："</span><span><span>, np.</span><span>sum</span><span>(lam </span><span>&gt;</span><span> </span></span><span>0</span><span>), </span><span>"，负特征值个数："</span><span><span>, np.</span><span>sum</span><span>(lam </span><span>&lt;</span><span> </span></span><span>0</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 主元之积 = 特征值之积 = 行列式（用对数避免溢出）</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>sign, logdet </span><span>=</span><span> np.linalg.</span><span>slogdet</span><span>(A)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>35</div></div><div><span>        </span><span>"主元之积的符号和 log|.|："</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>np.</span><span>prod</span><span>(np.</span><span>sign</span><span>(d)),</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>np.</span><span>sum</span><span>(np.</span><span>log</span><span>(np.</span><span>abs</span><span>(d))),</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>40</div></div><div><span>        </span><span>"特征值之积的符号和 log|.|："</span><span>,</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>np.</span><span>prod</span><span>(np.</span><span>sign</span><span>(lam)),</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>np.</span><span>sum</span><span>(np.</span><span>log</span><span>(np.</span><span>abs</span><span>(lam))),</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"slogdet："</span><span>, sign, logdet)</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>    </span><span># 平移：A - 7I 的负主元个数 = A 的小于 7 的特征值个数</span></div></div><div><div><div>47</div></div><div><span>    </span><span>for</span><span> shift </span><span>in</span><span> [</span><span>0.0</span><span>, </span><span>7.0</span><span>, </span><span>10.0</span><span>]:</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>d_s </span><span>=</span><span> </span><span>pivots</span><span>(A </span><span>-</span><span> shift </span><span>*</span><span> np.</span><span>eye</span><span>(</span><span>50</span><span>))</span></div></div><div><div><div>49</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>50</div></div><div><span>            </span><span>f</span><span>"平移 </span><span>{</span><span>shift</span><span>}</span><span>：负主元 </span><span>{</span><span><span>np.</span><span>sum</span><span>(d_s </span><span>&lt;</span><span> </span></span><span>0</span><span>)</span><span>}</span><span> 个，"</span><span>,</span></div></div><div><div><div>51</div></div><div><span>            </span><span>f</span><span>"小于 </span><span>{</span><span>shift</span><span>}</span><span> 的特征值 </span><span>{</span><span><span>np.</span><span>sum</span><span>(lam </span><span>&lt;</span><span> shift)</span></span><span>}</span><span> 个"</span><span>,</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>正主元个数： 24 ，负主元个数： 26</span></div></div><div><div><div>2</div></div><div><span>正特征值个数： 24 ，负特征值个数： 26</span></div></div><div><div><div>3</div></div><div><span>主元之积的符号和 log|.|： 1.0 91.5130695575681</span></div></div><div><div><div>4</div></div><div><span>特征值之积的符号和 log|.|： 1.0 91.51306955756814</span></div></div><div><div><div>5</div></div><div><span>slogdet： 1.0 91.51306955756817</span></div></div><div><div><div>6</div></div><div><span>平移 0.0：负主元 26 个， 小于 0.0 的特征值 26 个</span></div></div><div><div><div>7</div></div><div><span>平移 7.0：负主元 35 个， 小于 7.0 的特征值 35 个</span></div></div><div><div><div>8</div></div><div><span>平移 10.0：负主元 40 个， 小于 10.0 的特征值 40 个</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>24 个正主元、26 个负主元对 24 个正特征值、26 个负特征值；平移 7 以后负主元 35 个，小于 7 的特征值也是 35 个；平移 10 以后是 40 对 40。三个平移全都对得上。主元之积的对数是 91.5131，与特征值之积和 <code>slogdet</code> 在小数点后 13 位以内一致（用对数是因为乘 50 个数会溢出到很大的量级）。</p><p>注意这个函数遇到 0 主元就直接报错，没有处理换行。随机矩阵碰到 0 主元的概率是 0，所以这里没事。严格说，主元的正负个数等于特征值的正负个数这个结论，需要消元过程不需要换行，我没有去验证需要换行时它会怎样。</p></section>
<section><h2>正定矩阵<a href="#正定矩阵"><span>#</span></a></h2><p>最后几分钟他开始讲正定（positive definite）。定义很自然：对称矩阵里，特征值全为正的那一类。对称矩阵好，正定矩阵“excellent”（字幕原话），他还说下一节的教材就是讲这个。</p><p>测试办法有两个：</p><ul>
<li>看主元：因为主元的符号和特征值的符号个数一致，所以所有特征值为正等价于所有主元为正。</li>
<li>看左上角的子行列式：<span><span>1×11 \times 1</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span>、<span><span>…\dots</span><span><span><span></span><span>…</span></span></span></span>、<span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的左上角行列式全为正。只看整个矩阵的行列式不够，因为两个负特征值乘起来也是正的。</li>
</ul><p>他的例子是 <span><span>A=[5223]A = \begin{bmatrix}5&amp;2\\2&amp;3\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>：第一个主元 5，行列式 11，所以第二个主元是 <span><span>11/511/5</span><span><span><span></span><span>11/5</span></span></span></span>，都是正的，所以特征值也都是正的。特征值是 <span><span>λ2−8λ+11=0\lambda^2 - 8\lambda + 11 = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>8</span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>11</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的根（8 是迹，11 是行列式），也就是 <span><span>4±54 \pm \sqrt5</span><span><span><span></span><span>4</span><span></span><span>±</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>5</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，他说二阶的根不算太糟，但也谈不上漂亮，主元就简单多了。</p><p>他又给了一个反例，字幕里只读出了数字“minus one minus three”，没有写出整个矩阵。我按“整个矩阵的行列式是 3”“主元和特征值也都是负的”这两句话，猜是 <span><span>diag(−1,−3)\mathrm{diag}(-1, -3)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>3</span><span>)</span></span></span></span>。如果猜错了，只影响这一个反例，不影响结论。它的行列式 <span><span>=3&gt;0= 3 &gt; 0</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>，但第一个子行列式 <span><span>−1&lt;0-1 &lt; 0</span><span><span><span></span><span>−</span><span>1</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以通不过测试，而这就是“只看总行列式不够”的原因：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>5.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span>    </span><span># 字幕里读出来的是 “-1 -3”，我猜是对角矩阵 diag(-1, -3)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>]])</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> name, M </span><span>in</span><span> [(</span><span>"A"</span><span>, A), (</span><span>"D"</span><span>, D)]:</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>piv </span><span>=</span><span> [M[</span><span>0</span><span>, </span><span>0</span><span><span>], np.linalg.</span><span>det</span><span>(M) </span><span>/</span><span> M[</span></span><span>0</span><span>, </span><span>0</span><span>]]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span> 的主元："</span><span><span>, np.</span><span>array</span><span>(piv))</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>15</div></div><div><span>            </span><span>f</span><span>"  左上角子行列式：</span><span>{</span><span>M[</span><span>0</span><span>, </span><span>0</span><span>]</span><span>:.1f</span><span>}</span><span>，</span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(M)</span></span><span>:.1f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>16</div></div><div><span>            </span><span>"；特征值："</span><span>,</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>np.linalg.</span><span>eigvalsh</span><span>(M),</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>ok </span><span>=</span><span> M[</span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>&gt;</span><span> </span></span><span>0</span><span> </span><span>and</span><span><span> np.linalg.</span><span>det</span><span>(M) </span><span>&gt;</span><span> </span></span><span>0</span></div></div><div><div><div>20</div></div><div><span>        </span><span>print</span><span>(</span><span>"  所有子行列式为正："</span><span>, ok)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># A 的特征值公式：4 加减 sqrt(5)，来自 lambda^2 - 8 lambda + 11 = 0</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的迹和行列式："</span><span><span>, np.</span><span>trace</span><span>(A), </span></span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(A), </span></span><span>6</span><span>))</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"第二个主元 11/5 ="</span><span>, </span><span>11</span><span><span> </span><span>/</span><span> </span></span><span>5</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"lambda^2 - 8 lambda + 11 的根："</span><span><span>, np.</span><span>roots</span><span>([</span></span><span>1.0</span><span><span>, </span><span>-</span></span><span>8.0</span><span>, </span><span>11.0</span><span>]))</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 对称，且所有特征值为正时，x^T A x 对任意非零 x 都为正</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>100000</span><span>))</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>q_A </span><span>=</span><span> np.</span><span>einsum</span><span>(</span><span>"ij,ij-&gt;j"</span><span><span>, X, A </span><span>@</span><span> X)</span></span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>q_D </span><span>=</span><span> np.</span><span>einsum</span><span>(</span><span>"ij,ij-&gt;j"</span><span><span>, X, D </span><span>@</span><span> X)</span></span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"A：10 万个随机 x 里 x^T A x &gt; 0 的个数："</span><span><span>, np.</span><span>sum</span><span>(q_A </span><span>&gt;</span><span> </span></span><span>0</span><span>))</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"D：10 万个随机 x 里 x^T D x &gt; 0 的个数："</span><span><span>, np.</span><span>sum</span><span>(q_D </span><span>&gt;</span><span> </span></span><span>0</span><span>))</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 的主元： [5.  2.2]</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>左上角子行列式：5.0，11.0 ；特征值： [1.7639 6.2361]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>所有子行列式为正： True</span></div></div><div><div><div>4</div></div><div><span>D 的主元： [-1. -3.]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>左上角子行列式：-1.0，3.0 ；特征值： [-3. -1.]</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>所有子行列式为正： False</span></div></div><div><div><div>7</div></div><div><span>A 的迹和行列式： 8.0 11.0</span></div></div><div><div><div>8</div></div><div><span>第二个主元 11/5 = 2.2</span></div></div><div><div><div>9</div></div><div><span>lambda^2 - 8 lambda + 11 的根： [6.2361 1.7639]</span></div></div><div><div><div>10</div></div><div><span>A：10 万个随机 x 里 x^T A x &gt; 0 的个数： 100000</span></div></div><div><div><div>11</div></div><div><span>D：10 万个随机 x 里 x^T D x &gt; 0 的个数： 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的主元是 5 和 2.2，两个子行列式 5.0 和 11.0，特征值 1.7639 和 6.2361，三种判据一致。<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 的行列式是 3.0，是个正数，但主元 <span><span>−1,−3-1, -3</span><span><span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>3</span></span></span></span> 和特征值 <span><span>−3,−1-3, -1</span><span><span><span></span><span>−</span><span>3</span><span>,</span><span></span><span>−</span><span>1</span></span></span></span> 都是负的，子行列式 <span><span>−1.0-1.0</span><span><span><span></span><span>−</span><span>1.0</span></span></span></span> 没过关。</p><p>最后两行是后面几讲才系统讲的：正定矩阵 <span><span>xTAxx^\mathsf{T}Ax</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span> 对任意非零 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 都是正的。我随手取 10 万个随机向量：对 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 全是正的（100000 个），对 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 一个都不是（0 个）。（<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是个负定矩阵，所以结果是 0 而不是“一半一半”，这个反例选得有点极端，我猜他只是想要一个行列式为正的坏例子。）这个二次型的几何意义，要等到第 27 讲才有，这里先放着。</p><p>整门课到这里，三条线汇合了：第一周的主元、中间的行列式、最近的特征值，对于对称矩阵来说，它们在正定这件事上给出的是同一个答案。他说这是整门课开始汇合的地方，下一讲和下下一讲会继续，最后的高潮是对一般的 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵把这些东西合起来。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>协方差矩阵是这一讲里最常碰到的对称矩阵，也恰好是“半正定”的最自然来源。卡尔曼滤波里的状态协方差 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span>（见 <a href="/posts/kalman-filter-02-multivariate/">升到高维：多变量卡尔曼滤波的推导与实现</a>）就是一个对称半正定矩阵：沿着任意方向 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 的方差是 <span><span>xTPx\mathbf{x}^\mathsf{T}P\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>P</span><span>x</span></span></span></span>，方差不会是负数，所以特征值不会是负数。</p><p>我用一组相关的二维样本把它数值地看一遍：协方差矩阵是对称的，特征值是沿主轴的方差，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的列是主轴方向，换到主轴坐标之后协方差变成对角阵，两个分量不再相关。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 两个强相关的测量量：第一个的标准差约 2，第二个与它正相关</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>mix </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.2</span><span>, </span><span>0.5</span><span>]])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>samples </span><span>=</span><span> (mix </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>5000</span><span>))).T</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>cov</span><span>(samples, </span><span>rowvar</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"样本协方差 C ="</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(C)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"C 对称："</span><span><span>, np.</span><span>allclose</span><span>(C, C.T))</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>lam, Q </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(C)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值（沿主轴的方差）："</span><span>, lam)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"最小特征值非负："</span><span><span>, lam.</span><span>min</span><span>() </span><span>&gt;=</span><span> </span></span><span>0</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"主轴方向（Q 的列）："</span><span>)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(Q)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 换到主轴坐标：协方差变成对角阵，两个分量不再相关</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>rotated </span><span>=</span><span> samples </span><span>@</span><span> Q</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>C_rot </span><span>=</span><span> np.</span><span>cov</span><span>(rotated, </span><span>rowvar</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"主轴坐标下的协方差："</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(C_rot)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"对角阵 Lambda："</span><span><span>, np.</span><span>allclose</span><span>(C_rot, np.</span><span>diag</span><span>(lam)))</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 任意方向 x 上的方差 x^T C x 等于投影后样本的方差，所以不会是负的</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>]) </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>2.0</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"x^T C x ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>x </span><span>@</span><span> C </span><span>@</span><span> x</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"投影后样本的方差 ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.</span><span>var</span><span>(samples </span><span>@</span><span> x, </span></span><span>ddof</span><span>=</span><span>1</span><span>)</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>样本协方差 C =</span></div></div><div><div><div>2</div></div><div><span>[[4.0089 2.3967]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[2.3967 1.6811]]</span></div></div><div><div><div>4</div></div><div><span>C 对称： True</span></div></div><div><div><div>5</div></div><div><span>特征值（沿主轴的方差）： [0.1807 5.5094]</span></div></div><div><div><div>6</div></div><div><span>最小特征值非负： True</span></div></div><div><div><div>7</div></div><div><span>主轴方向（Q 的列）：</span></div></div><div><div><div>8</div></div><div><span>[[ 0.5306 -0.8476]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[-0.8476 -0.5306]]</span></div></div><div><div><div>10</div></div><div><span>主轴坐标下的协方差：</span></div></div><div><div><div>11</div></div><div><span>[[ 0.1807 -0.    ]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[-0.      5.5094]]</span></div></div><div><div><div>13</div></div><div><span>对角阵 Lambda： True</span></div></div><div><div><div>14</div></div><div><span>x^T C x = 0.4483</span></div></div><div><div><div>15</div></div><div><span>投影后样本的方差 = 0.4483</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个特征值 0.1807 和 5.5094 都非负；换到主轴坐标后，非对角元素显示成 <code>-0.</code>（舍入后的零），协方差成了 <span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span>。最后两行验证了“沿任意方向的方差就是 <span><span>xTCx\mathbf{x}^\mathsf{T}C\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>x</span></span></span></span>”：沿 <span><span>(1,−1)/2(1, -1)/\sqrt2</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span><span>/</span><span><span><span><span><span><span></span><span>2</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 方向投影的样本方差和二次型都是 0.4483。这个方向离长轴很远，所以方差远小于最大特征值 5.5094。滤波里的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 画成椭圆，椭圆的主轴就是 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的列，半轴长和 <span><span>λ\sqrt{\lambda}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>λ</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 成正比。</p><p>我猜，实现卡尔曼滤波时，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 在多次更新后因为舍入误差失去严格对称，或者出现一个很小的负特征值，是很常见的问题，这也是为什么很多实现要显式写 <span><span>P←(P+PT)/2P \leftarrow (P + P^\mathsf{T})/2</span><span><span><span></span><span>P</span><span></span><span>←</span><span></span></span><span><span></span><span>(</span><span>P</span><span></span><span>+</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span>/2</span></span></span></span> 或者用 Joseph 形式。我没有在这一篇里验证这一点，细节留给那一篇。</p><p>下一讲 Strang 要把这一讲里露了一下头的复数讲透：复向量的内积、Hermitian 矩阵、Fourier 矩阵，以及把 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 次乘法砍到 <span><span>n2log⁡2n\frac{n}{2}\log_2 n</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>n</span></span></span></span> 的快速 Fourier 变换。见 <a href="/posts/linear-algebra-26-complex-matrices-fft/">18.06 第 26 讲：复矩阵与快速 Fourier 变换</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-24b-quiz-2-review/</id>
      <title type="text">18.06 第 24b 讲：第二次测验复习</title>
      <published>2026-10-18T08:00:00.000Z</published>
      <updated>2026-10-18T08:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-24b-quiz-2-review/"/>
      <summary type="text">MIT 18.06 第 24b 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 Strang 在复习课上讲的 1984 年期中题与 1992 年期末题逐题重算了一遍，三对角行列式的递推矩阵 M 的六次幂恰好是单位矩阵，4 阶那个矩阵的行列式是 9。</summary>
      <content type="html"><![CDATA[<p>这一讲没有新东西，Strang 拿两份旧试卷（1984 年的期中，1992 年的期末）把第四到六章串了一遍：投影、最小二乘、Gram-Schmidt、行列式、特征值。我把他黑板上的每道题都交给 NumPy 重算，最有意思的两个结果是：一个投影矩阵作用一次之后，迭代 <span><span>uk+1=Puk\mathbf{u}_{k+1} = P\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就再也不动了，停在 <span><span>(6,3,6)(6, 3, 6)</span><span><span><span></span><span>(</span><span>6</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>；一个看起来像 Fibonacci 的递推，递推矩阵的六次幂恰好是单位矩阵，周期是 6。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=QuZL5IKpO_U" target="_blank">Lecture 24b: Quiz 2 review</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-24b-quiz-2-review/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 第 4 到 6 章
前置：<a href="/posts/linear-algebra-24-markov-fourier/">第 24 讲：Markov 矩阵与 Fourier 级数</a>
一句话：投影矩阵、特征值、行列式和差分方程不是四个话题，而是同一个矩阵的四种问法，一道题常常把它们连着问。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几分钟他把第二部分的内容列了一遍（<span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>、投影、最小二乘、Gram-Schmidt，行列式的十条性质，特征值与矩阵的幂）。这一段值得听，它是一份考点清单，也是本篇小节顺序的依据。</li>
<li>投影、拟合直线和 Gram-Schmidt 三题他讲得很快，本篇都用代码算过，可以倍速。但“最小二乘有两幅图”的那一段要认真听：一幅在 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span>-<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 平面里，一幅在 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 所在的空间里。</li>
<li>行列式递推那一题（从余子式到一阶系统，再到复特征值）是整堂课的重头戏，值得完整看一遍。</li>
<li>结尾他猜“奇数阶的奇异，偶数阶的可逆”，本篇最后一节用代码验证了。</li>
</ul></div></div>
<section><h2>先看清考什么<a href="#先看清考什么"><span>#</span></a></h2><p>开头他把范围列得很清楚。第四章是正交：<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的列是标准正交向量，写成矩阵语言就是 <span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>；然后是投影到直线和子空间，用投影解无解的 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>（最小二乘），最后是 Gram-Schmidt，把一组无关向量变成标准正交基。第五章是行列式：性质 1 到 3 定义了行列式，性质 4 到 10 是推论，再加上有 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 项的大公式、余子式公式，以及由此得到的逆矩阵公式。第六章只考到 6.1 和 6.2，也就是特征值、<span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 的机械步骤、对角化与矩阵的幂。微分方程（教材第 6 版 §6.5，对应视频里说的 6.3）留给第三次测验。</p><p>他说他复习的办法一般是拿一份旧试卷，挑出有代表性的题，边写边讲。这份试卷是 1984 年 11 月的，“那时候连 Web 都没有”。我就按他黑板上的顺序走。（这篇没有新定理，我的贡献是把他心算的几十个数交给 NumPy 再核对一遍，相当于给黑板配了一台计算器。）</p></section>
<section><h2>投影矩阵、特征值和差分方程<a href="#投影矩阵特征值和差分方程"><span>#</span></a></h2><p>第一题给了 <span><span>a=(2,1,2)\mathbf{a} = (2, 1, 2)</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，求投影到过 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 的直线上的矩阵 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span>。直线情形下一般公式 <span><span>A(ATA)−1ATA(A^TA)^{-1}A^T</span><span><span><span></span><span>A</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 里的 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 只有一列，<span><span>ATAA^TA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 是一个数 <span><span>aTa=9\mathbf{a}^T\mathbf{a} = 9</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>9</span></span></span></span>，所以</p><span><span><span>P=aaTaTa=19[424212424]P = \frac{\mathbf{a}\mathbf{a}^T}{\mathbf{a}^T\mathbf{a}}
= \frac{1}{9}
\begin{bmatrix} 4 &amp; 2 &amp; 4 \\ 2 &amp; 1 &amp; 2 \\ 4 &amp; 2 &amp; 4 \end{bmatrix}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>接着他一路追问：秩是多少？列空间是什么？特征值是什么？答案分别是 1、这条直线、<span><span>0,0,10, 0, 1</span><span><span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span></span></span></span>。秩 1 意味着零空间是二维的，所以特征值 0 重复两次；第三个特征值从迹得到，<span><span>tr⁡P=9/9=1\operatorname{tr} P = 9/9 = 1</span><span><span><span></span><span><span>tr</span></span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>9/9</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>；特征值 1 对应的特征向量是 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 本身，因为 <span><span>Pa=aP\mathbf{a} = \mathbf{a}</span><span><span><span></span><span>P</span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span></span></span></span>（不动的向量）。</p><p>最后一问是解 <span><span>uk+1=Puk\mathbf{u}_{k+1} = P\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>u0=(9,9,0)\mathbf{u}_0 = (9, 9, 0)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>9</span><span>,</span><span></span><span>9</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。他直接算 <span><span>u1=Pu0\mathbf{u}_1 = P\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：<span><span>aTu0=27\mathbf{a}^T\mathbf{u}_0 = 27</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>27</span></span></span></span>，除以 <span><span>aTa=9\mathbf{a}^T\mathbf{a} = 9</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>9</span></span></span></span> 得 3，所以 <span><span>u1=3a=(6,3,6)\mathbf{u}_1 = 3\mathbf{a} = (6, 3, 6)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>6</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>。已经在直线上的点再投影一次不会动，所以 <span><span>uk\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 全是 <span><span>(6,3,6)(6, 3, 6)</span><span><span><span></span><span>(</span><span>6</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>outer</span><span>(a, a) </span><span>/</span><span> (a </span><span>@</span><span> a)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>print</span><span>(</span><span>"9 P ="</span><span>)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>9</span><span><span> </span><span>*</span><span> P)</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ P == P："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P))</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"P.T == P："</span><span><span>, np.</span><span>allclose</span><span>(P.T, P))</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(P), </span></span><span>"，迹："</span><span><span>, np.</span><span>trace</span><span>(P))</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 特征值：两个 0 和一个 1，特征值 1 的特征向量是 a 本身</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvalsh</span><span>(P)).</span><span>round</span><span>(</span></span><span>8</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ a ="</span><span><span>, P </span><span>@</span><span> a)</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 差分方程 u(k+1) = P u(k)，u0 = (9, 9, 0)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>u0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>9.0</span><span>, </span><span>9.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> u0</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>4</span><span>):</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> P </span><span>@</span><span> u</span></div></div><div><div><div>23</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"u</span><span>{</span><span>k</span><span>}</span><span> ="</span><span>, u)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 对照：直接算 P^10 @ u0</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"P^10 @ u0 ="</span><span><span>, np.linalg.</span><span>matrix_power</span><span>(P, </span></span><span>10</span><span><span>) </span><span>@</span><span> u0)</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>9 P =</span></div></div><div><div><div>2</div></div><div><span>[[4. 2. 4.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[2. 1. 2.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[4. 2. 4.]]</span></div></div><div><div><div>5</div></div><div><span>P @ P == P： True</span></div></div><div><div><div>6</div></div><div><span>P.T == P： True</span></div></div><div><div><div>7</div></div><div><span>秩： 1 ，迹： 1.0</span></div></div><div><div><div>8</div></div><div><span>特征值： [0. 0. 1.]</span></div></div><div><div><div>9</div></div><div><span>P @ a = [2. 1. 2.]</span></div></div><div><div><div>10</div></div><div><span>u1 = [6. 3. 6.]</span></div></div><div><div><div>11</div></div><div><span>u2 = [6. 3. 6.]</span></div></div><div><div><div>12</div></div><div><span>u3 = [6. 3. 6.]</span></div></div><div><div><div>13</div></div><div><span>P^10 @ u0 = [6. 3. 6.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出里最要紧的是 <span><span>u1\mathbf{u}_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 到 <span><span>u3\mathbf{u}_3</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 完全一样，和直接算 <span><span>P10u0P^{10}\mathbf{u}_0</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的结果也一样。对投影矩阵来说 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>，所以不用做特征分解。他特意提醒：考试里这道题很可能换成一个不是投影的矩阵，那时就老老实实走三步，找特征值、找特征向量、解出 <span><span>u0=c1x1+c2x2+c3x3\mathbf{u}_0 = c_1\mathbf{x}_1 + c_2\mathbf{x}_2 + c_3\mathbf{x}_3</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里的系数 <span><span>cic_i</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，然后 <span><span>uk=c1λ1kx1+⋯\mathbf{u}_k = c_1\lambda_1^k\mathbf{x}_1 + \cdots</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span></span></span></span>。投影矩阵的两个 0 特征值把另外两项直接扔掉，只剩 <span><span>c3x3c_3\mathbf{x}_3</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p></section>
<section><h2>过原点的直线与 Gram-Schmidt<a href="#过原点的直线与-gram-schmidt"><span>#</span></a></h2><p>第二题拟合经过原点的直线 <span><span>y=Dty = Dt</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>D</span><span>t</span></span></span></span>，三个点是 <span><span>(1,4)(1, 4)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>、<span><span>(2,5)(2, 5)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span>、<span><span>(3,8)(3, 8)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span>。只有一个未知数 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span>，所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 只有一列：</p><span><span><span>[123]D=[458]\begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} D
= \begin{bmatrix} 4 \\ 5 \\ 8 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>正规方程 <span><span>ATAD^=ATbA^TA\hat{D} = A^T\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>D</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 是 <span><span>14D^=3814\hat{D} = 38</span><span><span><span></span><span>14</span><span><span><span><span><span><span></span><span>D</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>38</span></span></span></span>，所以 <span><span>D^=38/14≈2.7143\hat{D} = 38/14 \approx 2.7143</span><span><span><span></span><span><span><span><span><span><span></span><span>D</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>38/14</span><span></span><span>≈</span><span></span></span><span><span></span><span>2.7143</span></span></span></span>。他接着问了一个我觉得最像考试陷阱的问题：刚才到底把什么向量投影到了什么上面？答案是，在 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span>-<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 平面的那张图里什么都没有被投影；投影发生在 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 所在的三维空间里，把 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 投到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间（这里是一条线）上。</p><p>同一道大题的最后一问是 Gram-Schmidt：<span><span>a1=(1,2,3)\mathbf{a}_1 = (1, 2, 3)</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，<span><span>a2=(1,1,1)\mathbf{a}_2 = (1, 1, 1)</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，找这个平面里两个正交的向量。保留 <span><span>a1\mathbf{a}_1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，把 <span><span>a2\mathbf{a}_2</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里沿 <span><span>a1\mathbf{a}_1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的部分减掉：<span><span>B=a2−614a1\mathbf{B} = \mathbf{a}_2 - \frac{6}{14}\mathbf{a}_1</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>6</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。题目只要正交，不要单位化，所以不用开方。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 过原点的直线 y = D t，三个点 (1, 4)、(2, 5)、(3, 8)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>], [</span><span>2.0</span><span>], [</span><span>3.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>4.0</span><span>, </span><span>5.0</span><span>, </span><span>8.0</span><span>])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>AtA </span><span>=</span><span> (A.T </span><span>@</span><span> A)[</span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>Atb </span><span>=</span><span> (A.T </span><span>@</span><span> b)[</span><span>0</span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> Atb </span><span>/</span><span> AtA</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"AᵀA ="</span><span>, AtA, </span><span>"，Aᵀb ="</span><span>, Atb)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"D = 38/14 = </span><span>{</span><span>D</span><span>:.4f</span><span>}</span><span>，</span><span>{</span><span><span>np.</span><span>isclose</span><span>(D, </span></span><span>38</span><span><span> </span><span>/</span><span> </span></span><span>14</span><span>)</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>x, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"lstsq 的 D = </span><span>{</span><span>x[</span><span>0</span><span>]</span><span>:.4f</span><span>}</span><span>，一致：</span><span>{</span><span><span>np.</span><span>allclose</span><span>(x[</span></span><span>0</span><span>], D)</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 投影图：b 投影到 A 的列空间，误差与列空间正交</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> A[:, </span><span>0</span><span><span>] </span><span>*</span><span> D</span></span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> b </span><span>-</span><span> p</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"p ="</span><span>, p)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"e ="</span><span>, e)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"Aᵀe 为零："</span><span><span>, np.</span><span>allclose</span><span>(A.T </span><span>@</span><span> e, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># Gram-Schmidt：a1 = (1, 2, 3)，a2 = (1, 1, 1)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>a1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>a2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> a2 </span><span>-</span><span> (a1 </span><span>@</span><span> a2) </span><span>/</span><span> (a1 </span><span>@</span><span> a1) </span><span>*</span><span> a1</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"a1ᵀa2 ="</span><span><span>, a1 </span><span>@</span><span> a2, </span></span><span>"，a1ᵀa1 ="</span><span><span>, a1 </span><span>@</span><span> a1)</span></span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"14 B ="</span><span>, </span><span>14</span><span><span> </span><span>*</span><span> B)</span></span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"a1 与 B 正交："</span><span><span>, np.</span><span>isclose</span><span>(a1 </span><span>@</span><span> B, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span># 对照 np.linalg.qr：单位化之后，两组向量最多差一个符号</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>Q, _ </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(np.</span><span>column_stack</span><span>([a1, a2]))</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>mine </span><span>=</span><span> np.</span><span>column_stack</span><span>([a1 </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(a1), B </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(B)])</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"与 qr 一致："</span><span><span>, np.</span><span>allclose</span><span>(np.</span><span>abs</span><span>(Q), np.</span><span>abs</span><span>(mine)))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>AᵀA = 14.0 ，Aᵀb = 38.0</span></div></div><div><div><div>2</div></div><div><span>D = 38/14 = 2.7143，True</span></div></div><div><div><div>3</div></div><div><span>lstsq 的 D = 2.7143，一致：True</span></div></div><div><div><div>4</div></div><div><span>p = [2.7143 5.4286 8.1429]</span></div></div><div><div><div>5</div></div><div><span>e = [ 1.2857 -0.4286 -0.1429]</span></div></div><div><div><div>6</div></div><div><span>Aᵀe 为零： True</span></div></div><div><div><div>7</div></div><div><span>a1ᵀa2 = 6.0 ，a1ᵀa1 = 14.0</span></div></div><div><div><div>8</div></div><div><span>14 B = [ 8.  2. -4.]</span></div></div><div><div><div>9</div></div><div><span>a1 与 B 正交： True</span></div></div><div><div><div>10</div></div><div><span>与 qr 一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>14B=(8,2,−4)14\mathbf{B} = (8, 2, -4)</span><span><span><span></span><span>14</span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>8</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>−</span><span>4</span><span>)</span></span></span></span>，也就是 <span><span>B=(4,1,−2)/7\mathbf{B} = (4, 1, -2)/7</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span><span>/7</span></span></span></span>。<span><span>B\mathbf{B}</span><span><span><span></span><span>B</span></span></span></span> 与 <span><span>a1\mathbf{a}_1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 正交，单位化之后和 <code>np.linalg.qr</code> 给出的两列在符号之外一致（QR 的符号取决于实现，所以我比较的是绝对值）。顺带验证了拟合的误差 <span><span>e=b−p\mathbf{e} = \mathbf{b} - \mathbf{p}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>p</span></span></span></span> 与列空间垂直。</p></section>
<section><h2>特征值的几条“白送”的性质<a href="#特征值的几条白送的性质"><span>#</span></a></h2><p>第三题是一个 <span><span>4×44 \times 4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，特征值是 <span><span>λ1,…,λ4\lambda_1, \ldots, \lambda_4</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，问三件事：</p><ul>
<li><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 可逆的条件？没有零特征值（零特征值就是零空间里有非零向量）。</li>
<li><span><span>det⁡A−1\det A^{-1}</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>？<span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的特征值是 <span><span>1/λi1/\lambda_i</span><span><span><span></span><span>1/</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，行列式是它们的积，即 <span><span>1/(λ1λ2λ3λ4)1/(\lambda_1\lambda_2\lambda_3\lambda_4)</span><span><span><span></span><span>1/</span><span>(</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。</li>
<li><span><span>tr⁡(A+I)\operatorname{tr}(A + I)</span><span><span><span></span><span><span>tr</span></span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span>)</span></span></span></span>？<span><span>A+IA + I</span><span><span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span></span></span></span> 的特征值是 <span><span>λi+1\lambda_i + 1</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span>，迹是它们的和，即 <span><span>λ1+λ2+λ3+λ4+4\lambda_1 + \lambda_2 + \lambda_3 + \lambda_4 + 4</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>4</span></span></span></span>。</li>
</ul><p>他没有给矩阵，所以我自己造一个：取特征值 <span><span>2,−1,0.5,32, -1, 0.5, 3</span><span><span><span></span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0.5</span><span>,</span><span></span><span>3</span></span></span></span>，用 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>（<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 是随机矩阵）造出来，再逐条核对。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 构造一个特征值已知的 4 x 4 矩阵 A = S diag(lam) S^-1</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.5</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> S </span><span>@</span><span> np.</span><span>diag</span><span>(lam) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>eigs </span><span>=</span><span> np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A).real)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.</span><span>round</span><span>(eigs, </span></span><span>4</span><span>))</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 可逆当且仅当没有零特征值；det A 是特征值之积</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"det A = </span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(A)</span></span><span>:.4f</span><span>}</span><span>，积 = </span><span>{</span><span><span>np.</span><span>prod</span><span>(lam)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span># A 的逆的行列式是 1 / (λ1 λ2 λ3 λ4)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>det_inv </span><span>=</span><span> np.linalg.</span><span>det</span><span>(np.linalg.</span><span>inv</span><span>(A))</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"det A⁻¹ = </span><span>{</span><span>det_inv</span><span>:.4f</span><span>}</span><span>，1/积 = </span><span>{1</span><span><span> </span><span>/</span><span> np.</span><span>prod</span><span>(lam)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span># A + I 的迹等于特征值之和再加 4</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>tr </span><span>=</span><span> np.</span><span>trace</span><span>(A </span><span>+</span><span> np.</span><span>eye</span><span>(</span><span>4</span><span>))</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"tr(A + I) = </span><span>{</span><span>tr</span><span>:.4f</span><span>}</span><span>，和 + 4 = </span><span>{</span><span><span>lam.</span><span>sum</span><span>() </span><span>+</span><span> </span></span><span>4</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 反过来：有一个特征值是 0 的矩阵不可逆</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>lam0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> S </span><span>@</span><span> np.</span><span>diag</span><span>(lam0) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"含 0 特征值的矩阵：秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"det = </span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(B)</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>特征值： [-1.   0.5  2.   3. ]</span></div></div><div><div><div>2</div></div><div><span>det A = -3.0000，积 = -3.0000</span></div></div><div><div><div>3</div></div><div><span>det A⁻¹ = -0.3333，1/积 = -0.3333</span></div></div><div><div><div>4</div></div><div><span>tr(A + I) = 8.5000，和 + 4 = 8.5000</span></div></div><div><div><div>5</div></div><div><span>含 0 特征值的矩阵：秩 = 3</span></div></div><div><div><div>6</div></div><div><span>det = -1.9e-15</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>det⁡A=−3\det A = -3</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3</span></span></span></span>，正是 <span><span>2×(−1)×0.5×32 \times (-1) \times 0.5 \times 3</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>0.5</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span>；<span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的行列式是 <span><span>−1/3-1/3</span><span><span><span></span><span>−</span><span>1/3</span></span></span></span>；<span><span>tr⁡(A+I)=8.5=(2−1+0.5+3)+4\operatorname{tr}(A + I) = 8.5 = (2 - 1 + 0.5 + 3) + 4</span><span><span><span></span><span><span>tr</span></span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>8.5</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>0.5</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span></span></span></span>。最后两行是反例：把一个特征值换成 0，行列式从 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span> 掉到 <span><span>−1.9×10−15-1.9 \times 10^{-15}</span><span><span><span></span><span>−</span><span>1.9</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，数值上就是 0（它的秩是 3，不是 4，因为这个矩阵可对角化，秩等于非零特征值的个数）。</p></section>
<section><h2>行列式递推：从余子式到周期 6<a href="#行列式递推从余子式到周期-6"><span>#</span></a></h2><p>这是我觉得整堂课最漂亮的一题。<span><span>AnA_n</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是对角线、上下副对角线全是 1 的三对角矩阵，<span><span>Dn=det⁡AnD_n = \det A_n</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。第一问：用余子式证明 <span><span>Dn=aDn−1+bDn−2D_n = a D_{n-1} + b D_{n-2}</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，求 <span><span>aa</span><span><span><span></span><span>a</span></span></span></span>、<span><span>bb</span><span><span><span></span><span>b</span></span></span></span>。</p><p>他沿第一行展开。第一项是 <span><span>1⋅Dn−11 \cdot D_{n-1}</span><span><span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。第二项是位置 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 上的 1，它的余子式要带一个负号；这个子矩阵的第一列只有一个非零元，再按第一列展开，得到 <span><span>Dn−2D_{n-2}</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。所以符号叠起来是 <span><span>−Dn−2-D_{n-2}</span><span><span><span></span><span>−</span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>Dn=Dn−1−Dn−2,D1=1, D2=0D_n = D_{n-1} - D_{n-2},\qquad D_1 = 1,\ D_2 = 0</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span> </span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>他提醒这不是 Fibonacci，因为有一个负号。第二问是把二阶递推写成一阶系统，这是第 22 讲里对付 Fibonacci 的老办法：</p><span><span><span>[DnDn−1]=[1−110]⏟M[Dn−1Dn−2]\begin{bmatrix} D_n \\ D_{n-1} \end{bmatrix}
= \underbrace{\begin{bmatrix} 1 &amp; -1 \\ 1 &amp; 0 \end{bmatrix}}_{M}
\begin{bmatrix} D_{n-1} \\ D_{n-2} \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>D</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>M</span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p><span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 的特征方程是 <span><span>λ2−λ+1=0\lambda^2 - \lambda + 1 = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，解是 <span><span>λ=1±3 i2\lambda = \frac{1 \pm \sqrt{3}\,i}{2}</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span><span>±</span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。模长：实部平方 <span><span>1/41/4</span><span><span><span></span><span>1/4</span></span></span></span> 加虚部平方 <span><span>3/43/4</span><span><span><span></span><span>3/4</span></span></span></span>，等于 1，所以它们在单位圆上，<span><span>λ=e±iπ/3\lambda = e^{\pm i\pi/3}</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>±</span><span>iπ</span><span>/3</span></span></span></span></span></span></span></span></span></span></span></span>。稳定吗？既不衰减也不爆炸，绕着圈转。题目还告诉你 <span><span>λ6=1\lambda^6 = 1</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，所以 <span><span>M6M^6</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 的特征值都是 1，对角化之后就是单位矩阵：转六步回到出发点。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>tridiagonal_det</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""对角线、上下副对角线全是 1 的 n 阶矩阵的行列式。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>eye</span><span>(n) </span><span>+</span><span> np.</span><span>eye</span><span>(n, </span><span>k</span><span>=</span><span>1</span><span><span>) </span><span>+</span><span> np.</span><span>eye</span><span>(n, </span></span><span>k</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> np.linalg.</span><span>det</span><span>(A)</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 递推 D(n) = D(n-1) - D(n-2)，D1 = 1，D2 = 0</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> [</span><span>1.0</span><span>, </span><span>0.0</span><span>]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>10</span><span>):</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>D.</span><span>append</span><span>(D[</span><span>-</span><span>1</span><span><span>] </span><span>-</span><span> D[</span><span>-</span></span><span>2</span><span>])</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"递推："</span><span><span>, np.</span><span>array</span><span>(D))</span></span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>direct </span><span>=</span><span> [</span><span>tridiagonal_det</span><span>(n) </span><span>for</span><span> n </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>13</span><span>)]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"直接算："</span><span><span>, np.</span><span>array</span><span>(direct))</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"一致："</span><span><span>, np.</span><span>allclose</span><span>(D, direct))</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 写成一阶系统 (D(n), D(n-1)) = M (D(n-1), D(n-2))</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> np.linalg.</span><span>eigvals</span><span>(M)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, lam)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"模长："</span><span><span>, np.</span><span>abs</span><span>(lam))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"辐角（度）："</span><span><span>, np.</span><span>degrees</span><span>(np.</span><span>angle</span><span>(lam)))</span></span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"λ^6 全为 1："</span><span><span>, np.</span><span>allclose</span><span>(lam</span><span>**</span></span><span>6</span><span>, </span><span>1.0</span><span>))</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>M6 </span><span>=</span><span> np.linalg.</span><span>matrix_power</span><span>(M, </span><span>6</span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"M^6 ="</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(M6)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"M^6 是单位矩阵："</span><span><span>, np.</span><span>allclose</span><span>(M6, np.</span><span>eye</span><span>(</span></span><span>2</span><span>)))</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>递推： [ 1.  0. -1. -1.  0.  1.  1.  0. -1. -1.  0.  1.]</span></div></div><div><div><div>2</div></div><div><span>直接算： [ 1.  0. -1. -1.  0.  1.  1.  0. -1. -1.  0.  1.]</span></div></div><div><div><div>3</div></div><div><span>一致： True</span></div></div><div><div><div>4</div></div><div><span>特征值： [0.5+0.866j 0.5-0.866j]</span></div></div><div><div><div>5</div></div><div><span>模长： [1. 1.]</span></div></div><div><div><div>6</div></div><div><span>辐角（度）： [ 60. -60.]</span></div></div><div><div><div>7</div></div><div><span>λ^6 全为 1： True</span></div></div><div><div><div>8</div></div><div><span>M^6 =</span></div></div><div><div><div>9</div></div><div><span>[[1. 0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0. 1.]]</span></div></div><div><div><div>11</div></div><div><span>M^6 是单位矩阵： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>递推给出的 <span><span>D1,…,D12D_1, \ldots, D_{12}</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>1,0,−1,−1,0,11, 0, -1, -1, 0, 1</span><span><span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span></span></span></span> 重复两遍，和 <code>np.linalg.det</code> 直接算十二个三对角矩阵的结果完全一致。特征值的模长是 1，辐角是 <span><span>±60\pm 60</span><span><span><span></span><span>±</span><span>60</span></span></span></span> 度，<span><span>M6=IM^6 = I</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，全都对得上。（他顺口说出“六次幂是单位矩阵”之后又自嘲，这本来是一道很好的测验题，不该脱口而出。）</p></section>
<section><h2>1992 年期末：一族对称矩阵<a href="#1992-年期末一族对称矩阵"><span>#</span></a></h2><p>后半堂课他换了一份 1992 年的期末卷，很多题围绕同一族矩阵：对角线全是 0，副对角线依次是 <span><span>1,2,3,…1, 2, 3, \ldots</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>…</span></span></span></span> 的对称矩阵。<span><span>A3A_3</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是</p><span><span><span>A3=[010102020]A_3 = \begin{bmatrix} 0 &amp; 1 &amp; 0 \\ 1 &amp; 0 &amp; 2 \\ 0 &amp; 2 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>他问的几件事：<span><span>A3A_3</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 奇异还是可逆？第三列是第一列的两倍，所以奇异，列空间是一个平面。求投影到这个平面的矩阵：取前两列做基，套 <span><span>B(BTB)−1BTB(B^TB)^{-1}B^T</span><span><span><span></span><span>B</span><span>(</span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。再求特征值：<span><span>det⁡(A3−λI)=−λ3+5λ\det(A_3 - \lambda I) = -\lambda^3 + 5\lambda</span><span><span><span></span><span>det</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>5</span><span>λ</span></span></span></span>，所以 <span><span>λ=0,±5\lambda = 0, \pm\sqrt{5}</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span>±</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。他说他不敢不查迹就写下这三个数：迹是 0，三个特征值加起来也是 0。（他也承认，行列式是 0，帮不上忙：如果那个 5 写成了 4，迹和行列式都抓不出来。）</p><p>最后是 <span><span>A4A_4</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的列空间上的投影矩阵，题目旁边写着“不难”。如果 <span><span>A4A_4</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 可逆，列空间就是 <span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span>，投影矩阵就是 <span><span>4×44 \times 4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的单位矩阵，确实不难。可逆吗？沿第一行再沿第一列做两次余子式展开，<span><span>det⁡A4=9\det A_4 = 9</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>9</span></span></span></span>。他于是猜：奇数阶的奇异，偶数阶的可逆。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>family</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""对称矩阵，零对角线，副对角线是 1, 2, ..., n-1。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>arange</span><span>(</span><span>1.0</span><span>, n)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>diag</span><span>(v, </span></span><span>k</span><span>=</span><span>1</span><span><span>) </span><span>+</span><span> np.</span><span>diag</span><span>(v, </span></span><span>k</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>A3 </span><span>=</span><span> </span><span>family</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"A3 ="</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(A3)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A3), </span></span><span>"，det ="</span><span><span>, np.linalg.</span><span>det</span><span>(A3))</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># A3 奇异，列空间是平面：取前两列作基，P = B (BᵀB)⁻¹ Bᵀ</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> A3[:, :</span><span>2</span><span>]</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> B </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(B.T </span><span>@</span><span> B) </span><span>@</span><span> B.T</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ A3 == A3："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> A3, A3))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"与 A3 @ pinv(A3) 一致："</span><span><span>, np.</span><span>allclose</span><span>(P, A3 </span><span>@</span><span> np.linalg.</span><span>pinv</span><span>(A3)))</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 特征值：0 和 ±√5，迹为 0</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"A3 的特征值："</span><span><span>, np.linalg.</span><span>eigvalsh</span><span>(A3))</span></span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"±√5 ="</span><span><span>, np.</span><span>sqrt</span><span>(</span></span><span>5.0</span><span>), </span><span>"，迹 ="</span><span><span>, np.</span><span>trace</span><span>(A3))</span></span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># A4 的行列式是 9，列空间是 R^4，投影矩阵是单位矩阵。逐阶看行列式</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>9</span><span>):</span></div></div><div><div><div>32</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"det A</span><span>{</span><span>n</span><span>}</span><span> ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(</span><span>family</span><span>(n)), </span></span><span>6</span><span>))</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A3 =</span></div></div><div><div><div>2</div></div><div><span>[[0. 1. 0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[1. 0. 2.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[0. 2. 0.]]</span></div></div><div><div><div>5</div></div><div><span>秩： 2 ，det = 0.0</span></div></div><div><div><div>6</div></div><div><span>P =</span></div></div><div><div><div>7</div></div><div><span>[[0.2 0.  0.4]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[0.  1.  0. ]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[0.4 0.  0.8]]</span></div></div><div><div><div>10</div></div><div><span>P @ A3 == A3： True</span></div></div><div><div><div>11</div></div><div><span>与 A3 @ pinv(A3) 一致： True</span></div></div><div><div><div>12</div></div><div><span>A3 的特征值： [-2.2361  0.      2.2361]</span></div></div><div><div><div>13</div></div><div><span>±√5 = 2.23606797749979 ，迹 = 0.0</span></div></div><div><div><div>14</div></div><div><span>det A1 = 0.0</span></div></div><div><div><div>15</div></div><div><span>det A2 = -1.0</span></div></div><div><div><div>16</div></div><div><span>det A3 = 0.0</span></div></div><div><div><div>17</div></div><div><span>det A4 = 9.0</span></div></div><div><div><div>18</div></div><div><span>det A5 = 0.0</span></div></div><div><div><div>19</div></div><div><span>det A6 = -225.0</span></div></div><div><div><div>20</div></div><div><span>det A7 = 0.0</span></div></div><div><div><div>21</div></div><div><span>det A8 = 11025.0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的列确实落在平面里（<span><span>PA3=A3PA_3 = A_3</span><span><span><span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>），也和 <code>A3 @ pinv(A3)</code> 一致；<span><span>A3A_3</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的特征值 <span><span>−2.2361,0,2.2361-2.2361, 0, 2.2361</span><span><span><span></span><span>−</span><span>2.2361</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>2.2361</span></span></span></span> 就是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span> 和 <span><span>±5\pm\sqrt{5}</span><span><span><span></span><span>±</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>；<span><span>det⁡A4=9\det A_4 = 9</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>9</span></span></span></span>。我多算到 <span><span>n=8n = 8</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>8</span></span></span></span>：奇数阶行列式全是 0，偶数阶是 <span><span>−1,9,−225,11025-1, 9, -225, 11025</span><span><span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>9</span><span>,</span><span></span><span>−</span><span>225</span><span>,</span><span></span><span>11025</span></span></span></span>，绝对值分别是 <span><span>12,32,152,10521^2, 3^2, 15^2, 105^2</span><span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>3</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>,</span><span></span><span>1</span><span><span>5</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>,</span><span></span><span>10</span><span><span>5</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，也就是 <span><span>1,1⋅3,1⋅3⋅5,1⋅3⋅5⋅71, 1 \cdot 3, 1 \cdot 3 \cdot 5, 1 \cdot 3 \cdot 5 \cdot 7</span><span><span><span></span><span>1</span><span>,</span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>3</span><span>,</span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>3</span><span></span><span>⋅</span><span></span></span><span><span></span><span>5</span><span>,</span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>3</span><span></span><span>⋅</span><span></span></span><span><span></span><span>5</span><span></span><span>⋅</span><span></span></span><span><span></span><span>7</span></span></span></span> 的平方。他说他押这些行列式有漂亮的公式，至少到 8 阶，他押对了。</p><p>奇数阶为什么奇异，倒是可以一句话说清：令 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 的偶数位分量为 0，奇数阶矩阵里偶数行的方程只含奇数位分量，个数比未知数少一个，所以必有非零解 <span><span>Anx=0A_n\mathbf{x} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。偶数阶的公式我只验证了数值，没有证明。</p><p>下一讲回到新内容：<a href="/posts/linear-algebra-25-symmetric-positive-definite/">对称矩阵与正定性</a>。刚才反复出现的 <span><span>A3A_3</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>A4A_4</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都是对称的，这不是巧合：对称矩阵的特征值一定是实数，特征向量可以取成互相正交的，下一讲就从这里开始。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-24-markov-fourier/</id>
      <title type="text">18.06 第 24 讲：Markov 矩阵与 Fourier 级数</title>
      <published>2026-10-18T07:40:00.000Z</published>
      <updated>2026-10-18T07:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-24-markov-fourier/"/>
      <summary type="text">MIT 18.06 第 24 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把加州与麻州的人口迁移连乘了 20 年，离稳态 (666.67, 333.33) 只差 0.7523 人，再用 np.trapezoid 把方波和三角波展开成 Fourier 级数，取到 19 次谐波时三角波的误差约为方波的 1/35。</summary>
      <content type="html"><![CDATA[<p>这一讲其实是两件互不相干的事，共用一个工具：特征值管“乘很多次会怎样”，标准正交基管“系数怎么一步算出来”。Strang 先拿 Markov 矩阵做特征值的应用，再用后半讲把投影推到无穷维，得到 Fourier 级数。我把他的加州和麻州人口迁移连乘了 20 年，离稳态 <span><span>(666.67,333.33)(666.67, 333.33)</span><span><span><span></span><span>(</span><span>666.67</span><span>,</span><span></span><span>333.33</span><span>)</span></span></span></span> 只差 0.7523 人，每多乘一年，差距就缩成原来的 0.7 倍。后半段我用 <code>np.trapezoid</code> 算 Fourier 系数，取到 19 次谐波时，连续的三角波误差是 0.0103，带跳变的方波是 0.3558，差了 35 倍。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=lGGDIGizcQ0" target="_blank">Lecture 24: Markov matrices; Fourier series</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-24-markov-matrices-fourier-series/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 Appendix 8 Markov Matrices and Perron-Frobenius；Fourier 级数在第 6 版移到了网站：<a href="https://math.mit.edu/~gs/linearalgebra/ila6/ila5fourierseries.pdf" target="_blank">ila5fourierseries.pdf</a>
前置：<a href="/posts/linear-algebra-23-differential-equations-exp-at/">第 23 讲：微分方程与 exp(At)</a>
一句话：每列和为 1 的非负矩阵必有特征值 1，它的特征向量就是稳态；用标准正交基展开，每个系数只是一次内积，Fourier 级数就是把这个想法用在函数上。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先写下一个凭空编出来的 3 × 3 Markov 矩阵，然后追问“为什么 1 一定是特征值”。这一段（<span><span>A−IA - I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span></span></span></span> 的各列加起来是 0，所以各行线性相关）值得认真看，它是本讲唯一需要动脑的论证。</li>
<li>他顺手算出特征向量 <span><span>(0.6,33,0.7)(0.6, 33, 0.7)</span><span><span><span></span><span>(</span><span>0.6</span><span>,</span><span></span><span>33</span><span>,</span><span></span><span>0.7</span><span>)</span></span></span></span> 的那几句，本篇第一节已经用代码验算过，倍速即可。</li>
<li>加州与麻州的例子是全讲的主线，他心算 <span><span>c1=1000/3c_1 = 1000/3</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1000/3</span></span></span></span>、<span><span>c2=2000/3c_2 = 2000/3</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2000/3</span></span></span></span> 的过程建议跟着做一遍。</li>
<li>最后一部分是标准正交基展开和 Fourier 级数。时间很赶，他只推了系数公式，没有算具体函数。我在本篇最后一节补了两个函数的数值例子。</li>
</ul></div></div>
<section><h2>Markov 矩阵：非负、列和为 1<a href="#markov-矩阵非负列和为-1"><span>#</span></a></h2><p>他在黑板上写下的矩阵是</p><span><span><span>A=[0.10.010.30.20.990.30.700.4]A = \begin{bmatrix}
0.1 &amp; 0.01 &amp; 0.3 \\
0.2 &amp; 0.99 &amp; 0.3 \\
0.7 &amp; 0 &amp; 0.4
\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0.1</span></span></span><span><span></span><span><span>0.2</span></span></span><span><span></span><span><span>0.7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0.01</span></span></span><span><span></span><span><span>0.99</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0.3</span></span></span><span><span></span><span><span>0.3</span></span></span><span><span></span><span><span>0.4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p><strong>什么样的矩阵叫 Markov 矩阵？</strong> 两条：每个元素大于等于 0；每一列加起来等于 1。第一条保证做概率用的时候不出现负数，第二条保证“每个人都有去处”，总量不变。它的每个幂依然满足这两条，所以 <span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 也是 Markov 矩阵。</p><p>他关心的还是 <span><span>uk+1=Auk\mathbf{u}_{k+1} = A\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，即 <span><span>uk=Aku0\mathbf{u}_k = A^k\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。上一讲的微分方程里，稳态来自 <span><span>λ=0\lambda = 0</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>（<span><span>e0t=1e^{0t} = 1</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>0</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 不变）；幂的情形里，<span><span>λ=0\lambda = 0</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的分量一步就死了，真正管稳态的是 <span><span>λ=1\lambda = 1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（<span><span>1k=11^k = 1</span><span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 不变）。他预告了两个事实：</p><ul>
<li><span><span>λ=1\lambda = 1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 一定是特征值；</li>
<li>其余特征值的模都小于 1（他留了余地：特殊情形下可能有另一个模等于 1 的，但绝不会大于 1）。</li>
</ul><p>于是 <span><span>u0=c1x1+c2x2+⋯\mathbf{u}_0 = c_1\mathbf{x}_1 + c_2\mathbf{x}_2 + \cdots</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span></span></span></span>，乘 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 次之后 <span><span>uk=c1x1+c2λ2kx2+⋯\mathbf{u}_k = c_1\mathbf{x}_1 + c_2\lambda_2^k\mathbf{x}_2 + \cdots</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span></span></span></span>，后面的项全部衰减，剩下 <span><span>c1x1c_1\mathbf{x}_1</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：初始条件里沿 <span><span>x1\mathbf{x}_1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的那一份就是稳态。（他照例说了一遍每讲一次的良心话：这需要一组完整的特征向量。）</p><p><strong>为什么 1 一定是特征值？</strong> 这是我觉得整讲最漂亮的一步。特征值是把对角线减掉之后能让矩阵变奇异的那些数，所以他检查 <span><span>A−IA - I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span></span></span></span>：每个对角元减 1，每一列的和从 1 变成 0。各列加起来是 0，意味着把三行各乘 1 再相加，得到零行，即 <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 在 <span><span>A−IA - I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span></span></span></span> 的左零空间里。方阵的行线性相关，矩阵就奇异，所以 1 是特征值。不用算行列式，对任何 Markov 矩阵都成立。</p><p>他接着多说了一句：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 与 <span><span>ATA^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的特征值相同，因为 <span><span>det⁡(A−λI)=det⁡(AT−λI)\det(A - \lambda I) = \det(A^T - \lambda I)</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span></span></span></span>（行列式性质里的第 10 条，转置不改变行列式）。所以上面其实是在说 <span><span>ATA^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 有特征向量 <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，对应特征值 1；而 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 自己的特征向量是 <span><span>A−IA - I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span></span></span></span> 的零空间里的另一个向量，得另外去找。</p><p>我把这些逐条交给 NumPy：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># Strang 黑板上凭空编出来的 Markov 矩阵，每一列是一个概率分布</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.1</span><span>, </span><span>0.01</span><span>, </span><span>0.3</span><span>], [</span><span>0.2</span><span>, </span><span>0.99</span><span>, </span><span>0.3</span><span>], [</span><span>0.7</span><span>, </span><span>0.0</span><span>, </span><span>0.4</span><span>]])</span></div></div><div><div><div>8</div></div><div><span>    </span><span>print</span><span>(</span><span>"列和："</span><span><span>, A.</span><span>sum</span><span>(</span></span><span>axis</span><span>=</span><span>0</span><span>))</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^2 的列和："</span><span><span>, (A </span><span>@</span><span> A).</span><span>sum</span><span>(</span></span><span>axis</span><span>=</span><span>0</span><span>))</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^2 的最小元素 &gt;= 0："</span><span><span>, (A </span><span>@</span><span> A).</span><span>min</span><span>() </span><span>&gt;=</span><span> </span></span><span>0</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 全 1 向量在左零空间：1^T (A - I) = 0，所以 A - I 奇异</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>ones </span><span>=</span><span> np.</span><span>ones</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"1^T (A - I) ="</span><span><span>, ones </span><span>@</span><span> (A </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(A - I) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(A </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>))</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>vals, vecs </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的特征值："</span><span>, vals)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T 的特征值："</span><span><span>, np.linalg.</span><span>eigvals</span><span>(A.T))</span></span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 他手算的特征向量 (0.6, 33, 0.7)，验证它在 A - I 的零空间里</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>x1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.6</span><span>, </span><span>33.0</span><span>, </span><span>0.7</span><span>])</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"(A - I) @ (0.6, 33, 0.7) ="</span><span><span>, (A </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span><span>)) </span><span>@</span><span> x1)</span></span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>k </span><span>=</span><span> np.</span><span>argmin</span><span>(np.</span><span>abs</span><span>(vals </span><span>-</span><span> </span><span>1.0</span><span>))</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> vecs[:, k].real</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"eig 给出的 λ = 1 的特征向量，缩放后："</span><span><span>, v </span><span>/</span><span> v[</span></span><span>0</span><span><span>] </span><span>*</span><span> </span></span><span>0.6</span><span>)</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 稳态：把特征向量缩放成和为 1 的概率分布</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>steady </span><span>=</span><span> x1 </span><span>/</span><span> x1.</span><span>sum</span><span>()</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"稳态（和为 1）："</span><span>, steady)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.linalg.</span><span>matrix_power</span><span>(A, </span><span>100</span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^100 的每一列："</span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"每一列都接近稳态："</span><span><span>, np.</span><span>allclose</span><span>(P, steady[:, </span></span><span>None</span><span>]))</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>列和： [1. 1. 1.]</span></div></div><div><div><div>2</div></div><div><span>A^2 的列和： [1. 1. 1.]</span></div></div><div><div><div>3</div></div><div><span>A^2 的最小元素 &gt;= 0： True</span></div></div><div><div><div>4</div></div><div><span>1^T (A - I) = [ 0. -0.  0.]</span></div></div><div><div><div>5</div></div><div><span>det(A - I) = -2.33e-18</span></div></div><div><div><div>6</div></div><div><span>A 的特征值： [-0.2315  0.7215  1.    ]</span></div></div><div><div><div>7</div></div><div><span>A^T 的特征值： [-0.2315  0.7215  1.    ]</span></div></div><div><div><div>8</div></div><div><span>(A - I) @ (0.6, 33, 0.7) = [-0. -0.  0.]</span></div></div><div><div><div>9</div></div><div><span>eig 给出的 λ = 1 的特征向量，缩放后： [ 0.6 33.   0.7]</span></div></div><div><div><div>10</div></div><div><span>稳态（和为 1）： [0.0175 0.9621 0.0204]</span></div></div><div><div><div>11</div></div><div><span>A^100 的每一列：</span></div></div><div><div><div>12</div></div><div><span>[[0.0175 0.0175 0.0175]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[0.9621 0.9621 0.9621]</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[0.0204 0.0204 0.0204]]</span></div></div><div><div><div>15</div></div><div><span>每一列都接近稳态： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li>列和是 <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>A2A^2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 也是；<span><span>1T(A−I)1^T(A - I)</span><span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>)</span></span></span></span> 是零向量，<span><span>det⁡(A−I)\det(A - I)</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>)</span></span></span></span> 是 <span><span>−2.33×10−18-2.33 \times 10^{-18}</span><span><span><span></span><span>−</span><span>2.33</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>18</span></span></span></span></span></span></span></span></span></span></span></span>，浮点意义上的 0。</li>
<li><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>ATA^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的特征值都是 <span><span>−0.2315-0.2315</span><span><span><span></span><span>−</span><span>0.2315</span></span></span></span>、<span><span>0.72150.7215</span><span><span><span></span><span>0.7215</span></span></span></span>、<span><span>11</span><span><span><span></span><span>1</span></span></span></span>：1 在，其余两个模都小于 1。</li>
<li>他手算的特征向量 <span><span>(0.6,33,0.7)(0.6, 33, 0.7)</span><span><span><span></span><span>(</span><span>0.6</span><span>,</span><span></span><span>33</span><span>,</span><span></span><span>0.7</span><span>)</span></span></span></span> 通过了检验：他说“这是线性代数史上第一次特征向量里出现 3300 这种分量”，随即发现多读了两位小数，其实只是 33。<code>np.linalg.eig</code> 给出的向量缩放后和他的完全一致。</li>
<li>缩放成和为 1 的概率分布，稳态是 <span><span>(0.0175,0.9621,0.0204)(0.0175, 0.9621, 0.0204)</span><span><span><span></span><span>(</span><span>0.0175</span><span>,</span><span></span><span>0.9621</span><span>,</span><span></span><span>0.0204</span><span>)</span></span></span></span>。<span><span>A100A^{100}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span></span></span></span></span></span></span></span> 的三列都收敛到它，三列相同，意味着不管从哪个状态出发，100 步之后都是同一个分布。第二个状态的自留率 0.99 太高了，所以稳态里它占了 96%。</li>
</ul><p>特征向量的分量全是正的，这点他说理论保证，但没有证，我也不证（据我所知这是 Perron-Frobenius 定理的内容，教材的 Appendix 8 讲的就是它）。</p></section>
<section><h2>加州与麻州：特征值管着人口往哪走<a href="#加州与麻州特征值管着人口往哪走"><span>#</span></a></h2><p><strong>稳态到底有什么用？</strong> 他的例子是两个州的人口迁移：每年 10% 的加州人搬去麻州，20% 的麻州人搬去加州（他说“Red Sox 又输了”），其余的留在原地：</p><span><span><span>[uCaluMass]k+1=[0.90.20.10.8][uCaluMass]k\begin{bmatrix} u_\text{Cal} \\ u_\text{Mass} \end{bmatrix}_{k+1}
= \begin{bmatrix} 0.9 &amp; 0.2 \\ 0.1 &amp; 0.8 \end{bmatrix}
\begin{bmatrix} u_\text{Cal} \\ u_\text{Mass} \end{bmatrix}_k</span><span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>Cal</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>Mass</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0.9</span></span></span><span><span></span><span><span>0.1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0.2</span></span></span><span><span></span><span><span>0.8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>Cal</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>Mass</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>（我怀疑这两个比例是为了让黑板上的数好算而挑的，现实里大概没有人这么整齐地搬家。）第一列是加州人的去向（0.9 留下，0.1 搬走，合计 1），第二列是麻州人的去向。初始状态：加州 0 人，麻州 1000 人。</p><p>一步之后是 <span><span>(200,800)(200, 800)</span><span><span><span></span><span>(</span><span>200</span><span>,</span><span></span><span>800</span><span>)</span></span></span></span>：200 人去了加州。再乘一次，加州会超过 200，麻州会低于 800，总数仍是 1000。这个矩阵的特征值不用解方程：1 肯定有；迹是 1.7，所以另一个是 0.7（验证：行列式 <span><span>0.72−0.02=0.70.72 - 0.02 = 0.7</span><span><span><span></span><span>0.72</span><span></span><span>−</span><span></span></span><span><span></span><span>0.02</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>）。特征向量：<span><span>A−IA - I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span></span></span></span> 的零空间是 <span><span>(2,1)(2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>A−0.7IA - 0.7I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>0.7</span><span>I</span></span></span></span> 的零空间是 <span><span>(−1,1)(-1, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。</p><p>稳态是 <span><span>(2,1)(2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 的某个倍数，倍数由“总数 1000”定出：<span><span>10003×(2,1)\frac{1000}{3} \times (2, 1)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1000</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，即加州 <span><span>666.67666.67</span><span><span><span></span><span>666.67</span></span></span></span>、麻州 <span><span>333.33333.33</span><span><span><span></span><span>333.33</span></span></span></span>。再把 <span><span>u0\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 展开：</p><span><span><span>uk=10003⋅1k[21]+20003⋅0.7k[−11]\mathbf{u}_k = \frac{1000}{3} \cdot 1^k \begin{bmatrix} 2 \\ 1 \end{bmatrix} +
\frac{2000}{3} \cdot 0.7^k \begin{bmatrix} -1 \\ 1 \end{bmatrix}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1000</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span><span><span></span><span><span>2000</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.</span><span><span>7</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p><span><span>k=0k = 0</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时第一个分量是 <span><span>20003−20003=0\frac{2000}{3} - \frac{2000}{3} = 0</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2000</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2000</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，第二个是 <span><span>10003+20003=1000\frac{1000}{3} + \frac{2000}{3} = 1000</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1000</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2000</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1000</span></span></span></span>，对上了。下面的脚本验算这一整套，并看看闭式公式和直接连乘是不是同一回事、离稳态有多远：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 加州和麻州：每年 10% 的加州人搬去麻州，20% 的麻州人搬去加州</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.9</span><span>, </span><span>0.2</span><span>], [</span><span>0.1</span><span>, </span><span>0.8</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>u0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>1000.0</span><span>])  </span><span># (加州, 麻州)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> u0.</span><span>copy</span><span>()</span></div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>4</span><span>):</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> A </span><span>@</span><span> u</span></div></div><div><div><div>13</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"第 </span><span>{</span><span>k</span><span>}</span><span> 年："</span><span>, u, </span><span>"总人数："</span><span><span>, u.</span><span>sum</span><span>())</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>vals, vecs </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, vals)  </span><span># 迹 1.7，行列式 0.7</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 他手算的两个特征向量：λ = 1 对应 (2, 1)，λ = 0.7 对应 (-1, 1)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>x1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>x2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>-</span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x1 - x1 ="</span><span><span>, A </span><span>@</span><span> x1 </span><span>-</span><span> x1)</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x2 - 0.7 * x2 ="</span><span><span>, A </span><span>@</span><span> x2 </span><span>-</span><span> </span></span><span>0.7</span><span><span> </span><span>*</span><span> x2)</span></span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># u0 = c1 x1 + c2 x2，他心算得到 c1 = 1000/3，c2 = 2000/3</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>column_stack</span><span>([x1, x2])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(S, u0)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"c ="</span><span>, c, </span><span>"，1000/3 ="</span><span>, </span><span>1000</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>steady </span><span>=</span><span> c[</span><span>0</span><span><span>] </span><span>*</span><span> x1</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"稳态："</span><span>, steady)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> [</span><span>1</span><span>, </span><span>5</span><span>, </span><span>10</span><span>, </span><span>20</span><span>, </span><span>100</span><span>]:</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>closed </span><span>=</span><span> c[</span><span>0</span><span><span>] </span><span>*</span><span> x1 </span><span>+</span><span> c[</span></span><span>1</span><span><span>] </span><span>*</span><span> </span></span><span>0.7</span><span><span>**</span><span>k </span><span>*</span><span> x2</span></span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>direct </span><span>=</span><span> np.linalg.</span><span>matrix_power</span><span>(A, k) </span><span>@</span><span> u0</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>gap </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(direct </span><span>-</span><span> steady)</span></div></div><div><div><div>35</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>36</div></div><div><span>            </span><span>f</span><span>"k = </span><span>{</span><span>k</span><span>:3d</span><span>}</span><span>：公式与连乘一致 </span><span>{</span><span><span>np.</span><span>allclose</span><span>(closed, direct)</span></span><span>}</span><span>，"</span></div></div><div><div><div>37</div></div><div><span>            </span><span>f</span><span>"离稳态 </span><span>{</span><span>gap</span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>第 1 年： [200. 800.] 总人数： 1000.0</span></div></div><div><div><div>2</div></div><div><span>第 2 年： [340. 660.] 总人数： 1000.0</span></div></div><div><div><div>3</div></div><div><span>第 3 年： [438. 562.] 总人数： 1000.0</span></div></div><div><div><div>4</div></div><div><span>特征值： [1.  0.7]</span></div></div><div><div><div>5</div></div><div><span>A @ x1 - x1 = [0. 0.]</span></div></div><div><div><div>6</div></div><div><span>A @ x2 - 0.7 * x2 = [0. 0.]</span></div></div><div><div><div>7</div></div><div><span>c = [333.3333 666.6667] ，1000/3 = 333.3333333333333</span></div></div><div><div><div>8</div></div><div><span>稳态： [666.6667 333.3333]</span></div></div><div><div><div>9</div></div><div><span>k =   1：公式与连乘一致 True，离稳态 659.9663</span></div></div><div><div><div>10</div></div><div><span>k =   5：公式与连乘一致 True，离稳态 158.4579</span></div></div><div><div><div>11</div></div><div><span>k =  10：公式与连乘一致 True，离稳态 26.6320</span></div></div><div><div><div>12</div></div><div><span>k =  20：公式与连乘一致 True，离稳态 0.7523</span></div></div><div><div><div>13</div></div><div><span>k = 100：公式与连乘一致 True，离稳态 0.0000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>前三年是 <span><span>(200,800)(200, 800)</span><span><span><span></span><span>(</span><span>200</span><span>,</span><span></span><span>800</span><span>)</span></span></span></span>、<span><span>(340,660)(340, 660)</span><span><span><span></span><span>(</span><span>340</span><span>,</span><span></span><span>660</span><span>)</span></span></span></span>、<span><span>(438,562)(438, 562)</span><span><span><span></span><span>(</span><span>438</span><span>,</span><span></span><span>562</span><span>)</span></span></span></span>，每年总数都是 1000。</li>
<li><code>eig</code> 给出的特征值是 <span><span>11</span><span><span><span></span><span>1</span></span></span></span> 和 <span><span>0.70.7</span><span><span><span></span><span>0.7</span></span></span></span>，<span><span>c=(333.33,666.67)c = (333.33, 666.67)</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>333.33</span><span>,</span><span></span><span>666.67</span><span>)</span></span></span></span>，与他心算的 <span><span>1000/31000/3</span><span><span><span></span><span>1000/3</span></span></span></span>、<span><span>2000/32000/3</span><span><span><span></span><span>2000/3</span></span></span></span> 一致。</li>
<li>闭式公式在 <span><span>k=1,5,10,20,100k = 1, 5, 10, 20, 100</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>20</span><span>,</span><span></span><span>100</span></span></span></span> 都与 <code>matrix_power</code> 连乘一致。</li>
<li>离稳态的距离：第 10 年是 26.6320，第 20 年是 0.7523。二者的比 <span><span>0.7523/26.6320≈0.02820.7523 / 26.6320 \approx 0.0282</span><span><span><span></span><span>0.7523/26.6320</span><span></span><span>≈</span><span></span></span><span><span></span><span>0.0282</span></span></span></span>，正好是 <span><span>0.710≈0.02820.7^{10} \approx 0.0282</span><span><span><span></span><span>0.</span><span><span>7</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>0.0282</span></span></span></span>：十年缩小到原来的 2.8% 左右（约 1/35），每年缩小 30%。第 100 年，打印到四位小数已经是 0.0000。</li>
</ul><p>所以他说“波士顿的清教徒现在在哪”，答案和初始条件没有关系：只要总数是 1000，不管一开始 1000 个人全在麻州，还是全在加州，几百年之后都是 <span><span>(666.67,333.33)(666.67, 333.33)</span><span><span><span></span><span>(</span><span>666.67</span><span>,</span><span></span><span>333.33</span><span>)</span></span></span></span>。第二个特征值 0.7 只决定走得多快，不决定走向哪里。</p><p>他还提醒了一件会让你在别的书里糊涂的事：很多工程课程喜欢用行向量，<span><span>uk+1T=ukTA\mathbf{u}_{k+1}^T = \mathbf{u}_k^T A</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span></span></span></span>，这时 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是上面这个矩阵的转置，改成“每一行和为 1”。数学上是同一回事，注意一下约定就行了。我以后在控制和概率的书里碰到“行随机矩阵”，大概就是这个意思。</p></section>
<section><h2>标准正交基：系数只是一次内积<a href="#标准正交基系数只是一次内积"><span>#</span></a></h2><p>后半讲他说是“投影那一章的一个了不起的应用”。先回到有限维。<span><span>q1,…,qn\mathbf{q}_1, \dots, \mathbf{q}_n</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 维空间的一组标准正交基，任何向量 <span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 都能写成</p><span><span><span>v=x1q1+x2q2+⋯+xnqn\mathbf{v} = x_1\mathbf{q}_1 + x_2\mathbf{q}_2 + \cdots + x_n\mathbf{q}_n</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><strong>系数 <span><span>x1x_1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 怎么求？</strong> 一般的基要解方程组。标准正交基的好处是：两边同时和 <span><span>q1\mathbf{q}_1</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做内积，<span><span>q1Tqj\mathbf{q}_1^T\mathbf{q}_j</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 <span><span>j≠1j \ne 1</span><span><span><span></span><span>j</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>1</span></span></span></span> 时是 0，<span><span>j=1j = 1</span><span><span><span></span><span>j</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时是 1，其余全部消失，得到 <span><span>x1=q1Tvx_1 = \mathbf{q}_1^T\mathbf{v}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>v</span></span></span></span>。写成矩阵形式，<span><span>Qx=vQ\mathbf{x} = \mathbf{v}</span><span><span><span></span><span>Q</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span></span></span></span>，解是 <span><span>x=Q−1v\mathbf{x} = Q^{-1}\mathbf{v}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>v</span></span></span></span>，而 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 是正交矩阵时 <span><span>Q−1=QTQ^{-1} = Q^T</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，所以 <span><span>x=QTv\mathbf{x} = Q^T\mathbf{v}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>v</span></span></span></span>，第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个分量就是第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个 <span><span>qi\mathbf{q}_i</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与 <span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 的内积。他说这时的“展开”和“投影”是一回事：投影的正规方程 <span><span>(QTQ)x^=QTb(Q^TQ)\hat{\mathbf{x}} = Q^T\mathbf{b}</span><span><span><span></span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span>)</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 里 <span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，直接剩下 <span><span>x^=QTb\hat{\mathbf{x}} = Q^T\mathbf{b}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>。</p><p>我用随机正交基验证一遍：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 随机正交基：对随机矩阵做 QR，取 Q 的列</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>Q, _ </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>5</span><span>)))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>5</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"||Q^T Q - I|| ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q.T </span><span>@</span><span> Q </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>5</span><span>))</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 展开系数：x1 = q1^T v，一次点积，不用解方程</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>x_dot </span><span>=</span><span> np.</span><span>array</span><span>([Q[:, i] </span><span>@</span><span> v </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>5</span><span>)])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>x_matrix </span><span>=</span><span> Q.T </span><span>@</span><span> v</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>x_solve </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(Q, v)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"x ="</span><span>, x_matrix)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"逐个点积 = Q^T v："</span><span><span>, np.</span><span>allclose</span><span>(x_dot, x_matrix))</span></span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q^T v = solve(Q, v)："</span><span><span>, np.</span><span>allclose</span><span>(x_matrix, x_solve))</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q @ x 还原 v："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> x_matrix, v))</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>||Q^T Q - I|| = 5.10e-16</span></div></div><div><div><div>2</div></div><div><span>x = [-0.1733  0.336  -2.3959  2.6273  0.7986]</span></div></div><div><div><div>3</div></div><div><span>逐个点积 = Q^T v： True</span></div></div><div><div><div>4</div></div><div><span>Q^T v = solve(Q, v)： True</span></div></div><div><div><div>5</div></div><div><span>Q @ x 还原 v： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>随机矩阵做 QR 得到的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 满足 <span><span>∥QTQ−I∥=5.10×10−16\lVert Q^TQ - I\rVert = 5.10 \times 10^{-16}</span><span><span><span></span><span>∥</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>5.10</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，在舍入误差内是正交的。</li>
<li>逐个做点积、一次 <code>Q.T @ v</code>、解方程 <code>solve(Q, v)</code>，三种做法得到同一个 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>，<code>Q @ x</code> 能还原 <span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span>。</li>
</ul><p><code>solve</code> 要做一次 LU 分解，<code>Q.T @ v</code> 只是五个点积。这个差别在 <span><span>5×55 \times 5</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span> 里看不出来，在 Fourier 里它是决定性的：基向量有无穷多个，不能解无穷个方程，但每个系数仍然只需要一次积分。</p></section>
<section><h2>函数也是向量：Fourier 级数<a href="#函数也是向量fourier-级数"><span>#</span></a></h2><p><strong>怎么把这个想法搬到函数上？</strong> 他的原话大意是：Fourier 意识到，向量 <span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 可以换成函数 <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span>，正交向量 <span><span>q1,q2,…\mathbf{q}_1, \mathbf{q}_2, \dots</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span></span></span></span> 可以换成正交函数 <span><span>1,cos⁡x,sin⁡x,cos⁡2x,sin⁡2x,…1, \cos x, \sin x, \cos 2x, \sin 2x, \dots</span><span><span><span></span><span>1</span><span>,</span><span></span><span>cos</span><span></span><span>x</span><span>,</span><span></span><span>sin</span><span></span><span>x</span><span>,</span><span></span><span>cos</span><span></span><span>2</span><span>x</span><span>,</span><span></span><span>sin</span><span></span><span>2</span><span>x</span><span>,</span><span></span><span>…</span></span></span></span>，只不过无穷多个，因为这个空间是无穷维的。展开式是</p><span><span><span>f(x)=a0+a1cos⁡x+b1sin⁡x+a2cos⁡2x+b2sin⁡2x+⋯f(x) = a_0 + a_1\cos x + b_1\sin x + a_2\cos 2x + b_2\sin 2x + \cdots</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>2</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span>2</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span></span></span></span></span><p>“正交”要有内积。向量的内积是 <span><span>vTw=v1w1+⋯+vnwn\mathbf{v}^T\mathbf{w} = v_1w_1 + \cdots + v_nw_n</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：把对应分量相乘，再相加。函数有一整条连续的 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>，“对应分量相乘”就是在每个 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 处取 <span><span>f(x)g(x)f(x)g(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span>g</span><span>(</span><span>x</span><span>)</span></span></span></span>，“相加”换成积分：</p><span><span><span>⟨f,g⟩=∫02πf(x) g(x) dx\langle f, g \rangle = \int_0^{2\pi} f(x)\,g(x)\,dx</span><span><span><span></span><span>⟨</span><span>f</span><span>,</span><span></span><span>g</span><span>⟩</span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>2</span><span>π</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>g</span><span>(</span><span>x</span><span>)</span><span></span><span>d</span><span>x</span></span></span></span></span><p>积分区间是 <span><span>[0,2π][0, 2\pi]</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>2</span><span>π</span><span>]</span></span></span></span>，因为所有的正弦和余弦都以 <span><span>2π2\pi</span><span><span><span></span><span>2</span><span>π</span></span></span></span> 为周期，<span><span>ff</span><span><span><span></span><span>f</span></span></span></span> 也得是。他当场检验了一对：<span><span>∫02πsin⁡xcos⁡x dx=12sin⁡2x∣02π=0\int_0^{2\pi}\sin x\cos x\,dx = \frac12\sin^2 x \big|_0^{2\pi} = 0</span><span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>2</span><span>π</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span>x</span><span></span><span>cos</span><span></span><span>x</span><span></span><span>d</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>sin</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>x</span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>2</span><span>π</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，其余各对“用一点三角恒等式”就能得到。我用 <code>np.trapezoid</code> 把五个函数两两做内积，排成 Gram 矩阵：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>inner</span><span>(</span><span>f</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>g</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""函数内积：在 [0, 2π] 上对 f(x) g(x) 做数值积分。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.</span><span>trapezoid</span><span>(f </span><span>*</span><span> g, x))</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span><span> </span><span>*</span><span> np.pi, </span></span><span>2001</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>basis </span><span>=</span><span> [</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>(</span><span>"1"</span><span><span>, np.</span><span>ones_like</span><span>(x)),</span></span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>(</span><span>"cos x"</span><span><span>, np.</span><span>cos</span><span>(x)),</span></span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>(</span><span>"sin x"</span><span><span>, np.</span><span>sin</span><span>(x)),</span></span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>(</span><span>"cos 2x"</span><span><span>, np.</span><span>cos</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> x)),</span></span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>(</span><span>"sin 2x"</span><span><span>, np.</span><span>sin</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> x)),</span></span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 他口算的那一个：sin x 与 cos x 的内积</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"&lt;sin x, cos x&gt; ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>inner</span><span>(basis[</span></span><span>2</span><span>][</span><span>1</span><span>], basis[</span><span>1</span><span>][</span><span>1</span><span>], x)</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 五个函数两两做内积：对角线是长度的平方，其余应当是 0</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>inner</span><span>(f, g, x) </span><span>for</span><span> _, g </span><span>in</span><span> basis] </span><span>for</span><span> _, f </span><span>in</span><span> basis])</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(G)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"对角线："</span><span><span>, np.</span><span>diag</span><span>(G))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"π ="</span><span>, np.pi, </span><span>"，2π ="</span><span>, </span><span>2</span><span><span> </span><span>*</span><span> np.pi)</span></span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>off </span><span>=</span><span> np.</span><span>abs</span><span>(G </span><span>-</span><span> np.</span><span>diag</span><span>(np.</span><span>diag</span><span>(G))).</span><span>max</span><span>()</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"非对角元的最大绝对值："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>off</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;sin x, cos x&gt; = 0.00e+00</span></div></div><div><div><div>2</div></div><div><span>[[ 6.2832 -0.      0.     -0.     -0.    ]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[-0.      3.1416  0.     -0.      0.    ]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.      0.      3.1416  0.      0.    ]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[-0.     -0.      0.      3.1416 -0.    ]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[-0.      0.      0.     -0.      3.1416]]</span></div></div><div><div><div>7</div></div><div><span>对角线： [6.2832 3.1416 3.1416 3.1416 3.1416]</span></div></div><div><div><div>8</div></div><div><span>π = 3.141592653589793 ，2π = 6.283185307179586</span></div></div><div><div><div>9</div></div><div><span>非对角元的最大绝对值： 4.44e-16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><span><span>⟨sin⁡x,cos⁡x⟩=0\langle \sin x, \cos x \rangle = 0</span><span><span><span></span><span>⟨</span><span>sin</span><span></span><span>x</span><span>,</span><span></span><span>cos</span><span></span><span>x</span><span>⟩</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，与他的黑板一致。</li>
<li>Gram 矩阵除对角线外全是 0（最大的非对角元绝对值是 <span><span>4.44×10−164.44 \times 10^{-16}</span><span><span><span></span><span>4.44</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>），对角线是 <span><span>(2π,π,π,π,π)(2\pi, \pi, \pi, \pi, \pi)</span><span><span><span></span><span>(</span><span>2</span><span>π</span><span>,</span><span></span><span>π</span><span>,</span><span></span><span>π</span><span>,</span><span></span><span>π</span><span>,</span><span></span><span>π</span><span>)</span></span></span></span>：常数函数长度的平方是 <span><span>2π2\pi</span><span><span><span></span><span>2</span><span>π</span></span></span></span>，其余是 <span><span>π\pi</span><span><span><span></span><span>π</span></span></span></span>。他说 <span><span>∫cos⁡2x dx=π\int\cos^2 x\,dx = \pi</span><span><span><span></span><span>∫</span><span></span><span><span>cos</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>x</span><span></span><span>d</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>π</span></span></span></span>，这里印证了。</li>
</ul><p>注意：这五个函数是正交的，但没有归一化，所以它们更像“长度各不相同的正交基”。求系数时要除以长度的平方，这就是 Fourier 公式里 <span><span>1π\frac1\pi</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>π</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 的来历。</p><p><strong><span><span>a1a_1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 到底是多少？</strong> 套用有限维的做法：两边同时乘 <span><span>cos⁡x\cos x</span><span><span><span></span><span>cos</span><span></span><span>x</span></span></span></span> 再积分，右边只有 <span><span>a1∫cos⁡2x dx=a1πa_1\int\cos^2 x\,dx = a_1\pi</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span></span><span><span>cos</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>x</span><span></span><span>d</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>π</span></span></span></span> 这一项活着，所以</p><span><span><span>a1=1π∫02πf(x)cos⁡x dxa_1 = \frac{1}{\pi}\int_0^{2\pi} f(x)\cos x\,dx</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>2</span><span>π</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>cos</span><span></span><span>x</span><span></span><span>d</span><span>x</span></span></span></span></span><p>同理 <span><span>ak=1π∫fcos⁡kx dxa_k = \frac1\pi\int f\cos kx\,dx</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>π</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>∫</span><span></span><span>f</span><span></span><span>cos</span><span></span><span>k</span><span>x</span><span></span><span>d</span><span>x</span></span></span></span>，<span><span>bk=1π∫fsin⁡kx dxb_k = \frac1\pi\int f\sin kx\,dx</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>π</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>∫</span><span></span><span>f</span><span></span><span>sin</span><span></span><span>k</span><span>x</span><span></span><span>d</span><span>x</span></span></span></span>。常数项 <span><span>a0a_0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是例外，要除以 <span><span>∫1 dx=2π\int 1\,dx = 2\pi</span><span><span><span></span><span>∫</span><span></span><span>1</span><span></span><span>d</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>π</span></span></span></span>，所以它就是 <span><span>ff</span><span><span><span></span><span>f</span></span></span></span> 在一个周期里的平均值，他说“那个问题很容易”。</p><p>他到这里下课了，没有算一个具体函数。我挑了两个：方波（<span><span>[0,π)[0, \pi)</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>π</span><span>)</span></span></span></span> 上为 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>，<span><span>[π,2π)[\pi, 2\pi)</span><span><span><span></span><span>[</span><span>π</span><span>,</span><span></span><span>2</span><span>π</span><span>)</span></span></span></span> 上为 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>）和三角波 <span><span>∣x−π∣|x - \pi|</span><span><span><span></span><span>∣</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>π</span><span>∣</span></span></span></span>。代码里用 20001 个点做梯形积分，求出 <span><span>a0a_0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、前 20 个 <span><span>ak,bka_k, b_k</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，再看取前 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 次谐波的部分和离原函数有多远（用 <span><span>L2L^2</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 距离 <span><span>∫(f−s)2dx\sqrt{\int (f - s)^2dx}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>∫</span><span>(</span><span>f</span><span></span><span>−</span><span></span><span>s</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>d</span><span>x</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，也就是函数空间里的“长度”）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>inner</span><span>(</span><span>f</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>g</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""函数内积：在 [0, 2π] 上对 f(x) g(x) 做数值积分。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.</span><span>trapezoid</span><span>(f </span><span>*</span><span> g, x))</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>fourier</span><span>(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>f</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>n</span></span><span>:</span><span> </span><span>int</span></div></div><div><div><div>11</div></div><div><span>) -&gt; tuple[</span><span>float</span><span>, np.ndarray, np.ndarray]:</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"""返回 a0（平均值），以及 a_k、b_k（k = 1..n）。"""</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>a0 </span><span>=</span><span> </span><span>inner</span><span>(f, np.</span><span>ones_like</span><span>(x), x) </span><span>/</span><span> (</span><span>2.0</span><span><span> </span><span>*</span><span> np.pi)</span></span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>inner</span><span>(f, np.</span><span>cos</span><span>(k </span><span>*</span><span> x), x) </span><span>/</span><span> np.pi </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span><span>, n </span><span>+</span><span> </span></span><span>1</span><span>)])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>inner</span><span>(f, np.</span><span>sin</span><span>(k </span><span>*</span><span> x), x) </span><span>/</span><span> np.pi </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span><span>, n </span><span>+</span><span> </span></span><span>1</span><span>)])</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> a0, a, b</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>def</span><span> </span><span>partial_sum</span><span>(</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>a0</span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>a</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>n</span></span><span>:</span><span> </span><span>int</span></div></div><div><div><div>21</div></div><div><span>) -&gt; np.ndarray:</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> np.</span><span>full_like</span><span>(x, a0)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span><span>, n </span><span>+</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> a[k </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>*</span><span> np.</span><span>cos</span><span>(k </span><span>*</span><span> x) </span><span>+</span><span> b[k </span><span>-</span><span> </span></span><span>1</span><span><span>] </span><span>*</span><span> np.</span><span>sin</span><span>(k </span><span>*</span><span> x)</span></span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span> s</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span><span> </span><span>*</span><span> np.pi, </span></span><span>20001</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span># 方波：跳变点取 0（左右极限的平均），这样 [0, 2π] 两端也对称</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>square </span><span>=</span><span> np.</span><span>sign</span><span>(np.</span><span>round</span><span>(np.</span><span>sin</span><span>(x), </span><span>12</span><span>))</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>tri </span><span>=</span><span> np.</span><span>abs</span><span>(x </span><span>-</span><span> np.pi)  </span><span># 三角波，连续但有尖角</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span>for</span><span> name, f </span><span>in</span><span> [(</span><span>"方波"</span><span>, square), (</span><span>"三角波"</span><span>, tri)]:</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>a0, a, b </span><span>=</span><span> </span><span>fourier</span><span>(f, x, </span><span>20</span><span>)</span></div></div><div><div><div>37</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：a0 = </span><span>{</span><span>a0</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>38</div></div><div><span>        </span><span>print</span><span>(</span><span>"  a1..a5 ="</span><span>, a[:</span><span>5</span><span>])</span></div></div><div><div><div>39</div></div><div><span>        </span><span>print</span><span>(</span><span>"  b1..b5 ="</span><span>, b[:</span><span>5</span><span>])</span></div></div><div><div><div>40</div></div><div><span>        </span><span>for</span><span> n </span><span>in</span><span> [</span><span>1</span><span>, </span><span>3</span><span>, </span><span>9</span><span>, </span><span>19</span><span>]:</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>s </span><span>=</span><span> </span><span>partial_sum</span><span>(a0, a, b, x, n)</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>err </span><span>=</span><span> np.</span><span>sqrt</span><span>(</span><span>inner</span><span>((f </span><span>-</span><span> s) </span><span>**</span><span> </span><span>2</span><span><span>, np.</span><span>ones_like</span><span>(x), x))</span></span></div></div><div><div><div>43</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"  取到 </span><span>{</span><span>n</span><span>:2d</span><span>}</span><span> 次谐波：L2 误差 = </span><span>{</span><span>err</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span># 方波的理论值：b_k = 4 / (π k)（k 为奇数），偶数项为 0</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>_, _, b </span><span>=</span><span> </span><span>fourier</span><span>(square, x, </span><span>5</span><span>)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"方波 b_k 的数值："</span><span>, b)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span><span>"理论 4/(πk)："</span><span>, [</span><span>round</span><span>(</span><span>4</span><span><span> </span><span>/</span><span> (np.pi </span><span>*</span><span> k), </span></span><span>4</span><span>) </span><span>for</span><span> k </span><span>in</span><span> (</span><span>1</span><span>, </span><span>3</span><span>, </span><span>5</span><span>)])</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>    </span><span># 方波在跳变点附近的过冲（Gibbs 现象）</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>a0, a, b </span><span>=</span><span> </span><span>fourier</span><span>(square, x, </span><span>19</span><span>)</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>peak </span><span>=</span><span> </span><span>partial_sum</span><span>(a0, a, b, x, </span><span>19</span><span><span>).</span><span>max</span><span>()</span></span></div></div><div><div><div>53</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"方波 19 次部分和的最大值：</span><span>{</span><span>peak</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>方波：a0 = -0.0000</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>a1..a5 = [ 0. -0.  0. -0.  0.]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>b1..b5 = [ 1.2732 -0.      0.4244  0.      0.2546]</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>取到  1 次谐波：L2 误差 = 1.0907</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>取到  3 次谐波：L2 误差 = 0.7898</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>取到  9 次谐波：L2 误差 = 0.5032</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>取到 19 次谐波：L2 误差 = 0.3558</span></div></div><div><div><div>8</div></div><div><span>三角波：a0 = 1.5708</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>a1..a5 = [ 1.2732 -0.      0.1415 -0.      0.0509]</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>b1..b5 = [-0. -0. -0. -0. -0.]</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>取到  1 次谐波：L2 误差 = 0.2734</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>取到  3 次谐波：L2 误差 = 0.1090</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>取到  9 次谐波：L2 误差 = 0.0289</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>取到 19 次谐波：L2 误差 = 0.0103</span></div></div><div><div><div>15</div></div><div><span>方波 b_k 的数值： [ 1.2732 -0.      0.4244  0.      0.2546]</span></div></div><div><div><div>16</div></div><div><span>理论 4/(πk)： [1.2732, 0.4244, 0.2546]</span></div></div><div><div><div>17</div></div><div><span>方波 19 次部分和的最大值：1.1798</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li>方波是奇函数，所有 <span><span>aka_k</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都是 0（打印出来的 <span><span>±0\pm 0</span><span><span><span></span><span>±</span><span>0</span></span></span></span> 是舍入噪声）；偶数次的 <span><span>bkb_k</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 也是 0，只剩奇数次，数值积分给出 <span><span>b1,b3,b5=1.2732,0.4244,0.2546b_1, b_3, b_5 = 1.2732, 0.4244, 0.2546</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.2732</span><span>,</span><span></span><span>0.4244</span><span>,</span><span></span><span>0.2546</span></span></span></span>，与理论值 <span><span>4/(πk)4/(\pi k)</span><span><span><span></span><span>4/</span><span>(</span><span>π</span><span>k</span><span>)</span></span></span></span> 一致。</li>
<li>三角波关于 <span><span>x=πx = \pi</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>π</span></span></span></span> 对称，所有 <span><span>bkb_k</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都是 0；平均值 <span><span>a0=1.5708=π/2a_0 = 1.5708 = \pi/2</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.5708</span><span></span><span>=</span><span></span></span><span><span></span><span>π</span><span>/2</span></span></span></span>；奇数次的 <span><span>aka_k</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>1.2732,0.1415,0.05091.2732, 0.1415, 0.0509</span><span><span><span></span><span>1.2732</span><span>,</span><span></span><span>0.1415</span><span>,</span><span></span><span>0.0509</span></span></span></span>，即 <span><span>4/(πk2)4/(\pi k^2)</span><span><span><span></span><span>4/</span><span>(</span><span>π</span><span><span>k</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，衰减得比方波的 <span><span>4/(πk)4/(\pi k)</span><span><span><span></span><span>4/</span><span>(</span><span>π</span><span>k</span><span>)</span></span></span></span> 快得多。</li>
<li>取到 19 次谐波，三角波的误差是 0.0103，方波是 0.3558，相差约 35 倍。尖角还连得上，跳变连不上，高次项就得一直陪着。</li>
<li>方波 19 次部分和的最大值是 1.1798，而真值是 1。多取几项过冲不会消失，只是被挤到跳变点附近（Gibbs 现象，我是从别处看到的，这里只验证了 19 项的峰值）。</li>
</ul><p>我觉得这就像一块三棱镜：一束白光进去，按频率分成一排纯色，每一种颜色是多少，就是 <span><span>aka_k</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>bkb_k</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的大小。区别是棱镜分的是光，Fourier 级数分的是函数，而分的依据恰好是“正交”。</p><p>这一讲的两部分其实在谈同一个对象：一组特别好用的基。Markov 矩阵的特征向量是让 <span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 变简单的基，<span><span>sin⁡kx\sin kx</span><span><span><span></span><span>sin</span><span></span><span>k</span><span>x</span></span></span></span>、<span><span>cos⁡kx\cos kx</span><span><span><span></span><span>cos</span><span></span><span>k</span><span>x</span></span></span></span> 是让展开变简单的基。第 26 讲讲 Fourier 矩阵时，这两条线会撞到一起：Fourier 矩阵归一化之后是酉矩阵，也就是复数世界里的正交矩阵。</p><p>下一讲不是新内容，是第二次测验的复习，他在结尾说周一复习、周三考试。我会逐题用 NumPy 验证他讲的题，见 <a href="/posts/linear-algebra-24b-quiz-2-review/">18.06 第 24b 讲：第二次测验复习</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-23-differential-equations-exp-at/</id>
      <title type="text">18.06 第 23 讲：微分方程与 exp(At)</title>
      <published>2026-10-18T07:20:00.000Z</published>
      <updated>2026-10-18T07:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-23-differential-equations-exp-at/"/>
      <summary type="text">MIT 18.06 第 23 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 du/dt = Au 的解 exp(At)u(0) 用特征分解、Taylor 级数和缩放平方各算了一遍，t = 20 时 60 项 Taylor 的误差是 3.11e+24，缩放平方是 1.05e-15。</summary>
      <content type="html"><![CDATA[<p>上一讲 Strang 用特征值算 <span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span>，这一讲把离散的 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 换成连续的 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span>：<span><span>du/dt=Au\mathrm{d}\mathbf{u}/\mathrm{d}t = A\mathbf{u}</span><span><span><span></span><span>d</span><span>u</span><span>/</span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span></span></span></span> 的解是 <span><span>u(t)=eAtu(0)\mathbf{u}(t) = e^{At}\mathbf{u}(0)</span><span><span><span></span><span>u</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span><span>u</span><span>(</span><span>0</span><span>)</span></span></span></span>，而 <span><span>eAt=SeΛtS−1e^{At} = S e^{\Lambda t} S^{-1}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>Λ</span><span>t</span></span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>。公式很漂亮，但我顺手拿“把 Taylor 级数直接加起来”这个定义去算，在 <span><span>t=20t = 20</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 时 60 项的结果误差是 3.11e+24，而缩放平方只差 1.05e-15：定义是好定义，拿来当算法就不行了。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=IZqwi0wJovM" target="_blank">Lecture 23: Differential equations and exp(At)</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-23-differential-equations-and-exp-at/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §6.5 Solving Linear Differential Equations
前置：<a href="/posts/linear-algebra-22-diagonalization-powers/">第 22 讲：对角化与矩阵的幂</a>
一句话：<span><span>du/dt=Au\mathrm{d}\mathbf{u}/\mathrm{d}t = A\mathbf{u}</span><span><span><span></span><span>d</span><span>u</span><span>/</span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span></span></span></span> 的解由纯指数解 <span><span>eλtxe^{\lambda t}\mathbf{x}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span><span>x</span></span></span></span> 组成，系数由 <span><span>Sc=u(0)S\mathbf{c} = \mathbf{u}(0)</span><span><span><span></span><span>S</span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>u</span><span>(</span><span>0</span><span>)</span></span></span></span> 决定，是否收敛只看特征值的实部是否为负。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>前半节课的黑板例子（<span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 矩阵，特征值 0 和 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span>）值得认真看：他怎么先“目测”特征值，再预言解的样子。本篇第一节用代码把它算了一遍。</li>
<li>中间关于稳定性的几分钟（实部为负、复数的虚部只转圈、<span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 看迹与行列式）是整讲的重点，本篇第二节验证了这些结论，不过他的口头论证比我的数字更好懂，建议原速看。</li>
<li>定义 <span><span>eAte^{At}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 的那段（Taylor 级数、几何级数的旁白、<span><span>SeΛtS−1S e^{\Lambda t} S^{-1}</span><span><span><span></span><span>S</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>Λ</span><span>t</span></span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的推导）一步一步推，慢慢写下来最好；我的代码只验证结果，可以倍速。</li>
<li>最后一个例子是把二阶方程变成 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 方程组，只有几分钟，本篇最后一节用代码重算了一遍，还顺手做了五阶的情形。</li>
</ul></div></div>
<section><h2>一个例子：从特征值预言整条解<a href="#一个例子从特征值预言整条解"><span>#</span></a></h2><p>Strang 先把方程摆出来：</p><span><span><span>dudt=Au,A=[−121−2],u(0)=[10]\frac{\mathrm{d}\mathbf{u}}{\mathrm{d}t} = A\mathbf{u},\qquad
A = \begin{bmatrix} -1 &amp; 2 \\ 1 &amp; -2 \end{bmatrix},\qquad
\mathbf{u}(0) = \begin{bmatrix} 1 \\ 0 \end{bmatrix}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>d</span><span>t</span></span></span><span><span></span><span></span></span><span><span></span><span><span>d</span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span><span>,</span><span></span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span>u</span><span>(</span><span>0</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p><strong>为什么要从特征值开始？</strong> 因为常系数线性方程的解是指数函数，找指数函数只需要知道“指数上是什么、前面乘什么”，这正是特征值和特征向量。他还没解方程就先看矩阵：第二列是第一列的 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 倍，所以矩阵奇异，<span><span>λ1=0\lambda_1 = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；迹是 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span>，所以另一个特征值 <span><span>λ2=−3\lambda_2 = -3</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3</span></span></span></span>。他在这里就预言了：解里会有一项 <span><span>e0te^{0t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>0</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>（常数，稳态）和一项 <span><span>e−3te^{-3t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>（很快消失）。</p><p>特征向量：<span><span>λ1=0\lambda_1 = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 对应零空间里的 <span><span>x1=(2,1)\mathbf{x}_1 = (2, 1)</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>；<span><span>λ2=−3\lambda_2 = -3</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3</span></span></span></span> 对应 <span><span>A+3IA + 3I</span><span><span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>I</span></span></span></span> 的零空间，<span><span>x2=(1,−1)\mathbf{x}_2 = (1, -1)</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>。解是两个纯指数解的组合：</p><span><span><span>u(t)=c1e0t[21]+c2e−3t[1−1]\mathbf{u}(t) = c_1 e^{0t}\begin{bmatrix} 2 \\ 1 \end{bmatrix} +
c_2 e^{-3t}\begin{bmatrix} 1 \\ -1 \end{bmatrix}</span><span><span><span></span><span>u</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>0</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>他先验证了纯指数解确实满足方程：左边求导落下一个 <span><span>λ1\lambda_1</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，右边 <span><span>Aeλ1tx1=eλ1tλ1x1A e^{\lambda_1 t}\mathbf{x}_1 = e^{\lambda_1 t}\lambda_1\mathbf{x}_1</span><span><span><span></span><span>A</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，两边一样。这和上一讲的“纯幂”<span><span>λkx\lambda^k\mathbf{x}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>x</span></span></span></span> 是同一件事，只是 <span><span>λk\lambda^k</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 换成了 <span><span>eλte^{\lambda t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p>系数 <span><span>c1,c2c_1, c_2</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 由初值定：<span><span>t=0t = 0</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时 <span><span>c1x1+c2x2=u(0)c_1\mathbf{x}_1 + c_2\mathbf{x}_2 = \mathbf{u}(0)</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>u</span><span>(</span><span>0</span><span>)</span></span></span></span>，矩阵形式就是 <span><span>Sc=u(0)S\mathbf{c} = \mathbf{u}(0)</span><span><span><span></span><span>S</span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>u</span><span>(</span><span>0</span><span>)</span></span></span></span>，<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 是特征向量矩阵。解出来 <span><span>c1=c2=13c_1 = c_2 = \tfrac13</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。下面的脚本做这几件事，并且用四阶 Runge-Kutta 直接对微分方程积分，和公式对一次：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># Strang 黑板上的例子：du/dt = A u，u(0) = (1, 0)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>u0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>lam, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, lam)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹 ="</span><span><span>, np.</span><span>trace</span><span>(A), </span></span><span>"，行列式 ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(A), </span></span><span>12</span><span>))</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 他取的特征向量：lambda = 0 对应 (2, 1)，lambda = -3 对应 (1, -1)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x1 ="</span><span><span>, A </span><span>@</span><span> X[:, </span></span><span>0</span><span>], </span><span>"，A @ x2 ="</span><span><span>, A </span><span>@</span><span> X[:, </span></span><span>1</span><span>])</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># S c = u(0)：解出每个纯指数解在初值里占多少</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(X, u0)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"c ="</span><span>, c)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>def</span><span> </span><span>u_exact</span><span>(</span><span>t</span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>23</div></div><div><span>        </span><span># u(t) = c1 e^{0 t} x1 + c2 e^{-3 t} x2</span></div></div><div><div><div>24</div></div><div><span>        </span><span>return</span><span> c[</span><span>0</span><span><span>] </span><span>*</span><span> X[:, </span></span><span>0</span><span><span>] </span><span>+</span><span> c[</span></span><span>1</span><span><span>] </span><span>*</span><span> np.</span><span>exp</span><span>(</span><span>-</span></span><span>3.0</span><span><span> </span><span>*</span><span> t) </span><span>*</span><span> X[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 用四阶 Runge-Kutta 直接积分微分方程，对照公式</span></div></div><div><div><div>27</div></div><div><span>    </span><span>def</span><span> </span><span>rk4</span><span>(</span><span>u</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>h</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>k1 </span><span>=</span><span> A </span><span>@</span><span> u</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>k2 </span><span>=</span><span> A </span><span>@</span><span> (u </span><span>+</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> h </span><span>*</span><span> k1)</span></span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>k3 </span><span>=</span><span> A </span><span>@</span><span> (u </span><span>+</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> h </span><span>*</span><span> k2)</span></span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>k4 </span><span>=</span><span> A </span><span>@</span><span> (u </span><span>+</span><span> h </span><span>*</span><span> k3)</span></div></div><div><div><div>32</div></div><div><span>        </span><span>return</span><span><span> u </span><span>+</span><span> h </span><span>/</span><span> </span></span><span>6.0</span><span><span> </span><span>*</span><span> (k1 </span><span>+</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> k2 </span><span>+</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> k3 </span><span>+</span><span> k4)</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> u0.</span><span>copy</span><span>()</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> </span><span>0.01</span></div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> step </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>1001</span><span>):</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>rk4</span><span>(u, h)</span></div></div><div><div><div>38</div></div><div><span>        </span><span>if</span><span> step </span><span>in</span><span> (</span><span>10</span><span>, </span><span>100</span><span>, </span><span>1000</span><span>):</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>t </span><span>=</span><span> step </span><span>*</span><span> h</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(u </span><span>-</span><span> </span><span>u_exact</span><span>(t))</span></div></div><div><div><div>41</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"t = </span><span>{</span><span>t</span><span>:5.2f</span><span>}</span><span>  u = </span><span>{</span><span><span>u_exact</span><span>(t)</span></span><span>}</span><span>  与 RK4 的差 = </span><span>{</span><span>err</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>steady </span><span>=</span><span> c[</span><span>0</span><span><span>] </span><span>*</span><span> X[:, </span></span><span>0</span><span>]</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"稳态 u(inf) ="</span><span>, steady)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"等于 (2/3, 1/3)："</span><span><span>, np.</span><span>allclose</span><span>(steady, [</span></span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>, </span><span>1</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>]))</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>特征值： [ 0. -3.]</span></div></div><div><div><div>2</div></div><div><span>迹 = -3.0 ，行列式 = 0.0</span></div></div><div><div><div>3</div></div><div><span>A @ x1 = [0. 0.] ，A @ x2 = [-3.  3.]</span></div></div><div><div><div>4</div></div><div><span>c = [0.3333 0.3333]</span></div></div><div><div><div>5</div></div><div><span>t =  0.10  u = [0.9136 0.0864]  与 RK4 的差 = 7.25e-10</span></div></div><div><div><div>6</div></div><div><span>t =  1.00  u = [0.6833 0.3167]  与 RK4 的差 = 4.87e-10</span></div></div><div><div><div>7</div></div><div><span>t = 10.00  u = [0.6667 0.3333]  与 RK4 的差 = 4.97e-16</span></div></div><div><div><div>8</div></div><div><span>稳态 u(inf) = [0.6667 0.3333]</span></div></div><div><div><div>9</div></div><div><span>等于 (2/3, 1/3)： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>特征值是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span> 和 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span>，和他心算的一样。<span><span>c1=c2=0.3333c_1 = c_2 = 0.3333</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3333</span></span></span></span>。更值得看的是最后几行：公式和 RK4 在 <span><span>t=0.1t = 0.1</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span> 时只差 7.25e-10（这差的是 RK4 的截断误差，步长 0.01），<span><span>t=10t = 10</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时 <span><span>e−30e^{-30}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>30</span></span></span></span></span></span></span></span></span></span></span></span> 已经小到两者几乎无差，稳态就是 <span><span>c1x1=(2/3,1/3)c_1\mathbf{x}_1 = (2/3, 1/3)</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2/3</span><span>,</span><span></span><span>1/3</span><span>)</span></span></span></span>。</p><p>这就是他说的“流”：一开始所有东西都在 <span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里（<span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>），随着时间推移，一部分流到 <span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，最后停在 <span><span>(2/3,1/3)(2/3, 1/3)</span><span><span><span></span><span>(</span><span>2/3</span><span>,</span><span></span><span>1/3</span><span>)</span></span></span></span>。总量 <span><span>u1+u2u_1 + u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 一直是 1，不变，这和零特征值的左零空间有关，下一讲的 Markov 矩阵会把这件事讲透。</p><p></p><figure><img src="/assets/images/linear-algebra/l23-phase-plane.svg" alt="左：A 的轨迹，所有轨迹沿 (1, -1) 方向落到特征值 0 的特征向量那条线上。右：二阶方程 y'' + 0.4y' + 4y = 0 在 (y, y') 平面上的螺旋" /><figcaption>左：A 的轨迹，所有轨迹沿 (1, -1) 方向落到特征值 0 的特征向量那条线上。右：二阶方程 y'' + 0.4y' + 4y = 0 在 (y, y') 平面上的螺旋</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>def</span><span> </span><span>rk4_path</span><span>(</span><span>A</span><span>,</span><span><span> </span><span>u0</span></span><span>,</span><span><span> </span><span>T</span></span><span>,</span><span><span> </span><span>n</span></span><span>):</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>h </span><span>=</span><span> T </span><span>/</span><span> n</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>path </span><span>=</span><span> [u0]</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> u0</span></div></div><div><div><div>14</div></div><div><span>        </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>k1 </span><span>=</span><span> A </span><span>@</span><span> u</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>k2 </span><span>=</span><span> A </span><span>@</span><span> (u </span><span>+</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> h </span><span>*</span><span> k1)</span></span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>k3 </span><span>=</span><span> A </span><span>@</span><span> (u </span><span>+</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> h </span><span>*</span><span> k2)</span></span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>k4 </span><span>=</span><span> A </span><span>@</span><span> (u </span><span>+</span><span> h </span><span>*</span><span> k3)</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>u </span><span>=</span><span> u </span><span>+</span><span> h </span><span>/</span><span> </span><span>6.0</span><span><span> </span><span>*</span><span> (k1 </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> k2 </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> k3 </span><span>+</span><span> k4)</span></span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>path.</span><span>append</span><span>(u)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>(path)</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>fig, (ax1, ax2) </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 左：Strang 的例子，特征向量 (2, 1) 与 (1, -1)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>]])</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>-</span><span>1.5</span><span>, </span><span>1.5</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(</span></div></div><div><div><div>29</div></div><div><span>        </span><span>2</span><span><span> </span><span>*</span><span> s,</span></span></div></div><div><div><div>30</div></div><div><span>        </span><span>1</span><span><span> </span><span>*</span><span> s,</span></span></div></div><div><div><div>31</div></div><div><span>        </span><span>"--"</span><span>,</span></div></div><div><div><div>32</div></div><div><span>        </span><span>color</span><span>=</span><span>"tab:red"</span><span>,</span></div></div><div><div><div>33</div></div><div><span>        </span><span>lw</span><span>=</span><span>1</span><span>,</span></div></div><div><div><div>34</div></div><div><span>        </span><span>label</span><span>=</span><span>r</span><span><span>"$x_1=</span><span>(</span><span>2,1</span><span>)</span><span>$, $</span><span>\l</span><span>ambda=0$"</span></span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(</span></div></div><div><div><div>37</div></div><div><span>        </span><span>1</span><span><span> </span><span>*</span><span> s </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>,</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>-</span><span>1</span><span><span> </span><span>*</span><span> s </span><span>+</span><span> </span></span><span>1</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>,</span></div></div><div><div><div>39</div></div><div><span>        </span><span>"--"</span><span>,</span></div></div><div><div><div>40</div></div><div><span>        </span><span>color</span><span>=</span><span>"tab:green"</span><span>,</span></div></div><div><div><div>41</div></div><div><span>        </span><span>lw</span><span>=</span><span>1</span><span>,</span></div></div><div><div><div>42</div></div><div><span>        </span><span>label</span><span>=</span><span>r</span><span><span>"$x_2=</span><span>(</span><span>1,-1</span><span>)</span><span>$, $</span><span>\l</span><span>ambda=-3$"</span></span><span>,</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>for</span><span> u0 </span><span>in</span><span> [</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>(</span><span>1</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>(</span><span>0</span><span>, </span><span>1</span><span>),</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>(</span><span>-</span><span>0.5</span><span>, </span><span>0.2</span><span>),</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>(</span><span>1.5</span><span>, </span><span>1.0</span><span>),</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>(</span><span>0.2</span><span><span>, </span><span>-</span></span><span>0.8</span><span>),</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>(</span><span>-</span><span>1</span><span><span>, </span><span>-</span></span><span>0.5</span><span>),</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>]:</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> </span><span>rk4_path</span><span>(A, np.</span><span>array</span><span>(u0, </span><span>float</span><span>), </span><span>3.0</span><span>, </span><span>300</span><span>)</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>ax1.</span><span>plot</span><span>(p[:, </span><span>0</span><span>], p[:, </span><span>1</span><span>], </span><span>color</span><span>=</span><span>"tab:blue"</span><span>, </span><span>lw</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>ax1.</span><span>plot</span><span>(*u0, </span><span>"o"</span><span>, </span><span>color</span><span>=</span><span>"tab:blue"</span><span>, </span><span>ms</span><span>=</span><span>3</span><span>)</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> </span><span>rk4_path</span><span>(A, np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>]), </span><span>3.0</span><span>, </span><span>300</span><span>)</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(p[:, </span><span>0</span><span>], p[:, </span><span>1</span><span>], </span><span>color</span><span>=</span><span>"k"</span><span>, </span><span>lw</span><span>=</span><span>2</span><span>)</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(</span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>, </span><span>1</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>, </span><span>"k*"</span><span>, </span><span>ms</span><span>=</span><span>10</span><span>)</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span><span>"u(0) = (1, 0)"</span><span>, (</span><span>1</span><span>, </span><span>0</span><span>), (</span><span>1.25</span><span>, </span><span>0.15</span><span>))</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span><span>"u(inf) = (2/3, 1/3)"</span><span>, (</span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>, </span><span>1</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>), (</span><span>1.0</span><span>, </span><span>0.7</span><span>))</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlim</span><span>(</span><span>-</span><span>1.5</span><span>, </span><span>2.0</span><span>)</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylim</span><span>(</span><span>-</span><span>1.0</span><span>, </span><span>1.5</span><span>)</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlabel</span><span>(</span><span>"$u_1$"</span><span>)</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylabel</span><span>(</span><span>"$u_2$"</span><span>)</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_title</span><span>(</span><span>"Real eigenvalues 0 and -3: trajectories end on a line"</span><span>)</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>ax1.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"lower right"</span><span>, </span><span>fontsize</span><span>=</span><span>8</span><span>)</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>    </span><span># 右：y'' + 0.4 y' + 4 y = 0，u = (y', y)</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>0.4</span><span><span>, </span><span>-</span></span><span>4.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> </span><span>rk4_path</span><span>(B, np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>1.0</span><span>]), </span><span>15.0</span><span>, </span><span>3000</span><span>)</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(p[:, </span><span>1</span><span>], p[:, </span><span>0</span><span>], </span><span>color</span><span>=</span><span>"tab:blue"</span><span>, </span><span>lw</span><span>=</span><span>1.2</span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(</span><span>1</span><span>, </span><span>0</span><span>, </span><span>"o"</span><span>, </span><span>color</span><span>=</span><span>"k"</span><span>, </span><span>ms</span><span>=</span><span>4</span><span>)</span></div></div><div><div><div>73</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(</span><span>0</span><span>, </span><span>0</span><span>, </span><span>"k*"</span><span>, </span><span>ms</span><span>=</span><span>10</span><span>)</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlabel</span><span>(</span><span>"$y$"</span><span>)</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylabel</span><span>(</span><span>"$y'$"</span><span>)</span></div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_title</span><span>(</span><span>r</span><span><span>"Complex eigenvalues $-0.2 </span><span>\p</span><span>m 1.99i$: a spiral"</span></span><span>)</span></div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>78</div></div><div><span>    </span><span>for</span><span> ax </span><span>in</span><span> (ax1, ax2):</span></div></div><div><div><div>79</div></div><div><span><span>        </span></span><span>ax.</span><span>grid</span><span>(</span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>82</div></div><div><span>        </span><span>"l23-phase-plane.svg"</span><span>,</span></div></div><div><div><div>83</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>85</div></div><div>
</div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>左图是 <span><span>u(t)\mathbf{u}(t)</span><span><span><span></span><span>u</span><span>(</span><span>t</span><span>)</span></span></span></span> 在 <span><span>(u1,u2)(u_1, u_2)</span><span><span><span></span><span>(</span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 平面上的轨迹。每条线都沿 <span><span>x2=(1,−1)\mathbf{x}_2 = (1, -1)</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 的方向走向红色虚线（<span><span>λ=0\lambda = 0</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的特征向量，也就是 <span><span>Au=0A\mathbf{u} = \mathbf{0}</span><span><span><span></span><span>A</span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的所有点），<span><span>e−3te^{-3t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 的那个分量被压没了。粗线是从 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 出发的那条。右图留到最后一节再说。</p></section>
<section><h2>稳定性：只看实部<a href="#稳定性只看实部"><span>#</span></a></h2><p><strong>什么时候解会趋于零？</strong> 他的回答是：所有特征值的实部为负。这里他停下来专门讲了复数：如果 <span><span>λ=−3+6i\lambda = -3 + 6i</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>6</span><span>i</span></span></span></span>，那 <span><span>eλt=e−3t(cos⁡6t+isin⁡6t)e^{\lambda t} = e^{-3t}(\cos 6t + i\sin 6t)</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>t</span></span></span></span></span></span></span></span></span><span>(</span><span>cos</span><span></span><span>6</span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>i</span><span></span><span>sin</span><span></span><span>6</span><span>t</span><span>)</span></span></span></span>，后面那个因子的模恒为 1，只在单位圆上转，真正管增长和衰减的是 <span><span>e−3te^{-3t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>。虚部只负责在两个分量之间来回摆动。</p><p>三种情形：</p><ul>
<li>所有特征值实部为负：<span><span>u(t)→0\mathbf{u}(t) \to \mathbf{0}</span><span><span><span></span><span>u</span><span>(</span><span>t</span><span>)</span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span>，稳定。</li>
<li>有一个特征值是 0，其余实部为负：趋于稳态，就像刚才的例子。</li>
<li>任何一个特征值实部为正：爆炸。</li>
</ul><p>他还指出把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 变成 <span><span>−A-A</span><span><span><span></span><span>−</span><span>A</span></span></span></span> 的效果：所有特征值变号，稳定的变成爆炸的，只有 0 还是 0。</p><p>对 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 矩阵，他给了不用解特征值的判据。两个特征值实部都为负，迹 <span><span>=λ1+λ2= \lambda_1 + \lambda_2</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 必须为负（复数共轭对的虚部相加时消掉）。但光迹为负不够：他举的例子是 <span><span>[−2001]\begin{bmatrix} -2 &amp; 0 \\ 0 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，迹 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，特征值 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 和 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>，照样爆炸。还需要行列式 <span><span>=λ1λ2&gt;0= \lambda_1\lambda_2 &gt; 0</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>，两个都是负的，乘起来才是正的。结论：迹 <span><span>&lt;0&lt; 0</span><span><span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 且行列式 <span><span>&gt;0&gt; 0</span><span><span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>。</p><p>我用代码验证这些说法，并且拿 20000 个随机 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 矩阵检验迹与行列式的判据：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 复特征值：只有实部决定增长，虚部只负责转圈</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> </span><span>-</span><span>3.0</span><span><span> </span><span>+</span><span> </span></span><span>6.0</span><span>j</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> (</span><span>0.0</span><span>, </span><span>0.5</span><span>, </span><span>1.0</span><span>):</span></div></div><div><div><div>11</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>12</div></div><div><span>            </span><span>f</span><span>"t = </span><span>{</span><span>t</span><span>}</span><span>：|e^(lam t)| = </span><span>{</span><span>abs</span><span><span>(np.</span><span>exp</span><span>(lam </span><span>*</span><span> t))</span></span><span>:.4f</span><span>}</span><span>，"</span></div></div><div><div><div>13</div></div><div><span>            </span><span>f</span><span>"e^(-3t) = </span><span>{</span><span><span>np.</span><span>exp</span><span>(</span><span>-</span></span><span>3.0</span><span><span> </span><span>*</span><span> t)</span></span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># 迹为负仍然会爆炸的例子</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>2.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的迹 ="</span><span><span>, np.</span><span>trace</span><span>(B), </span></span><span>"，特征值 ="</span><span><span>, np.linalg.</span><span>eigvals</span><span>(B))</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 反号：A 稳定，-A 就爆炸</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>]])</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的特征值："</span><span><span>, np.linalg.</span><span>eigvals</span><span>(A))</span></span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"-A 的特征值："</span><span><span>, np.linalg.</span><span>eigvals</span><span>(</span><span>-</span><span>A))</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 2 x 2 判据：迹 &lt; 0 且 行列式 &gt; 0，等价于两个实部都为负</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>count </span><span>=</span><span> </span><span>20000</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>agree </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>stable </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(count):</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>by_eig </span><span>=</span><span> np.</span><span>all</span><span>(np.linalg.</span><span>eigvals</span><span>(M).real </span><span>&lt;</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>by_trace_det </span><span>=</span><span> np.</span><span>trace</span><span>(M) </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>and</span><span><span> np.linalg.</span><span>det</span><span>(M) </span><span>&gt;</span><span> </span></span><span>0</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>agree </span><span>+=</span><span> </span><span>int</span><span><span>(by_eig </span><span>==</span><span> by_trace_det)</span></span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>stable </span><span>+=</span><span> </span><span>int</span><span>(by_eig)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"随机 2 x 2 矩阵 </span><span>{</span><span>count</span><span>}</span><span> 个，两种判断一致：</span><span>{</span><span>agree</span><span>}</span><span> 个"</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"其中稳定的占 </span><span>{</span><span><span>stable </span><span>/</span><span> count</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span># 只看迹：1 / 2 的随机矩阵迹为负，但稳定的只有大约 1 / 4</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>neg_trace </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(count):</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>neg_trace </span><span>+=</span><span> </span><span>int</span><span><span>(np.</span><span>trace</span><span>(M) </span><span>&lt;</span><span> </span></span><span>0</span><span>)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"迹为负的占 </span><span>{</span><span><span>neg_trace </span><span>/</span><span> count</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>t = 0.0：|e^(lam t)| = 1.0000，e^(-3t) = 1.0000</span></div></div><div><div><div>2</div></div><div><span>t = 0.5：|e^(lam t)| = 0.2231，e^(-3t) = 0.2231</span></div></div><div><div><div>3</div></div><div><span>t = 1.0：|e^(lam t)| = 0.0498，e^(-3t) = 0.0498</span></div></div><div><div><div>4</div></div><div><span>B 的迹 = -1.0 ，特征值 = [-2.  1.]</span></div></div><div><div><div>5</div></div><div><span>A 的特征值： [ 0. -3.]</span></div></div><div><div><div>6</div></div><div><span>-A 的特征值： [0. 3.]</span></div></div><div><div><div>7</div></div><div><span>随机 2 x 2 矩阵 20000 个，两种判断一致：20000 个</span></div></div><div><div><div>8</div></div><div><span>其中稳定的占 0.2463</span></div></div><div><div><div>9</div></div><div><span>迹为负的占 0.5040</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>∣eλt∣|e^{\lambda t}|</span><span><span><span></span><span>∣</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span><span>∣</span></span></span></span> 和 <span><span>e−3te^{-3t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 在三个时刻一模一样，<span><span>t=1t = 1</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时都是 0.0498：虚部 6 一点贡献也没有。<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的迹是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，特征值却是 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 和 <span><span>11</span><span><span><span></span><span>1</span></span></span></span>。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span> 和 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span>，换成 <span><span>−A-A</span><span><span><span></span><span>−</span><span>A</span></span></span></span> 就是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span> 和 <span><span>33</span><span><span><span></span><span>3</span></span></span></span>，和他说的“符号一变，稳定变爆炸”对得上，而 <span><span>00</span><span><span><span></span><span>0</span></span></span></span> 还是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>，稳态那一项不受影响，只是另一项从 <span><span>e−3te^{-3t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 变成了 <span><span>e3te^{3t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>3</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>。最关键的是 20000 个随机矩阵上，“迹 <span><span>&lt;0&lt; 0</span><span><span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 且行列式 <span><span>&gt;0&gt; 0</span><span><span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>”和“两个特征值实部都为负”一致 20000 次，没有一次例外。</p><p>另一个数字我觉得有意思：随机矩阵里迹为负的占 0.5040，大约一半，而真正稳定的只有 0.2463，大约四分之一。也就是说只看迹，会把稳定的数量高估一倍左右（我猜这就是他要补一个行列式条件的原因，当然这只是我的解读）。</p><p>稳定的区域画在复平面上，Strang 画了两个：对微分方程 <span><span>du/dt=Au\mathrm{d}\mathbf{u}/\mathrm{d}t = A\mathbf{u}</span><span><span><span></span><span>d</span><span>u</span><span>/</span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span></span></span></span>，特征值要在左半平面，<span><span>Re⁡λ&lt;0\operatorname{Re}\lambda &lt; 0</span><span><span><span></span><span><span>Re</span></span><span></span><span>λ</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span>；对差分方程 <span><span>uk+1=Auk\mathbf{u}_{k+1} = A\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（上一讲），特征值要在单位圆内，<span><span>∣λ∣&lt;1|\lambda| &lt; 1</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span>。一个看实部，一个看模。</p></section>
<section><h2>矩阵指数：把 Taylor 级数抄到矩阵上<a href="#矩阵指数把-taylor-级数抄到矩阵上"><span>#</span></a></h2><p>解的形式是 <span><span>u(t)=SeΛtS−1u(0)\mathbf{u}(t) = S e^{\Lambda t} S^{-1}\mathbf{u}(0)</span><span><span><span></span><span>u</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>Λ</span><span>t</span></span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>u</span><span>(</span><span>0</span><span>)</span></span></span></span>，他的推导是：令 <span><span>u=Sv\mathbf{u} = S\mathbf{v}</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>v</span></span></span></span>，代进 <span><span>u′=Au\mathbf{u}' = A\mathbf{u}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span></span></span></span> 得 <span><span>Sv′=ASvS\mathbf{v}' = AS\mathbf{v}</span><span><span><span></span><span>S</span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>S</span><span>v</span></span></span></span>，左乘 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 得 <span><span>v′=Λv\mathbf{v}' = \Lambda\mathbf{v}</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Λ</span><span>v</span></span></span></span>，也就是 <span><span>vi′=λiviv_i' = \lambda_i v_i</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个互不相干的标量方程。特征向量就是把耦合的方程组“解耦”的基，解是 <span><span>v(t)=eΛtv(0)\mathbf{v}(t) = e^{\Lambda t}\mathbf{v}(0)</span><span><span><span></span><span>v</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>Λ</span><span>t</span></span></span></span></span></span></span></span></span><span>v</span><span>(</span><span>0</span><span>)</span></span></span></span>，再换回去。</p><p>那 <span><span>eAte^{At}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 到底是什么？他回到最漂亮的 Taylor 级数：</p><span><span><span>eAt=I+At+(At)22+(At)36+⋯=∑n=0∞(At)nn!e^{At} = I + At + \frac{(At)^2}{2} + \frac{(At)^3}{6} + \cdots
= \sum_{n=0}^{\infty}\frac{(At)^n}{n!}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>A</span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>A</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>A</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>n</span><span>=</span><span>0</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>∞</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>n</span><span>!</span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>A</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>然后把 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 代进去。<span><span>A2=SΛ2S−1A^2 = S\Lambda^2 S^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，中间的 <span><span>S−1SS^{-1}S</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>S</span></span></span></span> 抵消；把单位矩阵写成 <span><span>SS−1SS^{-1}</span><span><span><span></span><span>S</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，左边提出一个 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span>，右边提出一个 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，中间剩下 <span><span>I+Λt+(Λt)2/2+⋯=eΛtI + \Lambda t + (\Lambda t)^2/2 + \cdots = e^{\Lambda t}</span><span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>Λ</span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>Λ</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/2</span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>Λ</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>，一个对角矩阵，对角线上是 <span><span>eλ1t,…,eλnte^{\lambda_1 t}, \dots, e^{\lambda_n t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>。所以</p><span><span><span>eAt=S eΛt S−1e^{At} = S\, e^{\Lambda t}\, S^{-1}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>Λ</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><p>前提是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 可对角化（有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个线性无关的特征向量），这一点他提醒了：不可对角化的矩阵就没有这个公式，那时只能变成三角形，之后再说（第 28 讲的 Jordan 标准形）。</p><p>他中间还有一段旁白：另一个“漂亮的 Taylor 级数”是不除阶乘的几何级数 <span><span>1+x+x2+⋯=1/(1−x)1 + x + x^2 + \cdots = 1/(1 - x)</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>x</span><span>)</span></span></span></span>，放到矩阵上是 <span><span>(I−At)−1=I+At+(At)2+⋯(I - At)^{-1} = I + At + (At)^2 + \cdots</span><span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>A</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>A</span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>A</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span></span></span></span>。但这个级数要特征值的模小于 1 才收敛，而指数级数除了 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span>，对任何矩阵、任何 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span> 都收敛。这是指数的一点“特权”。</p><p>我把三种算法都写出来：特征分解、直接累加 Taylor 级数、缩放平方（把 <span><span>AtAt</span><span><span><span></span><span>A</span><span>t</span></span></span></span> 先除以 <span><span>2s2^s</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span></span></span></span></span></span></span></span> 使它足够小，Taylor 之后再平方 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 次，利用 <span><span>eM=(eM/2)2e^{M} = (e^{M/2})^2</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>M</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>M</span><span>/2</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>）。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>expm_eig</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>t</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""用特征分解算 exp(At) = S exp(Lambda t) S^{-1}。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>lam, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> (S </span><span>*</span><span> np.</span><span>exp</span><span>(lam </span><span>*</span><span> t)) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S)</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>expm_taylor</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>t</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>terms</span></span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>60</span></span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>11</div></div><div><span>    </span><span>"""直接累加 I + At + (At)^2/2! + ...，共 terms 项。"""</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>term </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>total </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, terms):</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>term </span><span>=</span><span> term </span><span>@</span><span> (A </span><span>*</span><span> t) </span><span>/</span><span> k</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>total </span><span>=</span><span> total </span><span>+</span><span> term</span></div></div><div><div><div>18</div></div><div><span>    </span><span>return</span><span> total</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>def</span><span> </span><span>expm_scaling</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>t</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>22</div></div><div><span>    </span><span>"""缩放平方：先把 At 缩小 2^s 倍，Taylor 后再平方 s 次。"""</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> A </span><span>*</span><span> t</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>max</span><span>(</span><span>0</span><span>, </span><span>int</span><span><span>(np.</span><span>ceil</span><span>(np.</span><span>log2</span><span>(</span></span><span>max</span><span><span>(np.linalg.</span><span>norm</span><span>(B, </span></span><span>1</span><span>), </span><span>1e-16</span><span><span>)))) </span><span>+</span><span> </span></span><span>1</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> </span><span>expm_taylor</span><span>(B </span><span>/</span><span> </span><span>2</span><span><span>**</span><span>s, </span></span><span>1.0</span><span>, </span><span>terms</span><span>=</span><span>20</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(s):</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>E </span><span>=</span><span> E </span><span>@</span><span> E</span></div></div><div><div><div>28</div></div><div><span>    </span><span>return</span><span> E</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>]])</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># t = 1：三种方法互相对照</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>E1 </span><span>=</span><span> </span><span>expm_eig</span><span>(A, </span><span>1.0</span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>E2 </span><span>=</span><span> </span><span>expm_taylor</span><span>(A, </span><span>1.0</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>E3 </span><span>=</span><span> </span><span>expm_scaling</span><span>(A, </span><span>1.0</span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"exp(A) =</span><span>\n</span><span>"</span><span>, E1)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"Taylor 与特征分解：</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(E2 </span><span>-</span><span> E1)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"缩放平方与特征分解：</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(E3 </span><span>-</span><span> E1)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span># Taylor 的项数：t = 1 时每多一项，误差掉多少</span></div></div><div><div><div>44</div></div><div><span>    </span><span>for</span><span> terms </span><span>in</span><span> (</span><span>3</span><span>, </span><span>6</span><span>, </span><span>9</span><span>, </span><span>12</span><span>, </span><span>16</span><span>):</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(</span><span>expm_taylor</span><span>(A, </span><span>1.0</span><span><span>, terms) </span><span>-</span><span> E1)</span></span></div></div><div><div><div>46</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"  Taylor </span><span>{</span><span>terms</span><span>:2d</span><span>}</span><span> 项，误差 </span><span>{</span><span>err</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>    </span><span># t 很大时：Taylor 的项先涨到天文数字再被抵消</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> </span><span>20.0</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>ref </span><span>=</span><span> </span><span>expm_eig</span><span>(A, t)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"t = 20，exp(At) 的真值（极限）：</span><span>\n</span><span>"</span><span>, ref)</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>err_taylor </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(</span><span>expm_taylor</span><span>(A, t) </span><span>-</span><span> ref)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>err_scaling </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(</span><span>expm_scaling</span><span>(A, t) </span><span>-</span><span> ref)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"  Taylor 60 项误差：</span><span>{</span><span>err_taylor</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"  缩放平方误差：</span><span>{</span><span>err_scaling</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>56</div></div><div><span>    </span><span># exp(A(s+t)) = exp(As) exp(At)</span></div></div><div><div><div>57</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>58</div></div><div><span>        </span><span>"exp(A * 3) = exp(A) @ exp(2A)："</span><span>,</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(</span><span>expm_eig</span><span>(A, </span><span>3.0</span><span><span>), E1 </span><span>@</span><span> </span><span>expm_eig</span><span>(A, </span></span><span>2.0</span><span>)),</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>exp(A) =</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[[0.6833 0.6335]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0.3167 0.3665]]</span></div></div><div><div><div>4</div></div><div><span>Taylor 与特征分解：2.00e-16</span></div></div><div><div><div>5</div></div><div><span>缩放平方与特征分解：1.57e-16</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>Taylor  3 项，误差 2.58e+00</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>Taylor  6 项，误差 7.38e-01</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>Taylor  9 项，误差 4.38e-02</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>Taylor 12 项，误差 9.48e-04</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>Taylor 16 项，误差 1.84e-06</span></div></div><div><div><div>11</div></div><div><span>t = 20，exp(At) 的真值（极限）：</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[[0.6667 0.6667]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[0.3333 0.3333]]</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>Taylor 60 项误差：3.11e+24</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>缩放平方误差：1.05e-15</span></div></div><div><div><div>16</div></div><div><span>exp(A * 3) = exp(A) @ exp(2A)： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>t=1t = 1</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的时候三种方法一致：Taylor 级数和特征分解差 2.00e-16，缩放平方差 1.57e-16，都在机器精度上。Taylor 的收敛速度也能读出来：3 项误差 2.58，6 项 0.738，12 项 9.48e-04，16 项 1.84e-06，项数越多，每多一项误差降得越快：从 12 项到 16 项，误差降了大约 500 倍。</p><p>接着看 <span><span>t=20t = 20</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span>。<span><span>eAte^{At}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 此时应该已经是极限矩阵（两列都是 <span><span>(2/3,1/3)(2/3, 1/3)</span><span><span><span></span><span>(</span><span>2/3</span><span>,</span><span></span><span>1/3</span><span>)</span></span></span></span>），Taylor 级数 60 项却给出 3.11e+24 的误差。原因：<span><span>∥At∥\|At\|</span><span><span><span></span><span>∥</span><span>A</span><span>t</span><span>∥</span></span></span></span> 约为 63，<span><span>(At)n/n!(At)^n/n!</span><span><span><span></span><span>(</span><span>A</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span><span>/</span><span>n</span><span>!</span></span></span></span> 的项要到 <span><span>n≈60n \approx 60</span><span><span><span></span><span>n</span><span></span><span>≈</span><span></span></span><span><span></span><span>60</span></span></span></span> 才到峰值（约 <span><span>102510^{25}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>25</span></span></span></span></span></span></span></span></span></span></span></span>），60 项根本没加完，截断误差就是 <span><span>102410^{24}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>24</span></span></span></span></span></span></span></span></span></span></span></span> 量级；项数再加多，一正一负的大数相减又会丢光有效数字。（Strang 说的“级数总是收敛”是数学上的事，“数值上好算”是另一回事。我猜这就是没人真的用定义来写 <code>expm</code> 的原因。）缩放平方只差 1.05e-15，仍然在机器精度上。（“60 项总够了吧”是我写这段代码时的想法，事实证明这是个糟糕的想法。）如果要把 <span><span>eAte^{At}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 当成工具来用，我会直接调现成的矩阵指数函数，自己手写这三种只是为了知道它里面发生了什么。</p><p>最后一行验证了 <span><span>eA⋅3=eAe2Ae^{A\cdot 3} = e^{A}e^{2A}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>⋅</span><span>3</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span></span></span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>2</span><span>A</span></span></span></span></span></span></span></span></span></span></span></span>，这条规则对一个矩阵的指数成立，<span><span>eAteAs=eA(t+s)e^{At}e^{As} = e^{A(t+s)}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>s</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>(</span><span>t</span><span>+</span><span>s</span><span>)</span></span></span></span></span></span></span></span></span></span></span></span>，和标量指数一样。（对两个不同的矩阵 <span><span>eAeB≠eA+Be^{A}e^{B} \ne e^{A+B}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span></span></span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>B</span></span></span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>+</span><span>B</span></span></span></span></span></span></span></span></span></span></span></span>，一般不成立，这里不用管。）</p></section>
<section><h2>二阶方程变成一阶方程组<a href="#二阶方程变成一阶方程组"><span>#</span></a></h2><p>最后一个例子，他用的是和 Fibonacci 一样的技巧。一个二阶方程 <span><span>y′′+by′+ky=0y'' + by' + ky = 0</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>k</span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，加一条平凡的方程 <span><span>y′=y′y' = y'</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span>，令 <span><span>u=(y′,y)\mathbf{u} = (y', y)</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>,</span><span></span><span>y</span><span>)</span></span></span></span>，则</p><span><span><span>ddt[y′y]=[−b−k10][y′y]\frac{\mathrm{d}}{\mathrm{d}t}\begin{bmatrix} y' \\ y \end{bmatrix}
= \begin{bmatrix} -b &amp; -k \\ 1 &amp; 0 \end{bmatrix}
\begin{bmatrix} y' \\ y \end{bmatrix}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>d</span><span>t</span></span></span><span><span></span><span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>b</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>k</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>第一行是方程本身（<span><span>y′′=−by′−kyy'' = -by' - ky</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>b</span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>k</span><span>y</span></span></span></span>），第二行是那条平凡的 <span><span>y′=y′y' = y'</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span>。（字幕里他写符号有些混乱，把 <span><span>y′′−by′−kyy'' - by' - ky</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>k</span><span>y</span></span></span></span> 当成了方程，这里我按 <span><span>y′′+by′+ky=0y'' + by' + ky = 0</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>k</span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 写，得到的就是上面的矩阵。）五阶方程就是 <span><span>5×55 \times 5</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span>，第一行放系数，下面是一条斜线的 1。特征值“自然地”和微分方程联系在一起：这个矩阵的特征多项式正好就是 <span><span>λ2+bλ+k\lambda^2 + b\lambda + k</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>bλ</span><span></span><span>+</span><span></span></span><span><span></span><span>k</span></span></span></span>。</p><p>下面的脚本取 <span><span>b=0.4b = 0.4</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.4</span></span></span></span>、<span><span>k=4k = 4</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>（阻尼很小的振子），把解和解析解对比，再做一个五阶的例子：多项式 <span><span>(s+1)(s+2)(s+3)(s+4)(s+5)(s+1)(s+2)(s+3)(s+4)(s+5)</span><span><span><span></span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>)</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>)</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span>)</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>5</span><span>)</span></span></span></span> 的伴随矩阵，特征值应该正好是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 到 <span><span>−5-5</span><span><span><span></span><span>−</span><span>5</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>companion</span><span>(</span><span>coeffs</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""首项系数为 1 的多项式 -&gt; 伴随矩阵：第一行是 -系数，下面是 1。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span><span>(coeffs) </span><span>-</span><span> </span></span><span>1</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>zeros</span><span>((n, n))</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>C[</span><span>0</span><span><span>] </span><span>=</span><span> </span><span>-</span><span>coeffs[</span></span><span>1</span><span>:]</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>C[</span><span>1</span><span><span>:, :</span><span>-</span></span><span>1</span><span><span>] </span><span>=</span><span> np.</span><span>eye</span><span>(n </span><span>-</span><span> </span></span><span>1</span><span>)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> C</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># y'' + b y' + k y = 0，令 u = (y', y)，则 u' = A u</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>b, k </span><span>=</span><span> </span><span>0.4</span><span>, </span><span>4.0</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>companion</span><span>(np.</span><span>array</span><span>([</span><span>1.0</span><span>, b, k]))</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A =</span><span>\n</span><span>"</span><span>, A)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.linalg.</span><span>eigvals</span><span>(A))</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征多项式 s^2 + b s + k 的根："</span><span><span>, np.</span><span>roots</span><span>([</span></span><span>1.0</span><span>, b, k]))</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 初值 y(0) = 1，y'(0) = 0：用特征分解算 u(t) = exp(At) u(0)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>u0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>lam, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(S, u0)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>sqrt</span><span>(k </span><span>-</span><span> b</span><span>**</span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>4</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>worst </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span><span> np.</span><span>linspace</span><span>(</span></span><span>0.0</span><span>, </span><span>10.0</span><span>, </span><span>101</span><span>):</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> (S </span><span>@</span><span> (c </span><span>*</span><span> np.</span><span>exp</span><span>(lam </span><span>*</span><span> t))).real</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> np.</span><span>exp</span><span>(</span><span>-</span><span>b </span><span>*</span><span> t </span><span>/</span><span> </span><span>2</span><span><span>) </span><span>*</span><span> (np.</span><span>cos</span><span>(w </span><span>*</span><span> t) </span><span>+</span><span> b </span><span>/</span><span> (</span></span><span>2</span><span><span> </span><span>*</span><span> w) </span><span>*</span><span> np.</span><span>sin</span><span>(w </span><span>*</span><span> t))</span></span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>worst </span><span>=</span><span> </span><span>max</span><span>(worst, </span><span>abs</span><span>(u[</span><span>1</span><span><span>] </span><span>-</span><span> y))</span></span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"101 个时刻上，与解析解 y(t) 的最大差：</span><span>{</span><span>worst</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 五阶：(s+1)(s+2)(s+3)(s+4)(s+5)，系数从根得到</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>coeffs </span><span>=</span><span> np.</span><span>poly</span><span>([</span><span>-</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span><span>, </span><span>-</span></span><span>4.0</span><span><span>, </span><span>-</span></span><span>5.0</span><span>])</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> </span><span>companion</span><span>(coeffs)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"五阶多项式系数："</span><span>, coeffs)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"5 x 5 伴随矩阵：</span><span>\n</span><span>"</span><span>, C)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(C).real))</span></span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A =</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[[-0.4 -4. ]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 1.   0. ]]</span></div></div><div><div><div>4</div></div><div><span>特征值： [-0.2+1.99j -0.2-1.99j]</span></div></div><div><div><div>5</div></div><div><span>特征多项式 s^2 + b s + k 的根： [-0.2+1.99j -0.2-1.99j]</span></div></div><div><div><div>6</div></div><div><span>101 个时刻上，与解析解 y(t) 的最大差：3.33e-16</span></div></div><div><div><div>7</div></div><div><span>五阶多项式系数： [  1.  15.  85. 225. 274. 120.]</span></div></div><div><div><div>8</div></div><div><span>5 x 5 伴随矩阵：</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[[ -15.  -85. -225. -274. -120.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[   1.    0.    0.    0.    0.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[   0.    1.    0.    0.    0.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[   0.    0.    1.    0.    0.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[   0.    0.    0.    1.    0.]]</span></div></div><div><div><div>14</div></div><div><span>特征值： [-5. -4. -3. -2. -1.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值是 <span><span>−0.2±1.99i-0.2 \pm 1.99i</span><span><span><span></span><span>−</span><span>0.2</span><span></span><span>±</span><span></span></span><span><span></span><span>1.99</span><span>i</span></span></span></span>，和多项式的根一样。用特征分解算出的 <span><span>y(t)y(t)</span><span><span><span></span><span>y</span><span>(</span><span>t</span><span>)</span></span></span></span> 与解析解 <span><span>e−bt/2(cos⁡ωt+b2ωsin⁡ωt)e^{-bt/2}\left(\cos\omega t + \tfrac{b}{2\omega}\sin\omega t\right)</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>b</span><span>t</span><span>/2</span></span></span></span></span></span></span></span></span><span></span><span><span><span>(</span></span><span>cos</span><span></span><span>ω</span><span>t</span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span><span>ω</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>sin</span><span></span><span>ω</span><span>t</span><span><span>)</span></span></span></span></span></span> 在 101 个时刻上最大只差 3.33e-16。实部 <span><span>−0.2-0.2</span><span><span><span></span><span>−</span><span>0.2</span></span></span></span> 管衰减，虚部 <span><span>1.991.99</span><span><span><span></span><span>1.99</span></span></span></span> 管振荡，和上一节讲的一致，右边那幅相平面图里的螺旋就是它。五阶的情形，伴随矩阵的特征值回到 <span><span>−1,…,−5-1, \dots, -5</span><span><span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>−</span><span>5</span></span></span></span>，系数 <code>[1, 15, 85, 225, 274, 120]</code> 就是展开后的多项式。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>控制里的状态方程 <span><span>x˙=Ax+Bu\dot{\mathbf{x}} = A\mathbf{x} + B\mathbf{u}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>u</span></span></span></span> 就是这一讲的方程再加一个输入。没有输入时 <span><span>x(t)=eAtx(0)\mathbf{x}(t) = e^{At}\mathbf{x}(0)</span><span><span><span></span><span>x</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span><span>x</span><span>(</span><span>0</span><span>)</span></span></span></span>，系统的“极点”就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值，稳定性判据是所有极点在左半平面，也就是本讲第二节的结论。二阶方程变成一阶方程组的那一步，换个名字就是“把传递函数里的高阶微分方程写成状态空间形式”。相关的背景可以看 <a href="/posts/pid-control-02-transfer-functions/">传递函数那一篇</a>。</p><p>LQR 在这个基础上选反馈 <span><span>u=−Kx\mathbf{u} = -K\mathbf{x}</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span></span></span></span>，闭环矩阵 <span><span>A−BKA - BK</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>B</span><span>K</span></span></span></span> 的特征值就是闭环极点，设计 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的过程就是在移动这些特征值，我把它放在 <a href="/posts/pid-control-09-lqr/">LQR 那一篇</a>里讲。顺便一提，数字控制器是离散的，这时稳定区域从左半平面换成单位圆内部，也就是上一讲的 <span><span>∣λ∣&lt;1|\lambda| &lt; 1</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span>。</p></section>
<section><h2>下一步<a href="#下一步"><span>#</span></a></h2><p>这一讲留了一个小线索：零特征值带来稳态，而且总量不变。下一讲 Strang 用 Markov 矩阵（特征值 1 的离散版）把这件事讲清楚，再加上 Fourier 级数，见 <a href="/posts/linear-algebra-24-markov-fourier/">18.06 第 24 讲：Markov 矩阵与 Fourier 级数</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-22-diagonalization-powers/</id>
      <title type="text">18.06 第 22 讲：对角化与矩阵的幂</title>
      <published>2026-10-18T07:00:00.000Z</published>
      <updated>2026-10-18T07:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-22-diagonalization-powers/"/>
      <summary type="text">MIT 18.06 第 22 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 A = SΛS⁻¹ 用在 Fibonacci 的 2 × 2 矩阵上，靠两个特征值直接算出第 100 项，和 Python 整数精确值的相对误差只有 3.33e-15。</summary>
      <content type="html"><![CDATA[<p>上一讲找到了 <span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span>，这一讲回答“找到之后拿它干什么”：把特征向量排成矩阵 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span>，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 就被拆成 <span><span>SΛS−1S\Lambda S^{-1}</span><span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，乘一百次 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 变成只对对角线上的几个数做一百次方。我拿 Fibonacci 的 2 × 2 矩阵试了一下，两个特征值算出的 <span><span>F100F_{100}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>3.542248×10203.542248 \times 10^{20}</span><span><span><span></span><span>3.542248</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span></span></span></span>，和 Python 整数迭代的精确值只差 <span><span>3.33×10−153.33 \times 10^{-15}</span><span><span><span></span><span>3.33</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>（相对误差），而这个精确值有 21 位。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=13r9QY6cmjc" target="_blank">Lecture 22: Diagonalization and powers of A</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-22-diagonalization-and-powers-of-a/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §6.2 Diagonalizing a Matrix
前置：<a href="/posts/linear-algebra-21-eigenvalues-eigenvectors/">第 21 讲：特征值与特征向量</a>
一句话：有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个线性无关的特征向量时 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，于是 <span><span>Ak=SΛkS−1A^k = S\Lambda^k S^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的幂是增长还是衰减，只看特征值的绝对值是否小于 1。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头 <span><span>AS=SΛAS = S\Lambda</span><span><span><span></span><span>A</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span></span></span></span> 的那段推导要认真看：他逐列算 <span><span>ASAS</span><span><span><span></span><span>A</span><span>S</span></span></span></span>，再把 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 从各列里“剥”到右边，是矩阵乘法“按列算”这个看法最漂亮的一次应用。</li>
<li><span><span>A2A^2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>、<span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 的特征值那一段很短，本篇第一个代码块已经算过，倍速即可。</li>
<li>讲“哪些矩阵不能对角化”的那段值得听完：他用 <span><span>[2102]\begin{bmatrix} 2 &amp; 1 \\ 0 &amp; 2 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 区分代数重数和几何重数，下面第三节用代码重现了。</li>
<li>Fibonacci 例子是全讲的重点，从“把二阶问题变成一阶方程组”的小技巧到特征值估计增长率都在里面。他最后没有把公式算完，只留下一句“思想最重要”，我在第四节替他算完。</li>
</ul></div></div>
<section><h2>从 <span><span>AS=SΛAS = S\Lambda</span><span><span><span></span><span>A</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span></span></span></span> 到 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span><a href="#从-assλas--slambdaassλ-到-asλs1a--slambda-s-1asλs1"><span>#</span></a></h2><p>假设 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个线性无关的特征向量 <span><span>x1,…,xn\mathbf{x}_1, \dots, \mathbf{x}_n</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，把它们放进 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的各列。<strong><span><span>ASAS</span><span><span><span></span><span>A</span><span>S</span></span></span></span> 等于什么？</strong> 按列算：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列得到 <span><span>Axj=λjxjA\mathbf{x}_j = \lambda_j\mathbf{x}_j</span><span><span><span></span><span>A</span><span><span>x</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以</p><span><span><span>AS=A[x1⋯xn]=[λ1x1⋯λnxn]=[x1⋯xn][λ1⋱λn]=SΛAS = A\begin{bmatrix} \mathbf{x}_1 &amp; \cdots &amp; \mathbf{x}_n \end{bmatrix}
= \begin{bmatrix} \lambda_1\mathbf{x}_1 &amp; \cdots &amp; \lambda_n\mathbf{x}_n \end{bmatrix}
= \begin{bmatrix} \mathbf{x}_1 &amp; \cdots &amp; \mathbf{x}_n \end{bmatrix}
\begin{bmatrix} \lambda_1 &amp; &amp; \\ &amp; \ddots &amp; \\ &amp; &amp; \lambda_n \end{bmatrix}
= S\Lambda</span><span><span><span></span><span>A</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>⋯</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span><span>⋱</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span></span></span></span></span><p>倒数第二步是第 3 讲的“按列乘”：想让数 <span><span>λj\lambda_j</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 去乘第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列，就把它放在右边那个矩阵的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个位置上。<span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span> 是大写的 lambda，对角线上放特征值。</p><p>因为有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个线性无关的特征向量，<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 可逆，两边可以乘 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>S−1AS=Λ,A=SΛS−1S^{-1}AS = \Lambda, \qquad A = S\Lambda S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>Λ</span><span>,</span><span></span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><p>Strang 说这是继消元的 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>、Gram-Schmidt 的 <span><span>A=QRA = QR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>QR</span></span></span></span> 之后的第三种分解，整章都会看到“一个 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 夹着一个 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>”的形状。他还教了一个记法：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘的是 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span>（特征向量），所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 写在 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的左边；<span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 放在右边把整件事变对角。</p><p><strong>好处在哪？</strong> 平方一下就看到了：</p><span><span><span>A2=SΛS−1 SΛS−1=SΛ2S−1A^2 = S\Lambda S^{-1}\,S\Lambda S^{-1} = S\Lambda^2 S^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><p>中间的 <span><span>S−1SS^{-1}S</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>S</span></span></span></span> 抵消成单位矩阵。<span><span>A2A^2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的特征向量还是 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的列，特征值是 <span><span>λ2\lambda^2</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>（他用另一个更短的推导也得到这个结论：<span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 两边再乘一次 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，得到 <span><span>A2x=λ2xA^2\mathbf{x} = \lambda^2\mathbf{x}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>x</span></span></span></span>）。推广到 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 次：</p><span><span><span>Ak=SΛkS−1A^k = S\Lambda^k S^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><p>他对比了 <span><span>LULU</span><span><span><span></span><span>LU</span></span></span></span>：<span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 自乘一百次，是一百个 <span><span>LULU</span><span><span><span></span><span>LU</span></span></span></span> 排成一排，什么也化简不了；而 <span><span>SΛS−1S\Lambda S^{-1}</span><span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 自乘一百次，中间九十九对 <span><span>S−1SS^{-1}S</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>S</span></span></span></span> 全部抵消。这是 <span><span>LULU</span><span><span><span></span><span>LU</span></span></span></span> 不具备的能力。</p><p>下面用一个我自己挑的 2 × 2 矩阵 <span><span>A=[4123]A = \begin{bmatrix} 4 &amp; 1 \\ 2 &amp; 3 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 验证（字幕里这一段没有具体数字。我挑它的理由很不浪漫：特征值是整数 5 和 2，手算也能核对）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>4.0</span><span>, </span><span>1.0</span><span>], [</span><span>2.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 特征向量放进 S 的列，特征值放进对角阵 Lam</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>eigenvalues, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>Lam </span><span>=</span><span> np.</span><span>diag</span><span>(eigenvalues)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, eigenvalues)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"S =</span><span>\n</span><span>"</span><span>, S)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ S 等于 S @ Lam："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> S, S </span><span>@</span><span> Lam))</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"inv(S) @ A @ S =</span><span>\n</span><span>"</span><span><span>, np.linalg.</span><span>inv</span><span>(S) </span><span>@</span><span> A </span><span>@</span><span> S)</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># A^k = S Lam^k S^{-1}：只有对角线上的数在做 k 次方</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>k </span><span>=</span><span> </span><span>10</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>Ak </span><span>=</span><span> S </span><span>@</span><span> np.</span><span>diag</span><span>(eigenvalues</span><span>**</span><span>k) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^10 =</span><span>\n</span><span>"</span><span>, Ak)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>21</div></div><div><span>        </span><span>"和 matrix_power 一致："</span><span><span>, np.</span><span>allclose</span><span>(Ak, np.linalg.</span><span>matrix_power</span><span>(A, k))</span></span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># A^2 的特征值是平方，特征向量不变</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>values_sq, _ </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A </span><span>@</span><span> A)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^2 的特征值："</span><span><span>, np.</span><span>sort</span><span>(values_sq))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的特征值平方："</span><span><span>, np.</span><span>sort</span><span>(eigenvalues</span><span>**</span></span><span>2</span><span>))</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>特征值： [5. 2.]</span></div></div><div><div><div>2</div></div><div><span>S =</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[[ 0.7071 -0.4472]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.7071  0.8944]]</span></div></div><div><div><div>5</div></div><div><span>A @ S 等于 S @ Lam： True</span></div></div><div><div><div>6</div></div><div><span>inv(S) @ A @ S =</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[[ 5. -0.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[ 0.  2.]]</span></div></div><div><div><div>9</div></div><div><span>A^10 =</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[[6510758. 3254867.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[6509734. 3255891.]]</span></div></div><div><div><div>12</div></div><div><span>和 matrix_power 一致： True</span></div></div><div><div><div>13</div></div><div><span>A^2 的特征值： [ 4. 25.]</span></div></div><div><div><div>14</div></div><div><span>A 的特征值平方： [ 4. 25.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四件事都对上了。<code>A @ S</code> 等于 <code>S @ Lam</code>，<code>inv(S) @ A @ S</code> 是对角阵（<span><span>−0.-0.</span><span><span><span></span><span>−</span><span>0.</span></span></span></span> 是浮点数里的负零，不是错误）。<span><span>A10A^{10}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 用 <span><span>SΛ10S−1S\Lambda^{10}S^{-1}</span><span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 算出来，和 <code>np.linalg.matrix_power</code> 一致，而前者只对两个数 <span><span>55</span><span><span><span></span><span>5</span></span></span></span> 和 <span><span>22</span><span><span><span></span><span>2</span></span></span></span> 做了十次方。最后两行是 <span><span>A2A^2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的特征值 <span><span>[4,25][4, 25]</span><span><span><span></span><span>[</span><span>4</span><span>,</span><span></span><span>25</span><span>]</span></span></span></span>，正好是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值 <span><span>[5,2][5, 2]</span><span><span><span></span><span>[</span><span>5</span><span>,</span><span></span><span>2</span><span>]</span></span></span></span> 的平方。</p><p><code>np.linalg.eig</code> 返回的特征向量已经单位化（第一列 <span><span>(0.7071,0.7071)(0.7071, 0.7071)</span><span><span><span></span><span>(</span><span>0.7071</span><span>,</span><span></span><span>0.7071</span><span>)</span></span></span></span> 就是 <span><span>(1,1)/2(1, 1)/\sqrt{2}</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span><span>/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>），所以 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的列长度都是 1。特征向量可以随意缩放，<span><span>SΛS−1S\Lambda S^{-1}</span><span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 不受影响。</p></section>
<section><h2>稳定：幂什么时候趋于零<a href="#稳定幂什么时候趋于零"><span>#</span></a></h2><p><strong><span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 会不会趋于零？</strong> 这个信息不在主元里（<span><span>LULU</span><span><span><span></span><span>LU</span></span></span></span> 的主元对此无话可说），而在特征值里。<span><span>Ak=SΛkS−1A^k = S\Lambda^kS^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 中 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 和 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 不动，只有 <span><span>Λk\Lambda^k</span><span><span><span></span><span><span>Λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 在变，对角线上是 <span><span>λ1k,…,λnk\lambda_1^k, \dots, \lambda_n^k</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。所以</p><span><span><span>Ak→0  ⟺  所有 ∣λi∣&lt;1A^k \to 0 \iff \text{所有 } |\lambda_i| &lt; 1</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span>→</span><span></span></span><span><span></span><span>0</span><span></span><span></span><span>⟺</span><span></span><span></span></span><span><span></span><span><span>所有</span><span> </span></span><span>∣</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span></span><p>他特意写了绝对值：特征值可以是负数，也可以是复数，取的是模。他同时提醒，这个推导用了 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 可逆的前提，没有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个特征向量的矩阵这里没有证。（据我所知，结论对那些矩阵依然成立，要靠后面的 Jordan 形式，第 28 讲再说。）</p><p>我想试试“看特征值，不看矩阵的元素”到底有多可靠，于是造了一个元素很大但特征值很小的矩阵，和一个元素温和但有一个特征值超过 1 的矩阵：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 两个矩阵，一个特征值都小于 1，一个有 1.1</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>stable </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.9</span><span>, </span><span>5.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.5</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>unstable </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.1</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.1</span><span>]])</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> name, A </span><span>in</span><span> [(</span><span>"stable"</span><span>, stable), (</span><span>"unstable"</span><span>, unstable)]:</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>values </span><span>=</span><span> np.linalg.</span><span>eigvals</span><span>(A)</span></div></div><div><div><div>12</div></div><div><span>        </span><span>print</span><span>(name, </span><span>"特征值："</span><span>, values, </span><span>"，最大模："</span><span><span>, np.</span><span>abs</span><span>(values).</span><span>max</span><span>())</span></span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> k </span><span>in</span><span> [</span><span>1</span><span>, </span><span>10</span><span>, </span><span>50</span><span>, </span><span>100</span><span>, </span><span>200</span><span>]:</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>norm </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(np.linalg.</span><span>matrix_power</span><span>(A, k), </span><span>2</span><span>)</span></div></div><div><div><div>15</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"  k = </span><span>{</span><span>k</span><span>:3d</span><span>}</span><span>，||A^k|| = </span><span>{</span><span>norm</span><span>:.4e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 稳定的那个先涨后落：看过程中范数的峰值</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>norms </span><span>=</span><span> [</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>norm</span><span>(np.linalg.</span><span>matrix_power</span><span>(stable, k), </span><span>2</span><span>)</span></div></div><div><div><div>20</div></div><div><span>        </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>201</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>peak </span><span>=</span><span> </span><span>int</span><span><span>(np.</span><span>argmax</span><span>(norms)) </span><span>+</span><span> </span></span><span>1</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"stable 的范数峰值："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>max</span><span>(norms)</span><span>:.4f</span><span>}</span><span>"</span><span>, </span><span>"，出现在 k ="</span><span>, peak)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>stable 特征值： [0.9 0.5] ，最大模： 0.9</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>k =   1，||A^k|| = 5.1041e+00</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>k =  10，||A^k|| = 4.3602e+00</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>k =  50，||A^k|| = 6.4628e-02</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>k = 100，||A^k|| = 3.3308e-04</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>k = 200，||A^k|| = 8.8470e-09</span></div></div><div><div><div>7</div></div><div><span>unstable 特征值： [1.1 0.1] ，最大模： 1.1</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>k =   1，||A^k|| = 1.1000e+00</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>k =  10，||A^k|| = 2.5937e+00</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>k =  50，||A^k|| = 1.1739e+02</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>k = 100，||A^k|| = 1.3781e+04</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>k = 200，||A^k|| = 1.8991e+08</span></div></div><div><div><div>13</div></div><div><span>stable 的范数峰值： 7.5861 ，出现在 k = 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>stable</code> 的两个特征值是 <span><span>0.90.9</span><span><span><span></span><span>0.9</span></span></span></span> 和 <span><span>0.50.5</span><span><span><span></span><span>0.5</span></span></span></span>，都小于 1，所以 <span><span>∥Ak∥\|A^k\|</span><span><span><span></span><span>∥</span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>∥</span></span></span></span> 最终掉到 <span><span>200200</span><span><span><span></span><span>200</span></span></span></span> 步后的 <span><span>8.8×10−98.8 \times 10^{-9}</span><span><span><span></span><span>8.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>。但注意它不是单调下降的：<span><span>k=1k = 1</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时范数是 5.10，<span><span>k=3k = 3</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span> 时涨到峰值 7.59，才开始掉。右上角那个 5 让矩阵先把向量拉长，然后特征值才接管。这是我觉得“稳定”这个词有点误导人的地方：特征值保证最终衰减，不保证每一步都衰减。</p><p><code>unstable</code> 的一个特征值是 <span><span>1.11.1</span><span><span><span></span><span>1.1</span></span></span></span>，范数 <span><span>k=10k = 10</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时 2.59，<span><span>k=100k = 100</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>100</span></span></span></span> 时 <span><span>1.38×1041.38 \times 10^4</span><span><span><span></span><span>1.38</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span>，<span><span>k=200k = 200</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>200</span></span></span></span> 时 <span><span>1.90×1081.90 \times 10^8</span><span><span><span></span><span>1.90</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span></span></span></span></span></span></span></span>。注意 <span><span>1.1100≈1.38×1041.1^{100} \approx 1.38 \times 10^4</span><span><span><span></span><span>1.</span><span><span>1</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1.38</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span>，和输出里的范数吻合：<span><span>∥Ak∥\|A^k\|</span><span><span><span></span><span>∥</span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>∥</span></span></span></span> 几乎就是 <span><span>∣λmax⁡∣k|\lambda_{\max}|^k</span><span><span><span></span><span>∣</span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>a</span><span>x</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>∣</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span>。</p></section>
<section><h2>哪些矩阵不能对角化<a href="#哪些矩阵不能对角化"><span>#</span></a></h2><p>Strang 在这里讲了三件事，我用三种矩阵对应着做：</p><ol>
<li><strong>特征值互不相同，一定能对角化。</strong> 每个特征值至少带一个特征向量，不同特征值的特征向量自动线性无关（证明在教材里）。他提到用 MATLAB 的 <code>eig(rand(10))</code> 取随机矩阵，会得到十个互不相同的特征值。</li>
<li><strong>特征值重复，可能能也可能不能。</strong> 单位矩阵的特征值 <span><span>11</span><span><span><span></span><span>1</span></span></span></span> 重复十次，但每个向量都是特征向量，随便挑十个线性无关的就行，<span><span>S−1IS=IS^{-1}IS = I</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>I</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 已经是对角阵。</li>
<li><strong>三角矩阵 <span><span>[2102]\begin{bmatrix} 2 &amp; 1 \\ 0 &amp; 2 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 是坏例子。</strong> 特征值是 <span><span>22</span><span><span><span></span><span>2</span></span></span></span> 和 <span><span>22</span><span><span><span></span><span>2</span></span></span></span>（<span><span>det⁡(A−λI)=(2−λ)2\det(A - \lambda I) = (2 - \lambda)^2</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>），但 <span><span>A−2I=[0100]A - 2I = \begin{bmatrix} 0 &amp; 1 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的零空间只有一维，特征向量只有 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 这一个方向。特征值作为根的重数叫代数重数（这里是 2），特征向量空间的维数叫几何重数（这里是 1），几何重数小于代数重数就不能对角化。</li>
</ol><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 随机 10 x 10：特征值两两不同，S 可逆</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>10</span><span>, </span><span>10</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>values, V </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(M)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>gaps </span><span>=</span><span> np.</span><span>abs</span><span>(values[:, </span><span>None</span><span><span>] </span><span>-</span><span> values[</span></span><span>None</span><span><span>, :]) </span><span>+</span><span> np.</span><span>eye</span><span>(</span></span><span>10</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"随机矩阵的最小特征值间距："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>gaps.</span><span>min</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"S 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(V))</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"cond(S) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(V)</span></span><span>:.1f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>rebuilt </span><span>=</span><span> V </span><span>@</span><span> np.</span><span>diag</span><span>(values) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(V)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"重构误差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(rebuilt </span><span>-</span><span> M)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 单位矩阵：特征值 1 重复 10 次，但每个向量都是特征向量</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>values, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(np.</span><span>eye</span><span>(</span><span>10</span><span>))</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"单位矩阵的特征值："</span><span>, values[:</span><span>3</span><span>], </span><span>"..."</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"单位矩阵的 S 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(S))</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 三角矩阵 [[2, 1], [0, 2]]：特征值 2 和 2，但只有一个方向</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>values, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, values)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"S =</span><span>\n</span><span>"</span><span>, S)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>geometric </span><span>=</span><span> </span><span>2</span><span><span> </span><span>-</span><span> np.linalg.</span><span>matrix_rank</span><span>(A </span><span>-</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>))</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"A - 2I 的零空间维数（几何重数）："</span><span>, geometric)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"S 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(S))</span></span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"cond(S) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(S)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 硬把 S 求逆再重构：得到的是 2I，不是 A</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>rebuilt </span><span>=</span><span> S </span><span>@</span><span> np.</span><span>diag</span><span>(values) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"重构结果：</span><span>\n</span><span>"</span><span>, rebuilt)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"重构误差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(rebuilt </span><span>-</span><span> A)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>随机矩阵的最小特征值间距： 0.6635</span></div></div><div><div><div>2</div></div><div><span>S 的秩： 10</span></div></div><div><div><div>3</div></div><div><span>cond(S) = 21.9</span></div></div><div><div><div>4</div></div><div><span>重构误差： 1.77e-14</span></div></div><div><div><div>5</div></div><div><span>单位矩阵的特征值： [1. 1. 1.] ...</span></div></div><div><div><div>6</div></div><div><span>单位矩阵的 S 的秩： 10</span></div></div><div><div><div>7</div></div><div><span>特征值： [2. 2.]</span></div></div><div><div><div>8</div></div><div><span>S =</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[[ 1. -1.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 0.  0.]]</span></div></div><div><div><div>11</div></div><div><span>A - 2I 的零空间维数（几何重数）： 1</span></div></div><div><div><div>12</div></div><div><span>S 的秩： 1</span></div></div><div><div><div>13</div></div><div><span>cond(S) = 4.50e+15</span></div></div><div><div><div>14</div></div><div><span>重构结果：</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[[2. 0.]</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>[0. 2.]]</span></div></div><div><div><div>17</div></div><div><span>重构误差： 1.00e+00</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>随机的 <span><span>10×1010 \times 10</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>10</span></span></span></span> 矩阵：最小的特征值间距是 0.6635，特征值确实互不相同；<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的秩是 10，条件数 21.9，用 <span><span>SΛS−1S\Lambda S^{-1}</span><span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 重构回原矩阵的误差是 <span><span>1.77×10−141.77 \times 10^{-14}</span><span><span><span></span><span>1.77</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>。（实矩阵的特征值可以是复数，这里 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 是复矩阵，NumPy 自动处理了。）</p><p>单位矩阵的 <code>eig</code> 给出的 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 秩为 10，没有任何问题。</p><p>问题出在 <span><span>[2102]\begin{bmatrix} 2 &amp; 1 \\ 0 &amp; 2 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 上。几何重数是 1，<code>eig</code> 还是交了两列：<span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和 <span><span>(−1,0)(-1, 0)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，它们平行，<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的秩是 1，条件数 <span><span>4.50×10154.50 \times 10^{15}</span><span><span><span></span><span>4.50</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>（大约就是 <span><span>1/ε1/\varepsilon</span><span><span><span></span><span>1/</span><span>ε</span></span></span></span>，数值上和奇异没有区别）。硬着头皮把 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 求逆再重构，得到的是 <span><span>2I2I</span><span><span><span></span><span>2</span><span>I</span></span></span></span> 而不是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，误差范数恰好是 1.00：<span><span>Λ=2I\Lambda = 2I</span><span><span><span></span><span>Λ</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>I</span></span></span></span> 夹在任何可逆的 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 之间都还是 <span><span>2I2I</span><span><span><span></span><span>2</span><span>I</span></span></span></span>，所以不可能还原出右上角那个 1。</p><p>这和第 21 讲的零空间是同一件事：特征向量是 <span><span>A−λIA - \lambda I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span></span></span></span> 的零空间，零空间太小，特征向量就凑不够。</p></section>
<section><h2>Fibonacci：幂的一个实际用处<a href="#fibonacci幂的一个实际用处"><span>#</span></a></h2><p>这是他在课上做的例子，也是全讲最值钱的地方。Fibonacci 数 <span><span>0,1,1,2,3,5,8,13,…0, 1, 1, 2, 3, 5, 8, 13, \dots</span><span><span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>8</span><span>,</span><span></span><span>13</span><span>,</span><span></span><span>…</span></span></span></span> 满足 <span><span>Fk+2=Fk+1+FkF_{k+2} = F_{k+1} + F_k</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。问题是：第 100 个是多少？它们增长得多快？</p><p>这不是 <span><span>uk+1=Auk\mathbf{u}_{k+1} = A\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的形状，它是二阶的标量方程。他用一个小技巧把它变成一阶方程组：令 <span><span>uk=(Fk+1,Fk)\mathbf{u}_k = (F_{k+1}, F_k)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，补一个平凡的方程 <span><span>Fk+1=Fk+1F_{k+1} = F_{k+1}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，</p><span><span><span>[Fk+2Fk+1]=[1110][Fk+1Fk],u0=[F1F0]=[10]\begin{bmatrix} F_{k+2} \\ F_{k+1} \end{bmatrix}
= \begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 0 \end{bmatrix}
\begin{bmatrix} F_{k+1} \\ F_k \end{bmatrix},
\qquad \mathbf{u}_0 = \begin{bmatrix} F_1 \\ F_0 \end{bmatrix} = \begin{bmatrix} 1 \\ 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>F</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>于是 <span><span>uk=Aku0\mathbf{u}_k = A^k\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，第二个分量就是 <span><span>FkF_k</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他说“这就是最紧凑的公式”，但要真的算出 <span><span>u100\mathbf{u}_{100}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就得靠特征值。</p><p>他手算 <span><span>det⁡(A−λI)=λ2−λ−1\det(A - \lambda I) = \lambda^2 - \lambda - 1</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>（和递推式本身长得一模一样），再用求根公式：</p><span><span><span>λ1=1+52≈1.618,λ2=1−52≈−0.618\lambda_1 = \frac{1 + \sqrt{5}}{2} \approx 1.618, \qquad
\lambda_2 = \frac{1 - \sqrt{5}}{2} \approx -0.618</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1.618</span><span>,</span><span></span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>−</span><span>0.618</span></span></span></span></span><p>他先用迹和行列式核对：<span><span>λ1+λ2=1\lambda_1 + \lambda_2 = 1</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（迹），<span><span>λ1λ2=−1\lambda_1\lambda_2 = -1</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>（行列式）。他还顺口说 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是对称矩阵，所以特征值是实数、特征向量正交，这个等第 25 讲。特征向量他用 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的小技巧：<span><span>(A−λI)x=0(A - \lambda I)\mathbf{x} = 0</span><span><span><span></span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的第二行是 <span><span>x1−λx2=0x_1 - \lambda x_2 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以 <span><span>x=(λ,1)\mathbf{x} = (\lambda, 1)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>λ</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。</p><p>然后是这一讲的思想：<strong>把 <span><span>u0\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 写成特征向量的组合 <span><span>u0=c1x1+c2x2\mathbf{u}_0 = c_1\mathbf{x}_1 + c_2\mathbf{x}_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></strong>，每一块各走各的：</p><span><span><span>uk=c1λ1kx1+c2λ2kx2=SΛkc,c=S−1u0\mathbf{u}_k = c_1\lambda_1^k\mathbf{x}_1 + c_2\lambda_2^k\mathbf{x}_2
= S\Lambda^k\mathbf{c}, \qquad \mathbf{c} = S^{-1}\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>c</span><span>,</span><span></span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>∣λ1∣&gt;1&gt;∣λ2∣|\lambda_1| &gt; 1 &gt; |\lambda_2|</span><span><span><span></span><span>∣</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>1</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>∣</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span>，所以第一块越来越大，第二块越来越小，增长率由大特征值 <span><span>1.6181.618</span><span><span><span></span><span>1.618</span></span></span></span> 决定。下面我替他把这个算完，并和精确值对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>u0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>])  </span><span># (F1, F0)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>values, S </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, values)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹 ="</span><span><span>, np.</span><span>trace</span><span>(A), </span></span><span>"，和 ="</span><span><span>, values.</span><span>sum</span><span>())</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"行列式 ="</span><span><span>, np.linalg.</span><span>det</span><span>(A), </span></span><span>"，积 ="</span><span><span>, values.</span><span>prod</span><span>())</span></span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>golden </span><span>=</span><span> (</span><span>1</span><span><span> </span><span>+</span><span> np.</span><span>sqrt</span><span>(</span></span><span>5</span><span><span>)) </span><span>/</span><span> </span></span><span>2</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"(1 + sqrt(5)) / 2 ="</span><span>, golden)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># 把 u0 写成特征向量的组合：u0 = S c</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(S, u0)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"c ="</span><span>, c)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># u_k = S Lam^k c，第二个分量就是 F_k</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>k </span><span>=</span><span> </span><span>100</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> S </span><span>@</span><span> (values</span><span>**</span><span>k </span><span>*</span><span> c)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"用特征值算的 F100 ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span>u[</span><span>1</span><span>]</span><span>:.6e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 用 Python 整数精确迭代作对照</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>a, b </span><span>=</span><span> </span><span>0</span><span>, </span><span>1</span><span>  </span><span># F0, F1</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(k):</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>a, b </span><span>=</span><span> b, a </span><span>+</span><span> b</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"精确的 F100 ="</span><span>, a)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"相对误差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>abs</span><span>(u[</span><span>1</span><span><span>] </span><span>-</span><span> a) </span><span>/</span><span> a</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 只留大特征值那一项，再四舍五入</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>approx </span><span>=</span><span> golden</span><span>**</span><span>k </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span><span>5</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"只用 lambda1^100 / sqrt(5) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span>approx</span><span>:.6e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"第二个特征值的 100 次方："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>values.</span><span>min</span><span>() </span><span>**</span><span> k</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>特征值： [ 1.618 -0.618]</span></div></div><div><div><div>2</div></div><div><span>迹 = 1.0 ，和 = 1.0</span></div></div><div><div><div>3</div></div><div><span>行列式 = -1.0 ，积 = -0.9999999999999999</span></div></div><div><div><div>4</div></div><div><span>(1 + sqrt(5)) / 2 = 1.618033988749895</span></div></div><div><div><div>5</div></div><div><span>c = [ 0.8507 -0.5257]</span></div></div><div><div><div>6</div></div><div><span>用特征值算的 F100 = 3.542248e+20</span></div></div><div><div><div>7</div></div><div><span>精确的 F100 = 354224848179261915075</span></div></div><div><div><div>8</div></div><div><span>相对误差： 3.33e-15</span></div></div><div><div><div>9</div></div><div><span>只用 lambda1^100 / sqrt(5) = 3.542248e+20</span></div></div><div><div><div>10</div></div><div><span>第二个特征值的 100 次方： 1.26e-21</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>特征值是 <span><span>1.6181.618</span><span><span><span></span><span>1.618</span></span></span></span> 和 <span><span>−0.618-0.618</span><span><span><span></span><span>−</span><span>0.618</span></span></span></span>，和为 1、积为 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>（浮点里是 <span><span>−0.9999999999999999-0.9999999999999999</span><span><span><span></span><span>−</span><span>0.9999999999999999</span></span></span></span>）。系数 <span><span>c=(0.8507,−0.5257)\mathbf{c} = (0.8507, -0.5257)</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0.8507</span><span>,</span><span></span><span>−</span><span>0.5257</span><span>)</span></span></span></span> 是因为 <code>eig</code> 把特征向量单位化了，不是教材里常见的 <span><span>1/51/\sqrt{5}</span><span><span><span></span><span>1/</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>（对应未单位化的 <span><span>(λ,1)(\lambda, 1)</span><span><span><span></span><span>(</span><span>λ</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>）。</p><p><span><span>F100F_{100}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 用 <span><span>SΛ100cS\Lambda^{100}\mathbf{c}</span><span><span><span></span><span>S</span><span><span>Λ</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span></span></span></span></span><span>c</span></span></span></span> 算出来是 <span><span>3.542248×10203.542248 \times 10^{20}</span><span><span><span></span><span>3.542248</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span></span></span></span>，精确值是 <span><span>354224848179261915075354224848179261915075</span><span><span><span></span><span>354224848179261915075</span></span></span></span>，相对误差 <span><span>3.33×10−153.33 \times 10^{-15}</span><span><span><span></span><span>3.33</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。双精度大约只有 16 位有效数字，<span><span>F100F_{100}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 有 21 位，所以最后几位是对不上的，但前 15 位全对。只留 <span><span>λ1100/5\lambda_1^{100}/\sqrt{5}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 这一项的结果也是 <span><span>3.542248×10203.542248 \times 10^{20}</span><span><span><span></span><span>3.542248</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span></span></span></span>。第二项 <span><span>λ2100\lambda_2^{100}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 只有 <span><span>1.26×10−211.26 \times 10^{-21}</span><span><span><span></span><span>1.26</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>21</span></span></span></span></span></span></span></span></span></span></span></span>，比第一项小 41 个数量级，在这个尺度上等于没有。</p><p>这解释了为什么他说“<span><span>F100F_{100}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 大约是 <span><span>c1λ1100c_1\lambda_1^{100}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span><span>100</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>”。更具体地看增长率和四舍五入公式：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>])  </span><span># (F1, F0)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>golden </span><span>=</span><span> (</span><span>1</span><span><span> </span><span>+</span><span> np.</span><span>sqrt</span><span>(</span></span><span>5</span><span><span>)) </span><span>/</span><span> </span></span><span>2</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span># 一步一步乘 A：看增长率逼近 lambda1，以及何时四舍五入失效</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>" k   F_k   F_{k+1}/F_k"</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>exact </span><span>=</span><span> [</span><span>0</span><span>, </span><span>1</span><span>]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>41</span><span>):</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> A </span><span>@</span><span> u</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>exact.</span><span>append</span><span>(exact[</span><span>-</span><span>1</span><span><span>] </span><span>+</span><span> exact[</span><span>-</span></span><span>2</span><span>])</span></div></div><div><div><div>16</div></div><div><span>        </span><span>if</span><span> k </span><span>in</span><span> (</span><span>1</span><span>, </span><span>2</span><span>, </span><span>5</span><span>, </span><span>10</span><span>, </span><span>20</span><span>, </span><span>40</span><span>):</span></div></div><div><div><div>17</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>k</span><span>:2d</span><span>}</span><span> </span><span>{</span><span>int</span><span>(exact[k])</span><span>:9d</span><span>}</span><span>   </span><span>{</span><span>u[</span><span>0</span><span><span>] </span><span>/</span><span> u[</span></span><span>1</span><span>]</span><span>:.10f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"lambda1 ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span>golden</span><span>:.10f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># F_k = round(lambda1^k / sqrt(5)) 能撑到多大的 k</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>bad </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>, </span><span>80</span><span>):</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>guess </span><span>=</span><span> </span><span>round</span><span><span>(golden</span><span>**</span><span>k </span><span>/</span><span> np.</span><span>sqrt</span><span>(</span></span><span>5</span><span>))</span></div></div><div><div><div>24</div></div><div><span>        </span><span>if</span><span><span> k </span><span>&gt;=</span><span> </span></span><span>len</span><span>(exact):</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>exact.</span><span>append</span><span>(exact[</span><span>-</span><span>1</span><span><span>] </span><span>+</span><span> exact[</span><span>-</span></span><span>2</span><span>])</span></div></div><div><div><div>26</div></div><div><span>        </span><span>if</span><span><span> guess </span><span>!=</span><span> exact[k]:</span></span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>bad </span><span>=</span><span> k</span></div></div><div><div><div>28</div></div><div><span>            </span><span>break</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"四舍五入公式第一次出错的 k："</span><span>, bad)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>k   F_k   F_{k+1}/F_k</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>1         1   1.0000000000</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>2         1   2.0000000000</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>5         5   1.6000000000</span></div></div><div><div><div>5</div></div><div><span>10        55   1.6181818182</span></div></div><div><div><div>6</div></div><div><span>20      6765   1.6180339985</span></div></div><div><div><div>7</div></div><div><span>40 102334155   1.6180339887</span></div></div><div><div><div>8</div></div><div><span>lambda1 = 1.6180339887</span></div></div><div><div><div>9</div></div><div><span>四舍五入公式第一次出错的 k： 71</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>Fk+1/FkF_{k+1}/F_k</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 <span><span>k=2k = 2</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 的 2.0 开始，<span><span>k=10k = 10</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时 1.6181818182，<span><span>k=20k = 20</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> 时 1.6180339985，<span><span>k=40k = 40</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>40</span></span></span></span> 时 1.6180339887，已经和 <span><span>λ1=1.6180339887\lambda_1 = 1.6180339887</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.6180339887</span></span></span></span> 在十位小数上一致了。不到 40 步，增长率就被特征值锁定了。</p><p>因为 <span><span>λ2k/5\lambda_2^k/\sqrt{5}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 小于 <span><span>1/21/2</span><span><span><span></span><span>1/2</span></span></span></span>，把 <span><span>λ1k/5\lambda_1^k/\sqrt{5}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 四舍五入就是 <span><span>FkF_k</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（课上没讲这句，教材里有）。这个公式在双精度下能撑到 <span><span>k=70k = 70</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>70</span></span></span></span>，<span><span>k=71k = 71</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>71</span></span></span></span> 开始失败：<span><span>F71F_{71}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>71</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 已经是 15 位数，我猜是 <code>golden**k</code> 的浮点误差已经接近 0.5 了，没有进一步验证。（顺便一提，这是个很好的例子：数学上严格成立的公式，在浮点里有保质期。）</p><p>他把整件事总结成一句话：当系统按一阶方程 <span><span>uk+1=Auk\mathbf{u}_{k+1} = A\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 演化，特征值已经告诉你解是爆炸还是衰减，要精确公式就把 <span><span>u0\mathbf{u}_0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 展开成特征向量的组合，让每一块各走各的。下一讲就是它在连续时间的版本，<span><span>du/dt=Au\mathrm{d}\mathbf{u}/\mathrm{d}t = A\mathbf{u}</span><span><span><span></span><span>d</span><span>u</span><span>/</span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span></span></span></span>。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>离散时间系统 <span><span>x[k+1]=Ax[k]\mathbf{x}[k+1] = A\mathbf{x}[k]</span><span><span><span></span><span>x</span><span>[</span><span>k</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span>[</span><span>k</span><span>]</span></span></span></span> 就是这一讲的 <span><span>uk+1=Auk\mathbf{u}_{k+1} = A\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，只是名字不同。第二节的结论原样搬过来：状态趋于零，当且仅当 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的所有特征值落在单位圆内，<span><span>∣λi∣&lt;1|\lambda_i| &lt; 1</span><span><span><span></span><span>∣</span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span>。这个判据在控制里叫“极点在单位圆内”。</p><p>连续系统的 PID 离散化之后（见 <a href="/posts/pid-control-04-discretization/">PID 控制 04：离散化</a>），闭环极点从 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 平面的左半平面变成 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 平面的单位圆内部，判断稳定性的方法就是算特征值的模。第二节 <code>stable</code> 矩阵的那个先涨后落的峰值也值得记住：极点都在圆内只保证最终收敛，过程里状态可以先冲到初值的好几倍（那个例子里范数峰值是 7.59，初值 1），实际系统里这是超调，不是错误。</p></section>
<section><h2>下一步<a href="#下一步"><span>#</span></a></h2><p>这一讲处理的是 <span><span>uk+1=Auk\mathbf{u}_{k+1} = A\mathbf{u}_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，一步一步走。如果时间是连续的，<span><span>du/dt=Au\mathrm{d}\mathbf{u}/\mathrm{d}t = A\mathbf{u}</span><span><span><span></span><span>d</span><span>u</span><span>/</span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>u</span></span></span></span>，每个特征向量的系数就不是 <span><span>λk\lambda^k</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span>，而是 <span><span>eλte^{\lambda t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>λ</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 也变成矩阵指数 <span><span>eAte^{At}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>。同样的 <span><span>SΛS−1S\Lambda S^{-1}</span><span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，换一个函数。见 <a href="/posts/linear-algebra-23-differential-equations-exp-at/">18.06 第 23 讲：微分方程与 exp(At)</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-21-eigenvalues-eigenvectors/</id>
      <title type="text">18.06 第 21 讲：特征值与特征向量</title>
      <published>2026-10-18T06:40:00.000Z</published>
      <updated>2026-10-18T06:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-21-eigenvalues-eigenvectors/"/>
      <summary type="text">MIT 18.06 第 21 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 算了投影、置换、旋转和三角矩阵的特征值，90 度旋转矩阵的特征值是 ±i，而 [[3, 1], [0, 3]] 的特征向量矩阵条件数高达 3.00e+15。</summary>
      <content type="html"><![CDATA[<p>这一讲要找的是一类特殊的向量：矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘上去以后，方向不变，只是被拉长或压短。一般的向量做不到，但 <span><span>[3113]\begin{bmatrix} 3 &amp; 1 \\ 1 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 有两条这样的直线，沿 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 放大 4 倍，沿 <span><span>(−1,1)(-1, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 放大 2 倍，迹 6 和行列式 8 恰好就是这两个倍数的和与积。我把 Strang 黑板上的几个矩阵逐个交给 NumPy：一个旋转矩阵没有任何实特征向量，一个三角矩阵的两个特征向量被压成了同一条线，后者的特征向量矩阵条件数是 <span><span>3.00×10153.00 \times 10^{15}</span><span><span><span></span><span>3.00</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=cdZnhQjJu4I" target="_blank">Lecture 21: Eigenvalues and eigenvectors</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-21-eigenvalues-and-eigenvectors/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §6.1 Introduction to Eigenvalues : Ax = λx
前置：<a href="/posts/linear-algebra-20-cramer-inverse-volume/">第 20 讲：Cramer 法则、逆矩阵与体积</a>
一句话：特征向量是满足 <span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 的非零向量，<span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 是使 <span><span>A−λIA - \lambda I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span></span></span></span> 奇异的数，也就是 <span><span>det⁡(A−λI)=0\det(A - \lambda I) = 0</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的根。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先说这是特征值与特征向量这个大主题的第一讲，后面要占掉课程的大半。这一讲只回答“它们是什么”，“为什么要它们”留给后面，所以不必着急。</li>
<li>投影矩阵和置换矩阵那两段（先猜答案，再回头验证）值得认真看：他在不写任何行列式的情况下，靠图和直觉把特征值说了出来。本篇第二节用代码算过一遍，看视频时可以快一点。</li>
<li>他推导 <span><span>det⁡(A−λI)=0\det(A - \lambda I) = 0</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 和 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 例子的那段要认真听，特别是他指出“6 是迹，8 是行列式”的地方，这是这一讲里最容易用上的结论。</li>
<li>旋转矩阵和三角矩阵两个“出问题”的例子在视频最后，本篇第四、五节有对应的数值。他说重复特征值是“18.06 里一切麻烦的来源”，这句话下一讲就会兑现。</li>
</ul></div></div>
<section><h2>方向不变的向量<a href="#方向不变的向量"><span>#</span></a></h2><p><strong>矩阵对向量做了什么？</strong> Strang 的开场是这样的：矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 像函数，微积分里放进去一个数 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>，出来 <span><span>f(x)f(x)</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span></span></span></span>；线性代数里放进去一个向量 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>，出来 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span>。大多数向量进去和出来的方向不一样。但有一些向量，出来的 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 和进去的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 平行，这些就是特征向量：</p><span><span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span></span><p>“平行”被他放宽到包括方向相反（<span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 为负）和压成零（<span><span>λ=0\lambda = 0</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>）。这个倍数 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 叫特征值。<span><span>λ=0\lambda = 0</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 并没有什么特别：它说的是 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以特征值为 0 的特征向量就是零空间里的非零向量，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 奇异等价于 0 是它的一个特征值。这是前面几讲的旧账，现在换了个说法。</p><p>方程里 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 和 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 都未知，还乘在一起，消元派不上用场。他说需要一个好办法，那个办法要用到行列式。在讲办法之前，他先挑了几个“已经知道矩阵在干什么”的例子，看看答案长什么样。</p><p>先放一张图，把“方向不变”画出来。矩阵是后面要算的 <span><span>[3113]\begin{bmatrix} 3 &amp; 1 \\ 1 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，灰圈是单位圆上的所有 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>，蓝色椭圆是它们的像 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span>。黑色箭头是一个普通向量 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和它的像 <span><span>(3,1)(3, 1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，方向转了；绿色和红色是两个特征向量，浅色是进去的，深色是出来的，同一条线上，只是长度变了。图里的 4 和 2 来自第三节的代码输出：</p><p></p><figure><img src="/assets/images/linear-algebra/l21-eigen-directions.svg" alt="特征向量方向不变：A 把 (1, 1) 方向拉长 4 倍，把 (-1, 1) 方向拉长 2 倍，普通向量 (1, 0) 被转向 (3, 1)" /><figcaption>特征向量方向不变：A 把 (1, 1) 方向拉长 4 倍，把 (-1, 1) 方向拉长 2 倍，普通向量 (1, 0) 被转向 (3, 1)</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>values, vectors </span><span>=</span><span> np.linalg.</span><span>eigh</span><span>(A)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span><span> </span><span>*</span><span> np.pi, </span></span><span>400</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>circle </span><span>=</span><span> np.</span><span>vstack</span><span>([np.</span><span>cos</span><span>(theta), np.</span><span>sin</span><span>(theta)])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>ellipse </span><span>=</span><span> A </span><span>@</span><span> circle</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>fig, ax </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>figsize</span><span><span>=</span><span>(</span></span><span>6</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>ax.</span><span>plot</span><span>(*circle, </span><span>color</span><span>=</span><span>"0.6"</span><span>, </span><span>lw</span><span>=</span><span>1.2</span><span>, </span><span>label</span><span>=</span><span>"unit circle $x$"</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>ax.</span><span>plot</span><span>(*ellipse, </span><span>color</span><span>=</span><span>"C0"</span><span>, </span><span>lw</span><span>=</span><span>1.2</span><span>, </span><span>label</span><span>=</span><span>"image $Ax$"</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>arrow </span><span>=</span><span> </span><span>dict</span><span>(</span><span>arrowstyle</span><span>=</span><span>"-|&gt;"</span><span>, </span><span>lw</span><span>=</span><span>1.8</span><span>, </span><span>shrinkA</span><span>=</span><span>0</span><span>, </span><span>shrinkB</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> lam, v, c </span><span>in</span><span> </span><span>zip</span><span>(values, vectors.T, [</span><span>"C3"</span><span>, </span><span>"C2"</span><span>]):</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>ax.</span><span>annotate</span><span>(</span></div></div><div><div><div>24</div></div><div><span>            </span><span>""</span><span>, </span><span>xy</span><span><span>=</span><span>lam </span><span>*</span><span> v, </span></span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>), </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>color</span><span><span>=</span><span>c, **arrow)</span></span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>ax.</span><span>annotate</span><span>(</span></div></div><div><div><div>27</div></div><div><span>            </span><span>""</span><span>,</span></div></div><div><div><div>28</div></div><div><span>            </span><span>xy</span><span><span>=</span><span>v,</span></span></div></div><div><div><div>29</div></div><div><span>            </span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>30</div></div><div><span>            </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>color</span><span><span>=</span><span>c, </span></span><span>alpha</span><span>=</span><span>0.5</span><span>, **arrow),</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>ax.</span><span>text</span><span>(</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>*(lam </span><span>*</span><span> v </span><span>*</span><span> </span><span>1.08</span><span>),</span></div></div><div><div><div>34</div></div><div><span>            </span><span>f</span><span>"$</span><span>\\</span><span>lambda=</span><span>{</span><span>lam</span><span>:.0f</span><span>}</span><span>$"</span><span>,</span></div></div><div><div><div>35</div></div><div><span>            </span><span>color</span><span><span>=</span><span>c,</span></span></div></div><div><div><div>36</div></div><div><span>            </span><span>ha</span><span>=</span><span>"center"</span><span>,</span></div></div><div><div><div>37</div></div><div><span>            </span><span>va</span><span>=</span><span>"center"</span><span>,</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>ax.</span><span>annotate</span><span>(</span></div></div><div><div><div>42</div></div><div><span>        </span><span>""</span><span>, </span><span>xy</span><span><span>=</span><span>A </span><span>@</span><span> x, </span></span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>), </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>color</span><span>=</span><span>"k"</span><span>, **arrow)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>ax.</span><span>annotate</span><span>(</span></div></div><div><div><div>45</div></div><div><span>        </span><span>""</span><span>, </span><span>xy</span><span><span>=</span><span>x, </span></span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>), </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>color</span><span>=</span><span>"k"</span><span>, </span><span>alpha</span><span>=</span><span>0.5</span><span>, **arrow)</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>ax.</span><span>text</span><span>(*(A </span><span>@</span><span> x </span><span>+</span><span> np.</span><span>array</span><span>([</span><span>0.3</span><span><span>, </span><span>-</span></span><span>0.45</span><span>])), </span><span>"$Ax$ turns"</span><span>, </span><span>ha</span><span>=</span><span>"left"</span><span>)</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>ax.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>ax.</span><span>set_xlim</span><span>(</span><span>-</span><span>4.6</span><span>, </span><span>4.6</span><span>)</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>ax.</span><span>set_ylim</span><span>(</span><span>-</span><span>4.6</span><span>, </span><span>4.6</span><span>)</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>ax.</span><span>axhline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"0.85"</span><span>, </span><span>lw</span><span>=</span><span>0.6</span><span>)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>ax.</span><span>axvline</span><span>(</span><span>0</span><span>, </span><span>color</span><span>=</span><span>"0.85"</span><span>, </span><span>lw</span><span>=</span><span>0.6</span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>ax.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"lower right"</span><span>, </span><span>fontsize</span><span>=</span><span>8</span><span>)</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>57</div></div><div><span>        </span><span>"l21-eigen-directions.svg"</span><span>,</span></div></div><div><div><div>58</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>椭圆的两条轴恰好落在两个特征向量的方向上。这是对称矩阵才有的整齐性质，第 25 讲会回来解释。</p></section>
<section><h2>投影矩阵：先猜后验<a href="#投影矩阵先猜后验"><span>#</span></a></h2><p><strong>投影矩阵的特征值是多少？</strong> 这是他的第一个问题。取三维空间里的一个平面，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 把任意向量 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 投到平面上。<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 本身不是特征向量，因为 <span><span>PbP\mathbf{b}</span><span><span><span></span><span>P</span><span>b</span></span></span></span> 方向变了。什么向量投影之后方向不变？本来就在平面里的向量：<span><span>Px=xP\mathbf{x} = \mathbf{x}</span><span><span><span></span><span>P</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span></span></span></span>，<span><span>λ=1\lambda = 1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。整个平面都是 <span><span>λ=1\lambda = 1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的特征向量。垂直于平面的向量被投成零：<span><span>Px=0P\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>P</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>λ=0\lambda = 0</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。三维空间里两个在平面里、一个垂直于平面，三个独立的特征向量，特征值是 1、1、0。</p><p>用代码验证。平面取 <span><span>M=[100111]M = \begin{bmatrix} 1 &amp; 0 \\ 0 &amp; 1 \\ 1 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span> 的两列张成的平面，投影矩阵 <span><span>P=M(MTM)−1MTP = M(M^\mathsf{T}M)^{-1}M^\mathsf{T}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>M</span><span>(</span><span><span>M</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>M</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是第 15 讲的内容，这里先当现成的用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 投影到 R^3 里的一个平面：平面由 M 的两列张成</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> M </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(M.T </span><span>@</span><span> M) </span><span>@</span><span> M.T</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>values, vectors </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(P)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>order </span><span>=</span><span> np.</span><span>argsort</span><span>(</span><span>-</span><span>values)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, values[order])</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 平面里的向量原样不动，垂直于平面的向量被压成 0</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>in_plane </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> M[:, </span></span><span>0</span><span><span>] </span><span>-</span><span> </span></span><span>3.0</span><span><span> </span><span>*</span><span> M[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>normal </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"M.T @ normal ="</span><span><span>, M.T </span><span>@</span><span> normal)</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ in_plane == in_plane："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> in_plane, in_plane))</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ normal == 0："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> normal, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 一个一般的 b：投影之后方向变了</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>4.0</span><span>])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>Pb </span><span>=</span><span> P </span><span>@</span><span> b</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>cosine </span><span>=</span><span> b </span><span>@</span><span> Pb </span><span>/</span><span> (np.linalg.</span><span>norm</span><span>(b) </span><span>*</span><span> np.linalg.</span><span>norm</span><span>(Pb))</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"b 与 Pb 的夹角余弦："</span><span>, </span><span>round</span><span>(cosine, </span><span>4</span><span>))</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 每一对 (lambda, x) 都满足 A x = lambda x</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>residual </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(P </span><span>@</span><span> vectors </span><span>-</span><span> vectors </span><span>*</span><span> values)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"||P X - X Lambda|| ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span>residual</span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P =</span></div></div><div><div><div>2</div></div><div><span>[[ 0.6667 -0.3333  0.3333]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[-0.3333  0.6667  0.3333]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.3333  0.3333  0.6667]]</span></div></div><div><div><div>5</div></div><div><span>特征值： [1. 1. 0.]</span></div></div><div><div><div>6</div></div><div><span>M.T @ normal = [0. 0.]</span></div></div><div><div><div>7</div></div><div><span>P @ in_plane == in_plane： True</span></div></div><div><div><div>8</div></div><div><span>P @ normal == 0： True</span></div></div><div><div><div>9</div></div><div><span>b 与 Pb 的夹角余弦： 0.992</span></div></div><div><div><div>10</div></div><div><span>||P X - X Lambda|| = 1.7e-16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>特征值是 <span><span>1,1,01, 1, 0</span><span><span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span></span></span></span>，和他从图上读出来的一样。平面里的 <span><span>2 列一−3 列二2\,\text{列一} - 3\,\text{列二}</span><span><span><span></span><span>2</span><span></span><span><span>列一</span></span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span><span>列二</span></span></span></span></span> 原样不动，垂直于平面的 <span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 被压成零。随手取的 <span><span>b=(1,2,4)\mathbf{b} = (1, 2, 4)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 与 <span><span>PbP\mathbf{b}</span><span><span><span></span><span>P</span><span>b</span></span></span></span> 的夹角余弦是 0.992，不是 1，方向确实变了（偏得不多，因为我挑的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 离平面本来就不远）。最后一行把 <code>np.linalg.eig</code> 给的整套 <span><span>(λ,x)(\lambda, \mathbf{x})</span><span><span><span></span><span>(</span><span>λ</span><span>,</span><span></span><span>x</span><span>)</span></span></span></span> 代回方程，残差 <span><span>1.7×10−161.7 \times 10^{-16}</span><span><span><span></span><span>1.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，也就是舍入误差。</p></section>
<section><h2>先求 λ，再求 x<a href="#先求-λ再求-x"><span>#</span></a></h2><p><strong>怎么找？</strong> 他的技巧很短：把 <span><span>λx\lambda\mathbf{x}</span><span><span><span></span><span>λ</span><span>x</span></span></span></span> 移到左边，</p><span><span><span>(A−λI) x=0(A - \lambda I)\,\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>我们要的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 不是零向量，所以矩阵 <span><span>A−λIA - \lambda I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span></span></span></span> 必须奇异，行列式必须为零：</p><span><span><span>det⁡(A−λI)=0\det(A - \lambda I) = 0</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>这叫特征方程。它里面已经没有 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 了，是 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 的 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 次多项式，有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个根（可能有重根）。他的顺序是先求 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span>，再对每个 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span> 解 <span><span>(A−λI)x=0(A - \lambda I)\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：<span><span>A−λIA - \lambda I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span></span></span></span> 是个奇异矩阵，要的是零空间，第 7 讲已经熟练地做过。自由变量取 1，得到一个特征向量。</p><p>他的主例子是</p><span><span><span>A=[3113],det⁡(A−λI)=∣3−λ113−λ∣=λ2−6λ+8=(λ−4)(λ−2)A = \begin{bmatrix} 3 &amp; 1 \\ 1 &amp; 3 \end{bmatrix}, \qquad
\det(A - \lambda I) =
\begin{vmatrix} 3 - \lambda &amp; 1 \\ 1 &amp; 3 - \lambda \end{vmatrix}
= \lambda^2 - 6\lambda + 8 = (\lambda - 4)(\lambda - 2)</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>3</span><span></span><span>−</span><span></span><span>λ</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span><span></span><span>−</span><span></span><span>λ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>6</span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>8</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>λ</span><span></span><span>−</span><span></span></span><span><span></span><span>4</span><span>)</span><span>(</span><span>λ</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>)</span></span></span></span></span><p>特征值是 4 和 2。<span><span>A−4IA - 4I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>4</span><span>I</span></span></span></span> 是 <span><span>[−111−1]\begin{bmatrix} -1 &amp; 1 \\ 1 &amp; -1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，零空间里是 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>；<span><span>A−2IA - 2I</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>I</span></span></span></span> 是 <span><span>[1111]\begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，零空间里是 <span><span>(−1,1)(-1, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。零空间是一整条直线，所以严格说不存在“那个”特征向量，而是一条线上的所有向量，我们只要选一个当代表。</p><p>方程里的 6 和 8 他特意问过学生：6 是 <span><span>3+33 + 3</span><span><span><span></span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span></span></span></span>，对角线之和，叫迹；8 是行列式。所以 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的特征方程就是</p><span><span><span>λ2−(迹) λ+(行列式)=0\lambda^2 - (\text{迹})\,\lambda + (\text{行列式}) = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span><span>迹</span></span><span>)</span><span></span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>行列式</span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>两个根的和等于迹，积等于行列式。一般的 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 也一样：<strong>特征值之和等于迹，特征值之积等于行列式</strong>。不过 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 大了没有现成公式去解多项式，这个事实更多是拿来检查的。（他也说了，<span><span>n≥3n \geq 3</span><span><span><span></span><span>n</span><span></span><span>≥</span><span></span></span><span><span></span><span>3</span></span></span></span> 之后求根就不好玩了，这部分交给计算机。）</p><p>下面这个脚本做四件事：置换矩阵 <span><span>[0110]\begin{bmatrix} 0 &amp; 1 \\ 1 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的两个特征向量；加 <span><span>3I3I</span><span><span><span></span><span>3</span><span>I</span></span></span></span> 得到上面的 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>；<code>np.poly</code> 取出特征多项式的系数，和迹、行列式对照；最后验证他警告的那件事，知道 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的特征值，不等于知道 <span><span>A+BA + B</span><span><span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span></span></span></span> 的特征值。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 置换矩阵：交换两个分量</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>x1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>x2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>-</span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ (1, 1) ="</span><span><span>, P </span><span>@</span><span> x1, </span></span><span>"，P @ (-1, 1) ="</span><span><span>, P </span><span>@</span><span> x2)</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(P)))</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹 ="</span><span><span>, np.</span><span>trace</span><span>(P), </span></span><span>"，行列式 ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(P), </span></span><span>4</span><span>))</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># 加上 3 * 单位矩阵：特征值各加 3，特征向量不变</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> P </span><span>+</span><span> </span><span>3.0</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"A ="</span><span><span>, A.</span><span>tolist</span><span>())</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A)))</span></span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ (1, 1) ="</span><span><span>, A </span><span>@</span><span> x1, </span></span><span>"，A @ (-1, 1) ="</span><span><span>, A </span><span>@</span><span> x2)</span></span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 特征方程 lambda^2 - 迹 * lambda + 行列式 = 0 的系数</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.poly(A) ="</span><span><span>, np.</span><span>poly</span><span>(A))</span></span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹 ="</span><span><span>, np.</span><span>trace</span><span>(A), </span></span><span>"，行列式 ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(A), </span></span><span>4</span><span>))</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"两个特征向量的内积："</span><span><span>, x1 </span><span>@</span><span> x2)</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 特征值的和与积并不能拆开：A + B 的特征值不是两边特征值的和</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"eig(A) ="</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A)))</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"eig(B) ="</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(B)))</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"eig(A + B) ="</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A </span><span>+</span><span> B)))</span></span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"eig(A @ B) ="</span><span><span>, np.</span><span>sort</span><span>(np.linalg.</span><span>eigvals</span><span>(A </span><span>@</span><span> B)))</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P @ (1, 1) = [1. 1.] ，P @ (-1, 1) = [ 1. -1.]</span></div></div><div><div><div>2</div></div><div><span>特征值： [-1.  1.]</span></div></div><div><div><div>3</div></div><div><span>迹 = 0.0 ，行列式 = -1.0</span></div></div><div><div><div>4</div></div><div><span>A = [[3.0, 1.0], [1.0, 3.0]]</span></div></div><div><div><div>5</div></div><div><span>特征值： [2. 4.]</span></div></div><div><div><div>6</div></div><div><span>A @ (1, 1) = [4. 4.] ，A @ (-1, 1) = [-2.  2.]</span></div></div><div><div><div>7</div></div><div><span>np.poly(A) = [ 1. -6.  8.]</span></div></div><div><div><div>8</div></div><div><span>迹 = 6.0 ，行列式 = 8.0</span></div></div><div><div><div>9</div></div><div><span>两个特征向量的内积： 0.0</span></div></div><div><div><div>10</div></div><div><span>eig(A) = [2. 4.]</span></div></div><div><div><div>11</div></div><div><span>eig(B) = [1. 1.]</span></div></div><div><div><div>12</div></div><div><span>eig(A + B) = [2.2679 5.7321]</span></div></div><div><div><div>13</div></div><div><span>eig(A @ B) = [1.1716 6.8284]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读几个结果。</p><ul>
<li>置换矩阵交换两个分量，<span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 交换后不变，<span><span>λ=1\lambda = 1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>；<span><span>(−1,1)(-1, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 交换后变成 <span><span>(1,−1)(1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，<span><span>λ=−1\lambda = -1</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>。迹是 0，所以知道一个特征值是 1，另一个就必须是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。他还特意指出：一个元素全不为负的矩阵，也可以有负的特征值。</li>
<li>加 <span><span>3I3I</span><span><span><span></span><span>3</span><span>I</span></span></span></span> 以后，特征值 <span><span>−1,1-1, 1</span><span><span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span></span></span></span> 变成 <span><span>2,42, 4</span><span><span><span></span><span>2</span><span>,</span><span></span><span>4</span></span></span></span>，各加了 3；两个特征向量 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 和 <span><span>(−1,1)(-1, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 完全没动。原因是 <span><span>Px=λxP\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>P</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span> 两边各加 <span><span>3x3\mathbf{x}</span><span><span><span></span><span>3</span><span>x</span></span></span></span>，就得到 <span><span>(P+3I)x=(λ+3)x(P + 3I)\mathbf{x} = (\lambda + 3)\mathbf{x}</span><span><span><span></span><span>(</span><span>P</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>I</span><span>)</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>)</span><span>x</span></span></span></span>。<code>np.poly(A)</code> 给出 <span><span>[1,−6,8][1, -6, 8]</span><span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span>−</span><span>6</span><span>,</span><span></span><span>8</span><span>]</span></span></span></span>，对应 <span><span>λ2−6λ+8\lambda^2 - 6\lambda + 8</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>6</span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>8</span></span></span></span>，迹 6.0，行列式 8.0，全部对上。</li>
<li>两个特征向量的内积是 0.0，垂直。他提前剧透过：对称矩阵的特征值是实数，特征向量互相垂直，这不是巧合，是定理，第 25 讲证明。</li>
<li>最后一组：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值是 2 和 4，我取的 <span><span>B=[1201]B = \begin{bmatrix} 1 &amp; 2 \\ 0 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的特征值是 1 和 1。如果特征值能拆开相加，<span><span>A+BA + B</span><span><span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span></span></span></span> 的特征值应该落在 <span><span>2+1=32 + 1 = 3</span><span><span><span></span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span> 和 <span><span>4+1=54 + 1 = 5</span><span><span><span></span><span>4</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 上，实际算出来是 2.2679 和 5.7321。<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 的特征值 1.1716 和 6.8284 也不是 <span><span>2×12 \times 1</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span> 和 <span><span>4×14 \times 1</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span>。</li>
</ul><p>最后这点在他的黑板上写成一个“错误论证”：<span><span>Ax=λxA\mathbf{x} = \lambda\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>x</span></span></span></span>，<span><span>Bx=αxB\mathbf{x} = \alpha\mathbf{x}</span><span><span><span></span><span>B</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>α</span><span>x</span></span></span></span>，相加得 <span><span>(A+B)x=(λ+α)x(A + B)\mathbf{x} = (\lambda + \alpha)\mathbf{x}</span><span><span><span></span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>)</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>λ</span><span></span><span>+</span><span></span></span><span><span></span><span>α</span><span>)</span><span>x</span></span></span></span>。毛病在于 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征向量一般不是 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的特征向量，<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 有自己的 <span><span>y\mathbf{y}</span><span><span><span></span><span>y</span></span></span></span>。特征值不是线性的，也不能相乘。只有 <span><span>B=3IB = 3I</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>I</span></span></span></span> 这种每个向量都是特征向量的矩阵，论证才成立。</p></section>
<section><h2>旋转：实矩阵，复特征值<a href="#旋转实矩阵复特征值"><span>#</span></a></h2><p><strong>什么情况下没有特征向量？</strong> 取 90 度旋转矩阵 <span><span>Q=[0−110]Q = \begin{bmatrix} 0 &amp; -1 \\ 1 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>（他写成 <span><span>[cos⁡90°−sin⁡90°sin⁡90°cos⁡90°]\begin{bmatrix} \cos 90° &amp; -\sin 90° \\ \sin 90° &amp; \cos 90° \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>cos</span><span></span><span>90°</span></span></span><span><span></span><span><span>sin</span><span></span><span>90°</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span></span><span>sin</span><span></span><span>90°</span></span></span><span><span></span><span><span>cos</span><span></span><span>90°</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的形式，并说它是“非常正交”的矩阵）。把每个向量转 90 度，哪个向量会和自己平行？没有。</p><p>方程给出同样的警告。迹是 0，所以两个特征值加起来是 0，一正一负；行列式是 1，所以乘起来是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>。两个实数一正一负，积不可能是正的。特征方程是 <span><span>λ2+1=0\lambda^2 + 1 = 0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，根是 <span><span>±i\pm i</span><span><span><span></span><span>±</span><span>i</span></span></span></span>：实矩阵出现了复特征值。他在这里半开玩笑地说，复数到底还是要进 18.06 了（“Boo”）。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rotation</span><span>(</span><span>theta</span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>c, s </span><span>=</span><span> np.</span><span>cos</span><span>(theta), np.</span><span>sin</span><span>(theta)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([[c, </span><span>-</span><span>s], [s, c]])</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># Strang 的 90 度旋转：他写成 [[0, -1], [1, 0]]</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> </span><span>rotation</span><span>(np.pi </span><span>/</span><span> </span><span>2</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>values, vectors </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(Q)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q ="</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(Q)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值："</span><span>, values)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量（按列）："</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(vectors)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q.T == -Q："</span><span><span>, np.</span><span>allclose</span><span>(Q.T, </span><span>-</span><span>Q))</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>22</div></div><div><span>        </span><span>"迹 ="</span><span>, </span><span>round</span><span><span>(np.</span><span>trace</span><span>(Q), </span></span><span>4</span><span>), </span><span>"，行列式 ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(Q), </span></span><span>4</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"lambda^2 + 1 = 0 的根："</span><span><span>, np.</span><span>roots</span><span>([</span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]))</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 复特征向量满足 Q v = lambda v</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> vectors[:, </span><span>0</span><span>]</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q @ v == lambda * v："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> v, values[</span></span><span>0</span><span><span>] </span><span>*</span><span> v))</span></span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 转 30 度：特征值是 exp(+-i theta)，模长是 1，幅角是 +-30 度</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> </span><span>rotation</span><span>(np.</span><span>deg2rad</span><span>(</span><span>30.0</span><span>))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>mu </span><span>=</span><span> np.linalg.</span><span>eigvals</span><span>(R)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"30 度旋转的特征值："</span><span>, mu)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"模长："</span><span><span>, np.</span><span>abs</span><span>(mu), </span></span><span>"，幅角（度）："</span><span><span>, np.</span><span>rad2deg</span><span>(np.</span><span>angle</span><span>(mu)))</span></span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Q =</span></div></div><div><div><div>2</div></div><div><span>[[ 0. -1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 1.  0.]]</span></div></div><div><div><div>4</div></div><div><span>特征值： [0.+1.j 0.-1.j]</span></div></div><div><div><div>5</div></div><div><span>特征向量（按列）：</span></div></div><div><div><div>6</div></div><div><span>[[0.7071+0.j     0.7071-0.j    ]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[0.    -0.7071j 0.    +0.7071j]]</span></div></div><div><div><div>8</div></div><div><span>Q.T == -Q： True</span></div></div><div><div><div>9</div></div><div><span>迹 = 0.0 ，行列式 = 1.0</span></div></div><div><div><div>10</div></div><div><span>lambda^2 + 1 = 0 的根： [-0.+1.j  0.-1.j]</span></div></div><div><div><div>11</div></div><div><span>Q @ v == lambda * v： True</span></div></div><div><div><div>12</div></div><div><span>30 度旋转的特征值： [0.866+0.5j 0.866-0.5j]</span></div></div><div><div><div>13</div></div><div><span>模长： [1. 1.] ，幅角（度）： [ 30. -30.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三点。</p><ul>
<li><code>np.linalg.eig</code> 对实矩阵 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 返回了复数特征值 <span><span>±i\pm i</span><span><span><span></span><span>±</span><span>i</span></span></span></span>，特征向量是复向量 <span><span>(1,−i)/2(1, -i)/\sqrt{2}</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>i</span><span>)</span><span>/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 和它的共轭，<span><span>Qv=λvQ\mathbf{v} = \lambda\mathbf{v}</span><span><span><span></span><span>Q</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>λ</span><span>v</span></span></span></span> 验证通过。复特征值总是成共轭对出现，这对实矩阵是定理。</li>
<li><span><span>QT=−QQ^\mathsf{T} = -Q</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>Q</span></span></span></span>：反对称。他的结论是：对称矩阵特征值是实的，反对称矩阵特征值是纯虚数，两者之间的一般矩阵是实部与虚部的混合。</li>
<li>我顺手把 90 度换成 30 度：特征值是 <span><span>cos⁡30°±isin⁡30°=0.866±0.5i\cos 30° \pm i\sin 30° = 0.866 \pm 0.5i</span><span><span><span></span><span>cos</span><span></span><span>30°</span><span></span><span>±</span><span></span></span><span><span></span><span>i</span><span></span><span>sin</span><span></span><span>30°</span><span></span><span>=</span><span></span></span><span><span></span><span>0.866</span><span></span><span>±</span><span></span></span><span><span></span><span>0.5</span><span>i</span></span></span></span>，模长是 1，幅角是 <span><span>±30°\pm 30°</span><span><span><span></span><span>±</span><span>30°</span></span></span></span>。旋转矩阵的特征值就是 <span><span>e±iθe^{\pm i\theta}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>±</span><span>i</span><span>θ</span></span></span></span></span></span></span></span></span></span></span></span>，模长为 1，意味着只转不伸缩。</li>
</ul></section>
<section><h2>重复特征值：特征向量不够用<a href="#重复特征值特征向量不够用"><span>#</span></a></h2><p><strong>还有更糟的情况吗？</strong> 把 <span><span>[3113]\begin{bmatrix} 3 &amp; 1 \\ 1 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 左下角的 1 改成 0，得到三角矩阵 <span><span>[3103]\begin{bmatrix} 3 &amp; 1 \\ 0 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。三角矩阵的行列式是对角线之积，特征值直接读出来：3 和 3，重复的特征值。他说，重复特征值是 18.06 里一切麻烦的来源。</p><p>麻烦在特征向量上。<span><span>A−3I=[0100]A - 3I = \begin{bmatrix} 0 &amp; 1 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，零空间只有 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 这一条线。第二个 <span><span>λ=3\lambda = 3</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span> 要找一个和 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 无关的特征向量，找不到。这个矩阵是 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span>，却只有一个独立的特征向量，他管它叫“退化”矩阵。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># Strang 把 [[3, 1], [1, 3]] 的左下角改成 0，得到三角矩阵</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>values, vectors </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(A)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征值（读对角线就行）："</span><span>, values)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.eig 给的特征向量（按列）："</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(vectors)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># A - 3I 的零空间只有一条线：秩是 1，不是 0</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>shifted </span><span>=</span><span> A </span><span>-</span><span> </span><span>3.0</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"A - 3I 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(shifted))</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>18</div></div><div><span>        </span><span>"(1, 0) 是特征向量："</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(A </span><span>@</span><span> [</span><span>1.0</span><span>, </span><span>0.0</span><span>], </span><span>3.0</span><span><span> </span><span>*</span><span> np.</span><span>array</span><span>([</span></span><span>1.0</span><span>, </span><span>0.0</span><span>])),</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>22</div></div><div><span>        </span><span>"(0, 1) 是特征向量："</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(A </span><span>@</span><span> [</span><span>0.0</span><span>, </span><span>1.0</span><span>], </span><span>3.0</span><span><span> </span><span>*</span><span> np.</span><span>array</span><span>([</span></span><span>0.0</span><span>, </span><span>1.0</span><span>])),</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 两列特征向量几乎平行：行列式接近 0，矩阵几乎不可逆</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量矩阵的行列式："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(vectors)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量矩阵的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(vectors))</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"特征向量矩阵的条件数："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(vectors)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span># 对照：特征值不同的矩阵，特征向量矩阵可逆得很好</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>_, sv </span><span>=</span><span> np.linalg.</span><span>eig</span><span>(S)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>35</div></div><div><span>        </span><span>"[[3, 1], [1, 3]] 的特征向量矩阵条件数："</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>cond</span><span>(sv), </span></span><span>4</span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>特征值（读对角线就行）： [3. 3.]</span></div></div><div><div><div>2</div></div><div><span>np.linalg.eig 给的特征向量（按列）：</span></div></div><div><div><div>3</div></div><div><span>[[ 1. -1.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  0.]]</span></div></div><div><div><div>5</div></div><div><span>A - 3I 的秩： 1</span></div></div><div><div><div>6</div></div><div><span>(1, 0) 是特征向量： True</span></div></div><div><div><div>7</div></div><div><span>(0, 1) 是特征向量： False</span></div></div><div><div><div>8</div></div><div><span>特征向量矩阵的行列式： 6.66e-16</span></div></div><div><div><div>9</div></div><div><span>特征向量矩阵的秩： 1</span></div></div><div><div><div>10</div></div><div><span>特征向量矩阵的条件数： 3.00e+15</span></div></div><div><div><div>11</div></div><div><span>[[3, 1], [1, 3]] 的特征向量矩阵条件数： 1.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>np.linalg.eig</code> 不会告诉你“找不到”，它老老实实地返回了两列：<span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和 <span><span>(−1,0)(-1, 0)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，第二列只是第一列的相反数。行列式 <span><span>6.66×10−166.66 \times 10^{-16}</span><span><span><span></span><span>6.66</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，秩 1，条件数 <span><span>3.00×10153.00 \times 10^{15}</span><span><span><span></span><span>3.00</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，已经逼近双精度的极限 <span><span>1/ε≈4.5×10151/\varepsilon \approx 4.5 \times 10^{15}</span><span><span><span></span><span>1/</span><span>ε</span><span></span><span>≈</span><span></span></span><span><span></span><span>4.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。对照一下，<span><span>[3113]\begin{bmatrix} 3 &amp; 1 \\ 1 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的特征向量矩阵条件数是 1.0，两个特征向量垂直，不能更好了。</p><p>下一讲会看到，这个“特征向量不够”的矩阵为什么不能对角化。教训是：函数返回了一个矩阵，不代表里面的列真的构成了基，要自己看秩或条件数。（我的运气好一点：它至少没有崩溃。）</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>离散系统 <span><span>x[k+1]=Ax[k]\mathbf{x}[k+1] = A\mathbf{x}[k]</span><span><span><span></span><span>x</span><span>[</span><span>k</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span>[</span><span>k</span><span>]</span></span></span></span> 的解是 <span><span>x[k]=Akx[0]\mathbf{x}[k] = A^k\mathbf{x}[0]</span><span><span><span></span><span>x</span><span>[</span><span>k</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>x</span><span>[</span><span>0</span><span>]</span></span></span></span>，它会不会衰减，看的就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值。如果 <span><span>x[0]\mathbf{x}[0]</span><span><span><span></span><span>x</span><span>[</span><span>0</span><span>]</span></span></span></span> 恰好是特征向量，<span><span>Akx[0]=λkx[0]A^k\mathbf{x}[0] = \lambda^k\mathbf{x}[0]</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>x</span><span>[</span><span>0</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>x</span><span>[</span><span>0</span><span>]</span></span></span></span>，每一步乘一个 <span><span>λ\lambda</span><span><span><span></span><span>λ</span></span></span></span>。所以 <span><span>∣λ∣&lt;1|\lambda| &lt; 1</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span> 衰减，<span><span>∣λ∣&gt;1|\lambda| &gt; 1</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>1</span></span></span></span> 发散，<span><span>∣λ∣=1|\lambda| = 1</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 不增不减；复特征值的幅角只管转，不影响模长。稳定性的判据是谱半径（特征值模长的最大值）是否小于 1，也就是所有特征值都在复平面的单位圆内。</p><p>我用三个形如 <span><span>[ab−ba]\begin{bmatrix} a &amp; b \\ -b &amp; a \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>−</span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的矩阵验证。这种矩阵等于 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 倍的旋转，<span><span>r=a2+b2r = \sqrt{a^2 + b^2}</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，特征值是 <span><span>a±bia \pm bi</span><span><span><span></span><span>a</span><span></span><span>±</span><span></span></span><span><span></span><span>bi</span></span></span></span>，所以 <span><span>∥x[k]∥=rk∥x[0]∥\|\mathbf{x}[k]\| = r^k\|\mathbf{x}[0]\|</span><span><span><span></span><span>∥</span><span>x</span><span>[</span><span>k</span><span>]</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span><span>r</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span>∥</span><span>x</span><span>[</span><span>0</span><span>]</span><span>∥</span></span></span></span> 是精确的。最后一个例子是视频里没有的，我自己加的：特征值全在单位圆内，但矩阵远离对称，状态的模长依然可以先涨后落。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>simulate</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>x0</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>steps</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 x[0], x[1], ..., x[steps] 的模长。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> x0.</span><span>copy</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>norms </span><span>=</span><span> [np.linalg.</span><span>norm</span><span>(x)]</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(steps):</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> A </span><span>@</span><span> x</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>norms.</span><span>append</span><span>(np.linalg.</span><span>norm</span><span>(x))</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>(norms)</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>x0 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>systems </span><span>=</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"稳定"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>0.9</span><span>, </span><span>0.2</span><span><span>], [</span><span>-</span></span><span>0.2</span><span>, </span><span>0.9</span><span>]]),</span></div></div><div><div><div>20</div></div><div><span>        </span><span>"临界"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>0.0</span><span>, </span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>]]),</span></div></div><div><div><div>21</div></div><div><span>        </span><span>"发散"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>, </span><span>0.2</span><span><span>], [</span><span>-</span></span><span>0.2</span><span>, </span><span>1.0</span><span>]]),</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>    </span><span>for</span><span> name, A </span><span>in</span><span><span> systems.</span><span>items</span><span>():</span></span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>radius </span><span>=</span><span> np.</span><span>max</span><span>(np.</span><span>abs</span><span>(np.linalg.</span><span>eigvals</span><span>(A)))</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>norms </span><span>=</span><span> </span><span>simulate</span><span>(A, x0, </span><span>100</span><span>)</span></div></div><div><div><div>26</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>27</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：谱半径 = </span><span>{</span><span>radius</span><span>:.4f</span><span>}</span><span>，"</span></div></div><div><div><div>28</div></div><div><span>            </span><span>f</span><span>"||x[100]|| = </span><span>{</span><span><span>norms[</span><span>-</span></span><span>1</span><span>]</span><span>:.4g</span><span>}</span><span>，"</span></div></div><div><div><div>29</div></div><div><span>            </span><span>f</span><span>"radius^100 * ||x[0]|| = </span><span>{</span><span><span>radius</span><span>**</span></span><span>100</span><span><span> </span><span>*</span><span> norms[</span></span><span>0</span><span>]</span><span>:.4g</span><span>}</span><span>"</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 特征值在单位圆内，模长也可能先涨后落</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.9</span><span>, </span><span>5.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.9</span><span>]])</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>norms </span><span>=</span><span> </span><span>simulate</span><span>(N, x0, </span><span>100</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"非对称的例子：谱半径 ="</span><span><span>, np.</span><span>max</span><span>(np.</span><span>abs</span><span>(np.linalg.</span><span>eigvals</span><span>(N))))</span></span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>37</div></div><div><span>        </span><span>f</span><span>"  峰值 ||x[k]|| = </span><span>{</span><span><span>norms.</span><span>max</span><span>()</span></span><span>:.2f</span><span>}</span><span>（k = </span><span>{</span><span><span>norms.</span><span>argmax</span><span>()</span></span><span>}</span><span>），"</span></div></div><div><div><div>38</div></div><div><span>        </span><span>f</span><span>"||x[100]|| = </span><span>{</span><span><span>norms[</span><span>-</span></span><span>1</span><span>]</span><span>:.4f</span><span>}</span><span>"</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>稳定：谱半径 = 0.9220，||x[100]|| = 0.0004183，radius^100 * ||x[0]|| = 0.0004183</span></div></div><div><div><div>2</div></div><div><span>临界：谱半径 = 1.0000，||x[100]|| = 1.414，radius^100 * ||x[0]|| = 1.414</span></div></div><div><div><div>3</div></div><div><span>发散：谱半径 = 1.0198，||x[100]|| = 10.05，radius^100 * ||x[0]|| = 10.05</span></div></div><div><div><div>4</div></div><div><span>非对称的例子：谱半径 = 0.9</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>峰值 ||x[k]|| = 19.76（k = 9），||x[100]|| = 0.0148</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>稳定的那个谱半径 0.9220，100 步后模长 0.0004183；发散的那个谱半径 1.0198，100 步后涨到 10.05；临界的（90 度旋转）转了 100 步也只是转，模长一直是 <span><span>2=1.414\sqrt{2} = 1.414</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.414</span></span></span></span>。前三行里实际模长和“谱半径的 100 次方乘初始模长”完全一致。</p><p>最后一个例子需要小心。<span><span>[0.9500.9]\begin{bmatrix} 0.9 &amp; 5 \\ 0 &amp; 0.9 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0.9</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>0.9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 就是上一节那类“重复特征值、一条特征向量”的矩阵，特征值只有 0.9，模长最终一定会衰减，但中间先冲到了 19.76（第 9 步），100 步后才回落到 0.0148。所以特征值回答的是“最终去哪里”，不回答“途中走多远”。对控制来说，这就是为什么只看极点位置有时不够。第 28 讲会把这种矩阵正式叫作 Jordan 块。</p><p>下一讲 Strang 会把特征向量收集成矩阵 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span>，把特征值放进对角矩阵 <span><span>Λ\Lambda</span><span><span><span></span><span>Λ</span></span></span></span>，写出 <span><span>A=SΛS−1A = S\Lambda S^{-1}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>S</span><span>Λ</span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，矩阵的幂 <span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 一下子就变得简单。刚才那个特征向量不够用的三角矩阵为什么写不成这种形式，也会在那里解释：<a href="/posts/linear-algebra-22-diagonalization-powers/">18.06 第 22 讲：对角化与矩阵的幂</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-20-cramer-inverse-volume/</id>
      <title type="text">18.06 第 20 讲：Cramer 法则、逆矩阵与体积</title>
      <published>2026-10-18T06:20:00.000Z</published>
      <updated>2026-10-18T06:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-20-cramer-inverse-volume/"/>
      <summary type="text">MIT 18.06 第 20 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 验证了余子式求逆、Cramer 法则和 |det| 等于面积与体积，并数了一下：解 10 阶方程组，Cramer 法则（用余子式算行列式）要 68588300 次乘法，消元大约 333 次，差了 205971 倍。</summary>
      <content type="html"><![CDATA[<p>行列式把一个矩阵压缩成一个数，这一讲 Strang 要用这个数给我们已经会算的东西配上公式：逆矩阵是 <span><span>CT/det⁡AC^T / \det A</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span></span><span>det</span><span></span><span>A</span></span></span></span>，解方程组是 <span><span>xj=det⁡Bj/det⁡Ax_j = \det B_j / \det A</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span></span><span>det</span><span></span><span>A</span></span></span></span>，平行四边形的面积是 <span><span>∣ad−bc∣|ad - bc|</span><span><span><span></span><span>∣</span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span><span>∣</span></span></span></span>。公式漂亮，但他一边写一边说“我不想让你用它”。我把 Cramer 法则的乘法次数数了一遍：10 阶方程组要 68588300 次，消元大约 333 次，差了 205971 倍。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=QNpj-gOXW9M" target="_blank">Lecture 20: Cramer’s rule, inverse matrix, and volume</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-20-cramers-rule-inverse-matrix-and-volume/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §5.2；§5.3 Areas and Volumes by Determinants
前置：<a href="/posts/linear-algebra-19-determinant-formulas-cofactors/">第 19 讲：行列式公式与代数余子式</a>
一句话：<span><span>A−1=CT/det⁡AA^{-1} = C^T / \det A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span></span><span>det</span><span></span><span>A</span></span></span></span>，<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 是代数余子式矩阵；由它推出 Cramer 法则，而 <span><span>∣det⁡A∣|\det A|</span><span><span><span></span><span>∣</span><span></span><span>det</span><span></span><span>A</span><span>∣</span></span></span></span> 就是以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的行为边的平行四边形或平行六面体的面积或体积。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他从 2 × 2 逆矩阵公式猜出 <span><span>CTC^T</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，再逐项验证 <span><span>ACT=(det⁡A)IAC^T = (\det A)I</span><span><span><span></span><span>A</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>det</span><span></span><span>A</span><span>)</span><span>I</span></span></span></span>。非对角线为什么是 0 那一段（他叫它“A screwed up”）值得认真听，本篇只用代码验了结论，没有重讲论证。</li>
<li>Cramer 法则的推导很短，他讲完就开始吐槽它的计算量，还提到小时候读过的一本书。这段可以放松地看。</li>
<li>体积那一段是全讲的重头：他把行列式的性质一条条拿出来，说“如果体积也满足这几条，体积就是行列式”。最后他自己说时间到了，性质 3b 的证明没有做完，教材里有图。</li>
<li>本篇已经用代码算过的内容：余子式求逆、Cramer 法则对照 <code>solve</code>、面积与体积的数值检查。想省时间的话，这几处可以倍速。</li>
</ul></div></div>
<section><h2>逆矩阵的公式：余子式矩阵的转置<a href="#逆矩阵的公式余子式矩阵的转置"><span>#</span></a></h2><p>Strang 开头先问：2 × 2 的逆矩阵我们早就会写了，</p><span><span><span>[abcd]−1=1ad−bc[d−b−ca]\begin{bmatrix} a &amp; b \\ c &amp; d \end{bmatrix}^{-1}
= \frac{1}{ad - bc}
\begin{bmatrix} d &amp; -b \\ -c &amp; a \end{bmatrix}</span><span><span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>a</span><span>d</span><span></span><span>−</span><span></span><span>b</span><span>c</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>−</span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>b</span></span></span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>分母是行列式，这很合理：行列式不是 0 的时候才有逆，除以它刚好。那右边那个矩阵是什么？他的提示是“想想余子式”。</p><p><span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 是 <span><span>aa</span><span><span><span></span><span>a</span></span></span></span> 的余子式（划掉第 1 行第 1 列，剩下 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span>）。<span><span>−b-b</span><span><span><span></span><span>−</span><span>b</span></span></span></span> 是 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 的余子式：划掉 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 所在的行和列，剩下 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span>，而 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 在第 2 行第 1 列，<span><span>2+12 + 1</span><span><span><span></span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 是奇数，所以带负号。所以右边的矩阵是余子式矩阵 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 再转置：<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第 <span><span>(i,j)(i, j)</span><span><span><span></span><span>(</span><span>i</span><span>,</span><span></span><span>j</span><span>)</span></span></span></span> 项是 <span><span>aija_{ij}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的余子式，<span><span>CTC^T</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 里的 <span><span>−b-b</span><span><span><span></span><span>−</span><span>b</span></span></span></span> 出现在第 1 行第 2 列，位置换了，是转置的缘故。</p><p>他把它写成一般形式：</p><span><span><span>A−1=1det⁡A CTA^{-1} = \frac{1}{\det A}\, C^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>det</span><span></span><span>A</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>第一个问题是：2 × 2 碰巧成立，别的公式也可能碰巧成立，凭什么相信它对 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 都对？他的办法是直接验证 <span><span>ACT=(det⁡A)IAC^T = (\det A) I</span><span><span><span></span><span>A</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>det</span><span></span><span>A</span><span>)</span><span>I</span></span></span></span>。</p><p>乘积 <span><span>ACTAC^T</span><span><span><span></span><span>A</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的第 <span><span>(i,j)(i, j)</span><span><span><span></span><span>(</span><span>i</span><span>,</span><span></span><span>j</span><span>)</span></span></span></span> 项是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行点乘 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行。</p><ul>
<li>对角线上 <span><span>i=ji = j</span><span><span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>j</span></span></span></span>：这是“第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行的每个元素乘它自己的余子式再相加”，正是上一讲的余子式展开，结果是 <span><span>det⁡A\det A</span><span><span><span></span><span>det</span><span></span><span>A</span></span></span></span>。</li>
<li>非对角线上 <span><span>i≠ji \neq j</span><span><span><span></span><span>i</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>j</span></span></span></span>：是第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行的元素乘第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行的余子式。他的论证是：这等于把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行换成第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行，得到一个有两行相同的矩阵，再按第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行做余子式展开。有两行相同的矩阵是奇异的，行列式是 0。而换掉第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行并不改变第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行的余子式（余子式是划掉第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行之后算的），所以展开式里的数字就是我们要的那个乘积。</li>
</ul><p>我用一个 3 × 3 矩阵把整条链都跑一遍：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>cofactor_matrix</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回代数余子式矩阵 C，C[i, j] 是划掉第 i 行第 j 列后的带符号行列式。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>zeros_like</span><span>(A)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>9</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>minor </span><span>=</span><span> np.</span><span>delete</span><span>(np.</span><span>delete</span><span>(A, i, </span><span>axis</span><span>=</span><span>0</span><span>), j, </span><span>axis</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>C[i, j] </span><span>=</span><span> (</span><span>-</span><span>1</span><span><span>) </span><span>**</span><span> (i </span><span>+</span><span> j) </span><span>*</span><span> np.linalg.</span><span>det</span><span>(minor)</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> C</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 2 x 2：d、-b、-c、a 就是余子式再转置</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>a, b, c, d </span><span>=</span><span> </span><span>3.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>4.0</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>A2 </span><span>=</span><span> np.</span><span>array</span><span>([[a, b], [c, d]])</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>C2 </span><span>=</span><span> </span><span>cofactor_matrix</span><span>(A2)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"2 x 2 的 C ="</span><span><span>, np.</span><span>round</span><span>(C2, </span></span><span>4</span><span><span>).</span><span>tolist</span><span>())</span></span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>24</div></div><div><span>        </span><span>"C.T / det 等于 inv(A)："</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(C2.T </span><span>/</span><span> np.linalg.</span><span>det</span><span>(A2), np.linalg.</span><span>inv</span><span>(A2)),</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 3 x 3：A @ C.T 应当是 det(A) * 单位阵</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>5.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>6.0</span><span>]])</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> </span><span>cofactor_matrix</span><span>(A)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>detA </span><span>=</span><span> np.linalg.</span><span>det</span><span>(A)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(A) ="</span><span>, </span><span>round</span><span>(detA, </span><span>4</span><span>))</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"C ="</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(C)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ C.T ="</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span><span>(np.</span><span>round</span><span>(A </span><span>@</span><span> C.T, </span></span><span>4</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>A_inv </span><span>=</span><span> C.T </span><span>/</span><span> detA</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>39</div></div><div><span>        </span><span>"C.T / det 等于 np.linalg.inv："</span><span><span>, np.</span><span>allclose</span><span>(A_inv, np.linalg.</span><span>inv</span><span>(A))</span></span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span># 对角线是“本行乘本行的余子式”，非对角线是“本行乘别的行的余子式”</span></div></div><div><div><div>43</div></div><div><span>    </span><span># 后者等于把 A 的第 1 行抄到第 2 行后再按第 2 行展开，所以是 0</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>A_screwed </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>A_screwed[</span><span>1</span><span><span>] </span><span>=</span><span> A[</span></span><span>0</span><span>]</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"第 1 行乘第 2 行的余子式 ="</span><span>, </span><span>round</span><span>(A[</span><span>0</span><span><span>] </span><span>@</span><span> C[</span></span><span>1</span><span>], </span><span>4</span><span>))</span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"两行相同的矩阵的 det ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(A_screwed), </span></span><span>4</span><span>))</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span># Strang 的问题：把 a11 加 1，逆矩阵怎么变</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>E[</span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>A_new </span><span>=</span><span> A </span><span>+</span><span> E</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>C_new </span><span>=</span><span> </span><span>cofactor_matrix</span><span>(A_new)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>55</div></div><div><span>        </span><span>"det 的变化 ="</span><span>,</span></div></div><div><div><div>56</div></div><div><span>        </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(A_new) </span><span>-</span><span> detA, </span></span><span>4</span><span>),</span></div></div><div><div><div>57</div></div><div><span>        </span><span>"，C11 ="</span><span>,</span></div></div><div><div><div>58</div></div><div><span>        </span><span>round</span><span>(C[</span><span>0</span><span>, </span><span>0</span><span>], </span><span>4</span><span>),</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>61</div></div><div><span>        </span><span>"第 1 行与第 1 列的余子式没变："</span><span>,</span></div></div><div><div><div>62</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(C_new[</span><span>0</span><span>], C[</span><span>0</span><span>]) </span><span>and</span><span><span> np.</span><span>allclose</span><span>(C_new[:, </span></span><span>0</span><span>], C[:, </span><span>0</span><span>]),</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>2 x 2 的 C = [[4.0, -2.0], [-1.0, 3.0]]</span></div></div><div><div><div>2</div></div><div><span>C.T / det 等于 inv(A)： True</span></div></div><div><div><div>3</div></div><div><span>det(A) = 22.0</span></div></div><div><div><div>4</div></div><div><span>C =</span></div></div><div><div><div>5</div></div><div><span>[[ 24.   5.  -4.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[-12.   3.   2.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ -2.  -5.   4.]]</span></div></div><div><div><div>8</div></div><div><span>A @ C.T =</span></div></div><div><div><div>9</div></div><div><span>[[22.  0.  0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 0. 22.  0.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 0.  0. 22.]]</span></div></div><div><div><div>12</div></div><div><span>C.T / det 等于 np.linalg.inv： True</span></div></div><div><div><div>13</div></div><div><span>第 1 行乘第 2 行的余子式 = 0.0</span></div></div><div><div><div>14</div></div><div><span>两行相同的矩阵的 det = 0.0</span></div></div><div><div><div>15</div></div><div><span>det 的变化 = 24.0 ，C11 = 24.0</span></div></div><div><div><div>16</div></div><div><span>第 1 行与第 1 列的余子式没变： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>先看 2 × 2：<span><span>[3124]\begin{bmatrix} 3 &amp; 1 \\ 2 &amp; 4 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 是 <span><span>[4−2−13]\begin{bmatrix} 4 &amp; -2 \\ -1 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，正好是 <span><span>d,−c,−b,ad, -c, -b, a</span><span><span><span></span><span>d</span><span>,</span><span></span><span>−</span><span>c</span><span>,</span><span></span><span>−</span><span>b</span><span>,</span><span></span><span>a</span></span></span></span>（和他黑板上一样，<span><span>−b-b</span><span><span><span></span><span>−</span><span>b</span></span></span></span> 落在 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 的位置）。3 × 3 的 <span><span>ACTAC^T</span><span><span><span></span><span>A</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是 <span><span>22I22I</span><span><span><span></span><span>22</span><span>I</span></span></span></span>：对角线是 <span><span>det⁡A=22\det A = 22</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>22</span></span></span></span>，所有非对角线都是 0。<span><span>CT/det⁡AC^T / \det A</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span></span><span>det</span><span></span><span>A</span></span></span></span> 和 <code>np.linalg.inv</code> 一致。</p><p>“第 1 行乘第 2 行的余子式 = 0”这一行就是他那段论证的数值版本：它等于“两行相同的矩阵的行列式”，后者也是 0。</p><p>顺便，他还留了一个问题：把 <span><span>a11a_{11}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 加 1，逆矩阵会怎样？他说公式里什么都看得见，只要看行列式和余子式怎么变。我照做了：行列式的变化恰好是 24，等于 <span><span>C11C_{11}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，而 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第 1 行和第 1 列一点没变。道理一眼就看得出来：余子式 <span><span>CijC_{ij}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是划掉第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列之后算的，第 1 行和第 1 列的余子式都把 <span><span>a11a_{11}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 划掉了，当然不含它。反过来，<span><span>det⁡A\det A</span><span><span><span></span><span>det</span><span></span><span>A</span></span></span></span> 按第 1 行展开是 <span><span>a11C11+⋯a_{11} C_{11} + \cdots</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span></span></span></span>，<span><span>a11a_{11}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 加 1，行列式就多一个 <span><span>C11C_{11}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p><p>有一个小观察，是他中途顺口说的，我觉得挺好：把 3 × 3 矩阵的元素按行写成 <span><span>a,b,c,d,e,f,g,h,ia, b, c, d, e, f, g, h, i</span><span><span><span></span><span>a</span><span>,</span><span></span><span>b</span><span>,</span><span></span><span>c</span><span>,</span><span></span><span>d</span><span>,</span><span></span><span>e</span><span>,</span><span></span><span>f</span><span>,</span><span></span><span>g</span><span>,</span><span></span><span>h</span><span>,</span><span></span><span>i</span></span></span></span>，<span><span>det⁡A\det A</span><span><span><span></span><span>det</span><span></span><span>A</span></span></span></span> 的每一项是 3 个元素的乘积（比如 <span><span>aeiaei</span><span><span><span></span><span>a</span><span>e</span><span>i</span></span></span></span>），而余子式的每一项只有 2 个元素的乘积（<span><span>aa</span><span><span><span></span><span>a</span></span></span></span> 的余子式是 <span><span>ei−fhei - fh</span><span><span><span></span><span>e</span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span>f</span><span>h</span></span></span></span>）。一般地，行列式的项有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个因子，余子式的项有 <span><span>n−1n - 1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 个。我们用 Gauss-Jordan（<span><span>[A I]→[I A−1][A\ I] \to [I\ A^{-1}]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>I</span><span>]</span><span></span><span>→</span><span></span></span><span><span></span><span>[</span><span>I</span><span> </span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>]</span></span></span></span>）求逆时从来没看见过这件事，因为那是算法，不是代数。有了公式，才能对着字母看清 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的每一项由哪些元素构成。</p></section>
<section><h2>Cramer 法则：用公式解方程组<a href="#cramer-法则用公式解方程组"><span>#</span></a></h2><p>有了逆矩阵的公式，解 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 就是 <span><span>x=A−1b=1det⁡ACTb\mathbf{x} = A^{-1}\mathbf{b} = \frac{1}{\det A} C^T \mathbf{b}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>d</span><span>e</span><span>t</span></span><span></span><span>A</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>。它给出的当然还是第 1 讲用消元得到的那个唯一的解，现在是用字母写出来的了。</p><p>看 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 的第一个分量：<span><span>CTbC^T \mathbf{b}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 的第一项是 <span><span>b1C11+b2C21+⋯+bnCn1b_1 C_{11} + b_2 C_{21} + \cdots + b_n C_{n1}</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>n</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，是一列余子式乘一列数。Strang 说：“每次你把余子式乘上数，就是在算某个东西的行列式。”是什么东西的？按第 1 列做余子式展开：一个第 1 列是 <span><span>b1,…,bnb_1, \dots, b_n</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的矩阵，它的余子式恰好是 <span><span>C11,C21,…C_{11}, C_{21}, \dots</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span></span></span></span>（余子式只依赖于划掉第 1 列之后剩下的部分，也就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的其余各列）。这个矩阵叫 <span><span>B1B_1</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 1 列换成 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>。</p><p>一般地：</p><span><span><span>xj=det⁡Bjdet⁡A,Bj=A 的第 j 列换成 bx_j = \frac{\det B_j}{\det A},\qquad B_j = A \text{ 的第 } j \text{ 列换成 } \mathbf{b}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>det</span><span></span><span>A</span></span></span><span><span></span><span></span></span><span><span></span><span><span>det</span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span> </span><span>的第</span><span> </span></span><span>j</span><span><span> </span><span>列换成</span><span> </span></span><span>b</span></span></span></span></span><p>这就是 Cramer 法则。他说教材里有另一个更巧的证明，不用这么多余子式，他讲的这个胜在顺手：上面的每一步都是刚学过的东西。</p><section><h3>它好用吗？<a href="#它好用吗"><span>#</span></a></h3><p>这是他这讲里最有意思的一段。要算 <span><span>n+1n + 1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个行列式（<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 一个，<span><span>B1,…,BnB_1, \dots, B_n</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 各一个），“好”还是“不好”要看这些行列式怎么算。他小时候读过一本给爱好数学的孩子看的书，叫《Mathematics for the Million》，书里把消元讲得极其复杂，然后拿出 Cramer 法则说这才是正路。他现在的看法是：Cramer 法则是灾难性的，计算量“大约是永远”，所以那本书的名字应该是 Mathematics for the Millionaire（买得起这种计算量的人才用得上）。</p><p>我来数一下“永远”是多久。先验证 Cramer 法则对：用第 1 讲的 3 × 3 方程组和一个随机的 6 × 6 方程组对照 <code>np.linalg.solve</code>。然后数代价：行列式用余子式展开来算，乘法次数满足 <span><span>m(n)=n (m(n−1)+1)m(n) = n\,(m(n-1) + 1)</span><span><span><span></span><span>m</span><span>(</span><span>n</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>(</span><span>m</span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>（<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个元素各乘一个 <span><span>n−1n-1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 阶行列式，再多一次乘法），<span><span>m(1)=0m(1) = 0</span><span><span><span></span><span>m</span><span>(</span><span>1</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。Cramer 法则要 <span><span>n+1n + 1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个这样的行列式，消元大约 <span><span>n3/3n^3/3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span></span> 次乘法。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>det_cofactor</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; tuple[</span><span>float</span><span>, </span><span>int</span><span>]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""按第一行做余子式展开，返回行列式与乘法次数。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span> </span><span>float</span><span>(A[</span><span>0</span><span>, </span><span>0</span><span>]), </span><span>0</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>total, mults </span><span>=</span><span> </span><span>0.0</span><span>, </span><span>0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>minor </span><span>=</span><span> np.</span><span>delete</span><span>(A[</span><span>1</span><span>:], j, </span><span>axis</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>sub, m </span><span>=</span><span> </span><span>det_cofactor</span><span>(minor)</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>total </span><span>+=</span><span> (</span><span>-</span><span>1</span><span><span>) </span><span>**</span><span> j </span><span>*</span><span> A[</span></span><span>0</span><span><span>, j] </span><span>*</span><span> sub</span></span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>mults </span><span>+=</span><span> m </span><span>+</span><span> </span><span>1</span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span> total, mults</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>def</span><span> </span><span>cramer</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>"""Cramer 法则：x_j = det(B_j) / det(A)，B_j 是 A 的第 j 列换成 b。"""</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>detA </span><span>=</span><span> np.linalg.</span><span>det</span><span>(A)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>zeros</span><span>(A.shape[</span><span>0</span><span>])</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>0</span><span>]):</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>B </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>B[:, j] </span><span>=</span><span> b</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>x[j] </span><span>=</span><span> np.linalg.</span><span>det</span><span>(B) </span><span>/</span><span> detA</span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span> x</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># Strang 第 1 讲的 3 x 3 方程组，Cramer 法则与 solve 对照</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>, </span><span>4.0</span><span>]])</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>4.0</span><span>])</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"Cramer："</span><span><span>, </span><span>cramer</span><span>(A, b))</span></span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"solve： "</span><span><span>, np.linalg.</span><span>solve</span><span>(A, b) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 随机 6 x 6：两种方法的差</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>6</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>x_c, x_s </span><span>=</span><span> </span><span>cramer</span><span>(M, r), np.linalg.</span><span>solve</span><span>(M, r)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"6 x 6 两种解的最大差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.</span><span>abs</span><span>(x_c </span><span>-</span><span> x_s).</span><span>max</span><span>()</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span># 代价：Cramer 要算 n + 1 个行列式，这里每个都用余子式展开</span></div></div><div><div><div>46</div></div><div><span>    </span><span># 余子式展开的乘法次数满足 m(n) = n * (m(n - 1) + 1)，m(1) = 0</span></div></div><div><div><div>47</div></div><div><span>    </span><span># 消元解一个方程组大约要 n^3 / 3 次乘法</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span><span>"n  Cramer 乘法次数  消元 n^3/3  比值"</span><span>)</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>50</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>, </span><span>11</span><span>):</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>m </span><span>=</span><span> n </span><span>*</span><span> (m </span><span>+</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>52</div></div><div><span>        </span><span>if</span><span> n </span><span>in</span><span> (</span><span>3</span><span>, </span><span>5</span><span>, </span><span>8</span><span>, </span><span>10</span><span>):</span></div></div><div><div><div>53</div></div><div><span><span>            </span></span><span>_, counted </span><span>=</span><span> </span><span>det_cofactor</span><span>(rng.</span><span>standard_normal</span><span>((n, n)))</span></div></div><div><div><div>54</div></div><div><span><span>            </span></span><span>cramer_cost </span><span>=</span><span> (n </span><span>+</span><span> </span><span>1</span><span><span>) </span><span>*</span><span> m</span></span></div></div><div><div><div>55</div></div><div><span><span>            </span></span><span>elim_cost </span><span>=</span><span> n</span><span>**</span><span>3</span><span><span> </span><span>//</span><span> </span></span><span>3</span></div></div><div><div><div>56</div></div><div><span>            </span><span>print</span><span>(</span></div></div><div><div><div>57</div></div><div><span>                </span><span>f</span><span>"</span><span>{</span><span>n</span><span>:&lt;2d</span><span>}</span><span> </span><span>{</span><span>cramer_cost</span><span>:&gt;15d</span><span>}</span><span> </span><span>{</span><span>elim_cost</span><span>:&gt;11d</span><span>}</span><span> "</span></div></div><div><div><div>58</div></div><div><span>                </span><span>f</span><span>"</span><span>{</span><span><span>cramer_cost </span><span>/</span><span> elim_cost</span></span><span>:&gt;8.0f</span><span>}</span><span>  递推与计数一致：</span><span>{</span><span><span>m </span><span>==</span><span> counted</span></span><span>}</span><span>"</span></div></div><div><div><div>59</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Cramer： [0. 0. 1.]</span></div></div><div><div><div>2</div></div><div><span>solve：  [0. 0. 1.]</span></div></div><div><div><div>3</div></div><div><span>6 x 6 两种解的最大差： 1.3e-15</span></div></div><div><div><div>4</div></div><div><span>n  Cramer 乘法次数  消元 n^3/3  比值</span></div></div><div><div><div>5</div></div><div><span>3               36           9        4  递推与计数一致：True</span></div></div><div><div><div>6</div></div><div><span>5             1230          41       30  递推与计数一致：True</span></div></div><div><div><div>7</div></div><div><span>8           623520         170     3668  递推与计数一致：True</span></div></div><div><div><div>8</div></div><div><span>10        68588300         333   205971  递推与计数一致：True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对不对：第 1 讲那个方程组的解是 <span><span>(0,0,1)(0, 0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，两种方法都给出这个解；6 × 6 的随机方程组两种解最大差 <span><span>1.3×10−151.3 \times 10^{-15}</span><span><span><span></span><span>1.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，是舍入误差。Cramer 法则没毛病，毛病在价格。</p><p>价格：3 阶时 Cramer 只贵 4 倍；5 阶是 30 倍；8 阶 3668 倍；10 阶 68588300 次乘法对 333 次，差了 205971 倍。最后一列“递推与计数一致”是我的 <code>det_cofactor</code> 真的数出来的乘法次数和递推式相等，所以这些数字不是凭公式推的。余子式展开的乘法次数增长得像 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span>，我猜这就是他说的“大约是永远”。</p><p>必须说明一下公平性：上面的 <code>cramer</code> 函数里行列式是 <code>np.linalg.det</code> 算的，那用的是 LU 分解，不是余子式。Strang 说的灾难是指用定义（余子式或大公式）去算行列式。如果每个行列式也用消元算，<span><span>n+1n + 1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个行列式就是 <span><span>n+1n + 1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 次消元，大约比直接消元一次贵 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 倍，没有那么夸张，但还是白白多做了 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 倍。这是我按复杂度推的，没测。无论怎样，他的结论成立：MATLAB（NumPy 也一样）绝不会这么干。</p><p>那它还有什么用？他的回答我很同意：用字母做代数。逆矩阵和解各个分量的显式公式，可以让你推“某个系数变化时解怎么变”，这是算法告诉不了你的。（我自己的取舍：定理要记，代码不写。）</p></section></section>
<section><h2>行列式是体积<a href="#行列式是体积"><span>#</span></a></h2><p>第三个应用最漂亮。他先说结论：</p><blockquote><p><span><span>∣det⁡A∣|\det A|</span><span><span><span></span><span>∣</span><span></span><span>det</span><span></span><span>A</span><span>∣</span></span></span></span> 等于以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的行为边的盒子的体积。</p></blockquote><p>3 × 3 的情况：三行是三个向量，从原点画出去，当作盒子的三条边，补全成平行六面体（他说正式的名字是 parallelepiped，“为了显而易见的原因我写的是 box”）。体积就是 <span><span>∣det⁡A∣|\det A|</span><span><span><span></span><span>∣</span><span></span><span>det</span><span></span><span>A</span><span>∣</span></span></span></span>。符号呢？他说符号告诉我们这个盒子是右手系还是左手系：交换两条边，体积不变，只有顺序的方向反过来。这和第 18 讲的“交换两行，行列式变号”是同一件事。</p><section><h3>为什么<a href="#为什么"><span>#</span></a></h3><p>他的证法是：行列式由三条性质定义（单位阵的行列式是 1，交换两行变号，对每一行线性）。如果体积也满足这三条，它就必须是行列式（取绝对值）。</p><ol>
<li>单位阵：三条边是坐标轴上的单位向量，盒子是单位立方体，体积 1。</li>
<li>正交矩阵 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>：<span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。两边取行列式，<span><span>det⁡(QT)det⁡(Q)=1\det(Q^T)\det(Q) = 1</span><span><span><span></span><span>det</span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>det</span><span>(</span><span>Q</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，而 <span><span>det⁡(QT)=det⁡(Q)\det(Q^T) = \det(Q)</span><span><span><span></span><span>det</span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span>(</span><span>Q</span><span>)</span></span></span></span>，所以 <span><span>(det⁡Q)2=1(\det Q)^2 = 1</span><span><span><span></span><span>(</span><span>det</span><span></span><span>Q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，<span><span>det⁡Q=±1\det Q = \pm 1</span><span><span><span></span><span>det</span><span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>±</span><span>1</span></span></span></span>。几何上：<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的行是互相垂直的单位向量，盒子还是单位立方体，只是转了一下，体积还是 1。</li>
<li>性质 3a：把一条边加倍（保持直角），体积加倍；行列式的一行乘 2，行列式也乘 2。</li>
<li>性质 3b：一行拆成两个向量的和，行列式等于两个行列式之和。这一步他没有证完（时间到了）；他在 2 × 2 里把它画成平行四边形，说教材里有一幅仔细的图。</li>
</ol><p>我用代码检查了第 1 到第 3 点的数值，3b 用线性关系检查，并多加了一个剪切：第一行加上第二行的 0.7 倍，盒子被推歪了，体积没有变（第 18 讲的“一行加上另一行的倍数，行列式不变”）。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 单位立方体与旋转后的立方体</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>Q, _ </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(rng.</span><span>standard_normal</span><span>((</span><span>3</span><span>, </span><span>3</span><span>)))</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q^T Q = 单位阵："</span><span><span>, np.</span><span>allclose</span><span>(Q.T </span><span>@</span><span> Q, identity))</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(I) ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(identity), </span></span><span>4</span><span>))</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>14</div></div><div><span>        </span><span>"det(Q) ="</span><span>,</span></div></div><div><div><div>15</div></div><div><span>        </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(Q), </span></span><span>4</span><span>),</span></div></div><div><div><div>16</div></div><div><span>        </span><span>"，det(Q)^2 ="</span><span>,</span></div></div><div><div><div>17</div></div><div><span>        </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(Q) </span><span>**</span><span> </span></span><span>2</span><span>, </span><span>4</span><span>),</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 把第一条边加倍，体积加倍（性质 3a）</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> Q.</span><span>copy</span><span>()</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>D[</span><span>0</span><span><span>] </span><span>*=</span><span> </span></span><span>2.0</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"加倍第一行："</span><span>, </span><span>round</span><span>(</span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(D)), </span></span><span>4</span><span>))</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 把一条边改成两条边的和（性质 3b 的线性）</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>M_v, M_w, M_sum </span><span>=</span><span> Q.</span><span>copy</span><span>(), Q.</span><span>copy</span><span>(), Q.</span><span>copy</span><span>()</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>M_v[</span><span>0</span><span>], M_w[</span><span>0</span><span>], M_sum[</span><span>0</span><span><span>] </span><span>=</span><span> v, w, v </span><span>+</span><span> w</span></span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>lhs </span><span>=</span><span> np.linalg.</span><span>det</span><span>(M_sum)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>rhs </span><span>=</span><span> np.linalg.</span><span>det</span><span>(M_v) </span><span>+</span><span> np.linalg.</span><span>det</span><span>(M_w)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(v + w 行) 等于两个 det 之和："</span><span><span>, np.</span><span>isclose</span><span>(lhs, rhs))</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span># 剪切：第一行加上第二行的 0.7 倍，盒子歪了，体积不变</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> Q.</span><span>copy</span><span>()</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>S[</span><span>0</span><span><span>] </span><span>+=</span><span> </span></span><span>0.7</span><span><span> </span><span>*</span><span> S[</span></span><span>1</span><span>]</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"剪切后体积："</span><span>, </span><span>round</span><span>(</span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(S)), </span></span><span>4</span><span>))</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Q^T Q = 单位阵： True</span></div></div><div><div><div>2</div></div><div><span>det(I) = 1.0</span></div></div><div><div><div>3</div></div><div><span>det(Q) = 1.0 ，det(Q)^2 = 1.0</span></div></div><div><div><div>4</div></div><div><span>加倍第一行： 2.0</span></div></div><div><div><div>5</div></div><div><span>det(v + w 行) 等于两个 det 之和： True</span></div></div><div><div><div>6</div></div><div><span>剪切后体积： 1.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>随机生成的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>（来自一个固定种子的 QR 分解）满足 <span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，<span><span>det⁡Q=1.0\det Q = 1.0</span><span><span><span></span><span>det</span><span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>1.0</span></span></span></span>（这次是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>，换个种子也可能是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，平方一定是 1）。把一行加倍，体积从 1.0 变成 2.0；剪切后体积仍然是 1.0。拆行那一项两边相等。四条数值检验和他的论证是一一对应的。</p></section><section><h3>面积：没有平方根的公式<a href="#面积没有平方根的公式"><span>#</span></a></h3><p>他最想让我们记住的是 2 × 2 的情形：以 <span><span>(a,b)(a, b)</span><span><span><span></span><span>(</span><span>a</span><span>,</span><span></span><span>b</span><span>)</span></span></span></span> 和 <span><span>(c,d)(c, d)</span><span><span><span></span><span>(</span><span>c</span><span>,</span><span></span><span>d</span><span>)</span></span></span></span> 为边的平行四边形，面积是</p><span><span><span>∣det⁡[abcd]∣=∣ad−bc∣\left|\det\begin{bmatrix} a &amp; b \\ c &amp; d \end{bmatrix}\right| = |ad - bc|</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>det</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>∣</span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span><span>∣</span></span></span></span></span><p>他说，如果去年问你平行四边形的面积，你会说底乘高：先算底边的长度 <span><span>a2+b2\sqrt{a^2 + b^2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，再算高，里面有更多的平方根和角度，“令人反胃”。现在只有一个 <span><span>ad−bcad - bc</span><span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span></span></span></span>，没有平方根，没有角度，没有高，只用到坐标。三角形是一半：<span><span>12∣ad−bc∣\tfrac12 |ad - bc|</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>∣</span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span><span>∣</span></span></span></span>。</p><p>如果三角形不在原点呢？他给出的是</p><span><span><span>面积=12∣det⁡[x1y11x2y21x3y31]∣\text{面积} = \frac12 \left|\det\begin{bmatrix} x_1 &amp; y_1 &amp; 1 \\ x_2 &amp; y_2 &amp; 1 \\ x_3 &amp; y_3 &amp; 1 \end{bmatrix}\right|</span><span><span><span></span><span><span>面积</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>det</span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>“只给六个数就能定面积”。他的解释是：第 2 行减第 1 行，第 3 行减第 1 行，最后一列就被消掉，剩下的是两条边向量的 2 × 2 行列式，相当于把三角形平移到了原点（这一步他讲得很快，教材有图）。</p><p>我分别用底乘高和行列式算平行四边形的面积，再用 3 × 3 行列式和平移后的 2 × 2 行列式算同一个三角形，最后算一个 3 维盒子的体积，并用标量三重积对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 平行四边形：两条边是 A 的两行</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>u, v </span><span>=</span><span> A[</span><span>0</span><span>], A[</span><span>1</span><span>]</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>base </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(u)</span></div></div><div><div><div>11</div></div><div><span>    </span><span># 底乘高：v 在 u 垂直方向上的分量长度就是高</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>height </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(v </span><span>-</span><span> (u </span><span>@</span><span> v) </span><span>/</span><span> (u </span><span>@</span><span> u) </span><span>*</span><span> u)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"det ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(A), </span></span><span>4</span><span>))</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"底 x 高 ="</span><span>,</span></div></div><div><div><div>16</div></div><div><span>        </span><span>round</span><span><span>(base </span><span>*</span><span> height, </span></span><span>4</span><span>),</span></div></div><div><div><div>17</div></div><div><span>        </span><span>"，底 ="</span><span>,</span></div></div><div><div><div>18</div></div><div><span>        </span><span>round</span><span>(base, </span><span>4</span><span>),</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"，高 ="</span><span>,</span></div></div><div><div><div>20</div></div><div><span>        </span><span>round</span><span>(height, </span><span>4</span><span>),</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 三角形：三个顶点任意放，面积 = |det [[x, y, 1], ...]| / 2</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>5.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>]])</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>T </span><span>=</span><span> np.</span><span>hstack</span><span>([P, np.</span><span>ones</span><span>((</span><span>3</span><span>, </span><span>1</span><span>))])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>area </span><span>=</span><span> </span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(T)) </span><span>/</span><span> </span></span><span>2</span></div></div><div><div><div>27</div></div><div><span>    </span><span># 对照：平移到原点后取两条边，面积是 |det| / 2</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>edges </span><span>=</span><span> np.</span><span>array</span><span>([P[</span><span>1</span><span><span>] </span><span>-</span><span> P[</span></span><span>0</span><span>], P[</span><span>2</span><span><span>] </span><span>-</span><span> P[</span></span><span>0</span><span>]])</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"三角形面积（3 x 3 行列式）="</span><span>, </span><span>round</span><span>(area, </span><span>4</span><span>))</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>31</div></div><div><span>        </span><span>"三角形面积（平移后 2 x 2）="</span><span>, </span><span>round</span><span>(</span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(edges)) </span><span>/</span><span> </span></span><span>2</span><span>, </span><span>4</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span># 三维：三行是盒子的三条边，体积是 |det|</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"盒子体积 ="</span><span>, </span><span>round</span><span>(</span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(B)), </span></span><span>4</span><span>))</span></div></div><div><div><div>37</div></div><div><span>    </span><span># 对照：标量三重积 u . (v x w)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"三重积 ="</span><span>, </span><span>round</span><span>(B[</span><span>0</span><span><span>] </span><span>@</span><span> np.</span><span>cross</span><span>(B[</span></span><span>1</span><span>], B[</span><span>2</span><span>]), </span><span>4</span><span>))</span></div></div><div><div><div>39</div></div><div><span>    </span><span># 交换两条边，体积不变，符号反过来</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>B_swap </span><span>=</span><span> B[[</span><span>1</span><span>, </span><span>0</span><span>, </span><span>2</span><span>]]</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"交换两行后 det ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(B_swap), </span></span><span>4</span><span>))</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>det = 5.0</span></div></div><div><div><div>2</div></div><div><span>底 x 高 = 5.0 ，底 = 3.1623 ，高 = 1.5811</span></div></div><div><div><div>3</div></div><div><span>三角形面积（3 x 3 行列式）= 5.5</span></div></div><div><div><div>4</div></div><div><span>三角形面积（平移后 2 x 2）= 5.5</span></div></div><div><div><div>5</div></div><div><span>盒子体积 = 6.0</span></div></div><div><div><div>6</div></div><div><span>三重积 = 6.0</span></div></div><div><div><div>7</div></div><div><span>交换两行后 det = -6.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>平行四边形的两条边是 <span><span>(3,1)(3, 1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 和 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>：行列式是 5.0。底乘高这条路要先算底 <span><span>10≈3.1623\sqrt{10} \approx 3.1623</span><span><span><span></span><span><span><span><span><span><span></span><span><span>10</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>3.1623</span></span></span></span>，再用投影减掉平行分量求出高 1.5811，乘出来也是 5.0，只是多绕了一圈，还要投影（第 15 讲的东西）。三角形用 3 × 3 行列式和平移后的 2 × 2 都得到 5.5。盒子的体积 6.0 与三重积相同，交换两行之后行列式变成 <span><span>−6.0-6.0</span><span><span><span></span><span>−</span><span>6.0</span></span></span></span>，体积还是 6。</p><p>下面这幅图把前两个例子画了出来：</p><p></p><figure><img src="/assets/images/linear-algebra/l20-area.svg" alt="左：以 A 的两行 (3, 1) 与 (1, 2) 为边的平行四边形，面积是 |det A| = 5。右：顶点为 (1, 1)、(5, 2)、(2, 4) 的三角形，面积是 5.5" /><figcaption>左：以 A 的两行 (3, 1) 与 (1, 2) 为边的平行四边形，面积是 |det A| = 5。右：顶点为 (1, 1)、(5, 2)、(2, 4) 的三角形，面积是 5.5</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div><span>from</span><span> matplotlib.patches </span><span>import</span><span> Polygon</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>fig, (ax1, ax2) </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 左：平行四边形，两条边是 A 的两行，面积 = |det A|</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>u, v </span><span>=</span><span> A[</span><span>0</span><span>], A[</span><span>1</span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>area </span><span>=</span><span> </span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(A))</span></span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>ax1.</span><span>add_patch</span><span>(</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>Polygon</span><span>(</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>[[</span><span>0</span><span>, </span><span>0</span><span><span>], u, u </span><span>+</span><span> v, v],</span></span></div></div><div><div><div>20</div></div><div><span>            </span><span>closed</span><span>=</span><span>True</span><span>,</span></div></div><div><div><div>21</div></div><div><span>            </span><span>fc</span><span>=</span><span>"#9ecae1"</span><span>,</span></div></div><div><div><div>22</div></div><div><span>            </span><span>ec</span><span>=</span><span>"#2171b5"</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>alpha</span><span>=</span><span>0.7</span><span>,</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>for</span><span> w, name, off </span><span>in</span><span> [</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>(u, </span><span>"row 1 = (3, 1)"</span><span>, (</span><span>0.1</span><span><span>, </span><span>-</span></span><span>0.35</span><span>)),</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>(v, </span><span>"row 2 = (1, 2)"</span><span><span>, (</span><span>-</span></span><span>1.5</span><span>, </span><span>0.1</span><span>)),</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>]:</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>ax1.</span><span>annotate</span><span>(</span></div></div><div><div><div>31</div></div><div><span>            </span><span>""</span><span>,</span></div></div><div><div><div>32</div></div><div><span>            </span><span>xy</span><span><span>=</span><span>w,</span></span></div></div><div><div><div>33</div></div><div><span>            </span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>34</div></div><div><span>            </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>arrowstyle</span><span>=</span><span>"-&gt;"</span><span>, </span><span>color</span><span>=</span><span>"#08519c"</span><span>, </span><span>lw</span><span>=</span><span>2</span><span>),</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>ax1.</span><span>text</span><span>(w[</span><span>0</span><span><span>] </span><span>+</span><span> off[</span></span><span>0</span><span>], w[</span><span>1</span><span><span>] </span><span>+</span><span> off[</span></span><span>1</span><span>], name, </span><span>color</span><span>=</span><span>"#08519c"</span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>ax1.</span><span>text</span><span>(</span><span>1.6</span><span>, </span><span>1.6</span><span>, </span><span>f</span><span>"area = |det A| = </span><span>{</span><span>area</span><span>:.0f</span><span>}</span><span>"</span><span>, </span><span>ha</span><span>=</span><span>"center"</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlim</span><span>(</span><span>-</span><span>0.8</span><span>, </span><span>5.2</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylim</span><span>(</span><span>-</span><span>0.8</span><span>, </span><span>3.8</span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_title</span><span>(</span><span>"Parallelogram from the rows of A"</span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>ax1.</span><span>grid</span><span>(</span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span># 右：任意位置的三角形，面积 = |det [[x, y, 1], ...]| / 2</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>5.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>]])</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>T </span><span>=</span><span> np.</span><span>hstack</span><span>([P, np.</span><span>ones</span><span>((</span><span>3</span><span>, </span><span>1</span><span>))])</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>tri </span><span>=</span><span> </span><span>abs</span><span><span>(np.linalg.</span><span>det</span><span>(T)) </span><span>/</span><span> </span></span><span>2</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>ax2.</span><span>add_patch</span><span>(</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>Polygon</span><span>(P, </span><span>closed</span><span>=</span><span>True</span><span>, </span><span>fc</span><span>=</span><span>"#fdd0a2"</span><span>, </span><span>ec</span><span>=</span><span>"#d94801"</span><span>, </span><span>alpha</span><span>=</span><span>0.7</span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>for</span><span> x, y </span><span>in</span><span> P:</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>ax2.</span><span>plot</span><span>(x, y, </span><span>"o"</span><span>, </span><span>color</span><span>=</span><span>"#d94801"</span><span>)</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>ax2.</span><span>text</span><span>(x </span><span>+</span><span> </span><span>0.1</span><span><span>, y </span><span>-</span><span> </span></span><span>0.3</span><span>, </span><span>f</span><span>"(</span><span>{</span><span>x</span><span>:.0f</span><span>}</span><span>, </span><span>{</span><span>y</span><span>:.0f</span><span>}</span><span>)"</span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>ax2.</span><span>text</span><span>(</span><span>2.9</span><span>, </span><span>2.4</span><span>, </span><span>f</span><span>"area = </span><span>{</span><span>tri</span><span>:.1f</span><span>}</span><span>"</span><span>, </span><span>ha</span><span>=</span><span>"center"</span><span>)</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlim</span><span>(</span><span>-</span><span>0.8</span><span>, </span><span>6.2</span><span>)</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylim</span><span>(</span><span>-</span><span>0.8</span><span>, </span><span>5.2</span><span>)</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_title</span><span>(</span><span>"Triangle: |det [x y 1]| / 2"</span><span>)</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>ax2.</span><span>grid</span><span>(</span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>63</div></div><div><span>        </span><span>"l20-area.svg"</span><span>,</span></div></div><div><div><div>64</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我觉得这一讲有个有趣的转折：他对 Cramer 法则不以为然，对面积公式却说“very impressed”。同一个行列式，一个是你知道有、但不会用的公式，另一个是以后你无数次会用到的几何。后者在下一讲会以特征值的形式继续出现：特征值的乘积是行列式，而行列式是体积缩放的倍数。</p><p>下一讲他说开始学特征值，“我们真正进入大课题了”。行列式这三讲到这里为止，接下来 <span><span>det⁡(A−λI)=0\det(A - \lambda I) = 0</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span>λ</span><span>I</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 会成为最常写的一个式子：<a href="/posts/linear-algebra-21-eigenvalues-eigenvectors/">18.06 第 21 讲：特征值与特征向量</a>。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-19-determinant-formulas-cofactors/</id>
      <title type="text">18.06 第 19 讲：行列式公式与代数余子式</title>
      <published>2026-10-18T06:00:00.000Z</published>
      <updated>2026-10-18T06:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-19-determinant-formulas-cofactors/"/>
      <summary type="text">MIT 18.06 第 19 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 以 itertools.permutations 实现行列式的大公式、余子式展开和三对角矩阵的递推，6 × 6 随机矩阵的 720 项之和与 np.linalg.det 都是 -7.439860，61 阶三对角矩阵的行列式则是 1。</summary>
      <content type="html"><![CDATA[<p>上一讲 Strang 用三条性质推出了十条性质，这一讲他把同样的三条性质用到底，得到一个能直接从 <span><span>aija_{ij}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 算出行列式的公式：4 × 4 矩阵有 24 项，6 × 6 有 720 项，每一项都是“每行每列各取一个数”的乘积，再带上正负号。我拿他在黑板上现编的 4 × 4 例子去数，24 项里只有两项不是零，一项 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>、一项 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，加起来是 0。之后是第二个公式（余子式展开），以及一个三对角矩阵的行列式：它每六阶重复一次，所以 <span><span>det⁡A61=1\det A_{61} = 1</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>61</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=23LLB9mNJvc" target="_blank">Lecture 19: Determinant formulas and cofactors</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-19-determinant-formulas-and-cofactors/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §5.1 3 by 3 Determinants and Cofactors；§5.2 Computing and Using Determinants
前置：<a href="/posts/linear-algebra-18-determinant-properties/">第 18 讲：行列式的性质</a>
一句话：行列式等于 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 项“每行每列各取一个”的带符号乘积（大公式），也等于沿一行的 <span><span>a1ja_{1j}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>1</span><span>j</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 乘以各自的代数余子式之和，后者把 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶行列式拆成 <span><span>n−1n - 1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 阶的。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先用三条性质把 2 × 2 的 <span><span>ad−bcad - bc</span><span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span></span></span></span> 重新推了一遍，只是为了展示方法。这一段值得认真听，后面全是这个套路的重复。</li>
<li>3 × 3 的“幸存者”那段是整讲的核心：把每一行拆开，二十七块里只有每行每列各一个的才活下来，他还逐个判断了正负号。我把它写成了 <code>itertools.permutations</code>，本篇第一个代码块里算过。</li>
<li>现编的 4 × 4 例子（二十四项里二十二项为零）他自己说“以前没算过”，可以认真看他怎么把整个过程走下来。矩阵他写在黑板上，字幕里没有，我按他描述的路径还原了一个。</li>
<li>代数余子式的定义和符号规则（棋盘格）也要看；最后三对角矩阵的例子很短，周期是 6 那里是整讲最有意思的地方，我在最后一个代码块里算过。</li>
</ul></div></div>
<section><h2>把三条性质用到底：大公式<a href="#把三条性质用到底大公式"><span>#</span></a></h2><p><strong>行列式到底由什么构成？</strong> 回忆上一讲的三条性质：<span><span>det⁡I=1\det I = 1</span><span><span><span></span><span>det</span><span></span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>；交换两行，行列式变号；对每一行单独看，行列式是线性的。Strang 先在 2 × 2 上演示怎么用它们算出一个公式。</p><p>做法是把第一行 <span><span>(a,b)(a, b)</span><span><span><span></span><span>(</span><span>a</span><span>,</span><span></span><span>b</span><span>)</span></span></span></span> 拆成 <span><span>(a,0)+(0,b)(a, 0) + (0, b)</span><span><span><span></span><span>(</span><span>a</span><span>,</span><span></span><span>0</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>b</span><span>)</span></span></span></span>，再把第二行 <span><span>(c,d)(c, d)</span><span><span><span></span><span>(</span><span>c</span><span>,</span><span></span><span>d</span><span>)</span></span></span></span> 拆成 <span><span>(c,0)+(0,d)(c, 0) + (0, d)</span><span><span><span></span><span>(</span><span>c</span><span>,</span><span></span><span>0</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>d</span><span>)</span></span></span></span>，用性质三（对每一行线性）得到四个行列式：</p><span><span><span>∣abcd∣=∣a0c0∣+∣a00d∣+∣0bc0∣+∣0b0d∣\begin{aligned}
\begin{vmatrix} a &amp; b \\ c &amp; d \end{vmatrix}
&amp;= \begin{vmatrix} a &amp; 0 \\ c &amp; 0 \end{vmatrix} +
\begin{vmatrix} a &amp; 0 \\ 0 &amp; d \end{vmatrix} \\
&amp;\quad +
\begin{vmatrix} 0 &amp; b \\ c &amp; 0 \end{vmatrix} +
\begin{vmatrix} 0 &amp; b \\ 0 &amp; d \end{vmatrix}
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span><span></span><span></span><span></span><span>+</span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>第一个和第四个有一整列是 0，是奇异矩阵，行列式为 0，他说“它死了”。第二个是对角矩阵，行列式 <span><span>adad</span><span><span><span></span><span>a</span><span>d</span></span></span></span>。第三个要换一次行才变成对角矩阵，行列式是 <span><span>−bc-bc</span><span><span><span></span><span>−</span><span>b</span><span>c</span></span></span></span>。四项里两项活下来，<span><span>ad−bcad - bc</span><span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span></span></span></span> 就这么出来了。</p><p>重点是方法，不是这个答案。3 × 3 时，第一行拆成三块，第二行再拆成三块，第三行再来一次，一共 <span><span>33=273^3 = 27</span><span><span><span></span><span><span>3</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>27</span></span></span></span> 块。我数了一下，4 × 4 是 <span><span>44=2564^4 = 256</span><span><span><span></span><span><span>4</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>256</span></span></span></span> 块。这些块里，只要某一列没被任何一行选到，那一列全是零，整块的行列式为 0。</p><p><strong>幸存者是谁？</strong> 每一行选一个位置，每一列也恰好被选一次的那些块。他的 3 × 3 里有六个幸存者，对应列号 <span><span>(1,2,3)(1,2,3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>、<span><span>(1,3,2)(1,3,2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(2,1,3)(2,1,3)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>、<span><span>(2,3,1)(2,3,1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>、<span><span>(3,1,2)(3,1,2)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(3,2,1)(3,2,1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 的排列。每一块把选中的数提出来，剩下的是一个只有 0 和 1 的置换矩阵，它的行列式是 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span>，由把它换回单位矩阵需要的行交换次数的奇偶决定。<span><span>(1,3,2)(1,3,2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 要换一次，所以是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>；<span><span>(2,3,1)(2,3,1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 要换两次，所以是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>。合起来：</p><span><span><span>det⁡A=a11a22a33−a11a23a32−a12a21a33+a12a23a31+a13a21a32−a13a22a31\begin{aligned}
\det A &amp;= a_{11}a_{22}a_{33} - a_{11}a_{23}a_{32} -
a_{12}a_{21}a_{33} \\
&amp;\quad + a_{12}a_{23}a_{31} + a_{13}a_{21}a_{32} -
a_{13}a_{22}a_{31}
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>det</span><span></span><span>A</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>33</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>23</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>33</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span></span><span></span><span></span><span>+</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>23</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>正号三项沿着“往下”的方向，负号三项沿着“反方向”，这是他口中去迈阿密和洛杉矶的那两条线。他立刻说这个口诀只对 3 × 3 成立：4 × 4 的反对角线（全是 1 的那条）行列式是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>，因为交换第 1、4 行和第 2、3 行共两次就回到单位矩阵。</p><p>推广到 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶，就是他书里的大公式（big formula）：</p><span><span><span>det⁡A=∑所有排列 (α,β,…,ω)± a1α a2β⋯anω\det A = \sum_{\text{所有排列 }(\alpha,\beta,\ldots,\omega)}
\pm\, a_{1\alpha}\, a_{2\beta} \cdots a_{n\omega}</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>所有排列</span><span> </span></span><span>(</span><span>α</span><span>,</span><span>β</span><span>,</span><span>…</span><span>,</span><span>ω</span><span>)</span></span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>±</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>1</span><span>α</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>2</span><span>β</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋯</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>nω</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>第一行有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 种选法，选完占掉一列，第二行剩 <span><span>n−1n - 1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 种，依此类推，一共 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 项，一半正一半负。正负号看排列的奇偶。我猜这条公式如果做成棋盘上的游戏，就是在 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的棋盘上放 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个互不攻击的车，每种放法对应一项（这个类比我只用这一次）。</p><p>下面把它实现出来。<code>itertools.permutations</code> 给出所有列号排列，符号用逆序对的个数的奇偶判断（相邻两个数交换一次，逆序对数恰好变 1，所以它的奇偶和“交换次数的奇偶”是同一回事）。脚本先打印 3 × 3 的六个排列和符号，再数反对角线的行列式，数 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 的项数，最后在一个 6 × 6 的随机矩阵上对比 <code>np.linalg.det</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> itertools</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>perm_sign</span><span>(</span><span>perm</span><span>:</span><span> tuple[</span><span>int</span><span>, </span><span>...</span><span>]</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"""数逆序对的个数，奇数个就是奇置换。"""</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>inversions </span><span>=</span><span> </span><span>sum</span><span>(</span></div></div><div><div><div>9</div></div><div><span>        </span><span>1</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(perm))</span></div></div><div><div><div>11</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span><span>(i </span><span>+</span><span> </span></span><span>1</span><span>, </span><span>len</span><span>(perm))</span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span><span> perm[i] </span><span>&gt;</span><span> perm[j]</span></span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span><span> </span><span>-</span></span><span>1</span><span> </span><span>if</span><span><span> inversions </span><span>%</span><span> </span></span><span>2</span><span> </span><span>else</span><span> </span><span>1</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>def</span><span> </span><span>big_formula</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>18</div></div><div><span>    </span><span>"""行列式的大公式：每行每列各取一个数，共 n! 项。"""</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>total </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> perm </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(n)):</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>term </span><span>=</span><span> </span><span>perm_sign</span><span>(perm)</span></div></div><div><div><div>23</div></div><div><span>        </span><span>for</span><span> row, col </span><span>in</span><span> </span><span>enumerate</span><span>(perm):</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>term </span><span>*=</span><span> A[row, col]</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>total </span><span>+=</span><span> term</span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span> total</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 3 x 3 的六项：列号的排列、符号</span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> perm </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(</span><span>3</span><span>)):</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>cols </span><span>=</span><span> </span><span>tuple</span><span><span>(c </span><span>+</span><span> </span></span><span>1</span><span> </span><span>for</span><span> c </span><span>in</span><span> perm)</span></div></div><div><div><div>35</div></div><div><span>        </span><span>print</span><span>(cols, </span><span>"符号"</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>perm_sign</span><span>(perm)</span></span><span>:+d</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span># 反对角线：4 x 4 是 +1，3 x 3 是 -1</span></div></div><div><div><div>38</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> (</span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>):</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>J </span><span>=</span><span> np.</span><span>fliplr</span><span>(np.</span><span>eye</span><span>(n))</span></div></div><div><div><div>40</div></div><div><span>        </span><span>print</span><span>(n, </span><span>"阶反对角线的行列式："</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(J)))</span></span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span># 项数：n!</span></div></div><div><div><div>43</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> (</span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>, </span><span>5</span><span>, </span><span>6</span><span>):</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>count </span><span>=</span><span> </span><span>sum</span><span>(</span><span>1</span><span> </span><span>for</span><span> _ </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(n)))</span></div></div><div><div><div>45</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"n = </span><span>{</span><span>n</span><span>}</span><span>：</span><span>{</span><span>count</span><span>}</span><span> 项"</span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>6</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"大公式：</span><span>{</span><span><span>big_formula</span><span>(A)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"np.linalg.det：</span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(A)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"一致："</span><span><span>, np.</span><span>isclose</span><span>(</span><span>big_formula</span><span>(A), np.linalg.</span><span>det</span><span>(A)))</span></span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(1, 2, 3) 符号 +1</span></div></div><div><div><div>2</div></div><div><span>(1, 3, 2) 符号 -1</span></div></div><div><div><div>3</div></div><div><span>(2, 1, 3) 符号 -1</span></div></div><div><div><div>4</div></div><div><span>(2, 3, 1) 符号 +1</span></div></div><div><div><div>5</div></div><div><span>(3, 1, 2) 符号 +1</span></div></div><div><div><div>6</div></div><div><span>(3, 2, 1) 符号 -1</span></div></div><div><div><div>7</div></div><div><span>2 阶反对角线的行列式： -1</span></div></div><div><div><div>8</div></div><div><span>3 阶反对角线的行列式： -1</span></div></div><div><div><div>9</div></div><div><span>4 阶反对角线的行列式： 1</span></div></div><div><div><div>10</div></div><div><span>n = 2：2 项</span></div></div><div><div><div>11</div></div><div><span>n = 3：6 项</span></div></div><div><div><div>12</div></div><div><span>n = 4：24 项</span></div></div><div><div><div>13</div></div><div><span>n = 5：120 项</span></div></div><div><div><div>14</div></div><div><span>n = 6：720 项</span></div></div><div><div><div>15</div></div><div><span>大公式：-7.439860</span></div></div><div><div><div>16</div></div><div><span>np.linalg.det：-7.439860</span></div></div><div><div><div>17</div></div><div><span>一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>六个排列里三正三负，和他黑板上写的一致；反对角线单位矩阵的行列式是 2 阶 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、3 阶 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、4 阶 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>，确认了“口诀只对 3 × 3 成立”。6 × 6 矩阵用 720 项算出的 <span><span>−7.439860-7.439860</span><span><span><span></span><span>−</span><span>7.439860</span></span></span></span> 和 <code>np.linalg.det</code> 完全对上。</p><p>他还指出，大公式可以反推所有性质：<span><span>A=IA = I</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 时，只有 <span><span>a11a22⋯anna_{11}a_{22}\cdots a_{nn}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋯</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>nn</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 这一项（排列是 <span><span>1,2,…,n1,2,\ldots,n</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>n</span></span></span></span>，符号为正）不为零，所以 <span><span>det⁡I=1\det I = 1</span><span><span><span></span><span>det</span><span></span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。同样可以看出 <span><span>det⁡A=det⁡AT\det A = \det A^{\mathsf{T}}</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span>，因为转置只是把“行选列”换成“列选行”。至于 <span><span>det⁡AB=det⁡Adet⁡B\det AB = \det A \det B</span><span><span><span></span><span>det</span><span></span><span>A</span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>det</span><span></span><span>B</span></span></span></span>，他说从这条公式出发证明会弄到“下个星期还在做”，大概就是不会再提了。</p></section>
<section><h2>一个例子：二十四项里只有两项活着<a href="#一个例子二十四项里只有两项活着"><span>#</span></a></h2><p>Strang 现场编了一个 4 × 4 的 0-1 矩阵，说“我不知道它的行列式，数学正在你们眼前第一次被做出来”。字幕里没有写出矩阵，只有他的走法：第一条路取出排列 <span><span>(4,3,2,1)(4,3,2,1)</span><span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，交换两次回到 <span><span>(1,2,3,4)(1,2,3,4)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>；第二条路是 <span><span>(3,2,1,4)(3,2,1,4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，只要交换 1 和 3 一次，是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>；从别的位置出发都走不通。两条路加起来是 0，所以奇异。他接着发现第一行加第三行是全 1 的行，第二行加第四行也是，所以“行一减行二加行三减行四”是零行。</p><p>我按这些线索还原了一个满足所有描述的矩阵（我的版本可能没有他的那么有戏剧性）：</p><span><span><span>A=[0011011011001001]A = \begin{bmatrix}
0 &amp; 0 &amp; 1 &amp; 1 \\
0 &amp; 1 &amp; 1 &amp; 0 \\
1 &amp; 1 &amp; 0 &amp; 0 \\
1 &amp; 0 &amp; 0 &amp; 1
\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>把 24 个排列都过一遍，看有哪些乘积不为零：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> itertools</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>perm_sign</span><span>(</span><span>perm</span><span>:</span><span> tuple[</span><span>int</span><span>, </span><span>...</span><span>]</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>inversions </span><span>=</span><span> </span><span>sum</span><span>(</span></div></div><div><div><div>8</div></div><div><span>        </span><span>1</span></div></div><div><div><div>9</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(perm))</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span><span>(i </span><span>+</span><span> </span></span><span>1</span><span>, </span><span>len</span><span>(perm))</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> perm[i] </span><span>&gt;</span><span> perm[j]</span></span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> </span><span>-</span></span><span>1</span><span> </span><span>if</span><span><span> inversions </span><span>%</span><span> </span></span><span>2</span><span> </span><span>else</span><span> </span><span>1</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span># 字幕里没有写出矩阵，这是我按他的两条路径还原的 0-1 矩阵</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>[</span><span>0</span><span>, </span><span>0</span><span>, </span><span>1</span><span>, </span><span>1</span><span>],</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>[</span><span>0</span><span>, </span><span>1</span><span>, </span><span>1</span><span>, </span><span>0</span><span>],</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>[</span><span>1</span><span>, </span><span>1</span><span>, </span><span>0</span><span>, </span><span>0</span><span>],</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>[</span><span>1</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>1</span><span>],</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>],</span></div></div><div><div><div>26</div></div><div><span>        </span><span>dtype</span><span>=</span><span>float</span><span>,</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>survivors </span><span>=</span><span> []</span></div></div><div><div><div>30</div></div><div><span>    </span><span>for</span><span> perm </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(</span><span>4</span><span>)):</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>term </span><span>=</span><span> np.</span><span>prod</span><span>([A[row, col] </span><span>for</span><span> row, col </span><span>in</span><span> </span><span>enumerate</span><span>(perm)])</span></div></div><div><div><div>32</div></div><div><span>        </span><span>if</span><span><span> term </span><span>!=</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>survivors.</span><span>append</span><span>((</span><span>tuple</span><span><span>(c </span><span>+</span><span> </span></span><span>1</span><span> </span><span>for</span><span> c </span><span>in</span><span><span> perm), </span><span>perm_sign</span><span>(perm)))</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"24 项里不为零的："</span><span>, survivors)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>total </span><span>=</span><span> </span><span>sum</span><span>(sign </span><span>for</span><span> _, sign </span><span>in</span><span> survivors)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"它们的和："</span><span>, total)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.det："</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(A), </span></span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 奇异的另一个证据：行的组合给出零行，列的组合给出零向量</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"行一 - 行二 + 行三 - 行四："</span><span>, A[</span><span>0</span><span><span>] </span><span>-</span><span> A[</span></span><span>1</span><span><span>] </span><span>+</span><span> A[</span></span><span>2</span><span><span>] </span><span>-</span><span> A[</span></span><span>3</span><span>])</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"列一 - 列二 + 列三 - 列四："</span><span>, A[:, </span><span>0</span><span><span>] </span><span>-</span><span> A[:, </span></span><span>1</span><span><span>] </span><span>+</span><span> A[:, </span></span><span>2</span><span><span>] </span><span>-</span><span> A[:, </span></span><span>3</span><span>])</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>24 项里不为零的： [((3, 2, 1, 4), -1), ((4, 3, 2, 1), 1)]</span></div></div><div><div><div>2</div></div><div><span>它们的和： 0</span></div></div><div><div><div>3</div></div><div><span>np.linalg.det： 0.0</span></div></div><div><div><div>4</div></div><div><span>行一 - 行二 + 行三 - 行四： [0. 0. 0. 0.]</span></div></div><div><div><div>5</div></div><div><span>列一 - 列二 + 列三 - 列四： [0. 0. 0. 0.]</span></div></div><div><div><div>6</div></div><div><span>秩： 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>24 项里恰好两项不为零，列号 <span><span>(3,2,1,4)(3,2,1,4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 带 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，<span><span>(4,3,2,1)(4,3,2,1)</span><span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 带 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>，和他的说法一致。和为 0，<code>np.linalg.det</code> 也给出 0.0，秩是 3。行组合和列组合都能凑出零，所以零空间和左零空间各有一个非零向量。这就是“零多的矩阵可以直接用大公式”的好处：一眼看出来的结构，比消元更快。</p></section>
<section><h2>代数余子式：把 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶拆成 <span><span>n−1n - 1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 阶<a href="#代数余子式把-nnn-阶拆成-n1n---1n1-阶"><span>#</span></a></h2><p><strong>怎样把大公式拆开？</strong> 还是从 3 × 3 开始。把含有 <span><span>a11a_{11}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的两项合并：</p><span><span><span>a11(a22a33−a23a32)a_{11}\left(a_{22}a_{33} - a_{23}a_{32}\right)</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>33</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>23</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span></span><p>括号里正好是划掉第 1 行第 1 列之后剩下的 2 × 2 矩阵的行列式。含 <span><span>a12a_{12}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的两项是 <span><span>−a12(a21a33−a23a31)-a_{12}(a_{21}a_{33} - a_{23}a_{31})</span><span><span><span></span><span>−</span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>33</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>23</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，括号里同样是划掉第 1 行第 2 列后的 2 × 2 行列式，但前面多了一个负号。含 <span><span>a13a_{13}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的是 <span><span>+a13(a21a32−a22a31)+a_{13}(a_{21}a_{32} - a_{22}a_{31})</span><span><span><span></span><span>+</span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。</p><p>Strang 把“划掉后剩下的子矩阵行列式”连同这个符号一起叫作 <span><span>aija_{ij}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的代数余子式（cofactor）：</p><span><span><span>Cij=(−1)i+jdet⁡MijC_{ij} = (-1)^{i + j} \det M_{ij}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>i</span><span>+</span><span>j</span></span></span></span></span></span></span></span></span><span></span><span>det</span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>MijM_{ij}</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是划掉第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列后的 <span><span>(n−1)×(n−1)(n-1) \times (n-1)</span><span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span> 矩阵。符号由 <span><span>i+ji + j</span><span><span><span></span><span>i</span><span></span><span>+</span><span></span></span><span><span></span><span>j</span></span></span></span> 的奇偶决定，是一张棋盘格，左上角是加号。不带符号的 <span><span>det⁡Mij\det M_{ij}</span><span><span><span></span><span>det</span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 叫余子式（minor），他说那个词他不在乎，直接把符号收进去叫代数余子式。沿第一行展开：</p><span><span><span>det⁡A=a11C11+a12C12+⋯+a1nC1n\det A = a_{11}C_{11} + a_{12}C_{12} + \cdots + a_{1n}C_{1n}</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>1</span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>1</span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>沿第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行同理，只是把下标 1 换成 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>。2 × 2 的例子最小：<span><span>aa</span><span><span><span></span><span>a</span></span></span></span> 的余子式是 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span>，<span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 的余子式是 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span>，加上符号后是 <span><span>−c-c</span><span><span><span></span><span>−</span><span>c</span></span></span></span>，所以 <span><span>det⁡=a⋅d+b⋅(−c)=ad−bc\det = a \cdot d + b \cdot (-c) = ad - bc</span><span><span><span></span><span>det</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span>⋅</span><span></span></span><span><span></span><span>d</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>⋅</span><span></span></span><span><span></span><span>(</span><span>−</span><span>c</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span></span></span></span>。</p><p>他还提到，既然余子式能把 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶拆成 <span><span>n−1n - 1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span> 阶，再拆成 <span><span>n−2n - 2</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span></span></span></span> 阶，一路拆到 1 × 1，那么整门行列式的课完全可以从这个公式开始讲，有人就是这么讲的。他选择先讲性质，原因我猜是性质更容易看出行列式“为什么”这样。</p><p>他接着把三种算法的位置交代清楚：主元的乘积把所有繁琐的东西已经折进了消元里，效率最高；大公式把所有繁琐的东西摊开写出来；余子式公式介于两者之间，“容易的东西乘以可怕的东西”，它的用处是把大行列式拆成小行列式。</p><p>下面是余子式展开的递归实现。<code>det_cofactor</code> 沿指定的一行展开，每次调用时全局的计数器加一，用来数递归调用次数；我还对 6 × 6 矩阵每一行都展开一次，确认结果相同，并和 <code>np.linalg.det</code> 对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>calls </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>minor</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>i</span></span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>j</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"""划掉第 i 行第 j 列。"""</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>delete</span><span>(np.</span><span>delete</span><span>(A, i, </span></span><span>axis</span><span>=</span><span>0</span><span>), j, </span><span>axis</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>cofactor</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>i</span></span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>j</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"""代数余子式 C_ij = (-1)^(i+j) det(M_ij)。"""</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> (</span><span>-</span></span><span>1</span><span><span>) </span><span>**</span><span> (i </span><span>+</span><span> j) </span><span>*</span><span> </span><span>det_cofactor</span><span>(</span><span>minor</span><span>(A, i, j))</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>det_cofactor</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>row</span></span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span><span> </span><span>0</span></span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>17</div></div><div><span>    </span><span>"""沿第 row 行做余子式展开。"""</span></div></div><div><div><div>18</div></div><div><span>    </span><span>global</span><span> calls</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>calls </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>21</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>1</span><span>:</span></div></div><div><div><div>22</div></div><div><span>        </span><span>return</span><span> A[</span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span> </span><span>sum</span><span><span>(A[row, j] </span><span>*</span><span> </span><span>cofactor</span><span>(A, row, j) </span></span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n))</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>27</div></div><div><span>    </span><span>global</span><span> calls</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 符号是棋盘格</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>signs </span><span>=</span><span> np.</span><span>array</span><span>([[(</span><span>-</span><span>1</span><span><span>) </span><span>**</span><span> (i </span><span>+</span><span> j) </span></span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>5</span><span>)] </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>5</span><span>)])</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(signs)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span># 2 x 2：a * d + b * (-c)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>3.0</span><span>, </span><span>5.0</span><span>], [</span><span>2.0</span><span>, </span><span>7.0</span><span>]])</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"2 x 2："</span><span><span>, </span><span>det_cofactor</span><span>(B), </span></span><span>"，np.linalg.det："</span><span><span>, np.linalg.</span><span>det</span><span>(B))</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span># 沿任意一行展开，结果相同</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>6</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>41</div></div><div><span>    </span><span>for</span><span> row </span><span>in</span><span> </span><span>range</span><span>(</span><span>6</span><span>):</span></div></div><div><div><div>42</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"沿第 </span><span>{</span><span><span>row </span><span>+</span><span> </span></span><span>1}</span><span> 行：</span><span>{</span><span><span>det_cofactor</span><span>(A, row)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"np.linalg.det：</span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(A)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span># 递归调用次数，对比 n!</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"n    调用次数"</span><span>)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> (</span><span>3</span><span>, </span><span>4</span><span>, </span><span>5</span><span>, </span><span>6</span><span>, </span><span>7</span><span>, </span><span>8</span><span>):</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>calls </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>det_cofactor</span><span>(rng.</span><span>standard_normal</span><span>((n, n)))</span></div></div><div><div><div>50</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>n</span><span>}</span><span>    </span><span>{</span><span>calls</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[[ 1 -1  1 -1  1]</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[-1  1 -1  1 -1]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 1 -1  1 -1  1]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[-1  1 -1  1 -1]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 1 -1  1 -1  1]]</span></div></div><div><div><div>6</div></div><div><span>2 x 2： 11.0 ，np.linalg.det： 11.000000000000002</span></div></div><div><div><div>7</div></div><div><span>沿第 1 行：-7.439860</span></div></div><div><div><div>8</div></div><div><span>沿第 2 行：-7.439860</span></div></div><div><div><div>9</div></div><div><span>沿第 3 行：-7.439860</span></div></div><div><div><div>10</div></div><div><span>沿第 4 行：-7.439860</span></div></div><div><div><div>11</div></div><div><span>沿第 5 行：-7.439860</span></div></div><div><div><div>12</div></div><div><span>沿第 6 行：-7.439860</span></div></div><div><div><div>13</div></div><div><span>np.linalg.det：-7.439860</span></div></div><div><div><div>14</div></div><div><span>n    调用次数</span></div></div><div><div><div>15</div></div><div><span>3    10</span></div></div><div><div><div>16</div></div><div><span>4    41</span></div></div><div><div><div>17</div></div><div><span>5    206</span></div></div><div><div><div>18</div></div><div><span>6    1237</span></div></div><div><div><div>19</div></div><div><span>7    8660</span></div></div><div><div><div>20</div></div><div><span>8    69281</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>棋盘格的符号和他说的一样（第二行从减号开始）。沿六行中任意一行展开都得到 <span><span>−7.439860-7.439860</span><span><span><span></span><span>−</span><span>7.439860</span></span></span></span>，与 <code>np.linalg.det</code> 一致，也与上一节用大公式算出的同一个矩阵的值一致。</p><p>代价是另一回事：3 阶调用 10 次，8 阶调用 69281 次。这个调用次数已经比 <span><span>8!=403208! = 40320</span><span><span><span></span><span>8</span><span>!</span><span></span><span>=</span><span></span></span><span><span></span><span>40320</span></span></span></span> 大了 1.7 倍，因为余子式展开除了叶子还有中间层。这是稠密矩阵的情形；到 20 阶，<span><span>20!20!</span><span><span><span></span><span>20</span><span>!</span></span></span></span> 约 <span><span>2.4×10182.4 \times 10^{18}</span><span><span><span></span><span>2.4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>18</span></span></span></span></span></span></span></span></span></span></span></span>，而消元只需要大约 <span><span>203/3≈266720^3/3 \approx 2667</span><span><span><span></span><span>2</span><span><span>0</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span><span></span><span>≈</span><span></span></span><span><span></span><span>2667</span></span></span></span> 次乘法（见<a href="/posts/linear-algebra-04-lu-factorization/">第 4 讲</a>）。所以在实际计算里算行列式用消元，大公式和余子式的价值在于它们给出了结构：零很多的时候可以一眼读出结论，符号规则也是后面逆矩阵公式的基础。</p></section>
<section><h2>三对角矩阵：行列式每六阶重复一次<a href="#三对角矩阵行列式每六阶重复一次"><span>#</span></a></h2><p><strong>零很多的时候，余子式能做出什么？</strong> Strang 的最后一个例子是对角线和两条副对角线全是 1 的 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶矩阵 <span><span>AnA_n</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，4 阶是</p><span><span><span>A4=[1100111001110011]A_4 = \begin{bmatrix}
1 &amp; 1 &amp; 0 &amp; 0 \\
1 &amp; 1 &amp; 1 &amp; 0 \\
0 &amp; 1 &amp; 1 &amp; 1 \\
0 &amp; 0 &amp; 1 &amp; 1
\end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>小的先算：<span><span>det⁡A1=1\det A_1 = 1</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，<span><span>det⁡A2=1⋅1−1⋅1=0\det A_2 = 1 \cdot 1 - 1 \cdot 1 = 0</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。<span><span>A3A_3</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的大公式有六项，其中三项非零，<span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，和是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>；也可以先用第二行减第三行，再沿新的行展开，得到 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。</p><p>到 <span><span>A4A_4</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，沿第一行展开，只有两个非零元素。<span><span>a11=1a_{11} = 1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的余子式是 <span><span>A3A_3</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的行列式，符号为正。<span><span>a12=1a_{12} = 1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 在 <span><span>(1,2)(1,2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 位置，符号为负，划掉第一行第二列后的 3 × 3 矩阵的第一列是 <span><span>(1,0,0)T(1, 0, 0)^{\mathsf{T}}</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span>，对这一列再展开一次（转置规则允许按列展开），剩下的是 <span><span>A2A_2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，符号是正。所以 <span><span>det⁡A4=det⁡A3−det⁡A2\det A_4 = \det A_3 - \det A_2</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。这个论证对任何 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 都成立：</p><span><span><span>det⁡An=det⁡An−1−det⁡An−2\det A_n = \det A_{n-1} - \det A_{n-2}</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>他说这些数你大概猜不到，然后一路往下算：1、0、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、0、1、1、……，到这里又开始重复，周期是 6（<span><span>A5A_5</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 0，<span><span>A6A_6</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>A7A_7</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都是 1）。所以 <span><span>det⁡A61=det⁡A1=1\det A_{61} = \det A_1 = 1</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>61</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（<span><span>61=6×10+161 = 6 \times 10 + 1</span><span><span><span></span><span>61</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>10</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span>）。</p><p>我把递推写成循环，从 1 阶到 12 阶与 <code>np.linalg.det</code> 逐个对照，再算 61 阶：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>tridiagonal_ones</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""对角线与两条副对角线全是 1 的 n 阶矩阵。"""</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>eye</span><span>(n) </span><span>+</span><span> np.</span><span>eye</span><span>(n, </span></span><span>k</span><span>=</span><span>1</span><span><span>) </span><span>+</span><span> np.</span><span>eye</span><span>(n, </span></span><span>k</span><span>=-</span><span>1</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>det_recurrence</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"""D_n = D_{n-1} - D_{n-2}，D_0 = 1，D_1 = 1。"""</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>prev, curr </span><span>=</span><span> </span><span>1</span><span>, </span><span>1</span></div></div><div><div><div>12</div></div><div><span>    </span><span>if</span><span><span> n </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>13</div></div><div><span>        </span><span>return</span><span> prev</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>prev, curr </span><span>=</span><span> curr, curr </span><span>-</span><span> prev</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> curr</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span><span>(</span><span>tridiagonal_ones</span><span>(</span></span><span>4</span><span>))</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"n   递推   np.linalg.det"</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>13</span><span>):</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>D </span><span>=</span><span> </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(</span><span>tridiagonal_ones</span><span>(n)))</span></span></div></div><div><div><div>26</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>n</span><span>:&lt;3d</span><span>}</span><span> </span><span>{</span><span><span>det_recurrence</span><span>(n)</span></span><span>:&gt;3d</span><span>}</span><span>    </span><span>{</span><span>D</span><span>:&gt;3d</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>61</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>exact </span><span>=</span><span> </span><span>det_recurrence</span><span>(n)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>numeric </span><span>=</span><span> np.linalg.</span><span>det</span><span>(</span><span>tridiagonal_ones</span><span>(n))</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"n = </span><span>{</span><span>n</span><span>}</span><span>：递推 = </span><span>{</span><span>exact</span><span>}</span><span>，np.linalg.det = </span><span>{</span><span>numeric</span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[[1. 1. 0. 0.]</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[1. 1. 1. 0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 1. 1. 1.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[0. 0. 1. 1.]]</span></div></div><div><div><div>5</div></div><div><span>n   递推   np.linalg.det</span></div></div><div><div><div>6</div></div><div><span>1     1      1</span></div></div><div><div><div>7</div></div><div><span>2     0      0</span></div></div><div><div><div>8</div></div><div><span>3    -1     -1</span></div></div><div><div><div>9</div></div><div><span>4    -1     -1</span></div></div><div><div><div>10</div></div><div><span>5     0      0</span></div></div><div><div><div>11</div></div><div><span>6     1      1</span></div></div><div><div><div>12</div></div><div><span>7     1      1</span></div></div><div><div><div>13</div></div><div><span>8     0      0</span></div></div><div><div><div>14</div></div><div><span>9    -1     -1</span></div></div><div><div><div>15</div></div><div><span>10   -1     -1</span></div></div><div><div><div>16</div></div><div><span>11    0      0</span></div></div><div><div><div>17</div></div><div><span>12    1      1</span></div></div><div><div><div>18</div></div><div><span>n = 61：递推 = 1，np.linalg.det = 1.000000</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>12 阶以内每一阶的递推值和数值行列式都一致：1、0、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、0、1，然后 1、0、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、0、1。61 阶的 <span><span>61×6161 \times 61</span><span><span><span></span><span>61</span><span></span><span>×</span><span></span></span><span><span></span><span>61</span></span></span></span> 矩阵让 <code>np.linalg.det</code> 也给出 1.000000，和递推相同。注意 <span><span>A2A_2</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的行列式是 0，说明直接消元的话第二个主元会出问题，需要换行；NumPy 内部是带行交换的 LU，所以不受影响。</p><p>这个例子我觉得挺漂亮的：一个看起来只能一阶一阶硬算的行列式，因为余子式的递推，变成了一个周期为 6 的数列。余子式在这里比消元好用，是因为矩阵的零恰好让每次展开只剩两项。</p><p>下一讲是行列式的最后一讲，他会用余子式给出逆矩阵的公式，再把 Cramer 法则和体积放进来：<a href="/posts/linear-algebra-20-cramer-inverse-volume/">Cramer 法则、逆矩阵与体积</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-18-determinant-properties/</id>
      <title type="text">18.06 第 18 讲：行列式的性质</title>
      <published>2026-10-18T05:40:00.000Z</published>
      <updated>2026-10-18T05:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-18-determinant-properties/"/>
      <summary type="text">MIT 18.06 第 18 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 逐条验证了 Strang 的十条行列式性质，又手写消元求主元乘积，算 100 × 100 随机矩阵的行列式，和 np.linalg.det 的相对差只有 9.99e-13。</summary>
      <content type="html"><![CDATA[<p>Strang 这一讲没有先给行列式的公式，而是只给三条性质：单位矩阵的行列式是 1，换行变号，对每一行单独线性。其余七条都从这三条推出来，包括最实用的一条：三角矩阵的行列式是对角线乘积，所以行列式等于主元乘积。我手写了一个只做消元的行列式函数，对一个 100 × 100 的随机矩阵，它算出 3.449180e+77，和 <code>np.linalg.det</code> 的相对差是 9.99e-13。三条性质，连同 <span><span>det⁡A≠0\det A \ne 0</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> 等价于 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 可逆，就把行列式立住了。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=srxexLishgY" target="_blank">Lecture 18: Properties of determinants</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-18-properties-of-determinants/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §5.1 3 by 3 Determinants and Cofactors；§5.2 Computing and Using Determinants
前置：<a href="/posts/linear-algebra-17-orthogonal-matrices-gram-schmidt/">第 17 讲：正交矩阵与 Gram-Schmidt</a>
一句话：行列式由“<span><span>det⁡I=1\det I = 1</span><span><span><span></span><span>det</span><span></span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、换行变号、对每一行线性”这三条性质唯一确定，由此得到 <span><span>det⁡A\det A</span><span><span><span></span><span>det</span><span></span><span>A</span></span></span></span> 等于主元乘积（带上换行的符号），<span><span>det⁡A≠0\det A \ne 0</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> 当且仅当 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 可逆。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先说这一讲是后半门课的开始：行列式是为特征值服务的。这几句值得听，后面几讲为什么要学行列式，这里交代得最清楚。</li>
<li>他推导性质 4（两行相等则行列式为 0）、性质 5（消元不改变行列式）、性质 7（三角矩阵是对角线乘积）的那几段要认真看。每一条都只用前面的性质，是“从三条公理往外长”的范例。本篇的代码只做验证，不替代推导。</li>
<li>性质 9 以后（乘积、逆、转置）讲得快，本篇有对应的数值验证，可以倍速。最后他用 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 证明 <span><span>det⁡AT=det⁡A\det A^\mathsf{T} = \det A</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>A</span></span></span></span>，没有讲完，留了一个“排列的奇偶性”的尾巴，下一讲接上。</li>
<li>字幕是自动转写的，有几处断句错位（比如性质 5 和性质 9 的位置夹进了别的句子），读不通时以我这里列出的性质为准。</li>
</ul></div></div>
<section><h2>先给三条性质，不给公式<a href="#先给三条性质不给公式"><span>#</span></a></h2><p>他开场说得很直白：行列式是每个方阵对应的一个数，写作 <span><span>det⁡A\det A</span><span><span><span></span><span>det</span><span></span><span>A</span></span></span></span> 或 <span><span>∣A∣|A|</span><span><span><span></span><span>∣</span><span>A</span><span>∣</span></span></span></span>。一个数装不下整个矩阵，但它想把尽可能多的信息塞进这一个数里，其中最重要的一条是：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 可逆当且仅当 <span><span>det⁡A≠0\det A \ne 0</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span>。</p><p>然后他问：先给一个大公式，一口气塞给你？他说不。公式里装了太多东西，他宁愿先给三条性质。</p><ul>
<li>性质 1：<span><span>det⁡I=1\det I = 1</span><span><span><span></span><span>det</span><span></span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。</li>
<li>性质 2：交换两行，行列式变号。</li>
<li>性质 3：行列式对每一行单独线性。3A：某一行乘 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span>，行列式乘 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span>。3B：某一行是两个向量的和，行列式等于两个行列式的和，其余各行不动。</li>
</ul><p>这三条一给，置换矩阵的行列式就定了：单位矩阵是 1，每换一次行变一次号，所以结果是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span> 或 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，取决于换行次数是偶数还是奇数。他顺手写了 2 × 2 的情形：单位矩阵是 1，换了行的是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。</p><p><strong>“线性”到底是什么意思？</strong> 他特意强调了两遍，我也在这里停一下，因为这是最容易误会的一条。线性是对一行而言的：保持其余各行不动，只拆第一行。它不是说 <span><span>det⁡(A+B)=det⁡A+det⁡B\det(A + B) = \det A + \det B</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>det</span><span></span><span>B</span></span></span></span>，那个不成立。他甚至说“怎么才能让录像里留下我没说那句话”。</p><p>下面的脚本把性质 1 到 3 逐条验证一遍，矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是我随手选的一个 3 阶整数矩阵（挑它只因为行列式恰好是 10，好认）。脚本里还枚举了 3 阶的六个置换矩阵，对照“逆序数”的奇偶性：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> itertools</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>det </span><span>=</span><span> np.linalg.det</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span># 性质 1：det(I) = 1</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"性质 1：det(I) ="</span><span><span>, </span><span>det</span><span>(np.</span><span>eye</span><span>(</span></span><span>4</span><span>)))</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 性质 2：交换两行，符号反转；置换矩阵的行列式是 +1 或 -1</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>3.0</span><span>, </span><span>8.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>swapped </span><span>=</span><span> A[[</span><span>1</span><span>, </span><span>0</span><span>, </span><span>2</span><span>]]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>17</div></div><div><span>        </span><span>"性质 2：det(A) ="</span><span>,</span></div></div><div><div><div>18</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(A), </span></span><span>4</span><span>),</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"，换行后"</span><span>,</span></div></div><div><div><div>20</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(swapped), </span></span><span>4</span><span>),</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> perm </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(</span><span>3</span><span>)):</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>P </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)[</span><span>list</span><span>(perm)]</span></div></div><div><div><div>24</div></div><div><span>        </span><span># 数交换次数：逆序对的个数的奇偶性等于交换次数的奇偶性</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>inversions </span><span>=</span><span> </span><span>sum</span><span>(</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>perm[i] </span><span>&gt;</span><span> perm[j] </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span><span>(i </span><span>+</span><span> </span></span><span>1</span><span>, </span><span>3</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>28</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"  </span><span>{</span><span>perm</span><span>}</span><span>  逆序数 </span><span>{</span><span>inversions</span><span>}</span><span>  det(P) = </span><span>{</span><span><span>det</span><span>(P)</span></span><span>:+.0f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 性质 3A：一行乘 t，行列式乘 t</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> </span><span>5.0</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>scaled </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>scaled[</span><span>0</span><span><span>] </span><span>*=</span><span> t</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"性质 3A："</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(scaled), </span></span><span>4</span><span>), </span><span>"="</span><span>, t, </span><span>"*"</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(A), </span></span><span>4</span><span>))</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 性质 3B：只对第一行线性，其余行不动</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>3.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>rest </span><span>=</span><span> A[</span><span>1</span><span>:]</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>first_u </span><span>=</span><span> np.</span><span>vstack</span><span>([u, rest])</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>first_v </span><span>=</span><span> np.</span><span>vstack</span><span>([v, rest])</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>first_sum </span><span>=</span><span> np.</span><span>vstack</span><span>([u </span><span>+</span><span> v, rest])</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>44</div></div><div><span>        </span><span>"性质 3B："</span><span>,</span></div></div><div><div><div>45</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(first_sum), </span></span><span>4</span><span>),</span></div></div><div><div><div>46</div></div><div><span>        </span><span>"="</span><span>,</span></div></div><div><div><div>47</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(first_u), </span></span><span>4</span><span>),</span></div></div><div><div><div>48</div></div><div><span>        </span><span>"+"</span><span>,</span></div></div><div><div><div>49</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(first_v), </span></span><span>4</span><span>),</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>    </span><span># 但 det(A + B) 不等于 det(A) + det(B)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(A + B) ="</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(A </span><span>+</span><span> B), </span></span><span>4</span><span>))</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(A) + det(B) ="</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(A) </span><span>+</span><span> </span><span>det</span><span>(B), </span></span><span>4</span><span>))</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>性质 1：det(I) = 1.0</span></div></div><div><div><div>2</div></div><div><span>性质 2：det(A) = 10.0 ，换行后 -10.0</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>(0, 1, 2)  逆序数 0  det(P) = +1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>(0, 2, 1)  逆序数 1  det(P) = -1</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>(1, 0, 2)  逆序数 1  det(P) = -1</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>(1, 2, 0)  逆序数 2  det(P) = +1</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>(2, 0, 1)  逆序数 2  det(P) = +1</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>(2, 1, 0)  逆序数 3  det(P) = -1</span></div></div><div><div><div>9</div></div><div><span>性质 3A： 50.0 = 5.0 * 10.0</span></div></div><div><div><div>10</div></div><div><span>性质 3B： 7.0 = 28.0 + -21.0</span></div></div><div><div><div>11</div></div><div><span>det(A + B) = 73.0</span></div></div><div><div><div>12</div></div><div><span>det(A) + det(B) = 15.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>性质 2 的两行 <span><span>det⁡A=10\det A = 10</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>、换行后 <span><span>−10-10</span><span><span><span></span><span>−</span><span>10</span></span></span></span>。六个置换矩阵的行列式里三个 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>、三个 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，符号正好是逆序数的奇偶性（逆序数就是把 <span><span>0,1,20, 1, 2</span><span><span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span></span></span></span> 排成这个顺序需要的相邻交换次数的奇偶）。性质 3A：第一行乘 5，行列式从 10 变成 50。性质 3B：我把第一行拆成 <span><span>u+vu + v</span><span><span><span></span><span>u</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span>，其余两行不动，<span><span>7=28+(−21)7 = 28 + (-21)</span><span><span><span></span><span>7</span><span></span><span>=</span><span></span></span><span><span></span><span>28</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>21</span><span>)</span></span></span></span>。</p><p>最后两行是反例：<span><span>det⁡(A+B)=73\det(A + B) = 73</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>73</span></span></span></span>，而 <span><span>det⁡A+det⁡B=10+5=15\det A + \det B = 10 + 5 = 15</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>det</span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span></span><span>+</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span>。差了将近五倍，“线性”确实只是对单独一行成立。</p></section>
<section><h2>其余的性质都是这三条推出来的<a href="#其余的性质都是这三条推出来的"><span>#</span></a></h2><p>他接下来的推导是这一讲最漂亮的部分：只用性质 1 到 3，往外长出性质 4 到 7。</p><p><strong>性质 4：两行相等，行列式为 0。</strong> 交换这两行，矩阵没变，所以行列式没变；可是性质 2 说行列式要变号。一个数等于它的相反数，只能是 0。他说“性质 2 就是为这个准备的”。这和“两行相等则秩小于 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>，不可逆”吻合。</p><p><strong>性质 5：从一行里减去另一行的倍数，行列式不变。</strong> 这是消元里一直在做的那一步，所以这一条最要紧：它说整个消元过程不改变行列式。证明用 2 × 2 就够：第二行变成 <span><span>(c−la, d−lb)(c - la,\ d - lb)</span><span><span><span></span><span>(</span><span>c</span><span></span><span>−</span><span></span></span><span><span></span><span>l</span><span>a</span><span>,</span><span> </span><span></span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>l</span><span>b</span><span>)</span></span></span></span>，按 3B 拆成 <span><span>(c,d)(c, d)</span><span><span><span></span><span>(</span><span>c</span><span>,</span><span></span><span>d</span><span>)</span></span></span></span> 和 <span><span>(−la,−lb)(-la, -lb)</span><span><span><span></span><span>(</span><span>−</span><span>l</span><span>a</span><span>,</span><span></span><span>−</span><span>l</span><span>b</span><span>)</span></span></span></span> 两个行列式，后一个用 3A 提出 <span><span>−l-l</span><span><span><span></span><span>−</span><span>l</span></span></span></span>，剩下两行相等，按性质 4 是 0。所以消元前后的行列式一样，后面他反复用这一条说明 <span><span>det⁡A=det⁡U\det A = \det U</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>U</span></span></span></span>（没有换行的话）。</p><p><strong>性质 6：有一行全是 0，行列式为 0。</strong> 他一开始想用 3A 乘 5，后来换成同学的办法：在 3A 里取 <span><span>t=0t = 0</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。这对应“消元消出一行 0，说明矩阵奇异”。</p><p><strong>性质 7：三角矩阵的行列式是对角线元素的乘积。</strong> 设对角线是 <span><span>d1,d2,…,dnd_1, d_2, \dots, d_n</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，对角线上方是任意数。若所有 <span><span>di≠0d_i \ne 0</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span>，用性质 5 往上消去所有非对角元，只剩对角矩阵；用 3A 把 <span><span>d1,…,dnd_1, \dots, d_n</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 逐行提出来，剩下单位矩阵，轮到性质 1，等于 1。所以结果是 <span><span>d1d2⋯dnd_1 d_2 \cdots d_n</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋯</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。若有某个 <span><span>di=0d_i = 0</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，消元会得到一行零，性质 6 给出 0，公式依然成立。</p><p>下面验证性质 4 到 7，再看他最后检查的 2 × 2 情形：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>det </span><span>=</span><span> np.linalg.det</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>3.0</span><span>, </span><span>8.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># 性质 4：两行相等，行列式为 0</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>equal_rows </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>equal_rows[</span><span>2</span><span><span>] </span><span>=</span><span> equal_rows[</span></span><span>0</span><span>]</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"性质 4：两行相等 det ="</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(equal_rows), </span></span><span>4</span><span>))</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 性质 5：从一行里减去另一行的倍数，行列式不变</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>step </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>step[</span><span>1</span><span><span>] </span><span>-=</span><span> </span></span><span>3.0</span><span><span> </span><span>*</span><span> step[</span></span><span>0</span><span>]</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"性质 5：消元前"</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(A), </span></span><span>4</span><span>), </span><span>"，消元后"</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(step), </span></span><span>4</span><span>))</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 性质 6：有一行全是 0，行列式为 0</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>zero_row </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>zero_row[</span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"性质 6：零行 det ="</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(zero_row), </span></span><span>4</span><span>))</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 性质 7：三角矩阵的行列式是对角线上元素的乘积</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> np.</span><span>triu</span><span>(A)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"性质 7：det(U) ="</span><span>,</span></div></div><div><div><div>28</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(U), </span></span><span>4</span><span>),</span></div></div><div><div><div>29</div></div><div><span>        </span><span>"，对角线乘积 ="</span><span>,</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>np.</span><span>prod</span><span>(np.</span><span>diag</span><span>(U)),</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 2 x 2：主元乘积就是 ad - bc</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>a, b, c, d </span><span>=</span><span> </span><span>3.0</span><span>, </span><span>5.0</span><span>, </span><span>2.0</span><span>, </span><span>4.0</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>second_pivot </span><span>=</span><span> d </span><span>-</span><span> (c </span><span>/</span><span> a) </span><span>*</span><span> b</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"2 x 2：a * (d - c/a * b) ="</span><span>, </span><span>round</span><span><span>(a </span><span>*</span><span> second_pivot, </span></span><span>4</span><span>))</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"2 x 2：ad - bc ="</span><span><span>, a </span><span>*</span><span> d </span><span>-</span><span> b </span><span>*</span><span> c)</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>性质 4：两行相等 det = 0.0</span></div></div><div><div><div>2</div></div><div><span>性质 5：消元前 10.0 ，消元后 10.0</span></div></div><div><div><div>3</div></div><div><span>性质 6：零行 det = 0.0</span></div></div><div><div><div>4</div></div><div><span>性质 7：det(U) = 8.0 ，对角线乘积 = 8.0</span></div></div><div><div><div>5</div></div><div><span>2 x 2：a * (d - c/a * b) = 2.0</span></div></div><div><div><div>6</div></div><div><span>2 x 2：ad - bc = 2.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>性质 4、5、6 的数字都和预期一致：把第三行抄成第一行，行列式是 0；第二行减去 3 倍第一行，行列式仍是 10；零行，行列式是 0。性质 7 里我取的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的上三角部分（<code>np.triu</code>），不是消元的结果，只想看 <span><span>det⁡U=1×8×1=8\det U = 1 \times 8 \times 1 = 8</span><span><span><span></span><span>det</span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>8</span></span></span></span>。</p><p>最后两行是他的 2 × 2 检查：用主元乘积算行列式。第一个主元是 <span><span>aa</span><span><span><span></span><span>a</span></span></span></span>，第二个主元是 <span><span>d−(c/a) bd - (c/a)\,b</span><span><span><span></span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>c</span><span>/</span><span>a</span><span>)</span><span></span><span>b</span></span></span></span>，两者相乘得 <span><span>a⋅(d−cb/a)=ad−bca \cdot (d - cb/a) = ad - bc</span><span><span><span></span><span>a</span><span></span><span>⋅</span><span></span></span><span><span></span><span>(</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>c</span><span>b</span><span>/</span><span>a</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span></span></span></span>。我取 <span><span>a,b,c,d=3,5,2,4a, b, c, d = 3, 5, 2, 4</span><span><span><span></span><span>a</span><span>,</span><span></span><span>b</span><span>,</span><span></span><span>c</span><span>,</span><span></span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span></span></span></span>，第二个主元是 <span><span>4−103=234 - \tfrac{10}{3} = \tfrac23</span><span><span><span></span><span>4</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>10</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，乘出来是 2，和 <span><span>ad−bc=12−10=2ad - bc = 12 - 10 = 2</span><span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span><span></span><span>−</span><span></span></span><span><span></span><span>10</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 一致。他自己打趣说，所有数学教授都在等他承认：<span><span>a=0a = 0</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时这一步不被允许，要先换行。如果换不成，是因为矩阵奇异。</p></section>
<section><h2>行列式就是主元乘积<a href="#行列式就是主元乘积"><span>#</span></a></h2><p>性质 7 加上性质 5，就得到他说的“MATLAB 以及任何像样的软件”是怎么算行列式的：</p><blockquote><p><span><span>det⁡A=± d1d2⋯dn\det A = \pm\, d_1 d_2 \cdots d_n</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>±</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋯</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>did_i</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是消元得到的主元，符号由换行的次数决定。</p></blockquote><p>没有换行时 <span><span>det⁡A=det⁡U\det A = \det U</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>U</span></span></span></span>，就是主元乘积（对照第 4 讲的 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>：<span><span>det⁡L=1\det L = 1</span><span><span><span></span><span>det</span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，所以 <span><span>det⁡A=det⁡U\det A = \det U</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>U</span></span></span></span>）。有换行时，每换一次乘一次 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。他的说法是“要特别留意加号还是减号”。</p><p>我手写了这个算法：消元到上三角，只在主元为 0 时才换行并记录符号，最后把主元乘起来。三个 3 阶矩阵（不换行、需要换行、奇异）加一个 100 × 100 随机矩阵，都和 <code>np.linalg.det</code> 对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>det_by_elimination</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""消元到上三角，返回（主元乘积 * 换行的符号）。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> U.shape[</span><span>0</span><span>]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>sign </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span># 主元为 0 时才换行：性质 2 每换一次行，符号反转</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> U[k, k] </span><span>==</span><span> </span></span><span>0.0</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>rows </span><span>=</span><span> np.</span><span>nonzero</span><span>(U[k </span><span>+</span><span> </span><span>1</span><span> :, k])[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span>            </span><span>if</span><span> </span><span>len</span><span><span>(rows) </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>14</div></div><div><span>                </span><span>return</span><span> </span><span>0.0</span><span>  </span><span># 该列下方全是 0，奇异，性质 6</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>swap </span><span>=</span><span> k </span><span>+</span><span> </span><span>1</span><span><span> </span><span>+</span><span> rows[</span></span><span>0</span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>U[[k, swap]] </span><span>=</span><span> U[[swap, k]]</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>sign </span><span>=</span><span> </span><span>-</span><span>sign</span></div></div><div><div><div>18</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(k </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> (U[i, k] </span><span>/</span><span> U[k, k]) </span><span>*</span><span> U[k]  </span><span># 性质 5，行列式不变</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span><span> sign </span><span>*</span><span> </span></span><span>float</span><span><span>(np.</span><span>prod</span><span>(np.</span><span>diag</span><span>(U)))</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>cases </span><span>=</span><span> {</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"无需换行"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>3.0</span><span>, </span><span>8.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>1.0</span><span>]]</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"需要换行"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>[[</span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>3.0</span><span>, </span><span>8.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>1.0</span><span>]]</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>33</div></div><div><span>        </span><span>"奇异"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>], [</span><span>4.0</span><span>, </span><span>5.0</span><span>, </span><span>6.0</span><span>], [</span><span>7.0</span><span>, </span><span>8.0</span><span>, </span><span>9.0</span><span>]]),</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>    </span><span>for</span><span> name, M </span><span>in</span><span><span> cases.</span><span>items</span><span>():</span></span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>mine </span><span>=</span><span> </span><span>det_by_elimination</span><span>(M)</span></div></div><div><div><div>37</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：消元 </span><span>{</span><span>mine</span><span>:.4f</span><span>}</span><span>，np.linalg.det </span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(M)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 100 x 100 的随机矩阵</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>100</span><span>, </span><span>100</span><span>))</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>mine </span><span>=</span><span> </span><span>det_by_elimination</span><span>(M)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>ref </span><span>=</span><span> np.linalg.</span><span>det</span><span>(M)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"100 x 100：消元 </span><span>{</span><span>mine</span><span>:.6e</span><span>}</span><span>，np.linalg.det </span><span>{</span><span>ref</span><span>:.6e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"相对差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>abs</span><span><span>(mine </span><span>-</span><span> ref) </span><span>/</span><span> </span></span><span>abs</span><span>(ref)</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>无需换行：消元 10.0000，np.linalg.det 10.0000</span></div></div><div><div><div>2</div></div><div><span>需要换行：消元 6.0000，np.linalg.det 6.0000</span></div></div><div><div><div>3</div></div><div><span>奇异：消元 0.0000，np.linalg.det 0.0000</span></div></div><div><div><div>4</div></div><div><span>100 x 100：消元 3.449180e+77，np.linalg.det 3.449180e+77</span></div></div><div><div><div>5</div></div><div><span>相对差： 9.99e-13</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个 3 阶矩阵里，第一个的主元是 <span><span>1,2,51, 2, 5</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span></span></span></span>，乘积是 10。第二个矩阵第一列的第一个元素是 0，要换行，换一次行之后三个主元是 <span><span>3,2,−13, 2, -1</span><span><span><span></span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>−</span><span>1</span></span></span></span>，乘积 <span><span>−6-6</span><span><span><span></span><span>−</span><span>6</span></span></span></span>，再乘换行的符号 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 得 6。第三个是 <span><span>1,2,…,91, 2, \dots, 9</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>9</span></span></span></span> 排成的奇异矩阵，消元之后最后一个主元是 0，行列式是 0.0000，<code>np.linalg.det</code> 也一样。三者都和库函数吻合。</p><p>100 × 100 那一行我最在意：我的函数没有选主元，只在 <span><span>Ukk=0U_{kk} = 0</span><span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span><span>k</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时才换行，数值上不算好的实现，但结果仍是 3.449180e+77，和 <code>np.linalg.det</code> 的相对差只有 9.99e-13（这个量级下 <span><span>10−1310^{-13}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 的差别，就是舍入误差的底噪）。顺便看一眼：一个 100 阶随机矩阵的行列式有 77 位数，教材里的“大公式”要加的项有 <span><span>100!≈10158100! \approx 10^{158}</span><span><span><span></span><span>100</span><span>!</span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>158</span></span></span></span></span></span></span></span></span></span></span></span> 个，没人会这样算。下一讲会回到那个公式。</p></section>
<section><h2>乘积、逆、转置<a href="#乘积逆转置"><span>#</span></a></h2><p>性质 7 之后，他把剩下的两条一口气讲完。</p><p><strong>性质 9：<span><span>det⁡(AB)=det⁡A⋅det⁡B\det(AB) = \det A \cdot \det B</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span>B</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>⋅</span><span></span></span><span><span></span><span>det</span><span></span><span>B</span></span></span></span>。</strong> 他说这条有点“从天上掉下来”：之前一直没讲过矩阵相乘，行列式突然有了乘法性质。记住行列式没有加法性质（上面 <span><span>73≠1573 \ne 15</span><span><span><span></span><span>73</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>15</span></span></span></span>），但有乘法性质。</p><p>由它立刻得到三个推论，他在黑板上一个一个写：</p><ul>
<li><span><span>det⁡A−1=1/det⁡A\det A^{-1} = 1/\det A</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span></span><span>det</span><span></span><span>A</span></span></span></span>：把 <span><span>A−1A=IA^{-1}A = I</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 两边取行列式，<span><span>det⁡A−1⋅det⁡A=det⁡I=1\det A^{-1} \cdot \det A = \det I = 1</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。他用 <span><span>A=diag(2,3)A = \mathrm{diag}(2, 3)</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 检查：<span><span>det⁡A=6\det A = 6</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span>，<span><span>A−1=diag(12,13)A^{-1} = \mathrm{diag}(\tfrac12, \tfrac13)</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span>，行列式 <span><span>16\tfrac16</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。<span><span>det⁡A=0\det A = 0</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的时候 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 不存在，<span><span>1/01/0</span><span><span><span></span><span>1/0</span></span></span></span> 也没有意义，两边“同时荒谬”。</li>
<li><span><span>det⁡A2=(det⁡A)2\det A^2 = (\det A)^2</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>det</span><span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。</li>
<li><span><span>det⁡(2A)=2ndet⁡A\det(2A) = 2^n \det A</span><span><span><span></span><span>det</span><span>(</span><span>2</span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span><span></span><span>det</span><span></span><span>A</span></span></span></span>，不是 <span><span>2det⁡A2 \det A</span><span><span><span></span><span>2</span><span></span><span>det</span><span></span><span>A</span></span></span></span>。他的理由：每一行都提出一个 2，一共 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 行，所以是 <span><span>2n2^n</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>。他把它和体积联系起来：一个盒子每条边都乘 2，体积乘 <span><span>2n2^n</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>，三维是 8。（这也是他在这一讲唯一提到行列式和体积关系的地方，第 20 讲会专门讲。）</li>
</ul><p><strong>性质 10：<span><span>det⁡AT=det⁡A\det A^\mathsf{T} = \det A</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>A</span></span></span></span>。</strong> 2 × 2 的 <span><span>ad−bcad - bc</span><span><span><span></span><span>a</span><span>d</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>c</span></span></span></span> 一眼就看出来。重要的是推论：之前所有关于“行”的性质，对“列”也成立。零列的行列式是 0，换两列变号。理由很简单：转置，把列变成行，用行的性质，再转置回去。</p><p>他的证明用到 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>：<span><span>det⁡A=det⁡L⋅det⁡U\det A = \det L \cdot \det U</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>L</span><span></span><span>⋅</span><span></span></span><span><span></span><span>det</span><span></span><span>U</span></span></span></span>，<span><span>det⁡AT=det⁡UT⋅det⁡LT\det A^\mathsf{T} = \det U^\mathsf{T} \cdot \det L^\mathsf{T}</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>det</span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 是对角线全为 1 的下三角，性质 7 说它的行列式是 1，<span><span>LTL^\mathsf{T}</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 也是；<span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>UTU^\mathsf{T}</span><span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的对角线相同，行列式都是主元乘积。所以两边相等。他说这只证明了“几乎所有”能 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 的矩阵，有换行的情形留给读者。</p><p>验证一下，用固定种子的 5 × 5 随机矩阵：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>det </span><span>=</span><span> np.linalg.det</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 性质 9：det(AB) = det(A) det(B)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(AB) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(A </span><span>@</span><span> B)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(A) det(B) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(A) </span><span>*</span><span> </span><span>det</span><span>(B)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># 推论：逆、平方、数乘</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(A) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(A)</span></span><span>:.6f</span><span>}</span><span>"</span><span>, </span><span>"，1/det(A) ="</span><span>, </span><span>f</span><span>"</span><span>{1</span><span><span> </span><span>/</span><span> </span><span>det</span><span>(A)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(inv(A)) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(np.linalg.</span><span>inv</span><span>(A))</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>20</div></div><div><span>        </span><span>"det(A @ A) ="</span><span>,</span></div></div><div><div><div>21</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(A </span><span>@</span><span> A)</span></span><span>:.6f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>22</div></div><div><span>        </span><span>"，det(A)^2 ="</span><span>,</span></div></div><div><div><div>23</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(A) </span><span>**</span><span> </span></span><span>2</span><span>:.6f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(2A) / det(A) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> A) </span><span>/</span><span> </span><span>det</span><span>(A)</span></span><span>:.4f</span><span>}</span><span>"</span><span>, </span><span>"，2^n ="</span><span>, </span><span>2</span><span><span>**</span><span>n)</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># Strang 的对角例子：A = diag(2, 3)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>diag</span><span>([</span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"diag(2, 3)："</span><span>,</span></div></div><div><div><div>31</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(D), </span></span><span>4</span><span>),</span></div></div><div><div><div>32</div></div><div><span>        </span><span>"，逆："</span><span>,</span></div></div><div><div><div>33</div></div><div><span>        </span><span>round</span><span><span>(</span><span>det</span><span>(np.linalg.</span><span>inv</span><span>(D)), </span></span><span>4</span><span>),</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 性质 10：det(A^T) = det(A)，所以列也有同样的性质</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"性质 10："</span><span><span>, np.</span><span>isclose</span><span>(</span><span>det</span><span>(A.T), </span><span>det</span><span>(A)))</span></span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>swapped_cols </span><span>=</span><span> A[:, [</span><span>1</span><span>, </span><span>0</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>]]</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"换两列："</span><span><span>, np.</span><span>isclose</span><span>(</span><span>det</span><span>(swapped_cols), </span><span>-</span><span>det</span><span>(A)))</span></span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>zero_col </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>zero_col[:, </span><span>2</span><span><span>] </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"零列 det ="</span><span>, </span><span>round</span><span><span>(</span><span>det</span><span>(zero_col), </span></span><span>4</span><span>))</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span># 性质 10 的证明思路：A = LU，det(L) = 1，U 与 U^T 对角线相同</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.</span><span>tril</span><span>(rng.</span><span>standard_normal</span><span>((n, n)), </span><span>-</span><span>1</span><span><span>) </span><span>+</span><span> np.</span><span>eye</span><span>(n)</span></span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> np.</span><span>triu</span><span>(rng.</span><span>standard_normal</span><span>((n, n)))</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> L </span><span>@</span><span> U</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(L) ="</span><span><span>, </span><span>det</span><span>(L), </span></span><span>"，det(L^T) ="</span><span><span>, </span><span>det</span><span>(L.T))</span></span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(M) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(M)</span></span><span>:.6f</span><span>}</span><span>"</span><span>, </span><span>"，det(U) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(U)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(M^T) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(M.T)</span></span><span>:.6f</span><span>}</span><span>"</span><span>, </span><span>"，det(U^T) ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>det</span><span>(U.T)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>det(AB) = 0.334636</span></div></div><div><div><div>2</div></div><div><span>det(A) det(B) = 0.334636</span></div></div><div><div><div>3</div></div><div><span>det(A) = 0.157837 ，1/det(A) = 6.335633</span></div></div><div><div><div>4</div></div><div><span>det(inv(A)) = 6.335633</span></div></div><div><div><div>5</div></div><div><span>det(A @ A) = 0.024913 ，det(A)^2 = 0.024913</span></div></div><div><div><div>6</div></div><div><span>det(2A) / det(A) = 32.0000 ，2^n = 32</span></div></div><div><div><div>7</div></div><div><span>diag(2, 3)： 6.0 ，逆： 0.1667</span></div></div><div><div><div>8</div></div><div><span>性质 10： True</span></div></div><div><div><div>9</div></div><div><span>换两列： True</span></div></div><div><div><div>10</div></div><div><span>零列 det = 0.0</span></div></div><div><div><div>11</div></div><div><span>det(L) = 1.0 ，det(L^T) = 1.0</span></div></div><div><div><div>12</div></div><div><span>det(M) = 0.007649 ，det(U) = 0.007649</span></div></div><div><div><div>13</div></div><div><span>det(M^T) = 0.007649 ，det(U^T) = 0.007649</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>逐条读：</p><ul>
<li><span><span>det⁡(AB)\det(AB)</span><span><span><span></span><span>det</span><span>(</span><span>A</span><span>B</span><span>)</span></span></span></span> 和 <span><span>det⁡A⋅det⁡B\det A \cdot \det B</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>⋅</span><span></span></span><span><span></span><span>det</span><span></span><span>B</span></span></span></span> 都是 0.334636。</li>
<li><span><span>det⁡A=0.157837\det A = 0.157837</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>0.157837</span></span></span></span>，倒数是 6.335633，和 <code>np.linalg.inv(A)</code> 的行列式一致。<span><span>det⁡A2=0.024913=0.1578372\det A^2 = 0.024913 = 0.157837^2</span><span><span><span></span><span>det</span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.024913</span><span></span><span>=</span><span></span></span><span><span></span><span>0.15783</span><span><span>7</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。</li>
<li><span><span>det⁡(2A)/det⁡A=32.0000=25\det(2A) / \det A = 32.0000 = 2^5</span><span><span><span></span><span>det</span><span>(</span><span>2</span><span>A</span><span>)</span><span>/</span><span></span><span>det</span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>32.0000</span><span></span><span>=</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span></span></span></span></span></span></span></span>，<span><span>n=5n = 5</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span>。数乘一个 5 阶矩阵，行列式涨到 32 倍，而不是 2 倍。</li>
<li>对角的例子 <span><span>diag(2,3)\mathrm{diag}(2, 3)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>：行列式 6.0，逆的行列式 0.1667，也就是 <span><span>16\tfrac16</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。</li>
<li>转置、换两列、零列，三条列性质都成立。</li>
<li>最后两行：我取了随机的单位下三角 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 和上三角 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>，<span><span>M=LUM = LU</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>，<span><span>det⁡L=1.0\det L = 1.0</span><span><span><span></span><span>det</span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span>1.0</span></span></span></span>，<span><span>det⁡M=det⁡U=0.007649\det M = \det U = 0.007649</span><span><span><span></span><span>det</span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>0.007649</span></span></span></span>，转置之后 <span><span>det⁡MT=det⁡UT=0.007649\det M^\mathsf{T} = \det U^\mathsf{T} = 0.007649</span><span><span><span></span><span>det</span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>det</span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.007649</span></span></span></span>。</li>
</ul></section>
<section><h2>行列式不是数值上的奇异检验<a href="#行列式不是数值上的奇异检验"><span>#</span></a></h2><p>这一节是我自己加的，不是视频里的内容。Strang 说行列式是“可逆性的检验”，数学上完全对。但是写代码的人要小心：这个检验在浮点数里不太可靠。</p><p>最简单的例子：<span><span>A=0.1I400A = 0.1 I_{400}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>400</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，也就是对角线全是 0.1 的 400 阶对角矩阵。它显然可逆，秩 400，条件数 1，和单位矩阵一样好。可它的行列式是 <span><span>10−40010^{-400}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>400</span></span></span></span></span></span></span></span></span></span></span></span>，比双精度能表示的最小正数（约 <span><span>5×10−3245 \times 10^{-324}</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>324</span></span></span></span></span></span></span></span></span></span></span></span>）还小，会下溢：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 可逆的矩阵，行列式在浮点数里却可能是 0.0：400 阶的 0.1 * I</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>400</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>0.1</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(n)</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>sign, logdet </span><span>=</span><span> np.linalg.</span><span>slogdet</span><span>(A)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(0.1 I) ="</span><span><span>, np.linalg.</span><span>det</span><span>(A))</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"slogdet：符号"</span><span>, sign, </span><span>"，log|det| ="</span><span>, </span><span>round</span><span>(logdet, </span><span>4</span><span>))</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"log10|det| ="</span><span>, </span><span>round</span><span><span>(logdet </span><span>/</span><span> np.</span><span>log</span><span>(</span></span><span>10.0</span><span>), </span><span>1</span><span>))</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A), </span></span><span>"，条件数 ="</span><span><span>, np.linalg.</span><span>cond</span><span>(A))</span></span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A, np.</span><span>ones</span><span>(n))</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"solve 没问题：x[0] ="</span><span>, </span><span>round</span><span>(x[</span><span>0</span><span>], </span><span>4</span><span>))</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>det(0.1 I) = 0.0</span></div></div><div><div><div>2</div></div><div><span>slogdet：符号 1.0 ，log|det| = -921.034</span></div></div><div><div><div>3</div></div><div><span>log10|det| = -400.0</span></div></div><div><div><div>4</div></div><div><span>秩 = 400 ，条件数 = 1.0</span></div></div><div><div><div>5</div></div><div><span>solve 没问题：x[0] = 10.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>np.linalg.det</code> 返回 0.0。如果拿 <code>det == 0</code> 判断奇异，这个矩阵会被判成奇异。但 <code>np.linalg.slogdet</code> 分开算符号和 <span><span>log⁡∣det⁡∣\log|\det|</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>∣</span><span></span><span>det</span><span></span><span>∣</span></span></span></span>：符号是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>，<span><span>log⁡∣det⁡A∣=−921.034\log|\det A| = -921.034</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>∣</span><span></span><span>det</span><span></span><span>A</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>921.034</span></span></span></span>，除以 <span><span>ln⁡10\ln 10</span><span><span><span></span><span>ln</span><span></span><span>10</span></span></span></span> 就是 <span><span>−400.0-400.0</span><span><span><span></span><span>−</span><span>400.0</span></span></span></span>，恰好是 <span><span>10−40010^{-400}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>400</span></span></span></span></span></span></span></span></span></span></span></span>。秩是 400，条件数是 1.0，<code>np.linalg.solve</code> 解出 <span><span>x0=10.0x_0 = 10.0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10.0</span></span></span></span>，一点问题没有。</p><p>所以我的习惯是：理论里 <span><span>det⁡A≠0\det A \ne 0</span><span><span><span></span><span>det</span><span></span><span>A</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span>，代码里看秩、条件数，或者用 <code>slogdet</code>。（<code>det</code> 是为了写证明，不是为了写 <code>if</code>。）反过来，上面 <span><span>det⁡(2A)=2ndet⁡A\det(2A) = 2^n \det A</span><span><span><span></span><span>det</span><span>(</span><span>2</span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span><span></span><span>det</span><span></span><span>A</span></span></span></span> 也提醒我们：行列式对整体缩放极其敏感，它的大小本来就不代表矩阵“离奇异有多远”。</p><p>最后一件事。他在视频末尾留了一个尾巴：每个置换要么是奇数次换行，要么是偶数次，不能两者兼有，否则性质 2 自相矛盾，“整个讲座就完了”。这个事实只需要再多一点代数技巧，他说，而一旦成立，行列式就由性质 1、2、3 唯一确定。这一点正好连到下一讲：他会从这三条性质直接写出 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 项的大公式，再按行展开出代数余子式，见 <a href="/posts/linear-algebra-19-determinant-formulas-cofactors/">18.06 第 19 讲：行列式公式与代数余子式</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-17-orthogonal-matrices-gram-schmidt/</id>
      <title type="text">18.06 第 17 讲：正交矩阵与 Gram-Schmidt</title>
      <published>2026-10-18T05:20:00.000Z</published>
      <updated>2026-10-18T05:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-17-orthogonal-matrices-gram-schmidt/"/>
      <summary type="text">MIT 18.06 第 17 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 手写了经典与修正两种 Gram-Schmidt，在 8 × 8 的 Hilbert 矩阵上，经典版的 ‖QᵀQ - I‖ 是 1.4，修正版是 3.6e-07，np.linalg.qr 是 1.5e-15。</summary>
      <content type="html"><![CDATA[<p>这一讲是“正交”这一章的收尾，Strang 想说的其实只有一句：如果一组基两两垂直、长度为 1，那么前面几讲里所有麻烦的公式都会塌缩成点积。他的 Gram-Schmidt 例子只有两个向量，我把同样的算法写成代码，丢给 8 × 8 的 Hilbert 矩阵，结果有点出乎意料：手写的经典版算出的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 连正交都谈不上（<span><span>∥QTQ−I∥=1.4\lVert Q^TQ - I \rVert = 1.4</span><span><span><span></span><span>∥</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>1.4</span></span></span></span>，本该是 0），而 <code>np.linalg.qr</code> 是 <span><span>1.5×10−151.5 \times 10^{-15}</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=0MtwqhIwdrI" target="_blank">Lecture 17: Orthogonal matrices and Gram-Schmidt</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-17-orthogonal-matrices-and-gram-schmidt/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §4.4 Orthonormal Bases and Gram-Schmidt
前置：<a href="/posts/linear-algebra-16-least-squares/">第 16 讲：投影矩阵与最小二乘</a>
一句话：列向量标准正交的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 满足 <span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，投影和最小二乘都不用求逆；Gram-Schmidt 把任意一组无关的列变成这样的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>，用矩阵写出来就是 <span><span>A=QRA = QR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>QR</span></span></span></span>，<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 是上三角矩阵。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他在抱怨“orthonormal matrix”这个名字没人用，只有方阵才叫 orthogonal matrix。这段是在定义术语，值得听一遍，后面的符号全靠它。接下来的置换、旋转、Hadamard 例子本篇已经验证过，可以倍速。</li>
<li>他回头把投影公式 <span><span>Q(QTQ)−1QTQ(Q^TQ)^{-1}Q^T</span><span><span><span></span><span>Q</span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 简化成 <span><span>QQTQQ^T</span><span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>、把正规方程简化成 <span><span>x^=QTb\hat{x} = Q^T\mathbf{b}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 的那几分钟要认真看，这是“为什么要有 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>”的全部理由。</li>
<li>讲 Gram-Schmidt 的一段（他先画 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 和 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，再算 <span><span>(1,1,1)(1,1,1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 和 <span><span>(1,0,2)(1,0,2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>）建议跟着他在纸上做一遍，我用代码只是对答案。</li>
<li>最后几句话讲 <span><span>A=QRA = QR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>QR</span></span></span></span> 为什么 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 是上三角，说得很快，教材里写得更完整。</li>
</ul></div></div>
<section><h2>正交基与正交矩阵<a href="#正交基与正交矩阵"><span>#</span></a></h2><p>先把名字说清楚。向量 <span><span>q1,…,qn\mathbf{q}_1, \dots, \mathbf{q}_n</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 如果满足</p><span><span><span>qiTqj={0i≠j1i=j\mathbf{q}_i^T\mathbf{q}_j =
\begin{cases}
0 &amp; i \ne j \\
1 &amp; i = j
\end{cases}</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>{</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span>i</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span><span>j</span></span></span><span><span></span><span><span>i</span><span></span><span>=</span><span></span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span></span></span></span></span></span><p>就叫标准正交（orthonormal）：两两垂直，长度为 1。把它们当列放进矩阵 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>，<span><span>QTQQ^TQ</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span></span></span></span> 的第 <span><span>(i,j)(i, j)</span><span><span><span></span><span>(</span><span>i</span><span>,</span><span></span><span>j</span><span>)</span></span></span></span> 个元素正好是 <span><span>qiTqj\mathbf{q}_i^T\mathbf{q}_j</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以</p><span><span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span></span><p>这里 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 不必是方阵。两列、每列四个分量也行，只要列标准正交，<span><span>QTQQ^TQ</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span></span></span></span> 就是单位矩阵。<strong>什么时候才叫正交矩阵？</strong> Strang 吐槽过这个约定：叫“标准正交矩阵”本来最贴切，但习惯上只有方阵才叫正交矩阵。方阵的情形有额外的好处：<span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 说明 <span><span>QTQ^T</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 就是 <span><span>Q−1Q^{-1}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，逆矩阵不用算，转置一下就是。</p><p>他在黑板上给了一串例子，我挑着写成同一个检查脚本：</p><ul>
<li>置换矩阵：把 1 放在不同位置，列当然垂直，长度当然是 1。他自己说这个例子“太容易了”。</li>
<li>旋转矩阵 <span><span>[cos⁡θ−sin⁡θsin⁡θcos⁡θ]\begin{bmatrix} \cos\theta &amp; -\sin\theta \\ \sin\theta &amp; \cos\theta \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span><span><span></span><span><span>sin</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span></span><span>sin</span><span></span><span>θ</span></span></span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。<span><span>[111−1]\begin{bmatrix} 1 &amp; 1 \\ 1 &amp; -1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的两列垂直，但长度是 <span><span>2\sqrt{2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，除以 <span><span>2\sqrt{2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 之后它就是 <span><span>θ=π/4\theta = \pi/4</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>π</span><span>/4</span></span></span></span> 的旋转加一个翻转（他说“大概是 <span><span>−π/4-\pi/4</span><span><span><span></span><span>−</span><span>π</span><span>/4</span></span></span></span> 之类的”，符号他没细究）。</li>
<li>Hadamard 矩阵：把上面的 2 × 2 放在左上角，重复到右上和左下，右下角取负，每列长度是 2，所以除以 2。（字幕里把名字转写成了 Adhemar，我猜是 Hadamard 被听岔了。）他说 2、4、16、64 阶都知道怎么造，哪些阶数存在这样的 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> 矩阵，至今没有完全搞清。</li>
<li>矩形的例子：两列 <span><span>(1,2,2)(1, 2, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 和 <span><span>(−2,−1,2)(-2, -1, 2)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，每列长度是 <span><span>1+4+4=3\sqrt{1 + 4 + 4} = 3</span><span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>4</span><span></span><span>+</span><span></span><span>4</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，所以除以 3。他还想再凑一个第三列，让整个 3 × 3 矩阵没有平方根，试着用 <span><span>1,2,21, 2, 2</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span></span></span></span> 的模式调符号。我取 <span><span>(2,−2,1)(2, -2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 做第三列，三对点积都是 0。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> np.pi </span><span>/</span><span> </span><span>4</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>examples </span><span>=</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>"置换矩阵"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>[[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>]]</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"旋转 pi/4"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>[</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>[np.</span><span>cos</span><span>(theta), </span><span>-</span><span>np.</span><span>sin</span><span>(theta)],</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>[np.</span><span>sin</span><span>(theta), np.</span><span>cos</span><span>(theta)],</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>17</div></div><div><span>        </span><span>"[[1, 1], [1, -1]] / sqrt(2)"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]])</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>/</span><span> np.</span><span>sqrt</span><span>(</span><span>2</span><span>),</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"4 x 4 Hadamard / 2"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>[</span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>],</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>],</span></div></div><div><div><div>24</div></div><div><span><span>                </span></span><span>[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>]</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>/</span><span> </span><span>2.0</span><span>,</span></div></div><div><div><div>28</div></div><div><span>        </span><span>"3 x 3，元素 1, 2, 2 / 3"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>[[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>]]</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>/</span><span> </span><span>3.0</span><span>,</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> name, Q </span><span>in</span><span><span> examples.</span><span>items</span><span>():</span></span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(Q.shape[</span><span>1</span><span>])</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>ok </span><span>=</span><span> np.</span><span>allclose</span><span>(Q.T </span><span>@</span><span> Q, identity)</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>inv_ok </span><span>=</span><span> np.</span><span>allclose</span><span>(np.linalg.</span><span>inv</span><span>(Q), Q.T)</span></div></div><div><div><div>37</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：Q^T Q = I </span><span>{</span><span>ok</span><span>}</span><span>，Q^-1 = Q^T </span><span>{</span><span>inv_ok</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 矩形的例子：两列，三个分量</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>], [</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>2.0</span><span>, </span><span>2.0</span><span><span>]]) </span><span>/</span><span> </span></span><span>3.0</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"矩形 3 x 2：Q^T Q = I"</span><span><span>, np.</span><span>allclose</span><span>(Q.T </span><span>@</span><span> Q, np.</span><span>eye</span><span>(</span></span><span>2</span><span>)))</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"矩形 3 x 2：Q Q^T = I"</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> Q.T, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> Q </span><span>@</span><span> Q.T</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"P = Q Q^T 对称："</span><span><span>, np.</span><span>allclose</span><span>(P, P.T))</span></span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ P = P："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P))</span></span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 投影：正规方程 Q^T Q x = Q^T b 退化成 x = Q^T b</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> Q.T </span><span>@</span><span> b</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>x_ls, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(Q, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"x = Q^T b ="</span><span>, x)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span><span>"lstsq 给出的 x ="</span><span><span>, x_ls, np.</span><span>allclose</span><span>(x, x_ls))</span></span></div></div><div><div><div>53</div></div><div><span>    </span><span>print</span><span>(</span><span>"投影 P @ b ="</span><span><span>, P </span><span>@</span><span> b, </span></span><span>"，Q @ x ="</span><span><span>, Q </span><span>@</span><span> x)</span></span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>置换矩阵：Q^T Q = I True，Q^-1 = Q^T True</span></div></div><div><div><div>2</div></div><div><span>旋转 pi/4：Q^T Q = I True，Q^-1 = Q^T True</span></div></div><div><div><div>3</div></div><div><span>[[1, 1], [1, -1]] / sqrt(2)：Q^T Q = I True，Q^-1 = Q^T True</span></div></div><div><div><div>4</div></div><div><span>4 x 4 Hadamard / 2：Q^T Q = I True，Q^-1 = Q^T True</span></div></div><div><div><div>5</div></div><div><span>3 x 3，元素 1, 2, 2 / 3：Q^T Q = I True，Q^-1 = Q^T True</span></div></div><div><div><div>6</div></div><div><span>矩形 3 x 2：Q^T Q = I True</span></div></div><div><div><div>7</div></div><div><span>矩形 3 x 2：Q Q^T = I False</span></div></div><div><div><div>8</div></div><div><span>P = Q Q^T 对称： True</span></div></div><div><div><div>9</div></div><div><span>P @ P = P： True</span></div></div><div><div><div>10</div></div><div><span>x = Q^T b = [3.6667 0.6667]</span></div></div><div><div><div>11</div></div><div><span>lstsq 给出的 x = [3.6667 0.6667] True</span></div></div><div><div><div>12</div></div><div><span>投影 P @ b = [0.7778 2.2222 2.8889] ，Q @ x = [0.7778 2.2222 2.8889]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>五个方阵都满足 <span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，而且 <code>np.linalg.inv(Q)</code> 和 <code>Q.T</code> 一致，这就是“逆就是转置”。矩形的 3 × 2 矩阵不一样：<span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 成立，但 <span><span>QQT=IQQ^T = I</span><span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 不成立。道理很直接，三维空间里只有两个方向，填不满，所以 <span><span>QQTQQ^T</span><span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 不可能是 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 的单位矩阵。它是什么呢？接着往下看。</p></section>
<section><h2>为什么 Q 好用：投影只剩转置<a href="#为什么-q-好用投影只剩转置"><span>#</span></a></h2><p>前两讲的投影矩阵是 <span><span>A(ATA)−1ATA(A^TA)^{-1}A^T</span><span><span><span></span><span>A</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。Strang 开玩笑说，这是“一行里四个 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>”。现在列向量标准正交，把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 换成 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>：</p><span><span><span>P=Q(QTQ)−1QT=QQTP = Q(Q^TQ)^{-1}Q^T = QQ^T</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>中间那个 <span><span>QTQQ^TQ</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span></span></span></span> 就是单位矩阵，求逆没了。他顺手验证了投影矩阵的两条性质：对称（<span><span>QQTQQ^T</span><span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 转置之后还是自己），以及投影两次等于一次，<span><span>(QQT)(QQT)=Q(QTQ)QT=QQT(QQ^T)(QQ^T) = Q(Q^TQ)Q^T = QQ^T</span><span><span><span></span><span>(</span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span>(</span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span>(</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span>)</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，中间的 <span><span>QTQ=IQ^TQ = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 一抵消就得到。如果 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 是方阵，列空间就是整个空间，投影到整个空间什么都不用动，所以 <span><span>QQT=IQQ^T = I</span><span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，这正是上面“方阵的逆就是转置”的另一种说法。</p><p>正规方程也一样。原来是 <span><span>ATAx^=ATbA^TA\hat{x} = A^T\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，要把一堆“笨拙的内积”算出来再解方程组；现在是</p><span><span><span>QTQx^=QTb⟹x^=QTbQ^TQ\hat{x} = Q^T\mathbf{b} \quad\Longrightarrow\quad \hat{x} = Q^T\mathbf{b}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span></span><p><span><span>x^\hat{x}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个分量就是 <span><span>qiTb\mathbf{q}_i^T\mathbf{b}</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>b</span></span></span></span>，一个点积。他说这大概是数学很多分支里最重要的一个公式：有了标准正交基，一个向量在每个方向上的分量，就是它和这个方向的点积。（之后的 Fourier 级数用的就是这一条，只是“向量”换成了函数。）</p><p>上面那段脚本最后几行对这两件事做了数值验证：<span><span>P=QQTP = QQ^T</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 对称，<span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>；用 <span><span>x^=QTb\hat{x} = Q^T\mathbf{b}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 得到的 <span><span>(3.6667,0.6667)(3.6667, 0.6667)</span><span><span><span></span><span>(</span><span>3.6667</span><span>,</span><span></span><span>0.6667</span><span>)</span></span></span></span> 和 <code>np.linalg.lstsq</code> 给出的完全一致，<span><span>PbP\mathbf{b}</span><span><span><span></span><span>P</span><span>b</span></span></span></span> 与 <span><span>Qx^Q\hat{x}</span><span><span><span></span><span>Q</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 也一致，都是 <span><span>(0.7778,2.2222,2.8889)(0.7778, 2.2222, 2.8889)</span><span><span><span></span><span>(</span><span>0.7778</span><span>,</span><span></span><span>2.2222</span><span>,</span><span></span><span>2.8889</span><span>)</span></span></span></span>。同一个答案，一个要解方程组，一个只做了两次点积。</p><p>他还提到一点：标准正交的向量一定线性无关，因为互相垂直，没有任何组合能凑出零。反过来就不一定了，所以下面要做的事才有意义。</p></section>
<section><h2>Gram-Schmidt：把投影扔掉<a href="#gram-schmidt把投影扔掉"><span>#</span></a></h2><p><strong>如果给我的向量不是标准正交的呢？</strong> 这是后半讲的问题：已知无关的 <span><span>a,b\mathbf{a}, \mathbf{b}</span><span><span><span></span><span>a</span><span>,</span><span></span><span>b</span></span></span></span>，想得到标准正交的 <span><span>q1,q2\mathbf{q}_1, \mathbf{q}_2</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他的做法分两步，先造垂直的，最后再统一除以长度（他说“除以长度是 Schmidt 的贡献”，语气是玩笑）。</p><p>为了不和矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 混淆，下面把他黑板上的大写向量写成粗体 <span><span>A,B,C\mathbf{A}, \mathbf{B}, \mathbf{C}</span><span><span><span></span><span>A</span><span>,</span><span></span><span>B</span><span>,</span><span></span><span>C</span></span></span></span>。第一个方向无所谓，<span><span>A=a\mathbf{A} = \mathbf{a}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span></span></span></span>。第二个方向要从 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 里把平行于 <span><span>A\mathbf{A}</span><span><span><span></span><span>A</span></span></span></span> 的那一块拿掉，剩下的就是垂直部分，也就是讲投影时的误差向量 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span>：</p><span><span><span>B=b−ATbATA A\mathbf{B} = \mathbf{b} - \frac{\mathbf{A}^T\mathbf{b}}{\mathbf{A}^T\mathbf{A}}\,\mathbf{A}</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>A</span></span></span></span></span><p>检查一下：两边左乘 <span><span>AT\mathbf{A}^T</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，第一项是 <span><span>ATb\mathbf{A}^T\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，第二项减掉的也是 <span><span>ATb\mathbf{A}^T\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，所以 <span><span>ATB=0\mathbf{A}^T\mathbf{B} = 0</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。第三个向量 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span> 要同时扣掉 <span><span>A\mathbf{A}</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>B\mathbf{B}</span><span><span><span></span><span>B</span></span></span></span> 方向的分量：</p><span><span><span>C=c−ATcATA A−BTcBTB B\mathbf{C} = \mathbf{c} - \frac{\mathbf{A}^T\mathbf{c}}{\mathbf{A}^T\mathbf{A}}\,\mathbf{A} - \frac{\mathbf{B}^T\mathbf{c}}{\mathbf{B}^T\mathbf{B}}\,\mathbf{B}</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>A</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>B</span></span></span></span></span><p>他的数值例子是 <span><span>a=(1,1,1)\mathbf{a} = (1, 1, 1)</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>b=(1,0,2)\mathbf{b} = (1, 0, 2)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>。他先算 <span><span>aTb\mathbf{a}^T\mathbf{b}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，发现是 3，又算 <span><span>aTa\mathbf{a}^T\mathbf{a}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span></span>，也是 3，他自己都笑了：“没想到会这样”。投影系数是 1，所以 <span><span>B=(1,0,2)−(1,1,1)=(0,−1,1)\mathbf{B} = (1, 0, 2) - (1, 1, 1) = (0, -1, 1)</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>2</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。然后 <span><span>A\mathbf{A}</span><span><span><span></span><span>A</span></span></span></span> 除以 <span><span>3\sqrt{3}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，<span><span>B\mathbf{B}</span><span><span><span></span><span>B</span></span></span></span> 除以 <span><span>2\sqrt{2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，得到</p><span><span><span>Q=[1/301/3−1/21/31/2]Q = \begin{bmatrix}
1/\sqrt{3} &amp; 0 \\
1/\sqrt{3} &amp; -1/\sqrt{2} \\
1/\sqrt{3} &amp; 1/\sqrt{2}
\end{bmatrix}</span><span><span><span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1/</span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span><span>1/</span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span><span>1/</span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span><span>1/</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>他还特别问了一个问题：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的列空间是什么关系？答案是同一个平面。<span><span>B\mathbf{B}</span><span><span><span></span><span>B</span></span></span></span> 是 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 和 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 的组合，我们始终没有离开原来的空间，只是把基掰成了直角。</p></section>
<section><h2>A = QR：Gram-Schmidt 的矩阵形式<a href="#a--qrgram-schmidt-的矩阵形式"><span>#</span></a></h2><p>他拿消元类比：消元学完之后，有人告诉你“用矩阵写就是 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>”。Gram-Schmidt 也有一个这样的矩阵形式，<span><span>A=QRA = QR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>QR</span></span></span></span>。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列是原来的向量，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的列是标准正交的新向量，两者有相同的列空间，所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的每一列都是 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的列的组合，组合系数放进 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span>：</p><span><span><span>[a1a2]=[q1q2][q1Ta1q1Ta2q2Ta1q2Ta2]\begin{bmatrix} \mathbf{a}_1 &amp; \mathbf{a}_2 \end{bmatrix}
= \begin{bmatrix} \mathbf{q}_1 &amp; \mathbf{q}_2 \end{bmatrix}
\begin{bmatrix}
\mathbf{q}_1^T\mathbf{a}_1 &amp; \mathbf{q}_1^T\mathbf{a}_2 \\
\mathbf{q}_2^T\mathbf{a}_1 &amp; \mathbf{q}_2^T\mathbf{a}_2
\end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p><span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的元素就是点积，这是 <span><span>x^=QTb\hat{x} = Q^T\mathbf{b}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span> 的又一次出场。他最后说的要点：<span><span>q2Ta1=0\mathbf{q}_2^T\mathbf{a}_1 = 0</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，因为每个新 <span><span>q\mathbf{q}</span><span><span><span></span><span>q</span></span></span></span> 都是对前面所有向量垂直造出来的，所以 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的左下角为零，<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 是上三角矩阵。他在课上只口头猜了 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的元素，没有写出数值，并说为什么 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 是上三角，请去书里看。我用代码算了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>gram_schmidt</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; tuple[np.ndarray, np.ndarray]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""经典 Gram-Schmidt，返回 Q 和 R。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>1</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>zeros_like</span><span>(A)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> np.</span><span>zeros</span><span>((n, n))</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> A[:, j].</span><span>copy</span><span>()</span></div></div><div><div><div>11</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(j):</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>R[i, j] </span><span>=</span><span> Q[:, i] </span><span>@</span><span> A[:, j]</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>v </span><span>-=</span><span> R[i, j] </span><span>*</span><span> Q[:, i]</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>R[j, j] </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(v)</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>Q[:, j] </span><span>=</span><span> v </span><span>/</span><span> R[j, j]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> Q, R</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>a, b </span><span>=</span><span> A[:, </span><span>0</span><span>], A[:, </span><span>1</span><span>]</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 按 Strang 在黑板上的步骤</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> b </span><span>-</span><span> (a </span><span>@</span><span> b) </span><span>/</span><span> (a </span><span>@</span><span> a) </span><span>*</span><span> a</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"a^T b ="</span><span><span>, a </span><span>@</span><span> b, </span></span><span>"，a^T a ="</span><span><span>, a </span><span>@</span><span> a)</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"B ="</span><span>, B, </span><span>"，a^T B ="</span><span><span>, a </span><span>@</span><span> B)</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>Q, R </span><span>=</span><span> </span><span>gram_schmidt</span><span>(A)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q ="</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span><span>(np.</span><span>round</span><span>(Q, </span></span><span>6</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"R ="</span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(R)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"A = QR："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> R, A))</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q^T Q = I："</span><span><span>, np.</span><span>allclose</span><span>(Q.T </span><span>@</span><span> Q, np.</span><span>eye</span><span>(</span></span><span>2</span><span>)))</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span># 两个列空间相同：Q 的列可以由 A 的列组合出来，反过来也行</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>both </span><span>=</span><span> np.</span><span>hstack</span><span>([A, Q])</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"[A Q] 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(both))</span></span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span># 和 NumPy 自带的 QR 对比，符号可能整列相反</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>Q_np, R_np </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(A)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.qr 的 R ="</span><span>)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(R_np)</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>signs </span><span>=</span><span> np.</span><span>sign</span><span>(np.</span><span>diag</span><span>(R_np))</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"统一符号后 Q 相同："</span><span><span>, np.</span><span>allclose</span><span>(Q_np </span><span>*</span><span> signs, Q))</span></span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"统一符号后 R 相同："</span><span><span>, np.</span><span>allclose</span><span>(R_np </span><span>*</span><span> signs[:, </span></span><span>None</span><span>], R))</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>a^T b = 3.0 ，a^T a = 3.0</span></div></div><div><div><div>2</div></div><div><span>B = [ 0. -1.  1.] ，a^T B = 0.0</span></div></div><div><div><div>3</div></div><div><span>Q =</span></div></div><div><div><div>4</div></div><div><span>[[ 0.5774  0.    ]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 0.5774 -0.7071]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[ 0.5774  0.7071]]</span></div></div><div><div><div>7</div></div><div><span>R =</span></div></div><div><div><div>8</div></div><div><span>[[1.7321 1.7321]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[0.     1.4142]]</span></div></div><div><div><div>10</div></div><div><span>A = QR： True</span></div></div><div><div><div>11</div></div><div><span>Q^T Q = I： True</span></div></div><div><div><div>12</div></div><div><span>[A Q] 的秩： 2</span></div></div><div><div><div>13</div></div><div><span>np.linalg.qr 的 R =</span></div></div><div><div><div>14</div></div><div><span>[[-1.7321 -1.7321]</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[ 0.      1.4142]]</span></div></div><div><div><div>16</div></div><div><span>统一符号后 Q 相同： True</span></div></div><div><div><div>17</div></div><div><span>统一符号后 R 相同： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几件事值得一看。</p><ul>
<li><span><span>aTb=aTa=3\mathbf{a}^T\mathbf{b} = \mathbf{a}^T\mathbf{a} = 3</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，和他黑板上的一模一样，<span><span>B=(0,−1,1)\mathbf{B} = (0, -1, 1)</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>aTB=0\mathbf{a}^T\mathbf{B} = 0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</li>
<li><span><span>R=[3302]R = \begin{bmatrix} \sqrt{3} &amp; \sqrt{3} \\ 0 &amp; \sqrt{2} \end{bmatrix}</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，左下角确实是 0。对角线是 <span><span>A\mathbf{A}</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>B\mathbf{B}</span><span><span><span></span><span>B</span></span></span></span> 的长度。</li>
<li><span><span>[A Q][A\ Q]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>Q</span><span>]</span></span></span></span> 的秩还是 2：把 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的两列拼到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 旁边，没有多出新方向，说明列空间是同一个。</li>
<li><code>np.linalg.qr</code> 给出的 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 对角线是负的（<span><span>−1.7321-1.7321</span><span><span><span></span><span>−</span><span>1.7321</span></span></span></span>），<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的对应列也反号。<span><span>A=QRA = QR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>QR</span></span></span></span> 的分解并不是唯一的，把 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的某一列和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的对应行同时乘 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 之后还是一个分解，LAPACK 的 Householder 实现碰巧选了另一种符号。统一符号之后，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 都和我手写的一致。</li>
</ul></section>
<section><h2>经典与修正：同一个公式，两种舍入<a href="#经典与修正同一个公式两种舍入"><span>#</span></a></h2><p>这一段是课上没有讲的，我自己加的实验。（他的例子只有两个向量，看不出差别。）</p><p>上面手写的版本，对第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列，是拿原来的 <span><span>aj\mathbf{a}_j</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 分别减去它在 <span><span>q1,…,qj−1\mathbf{q}_1, \dots, \mathbf{q}_{j-1}</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>j</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 上的分量。教科书上叫经典 Gram-Schmidt。数学上还有一种写法，每减掉一个方向，就用减过之后的向量去算下一个投影系数，叫修正 Gram-Schmidt（modified Gram-Schmidt）。在精确运算里，两种写法一模一样，差别只在浮点：经典版的 <span><span>qiTaj\mathbf{q}_i^T\mathbf{a}_j</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 用的是没有被修正过的原向量，前面的舍入误差没有机会被后面的步骤“看见”。</p><p>测试矩阵我选了随机矩阵和 Hilbert 矩阵 <span><span>Hij=1/(i+j−1)H_{ij} = 1/(i + j - 1)</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span>(</span><span>i</span><span></span><span>+</span><span></span></span><span><span></span><span>j</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>，后者是线性代数里的经典病态矩阵，列几乎共线。度量标准是 <span><span>∥QTQ−I∥\lVert Q^TQ - I \rVert</span><span><span><span></span><span>∥</span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>∥</span></span></span></span>（Frobenius 范数）：如果 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 真的标准正交，它应该在 <span><span>10−1510^{-15}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 左右。（拿 Hilbert 矩阵折磨 Gram-Schmidt，有点不厚道，但病态矩阵本来就是干这个用的。）</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>classical</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""经典 Gram-Schmidt：用原来的 a_j 去减每个 q_i 上的分量。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>zeros_like</span><span>(A)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>1</span><span>]):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> A[:, j].</span><span>copy</span><span>()</span></div></div><div><div><div>9</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(j):</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>v </span><span>-=</span><span> (Q[:, i] </span><span>@</span><span> A[:, j]) </span><span>*</span><span> Q[:, i]</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>Q[:, j] </span><span>=</span><span> v </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(v)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> Q</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>def</span><span> </span><span>modified</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>16</div></div><div><span>    </span><span>"""修正 Gram-Schmidt：每减掉一个方向，就用减过的 v 继续。"""</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>zeros_like</span><span>(A)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>1</span><span>]):</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> A[:, j].</span><span>copy</span><span>()</span></div></div><div><div><div>20</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(j):</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>v </span><span>-=</span><span> (Q[:, i] </span><span>@</span><span> v) </span><span>*</span><span> Q[:, i]</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>Q[:, j] </span><span>=</span><span> v </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(v)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span> Q</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>12</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>mats </span><span>=</span><span> {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"random 12x12"</span><span><span>: rng.</span><span>standard_normal</span><span>((n, n)),</span></span></div></div><div><div><div>31</div></div><div><span>        </span><span>"Hilbert 8x8"</span><span>: </span><span>1.0</span><span><span> </span><span>/</span><span> (np.</span><span>arange</span><span>(</span></span><span>8</span><span>)[:, </span><span>None</span><span><span>] </span><span>+</span><span> np.</span><span>arange</span><span>(</span></span><span>8</span><span><span>) </span><span>+</span><span> </span></span><span>1.0</span><span>),</span></div></div><div><div><div>32</div></div><div><span>        </span><span>"Hilbert 12x12"</span><span>: </span><span>1.0</span><span><span> </span><span>/</span><span> (np.</span><span>arange</span><span>(n)[:, </span></span><span>None</span><span><span>] </span><span>+</span><span> np.</span><span>arange</span><span>(n) </span><span>+</span><span> </span></span><span>1.0</span><span>),</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>header </span><span>=</span><span> [</span><span>"matrix"</span><span>, </span><span>"cond"</span><span>, </span><span>"classical"</span><span>, </span><span>"modified"</span><span>, </span><span>"np QR"</span><span>]</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>header[</span><span>0</span><span>]</span><span>:&lt;16</span><span>}</span><span>"</span><span><span> </span><span>+</span><span> </span></span><span>""</span><span><span>.</span><span>join</span><span>(</span></span><span>f</span><span>"</span><span>{</span><span>h</span><span>:&gt;12</span><span>}</span><span>"</span><span> </span><span>for</span><span> h </span><span>in</span><span> header[</span><span>1</span><span>:]))</span></div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> name, A </span><span>in</span><span><span> mats.</span><span>items</span><span>():</span></span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> A.shape[</span><span>1</span><span>]</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(k)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>errs </span><span>=</span><span> []</span></div></div><div><div><div>40</div></div><div><span>        </span><span>for</span><span> Q </span><span>in</span><span><span> (</span><span>classical</span><span>(A), </span><span>modified</span><span>(A), np.linalg.</span><span>qr</span><span>(A)[</span></span><span>0</span><span>]):</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>errs.</span><span>append</span><span>(np.linalg.</span><span>norm</span><span>(Q.T </span><span>@</span><span> Q </span><span>-</span><span> identity))</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>cond </span><span>=</span><span> np.linalg.</span><span>cond</span><span>(A)</span></div></div><div><div><div>43</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>44</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>name</span><span>:&lt;16</span><span>}{</span><span>cond</span><span>:&gt;10.1e</span><span>}</span><span>"</span></div></div><div><div><div>45</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>errs[</span><span>0</span><span>]</span><span>:&gt;12.1e</span><span>}{</span><span>errs[</span><span>1</span><span>]</span><span>:&gt;12.1e</span><span>}{</span><span>errs[</span><span>2</span><span>]</span><span>:&gt;12.1e</span><span>}</span><span>"</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>matrix                  cond   classical    modified       np QR</span></div></div><div><div><div>2</div></div><div><span>random 12x12       9.5e+01     2.3e-14     5.0e-15     2.1e-15</span></div></div><div><div><div>3</div></div><div><span>Hilbert 8x8        1.5e+10     1.4e+00     3.6e-07     1.5e-15</span></div></div><div><div><div>4</div></div><div><span>Hilbert 12x12      1.8e+16     5.5e+00     3.3e-01     2.1e-15</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>随机的 12 × 12 矩阵条件数只有 95，三种方法都不错：经典 <span><span>2.3×10−142.3 \times 10^{-14}</span><span><span><span></span><span>2.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>，修正 <span><span>5.0×10−155.0 \times 10^{-15}</span><span><span><span></span><span>5.0</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，<code>np.linalg.qr</code> <span><span>2.1×10−152.1 \times 10^{-15}</span><span><span><span></span><span>2.1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。</li>
<li>Hilbert 8 × 8，条件数 <span><span>1.5×10101.5 \times 10^{10}</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>：经典版 1.4，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 已经不是正交矩阵；修正版 <span><span>3.6×10−73.6 \times 10^{-7}</span><span><span><span></span><span>3.6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，差了约 <span><span>4×1064 \times 10^6</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 倍；<code>np.linalg.qr</code> 仍然是 <span><span>1.5×10−151.5 \times 10^{-15}</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，比经典版小了 15 个数量级。</li>
<li>Hilbert 12 × 12，条件数 <span><span>1.8×10161.8 \times 10^{16}</span><span><span><span></span><span>1.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，超过了 <span><span>1/ε≈4.5×10151/\varepsilon \approx 4.5 \times 10^{15}</span><span><span><span></span><span>1/</span><span>ε</span><span></span><span>≈</span><span></span></span><span><span></span><span>4.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>：经典 5.5，修正 0.33，<code>np.linalg.qr</code> 还是 <span><span>2.1×10−152.1 \times 10^{-15}</span><span><span><span></span><span>2.1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。修正版也败了，只是败得没那么惨。</li>
</ul><p>据我所知，<code>np.linalg.qr</code> 底层调用的是 LAPACK 的 Householder 反射，它不是在“扣投影”，而是把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 逐列乘上一连串的正交矩阵变成上三角，每一步本身就是正交变换，所以 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的正交性不依赖于 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的条件数。我没有读 LAPACK 源码，这个解释来自教科书。读到这里你大概能猜到数值线性代数里为什么很少有人直接用 Gram-Schmidt 去分解大矩阵，但它依然是理解 <span><span>A=QRA = QR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>QR</span></span></span></span> 的最短路径：<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的每个元素是什么、为什么是上三角，Gram-Schmidt 一眼就能看出来。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>旋转矩阵是正交矩阵，而且行列式是 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>。第 4 篇卡尔曼滤波里的姿态表示就是这样（见 <a href="/posts/kalman-filter-04-attitude-imu/">姿态的数学：旋转、四元数与 IMU 仿真器</a> 里的 SO(3) 一节）：机体系到世界系的 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 满足 <span><span>CTC=IC^TC = I</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，所以反过来转换只需转置，旋转之后向量的长度不变。这些在上面验证过，下面的脚本再用两个旋转的乘积确认一次。</p><p>麻烦出在积分上：姿态是一次一次乘上去的小旋转，每乘一次，舍入误差就会让 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 离正交更远一点。我用 <code>float32</code> 把一个很小的旋转连乘 100000 次，<span><span>∥CTC−I∥\lVert C^TC - I \rVert</span><span><span><span></span><span>∥</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>∥</span></span></span></span> 涨到 <span><span>6.78×10−36.78 \times 10^{-3}</span><span><span><span></span><span>6.78</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，“逆就是转置”已经不再严格成立。补救办法是对 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 做一次 QR，取 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>（再按 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 对角线的符号对齐，让行列式保持 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>），拉回之后的正交误差是 <span><span>4.26×10−164.26 \times 10^{-16}</span><span><span><span></span><span>4.26</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，代价是姿态被改了 <span><span>3.45×10−33.45 \times 10^{-3}</span><span><span><span></span><span>3.45</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>（Frobenius 范数）。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rot_z</span><span>(</span><span>angle</span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>c, s </span><span>=</span><span> np.</span><span>cos</span><span>(angle), np.</span><span>sin</span><span>(angle)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([[c, </span><span>-</span><span>s, </span></span><span>0.0</span><span>], [s, c, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>rot_y</span><span>(</span><span>angle</span><span>:</span><span> </span><span>float</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>c, s </span><span>=</span><span> np.</span><span>cos</span><span>(angle), np.</span><span>sin</span><span>(angle)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([[c, </span></span><span>0.0</span><span>, s], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span><span>s, </span></span><span>0.0</span><span>, c]])</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> </span><span>rot_z</span><span>(</span><span>0.3</span><span><span>) </span><span>@</span><span> </span><span>rot_y</span><span>(</span><span>-</span></span><span>0.5</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"C^T C = I："</span><span><span>, np.</span><span>allclose</span><span>(C.T </span><span>@</span><span> C, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"det C ="</span><span>, </span><span>round</span><span>(</span><span>float</span><span><span>(np.linalg.</span><span>det</span><span>(C)), </span></span><span>6</span><span>))</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>len_v, len_Cv </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(v), np.linalg.</span><span>norm</span><span>(C </span><span>@</span><span> v)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"|v| = </span><span>{</span><span>len_v</span><span>:.6f</span><span>}</span><span>，|C v| = </span><span>{</span><span>len_Cv</span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 每一步乘一个带舍入误差的小旋转，累积 100000 次，看正交性漂移</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>step </span><span>=</span><span> </span><span>rot_z</span><span>(</span><span>0.001</span><span><span>) </span><span>@</span><span> </span><span>rot_y</span><span>(</span></span><span>0.0007</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>C32 </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>, </span><span>dtype</span><span><span>=</span><span>np.float32)</span></span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>step32 </span><span>=</span><span> step.</span><span>astype</span><span>(np.float32)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>100_000</span><span>):</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>C32 </span><span>=</span><span> C32 </span><span>@</span><span> step32</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>I3 </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>drift </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(C32.T.</span><span>astype</span><span>(np.float64) </span><span>@</span><span> C32 </span><span>-</span><span> I3)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"float32 连乘 100000 次后 |C^T C - I| = </span><span>{</span><span>drift</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 用 QR 把它拉回最近的正交矩阵（符号对齐，保持行列式为正）</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>Q, R </span><span>=</span><span> np.linalg.</span><span>qr</span><span>(C32.</span><span>astype</span><span>(np.float64))</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> Q </span><span>*</span><span> np.</span><span>sign</span><span>(np.</span><span>diag</span><span>(R))</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"QR 之后 |Q^T Q - I| = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q.T </span><span>@</span><span> Q </span><span>-</span><span> I3)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"拉回的距离 |Q - C32| = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q </span><span>-</span><span> C32)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"det Q ="</span><span>, </span><span>round</span><span>(</span><span>float</span><span><span>(np.linalg.</span><span>det</span><span>(Q)), </span></span><span>6</span><span>))</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>C^T C = I： True</span></div></div><div><div><div>2</div></div><div><span>det C = 1.0</span></div></div><div><div><div>3</div></div><div><span>|v| = 3.741657，|C v| = 3.741657</span></div></div><div><div><div>4</div></div><div><span>float32 连乘 100000 次后 |C^T C - I| = 6.78e-03</span></div></div><div><div><div>5</div></div><div><span>QR 之后 |Q^T Q - I| = 4.26e-16</span></div></div><div><div><div>6</div></div><div><span>拉回的距离 |Q - C32| = 3.45e-03</span></div></div><div><div><div>7</div></div><div><span>det Q = 1.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>严格地说，QR 给出的是一个正交矩阵，不一定是离 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 最近的那个，最近的要用后面几讲的 SVD 来求。（我没有拿它和别的重正交化方案做过比较，实际工程里怎么选我不确定。）另外，那一篇的 TRIAD 把两个方向观测变成正交标架，用的是叉积：<span><span>t1=v1\mathbf{t}_1 = \mathbf{v}_1</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>t2∝v1×v2\mathbf{t}_2 \propto \mathbf{v}_1 \times \mathbf{v}_2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∝</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>t3=t1×t2\mathbf{t}_3 = \mathbf{t}_1 \times \mathbf{t}_2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。三维里叉积是 Gram-Schmidt 的一个捷径，它直接给出“垂直于前两个”的方向，不用减投影。</p><p>投影、<span><span>A=QRA = QR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>QR</span></span></span></span> 这些都是在“垂直”这个概念上打转，正交这一章到此为止。下一讲换一个词：行列式。一个方阵有没有逆，能不能用一个数回答？见 <a href="/posts/linear-algebra-18-determinant-properties/">18.06 第 18 讲：行列式的性质</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-16-least-squares/</id>
      <title type="text">18.06 第 16 讲：投影矩阵与最小二乘</title>
      <published>2026-10-18T05:00:00.000Z</published>
      <updated>2026-10-18T05:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-16-least-squares/"/>
      <summary type="text">MIT 18.06 第 16 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 (1, 1)、(2, 2)、(3, 2) 三个点的直线拟合解了一遍，正规方程给出 C = 2/3、D = 1/2，误差向量 (-1/6, 1/3, -1/6) 的平方长度只有 0.1667，而在 t = 0 处多加一个离群点，斜率就从 0.5 翻成 -1.1。</summary>
      <content type="html"><![CDATA[<p>最小二乘就是把“拟合直线”这件事翻译成上一讲的投影：三个点不在同一条直线上，方程组 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 无解，那就把 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 投影到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间里，解最近的那个方程。Strang 的三个点是 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>、<span><span>(2,2)(2, 2)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(3,2)(3, 2)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，我用 NumPy 算出来最佳直线是 <span><span>y=23+12ty = \frac23 + \frac12 t</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>t</span></span></span></span>，误差向量是 <span><span>(−16,13,−16)(-\frac16, \frac13, -\frac16)</span><span><span><span></span><span>(</span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span>，平方长度 0.1667。再往里加一个 <span><span>t=0t = 0</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 处高度为 6 的离群点，斜率直接从 <span><span>+0.5+0.5</span><span><span><span></span><span>+</span><span>0.5</span></span></span></span> 翻成 <span><span>−1.1-1.1</span><span><span><span></span><span>−</span><span>1.1</span></span></span></span>。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=osh80YCg_GM" target="_blank">Lecture 16: Projection matrices and least squares</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-16-projection-matrices-and-least-squares/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §4.3 Least Squares Approximations
前置：<a href="/posts/linear-algebra-15-projections/">第 15 讲：投影到子空间</a>
一句话：无解的 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 的最佳近似解满足正规方程 <span><span>ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，此时误差 <span><span>e=b−Ax^\mathbf{e} = \mathbf{b} - A\hat{\mathbf{x}}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 垂直于 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的整个列空间。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头一段回顾上一讲的投影公式：<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在列空间里时 <span><span>Pb=bP\mathbf{b} = \mathbf{b}</span><span><span><span></span><span>P</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间里时 <span><span>Pb=0P\mathbf{b} = \mathbf{0}</span><span><span><span></span><span>P</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。第一节已经用代码验证过，可以倍速，但“垂直于列空间就是在 <span><span>N(AT)N(A^\mathsf{T})</span><span><span><span></span><span>N</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 里”这句话值得听一遍。</li>
<li>三个点的直线拟合是这一讲的主体，他把正规方程一步步写出来，又用微积分重推一遍，还手算了 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span> 和 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span>。这一段要认真看，尤其是他画的“两幅图”：一幅是平面上的点和直线，一幅是三维空间里的向量和列空间。</li>
<li>中间关于离群点的一小段（统计学家会担心平方放大异常值）本篇用一个例子算了数，看完他的说法再看我的数字就行。</li>
<li>接近结尾的证明（<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列无关，则 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆）值得完整看一遍，是这一章里少有的一个完整证明。最后他预告下一讲的标准正交向量，本篇最后一节顺手给了一个数值预览。</li>
</ul></div></div>
<section><h2>上一讲的收尾：P 到底做了什么<a href="#上一讲的收尾p-到底做了什么"><span>#</span></a></h2><p>上一讲结尾他给了投影矩阵的公式 <span><span>P=A(ATA)−1ATP = A(A^\mathsf{T}A)^{-1}A^\mathsf{T}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。这一讲开头他换了个方式确认它没写错：看两个极端情形。</p><p><strong><span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在列空间里会怎样？</strong> 列空间里的向量都写成 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span>，于是 <span><span>Pb=A(ATA)−1ATAxP\mathbf{b} = A(A^\mathsf{T}A)^{-1}A^\mathsf{T}A\mathbf{x}</span><span><span><span></span><span>P</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span></span></span></span>，中间的 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 与它的逆抵消，剩下 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span>，也就是 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 自己。他提醒“列空间里的向量就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘某个 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>”这件事要记牢，说它一定会出现在期末考试里。</p><p><strong><span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 垂直于列空间会怎样？</strong> 垂直于所有的列，就是在 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间里，即 <span><span>ATb=0A^\mathsf{T}\mathbf{b} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。公式最右边正好是 <span><span>ATbA^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，所以 <span><span>Pb=0P\mathbf{b} = \mathbf{0}</span><span><span><span></span><span>P</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，直接得到零。</p><p>一般的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 是这两种的和：一个分量在列空间里，被 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 原样保留；一个分量垂直于列空间，被 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 消灭。留下的就是 <span><span>p=Pb\mathbf{p} = P\mathbf{b}</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span>b</span></span></span></span>，被消灭的那部分是 <span><span>e=(I−P)b\mathbf{e} = (I - P)\mathbf{b}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span>)</span><span>b</span></span></span></span>。他顺便说明了 <span><span>I−PI - P</span><span><span><span></span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>P</span></span></span></span> 也是投影矩阵，投到垂直空间上：若 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 对称，<span><span>I−PI - P</span><span><span><span></span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>P</span></span></span></span> 对称；若 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>，则 <span><span>(I−P)2=I−P(I - P)^2 = I - P</span><span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>P</span></span></span></span>。</p><p>下面的脚本用第 16 讲的矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>（下一节要用的那个）把这几句话全验证一遍：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> A </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(A.T </span><span>@</span><span> A) </span><span>@</span><span> A.T</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span># 两个极端：b 在列空间里（b = A x），b 垂直于列空间（A^T b = 0）</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>b_in </span><span>=</span><span> A </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>b_perp </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T b_perp ="</span><span><span>, A.T </span><span>@</span><span> b_perp)</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ b_in ="</span><span><span>, P </span><span>@</span><span> b_in, </span></span><span>"，b_in ="</span><span>, b_in)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ b_perp ="</span><span><span>, P </span><span>@</span><span> b_perp)</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 一般的 b：P b 与 (I - P) b 加起来还是 b</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> P </span><span>@</span><span> b</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> (identity </span><span>-</span><span> P) </span><span>@</span><span> b</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"P b ="</span><span>, p)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"(I - P) b ="</span><span>, e)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"p + e = b："</span><span><span>, np.</span><span>allclose</span><span>(p </span><span>+</span><span> e, b))</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># P 是投影，I - P 也是</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> identity </span><span>-</span><span> P</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"P^2 = P："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P))</span></span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"(I - P)^2 = I - P："</span><span><span>, np.</span><span>allclose</span><span>(Q </span><span>@</span><span> Q, Q))</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"I - P 对称："</span><span><span>, np.</span><span>allclose</span><span>(Q, Q.T))</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A^T b_perp = [0. 0.]</span></div></div><div><div><div>2</div></div><div><span>P @ b_in = [ 1.  0. -1.] ，b_in = [ 1.  0. -1.]</span></div></div><div><div><div>3</div></div><div><span>P @ b_perp = [0. 0. 0.]</span></div></div><div><div><div>4</div></div><div><span>P b = [1.1667 1.6667 2.1667]</span></div></div><div><div><div>5</div></div><div><span>(I - P) b = [-0.1667  0.3333 -0.1667]</span></div></div><div><div><div>6</div></div><div><span>p + e = b： True</span></div></div><div><div><div>7</div></div><div><span>P^2 = P： True</span></div></div><div><div><div>8</div></div><div><span>(I - P)^2 = I - P： True</span></div></div><div><div><div>9</div></div><div><span>I - P 对称： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的 <code>b_perp</code> 是我先解 <span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 得到的 <span><span>(1,−2,1)(1, -2, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，它既垂直于 <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，也垂直于 <span><span>(1,2,3)(1, 2, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，所以 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 把它压成了零向量。最后三行都是 <code>True</code>。再看中间两行：<span><span>Pb=(1.1667,1.6667,2.1667)P\mathbf{b} = (1.1667, 1.6667, 2.1667)</span><span><span><span></span><span>P</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1.1667</span><span>,</span><span></span><span>1.6667</span><span>,</span><span></span><span>2.1667</span><span>)</span></span></span></span> 和 <span><span>(I−P)b=(−0.1667,0.3333,−0.1667)(I - P)\mathbf{b} = (-0.1667, 0.3333, -0.1667)</span><span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span>)</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.1667</span><span>,</span><span></span><span>0.3333</span><span>,</span><span></span><span>−</span><span>0.1667</span><span>)</span></span></span></span> 是下一节的主角，先在这里露个脸。</p></section>
<section><h2>三个点的直线：无解方程组的最佳解<a href="#三个点的直线无解方程组的最佳解"><span>#</span></a></h2><p>他选了最不起眼的三个点：<span><span>t=1,2,3t = 1, 2, 3</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span></span></span></span> 处高度分别是 <span><span>1,2,21, 2, 2</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span></span></span></span>。（他自己说“不是特别精彩”，我同意，但手算时数字不会难看。）想找一条直线 <span><span>y=C+Dty = C + Dt</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span>t</span></span></span></span>。如果直线穿过三个点，就要同时满足</p><span><span><span>C+D=1C+2D=2C+3D=2A=[111213], x=[CD], b=[122]\begin{aligned}
C + D &amp;= 1 \\
C + 2D &amp;= 2 \\
C + 3D &amp;= 2
\end{aligned}
\qquad
A = \begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 2 \\ 1 &amp; 3 \end{bmatrix},\
\mathbf{x} = \begin{bmatrix} C \\ D \end{bmatrix},\
\mathbf{b} = \begin{bmatrix} 1 \\ 2 \\ 2 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>C</span><span></span><span>+</span><span></span><span>D</span></span></span><span><span></span><span><span>C</span><span></span><span>+</span><span></span><span>2</span><span>D</span></span></span><span><span></span><span><span>C</span><span></span><span>+</span><span></span><span>3</span><span>D</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>1</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>2</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span> </span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>C</span></span></span><span><span></span><span><span>D</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span> </span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>这是 3 × 2 的矩阵，两列无关，是列空间的一组基，但列空间是三维空间里的一个平面，<span><span>b=(1,2,2)\mathbf{b} = (1, 2, 2)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 不在这个平面里，所以 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 无解，没有直线能同时穿过三个点。</p><p><strong>“最好”是什么意思？</strong> 他的回答是：每个点在竖直方向上离直线有多远，记作 <span><span>e1,e2,e3e_1, e_2, e_3</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，把平方加起来：</p><span><span><span>E=∥Ax−b∥2=(C+D−1)2+(C+2D−2)2+(C+3D−2)2E = \|A\mathbf{x} - \mathbf{b}\|^2 = (C + D - 1)^2 + (C + 2D - 2)^2 + (C + 3D - 2)^2</span><span><span><span></span><span>E</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>C</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>C</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>D</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>C</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span>D</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span></span><p>选使 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 最小的 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 和 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span>。这就是最小二乘，统计里的名字是线性回归。他特别说明了为什么要平方：长度恰好为零当且仅当 <span><span>e=0\mathbf{e} = \mathbf{0}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，而平方让问题变成线性的，后面马上会看到。</p><p><strong>怎么解？</strong> 这就是上一讲的投影：列空间里离 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 最近的点 <span><span>p=Ax^\mathbf{p} = A\hat{\mathbf{x}}</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 对应的 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 满足</p><span><span><span>ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span></span><p>他把它称为正规方程（normal equations），并说这是他敢称之为“统计和估计里最重要的方程”的一个：凡是有噪声、有误差，用几个参数去拟合数据的场合，第一个要用的就是它。</p><p>他手算了 <span><span>ATA=[36614]A^\mathsf{T}A = \begin{bmatrix} 3 &amp; 6 \\ 6 &amp; 14 \end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>14</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 和 <span><span>ATb=(5,11)A^\mathsf{T}\mathbf{b} = (5, 11)</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>5</span><span>,</span><span></span><span>11</span><span>)</span></span></span></span>，消元后 <span><span>2D=12D = 1</span><span><span><span></span><span>2</span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，<span><span>D=12D = \frac12</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，回代得 <span><span>C=23C = \frac23</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。我让 NumPy 把同一条路走一遍，同时用 <code>np.linalg.lstsq</code> 对照，再检查 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span> 和 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> fractions </span><span>import</span><span> Fraction</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>column_stack</span><span>([np.</span><span>ones</span><span>(</span><span>3</span><span>), t])  </span><span># 第一列全 1，第二列是 t</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 正规方程 A^T A x = A^T b</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>AtA </span><span>=</span><span> A.T </span><span>@</span><span> A</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>Atb </span><span>=</span><span> A.T </span><span>@</span><span> b</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A =</span><span>\n</span><span>"</span><span>, AtA)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T b ="</span><span>, Atb)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(AtA, Atb)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"C, D ="</span><span>, x, </span><span>"="</span><span>, [</span><span>str</span><span><span>(</span><span>Fraction</span><span>(v).</span><span>limit_denominator</span><span>(</span></span><span>100</span><span>)) </span><span>for</span><span> v </span><span>in</span><span> x]</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 和 np.linalg.lstsq 对比</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>x_lstsq, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"lstsq 给出同样的解："</span><span><span>, np.</span><span>allclose</span><span>(x, x_lstsq))</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 投影 p = A x 和误差 e = b - p</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> A </span><span>@</span><span> x</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> b </span><span>-</span><span> p</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>sixths </span><span>=</span><span> [</span><span>str</span><span><span>(</span><span>Fraction</span><span>(v).</span><span>limit_denominator</span><span>(</span></span><span>100</span><span>)) </span><span>for</span><span> v </span><span>in</span><span> p]</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"p ="</span><span>, p, </span><span>"="</span><span>, sixths)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"e ="</span><span>, e, </span><span>"="</span><span>, [</span><span>str</span><span><span>(</span><span>Fraction</span><span>(v).</span><span>limit_denominator</span><span>(</span></span><span>100</span><span>)) </span><span>for</span><span> v </span><span>in</span><span> e])</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"平方误差 |e|^2 = </span><span>{</span><span><span>e </span><span>@</span><span> e</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span># e 垂直于 p，也垂直于列空间里的任何向量</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"p . e = </span><span>{</span><span><span>p </span><span>@</span><span> e</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T e ="</span><span><span>, np.</span><span>round</span><span>(A.T </span><span>@</span><span> e, </span></span><span>12</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"e . (1, 1, 1) = </span><span>{</span><span><span>e </span><span>@</span><span> np.</span><span>ones</span><span>(</span></span><span>3</span><span>)</span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"e . (1, 2, 3) = </span><span>{</span><span><span>e </span><span>@</span><span> t</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A^T A =</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[[ 3.  6.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 6. 14.]]</span></div></div><div><div><div>4</div></div><div><span>A^T b = [ 5. 11.]</span></div></div><div><div><div>5</div></div><div><span>C, D = [0.6667 0.5   ] = ['2/3', '1/2']</span></div></div><div><div><div>6</div></div><div><span>lstsq 给出同样的解： True</span></div></div><div><div><div>7</div></div><div><span>p = [1.1667 1.6667 2.1667] = ['7/6', '5/3', '13/6']</span></div></div><div><div><div>8</div></div><div><span>e = [-0.1667  0.3333 -0.1667] = ['-1/6', '1/3', '-1/6']</span></div></div><div><div><div>9</div></div><div><span>平方误差 |e|^2 = 0.1667</span></div></div><div><div><div>10</div></div><div><span>p . e = 7.8e-16</span></div></div><div><div><div>11</div></div><div><span>A^T e = [0. 0.]</span></div></div><div><div><div>12</div></div><div><span>e . (1, 1, 1) = 4.4e-16</span></div></div><div><div><div>13</div></div><div><span>e . (1, 2, 3) = 8.9e-16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>数字和他黑板上的一模一样：<span><span>C^=23\hat{C} = \frac23</span><span><span><span></span><span><span><span><span><span><span></span><span>C</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>、<span><span>D^=12\hat{D} = \frac12</span><span><span><span></span><span><span><span><span><span><span></span><span>D</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，<span><span>p=(76,53,136)\mathbf{p} = (\frac76, \frac53, \frac{13}6)</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>7</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>5</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span>，<span><span>e=(−16,13,−16)\mathbf{e} = (-\frac16, \frac13, -\frac16)</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span>。他在黑板上还犯了个小错：一开始把 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 的符号写反了，后来为了让 <span><span>p+e=b\mathbf{p} + \mathbf{e} = \mathbf{b}</span><span><span><span></span><span>p</span><span></span><span>+</span><span></span></span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 成立才改过来，我这里用 <code>b - p</code> 直接保证了符号。（他手算 <span><span>p⋅e\mathbf{p}\cdot\mathbf{e}</span><span><span><span></span><span>p</span><span></span><span>⋅</span><span></span></span><span><span></span><span>e</span></span></span></span> 的时候吊着一口气，算出零才说了一句“Thank God”，因为 <span><span>−736+2036−1336-\frac{7}{36} + \frac{20}{36} - \frac{13}{36}</span><span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>36</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>7</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>36</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>20</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>36</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>13</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 必须是零；NumPy 不用谢天谢地，只留下 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级的舍入误差。）</p><p>输出里最后四行是整讲的核心：</p><ul>
<li><span><span>p⋅e≈8×10−16\mathbf{p}\cdot\mathbf{e} \approx 8 \times 10^{-16}</span><span><span><span></span><span>p</span><span></span><span>⋅</span><span></span></span><span><span></span><span>e</span><span></span><span>≈</span><span></span></span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，也就是零：误差垂直于投影。</li>
<li><span><span>e⋅(1,1,1)\mathbf{e}\cdot(1, 1, 1)</span><span><span><span></span><span>e</span><span></span><span>⋅</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 与 <span><span>e⋅(1,2,3)\mathbf{e}\cdot(1, 2, 3)</span><span><span><span></span><span>e</span><span></span><span>⋅</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 也都是 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级：不止垂直于 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span>，而是垂直于列空间里每一个向量，因为这两个向量恰好是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的两列。</li>
<li><code>A^T e</code> 就是 <span><span>(0,0)(0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。把这一行展开看：<span><span>AT(b−Ax^)=0A^\mathsf{T}(\mathbf{b} - A\hat{\mathbf{x}}) = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，移项就是正规方程。</li>
</ul><p>所以正规方程不过是一句话的代数形式：误差向量要垂直于列空间。他强调的“两幅图”也是这句话的两个侧面：平面上那幅图里，<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 和 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 决定直线，<span><span>eie_i</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是每个点到直线的竖直距离；三维空间那幅图里，<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 和 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是两列的组合系数，<span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span> 是组合出来的点，<span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 是垂线。两幅图画的是同一件事。</p><p></p><figure><img src="/assets/images/linear-algebra/l16-fit-line.svg" alt="左：三个点的最佳直线 y = 2/3 + t/2 与竖直误差。右：在 t = 0 处加一个离群点后，最佳直线从虚线变成实线" /><figcaption>左：三个点的最佳直线 y = 2/3 + t/2 与竖直误差。右：在 t = 0 处加一个离群点后，最佳直线从虚线变成实线</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>column_stack</span><span>([np.</span><span>ones</span><span>(</span><span>3</span><span>), t])</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A.T </span><span>@</span><span> A, A.T </span><span>@</span><span> b)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> A </span><span>@</span><span> x</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>t4 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>b4 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>6.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>A4 </span><span>=</span><span> np.</span><span>column_stack</span><span>([np.</span><span>ones</span><span>(</span><span>4</span><span>), t4])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>x4, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A4, b4, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>fig, (ax1, ax2) </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>ts </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>-</span><span>0.2</span><span>, </span><span>3.4</span><span>, </span><span>50</span><span>)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(ts, x[</span><span>0</span><span><span>] </span><span>+</span><span> x[</span></span><span>1</span><span><span>] </span><span>*</span><span> ts, </span></span><span>color</span><span>=</span><span>"C0"</span><span>, </span><span>label</span><span>=</span><span>"y = 2/3 + t/2"</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> ti, bi, pi </span><span>in</span><span> </span><span>zip</span><span>(t, b, p):</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>ax1.</span><span>plot</span><span>([ti, ti], [bi, pi], </span><span>color</span><span>=</span><span>"C3"</span><span>, </span><span>lw</span><span>=</span><span>2</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>ax1.</span><span>scatter</span><span>(t, b, </span><span>color</span><span>=</span><span>"k"</span><span>, </span><span>zorder</span><span>=</span><span>3</span><span>, </span><span>label</span><span>=</span><span>"data b"</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>ax1.</span><span>scatter</span><span>(t, p, </span><span>color</span><span>=</span><span>"C0"</span><span>, </span><span>zorder</span><span>=</span><span>3</span><span>, </span><span>label</span><span>=</span><span>"projection p"</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_title</span><span>(</span><span>"Best line and errors e = (-1/6, 1/3, -1/6)"</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlabel</span><span>(</span><span>"t"</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylabel</span><span>(</span><span>"y"</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>ax1.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"upper left"</span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(ts, x[</span><span>0</span><span><span>] </span><span>+</span><span> x[</span></span><span>1</span><span><span>] </span><span>*</span><span> ts, </span></span><span>color</span><span>=</span><span>"C0"</span><span>, </span><span>ls</span><span>=</span><span>"--"</span><span>, </span><span>label</span><span>=</span><span>"three points"</span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(ts, x4[</span><span>0</span><span><span>] </span><span>+</span><span> x4[</span></span><span>1</span><span><span>] </span><span>*</span><span> ts, </span></span><span>color</span><span>=</span><span>"C1"</span><span>, </span><span>label</span><span>=</span><span>"with outlier at t = 0"</span><span>)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>ax2.</span><span>scatter</span><span>(t4, b4, </span><span>color</span><span>=</span><span>"k"</span><span>, </span><span>zorder</span><span>=</span><span>3</span><span>)</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_title</span><span>(</span><span>"One outlier turns the slope from 0.5 to -1.1"</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlabel</span><span>(</span><span>"t"</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylabel</span><span>(</span><span>"y"</span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>ax2.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"upper right"</span><span>)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>44</div></div><div><span>        </span><span>"l16-fit-line.svg"</span><span>,</span></div></div><div><div><div>45</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>左图里红色的短线就是 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 的三个分量，<span><span>−16-\frac16</span><span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>、<span><span>13\frac13</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>、<span><span>−16-\frac16</span><span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>6</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 说明数据点在直线的下方、上方、下方。右图留到下一节看。</p></section>
<section><h2>从微积分走一遍，再看离群点<a href="#从微积分走一遍再看离群点"><span>#</span></a></h2><p>他随后说想用微积分把正规方程再推一遍：<span><span>E(C,D)E(C, D)</span><span><span><span></span><span>E</span><span>(</span><span>C</span><span>,</span><span></span><span>D</span><span>)</span></span></span></span> 是关于两个变量的函数，对 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 和 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 分别求偏导并令其为零，得到的是同样的两个线性方程。我顺手把它数值化：<span><span>E=∥Ax−b∥2E = \|A\mathbf{x} - \mathbf{b}\|^2</span><span><span><span></span><span>E</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的梯度是 <span><span>2AT(Ax−b)2A^\mathsf{T}(A\mathbf{x} - \mathbf{b})</span><span><span><span></span><span>2</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span></span></span></span>，在 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 处该是零。再用固定种子在 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 周围随机扰动 1000 次，看有没有比它更好的点。</p><p>最后，他还提到了统计学家对平方的警惕：假如在 <span><span>t=0t = 0</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 处多一个特别离谱的测量，平方会把它的误差放大到别的点“看不见”的程度，这样的点叫离群点（outlier）。他没算数，我算了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>squared_error</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> A </span><span>@</span><span> x </span><span>-</span><span> b</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(e </span><span>@</span><span> e)</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>column_stack</span><span>([np.</span><span>ones</span><span>(</span><span>3</span><span>), t])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A.T </span><span>@</span><span> A, A.T </span><span>@</span><span> b)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 微积分：E(C, D) 的两个偏导数在 (C, D) = x 处都是 0</span></div></div><div><div><div>18</div></div><div><span>    </span><span># E = |A x - b|^2 的梯度是 2 A^T (A x - b)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"梯度 ="</span><span><span>, np.</span><span>round</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> A.T </span><span>@</span><span> (A </span><span>@</span><span> x </span><span>-</span><span> b), </span></span><span>12</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 在 x 周围随机扰动 1000 次，没有一次比 x 更好</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>E_best </span><span>=</span><span> </span><span>squared_error</span><span>(A, b, x)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>worse </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>24</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>1000</span><span>):</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>trial </span><span>=</span><span> x </span><span>+</span><span> </span><span>0.1</span><span><span> </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(</span></span><span>2</span><span>)</span></div></div><div><div><div>26</div></div><div><span>        </span><span>if</span><span><span> </span><span>squared_error</span><span>(A, b, trial) </span><span>&gt;=</span><span> E_best:</span></span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>worse </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"E(x) = </span><span>{</span><span>E_best</span><span>:.4f</span><span>}</span><span>，扰动后更差的次数：</span><span>{</span><span>worse</span><span>}</span><span> / 1000"</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 离群点：在 t = 0 处加一个高度 6 的点</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>t4 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>b4 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>6.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>A4 </span><span>=</span><span> np.</span><span>column_stack</span><span>([np.</span><span>ones</span><span>(</span><span>4</span><span>), t4])</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>x4, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A4, b4, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"三个点的直线：C, D ="</span><span>, x)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"加离群点之后：C, D ="</span><span>, x4)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"斜率 D 从"</span><span>, </span><span>round</span><span>(x[</span><span>1</span><span>], </span><span>4</span><span>), </span><span>"变成"</span><span>, </span><span>round</span><span>(x4[</span><span>1</span><span>], </span><span>4</span><span>))</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>梯度 = [0. 0.]</span></div></div><div><div><div>2</div></div><div><span>E(x) = 0.1667，扰动后更差的次数：1000 / 1000</span></div></div><div><div><div>3</div></div><div><span>三个点的直线：C, D = [0.6667 0.5   ]</span></div></div><div><div><div>4</div></div><div><span>加离群点之后：C, D = [ 4.4 -1.1]</span></div></div><div><div><div>5</div></div><div><span>斜率 D 从 0.5 变成 -1.1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>梯度是 <span><span>(0,0)(0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，随机扰动 1000 次，没有一次比 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 的平方误差 0.1667 更小：微积分和线性代数给出的是同一个极小点，这也不奇怪，因为 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 是二次函数，只有一个谷底。</p><p>离群点那一行才是惊人的。原本三个点的斜率是 <span><span>+0.5+0.5</span><span><span><span></span><span>+</span><span>0.5</span></span></span></span>，只多一个高度为 6 的点，最小二乘就把斜率扳成了 <span><span>−1.1-1.1</span><span><span><span></span><span>−</span><span>1.1</span></span></span></span>，截距从 0.6667 跳到 4.4：直线整个翻了个方向，穿过了原先三个点之外的地方。右图里实线（加了离群点）和虚线（原来三个点）对比最直观。我猜这就是他说“最小二乘最常用，但不是唯一用”的原因：平方对远处的点太宽容了，所以统计学家看见离群点会先去查是不是测量出了错。</p></section>
<section><h2><span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 为什么可逆<a href="#ataamathsftaata-为什么可逆"><span>#</span></a></h2><p>接近结尾的时候他补上了前面一直在用的一个事实：如果 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列无关，<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 就是可逆的。他说自己不爱在这门课里堆证明，但这个是“核心事实”，用到了前面学过的所有东西。</p><p><strong>证明的思路是什么？</strong> 要证明 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆，只要证明它的零空间里只有零向量。于是假设 <span><span>ATAx=0A^\mathsf{T}A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，两边左乘 <span><span>xT\mathbf{x}^\mathsf{T}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（他把这叫作“brilliant idea”）：</p><span><span><span>xTATAx=(Ax)T(Ax)=∥Ax∥2=0\mathbf{x}^\mathsf{T}A^\mathsf{T}A\mathbf{x} = (A\mathbf{x})^\mathsf{T}(A\mathbf{x}) = \|A\mathbf{x}\|^2 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>A</span><span>x</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>长度的平方是零，所以 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。现在才用上假设：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列无关，意味着 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 只有 <span><span>x=0\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 一个解。于是 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的零空间里只有零向量，它可逆。</p><p>他还点了 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的性质：对称，（在列无关时）可逆，课程后面还会说它是正定的。我用代码把对称性、关键等式 <span><span>xTATAx=∥Ax∥2\mathbf{x}^\mathsf{T}A^\mathsf{T}A\mathbf{x} = \|A\mathbf{x}\|^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 数了一遍，并对照一个列相关的反例：给 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 添上第三列，等于第一列加第二列。结尾顺带做他预告的一件事：标准正交的列，<span><span>QTQ=IQ^\mathsf{T}Q = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 列无关：A^T A 可逆</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> A.T </span><span>@</span><span> A</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"A 的秩 ="</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(A),</span></div></div><div><div><div>14</div></div><div><span>        </span><span>"，det(A^T A) ="</span><span>,</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>det</span><span>(G),</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A 对称："</span><span><span>, np.</span><span>allclose</span><span>(G, G.T))</span></span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 证明里的关键一步：x^T A^T A x = |A x|^2</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"x^T A^T A x = </span><span>{</span><span><span>x </span><span>@</span><span> G </span><span>@</span><span> x</span></span><span>:.6f</span><span>}</span><span>，|A x|^2 = </span><span>{</span><span><span>(A </span><span>@</span><span> x) </span><span>@</span><span> (A </span><span>@</span><span> x)</span></span><span>:.6f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 列相关：第三列 = 第一列 + 第二列，A^T A 奇异</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>column_stack</span><span>([A, A[:, </span><span>0</span><span><span>] </span><span>+</span><span> A[:, </span></span><span>1</span><span>]])</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>H </span><span>=</span><span> B.T </span><span>@</span><span> B</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"B 的秩 ="</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(B),</span></div></div><div><div><div>29</div></div><div><span>        </span><span>"，H 的秩 ="</span><span>,</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(H),</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])  </span><span># B v = 0</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"B v ="</span><span><span>, B </span><span>@</span><span> v, </span></span><span>"，H v ="</span><span><span>, H </span><span>@</span><span> v)</span></span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 预告：标准正交的列，Q^T Q = I</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> </span><span>0.6</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>[[np.</span><span>cos</span><span>(theta), </span><span>-</span><span>np.</span><span>sin</span><span>(theta)], [np.</span><span>sin</span><span>(theta), np.</span><span>cos</span><span>(theta)]]</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"Q^T Q =</span><span>\n</span><span>"</span><span><span>, Q.T </span><span>@</span><span> Q)</span></span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 的秩 = 2 ，det(A^T A) = 6.0</span></div></div><div><div><div>2</div></div><div><span>A^T A 对称： True</span></div></div><div><div><div>3</div></div><div><span>x^T A^T A x = 0.231946，|A x|^2 = 0.231946</span></div></div><div><div><div>4</div></div><div><span>B 的秩 = 2 ，H 的秩 = 2</span></div></div><div><div><div>5</div></div><div><span>B v = [0. 0. 0.] ，H v = [0. 0. 0.]</span></div></div><div><div><div>6</div></div><div><span>Q^T Q =</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[[1. 0.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[0. 1.]]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的秩是 2（列无关），<span><span>det⁡(ATA)=6\det(A^\mathsf{T}A) = 6</span><span><span><span></span><span>det</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span>，不是零，所以可逆：这正好是 Strang 前面手算时那个 <span><span>[36614]\begin{bmatrix} 3 &amp; 6 \\ 6 &amp; 14 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>14</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>（<span><span>3×14−6×6=63 \times 14 - 6 \times 6 = 6</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>14</span><span></span><span>−</span><span></span></span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span>）。</li>
<li>等式 <span><span>xTATAx=∥Ax∥2\mathbf{x}^\mathsf{T}A^\mathsf{T}A\mathbf{x} = \|A\mathbf{x}\|^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 在随机的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 上两边都是 0.231946。</li>
<li>反例里 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第三列是前两列的和，所以 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的秩是 2 而不是 3，<span><span>Bv=0B\mathbf{v} = \mathbf{0}</span><span><span><span></span><span>B</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>（<span><span>v=(1,1,−1)\mathbf{v} = (1, 1, -1)</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>）。<span><span>H=BTBH = B^\mathsf{T}B</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span></span></span></span> 是 3 × 3，秩是 2，<span><span>Hv=0H\mathbf{v} = \mathbf{0}</span><span><span><span></span><span>H</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的零空间向量也是 <span><span>BTBB^\mathsf{T}B</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span></span></span></span> 的零空间向量，<span><span>BTBB^\mathsf{T}B</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>B</span></span></span></span> 不可逆，正规方程没法直接解（需要伪逆，第 33 讲）。</li>
<li>最后他预告的 <span><span>QTQ=IQ^\mathsf{T}Q = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>：旋转矩阵 <span><span>[cos⁡θ−sin⁡θsin⁡θcos⁡θ]\begin{bmatrix} \cos\theta &amp; -\sin\theta \\ \sin\theta &amp; \cos\theta \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span><span><span></span><span><span>sin</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span></span><span>sin</span><span></span><span>θ</span></span></span><span><span></span><span><span>cos</span><span></span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的两列是互相垂直的单位向量，<span><span>QTQQ^\mathsf{T}Q</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span></span></span></span> 是单位矩阵。于是正规方程里的 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 消失，<span><span>x^=QTb\hat{\mathbf{x}} = Q^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，什么都不用解。</li>
</ul></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>最小二乘在控制里最常见的出场是辨识。比如我想知道一个一阶系统 <span><span>y[k+1]=a y[k]+b u[k]y[k+1] = a\,y[k] + b\,u[k]</span><span><span><span></span><span>y</span><span>[</span><span>k</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span>y</span><span>[</span><span>k</span><span>]</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>u</span><span>[</span><span>k</span><span>]</span></span></span></span> 的两个参数：把实验里采到的 <span><span>y[k]y[k]</span><span><span><span></span><span>y</span><span>[</span><span>k</span><span>]</span></span></span></span>、<span><span>u[k]u[k]</span><span><span><span></span><span>u</span><span>[</span><span>k</span><span>]</span></span></span></span>、<span><span>y[k+1]y[k+1]</span><span><span><span></span><span>y</span><span>[</span><span>k</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>]</span></span></span></span> 逐行排成 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>，其中 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的每一行是 <span><span>(y[k],u[k])(y[k], u[k])</span><span><span><span></span><span>(</span><span>y</span><span>[</span><span>k</span><span>]</span><span>,</span><span></span><span>u</span><span>[</span><span>k</span><span>])</span></span></span></span>，<span><span>x=(a,b)\mathbf{x} = (a, b)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>a</span><span>,</span><span></span><span>b</span><span>)</span></span></span></span>，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 是 <span><span>y[k+1]y[k+1]</span><span><span><span></span><span>y</span><span>[</span><span>k</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>]</span></span></span></span> 那一列。数据比未知数多得多，方程无解，于是解 <span><span>ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>。这和今天拟合三个点是同一个模板，只不过 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列从 <span><span>(1,t)(1, t)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>t</span><span>)</span></span></span></span> 换成了 <span><span>(y[k],u[k])(y[k], u[k])</span><span><span><span></span><span>(</span><span>y</span><span>[</span><span>k</span><span>]</span><span>,</span><span></span><span>u</span><span>[</span><span>k</span><span>])</span></span></span></span>。</p><p>传感器标定是更朴素的版本：对一个传感器喂几组已知的输入，记录读数，拟合 <span><span>读数=C+D 真值\text{读数} = C + D\,\text{真值}</span><span><span><span></span><span><span>读数</span></span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span></span><span><span>真值</span></span></span></span></span>。<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 是零偏，<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是增益，和 <span><span>C+DtC + Dt</span><span><span><span></span><span>C</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span>t</span></span></span></span> 完全对应。离群点那一节在这里也不是纯理论：偶尔有一个读数因为干扰跳出去，平方会让它主导整条拟合线，所以标定前最好先看一眼残差，把明显不对的点挑出来。</p><p>还有一个得留意的点是 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的可逆性：它要求 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列无关，在辨识里就是说激励信号得“足够丰富”，如果 <span><span>u[k]u[k]</span><span><span><span></span><span>u</span><span>[</span><span>k</span><span>]</span></span></span></span> 永远是常数且和 <span><span>y[k]y[k]</span><span><span><span></span><span>y</span><span>[</span><span>k</span><span>]</span></span></span></span> 成比例，两列相关，<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 奇异，参数根本辨识不出来。最后一节里那个列相关的反例就是这种情形的缩影。</p><p>下一讲他要回答今天结尾留下的问题：怎么挑出最好的基，让 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 变成单位矩阵。答案是标准正交向量，以及把任意一组基变成标准正交基的 Gram-Schmidt，见 <a href="/posts/linear-algebra-17-orthogonal-matrices-gram-schmidt/">18.06 第 17 讲：正交矩阵与 Gram-Schmidt</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-15-projections/</id>
      <title type="text">18.06 第 15 讲：投影到子空间</title>
      <published>2026-10-18T04:40:00.000Z</published>
      <updated>2026-10-18T04:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-15-projections/"/>
      <summary type="text">MIT 18.06 第 15 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 b 投影到一条直线和一个二维列空间上，验证 P² = P 与 Pᵀ = P，再在平面里随机撒 10 万个点，离 b 最近的也有 0.4085，比投影点的 0.4082 远。</summary>
      <content type="html"><![CDATA[<p>这一讲的核心是一个很朴素的问题：<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 没有解的时候，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间里离 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 最近的点在哪里。Strang 的答案是，找到那个点 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span>，使得误差 <span><span>e=b−p\mathbf{e} = \mathbf{b} - \mathbf{p}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>p</span></span></span></span> 垂直于列空间，由此得到 <span><span>ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>。我在他的三点例子上算了一遍：投影点到 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 的距离是 0.4082，我在平面里随机撒了 10 万个点，离 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 最近的一个也有 0.4085，没有一个能赢过它。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=Y_Ac6KiQ1t0" target="_blank">Lecture 15: Projections onto subspaces</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-15-projections-onto-subspaces/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §4.2 Projections onto Lines and Subspaces
前置：<a href="/posts/linear-algebra-14-orthogonal-subspaces/">第 14 讲：正交向量与正交子空间</a>
一句话：投影点 <span><span>p=Ax^\mathbf{p} = A\hat{\mathbf{x}}</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 由“误差垂直于列空间”这一个条件决定，投影矩阵是 <span><span>P=A(ATA)−1ATP = A(A^\mathsf{T}A)^{-1}A^\mathsf{T}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，它对称，而且 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>他先花了一整块黑板只讲投影到一条直线上，这一段是全讲的骨架，值得认真看。他问“<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 翻倍怎么样，<span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 翻倍怎么样”的那几分钟也有用，本篇用代码算了一遍，看完可以对照。</li>
<li>“<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 是对称的，<span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>”那一段要认真听。他说这两条性质合起来就是投影矩阵的全部身份。</li>
<li>他想把 <span><span>(ATA)−1(A^\mathsf{T}A)^{-1}</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 拆成两个逆的乘积、又自己叫停的那一段很短，本篇有一个小实验，看一眼即可。</li>
<li>最后几分钟是三个点的拟合问题，他只列出方程，没有解。数值例子在下一讲，本篇只在最后用代码做了一次预演。</li>
</ul></div></div>
<section><h2>先投影到一条直线上<a href="#先投影到一条直线上"><span>#</span></a></h2><p><strong>最近的点在哪里？</strong> Strang 从二维开始：一条过原点的直线，方向是 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span>；直线外有一个向量 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>。直线上离 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 最近的点记作 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span>，连接 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 与 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span> 的误差向量 <span><span>e=b−p\mathbf{e} = \mathbf{b} - \mathbf{p}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>p</span></span></span></span> 就是垂线。他提了一句，如果用三角函数，会出现 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>、<span><span>sin⁡θ\sin\theta</span><span><span><span></span><span>sin</span><span></span><span>θ</span></span></span></span>、<span><span>cos⁡θ\cos\theta</span><span><span><span></span><span>cos</span><span></span><span>θ</span></span></span></span>，公式很难看；线性代数能让这些都不出现。</p><p>条件只有两个：<span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span> 在直线上，所以 <span><span>p=xa\mathbf{p} = x\mathbf{a}</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span>a</span></span></span></span>，<span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 是个待求的数；误差垂直于 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span>，所以</p><span><span><span>aT(b−xa)=0⟹x aTa=aTb⟹x=aTbaTa\mathbf{a}^\mathsf{T}(\mathbf{b} - x\mathbf{a}) = 0
\quad\Longrightarrow\quad
x\,\mathbf{a}^\mathsf{T}\mathbf{a} = \mathbf{a}^\mathsf{T}\mathbf{b}
\quad\Longrightarrow\quad
x = \frac{\mathbf{a}^\mathsf{T}\mathbf{b}}{\mathbf{a}^\mathsf{T}\mathbf{a}}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>x</span><span>a</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span>x</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><span><span>aTa\mathbf{a}^\mathsf{T}\mathbf{a}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span></span> 是一个数（<span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 的长度平方），可以直接除。于是投影是</p><span><span><span>p=a aTbaTa=aaTaTa⏟P b\mathbf{p} = \mathbf{a}\,\frac{\mathbf{a}^\mathsf{T}\mathbf{b}}{\mathbf{a}^\mathsf{T}\mathbf{a}}
= \underbrace{\frac{\mathbf{a}\mathbf{a}^\mathsf{T}}{\mathbf{a}^\mathsf{T}\mathbf{a}}}_{P}\,\mathbf{b}</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>P</span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span></span><span></span><span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span>b</span></span></span></span></span><p>他强调的是括号放在哪里：把数 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 放在 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 右边，括号换个位置，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 前面站着的就是一个矩阵。分子 <span><span>aaT\mathbf{a}\mathbf{a}^\mathsf{T}</span><span><span><span></span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是列乘行，是一个 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵，分母仍然是一个数。我用他的“字母”配一组数字：<span><span>a=(2,1)\mathbf{a} = (2, 1)</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>b=(1,2)\mathbf{b} = (1, 2)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># 一维：x = aᵀb / aᵀa，p = a x，误差 e = b - p 与 a 垂直</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> (a </span><span>@</span><span> b) </span><span>/</span><span> (a </span><span>@</span><span> a)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> a </span><span>*</span><span> x</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> b </span><span>-</span><span> p</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"x ="</span><span>, x, </span><span>" p ="</span><span>, p, </span><span>" e ="</span><span>, e)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"aᵀe 等于 0："</span><span><span>, np.</span><span>isclose</span><span>(a </span><span>@</span><span> e, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># P = a aᵀ / aᵀa 是一个 2 x 2 矩阵，不是数</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>outer</span><span>(a, a) </span><span>/</span><span> (a </span><span>@</span><span> a)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ b 等于 p："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> b, p))</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># b 翻倍，投影翻倍；a 翻倍或变号，投影不变</span></div></div><div><div><div>23</div></div><div><span>    </span><span>for</span><span> name, P_new, b_new </span><span>in</span><span> [</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>(</span><span>"2b"</span><span>, P, </span><span>2</span><span><span> </span><span>*</span><span> b),</span></span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(</span><span>"2a"</span><span><span>, np.</span><span>outer</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> a, </span></span><span>2</span><span><span> </span><span>*</span><span> a) </span><span>/</span><span> ((</span></span><span>2</span><span><span> </span><span>*</span><span> a) </span><span>@</span><span> (</span></span><span>2</span><span><span> </span><span>*</span><span> a)), b),</span></span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>(</span><span>"-a"</span><span><span>, np.</span><span>outer</span><span>(</span><span>-</span><span>a, </span><span>-</span><span>a) </span><span>/</span><span> ((</span><span>-</span><span>a) </span><span>@</span><span> (</span><span>-</span><span>a)), b),</span></span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>]:</span></div></div><div><div><div>28</div></div><div><span>        </span><span>print</span><span>(name, </span><span>"的投影："</span><span><span>, P_new </span><span>@</span><span> b_new)</span></span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># P 的三条性质</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(P))</span></span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"P.T 等于 P："</span><span><span>, np.</span><span>allclose</span><span>(P.T, P))</span></span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ P 等于 P："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P))</span></span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>x = 0.8  p = [1.6 0.8]  e = [-0.6  1.2]</span></div></div><div><div><div>2</div></div><div><span>aᵀe 等于 0： True</span></div></div><div><div><div>3</div></div><div><span>P =</span></div></div><div><div><div>4</div></div><div><span>[[0.8 0.4]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[0.4 0.2]]</span></div></div><div><div><div>6</div></div><div><span>P @ b 等于 p： True</span></div></div><div><div><div>7</div></div><div><span>2b 的投影： [3.2 1.6]</span></div></div><div><div><div>8</div></div><div><span>2a 的投影： [1.6 0.8]</span></div></div><div><div><div>9</div></div><div><span>-a 的投影： [1.6 0.8]</span></div></div><div><div><div>10</div></div><div><span>秩： 1</span></div></div><div><div><div>11</div></div><div><span>P.T 等于 P： True</span></div></div><div><div><div>12</div></div><div><span>P @ P 等于 P： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>x=0.8x = 0.8</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0.8</span></span></span></span>，<span><span>p=(1.6,0.8)\mathbf{p} = (1.6, 0.8)</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1.6</span><span>,</span><span></span><span>0.8</span><span>)</span></span></span></span>，误差 <span><span>e=(−0.6,1.2)\mathbf{e} = (-0.6, 1.2)</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.6</span><span>,</span><span></span><span>1.2</span><span>)</span></span></span></span> 与 <span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 的点积为零。这一步的所有判断都用 <code>np.isclose</code>，因为 <span><span>2×(−0.6)+1×1.22 \times (-0.6) + 1 \times 1.2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.6</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>1.2</span></span></span></span> 在浮点数里剩下一个 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级的尾巴，直接打印出来很难看。</p><p>三个“问题”的答案和他在黑板上说的一致：<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 变成 <span><span>2b2\mathbf{b}</span><span><span><span></span><span>2</span><span>b</span></span></span></span>，投影从 <span><span>(1.6,0.8)(1.6, 0.8)</span><span><span><span></span><span>(</span><span>1.6</span><span>,</span><span></span><span>0.8</span><span>)</span></span></span></span> 变成 <span><span>(3.2,1.6)(3.2, 1.6)</span><span><span><span></span><span>(</span><span>3.2</span><span>,</span><span></span><span>1.6</span><span>)</span></span></span></span>，也翻倍；<span><span>a\mathbf{a}</span><span><span><span></span><span>a</span></span></span></span> 变成 <span><span>2a2\mathbf{a}</span><span><span><span></span><span>2</span><span>a</span></span></span></span> 或 <span><span>−a-\mathbf{a}</span><span><span><span></span><span>−</span><span>a</span></span></span></span>，投影一点没变，因为直线没变，分子分母各多出来的因子 4 和 <span><span>(−1)2(-1)^2</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 抵消了。</p><p><span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的性质他列了三条，我也一并验证：</p><ul>
<li>列空间是那条直线，所以秩是 1。它就是“一列乘一行”，也就是第 11 讲里的秩 1 矩阵。</li>
<li><span><span>PT=PP^\mathsf{T} = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>：转置 <span><span>aaT\mathbf{a}\mathbf{a}^\mathsf{T}</span><span><span><span></span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 还是 <span><span>aaT\mathbf{a}\mathbf{a}^\mathsf{T}</span><span><span><span></span><span>a</span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，分母是个数，不受影响。</li>
<li><span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>：投影一次已经落在直线上，再投影原地不动。</li>
</ul><p>他说“对称，且平方等于自己”这两条合起来，就是投影矩阵的全部特征。我没有去查这个说法的严格表述，但这里的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 两条都满足，这个证据对我够用。</p><p>下面的图把这一节和后面的例子放在一起：左边就是这一节的数，<span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 与直线之间那个小方块是直角。</p><p></p><figure><img src="/assets/images/linear-algebra/l15-projection.svg" alt="左：b = (1, 2) 投影到过 a = (2, 1) 的直线，p = (1.6, 0.8)，误差 e 与直线垂直。右：三个点 (1, 1)、(2, 2)、(3, 2) 的纵坐标 b 投影到 A 的列空间，得到直线上的三个点 p" /><figcaption>左：b = (1, 2) 投影到过 a = (2, 1) 的直线，p = (1.6, 0.8)，误差 e 与直线垂直。右：三个点 (1, 1)、(2, 2)、(3, 2) 的纵坐标 b 投影到 A 的列空间，得到直线上的三个点 p</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>fig </span><span>=</span><span> plt.</span><span>figure</span><span>(</span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 左：投影到直线（与 block1 同一组数）</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>ax1 </span><span>=</span><span> fig.</span><span>add_subplot</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>1</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> a </span><span>*</span><span> (a </span><span>@</span><span> b) </span><span>/</span><span> (a </span><span>@</span><span> a)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> b </span><span>-</span><span> p</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>-</span><span>0.5</span><span>, </span><span>3.0</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(t </span><span>*</span><span> a[</span><span>0</span><span><span>], t </span><span>*</span><span> a[</span></span><span>1</span><span>], </span><span>color</span><span>=</span><span>"gray"</span><span>, </span><span>lw</span><span>=</span><span>1</span><span>, </span><span>label</span><span>=</span><span>"line through a"</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span></div></div><div><div><div>21</div></div><div><span>        </span><span>""</span><span>,</span></div></div><div><div><div>22</div></div><div><span>        </span><span>xy</span><span><span>=</span><span>a,</span></span></div></div><div><div><div>23</div></div><div><span>        </span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>24</div></div><div><span>        </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>arrowstyle</span><span>=</span><span>"-&gt;"</span><span>, </span><span>color</span><span>=</span><span>"tab:blue"</span><span>),</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span></div></div><div><div><div>27</div></div><div><span>        </span><span>""</span><span>,</span></div></div><div><div><div>28</div></div><div><span>        </span><span>xy</span><span><span>=</span><span>b,</span></span></div></div><div><div><div>29</div></div><div><span>        </span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>30</div></div><div><span>        </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>arrowstyle</span><span>=</span><span>"-&gt;"</span><span>, </span><span>color</span><span>=</span><span>"tab:red"</span><span>),</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span></div></div><div><div><div>33</div></div><div><span>        </span><span>""</span><span>,</span></div></div><div><div><div>34</div></div><div><span>        </span><span>xy</span><span><span>=</span><span>p,</span></span></div></div><div><div><div>35</div></div><div><span>        </span><span>xytext</span><span><span>=</span><span>(</span></span><span>0</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>36</div></div><div><span>        </span><span>arrowprops</span><span>=</span><span>dict</span><span>(</span><span>arrowstyle</span><span>=</span><span>"-&gt;"</span><span>, </span><span>color</span><span>=</span><span>"tab:green"</span><span>, </span><span>lw</span><span>=</span><span>2</span><span>),</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>([p[</span><span>0</span><span>], b[</span><span>0</span><span>]], [p[</span><span>1</span><span>], b[</span><span>1</span><span>]], </span><span>"k--"</span><span>, </span><span>lw</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>ax1.</span><span>text</span><span>(a[</span><span>0</span><span><span>] </span><span>+</span><span> </span></span><span>0.05</span><span>, a[</span><span>1</span><span><span>] </span><span>-</span><span> </span></span><span>0.15</span><span>, </span><span>"a = (2, 1)"</span><span>, </span><span>color</span><span>=</span><span>"tab:blue"</span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>ax1.</span><span>text</span><span>(b[</span><span>0</span><span><span>] </span><span>-</span><span> </span></span><span>0.55</span><span>, b[</span><span>1</span><span><span>] </span><span>+</span><span> </span></span><span>0.08</span><span>, </span><span>"b = (1, 2)"</span><span>, </span><span>color</span><span>=</span><span>"tab:red"</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>ax1.</span><span>text</span><span>(p[</span><span>0</span><span><span>] </span><span>+</span><span> </span></span><span>0.05</span><span>, p[</span><span>1</span><span><span>] </span><span>-</span><span> </span></span><span>0.2</span><span>, </span><span>"p = (1.6, 0.8)"</span><span>, </span><span>color</span><span>=</span><span>"tab:green"</span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>ax1.</span><span>text</span><span>(</span><span>0.9</span><span>, </span><span>1.35</span><span>, </span><span>"e"</span><span>, </span><span>fontsize</span><span>=</span><span>12</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>0.12</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> a </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(a)</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> e </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(e)</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>[p[</span><span>0</span><span><span>] </span><span>-</span><span> s </span><span>*</span><span> u[</span></span><span>0</span><span>], p[</span><span>0</span><span><span>] </span><span>-</span><span> s </span><span>*</span><span> u[</span></span><span>0</span><span><span>] </span><span>+</span><span> s </span><span>*</span><span> v[</span></span><span>0</span><span>], p[</span><span>0</span><span><span>] </span><span>+</span><span> s </span><span>*</span><span> v[</span></span><span>0</span><span>]],</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>[p[</span><span>1</span><span><span>] </span><span>-</span><span> s </span><span>*</span><span> u[</span></span><span>1</span><span>], p[</span><span>1</span><span><span>] </span><span>-</span><span> s </span><span>*</span><span> u[</span></span><span>1</span><span><span>] </span><span>+</span><span> s </span><span>*</span><span> v[</span></span><span>1</span><span>], p[</span><span>1</span><span><span>] </span><span>+</span><span> s </span><span>*</span><span> v[</span></span><span>1</span><span>]],</span></div></div><div><div><div>49</div></div><div><span>        </span><span>color</span><span>=</span><span>"k"</span><span>,</span></div></div><div><div><div>50</div></div><div><span>        </span><span>lw</span><span>=</span><span>0.8</span><span>,</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlim</span><span>(</span><span>-</span><span>0.3</span><span>, </span><span>3.0</span><span>)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylim</span><span>(</span><span>-</span><span>0.3</span><span>, </span><span>2.4</span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_title</span><span>(</span><span>"projection onto a line"</span><span>)</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>    </span><span># 右：三个点的拟合（与 block2 同一组数）</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>ax2 </span><span>=</span><span> fig.</span><span>add_subplot</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>bb </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>xh </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A.T </span><span>@</span><span> A, A.T </span><span>@</span><span> bb)</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>pp </span><span>=</span><span> A </span><span>@</span><span> xh</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>tt </span><span>=</span><span> A[:, </span><span>1</span><span>]</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>tl </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.5</span><span>, </span><span>3.5</span><span>, </span><span>2</span><span>)</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(tl, xh[</span><span>0</span><span><span>] </span><span>+</span><span> xh[</span></span><span>1</span><span><span>] </span><span>*</span><span> tl, </span></span><span>color</span><span>=</span><span>"tab:green"</span><span>, </span><span>lw</span><span>=</span><span>1.5</span><span>)</span></div></div><div><div><div>66</div></div><div><span>    </span><span>for</span><span> ti, bi, pi </span><span>in</span><span> </span><span>zip</span><span>(tt, bb, pp):</span></div></div><div><div><div>67</div></div><div><span><span>        </span></span><span>ax2.</span><span>plot</span><span>([ti, ti], [bi, pi], </span><span>"k--"</span><span>, </span><span>lw</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(tt, bb, </span><span>"o"</span><span>, </span><span>color</span><span>=</span><span>"tab:red"</span><span>, </span><span>label</span><span>=</span><span>"b = (1, 2, 2)"</span><span>)</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>ax2.</span><span>plot</span><span>(tt, pp, </span><span>"s"</span><span>, </span><span>color</span><span>=</span><span>"tab:green"</span><span>, </span><span>label</span><span>=</span><span>"p = A x_hat"</span><span>)</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>ax2.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"upper left"</span><span>)</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlabel</span><span>(</span><span>"t"</span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylabel</span><span>(</span><span>"height"</span><span>)</span></div></div><div><div><div>73</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlim</span><span>(</span><span>0.5</span><span>, </span><span>3.5</span><span>)</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylim</span><span>(</span><span>0.5</span><span>, </span><span>2.8</span><span>)</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_title</span><span>(</span><span>"b projected onto the column space of A"</span><span>)</span></div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>78</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>79</div></div><div><span>        </span><span>"l15-projection.svg"</span><span>,</span></div></div><div><div><div>80</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>85</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>投影到一个子空间上<a href="#投影到一个子空间上"><span>#</span></a></h2><p><strong>直线换成平面，该怎么办？</strong> 他先说为什么要关心投影：<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 的方程比未知数多，多半无解。<span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 永远落在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间里，而 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 大概率不在，所以退一步，解一个能解的问题：把 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 换成列空间里最近的那个向量 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span>，解 <span><span>Ax^=pA\hat{\mathbf{x}} = \mathbf{p}</span><span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>p</span></span></span></span>。<span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 头上那顶小帽子，是用来提醒自己它不是那个不存在的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>。</p><p>平面怎么描述？给两个线性无关的向量 <span><span>a1\mathbf{a}_1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>a2\mathbf{a}_2</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 当基，它们不必垂直，但必须无关。把它们作为列拼成 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，这个平面就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间，之后的公式都用 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 写出来，两列还是 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 列都不用改。</p><p>投影 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span> 是列的组合，<span><span>p=Ax^\mathbf{p} = A\hat{\mathbf{x}}</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>，问题变成找 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>。条件仍然是误差垂直：<span><span>e=b−Ax^\mathbf{e} = \mathbf{b} - A\hat{\mathbf{x}}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 垂直于平面，所以垂直于 <span><span>a1\mathbf{a}_1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，也垂直于 <span><span>a2\mathbf{a}_2</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>a1T(b−Ax^)=0a2T(b−Ax^)=0⟹AT(b−Ax^)=0\begin{aligned}
\mathbf{a}_1^\mathsf{T}(\mathbf{b} - A\hat{\mathbf{x}}) &amp;= 0 \\
\mathbf{a}_2^\mathsf{T}(\mathbf{b} - A\hat{\mathbf{x}}) &amp;= 0
\end{aligned}
\qquad\Longrightarrow\qquad
A^\mathsf{T}(\mathbf{b} - A\hat{\mathbf{x}}) = \mathbf{0}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>b</span><span></span><span>−</span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>b</span><span></span><span>−</span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>0</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>两个方程叠起来，恰好就是 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（第一行是 <span><span>a1T\mathbf{a}_1^\mathsf{T}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，第二行是 <span><span>a2T\mathbf{a}_2^\mathsf{T}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）乘以误差等于零。一条直线时 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 只有一列，这个方程退回到上一节的 <span><span>aTe=0\mathbf{a}^\mathsf{T}\mathbf{e} = 0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</p><p>他接着做了一件我觉得很漂亮的事：把这个方程放回第 14 讲的四个子空间里。<span><span>ATe=0A^\mathsf{T}\mathbf{e} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 说的是 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 在 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间里，也就是左零空间，它和列空间正交。他在黑板上说，推导出来的方程恰好说出了他想要的几何，这很舒服。</p><p>整理方程，得到三个要记的公式：</p><span><span><span>ATAx^=ATb,x^=(ATA)−1ATb,p=Ax^,P=A(ATA)−1ATA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b},\qquad
\hat{\mathbf{x}} = (A^\mathsf{T}A)^{-1}A^\mathsf{T}\mathbf{b},\qquad
\mathbf{p} = A\hat{\mathbf{x}},\qquad
P = A(A^\mathsf{T}A)^{-1}A^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span>,</span><span></span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>对照一维：<span><span>aTa\mathbf{a}^\mathsf{T}\mathbf{a}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>a</span></span></span></span> 是个数，可以直接除；<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 是矩阵，只能写成逆。公式的形状没变。</p><p>下面我用他最后一个例子里的矩阵。三个点 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>、<span><span>(2,2)(2, 2)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(3,2)(3, 2)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，想让直线 <span><span>b=C+Dtb = C + Dt</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span>t</span></span></span></span> 穿过它们，方程是</p><span><span><span>[111213][CD]=[122]\begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 2 \\ 1 &amp; 3 \end{bmatrix}
\begin{bmatrix} C \\ D \end{bmatrix}
= \begin{bmatrix} 1 \\ 2 \\ 2 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>C</span></span></span><span><span></span><span><span>D</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>三个方程两个未知数，无解。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的两列张成三维空间里的一个平面，<span><span>b=(1,2,2)\mathbf{b} = (1, 2, 2)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 在平面外，正好是用来练习投影的材料：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 三个点 (1, 1)、(2, 2)、(3, 2)，想解 C + D t = b 但解不了</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span># 正规方程 AᵀA x = Aᵀb</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>x_hat </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A.T </span><span>@</span><span> A, A.T </span><span>@</span><span> b)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> A </span><span>@</span><span> x_hat</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> b </span><span>-</span><span> p</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"x_hat ="</span><span>, x_hat)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"p ="</span><span>, p)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"e ="</span><span>, e)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"Aᵀe 等于 0："</span><span><span>, np.</span><span>allclose</span><span>(A.T </span><span>@</span><span> e, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 投影矩阵 P = A (AᵀA)⁻¹ Aᵀ</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> A </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(A.T </span><span>@</span><span> A) </span><span>@</span><span> A.T</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ b 等于 p："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> b, p))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(P), </span></span><span>"，迹："</span><span>, </span><span>round</span><span><span>(np.</span><span>trace</span><span>(P), </span></span><span>6</span><span>))</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"P.T 等于 P："</span><span><span>, np.</span><span>allclose</span><span>(P.T, P))</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ P 等于 P："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> P, P))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ p 等于 p："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> p, p))</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># e 在 Aᵀ 的零空间里：Aᵀ 是 2 x 3，零空间由最后一个右奇异向量张成</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>_, _, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A.T)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> Vt[</span><span>-</span><span>1</span><span>]</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"零空间基向量 n ="</span><span>, n)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"e 平行于 n："</span><span><span>, np.</span><span>allclose</span><span>(np.</span><span>cross</span><span>(e, n), </span></span><span>0</span><span>))</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 与 lstsq 对照</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>x_lstsq, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"lstsq 的解一致："</span><span><span>, np.</span><span>allclose</span><span>(x_hat, x_lstsq))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>x_hat = [0.6667 0.5   ]</span></div></div><div><div><div>2</div></div><div><span>p = [1.1667 1.6667 2.1667]</span></div></div><div><div><div>3</div></div><div><span>e = [-0.1667  0.3333 -0.1667]</span></div></div><div><div><div>4</div></div><div><span>Aᵀe 等于 0： True</span></div></div><div><div><div>5</div></div><div><span>P =</span></div></div><div><div><div>6</div></div><div><span>[[ 0.8333  0.3333 -0.1667]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ 0.3333  0.3333  0.3333]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[-0.1667  0.3333  0.8333]]</span></div></div><div><div><div>9</div></div><div><span>P @ b 等于 p： True</span></div></div><div><div><div>10</div></div><div><span>秩： 2 ，迹： 2.0</span></div></div><div><div><div>11</div></div><div><span>P.T 等于 P： True</span></div></div><div><div><div>12</div></div><div><span>P @ P 等于 P： True</span></div></div><div><div><div>13</div></div><div><span>P @ p 等于 p： True</span></div></div><div><div><div>14</div></div><div><span>零空间基向量 n = [ 0.4082 -0.8165  0.4082]</span></div></div><div><div><div>15</div></div><div><span>e 平行于 n： True</span></div></div><div><div><div>16</div></div><div><span>lstsq 的解一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读一下这份输出：</p><ul>
<li><span><span>x^=(2/3,1/2)\hat{\mathbf{x}} = (2/3, 1/2)</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2/3</span><span>,</span><span></span><span>1/2</span><span>)</span></span></span></span>，投影点 <span><span>p=(7/6,5/3,13/6)\mathbf{p} = (7/6, 5/3, 13/6)</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>7/6</span><span>,</span><span></span><span>5/3</span><span>,</span><span></span><span>13/6</span><span>)</span></span></span></span>，误差 <span><span>e=(−1/6,1/3,−1/6)\mathbf{e} = (-1/6, 1/3, -1/6)</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1/6</span><span>,</span><span></span><span>1/3</span><span>,</span><span></span><span>−</span><span>1/6</span><span>)</span></span></span></span>。<span><span>ATe=0A^\mathsf{T}\mathbf{e} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 成立。</li>
<li><span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 是 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 的，秩是 2（列空间是平面，二维），<span><span>PT=PP^\mathsf{T} = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>，<span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span> 都成立。多补了一行 <span><span>Pp=pP\mathbf{p} = \mathbf{p}</span><span><span><span></span><span>P</span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>p</span></span></span></span>：投影点已经在平面上，再投影不动。</li>
<li>迹是 2.0，等于秩。我没有在字幕里看到他说这件事，这是我顺手打印的，下一节的随机矩阵里它也成立，我猜这是投影矩阵的一般性质（据我所知，投影矩阵的特征值只有 0 和 1，迹就是 1 的个数，也就是秩）。</li>
<li><span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 的方向和 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间基向量 <span><span>n\mathbf{n}</span><span><span><span></span><span>n</span></span></span></span> 平行，确认了 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 在左零空间里，和他在黑板上的话对得上。</li>
<li><code>np.linalg.lstsq</code> 给出了同一个 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>。它做的是同一件事，只是据我所知它内部用的是 SVD，不去解 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>（<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的条件数是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的平方，数值上不划算）。</li>
</ul><p><span><span>x^=(2/3,1/2)\hat{\mathbf{x}} = (2/3, 1/2)</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2/3</span><span>,</span><span></span><span>1/2</span><span>)</span></span></span></span> 就是这三个点的最佳直线 <span><span>b=23+12tb = \frac23 + \frac12 t</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>t</span></span></span></span> 的系数。Strang 说下一讲才算数值例子，我这里提前把结果打印出来了（图右半边画的就是它）：三个红点是 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，绿方块是 <span><span>p\mathbf{p}</span><span><span><span></span><span>p</span></span></span></span>，虚线是 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 的三个分量。</p></section>
<section><h2>公式里藏着的几个问题<a href="#公式里藏着的几个问题"><span>#</span></a></h2><p><strong>为什么不能把逆拆开？</strong> 他在黑板上写完 <span><span>P=A(ATA)−1ATP = A(A^\mathsf{T}A)^{-1}A^\mathsf{T}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 之后，立刻动了一个念头：乘积的逆等于逆的乘积反过来，那 <span><span>(ATA)−1=A−1(AT)−1(A^\mathsf{T}A)^{-1} = A^{-1}(A^\mathsf{T})^{-1}</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，代进去全部抵消，<span><span>P=IP = I</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。他的原话大意是，这件事要上录像了，我写在这里，你们别太认真看。（当场写了错的，然后自己叫停，我觉得这是整门课里很可爱的一段。）</p><p>错在哪里：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是 <span><span>3×23 \times 2</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的，不是方阵，没有逆。而 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 是 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的方阵，有逆。反过来，如果 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 本身是可逆方阵，列空间是整个空间，投影到整个空间当然是单位矩阵，这时拆开是合法的。这两件事我都让 NumPy 验证了。</p><p>另外两件事也一起验证：我在平面里随机撒点，看没有任何一个点离 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 更近；我又换一个 <span><span>50×550 \times 5</span><span><span><span></span><span>50</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span> 的随机矩阵，看对称和幂等不是三阶小矩阵的巧合。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> A </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(A.T </span><span>@</span><span> A) </span><span>@</span><span> A.T</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> P </span><span>@</span><span> b</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>dist </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(b </span><span>-</span><span> p)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 1. 平面里随机取 100000 个点，没有一个比 p 离 b 更近</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>coeffs </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>100_000</span><span>, </span><span>2</span><span><span>)) </span><span>*</span><span> </span></span><span>2.0</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>others </span><span>=</span><span> coeffs </span><span>@</span><span> A.T</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>dists </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(others </span><span>-</span><span> b, </span><span>axis</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"p 到 b 的距离：</span><span>{</span><span>dist</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"随机点到 b 的最小距离：</span><span>{</span><span><span>dists.</span><span>min</span><span>()</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"p 确实最近："</span><span>, </span><span>bool</span><span><span>(np.</span><span>all</span><span>(dists </span><span>&gt;=</span><span> dist </span><span>-</span><span> </span></span><span>1e-12</span><span>)))</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 2. 想把 (AᵀA)⁻¹ 拆成 A⁻¹ (Aᵀ)⁻¹，A 不是方阵，拆不了</span></div></div><div><div><div>22</div></div><div><span>    </span><span>try</span><span>:</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>inv</span><span>(A)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>except</span><span> np.linalg.LinAlgError </span><span>as</span><span> err:</span></div></div><div><div><div>25</div></div><div><span>        </span><span>print</span><span>(</span><span>"inv(A) 失败："</span><span>, err)</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 3. A 是可逆方阵时，列空间是整个空间，P 就是单位矩阵</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>P_full </span><span>=</span><span> S </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(S.T </span><span>@</span><span> S) </span><span>@</span><span> S.T</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"S 可逆时 P 等于 I："</span><span><span>, np.</span><span>allclose</span><span>(P_full, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 4. 更高维：50 x 5 的随机 A，P 仍然是对称幂等，迹等于秩</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>50</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> M </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(M.T </span><span>@</span><span> M) </span><span>@</span><span> M.T</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"‖Q - Qᵀ‖ ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q </span><span>-</span><span> Q.T)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"‖Q @ Q - Q‖ ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(Q </span><span>@</span><span> Q </span><span>-</span><span> Q)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"迹 ="</span><span>, </span><span>round</span><span>(</span><span>float</span><span><span>(np.</span><span>trace</span><span>(Q)), </span></span><span>6</span><span>))</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>p 到 b 的距离：0.4082</span></div></div><div><div><div>2</div></div><div><span>随机点到 b 的最小距离：0.4085</span></div></div><div><div><div>3</div></div><div><span>p 确实最近： True</span></div></div><div><div><div>4</div></div><div><span>inv(A) 失败： Last 2 dimensions of the array must be square</span></div></div><div><div><div>5</div></div><div><span>S 可逆时 P 等于 I： True</span></div></div><div><div><div>6</div></div><div><span>‖Q - Qᵀ‖ = 3.76e-16</span></div></div><div><div><div>7</div></div><div><span>‖Q @ Q - Q‖ = 6.52e-16</span></div></div><div><div><div>8</div></div><div><span>迹 = 5.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四行输出，四个结论：</p><ul>
<li>平面上 10 万个随机点，最近的离 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 是 0.4085，投影点是 0.4082，投影点赢。差距只有 0.0003，是因为随机点已经撒得够密，恰好有一个落在投影点附近（这当然不说明投影“只比随机略好”，要是只撒 100 个点，差距会大得多）。</li>
<li><code>np.linalg.inv(A)</code> 直接抛出 <code>LinAlgError</code>，原话是“最后两个维度必须是方阵”。</li>
<li>可逆方阵 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 等于 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，和他说的一致。</li>
<li><span><span>50×550 \times 5</span><span><span><span></span><span>50</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span> 的随机矩阵上，<span><span>∥PT−P∥\|P^\mathsf{T} - P\|</span><span><span><span></span><span>∥</span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span>∥</span></span></span></span> 是 <span><span>3.76×10−163.76 \times 10^{-16}</span><span><span><span></span><span>3.76</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>∥P2−P∥\|P^2 - P\|</span><span><span><span></span><span>∥</span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span>∥</span></span></span></span> 是 <span><span>6.52×10−166.52 \times 10^{-16}</span><span><span><span></span><span>6.52</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，都在双精度的舍入误差量级，迹恰好是 5，等于列数，也等于秩。</li>
</ul><p><strong>为什么随机点永远赢不了？</strong> 这不是运气，是勾股定理。平面里任取一点 <span><span>q\mathbf{q}</span><span><span><span></span><span>q</span></span></span></span>，<span><span>b−q=e+(p−q)\mathbf{b} - \mathbf{q} = \mathbf{e} + (\mathbf{p} - \mathbf{q})</span><span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>p</span><span></span><span>−</span><span></span></span><span><span></span><span>q</span><span>)</span></span></span></span>，其中 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 垂直于平面，<span><span>p−q\mathbf{p} - \mathbf{q}</span><span><span><span></span><span>p</span><span></span><span>−</span><span></span></span><span><span></span><span>q</span></span></span></span> 在平面里，两者垂直，所以 <span><span>∥b−q∥2=∥e∥2+∥p−q∥2≥∥e∥2\|\mathbf{b} - \mathbf{q}\|^2 = \|\mathbf{e}\|^2 + \|\mathbf{p} - \mathbf{q}\|^2 \ge \|\mathbf{e}\|^2</span><span><span><span></span><span>∥</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>q</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>e</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>∥</span><span>p</span><span></span><span>−</span><span></span></span><span><span></span><span>q</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>∥</span><span>e</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，等号只在 <span><span>q=p\mathbf{q} = \mathbf{p}</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>p</span></span></span></span> 时成立。用上面的数手算一遍：<span><span>∥b∥2=1+4+4=9\|\mathbf{b}\|^2 = 1 + 4 + 4 = 9</span><span><span><span></span><span>∥</span><span>b</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span></span><span>=</span><span></span></span><span><span></span><span>9</span></span></span></span>，<span><span>∥p∥2=53/6\|\mathbf{p}\|^2 = 53/6</span><span><span><span></span><span>∥</span><span>p</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>53/6</span></span></span></span>，<span><span>∥e∥2=1/6\|\mathbf{e}\|^2 = 1/6</span><span><span><span></span><span>∥</span><span>e</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1/6</span></span></span></span>，两者之和恰好是 9，同样是勾股，<span><span>1/61/6</span><span><span><span></span><span>1/6</span></span></span></span> 开方就是 0.4082。这也解释了为什么一维、二维、<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 维的推导可以用同一个条件：只要误差垂直，剩下的都是勾股。</p><p>顺便一提一个类比：这像太阳在头顶正上方时的影子。影子是物体投到地面上的点，光线垂直于地面，这就是“误差垂直于子空间”。影子一旦落在地上，再来一次，它还是在原地，这就是 <span><span>P2=PP^2 = P</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span>。（我只用这一个类比，再讲下去就要开始解释太阳的位置了。）</p></section>
<section><h2>什么时候 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆<a href="#什么时候-ataamathsftaata-可逆"><span>#</span></a></h2><p><strong>公式里的逆什么时候存在？</strong> 他没有在这一讲停下来讨论，我顺手补一句。这个例子里 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的两列线性无关，<span><span>ATA=[36614]A^\mathsf{T}A = \begin{bmatrix} 3 &amp; 6 \\ 6 &amp; 14 \end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>14</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 可逆，一切顺利。如果两列线性相关，<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 就奇异，<span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 不唯一。“<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆当且仅当 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列线性无关”这个结论在第 14 讲已经备好，这里是它第一次真正派上用场。</p><p>下一讲他说会把今天只写了字母的这些东西算成数，从三个点的拟合接着做下去，见 <a href="/posts/linear-algebra-16-least-squares/">18.06 第 16 讲：投影矩阵与最小二乘</a>。我猜他也不会再写“eight a’s in a row”那样的矩阵了。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-14-orthogonal-subspaces/</id>
      <title type="text">18.06 第 14 讲：正交向量与正交子空间</title>
      <published>2026-10-18T04:20:00.000Z</published>
      <updated>2026-10-18T04:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-14-orthogonal-subspaces/"/>
      <summary type="text">MIT 18.06 第 14 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 在一个随机的 4 × 6、秩 3 的矩阵上检查行空间与零空间正交，两组基的点积最大只有 1.0e-16，又在 1000 个随机矩阵上验证了 AᵀA 与 A 的秩始终相同。</summary>
      <content type="html"><![CDATA[<p>这一讲开了新的一章，Strang 管它叫“九十度的一章”。核心想法只有一句：行空间和零空间互相垂直，而且维数加起来正好是 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>。我在一个随机的 4 × 6 秩 3 矩阵上量了一下，行空间的基和零空间的基两两做点积，最大的绝对值是 <span><span>1.0×10−161.0 \times 10^{-16}</span><span><span><span></span><span>1.0</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，也就是舍入误差；同一个矩阵上，3 维的行空间加 3 维的零空间拼起来，秩正好是 6，一点缝都没有。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=YzZUIYRCE38" target="_blank">Lecture 14: Orthogonal vectors and subspaces</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-14-orthogonal-vectors-and-subspaces/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §4.1 Orthogonality of Vectors and Subspaces
前置：<a href="/posts/linear-algebra-13-quiz-1-review/">第 13 讲：第一次测验复习</a>
一句话：两个向量正交就是 <span><span>xTy=0\mathbf{x}^\mathsf{T}\mathbf{y} = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；行空间与零空间是 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span> 里的正交补；<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆当且仅当 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列线性无关。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他指着“四个子空间”的大图说，这一章要证明图里两对子空间之间是九十度。这一段要听，后面全是在兑现它。</li>
<li>用勾股定理推出 <span><span>xTy=0\mathbf{x}^\mathsf{T}\mathbf{y} = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的那段很短，本篇第一节已经算过，倍速即可。</li>
<li>他拿黑板和地板当例子问“这两个子空间正交吗”，然后投票，再讲为什么不行。这是本讲最有画面感的地方，值得完整看一遍。</li>
<li>讲完 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 推出行空间与零空间正交之后，他用一整段铺垫“没有解的方程组怎么办”和 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>，这是下一讲的预告。结尾处 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的两个小例子要看，本篇最后一节会逐个重算。</li>
</ul></div></div>
<section><h2>两个向量什么时候正交<a href="#两个向量什么时候正交"><span>#</span></a></h2><p><strong>怎么判断两个向量垂直？</strong> Strang 的答案是：算点积 <span><span>xTy\mathbf{x}^\mathsf{T}\mathbf{y}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span></span></span></span>，等于零就垂直。他先不直接宣布，而是从勾股定理出发。直角三角形的两条直角边是 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 和 <span><span>y\mathbf{y}</span><span><span><span></span><span>y</span></span></span></span>，斜边是 <span><span>x+y\mathbf{x} + \mathbf{y}</span><span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>y</span></span></span></span>，毕达哥拉斯说</p><span><span><span>xTx+yTy=(x+y)T(x+y).\mathbf{x}^\mathsf{T}\mathbf{x} + \mathbf{y}^\mathsf{T}\mathbf{y} = (\mathbf{x} + \mathbf{y})^\mathsf{T}(\mathbf{x} + \mathbf{y}).</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>y</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>y</span><span>)</span><span>.</span></span></span></span></span><p>右边展开，得到 <span><span>xTx+xTy+yTx+yTy\mathbf{x}^\mathsf{T}\mathbf{x} + \mathbf{x}^\mathsf{T}\mathbf{y} + \mathbf{y}^\mathsf{T}\mathbf{x} + \mathbf{y}^\mathsf{T}\mathbf{y}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>+</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span></span></span></span>。实向量的 <span><span>xTy\mathbf{x}^\mathsf{T}\mathbf{y}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span></span></span></span> 和 <span><span>yTx\mathbf{y}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span> 相等，两边相同的项消掉，剩下 <span><span>2 xTy=02\,\mathbf{x}^\mathsf{T}\mathbf{y} = 0</span><span><span><span></span><span>2</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。所以勾股定理成立，当且仅当点积为零。他还特意强调：这个等式不是永远成立，它恰好等价于直角。</p><p>他给的数值例子是 <span><span>x=(1,2,3)\mathbf{x} = (1, 2, 3)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，再“凑”一个垂直的 <span><span>y=(2,−1,0)\mathbf{y} = (2, -1, 0)</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>：<span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 的长度平方是 <span><span>1+4+9=141 + 4 + 9 = 14</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span></span><span>+</span><span></span></span><span><span></span><span>9</span><span></span><span>=</span><span></span></span><span><span></span><span>14</span></span></span></span>，<span><span>y\mathbf{y}</span><span><span><span></span><span>y</span></span></span></span> 的是 5，<span><span>x+y=(3,1,3)\mathbf{x} + \mathbf{y} = (3, 1, 3)</span><span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 的是 <span><span>9+1+9=199 + 1 + 9 = 19</span><span><span><span></span><span>9</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>9</span><span></span><span>=</span><span></span></span><span><span></span><span>19</span></span></span></span>，<span><span>14+5=1914 + 5 = 19</span><span><span><span></span><span>14</span><span></span><span>+</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>19</span></span></span></span>。（字幕里他口头说成“长度是 14”，指的是长度平方，这是口误。）我加了一个随手取的 <span><span>z=(1,1,1)\mathbf{z} = (1, 1, 1)</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 做反例：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>])  </span><span># 黑板上凑出来的，和 x 垂直</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>])  </span><span># 随手取的，大概率不垂直</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> name, v </span><span>in</span><span> [(</span><span>"y"</span><span>, y), (</span><span>"z"</span><span>, z)]:</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>lhs </span><span>=</span><span> x </span><span>@</span><span> x </span><span>+</span><span> v </span><span>@</span><span> v</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>rhs </span><span>=</span><span> (x </span><span>+</span><span> v) </span><span>@</span><span> (x </span><span>+</span><span> v)</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>cross </span><span>=</span><span> x </span><span>@</span><span> v</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>angle </span><span>=</span><span> np.</span><span>degrees</span><span>(</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>np.</span><span>arccos</span><span>(cross </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(x) </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(v))</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：x^T </span><span>{</span><span>name</span><span>}</span><span> = </span><span>{</span><span>cross</span><span>}</span><span>，夹角 = </span><span>{</span><span>angle</span><span>:.2f</span><span>}</span><span> 度"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"  |x|^2 + |</span><span>{</span><span>name</span><span>}</span><span>|^2 = </span><span>{</span><span>lhs</span><span>}</span><span>，|x + </span><span>{</span><span>name</span><span>}</span><span>|^2 = </span><span>{</span><span>rhs</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 零向量和任何向量都正交</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"0^T x ="</span><span><span>, np.</span><span>zeros</span><span>(</span></span><span>3</span><span><span>) </span><span>@</span><span> x)</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>y：x^T y = 0.0，夹角 = 90.00 度</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>|x|^2 + |y|^2 = 19.0，|x + y|^2 = 19.0</span></div></div><div><div><div>3</div></div><div><span>z：x^T z = 6.0，夹角 = 22.21 度</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|x|^2 + |z|^2 = 17.0，|x + z|^2 = 29.0</span></div></div><div><div><div>5</div></div><div><span>0^T x = 0.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对 <span><span>y\mathbf{y}</span><span><span><span></span><span>y</span></span></span></span>，两边都是 19，夹角 90.00 度；对 <span><span>z\mathbf{z}</span><span><span><span></span><span>z</span></span></span></span>，点积是 6，勾股定理两边差了 <span><span>29−17=1229 - 17 = 12</span><span><span><span></span><span>29</span><span></span><span>−</span><span></span></span><span><span></span><span>17</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>，正好是 <span><span>2 xTz=122\,\mathbf{x}^\mathsf{T}\mathbf{z} = 12</span><span><span><span></span><span>2</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>，夹角只有 22.21 度。“差多少”恰好由点积决定，这比“等不等”多说了一点东西，我觉得值得记住：以后把点积看成“距直角还差多少”会很顺手。</p><p>最后一行是他随口带过的一句：零向量和任何向量都正交，因为 <span><span>0Ty\mathbf{0}^\mathsf{T}\mathbf{y}</span><span><span><span></span><span><span>0</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span></span></span></span> 永远是零。他说“数学里要遵守规则”。这句看着没用，下一节讨论“直线和零子空间”时就要用到。</p></section>
<section><h2>两个子空间什么时候正交<a href="#两个子空间什么时候正交"><span>#</span></a></h2><p><strong>子空间之间的正交怎么定义？</strong> 他说：子空间 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 与 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 正交，指的是 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 里的每一个向量都和 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 里的每一个向量正交。“每一个”对“每一个”，所以条件比看起来强得多。</p><p>他用教室来演示：黑板是一个穿过原点的平面（比如 <span><span>xzxz</span><span><span><span></span><span>x</span><span>z</span></span></span></span> 平面），地板是另一个（<span><span>xyxy</span><span><span><span></span><span>x</span><span>y</span></span></span></span> 平面），两个都是 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 的二维子空间。它们正交吗？直觉上墙和地板成直角，他让大家举手投票，然后给出答案：不正交。理由有两层。一层是黑板上取一个四十五度的向量，地板上取一个不垂直的向量，点积就不为零。更彻底的一层是：这两个平面相交在墙角那条线上，这条线上的向量既在黑板里又在地板里，它得和自己正交，而一个非零向量和自己的点积是长度平方，不是零。所以两个正交子空间一定只在零向量处相交。</p><p>我把这个例子照搬进代码。子空间用基向量（作为列）表示，判断正交只需要看两组基两两的点积，因为任何向量都是基向量的组合，点积对组合是线性的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># 黑板是 xz 平面，地板是 xy 平面，各用两个基向量表示（作为列）</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>board </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>floor </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span># 两组基两两做点积：全部为 0 才说明两个子空间正交</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"黑板基 ^T 地板基："</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span><span>(board.T </span><span>@</span><span> floor)</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 黑板上的 45 度向量与地板上的向量</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> board </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> floor </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"u ="</span><span>, u, </span><span>"v ="</span><span>, v, </span><span>"u^T v ="</span><span><span>, u </span><span>@</span><span> v)</span></span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 两个平面的交线：黑板和地板同时包含 (1, 0, 0)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>crack </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>in_board </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>column_stack</span><span>([board, crack])) </span><span>==</span><span> </span><span>2</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>in_floor </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>column_stack</span><span>([floor, crack])) </span><span>==</span><span> </span><span>2</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"交线向量同时在两个平面里："</span><span>, in_board </span><span>and</span><span> in_floor)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"它和自己的点积："</span><span><span>, crack </span><span>@</span><span> crack)</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 平面里的两条直线：只在 0 相交，且互相垂直，才是正交子空间</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>line1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>line2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"两条直线的方向点积："</span><span><span>, line1 </span><span>@</span><span> line2)</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>黑板基 ^T 地板基：</span></div></div><div><div><div>2</div></div><div><span>[[1. 0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 0.]]</span></div></div><div><div><div>4</div></div><div><span>u = [1. 0. 1.] v = [1. 0. 0.] u^T v = 1.0</span></div></div><div><div><div>5</div></div><div><span>交线向量同时在两个平面里： True</span></div></div><div><div><div>6</div></div><div><span>它和自己的点积： 1.0</span></div></div><div><div><div>7</div></div><div><span>两条直线的方向点积： 0.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>基向量两两点积的矩阵里，左上角那个 1 就是罪魁祸首：黑板的第一个基向量和地板的第一个基向量都是 <span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，也就是墙角那条线。只要这个矩阵里有一个非零元素，两个子空间就不正交。黑板上的 <span><span>(1,0,1)(1, 0, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 和地板上的 <span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 点积是 1，不垂直，也和字幕说的一致。</p><p>他接着在平面 <span><span>R2\mathbb{R}^2</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 里把所有情况穷举了一遍，因为这里的子空间只有三种：零向量、过原点的直线、整个平面。过原点的直线和零子空间永远正交（零向量和谁都正交）；直线和整个平面永远不正交（平面里就有那条直线本身）；两条不同的直线，只有成直角时才正交，最后一行 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 和 <span><span>(1,−1)(1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 的点积是 0 就是一个例子。</p></section>
<section><h2>行空间与零空间正交<a href="#行空间与零空间正交"><span>#</span></a></h2><p>前面都是铺垫，这一节才是整讲的核心。Strang 说，大图里的这一对：行空间和零空间，是正交的。证明他只用了一句话：零空间里的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 满足 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 按行写出来，</p><span><span><span>[行 1行 2⋮行 m]x=[00⋮0].\begin{bmatrix} \text{行 } 1 \\ \text{行 } 2 \\ \vdots \\ \text{行 } m \end{bmatrix}
\mathbf{x} =
\begin{bmatrix} 0 \\ 0 \\ \vdots \\ 0 \end{bmatrix}.</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>行</span><span> </span></span><span>1</span></span></span><span><span></span><span><span><span>行</span><span> </span></span><span>2</span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span><span>行</span><span> </span></span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>.</span></span></span></span></span><p>每一行和 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 的点积都是零，所以 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 和每一行正交。（我喜欢这个证明，因为它是“方程本身就是答案”：<span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的定义和正交的定义根本是一句话的两种写法。）</p><p>不过他随即提醒自己：行空间不只有那几行，还有它们的所有组合。所以还要补一步：<span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 与每一行垂直，就与 <span><span>c1(行 1)+c2(行 2)+⋯c_1(\text{行 }1) + c_2(\text{行 }2) + \cdots</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>行</span><span> </span></span><span>1</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>行</span><span> </span></span><span>2</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span></span></span></span> 垂直，因为零乘常数、零加零还是零。这就是“线性”在起作用。左零空间和列空间那一对，他说不用再证了，把上面的话里 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 换成 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 就行。</p><p>然后他问了一个很有用的问题：能不能有两条互相垂直的直线，一条是行空间，一条是零空间，都在 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 里？不行，因为 <span><span>r+(n−r)=nr + (n - r) = n</span><span><span><span></span><span>r</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>，<span><span>1+1≠31 + 1 \ne 3</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>3</span></span></span></span>。他的例子是</p><span><span><span>A=[1252410],A = \begin{bmatrix} 1 &amp; 2 &amp; 5 \\ 2 &amp; 4 &amp; 10 \end{bmatrix},</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span></span></span></span></span><p>两行成比例，行空间是过 <span><span>(1,2,5)(1, 2, 5)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span> 的一条线，维数 1；零空间是一个平面，维数 2，而且就是以 <span><span>(1,2,5)(1, 2, 5)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span> 为法向量的那个平面，“你在微积分里学过的那个叫 N 的法向量，原来就是它”。第二行没起任何作用，去掉后方程是 <span><span>x1+2x2+5x3=0x_1 + 2x_2 + 5x_3 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>5</span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，一个方程三个未知数，一个平面，不奇怪。</p><p>光说“正交”还不够。“正交补”要求更多：零空间不只是有一些和行空间垂直的向量，而是包含了<strong>所有</strong>和行空间垂直的向量。线加平面，<span><span>1+2=31 + 2 = 3</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，刚好填满。换成两条互相垂直的线，虽然也正交，但它们不是正交补，空间里还留着一条空缺。下面我用 SVD 求出行空间与零空间的标准正交基（SVD 要到第 29 讲才讲，这里先当作“能给出两组正交基的黑箱”用），先复现他的例子，再在一个随机矩阵上检查：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>row_and_null_bases</span><span>(</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span></div></div><div><div><div>6</div></div><div><span>) -&gt; tuple[np.ndarray, np.ndarray]:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"""用 SVD 求行空间与零空间的标准正交基，各自作为列返回。"""</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>_, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>int</span><span><span>(np.</span><span>sum</span><span>(s </span><span>&gt;</span><span> tol))</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> Vt[:r].T, Vt[r:].T</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># Strang 的例子：两行成比例，行空间是一条线，零空间是一个平面</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>5.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>, </span><span>10.0</span><span>]])</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>Rb, Nb </span><span>=</span><span> </span><span>row_and_null_bases</span><span>(A)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"维数 r ="</span><span>, Rb.shape[</span><span>1</span><span>], </span><span>"，n - r ="</span><span>, Nb.shape[</span><span>1</span><span>])</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>normal </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>5.0</span><span>])</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"零空间基 ^T (1, 2, 5) ="</span><span><span>, Nb.T </span><span>@</span><span> normal)</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ 零空间基 = 0："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> Nb, </span></span><span>0</span><span>))</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 随机的 4 x 6 矩阵，秩为 3：行空间 3 维，零空间 3 维</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>3</span><span><span>)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((</span></span><span>3</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>Rb, Nb </span><span>=</span><span> </span><span>row_and_null_bases</span><span>(B)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>r, nullity </span><span>=</span><span> Rb.shape[</span><span>1</span><span>], Nb.shape[</span><span>1</span><span>]</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B), </span></span><span>"，r + (n - r) ="</span><span><span>, r </span><span>+</span><span> nullity)</span></span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>cross </span><span>=</span><span> Rb.T </span><span>@</span><span> Nb</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"行空间基 ^T 零空间基 的最大绝对值：</span><span>{</span><span><span>np.</span><span>abs</span><span>(cross).</span><span>max</span><span>()</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 行空间里的任意组合也和零空间垂直（行的组合 = B^T @ c）</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>combo </span><span>=</span><span> B.T </span><span>@</span><span> c</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"行的组合和零空间的点积：</span><span>{</span><span><span>np.</span><span>abs</span><span>(combo </span><span>@</span><span> Nb).</span><span>max</span><span>()</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span># 互补：两组基拼在一起正好是 R^6 的一组基</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>full </span><span>=</span><span> np.</span><span>column_stack</span><span>([Rb, Nb])</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"拼起来的秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(full), </span></span><span>"，满秩 6"</span><span>)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>维数 r = 1 ，n - r = 2</span></div></div><div><div><div>2</div></div><div><span>零空间基 ^T (1, 2, 5) = [0. 0.]</span></div></div><div><div><div>3</div></div><div><span>A @ 零空间基 = 0： True</span></div></div><div><div><div>4</div></div><div><span>B 的秩 = 3 ，r + (n - r) = 6</span></div></div><div><div><div>5</div></div><div><span>行空间基 ^T 零空间基 的最大绝对值：1.0e-16</span></div></div><div><div><div>6</div></div><div><span>行的组合和零空间的点积：4.9e-15</span></div></div><div><div><div>7</div></div><div><span>拼起来的秩 = 6 ，满秩 6</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前三行是他的例子：行空间 1 维，零空间 2 维，零空间的两个基向量和法向量 <span><span>(1,2,5)(1, 2, 5)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span> 的点积都是 0，且 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘零空间基是零矩阵。后四行是随机的 <span><span>B=(4×3)(3×6)B = (4 \times 3)(3 \times 6)</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span><span>)</span><span>(</span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span><span>)</span></span></span></span>，秩为 3：行空间基和零空间基两两的点积最大是 <span><span>1.0×10−161.0 \times 10^{-16}</span><span><span><span></span><span>1.0</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，随机取的行的组合（<span><span>BTcB^\mathsf{T}\mathbf{c}</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>c</span></span></span></span>）与零空间的点积最大是 <span><span>4.9×10−154.9 \times 10^{-15}</span><span><span><span></span><span>4.9</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，都在双精度的舍入范围内。</p><p>最后一行才是“补”的含义：把 3 个行空间基向量和 3 个零空间基向量拼成 <span><span>6×66 \times 6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 的矩阵，秩是 6，也就是说它们合起来是 <span><span>R6\mathbb{R}^6</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 的一组基。如果只是正交而不互补，比如 2 维加 2 维，拼起来的秩最多是 4。</p><p>Strang 把这个叫作线性代数基本定理的第二部分：第一部分是维数（<span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>n−rn-r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>m−rm-r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>，第 10 讲），第二部分是正交，第三部分将是这些子空间的正交基，下面几讲就会走到那里。</p></section>
<section><h2>无解的方程组与 AᵀA<a href="#无解的方程组与-aᵀa"><span>#</span></a></h2><p><strong><span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 没有解的时候怎么办？</strong> 这是他给这一章定的主问题，听起来荒唐，实际上天天遇到：测一颗卫星的位置，测一千次，参数只有六七个；量脉搏，量很多次，未知数只有一个，但每次都有噪声。方程比未知数多，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 几乎不可能落在列空间里。他举的办法是先排除一个：扔掉一些方程，直到剩下方阵再解。这不对，因为没有理由说这几次测量完美、那几次一文不值。</p><p>他的预告是：不要解 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>，改解</p><span><span><span>ATA x^=ATb.A^\mathsf{T}A\,\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}.</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span><span>.</span></span></span></span></span><p>这个 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 读作“x 帽”，是“最好的解”。他先直接把方程抛出来，然后把整节课剩下的时间用来研究 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>：它一定是方阵（<span><span>n×mn \times m</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span> 乘 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span>），一定对称（转置一次，<span><span>(ATA)T=ATA(A^\mathsf{T}A)^\mathsf{T} = A^\mathsf{T}A</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span>）。剩下的问题是：什么时候可逆？</p><p>他的两个例子在这里。第一个是</p><span><span><span>A=[111215],ATA=[38830].A = \begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 2 \\ 1 &amp; 5 \end{bmatrix},\qquad
A^\mathsf{T}A = \begin{bmatrix} 3 &amp; 8 \\ 8 &amp; 30 \end{bmatrix}.</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>30</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>.</span></span></span></span></span><p><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是 <span><span>3×23 \times 2</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span>，两列线性无关，秩为 2；<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的行列式 <span><span>3×30−8×8=263 \times 30 - 8 \times 8 = 26</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>30</span><span></span><span>−</span><span></span></span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span><span></span><span>=</span><span></span></span><span><span></span><span>26</span></span></span></span>，可逆。第二个把第二列换成“三份第一列”，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的秩变成 1，<span><span>ATA=[39927]A^\mathsf{T}A = \begin{bmatrix} 3 &amp; 9 \\ 9 &amp; 27 \end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>27</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，两行成比例，不可逆。他说他不意外，因为秩为 1 的矩阵的乘积秩不会超过 1。</p><p>字幕里他对第二个矩阵说的是“9 和 27”，没有把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 写全；我取的是最自然的候选 <span><span>[131313]\begin{bmatrix} 1 &amp; 3 \\ 1 &amp; 3 \\ 1 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span>，算出来正好是 3、9、9、27，和他念出的数字一致。</p><p>下面一块代码把这两个例子都算了一遍，再加两个东西：1000 个随机矩阵上检查秩是否一直相等，以及 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 无解时解一次 <span><span>ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，看看误差是什么样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>5.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> A.T </span><span>@</span><span> A</span></div></div><div><div><div>8</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T A ="</span><span>)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(G)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"秩 A ="</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(A),</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"，秩 A^T A ="</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(G),</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"，det ="</span><span>,</span></div></div><div><div><div>16</div></div><div><span>        </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(G), </span></span><span>4</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 把第二列换成三份第一列：秩降到 1</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>A1 </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>G1 </span><span>=</span><span> A1.T </span><span>@</span><span> A1</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A1^T A1 ="</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(G1)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>25</div></div><div><span>        </span><span>"秩 A1 ="</span><span>,</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(A1),</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"，秩 A1^T A1 ="</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(G1),</span></div></div><div><div><div>29</div></div><div><span>        </span><span>"，det ="</span><span>,</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>det</span><span>(G1),</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 随机检查：1000 个秩为 1 到 4 的 5 x 4 矩阵，秩 A^T A 是否总等于秩 A</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>equal </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>1000</span><span>):</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> </span><span>int</span><span><span>(rng.</span><span>integers</span><span>(</span></span><span>1</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>5</span><span><span>, k)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((k, </span></span><span>4</span><span>))</span></div></div><div><div><div>39</div></div><div><span>        </span><span>if</span><span><span> np.linalg.</span><span>matrix_rank</span><span>(M.T </span><span>@</span><span> M) </span><span>==</span><span> np.linalg.</span><span>matrix_rank</span><span>(M):</span></span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>equal </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩 A^T A == 秩 A 的次数："</span><span>, equal, </span><span>"/ 1000"</span><span>)</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span># 预告：b 不在列空间时，解 A^T A x = A^T b，误差和每一列垂直</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>46</div></div><div><span>        </span><span>"A x = b 有解吗："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>column_stack</span><span>([A, b])) </span><span>==</span><span> </span></span><span>2</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>x_hat </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(G, A.T </span><span>@</span><span> b)</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> b </span><span>-</span><span> A </span><span>@</span><span> x_hat</span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span><span>"x_hat ="</span><span>, x_hat, </span><span>"，误差 e ="</span><span>, e)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"A^T e ="</span><span><span>, A.T </span><span>@</span><span> e, </span></span><span>"，|e| ="</span><span>, </span><span>round</span><span><span>(np.linalg.</span><span>norm</span><span>(e), </span></span><span>4</span><span>))</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A^T A =</span></div></div><div><div><div>2</div></div><div><span>[[ 3.  8.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 8. 30.]]</span></div></div><div><div><div>4</div></div><div><span>秩 A = 2 ，秩 A^T A = 2 ，det = 26.0</span></div></div><div><div><div>5</div></div><div><span>A1^T A1 =</span></div></div><div><div><div>6</div></div><div><span>[[ 3.  9.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ 9. 27.]]</span></div></div><div><div><div>8</div></div><div><span>秩 A1 = 1 ，秩 A1^T A1 = 1 ，det = 0.0</span></div></div><div><div><div>9</div></div><div><span>秩 A^T A == 秩 A 的次数： 1000 / 1000</span></div></div><div><div><div>10</div></div><div><span>A x = b 有解吗： False</span></div></div><div><div><div>11</div></div><div><span>x_hat = [-0.3077  0.6154] ，误差 e = [ 0.6923 -0.9231  0.2308]</span></div></div><div><div><div>12</div></div><div><span>A^T e = [0. 0.] ，|e| = 1.1767</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前两个例子和字幕完全对上：<span><span>[38830]\begin{bmatrix} 3 &amp; 8 \\ 8 &amp; 30 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>30</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的行列式是 26.0，秩 2；<span><span>[39927]\begin{bmatrix} 3 &amp; 9 \\ 9 &amp; 27 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>9</span></span></span><span><span></span><span><span>27</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的行列式是 0.0，秩 1。1000 个随机矩阵（秩从 1 到 4 不等）里，<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 和 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的秩相等的有 1000 个，没有一个例外。当然这只是数值上的证据，不是证明。</p><p>证明其实很短，他在课上只说了结论，说“等有机会再说明为什么”，我补在这里：如果 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，那么 <span><span>ATAx=0A^\mathsf{T}A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的零空间在 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的零空间里。反过来，如果 <span><span>ATAx=0A^\mathsf{T}A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，两边左乘 <span><span>xT\mathbf{x}^\mathsf{T}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，得到 <span><span>xTATAx=(Ax)T(Ax)=∥Ax∥2=0\mathbf{x}^\mathsf{T}A^\mathsf{T}A\mathbf{x} = (A\mathbf{x})^\mathsf{T}(A\mathbf{x}) = \|A\mathbf{x}\|^2 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>A</span><span>x</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>A</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>∥</span><span>A</span><span>x</span><span><span>∥</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，一个向量的长度平方是零，这个向量只能是零，所以 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。两个零空间相同，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 相同，秩就相同。它用到的恰好是本讲第一节的“<span><span>xTx\mathbf{x}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span> 是长度平方，除了零向量都是正数”。</p><p>结论就是他最后念的：<span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 可逆，当且仅当它的零空间只有零向量，也就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列线性无关。</p><p>最后几行预告了下一讲：对没有解的 <span><span>b=(1,0,3)\mathbf{b} = (1, 0, 3)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，<code>matrix_rank</code> 报告增广矩阵的秩是 3，不是 2，所以 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 没有解。我还是解了 <span><span>ATAx^=ATbA^\mathsf{T}A\hat{\mathbf{x}} = A^\mathsf{T}\mathbf{b}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>b</span></span></span></span>，得到 <span><span>x^=(−0.3077,0.6154)\hat{\mathbf{x}} = (-0.3077, 0.6154)</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.3077</span><span>,</span><span></span><span>0.6154</span><span>)</span></span></span></span>，误差向量 <span><span>e=b−Ax^\mathbf{e} = \mathbf{b} - A\hat{\mathbf{x}}</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span>A</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 的长度是 1.1767（不为零，因为 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 就是不在列空间里），但 <span><span>ATeA^\mathsf{T}\mathbf{e}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>e</span></span></span></span> 是零向量：<span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 和 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的每一列都垂直，也就是 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span> 在左零空间里。这就是为什么今天的“正交”和这一章的主问题连得上：<strong>最好的解让误差落在列空间的正交补里</strong>。</p><p>这一步他今天没有讲，我只是顺手把方程解了一次。本篇只验证了 <span><span>x^\hat{\mathbf{x}}</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 满足这个方程、误差与列垂直，没有证明它在什么意义上“最好”（那是第 15、16 讲的事）。</p><p>下一讲他会正式回答：既然 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 不在列空间，那就找列空间里离它最近的点，这个点叫 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在列空间上的投影。投影矩阵长什么样，为什么它里面藏着 <span><span>(ATA)−1(A^\mathsf{T}A)^{-1}</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，见 <a href="/posts/linear-algebra-15-projections/">18.06 第 15 讲：投影到子空间</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-13-quiz-1-review/</id>
      <title type="text">18.06 第 13 讲：第一次测验复习</title>
      <published>2026-10-18T04:00:00.000Z</published>
      <updated>2026-10-18T04:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-13-quiz-1-review/"/>
      <summary type="text">MIT 18.06 第 13 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 Strang 在课上带着做的旧测验题逐题跑了一遍，其中 10 × 6 的矩阵 C 秩为 6，所以 C 转置的零空间维数是 10 - 6 = 4。</summary>
      <content type="html"><![CDATA[<p>这一讲没有新定理：Strang 翻出几份旧测验，一道一道读题，现场做。我把他做的每道题都交给 NumPy 重算了一遍，发现这些题几乎都靠同一个动作解决：先数形状和秩，再用 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>、<span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 把四个子空间的维数写出来。比如那个 10 × 6 的矩阵 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span>，秩是 6，<span><span>CTC^\mathsf{T}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间维数就是 <span><span>10−6=410 - 6 = 4</span><span><span><span></span><span>10</span><span></span><span>−</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>，全程不需要消元。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=l88D4r74gtM" target="_blank">Lecture 13: Quiz 1 review</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-13-quiz-1-review/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 第 1 到 3 章
前置：<a href="/posts/linear-algebra-12-graphs-and-incidence-matrices/">第 12 讲：图、网络与关联矩阵</a>
一句话：测验题的套路是“先看形状和秩，再用四个子空间的维数公式回答”，能不做乘法就不做乘法。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>这一讲是纯做题，本篇按他的出题顺序重算，每一题都有代码。想自己先想一遍，就先暂停视频，想完再对答案。</li>
<li>他做 <span><span>B=CDB = CD</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>D</span></span></span></span> 那道题时，“左乘可逆矩阵不改变零空间”这句话值得认真听，后面很多地方会用到。</li>
<li>最后几道判断题（交换两行、<span><span>(1,2,3)(1, 2, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 为什么不能同时是一行和零空间里的向量）很短，但它们直接通向下一讲，别倍速。</li>
<li>字幕是自动转写的，题目里的矩阵在黑板上，转写里经常缺数字。下面凡是我根据字幕上下文还原的矩阵，我都会说明。</li>
</ul></div></div>
<section><h2>三个向量张成几维<a href="#三个向量张成几维"><span>#</span></a></h2><p>第一题：<span><span>u\mathbf{u}</span><span><span><span></span><span>u</span></span></span></span>、<span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span>、<span><span>w\mathbf{w}</span><span><span><span></span><span>w</span></span></span></span> 是 <span><span>R7\mathbb{R}^7</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span></span></span></span> 里的非零向量，它们张成的子空间可能是几维？</p><p>答案是 1、2 或 3。不会是 0，因为向量非零（全是零向量才会得到零维子空间）；不会超过 3，因为只有三个向量。三种维数各举一个例子：三个都是 <span><span>u\mathbf{u}</span><span><span><span></span><span>u</span></span></span></span> 的倍数得 1 维，<span><span>w=u+v\mathbf{w} = \mathbf{u} + \mathbf{v}</span><span><span><span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span>u</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span> 得 2 维，三个随机向量得 3 维。这个检验放在下一节的代码块开头。</p></section>
<section><h2>5 × 3 的阶梯形与它的两个拼接<a href="#5--3-的阶梯形与它的两个拼接"><span>#</span></a></h2><p><strong>第二题是整场复习的主菜。</strong> <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 是 5 × 3 的阶梯形矩阵，有三个主元（<span><span>r=3r = 3</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>）。他连着问了几个小问题：</p><ol>
<li><span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的零空间是什么？三列无关，所以只有零向量。</li>
<li><span><span>B=[U2U]B = \begin{bmatrix} U \\ 2U \end{bmatrix}</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>U</span></span></span><span><span></span><span><span>2</span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 是 10 × 3，它的阶梯形是什么，秩是多少？</li>
<li><span><span>C=[UUU0]C = \begin{bmatrix} U &amp; U \\ U &amp; 0 \end{bmatrix}</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>U</span></span></span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>U</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 是 10 × 6，秩是多少？</li>
<li><span><span>CTC^\mathsf{T}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间维数是多少？</li>
</ol><p>对 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span>，他的想法是把消元看成块操作：第一块乘 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 加到第二块，得到 <span><span>[U0]\begin{bmatrix} U \\ 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>U</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，所以秩还是 3。对 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span>，第一行块减去第二行块得到 <span><span>[0U]\begin{bmatrix} 0 &amp; U \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，整理成 <span><span>[U00U]\begin{bmatrix} U &amp; 0 \\ 0 &amp; U \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>U</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的样子，每一块有 3 个主元，一共 6 个，秩是 6。最后一问：<span><span>CTC^\mathsf{T}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是 6 × 10，零空间在 <span><span>R10\mathbb{R}^{10}</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 里，维数是 <span><span>m−r=10−6=4m - r = 10 - 6 = 4</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span><span></span><span>−</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>。</p><p>字幕里没有 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的具体数字，我自己选了一个上三角的例子，下面的脚本同时验证了第一题的三种维数，以及这一题的每一问：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># R^7 里三个非零向量张成的子空间，维数只能是 1、2、3</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>7</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>7</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>cases </span><span>=</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"三个都是 u 的倍数"</span><span><span>: np.</span><span>column_stack</span><span>([u, </span></span><span>2</span><span><span> </span><span>*</span><span> u, </span><span>-</span></span><span>3</span><span><span> </span><span>*</span><span> u]),</span></span></div></div><div><div><div>13</div></div><div><span>        </span><span>"w = u + v"</span><span><span>: np.</span><span>column_stack</span><span>([u, v, u </span><span>+</span><span> v]),</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>"三个随机向量"</span><span><span>: rng.</span><span>standard_normal</span><span>((</span></span><span>7</span><span>, </span><span>3</span><span>)),</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> name, M </span><span>in</span><span><span> cases.</span><span>items</span><span>():</span></span></div></div><div><div><div>17</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：维数 = </span><span>{</span><span><span>np.linalg.</span><span>matrix_rank</span><span>(M)</span></span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 5 x 3 的阶梯形 U，三个主元</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>],</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>5.0</span><span>],</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>6.0</span><span>],</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"U 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(U))</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span># 零空间只有零向量：U @ x = 0 的最小二乘解就是 0，且 U 的最小奇异值 &gt; 0</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"U 的奇异值："</span><span><span>, np.linalg.</span><span>svd</span><span>(U, </span></span><span>compute_uv</span><span>=</span><span>False</span><span>))</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># B = [U; 2U]：把第一块乘 -2 加到第二块，消元一步就得到 [U; 0]</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>vstack</span><span>([U, </span><span>2</span><span><span> </span><span>*</span><span> U])</span></span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> np.</span><span>block</span><span>([[np.</span><span>eye</span><span>(</span><span>5</span><span><span>), np.</span><span>zeros</span><span>((</span></span><span>5</span><span>, </span><span>5</span><span><span>))], [</span><span>-</span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>5</span><span><span>), np.</span><span>eye</span><span>(</span></span><span>5</span><span>)]])</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B))</span></span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"E @ B 等于 [U; 0]："</span><span><span>, np.</span><span>allclose</span><span>(E </span><span>@</span><span> B, np.</span><span>vstack</span><span>([U, </span></span><span>0</span><span><span> </span><span>*</span><span> U])))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># C = [[U, U], [U, 0]]，10 x 6</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>block</span><span>([[U, U], [U, np.</span><span>zeros</span><span>((</span><span>5</span><span>, </span><span>3</span><span>))]])</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(C)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"C 的形状："</span><span>, C.shape, </span><span>"，秩："</span><span>, r)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"N(C^T) 的维数 = m - r ="</span><span>, C.shape[</span><span>0</span><span><span>] </span><span>-</span><span> r)</span></span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span># 用奇异值分解直接取出 N(C^T) 的一组基并验证</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>left, s, _ </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(C)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>Y </span><span>=</span><span> left[:, r:]</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>49</div></div><div><span>        </span><span>"N(C^T) 基的列数："</span><span>,</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>Y.shape[</span><span>1</span><span>],</span></div></div><div><div><div>51</div></div><div><span>        </span><span>"，C^T @ Y = 0："</span><span>,</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(C.T </span><span>@</span><span> Y, </span><span>0</span><span>),</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>三个都是 u 的倍数：维数 = 1</span></div></div><div><div><div>2</div></div><div><span>w = u + v：维数 = 2</span></div></div><div><div><div>3</div></div><div><span>三个随机向量：维数 = 3</span></div></div><div><div><div>4</div></div><div><span>U 的秩： 3</span></div></div><div><div><div>5</div></div><div><span>U 的奇异值： [9.0125 2.9975 0.8884]</span></div></div><div><div><div>6</div></div><div><span>B 的秩： 3</span></div></div><div><div><div>7</div></div><div><span>E @ B 等于 [U; 0]： True</span></div></div><div><div><div>8</div></div><div><span>C 的形状： (10, 6) ，秩： 6</span></div></div><div><div><div>9</div></div><div><span>N(C^T) 的维数 = m - r = 4</span></div></div><div><div><div>10</div></div><div><span>N(C^T) 基的列数： 4 ，C^T @ Y = 0： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的秩没有因为多了一块 <span><span>2U2U</span><span><span><span></span><span>2</span><span>U</span></span></span></span> 而增加：<span><span>E@B=[U0]E \mathbin{@} B = \begin{bmatrix} U \\ 0 \end{bmatrix}</span><span><span><span></span><span>E</span><span></span><span><span>@</span></span><span></span></span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>U</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，多出来的五行全被消成了零。<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的秩却从 3 涨到 6：消元之后左右两块各自带来三个新的主元，没有谁重复谁。最后用 SVD 取出 <span><span>N(CT)N(C^\mathsf{T})</span><span><span><span></span><span>N</span><span>(</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 的四个基向量，<span><span>CTYC^\mathsf{T} Y</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Y</span></span></span></span> 确实是零。（换成任何有三个主元的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>，这些答案都不变，它们只依赖形状和秩。）</p></section>
<section><h2>只给完全解，倒推矩阵<a href="#只给完全解倒推矩阵"><span>#</span></a></h2><p><strong>这类题的出法有点反着来：</strong> 不给矩阵，只给 <span><span>Ax=(2,4,2)A\mathbf{x} = (2, 4, 2)</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 的完全解，让你回答行空间的维数、矩阵本身、有解的条件。据我对字幕上下文的还原，完全解是</p><span><span><span>x=[200]+c1[110]+c2[001]\mathbf{x} =
\begin{bmatrix} 2 \\ 0 \\ 0 \end{bmatrix} +
c_1 \begin{bmatrix} 1 \\ 1 \\ 0 \end{bmatrix} +
c_2 \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>字幕里这几个向量的数字没有完整转写出来，我是靠他后面的推理反推的：他说第一列是 <span><span>(1,2,1)(1, 2, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，第二列是 <span><span>(−1,−2,−1)(-1, -2, -1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，最后一列全零。这三句话和上面的向量是自洽的。</p><p>推理很直接。完全解里有两个无关的零空间向量，所以零空间是 2 维，<span><span>n=3n = 3</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，秩 <span><span>r=3−2=1r = 3 - 2 = 1</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。矩阵怎么找：<span><span>Axp=bA \mathbf{x}_p = \mathbf{b}</span><span><span><span></span><span>A</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 而 <span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 只有第一个分量非零，所以第一列是 <span><span>b/2=(1,2,1)\mathbf{b} / 2 = (1, 2, 1)</span><span><span><span></span><span>b</span><span>/2</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>；<span><span>(0,0,1)(0, 0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 在零空间里，说明第三列是零；<span><span>(1,1,0)(1, 1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 在零空间里，说明第二列是第一列的相反数。结果</p><span><span><span>A=[1−102−201−10]A = \begin{bmatrix} 1 &amp; -1 &amp; 0 \\ 2 &amp; -2 &amp; 0 \\ 1 &amp; -1 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>列空间就是 <span><span>(1,2,1)(1, 2, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 的所有倍数，所以 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 有解，当且仅当 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 是 <span><span>(1,2,1)(1, 2, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 的倍数（他回答“能不能对每个 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 都有解”时说的是“不能”）。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 题目只给了完全解 x = x_p + c1 * n1 + c2 * n2，右端项是 b，没给矩阵</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>4.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>x_p </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>n1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>n2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># A @ x_p = b：x_p 只取第一列，所以第一列 = b / 2</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>col1 </span><span>=</span><span> b </span><span>/</span><span> x_p[</span><span>0</span><span>]</span></div></div><div><div><div>16</div></div><div><span>    </span><span># A @ n2 = 0：最后一列是零；A @ n1 = 0：第二列 = -第一列</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>column_stack</span><span>([col1, </span><span>-</span><span>col1, np.</span><span>zeros</span><span>(</span><span>3</span><span>)])</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"A ="</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(A)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x_p = b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x_p, b))</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ n1 = 0："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> n1, </span></span><span>0</span><span>))</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ n2 = 0："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> n2, </span></span><span>0</span><span>))</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"行空间维数 = 秩 ="</span><span>, r, </span><span>"，零空间维数 ="</span><span>, </span><span>3</span><span><span> </span><span>-</span><span> r)</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 任意 c1、c2 都给出解</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>c1, c2 </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> x_p </span><span>+</span><span> c1 </span><span>*</span><span> n1 </span><span>+</span><span> c2 </span><span>*</span><span> n2</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"随机取 c1、c2，A @ x = b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, b))</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 什么样的 b 有解：b 必须在列空间里，也就是 (1, 2, 1) 的倍数</span></div></div><div><div><div>34</div></div><div><span>    </span><span>for</span><span> rhs </span><span>in</span><span><span> (np.</span><span>array</span><span>([</span></span><span>3.0</span><span>, </span><span>6.0</span><span>, </span><span>3.0</span><span><span>]), np.</span><span>array</span><span>([</span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])):</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>solvable </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>column_stack</span><span>([A, rhs])) </span><span>==</span><span> r</span></div></div><div><div><div>36</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"b = </span><span>{</span><span>rhs</span><span>}</span><span>，有解："</span><span>, solvable)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A =</span></div></div><div><div><div>2</div></div><div><span>[[ 1. -1.  0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 2. -2.  0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 1. -1.  0.]]</span></div></div><div><div><div>5</div></div><div><span>A @ x_p = b： True</span></div></div><div><div><div>6</div></div><div><span>A @ n1 = 0： True</span></div></div><div><div><div>7</div></div><div><span>A @ n2 = 0： True</span></div></div><div><div><div>8</div></div><div><span>行空间维数 = 秩 = 1 ，零空间维数 = 2</span></div></div><div><div><div>9</div></div><div><span>随机取 c1、c2，A @ x = b： True</span></div></div><div><div><div>10</div></div><div><span>b = [3. 6. 3.]，有解： True</span></div></div><div><div><div>11</div></div><div><span>b = [1. 0. 0.]，有解： False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>行空间维数是 1，零空间维数是 2，加起来是 <span><span>n=3n = 3</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>。<span><span>b=(3,6,3)\mathbf{b} = (3, 6, 3)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>6</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 有解，<span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 没有。他在这里提醒了一句：别忘了秩等于 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 或等于 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 的那些好情形，上一章讲过，这道题只是“秩很小、零空间很大”的那一头。</p></section>
<section><h2>四道判断题<a href="#四道判断题"><span>#</span></a></h2><p><strong>判断题没有一道需要计算，但每一道都值得举反例。</strong> 他说真正的测验里不会有判断题，这几道只是拿来快速复习。</p><ul>
<li>若方阵的零空间只有零向量，则 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间也只有零向量。对：已知 <span><span>n−r=0n - r = 0</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，而 <span><span>m=nm = n</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>，所以 <span><span>m−r=0m - r = 0</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。他强调这是个非常重要的事实。</li>
<li>5 × 5 矩阵构成一个 25 维的向量空间，其中可逆的矩阵构成子空间吗？不：零矩阵不在里面，而且两个可逆矩阵的和可能奇异（<span><span>II</span><span><span><span></span><span>I</span></span></span></span> 加 <span><span>−I-I</span><span><span><span></span><span>−</span><span>I</span></span></span></span> 就是零矩阵）。奇异矩阵也不行，因为两个奇异矩阵的和可能可逆。</li>
<li>若 <span><span>B2=0B^2 = 0</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，则 <span><span>B=0B = 0</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>？错，经典反例是 <span><span>[0100]\begin{bmatrix} 0 &amp; 1 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。他管它叫“危险的矩阵”，说后面会反复出现，我猜是讲到特征值和 Jordan 形的时候。</li>
<li><span><span>m×mm \times m</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span> 的矩阵，列线性无关，则每个右端项都有解？对：秩是 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>，矩阵可逆，什么都不会出错。他在这里拿观众投票开玩笑，结果五五开（第十三讲了，台下还有一半人举手说不一定，我替他们捏一把汗）。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>nullity</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> M.shape[</span><span>1</span><span><span>] </span><span>-</span><span> np.linalg.</span><span>matrix_rank</span><span>(M)</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 1. 方阵 A 的零空间只有零向量，A^T 的零空间也只有零向量</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"dim N(A) ="</span><span><span>, </span><span>nullity</span><span>(A), </span></span><span>"，dim N(A^T) ="</span><span><span>, </span><span>nullity</span><span>(A.T))</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># 2. 5 x 5 的可逆矩阵不构成子空间：没有零矩阵，和也可能奇异</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>5</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> </span><span>-</span><span>np.</span><span>eye</span><span>(</span><span>5</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"det(P) ="</span><span><span>, np.linalg.</span><span>det</span><span>(P), </span></span><span>"，det(Q) ="</span><span><span>, np.linalg.</span><span>det</span><span>(Q))</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"P + Q 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(P </span><span>+</span><span> Q), </span></span><span>"（零矩阵，不可逆）"</span><span>)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># 3. B^2 = 0 推不出 B = 0</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"B @ B ="</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span><span>(B </span><span>@</span><span> B)</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 4. m x m 且列无关，则对每个右端项 Ax = b 有解</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>6</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>worst </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>30</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>1000</span><span>):</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>b </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(M, b)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>worst </span><span>=</span><span> </span><span>max</span><span><span>(worst, np.linalg.</span><span>norm</span><span>(M </span><span>@</span><span> x </span><span>-</span><span> b))</span></span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>rk </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(M)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩 ="</span><span>, rk, </span><span>"，1000 个随机 b 的最大残差 ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span>worst</span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>dim N(A) = 0 ，dim N(A^T) = 0</span></div></div><div><div><div>2</div></div><div><span>det(P) = 1.0 ，det(Q) = -1.0</span></div></div><div><div><div>3</div></div><div><span>P + Q 的秩： 0 （零矩阵，不可逆）</span></div></div><div><div><div>4</div></div><div><span>B @ B =</span></div></div><div><div><div>5</div></div><div><span>[[0. 0.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[0. 0.]]</span></div></div><div><div><div>7</div></div><div><span>秩 = 6 ，1000 个随机 b 的最大残差 = 1.3e-14</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一题对一个随机 5 × 5 矩阵，两个零空间的维数都是 0。第二题的 <span><span>I+(−I)I + (-I)</span><span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>I</span><span>)</span></span></span></span> 秩是 0。第四题我对 1000 个随机右端项求解，最大残差 1.3e-14，是舍入误差的量级，而不是“有时有解”。</p></section>
<section><h2>分解乘积 B = CD：左乘可逆矩阵不改变零空间<a href="#分解乘积-b--cd左乘可逆矩阵不改变零空间"><span>#</span></a></h2><p>下一题给的是一个以乘积形式出现的矩阵，<span><span>B=CDB = CD</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>D</span></span></span></span>：<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 是 3 × 3 的，<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是 3 × 4 的阶梯形。我根据字幕还原出的两个因子是</p><span><span><span>C=[110010101],D=[10−12011−10000]C = \begin{bmatrix} 1 &amp; 1 &amp; 0 \\ 0 &amp; 1 &amp; 0 \\ 1 &amp; 0 &amp; 1 \end{bmatrix},\quad
D = \begin{bmatrix} 1 &amp; 0 &amp; -1 &amp; 2 \\ 0 &amp; 1 &amp; 1 &amp; -1 \\ 0 &amp; 0 &amp; 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>字幕里数字是连着念出来的，我是按行断开的。我用来核对的是：他说 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第一列是 <span><span>(1,0,1)(1, 0, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，我还原出的 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 和 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 相乘确实给出这一列；他读出的特解里的 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>、<span><span>11</span><span><span><span></span><span>1</span></span></span></span>、<span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 也都对得上。</p><p><strong>这一题的关键是：</strong> <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 可逆，所以 <span><span>CDCD</span><span><span><span></span><span>C</span><span>D</span></span></span></span> 和 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 有相同的零空间。若 <span><span>Dx=0D\mathbf{x} = 0</span><span><span><span></span><span>D</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 则 <span><span>CDx=0CD\mathbf{x} = 0</span><span><span><span></span><span>C</span><span>D</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；反过来 <span><span>CDx=0CD\mathbf{x} = 0</span><span><span><span></span><span>C</span><span>D</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时左乘 <span><span>C−1C^{-1}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 就得到 <span><span>Dx=0D\mathbf{x} = 0</span><span><span><span></span><span>D</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。因此 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的零空间不用乘出来看，直接读 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span>：主元在前两列，自由变量是第 3、4 个，特解是 <span><span>[−FI]\begin{bmatrix} -F \\ I \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>F</span></span></span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的两列，其中 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span> 是 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 右上角的 2 × 2 块。</p><p>他接着解 <span><span>Bx=(1,0,1)B\mathbf{x} = (1, 0, 1)</span><span><span><span></span><span>B</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第一列恰好就是右端项，所以特解取 <span><span>(1,0,0,0)(1, 0, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，完全解是特解加上两个特解的任意组合。有学生问“特解是不是唯一的”，他的回答是不唯一，挑最方便的那个。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># B = C @ D：C 是 3 x 3，D 是 3 x 4 的阶梯形</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span>],</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>],</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> C </span><span>@</span><span> D</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"B ="</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(B)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>20</div></div><div><span>        </span><span>"det(C) ="</span><span>,</span></div></div><div><div><div>21</div></div><div><span>        </span><span>round</span><span><span>(np.linalg.</span><span>det</span><span>(C), </span></span><span>6</span><span>),</span></div></div><div><div><div>22</div></div><div><span>        </span><span>"，秩(B) ="</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(B),</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 零空间：D 的自由变量是第 3、4 个，特解 = [-F; I]</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>F </span><span>=</span><span> D[:</span><span>2</span><span>, </span><span>2</span><span>:]</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>vstack</span><span>([</span><span>-</span><span>F, np.</span><span>eye</span><span>(</span><span>2</span><span>)])</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"特解矩阵 N（每列一个特解）："</span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(N)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"D @ N = 0："</span><span><span>, np.</span><span>allclose</span><span>(D </span><span>@</span><span> N, </span></span><span>0</span><span>))</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"B @ N = 0："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> N, </span></span><span>0</span><span>))</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span># 解 B x = (1, 0, 1)：B 的第一列恰好是它</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>x_p </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的第一列 ="</span><span>, B[:, </span><span>0</span><span>], </span><span>"，B @ x_p = b："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> x_p, b))</span></span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> x_p </span><span>+</span><span> N </span><span>@</span><span> c</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"x_p + N @ c 仍是解："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> x, b))</span></span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>B =</span></div></div><div><div><div>2</div></div><div><span>[[ 1.  1.  0.  1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.  1.  1. -1.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 1.  0. -1.  2.]]</span></div></div><div><div><div>5</div></div><div><span>det(C) = 1.0 ，秩(B) = 2</span></div></div><div><div><div>6</div></div><div><span>特解矩阵 N（每列一个特解）：</span></div></div><div><div><div>7</div></div><div><span>[[ 1. -2.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[-1.  1.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[ 1.  0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>11</div></div><div><span>D @ N = 0： True</span></div></div><div><div><div>12</div></div><div><span>B @ N = 0： True</span></div></div><div><div><div>13</div></div><div><span>B 的第一列 = [1. 0. 1.] ，B @ x_p = b： True</span></div></div><div><div><div>14</div></div><div><span>x_p + N @ c 仍是解： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的秩是 2，4 列减 2 个主元，零空间是 2 维，和 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的两列一致。<code>B @ N</code> 和 <code>D @ N</code> 都是零，验证了“左乘可逆的 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 不改变零空间”。他还说，“特解加零空间”这个模式不只限于有限矩阵，它在整个线性系统的数学里都在用。</p></section>
<section><h2>四个子空间的几道判断题<a href="#四个子空间的几道判断题"><span>#</span></a></h2><p>最后一批题，都是在问“哪个子空间变了，哪个没变”。</p><ul>
<li>方阵的行空间等于列空间吗？不一定。<span><span>[0100]\begin{bmatrix} 0 &amp; 1 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的行空间是 <span><span>(0,1)(0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 的倍数，列空间是 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 的倍数，完全不同。对称矩阵能保证两者相等。</li>
<li><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>−A-A</span><span><span><span></span><span>−</span><span>A</span></span></span></span> 有相同的四个子空间吗？有。</li>
<li>两个矩阵有相同的四个子空间，一个就是另一个的倍数吗？错。反例：任意两个可逆的 6 × 6 矩阵，四个子空间都是 <span><span>R6\mathbb{R}^6</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 或 <span><span>{0}\{0\}</span><span><span><span></span><span>{</span><span>0</span><span>}</span></span></span></span>。他顺便说，它们至少有相同的秩，但真正能说的更多，他没有展开。</li>
<li>交换 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的两行，哪些子空间不变？行空间和零空间不变，列空间会变。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rank</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>int</span><span><span>(np.linalg.</span><span>matrix_rank</span><span>(M))</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 1. 方阵的行空间不一定等于列空间</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>row_vec </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>col_vec </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>], [</span><span>0.0</span><span>]])</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"(0, 1) 在行空间里："</span><span><span>, </span><span>rank</span><span>(np.</span><span>vstack</span><span>([N, row_vec])) </span><span>==</span><span> </span><span>rank</span><span>(N))</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"(1, 0) 在行空间里："</span><span><span>, </span><span>rank</span><span>(np.</span><span>vstack</span><span>([N, col_vec.T])) </span><span>==</span><span> </span><span>rank</span><span>(N))</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"(1, 0) 在列空间里："</span><span><span>, </span><span>rank</span><span>(np.</span><span>hstack</span><span>([N, col_vec])) </span><span>==</span><span> </span><span>rank</span><span>(N))</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 2. A 和 -A 的四个子空间相同</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>3</span><span><span>)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((</span></span><span>3</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的秩："</span><span><span>, </span><span>rank</span><span>(A), </span></span><span>"，A 是 4 x 5"</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"列空间相同："</span><span><span>, </span><span>rank</span><span>(np.</span><span>hstack</span><span>([A, </span><span>-</span><span>A])) </span><span>==</span><span> </span><span>rank</span><span>(A))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"行空间相同："</span><span><span>, </span><span>rank</span><span>(np.</span><span>vstack</span><span>([A, </span><span>-</span><span>A])) </span><span>==</span><span> </span><span>rank</span><span>(A))</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 3. 四个子空间相同，不代表差一个倍数：两个可逆的 6 x 6</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>6</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>Y </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>6</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, </span><span>rank</span><span>(X), </span><span>rank</span><span>(Y), </span></span><span>"，X / Y 的比值："</span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span><span>((X </span><span>/</span><span> Y)[</span></span><span>0</span><span>, :</span><span>3</span><span>])</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 4. 交换两行：行空间、零空间不变，列空间变</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>T </span><span>=</span><span> S[[</span><span>1</span><span>, </span><span>0</span><span>, </span><span>2</span><span>]]</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>37</div></div><div><span>        </span><span>"S @ n = 0，T @ n = 0："</span><span><span>, np.</span><span>allclose</span><span>(S </span><span>@</span><span> n, </span></span><span>0</span><span><span>), np.</span><span>allclose</span><span>(T </span><span>@</span><span> n, </span></span><span>0</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"行空间不变："</span><span><span>, </span><span>rank</span><span>(np.</span><span>vstack</span><span>([S, T])) </span><span>==</span><span> </span><span>rank</span><span>(S))</span></span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"列空间不变："</span><span><span>, </span><span>rank</span><span>(np.</span><span>hstack</span><span>([S, T])) </span><span>==</span><span> </span><span>rank</span><span>(S))</span></span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span># 5. (1, 2, 3) 不能既是某一行，又在零空间里</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>vstack</span><span>([v, rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>3</span><span>))])</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"M 的第一行乘 v ="</span><span><span>, (M </span><span>@</span><span> v)[</span></span><span>0</span><span>], </span><span>"，v 点乘 v ="</span><span><span>, v </span><span>@</span><span> v)</span></span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(0, 1) 在行空间里： True</span></div></div><div><div><div>2</div></div><div><span>(1, 0) 在行空间里： False</span></div></div><div><div><div>3</div></div><div><span>(1, 0) 在列空间里： True</span></div></div><div><div><div>4</div></div><div><span>A 的秩： 3 ，A 是 4 x 5</span></div></div><div><div><div>5</div></div><div><span>列空间相同： True</span></div></div><div><div><div>6</div></div><div><span>行空间相同： True</span></div></div><div><div><div>7</div></div><div><span>秩： 6 6 ，X / Y 的比值：</span></div></div><div><div><div>8</div></div><div><span>[-1.5896  2.3334 -0.5318]</span></div></div><div><div><div>9</div></div><div><span>S @ n = 0，T @ n = 0： True True</span></div></div><div><div><div>10</div></div><div><span>行空间不变： True</span></div></div><div><div><div>11</div></div><div><span>列空间不变： False</span></div></div><div><div><div>12</div></div><div><span>M 的第一行乘 v = 14.0 ，v 点乘 v = 14.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个小题各对一类：<span><span>[0100]\begin{bmatrix} 0 &amp; 1 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的 <span><span>(0,1)(0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 在行空间里，<span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 不在；<span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 在列空间里。两个随机的可逆矩阵逐元素相除，比值是 <span><span>−1.5896-1.5896</span><span><span><span></span><span>−</span><span>1.5896</span></span></span></span>、<span><span>2.33342.3334</span><span><span><span></span><span>2.3334</span></span></span></span>、<span><span>−0.5318-0.5318</span><span><span><span></span><span>−</span><span>0.5318</span></span></span></span>，不是常数，所以谁也不是谁的倍数。交换两行后，<span><span>n=(1,1,−1)\mathbf{n} = (1, 1, -1)</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 仍然在零空间里，行空间不变，列空间变了。</p></section>
<section><h2>最后一题：为什么 (1, 2, 3) 不能同时是一行和零空间里的向量<a href="#最后一题为什么-1-2-3-不能同时是一行和零空间里的向量"><span>#</span></a></h2><p>他用这道题收尾，说它引向下一章。如果 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第一行是 <span><span>(1,2,3)(1, 2, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，<span><span>v=(1,2,3)\mathbf{v} = (1, 2, 3)</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 又在零空间里，那 <span><span>AvA\mathbf{v}</span><span><span><span></span><span>A</span><span>v</span></span></span></span> 的第一个分量就是</p><span><span><span>1⋅1+2⋅2+3⋅3=141 \cdot 1 + 2 \cdot 2 + 3 \cdot 3 = 14</span><span><span><span></span><span>1</span><span></span><span>⋅</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span></span><span>⋅</span><span></span></span><span><span></span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span></span><span>⋅</span><span></span></span><span><span></span><span>3</span><span></span><span>=</span><span></span></span><span><span></span><span>14</span></span></span></span></span><p>不是零，矛盾。代码块最后一行用随机矩阵确认了这个 14：不管另外两行是什么，第一个分量都是 <span><span>v⋅v=14\mathbf{v} \cdot \mathbf{v} = 14</span><span><span><span></span><span>v</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>14</span></span></span></span>。</p><p>这说明行空间和零空间同在 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span> 里，却只共享零向量。更准确的说法是：零空间垂直于行空间。他说这是下一讲的内容，并祝大家考试顺利。</p><p>我读完这份复习有一个感受：每一道题我都是先看形状，再数秩，最后才碰数字。这像编程里先看函数签名再读函数体，能省下大把时间。（反过来说，我花在“还原字幕里被吞掉的数字”上的时间，大概是真正做题的三倍。）</p><p>下一讲是考试之后的第一讲，主题正是上面最后一题留下的：正交。见 <a href="/posts/linear-algebra-14-orthogonal-subspaces/">18.06 第 14 讲：正交向量与正交子空间</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-12-graphs-and-incidence-matrices/</id>
      <title type="text">18.06 第 12 讲：图、网络与关联矩阵</title>
      <published>2026-10-18T03:40:00.000Z</published>
      <updated>2026-10-18T03:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-12-graphs-and-incidence-matrices/"/>
      <summary type="text">MIT 18.06 第 12 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 搭了 Strang 的 4 节点 5 条边的关联矩阵，验证零空间是常数电势、左零空间是两个回路，又在 30 × 30 的网格上数出 841 个回路，节点数减边数加回路数恰好是 1。</summary>
      <content type="html"><![CDATA[<p>前十一讲里，Strang 的矩阵都是他在黑板上现编的，他自己在这一讲开头也承认了，还说有点内疚。这一讲他给了一个“有来历”的矩阵：画一张 4 个节点、5 条边的图，每条边一行，记下它从哪个节点出发、到哪个节点结束。这个 5 × 4 的矩阵每行只有两个非零元，四个基本子空间里的两个各自对应图上一个看得见的东西：零空间是“所有节点电势相等”，左零空间是“回路”。我把这个图放大成 30 × 30 的网格（900 个节点，1740 条边），左零空间的维数是 841，恰好等于网格里小方格的个数。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=6-wh6yvk6uc" target="_blank">Lecture 12: Graphs, networks, incidence matrices</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-12-graphs-networks-incidence-matrices/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版把这部分内容拆散了，我没有在目录里找到独立的一节
前置：<a href="/posts/linear-algebra-11-matrix-spaces-rank-one/">第 11 讲：矩阵空间、秩 1 矩阵与小世界图</a>
一句话：图的关联矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 把节点电势变成边上的电势差，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的零空间是常数电势，<span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间是回路，两者的维数合起来就是 Euler 公式。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头一段是他讲化学教授怎么做行化简、MathWorks 的生日聚会，可以跳过。从他说“我感觉好多了，今天要讲应用数学里最重要的模型”开始看。</li>
<li>他画图、给边编号、写出 5 × 4 矩阵的那一段要认真看，他边写边改编号，自己也被绕了一下（编号数了两遍）。我的代码里的边序和他最后定下来的一致。</li>
<li>他用“势差、电流、基尔霍夫电流定律”解释 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 和 <span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的那一段，是这一讲的核心。找两个回路当基的那几分钟本篇算过，可以倍速，但“为什么外圈那个回路不是第三个基向量”值得停一下。</li>
<li>最后他把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>、<span><span>CC</span><span><span><span></span><span>C</span></span></span></span>、<span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 串成一条链，并问 <span><span>ATCAA^\mathsf{T}CA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>A</span></span></span></span> 有什么性质。我在最后一节用代码解了一个小电路，可以对照着看。</li>
</ul></div></div>
<section><h2>一张图，一个矩阵<a href="#一张图一个矩阵"><span>#</span></a></h2><p><strong>图是什么？</strong> 节点加边，仅此而已。Strang 说他先随手画一个小的：四个节点，五条边。他提醒过，真实的图可以是所有网页、所有电话、所有人，这个只是玩具。</p><p>他给每条边定了一个方向，写在箭头上：</p><ul>
<li>边 1：节点 1 到节点 2</li>
<li>边 2：节点 2 到节点 3</li>
<li>边 3：节点 1 到节点 3</li>
<li>边 4：节点 1 到节点 4</li>
<li>边 5：节点 3 到节点 4</li>
</ul><p>方向是任意定的，只用来区分正负：电流顺着箭头流算正，逆着算负。接着是关联矩阵（incidence matrix）：每条边一行，每个节点一列，起点处写 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，终点处写 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span>，其余是 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>。图里 <span><span>n=4n = 4</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span> 个节点、<span><span>m=5m = 5</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 条边，所以是 <span><span>m×n=5×4m \times n = 5 \times 4</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的矩阵：</p><span><span><span>A=[−11000−110−1010−100100−11]A = \begin{bmatrix}
-1 &amp; 1 &amp; 0 &amp; 0 \\
0 &amp; -1 &amp; 1 &amp; 0 \\
-1 &amp; 0 &amp; 1 &amp; 0 \\
-1 &amp; 0 &amp; 0 &amp; 1 \\
0 &amp; 0 &amp; -1 &amp; 1
\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>每行恰好两个非零元，所以全矩阵的非零元个数是 <span><span>2m2m</span><span><span><span></span><span>2</span><span>m</span></span></span></span>。图一大，矩阵就是个又大又稀疏的矩阵，而且结构是图给的，不是我们编的。他的原话大意是：来自真实问题的矩阵有结构，用的人会利用这个结构。</p><p>脚本里我写了一个 <code>incidence</code> 函数，把边表变成矩阵。后面几个脚本都复用它（每个脚本得能单独跑，所以每次重贴）。先看矩阵本身，再验证他在黑板上做的几件事：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>incidence</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>edges</span></span><span>:</span><span> list[tuple[</span><span>int</span><span>, </span><span>int</span><span>]]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""每条边一行：起点 -1，终点 +1（节点编号从 1 开始）。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>len</span><span>(edges), n))</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> k, (tail, head) </span><span>in</span><span> </span><span>enumerate</span><span>(edges):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>A[k, tail </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>1.0</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>A[k, head </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> A</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>edges </span><span>=</span><span> [(</span><span>1</span><span>, </span><span>2</span><span>), (</span><span>2</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>4</span><span>), (</span><span>3</span><span>, </span><span>4</span><span>)]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>incidence</span><span>(</span><span>4</span><span>, edges)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(A)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"形状："</span><span>, A.shape, </span><span>"，非零元个数："</span><span><span>, np.</span><span>count_nonzero</span><span>(A))</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># A @ x 就是每条边两端的电势差</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>diff </span><span>=</span><span> np.</span><span>array</span><span>([x[head </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>-</span><span> x[tail </span><span>-</span><span> </span></span><span>1</span><span>] </span><span>for</span><span> tail, head </span><span>in</span><span> edges])</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x 等于逐边电势差："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, diff))</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 零空间：常数电势</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>ones </span><span>=</span><span> np.</span><span>ones</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ (1, 1, 1, 1) ="</span><span><span>, A </span><span>@</span><span> ones)</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"全体电势加 7.5，电势差不变："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> (x </span><span>+</span><span> </span></span><span>7.5</span><span><span>), A </span><span>@</span><span> x))</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A), </span></span><span>"，零空间维数 ="</span><span>, </span><span>4</span><span><span> </span><span>-</span><span> </span></span><span>3</span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 接地：删掉最后一列（令 x4 = 0），剩下三列线性无关</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>grounded </span><span>=</span><span> A[:, :</span><span>3</span><span>]</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"接地后的秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(grounded))</span></span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[[-1.  1.  0.  0.]</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[ 0. -1.  1.  0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[-1.  0.  1.  0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[-1.  0.  0.  1.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 0.  0. -1.  1.]]</span></div></div><div><div><div>6</div></div><div><span>形状： (5, 4) ，非零元个数： 10</span></div></div><div><div><div>7</div></div><div><span>A @ x 等于逐边电势差： True</span></div></div><div><div><div>8</div></div><div><span>A @ (1, 1, 1, 1) = [0. 0. 0. 0. 0.]</span></div></div><div><div><div>9</div></div><div><span>全体电势加 7.5，电势差不变： True</span></div></div><div><div><div>10</div></div><div><span>秩 = 3 ，零空间维数 = 1</span></div></div><div><div><div>11</div></div><div><span>接地后的秩 = 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 的第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个分量是 <span><span>xhead−xtailx_{\text{head}} - x_{\text{tail}}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span><span>head</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span><span>tail</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，也就是第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 条边两端的差。把 <span><span>x=(x1,x2,x3,x4)\mathbf{x} = (x_1, x_2, x_3, x_4)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 看成四个节点上的<strong>电势</strong>，<span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 就是五条边上的<strong>电势差</strong>，这是他整讲的第一条线索。上面的脚本里我用固定种子取了一个随机的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>，逐边算差和 <code>A @ x</code> 对比，结果是 <code>True</code>。</p></section>
<section><h2>零空间：只有常数电势<a href="#零空间只有常数电势"><span>#</span></a></h2><p><strong><span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 什么时候成立？</strong> 五个差都是零：<span><span>x2−x1=0x_2 - x_1 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>x3−x2=0x_3 - x_2 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>x3−x1=0x_3 - x_1 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>x4−x1=0x_4 - x_1 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>x4−x3=0x_4 - x_3 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。结论是四个电势全相等。所以 <span><span>(1,1,1,1)(1, 1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 是零空间里的向量，它的所有倍数 <span><span>(c,c,c,c)(c, c, c, c)</span><span><span><span></span><span>(</span><span>c</span><span>,</span><span></span><span>c</span><span>,</span><span></span><span>c</span><span>,</span><span></span><span>c</span><span>)</span></span></span></span> 是整个零空间，维数是 1。输出里 <code>A @ (1, 1, 1, 1)</code> 是五个 0，把全体电势加 7.5，电势差没有变化，这两条是同一件事。</p><p>物理上这说的是：电势只有差才有意义，整体抬高或降低一个常数，什么都不会动。他举了温度（摄氏度和从绝对零度算起），也提到了微积分里不定积分的 <span><span>+C+ C</span><span><span><span></span><span>+</span><span>C</span></span></span></span>。我再补一个自己的类比：这像海拔，珠峰有多高取决于从哪里算零点，但山顶和山脚差多少米，和零点放在哪里无关。</p><p>那个任意常数在实际计算里要处理掉，办法是<strong>接地</strong>：随便挑一个节点，规定它的电势是零。这等于把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的那一列删掉。输出里接地之后（删掉第 4 列），<span><span>5×35 \times 3</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 矩阵的秩是 3，三列线性无关，也就是说剩下三个电势都能被唯一确定。</p><p>秩本身也顺带有了答案：<span><span>n=4n = 4</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span> 列里有一个相关关系，所以 <span><span>r=n−1=3r = n - 1 = 3</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>。输出的“秩 = 3，零空间维数 = 1”和他在黑板上说的一致，而且任意三列都是线性无关的（他说“任意三个电势都是好变量”）。</p></section>
<section><h2>左零空间：基尔霍夫电流定律<a href="#左零空间基尔霍夫电流定律"><span>#</span></a></h2><p><strong><span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 是什么？</strong> 他说这大概是应用数学里最基本的方程。先写出 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，它是 <span><span>n×m=4×5n \times m = 4 \times 5</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span>：</p><span><span><span>AT=[−10−1−101−10000110−100011]A^\mathsf{T} = \begin{bmatrix}
-1 &amp; 0 &amp; -1 &amp; -1 &amp; 0 \\
1 &amp; -1 &amp; 0 &amp; 0 &amp; 0 \\
0 &amp; 1 &amp; 1 &amp; 0 &amp; -1 \\
0 &amp; 0 &amp; 0 &amp; 1 &amp; 1
\end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>y=(y1,…,y5)\mathbf{y} = (y_1, \ldots, y_5)</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 是五条边上的电流。逐行读：</p><ul>
<li>第 1 行：<span><span>−y1−y3−y4=0-y_1 - y_3 - y_4 = 0</span><span><span><span></span><span>−</span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。边 1、3、4 都是从节点 1 流出，流出量之和为零。</li>
<li>第 2 行：<span><span>y1−y2=0y_1 - y_2 = 0</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，节点 2 流进多少流出多少。</li>
<li>第 3 行：<span><span>y2+y3−y5=0y_2 + y_3 - y_5 = 0</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，节点 3 流进的是 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与 <span><span>y3y_3</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，流出 <span><span>y5y_5</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
<li>第 4 行：<span><span>y4+y5=0y_4 + y_5 = 0</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</li>
</ul><p>每个节点一个方程，说的都是“流进等于流出”，电荷不会堆积在节点上。这就是基尔霍夫电流定律（KCL）：守恒定律，<span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</p><p>零空间的维数公式他强调了一次：<span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是 <span><span>n×mn \times m</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span>，有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 列，<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个主元列，所以 <span><span>dim⁡N(AT)=m−r=5−3=2\dim N(A^\mathsf{T}) = m - r = 5 - 3 = 2</span><span><span><span></span><span>dim</span><span></span><span>N</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>。他不用消元，直接在图上找：让 1 安培顺着边 1 走，节点 2 上必须从边 2 走掉（<span><span>y2=1y_2 = 1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>），到了节点 3，再取 <span><span>y3=−1y_3 = -1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>，也就是 1 安培逆着边 3 回到节点 1，<span><span>y4=y5=0y_4 = y_5 = 0</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。这是绕着上面那个三角形（节点 1、2、3）转一圈：<span><span>y1=(1,1,−1,0,0)\mathbf{y}_1 = (1, 1, -1, 0, 0)</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。另一个绕着节点 1、3、4 的三角形：<span><span>y3=1y_3 = 1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>y5=1y_5 = 1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>y4=−1y_4 = -1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>，即 <span><span>y2=(0,0,1,−1,1)\mathbf{y}_2 = (0, 0, 1, -1, 1)</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。</p><p>有人会问，绕外圈那一大圈（边 1、2、5，再逆着边 4 回来）呢？它是 <span><span>(1,1,0,−1,1)(1, 1, 0, -1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，确实也满足 KCL。但它不是第三个基向量，因为它等于前两个相加：边 3 上两个回路的电流方向相反，抵消了。下面验证：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>incidence</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>edges</span></span><span>:</span><span> list[tuple[</span><span>int</span><span>, </span><span>int</span><span>]]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""每条边一行：起点 -1，终点 +1（节点编号从 1 开始）。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>len</span><span>(edges), n))</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> k, (tail, head) </span><span>in</span><span> </span><span>enumerate</span><span>(edges):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>A[k, tail </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>1.0</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>A[k, head </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> A</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>edges </span><span>=</span><span> [(</span><span>1</span><span>, </span><span>2</span><span>), (</span><span>2</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>4</span><span>), (</span><span>3</span><span>, </span><span>4</span><span>)]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>incidence</span><span>(</span><span>4</span><span>, edges)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"Aᵀ ="</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(A.T)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 基尔霍夫电流定律 Aᵀy = 0 的两个回路解</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>y_left </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])  </span><span># 1 -&gt; 2 -&gt; 3 -&gt; 1</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>y_right </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>])  </span><span># 1 -&gt; 3 -&gt; 4 -&gt; 1</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>y_outer </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>])  </span><span># 外圈</span></div></div><div><div><div>24</div></div><div><span>    </span><span>for</span><span> name, y </span><span>in</span><span> [(</span><span>"左"</span><span>, y_left), (</span><span>"右"</span><span>, y_right), (</span><span>"外圈"</span><span>, y_outer)]:</span></div></div><div><div><div>25</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>回路：Aᵀy ="</span><span><span>, A.T </span><span>@</span><span> y)</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"外圈 = 左 + 右："</span><span><span>, np.</span><span>allclose</span><span>(y_outer, y_left </span><span>+</span><span> y_right))</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># 用 SVD 独立地算出左零空间的维数</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>_, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A.T)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>rank </span><span>=</span><span> </span><span>int</span><span><span>(np.</span><span>sum</span><span>(s </span><span>&gt;</span><span> </span></span><span>1e-10</span><span>))</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"rank ="</span><span>, rank, </span><span>"，m - r ="</span><span>, A.shape[</span><span>0</span><span><span>] </span><span>-</span><span> rank)</span></span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> Vt[rank:].T  </span><span># 左零空间的一组标准正交基，5 x 2</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>34</div></div><div><span>        </span><span>"两个回路都在这个空间里："</span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(N </span><span>@</span><span> (N.T </span><span>@</span><span> y_left), y_left)</span></div></div><div><div><div>36</div></div><div><span>        </span><span>and</span><span><span> np.</span><span>allclose</span><span>(N </span><span>@</span><span> (N.T </span><span>@</span><span> y_right), y_right),</span></span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 逐列加入 Aᵀ，只有让秩增加的列才是主元列</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>pivots </span><span>=</span><span> []</span></div></div><div><div><div>41</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>0</span><span>]):</span></div></div><div><div><div>42</div></div><div><span>        </span><span>if</span><span><span> np.linalg.</span><span>matrix_rank</span><span>(A.T[:, pivots </span><span>+</span><span> [k]]) </span><span>&gt;</span><span> </span></span><span>len</span><span>(pivots):</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>pivots.</span><span>append</span><span>(k)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"主元列（边的编号）："</span><span><span>, [k </span><span>+</span><span> </span></span><span>1</span><span> </span><span>for</span><span> k </span><span>in</span><span> pivots])</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>loop_rank </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A[[</span><span>0</span><span>, </span><span>1</span><span>, </span><span>2</span><span>]])</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"边 1、2、3 组成回路，这三行的秩 ="</span><span>, loop_rank)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>48</div></div><div><span>        </span><span>"边 1、2、4 是一棵树，这三行的秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A[pivots])</span></span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Aᵀ =</span></div></div><div><div><div>2</div></div><div><span>[[-1.  0. -1. -1.  0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 1. -1.  0.  0.  0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  1.  1.  0. -1.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 0.  0.  0.  1.  1.]]</span></div></div><div><div><div>6</div></div><div><span>左回路：Aᵀy = [0. 0. 0. 0.]</span></div></div><div><div><div>7</div></div><div><span>右回路：Aᵀy = [0. 0. 0. 0.]</span></div></div><div><div><div>8</div></div><div><span>外圈回路：Aᵀy = [0. 0. 0. 0.]</span></div></div><div><div><div>9</div></div><div><span>外圈 = 左 + 右： True</span></div></div><div><div><div>10</div></div><div><span>rank = 3 ，m - r = 2</span></div></div><div><div><div>11</div></div><div><span>两个回路都在这个空间里： True</span></div></div><div><div><div>12</div></div><div><span>主元列（边的编号）： [1, 2, 4]</span></div></div><div><div><div>13</div></div><div><span>边 1、2、3 组成回路，这三行的秩 = 2</span></div></div><div><div><div>14</div></div><div><span>边 1、2、4 是一棵树，这三行的秩 = 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三条回路的 <span><span>ATyA^\mathsf{T}\mathbf{y}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span></span></span></span> 全是零，外圈等于左加右。独立的 SVD 算出 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的秩是 3，<span><span>m−r=2m - r = 2</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，我再把两个回路投影到 SVD 给出的左零空间基上，投影不改变它们，说明它们确实躺在这个二维空间里。</p><p>输出的最后几行是这一讲我最喜欢的部分。他顺手把 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的主元列也问了：消元之后主元列是第 1、2、4 列，对应边 1、2、4。我没有写手动消元，而是逐列加进去、看秩有没有涨；结果和他的答案一样，<code>[1, 2, 4]</code>。这三条边把四个节点连在一起，但没有回路。没有回路的图叫<strong>树</strong>。反过来，边 1、2、3 组成回路，这三行的秩只有 2：三行线性相关（第 1 行加第 2 行等于第 3 行）。<strong>行的相关关系对应图里的回路</strong>，行无关的边集合就是树。</p></section>
<section><h2>Euler 公式<a href="#euler-公式"><span>#</span></a></h2><p><strong>回路到底有几个？</strong> 左零空间的维数 <span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 就是独立回路的个数。而 <span><span>r=n−1r = n - 1</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>（有一个常数电势要扣掉）。代进去：</p><span><span><span>回路数=m−(n−1)⟹n−m+回路数=1\text{回路数} = m - (n - 1)
\quad\Longrightarrow\quad
n - m + \text{回路数} = 1</span><span><span><span></span><span><span>回路数</span></span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>m</span><span></span><span>+</span><span></span></span><span><span></span><span><span>回路数</span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></span><p>节点数减边数加回路数等于 1。节点是零维的点，边是一维的线，回路围出一块二维的面积，所以这个交替和就是 Euler 公式。他顺口说：我们又看到 Euler 了，这个人从没停过。线性代数在这里“证明”了图论里的一个事实。（这个证明用到了 <span><span>r=n−1r = n - 1</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>，而这一步要求图是连通的。视频里我没有听到他特别点出这一点，我猜他默认画的图都是连通的。）</p><p>他随手画了第二个图（5 个节点、7 条边、3 个回路）来验证，没有给出每条边的端点。我自己画了一个 5 节点 7 边的图，又加了两个反例：两个互不相连的三角形，以及一个 30 × 30 的网格：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>incidence</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>edges</span></span><span>:</span><span> list[tuple[</span><span>int</span><span>, </span><span>int</span><span>]]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""每条边一行：起点 -1，终点 +1（节点编号从 1 开始）。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>len</span><span>(edges), n))</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> k, (tail, head) </span><span>in</span><span> </span><span>enumerate</span><span>(edges):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>A[k, tail </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>1.0</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>A[k, head </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> A</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>euler</span><span>(</span><span>name</span><span>:</span><span> </span><span>str</span><span>,</span><span><span> </span><span>n</span></span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>edges</span></span><span>:</span><span> list[tuple[</span><span>int</span><span>, </span><span>int</span><span>]]</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>incidence</span><span>(n, edges)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> </span><span>len</span><span>(edges)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>loops </span><span>=</span><span> m </span><span>-</span><span> r  </span><span># 左零空间的维数</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：n = </span><span>{</span><span>n</span><span>}</span><span>，m = </span><span>{</span><span>m</span><span>}</span><span>，r = </span><span>{</span><span>r</span><span>}</span><span>，回路数 = </span><span>{</span><span>loops</span><span>}</span><span>，"</span><span>, </span><span>end</span><span>=</span><span>""</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"n - m + 回路数 = </span><span>{</span><span><span>n </span><span>-</span><span> m </span><span>+</span><span> loops</span></span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>def</span><span> </span><span>grid_edges</span><span>(</span><span>side</span><span>:</span><span> </span><span>int</span><span>) -&gt; list[tuple[</span><span>int</span><span>, </span><span>int</span><span>]]:</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>edges </span><span>=</span><span> []</span></div></div><div><div><div>24</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(side):</span></div></div><div><div><div>25</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(side):</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>node </span><span>=</span><span> i </span><span>*</span><span> side </span><span>+</span><span> j </span><span>+</span><span> </span><span>1</span></div></div><div><div><div>27</div></div><div><span>            </span><span>if</span><span><span> j </span><span>+</span><span> </span></span><span>1</span><span><span> </span><span>&lt;</span><span> side:</span></span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>edges.</span><span>append</span><span>((node, node </span><span>+</span><span> </span><span>1</span><span>))</span></div></div><div><div><div>29</div></div><div><span>            </span><span>if</span><span><span> i </span><span>+</span><span> </span></span><span>1</span><span><span> </span><span>&lt;</span><span> side:</span></span></div></div><div><div><div>30</div></div><div><span><span>                </span></span><span>edges.</span><span>append</span><span>((node, node </span><span>+</span><span> side))</span></div></div><div><div><div>31</div></div><div><span>    </span><span>return</span><span> edges</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>35</div></div><div><span>    </span><span># Strang 的图</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>euler</span><span>(</span><span>"Strang 的图"</span><span>, </span><span>4</span><span>, [(</span><span>1</span><span>, </span><span>2</span><span>), (</span><span>2</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>4</span><span>), (</span><span>3</span><span>, </span><span>4</span><span>)])</span></div></div><div><div><div>37</div></div><div><span>    </span><span># 我自己画的一个 5 节点 7 边的图</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>euler</span><span>(</span></div></div><div><div><div>39</div></div><div><span>        </span><span>"5 节点图"</span><span>,</span></div></div><div><div><div>40</div></div><div><span>        </span><span>5</span><span>,</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>[(</span><span>1</span><span>, </span><span>2</span><span>), (</span><span>2</span><span>, </span><span>3</span><span>), (</span><span>3</span><span>, </span><span>4</span><span>), (</span><span>4</span><span>, </span><span>5</span><span>), (</span><span>5</span><span>, </span><span>1</span><span>), (</span><span>1</span><span>, </span><span>3</span><span>), (</span><span>2</span><span>, </span><span>4</span><span>)],</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>43</div></div><div><span>    </span><span># 两个互不相连的三角形：零空间是二维的，公式右边变成连通块数</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>triangle </span><span>=</span><span> [(</span><span>1</span><span>, </span><span>2</span><span>), (</span><span>2</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>3</span><span>), (</span><span>4</span><span>, </span><span>5</span><span>), (</span><span>5</span><span>, </span><span>6</span><span>), (</span><span>4</span><span>, </span><span>6</span><span>)]</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>euler</span><span>(</span><span>"两个三角形"</span><span>, </span><span>6</span><span>, triangle)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 30 x 30 的网格：关联矩阵很大，非零元很少</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>side </span><span>=</span><span> </span><span>30</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>edges </span><span>=</span><span> </span><span>grid_edges</span><span>(side)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>euler</span><span>(</span><span>"30 x 30 网格"</span><span><span>, side </span><span>*</span><span> side, edges)</span></span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>incidence</span><span>(side </span><span>*</span><span> side, edges)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span><span>"网格关联矩阵的形状："</span><span>, A.shape)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>nonzero </span><span>=</span><span> np.</span><span>count_nonzero</span><span>(A)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"非零元：</span><span>{</span><span>nonzero</span><span>}</span><span>，占比 </span><span>{</span><span><span>nonzero </span><span>/</span><span> A.size</span></span><span>:.4%</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>"小方格的个数 (side - 1)² ="</span><span><span>, (side </span><span>-</span><span> </span></span><span>1</span><span><span>) </span><span>**</span><span> </span></span><span>2</span><span>)</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Strang 的图：n = 4，m = 5，r = 3，回路数 = 2，n - m + 回路数 = 1</span></div></div><div><div><div>2</div></div><div><span>5 节点图：n = 5，m = 7，r = 4，回路数 = 3，n - m + 回路数 = 1</span></div></div><div><div><div>3</div></div><div><span>两个三角形：n = 6，m = 6，r = 4，回路数 = 2，n - m + 回路数 = 2</span></div></div><div><div><div>4</div></div><div><span>30 x 30 网格：n = 900，m = 1740，r = 899，回路数 = 841，n - m + 回路数 = 1</span></div></div><div><div><div>5</div></div><div><span>网格关联矩阵的形状： (1740, 900)</span></div></div><div><div><div>6</div></div><div><span>非零元：3480，占比 0.2222%</span></div></div><div><div><div>7</div></div><div><span>小方格的个数 (side - 1)² = 841</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个图里，Strang 的图、我的 5 节点图和网格都给出 1。两个互不相连的三角形给出 2：零空间现在是二维的（每个连通块一个常数电势），<span><span>r=n−2r = n - 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span></span></span></span>，右边于是变成连通块的个数。所以 Euler 公式里的 1，实际上是“连通块数”，这是我跑完才注意到的细节。</p><p>30 × 30 的网格才是我真正想放在这里的数字：关联矩阵是 <span><span>1740×9001740 \times 900</span><span><span><span></span><span>1740</span><span></span><span>×</span><span></span></span><span><span></span><span>900</span></span></span></span>，一共 3480 个非零元，占 0.2222%，其余 99.78% 都是零。左零空间的维数 <span><span>m−r=1740−899=841m - r = 1740 - 899 = 841</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1740</span><span></span><span>−</span><span></span></span><span><span></span><span>899</span><span></span><span>=</span><span></span></span><span><span></span><span>841</span></span></span></span>，和网格里的小方格数 <span><span>292=84129^2 = 841</span><span><span><span></span><span>2</span><span><span>9</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>841</span></span></span></span> 完全一致。每个小方格是一个基本回路，大回路都可以由它们相加得到。这个计算的时候我并没有告诉程序“有多少方格”，它只看秩。</p></section>
<section><h2><span><span>ATCAA^\mathsf{T}CA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>A</span></span></span></span>：把三步连起来<a href="#atcaamathsftcaatca把三步连起来"><span>#</span></a></h2><p>最后他把前面散着的东西连成一条链：</p><span><span><span>x  →  A    e=Ax  →  C    y=Ce  →  AT    f=ATy\mathbf{x}
\;\xrightarrow{\;A\;}\;
\mathbf{e} = A\mathbf{x}
\;\xrightarrow{\;C\;}\;
\mathbf{y} = C\mathbf{e}
\;\xrightarrow{\;A^\mathsf{T}\;}\;
\mathbf{f} = A^\mathsf{T}\mathbf{y}</span><span><span><span></span><span>x</span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span>A</span><span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span></span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span>C</span><span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span></span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>e</span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span></span><span><span></span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span></span></span></span></span><ul>
<li><span><span>AA</span><span><span><span></span><span>A</span></span></span></span>：节点电势变成边上的电势差 <span><span>e\mathbf{e}</span><span><span><span></span><span>e</span></span></span></span>。</li>
<li><span><span>CC</span><span><span><span></span><span>C</span></span></span></span>：欧姆定律。边上的电流等于电导（电阻的倒数）乘以电势差，这里 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 是对角矩阵，每条边一个电导。</li>
<li><span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：基尔霍夫电流定律。电流在每个节点上求净流量，等于外部电流源 <span><span>f\mathbf{f}</span><span><span><span></span><span>f</span></span></span></span>。</li>
</ul><p>他说外部电流源是他一直没放进去的东西：在 <span><span>ATy=fA^\mathsf{T}\mathbf{y} = \mathbf{f}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span></span></span></span> 的右边加一个 <span><span>f\mathbf{f}</span><span><span><span></span><span>f</span></span></span></span>，比如往一个节点送进 1 安培，从另一个节点取走。他还说，这些是平衡态的方程，时间不在里面，牛顿定律也不在里面，只问“一切稳定下来之后，电流怎样分布”。三式合起来：<span><span>ATCA x=fA^\mathsf{T}CA\,\mathbf{x} = \mathbf{f}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>A</span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span></span></span></span>。</p><p>最后他问：<span><span>ATCAA^\mathsf{T}CA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>A</span></span></span></span> 有什么性质？（他自己只答了 <span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 总是对称的，然后就下课了。它还是半正定的，那是第 25 讲的内容。）下面用同一个图和一组电导 <span><span>C=diag⁡(1,2,1,2,1)C = \operatorname{diag}(1, 2, 1, 2, 1)</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 解一个小电路：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>incidence</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>edges</span></span><span>:</span><span> list[tuple[</span><span>int</span><span>, </span><span>int</span><span>]]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""每条边一行：起点 -1，终点 +1（节点编号从 1 开始）。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>len</span><span>(edges), n))</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> k, (tail, head) </span><span>in</span><span> </span><span>enumerate</span><span>(edges):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>A[k, tail </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>1.0</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>A[k, head </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> A</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>incidence</span><span>(</span><span>4</span><span>, [(</span><span>1</span><span>, </span><span>2</span><span>), (</span><span>2</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>3</span><span>), (</span><span>1</span><span>, </span><span>4</span><span>), (</span><span>3</span><span>, </span><span>4</span><span>)])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>diag</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>])  </span><span># 每条边的电导 1 / R</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"AᵀA ="</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span><span>(A.T </span><span>@</span><span> A)</span></span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>K </span><span>=</span><span> A.T </span><span>@</span><span> C </span><span>@</span><span> A</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"AᵀCA ="</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(K)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"对称："</span><span><span>, np.</span><span>allclose</span><span>(K, K.T), </span></span><span>"，秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(K))</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 外部电流源 f：节点 1 送进 1 安，节点 4 取走 1 安</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>-</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"f 的分量之和 ="</span><span><span>, f.</span><span>sum</span><span>())</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 接地节点 4：删掉第 4 行第 4 列，剩下的 3 x 3 可逆</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>K3 </span><span>=</span><span> K[:</span><span>3</span><span>, :</span><span>3</span><span>]</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>x[:</span><span>3</span><span><span>] </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(K3, f[:</span></span><span>3</span><span>])</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"电势 x ="</span><span>, x)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"接地后的秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(K3))</span></span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 三步：x -&gt; 电势差 e = A x -&gt; 电流 y = C e -&gt; f = Aᵀ y</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> A </span><span>@</span><span> x</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> C </span><span>@</span><span> e</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"电势差 e ="</span><span>, e)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"电流 y ="</span><span>, y)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"Aᵀy 等于 f："</span><span><span>, np.</span><span>allclose</span><span>(A.T </span><span>@</span><span> y, f))</span></span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span># 分量之和不为 0 的 f 无解：电荷没处去</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>f_bad </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>x_bad, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(K, f_bad, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"f = (1, 0, 0, 0) 的残差："</span><span><span>, np.linalg.</span><span>norm</span><span>(K </span><span>@</span><span> x_bad </span><span>-</span><span> f_bad))</span></span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>AᵀA =</span></div></div><div><div><div>2</div></div><div><span>[[ 3. -1. -1. -1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[-1.  2. -1.  0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[-1. -1.  3. -1.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[-1.  0. -1.  2.]]</span></div></div><div><div><div>6</div></div><div><span>AᵀCA =</span></div></div><div><div><div>7</div></div><div><span>[[ 4. -1. -1. -2.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[-1.  3. -2.  0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[-1. -2.  4. -1.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[-2.  0. -1.  3.]]</span></div></div><div><div><div>11</div></div><div><span>对称： True ，秩 = 3</span></div></div><div><div><div>12</div></div><div><span>f 的分量之和 = 0.0</span></div></div><div><div><div>13</div></div><div><span>电势 x = [-0.381  -0.2857 -0.2381  0.    ]</span></div></div><div><div><div>14</div></div><div><span>接地后的秩 = 3</span></div></div><div><div><div>15</div></div><div><span>电势差 e = [0.0952 0.0476 0.1429 0.381  0.2381]</span></div></div><div><div><div>16</div></div><div><span>电流 y = [0.0952 0.0952 0.1429 0.7619 0.2381]</span></div></div><div><div><div>17</div></div><div><span>Aᵀy 等于 f： True</span></div></div><div><div><div>18</div></div><div><span>f = (1, 0, 0, 0) 的残差： 0.5</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>分几步看这个输出。</p><ul>
<li><span><span>ATAA^\mathsf{T}A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span></span></span></span> 的对角线是 3, 2, 3, 2，正好是每个节点连着几条边（度数），非对角线上，相邻的节点是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，不相邻的是 0。节点 2 和 4 之间没有边，输出里第 (2, 4) 位置确实是 0。带权的 <span><span>ATCAA^\mathsf{T}CA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>A</span></span></span></span> 同样是对称的。</li>
<li><span><span>ATCAA^\mathsf{T}CA</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>A</span></span></span></span> 的秩是 3，而不是 4：它是 <span><span>4×44 \times 4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的奇异矩阵。零空间向量还是 <span><span>(1,1,1,1)(1, 1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，因为 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 已经是零，乘 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 和 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 之后还是零。</li>
<li>接地节点 4，删掉第 4 行第 4 列，得到可逆的 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 矩阵，解出 <span><span>x=(−0.381,−0.2857,−0.2381,0)\mathbf{x} = (-0.381, -0.2857, -0.2381, 0)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.381</span><span>,</span><span></span><span>−</span><span>0.2857</span><span>,</span><span></span><span>−</span><span>0.2381</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。电流源是 <span><span>f=(−1,0,0,1)\mathbf{f} = (-1, 0, 0, 1)</span><span><span><span></span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，意思是从节点 1 送进 1 安培、在节点 4 取走（<span><span>ATyA^\mathsf{T}\mathbf{y}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span></span></span></span> 算的是净流入减去净流出，所以注入处是负的）。</li>
<li>电流 <span><span>y=(0.0952,0.0952,0.1429,0.7619,0.2381)\mathbf{y} = (0.0952, 0.0952, 0.1429, 0.7619, 0.2381)</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0.0952</span><span>,</span><span></span><span>0.0952</span><span>,</span><span></span><span>0.1429</span><span>,</span><span></span><span>0.7619</span><span>,</span><span></span><span>0.2381</span><span>)</span></span></span></span>。边 4 直接连着节点 1 和节点 4，电导是 2，分得 0.7619 安培，最多，合理。从节点 1 流出的电流之和 <span><span>0.0952+0.1429+0.7619=10.0952 + 0.1429 + 0.7619 = 1</span><span><span><span></span><span>0.0952</span><span></span><span>+</span><span></span></span><span><span></span><span>0.1429</span><span></span><span>+</span><span></span></span><span><span></span><span>0.7619</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，正好是送进去的那 1 安培。最后一行检验 <span><span>ATy=fA^\mathsf{T}\mathbf{y} = \mathbf{f}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span></span></span></span> 是 <code>True</code>。</li>
</ul><p>最后一个数字是 0.5：给一个分量和不为零的 <span><span>f\mathbf{f}</span><span><span><span></span><span>f</span></span></span></span>（比如只往节点 1 送 1 安培、不取走），最小二乘能做到的最好残差是 0.5，不是零。这等于说：这个方程组无解。道理很直接，电荷没地方去。线性代数的说法是：<span><span>ATCA x=fA^\mathsf{T}CA\,\mathbf{x} = \mathbf{f}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span>A</span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span></span></span></span> 有解，当且仅当 <span><span>f\mathbf{f}</span><span><span><span></span><span>f</span></span></span></span> 和零空间里的 <span><span>(1,1,1,1)(1, 1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 垂直，即分量之和为零。“行空间和零空间垂直”要到第 14 讲才系统地讲，我这里先露个头。</p><p>我猜 Strang 反复说“结构”，是想让我们看到：前十一讲那些现编的矩阵，换一层外壳就是电路、管网和桥。（我得承认，我也是画了这几张图，才第一次觉得左零空间不是一个拿来考试的名词。）</p><p>他最后说下次是这几章的完整复习，也就是 <a href="/posts/linear-algebra-13-quiz-1-review/">18.06 第 13 讲：第一次测验复习</a>。我会把他讲的题逐道用 NumPy 过一遍。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-11-matrix-spaces-rank-one/</id>
      <title type="text">18.06 第 11 讲：矩阵空间、秩 1 矩阵与小世界图</title>
      <published>2026-10-18T03:20:00.000Z</published>
      <updated>2026-10-18T03:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-11-matrix-spaces-rank-one/"/>
      <summary type="text">MIT 18.06 第 11 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 验证了 3 × 3 矩阵空间里对称子空间与上三角子空间满足 6 + 6 = 3 + 9，又在 60 个人的圆环上加 6 条随机捷径，最远距离从 30 步降到 17 步。</summary>
      <content type="html"><![CDATA[<p>“向量”不一定是一列数。Strang 这一讲把线性代数的词汇搬到矩阵和函数上：3 × 3 矩阵的全体是 9 维空间，对称矩阵是其中 6 维的子空间，上三角矩阵也是 6 维，两者的交集是 3 维的对角阵，两者的和是整个 9 维空间，所以 <span><span>6+6=3+96 + 6 = 3 + 9</span><span><span><span></span><span>6</span><span></span><span>+</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>9</span></span></span></span>。我用 NumPy 把这几个维数一个一个算了出来；最后五分钟他讲小世界图，我在一个 60 人的圆环上加了 6 条随机捷径，最远的两个人从相隔 30 步变成相隔 17 步。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=2IdtqGM6KWU" target="_blank">Lecture 11: Matrix spaces; rank 1; small world graphs</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-11-matrix-spaces-rank-1-small-world-graphs/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §3.1 Vector Spaces and Subspaces；§1.4 Matrix Multiplication AB and CR（第 6 版目录里没有小世界图的独立小节，秩 1 矩阵“一列乘一行”对应 §1.4 的列乘行）
前置：<a href="/posts/linear-algebra-10-four-fundamental-subspaces/">第 10 讲：四个基本子空间</a>
一句话：只要能做加法和数乘，矩阵和函数都是向量；任何秩 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 的矩阵都是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个秩 1 矩阵（一列乘一行）之和。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>前半段在补第 10 讲末尾的矩阵空间：<span><span>SS</span><span><span><span></span><span>S</span></span></span></span>、<span><span>UU</span><span><span><span></span><span>U</span></span></span></span>、<span><span>S∩US \cap U</span><span><span><span></span><span>S</span><span></span><span>∩</span><span></span></span><span><span></span><span>U</span></span></span></span>、<span><span>S+US + U</span><span><span><span></span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>U</span></span></span></span> 的维数是这一讲最值得认真看的部分，尤其是他最后写出的 <span><span>dim⁡S+dim⁡U=dim⁡(S∩U)+dim⁡(S+U)\dim S + \dim U = \dim(S \cap U) + \dim(S + U)</span><span><span><span></span><span>dim</span><span></span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>dim</span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>dim</span><span>(</span><span>S</span><span></span><span>∩</span><span></span></span><span><span></span><span>U</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>dim</span><span>(</span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>U</span><span>)</span></span></span></span>。本篇第一节用代码把四个维数都算了一遍。</li>
<li>微分方程那段只有几分钟，他自己说“五分钟的 18.03”。本篇第二节只做了数值检查，听他讲一遍就够。</li>
<li>“秩 1 矩阵是积木”这段要认真听：他在黑板上写出 <span><span>A=uvTA = \mathbf{u}\mathbf{v}^\mathrm{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>u</span><span><span>v</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，这是后面特征值、SVD 反复出现的形状。</li>
<li>分量之和为零的子空间那一段，他把四个基本子空间全过了一遍，是第 10 讲的练习，可以倍速。</li>
<li>最后讲朋友关系图的五分钟，大半是在聊“你离 Clinton 有几步”，数学内容很少，可以快进；下一讲才开始真正用矩阵。</li>
</ul></div></div>
<section><h2>矩阵也是向量<a href="#矩阵也是向量"><span>#</span></a></h2><p>先回到他在上一讲末尾留下的那个空间：<span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 是所有 3 × 3 矩阵。它的“向量”是矩阵，只用到两种运算，矩阵加法和乘以一个数（他特意说，两个矩阵相乘不属于向量空间的图景）。这样的空间有基、有维数，和 <span><span>R9\mathbb{R}^9</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>9</span></span></span></span></span></span></span></span></span></span></span> 几乎没有区别，只是九个数排成了方阵。</p><p><strong><span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 的维数是多少？</strong> 他的做法是写出标准基：九个矩阵，每个只在一个位置上是 1，其余是 0。所以 <span><span>dim⁡M=9\dim M = 9</span><span><span><span></span><span>dim</span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>9</span></span></span></span>。</p><p>接下来是两个子空间：</p><ul>
<li><span><span>SS</span><span><span><span></span><span>S</span></span></span></span>：对称的 3 × 3 矩阵。两个对称矩阵之和还是对称的，所以是子空间（它们的乘积一般不对称，但这里不乘）。</li>
<li><span><span>UU</span><span><span><span></span><span>U</span></span></span></span>：上三角的 3 × 3 矩阵。两个上三角矩阵之和还是上三角的。</li>
</ul><p>他先问一个很细的问题：那九个标准基矩阵里，有几个落在 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 里？只有三个，就是对角线上的三个。而落在 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 里的有六个。所以对 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 来说“大空间的基里恰好有一组子空间的基”是个巧合；对 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 就不是：要得到 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的基，得重新想一遍。对称矩阵由对角线上的三个数和对角线上方的三个数决定，所以 <span><span>dim⁡S=6\dim S = 6</span><span><span><span></span><span>dim</span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span>，基里除了三个对角位置，还要三个“关于对角线对称的一对 1”。</p><p>然后是子空间的两种组合：</p><ul>
<li>交集 <span><span>S∩US \cap U</span><span><span><span></span><span>S</span><span></span><span>∩</span><span></span></span><span><span></span><span>U</span></span></span></span>：既对称又上三角，那只能是对角阵，维数 3。</li>
<li>并集：不是子空间（“就像在平面里取两条直线，然后就停了”），所以他不要并集，要和 <span><span>S+US + U</span><span><span><span></span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>U</span></span></span></span>：取 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 里任意一个矩阵加上 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 里任意一个矩阵。对称加上三角能凑出所有 3 × 3 矩阵，所以 <span><span>S+U=MS + U = M</span><span><span><span></span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>M</span></span></span></span>，维数 9。</li>
</ul><p>最后他指出，这四个数有一个很漂亮的关系：</p><span><span><span>dim⁡S+dim⁡U=dim⁡(S∩U)+dim⁡(S+U),6+6=3+9\dim S + \dim U = \dim(S \cap U) + \dim(S + U), \qquad 6 + 6 = 3 + 9</span><span><span><span></span><span>dim</span><span></span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>dim</span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>dim</span><span>(</span><span>S</span><span></span><span>∩</span><span></span></span><span><span></span><span>U</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>dim</span><span>(</span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>U</span><span>)</span><span>,</span><span></span><span></span><span>6</span><span></span><span>+</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>9</span></span></span></span></span><p>下面的代码把九个位置拉成 9 维向量，用矩阵的秩去数这些维数。交集的维数我是这样求的：把 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的基和 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的基放在一起得到矩阵 <span><span>[BS  −BU][B_S\ \ {-B_U}]</span><span><span><span></span><span>[</span><span><span>B</span><span><span><span><span><span><span></span><span><span>S</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span> </span><span> </span><span><span>−</span><span><span>B</span><span><span><span><span><span><span></span><span><span>U</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span>]</span></span></span></span>，它的零空间里的每个向量对应一个“既能写成 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的组合、又能写成 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的组合”的矩阵。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>unit</span><span>(</span><span>i</span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>j</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回第 (i, j) 个位置为 1、其余为 0 的 3 x 3 矩阵。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>E[i, j] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> E</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># M 的标准基：九个只有一个 1 的矩阵，拉成 9 维向量放进列</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>std </span><span>=</span><span> [</span><span>unit</span><span>(i, j) </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"标准基里对称的有"</span><span>, </span><span>sum</span><span><span>(np.</span><span>allclose</span><span>(E, E.T) </span></span><span>for</span><span> E </span><span>in</span><span> std), </span><span>"个"</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>18</div></div><div><span>        </span><span>"标准基里上三角的有"</span><span>,</span></div></div><div><div><div>19</div></div><div><span>        </span><span>sum</span><span><span>(np.</span><span>allclose</span><span>(E, np.</span><span>triu</span><span>(E)) </span></span><span>for</span><span> E </span><span>in</span><span> std),</span></div></div><div><div><div>20</div></div><div><span>        </span><span>"个"</span><span>,</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># S 的一组基：三个对角位置，加三个“关于对角线对称的一对 1”</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>sym </span><span>=</span><span> [</span><span>unit</span><span>(i, i) </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)]</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>sym </span><span>+=</span><span> [</span><span>unit</span><span>(i, j) </span><span>+</span><span> </span><span>unit</span><span>(j, i) </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span><span>(i </span><span>+</span><span> </span></span><span>1</span><span>, </span><span>3</span><span>)]</span></div></div><div><div><div>26</div></div><div><span>    </span><span># U 的一组基：对角线及其上方的六个位置</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>upper </span><span>=</span><span> [</span><span>unit</span><span>(i, j) </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(i, </span><span>3</span><span>)]</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>Bs </span><span>=</span><span> np.</span><span>column_stack</span><span>([E.</span><span>ravel</span><span>() </span><span>for</span><span> E </span><span>in</span><span> sym])</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>Bu </span><span>=</span><span> np.</span><span>column_stack</span><span>([E.</span><span>ravel</span><span>() </span><span>for</span><span> E </span><span>in</span><span> upper])</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>dim_s </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(Bs)</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>dim_u </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(Bu)</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>dim_sum </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>hstack</span><span>([Bs, Bu]))</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 交集：Bs @ a = Bu @ c 的解，对应 [Bs, -Bu] 的零空间</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>K </span><span>=</span><span> np.</span><span>hstack</span><span>([Bs, </span><span>-</span><span>Bu])</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>_, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(K)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>null_vectors </span><span>=</span><span> Vt[np.</span><span>sum</span><span>(s </span><span>&gt;</span><span> </span><span>1e-10</span><span>) :]</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>dim_cap </span><span>=</span><span> null_vectors.shape[</span><span>0</span><span>]</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"dim S ="</span><span>, dim_s, </span><span>"dim U ="</span><span>, dim_u)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"dim (S + U) ="</span><span>, dim_sum, </span><span>"dim (S 交 U) ="</span><span>, dim_cap)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>43</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span>dim_s</span><span>}</span><span> + </span><span>{</span><span>dim_u</span><span>}</span><span> = </span><span>{</span><span>dim_cap</span><span>}</span><span> + </span><span>{</span><span>dim_sum</span><span>}</span><span>："</span><span>,</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>dim_s </span><span>+</span><span> dim_u </span><span>==</span><span> dim_cap </span><span>+</span><span> dim_sum,</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 交集里的矩阵应当都是对角阵</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>in_both </span><span>=</span><span> [(Bs </span><span>@</span><span> v[:</span><span>6</span><span><span>]).</span><span>reshape</span><span>(</span></span><span>3</span><span>, </span><span>3</span><span>) </span><span>for</span><span> v </span><span>in</span><span> null_vectors]</span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>50</div></div><div><span>        </span><span>"交集中的矩阵都是对角阵："</span><span>,</span></div></div><div><div><div>51</div></div><div><span>        </span><span>all</span><span><span>(np.</span><span>allclose</span><span>(E, np.</span><span>diag</span><span>(np.</span><span>diag</span><span>(E))) </span></span><span>for</span><span> E </span><span>in</span><span> in_both),</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>    </span><span># 并集不是子空间：一个对称的加一个上三角的，两边都不属于</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>T </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> P </span><span>+</span><span> T</span></div></div><div><div><div>58</div></div><div><span>    </span><span>print</span><span>(</span><span>"P + T ="</span><span>)</span></div></div><div><div><div>59</div></div><div><span>    </span><span>print</span><span>(C)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>61</div></div><div><span>        </span><span>"对称："</span><span><span>, np.</span><span>allclose</span><span>(C, C.T), </span></span><span>"，上三角："</span><span><span>, np.</span><span>allclose</span><span>(C, np.</span><span>triu</span><span>(C))</span></span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>标准基里对称的有 3 个</span></div></div><div><div><div>2</div></div><div><span>标准基里上三角的有 6 个</span></div></div><div><div><div>3</div></div><div><span>dim S = 6 dim U = 6</span></div></div><div><div><div>4</div></div><div><span>dim (S + U) = 9 dim (S 交 U) = 3</span></div></div><div><div><div>5</div></div><div><span>6 + 6 = 3 + 9： True</span></div></div><div><div><div>6</div></div><div><span>交集中的矩阵都是对角阵： True</span></div></div><div><div><div>7</div></div><div><span>P + T =</span></div></div><div><div><div>8</div></div><div><span>[[0. 1. 0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[1. 0. 1.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0. 0. 0.]]</span></div></div><div><div><div>11</div></div><div><span>对称： False ，上三角： False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行和第二行对应他数的三个和六个。六、六、三、九四个数，以及 <span><span>6+6=3+96 + 6 = 3 + 9</span><span><span><span></span><span>6</span><span></span><span>+</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span></span><span>+</span><span></span></span><span><span></span><span>9</span></span></span></span> 都对上了；从零空间里拿出来的交集矩阵全部是对角阵，也和他的结论一致。最后三行是并集不能成为子空间的原因：一个对称矩阵 <code>P</code> 加上一个上三角矩阵 <code>T</code>，得到的 <code>P + T</code> 既不对称也不是上三角，它不在 <span><span>S∪US \cup U</span><span><span><span></span><span>S</span><span></span><span>∪</span><span></span></span><span><span></span><span>U</span></span></span></span> 里，但在 <span><span>S+US + U</span><span><span><span></span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>U</span></span></span></span> 里，所以我们才需要“和”而不是“并”。</p><p>（这个公式我猜是在刻意让人想起集合里的容斥原理，不过子空间的“并”不是子空间，所以只能用“和”。）</p></section>
<section><h2>微分方程的解空间<a href="#微分方程的解空间"><span>#</span></a></h2><p>他还给了另一个“向量不是一列数”的例子，用了大概五分钟：</p><span><span><span>d2ydx2+y=0\frac{d^2 y}{dx^2} + y = 0</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>d</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>d</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>这个方程的所有解构成一个向量空间：两个解相加还是解，解乘以常数还是解。他把它叫作微分方程的零空间。<span><span>y=cos⁡xy = \cos x</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>cos</span><span></span><span>x</span></span></span></span> 是一个解，<span><span>y=sin⁡xy = \sin x</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>sin</span><span></span><span>x</span></span></span></span> 是另一个，完全解是 <span><span>y=c1cos⁡x+c2sin⁡xy = c_1 \cos x + c_2 \sin x</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span>x</span></span></span></span>。所以 <span><span>cos⁡x\cos x</span><span><span><span></span><span>cos</span><span></span><span>x</span></span></span></span> 和 <span><span>sin⁡x\sin x</span><span><span><span></span><span>sin</span><span></span><span>x</span></span></span></span> 构成一组基，维数是 2，等于方程的阶数。</p><p>基不唯一：<span><span>eixe^{ix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>i</span><span>x</span></span></span></span></span></span></span></span></span></span></span></span> 和 <span><span>e−ixe^{-ix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>i</span><span>x</span></span></span></span></span></span></span></span></span></span></span></span> 也是一组基。他说线性微分方程这门课，本质上就是在找解空间的一组基。</p><p>我没有符号求解，只做了数值检查：用二阶中心差分近似 <span><span>y′′y''</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span></span></span></span>，看 <span><span>y′′+yy'' + y</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>′′</span></span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>y</span></span></span></span> 的最大绝对值。除了 <span><span>cos⁡\cos</span><span><span><span></span><span>cos</span></span></span></span> 和 <span><span>sin⁡\sin</span><span><span><span></span><span>sin</span></span></span></span>，我还试了一个组合 <span><span>3cos⁡x−2sin⁡x3\cos x - 2\sin x</span><span><span><span></span><span>3</span><span></span><span>cos</span><span></span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>sin</span><span></span><span>x</span></span></span></span>，以及一个不是解的 <span><span>y=xy = x</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span></span></span></span> 作对照。至于 <span><span>eixe^{ix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>i</span><span>x</span></span></span></span></span></span></span></span></span></span></span></span> 与 <span><span>e−ixe^{-ix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>i</span><span>x</span></span></span></span></span></span></span></span></span></span></span></span>，由欧拉公式，它们在 <span><span>(cos⁡x,sin⁡x)(\cos x, \sin x)</span><span><span><span></span><span>(</span><span>cos</span><span></span><span>x</span><span>,</span><span></span><span>sin</span><span></span><span>x</span><span>)</span></span></span></span> 这组基下的坐标是 <span><span>(1,i)(1, i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span> 和 <span><span>(1,−i)(1, -i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>i</span><span>)</span></span></span></span>，行列式不为零，所以是另一组基。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 微分方程 y'' + y = 0 的解空间：用 cos 与 sin 两个函数做基</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>0.0</span><span>, </span><span>2.0</span><span><span> </span><span>*</span><span> np.pi, </span></span><span>2001</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> x[</span><span>1</span><span><span>] </span><span>-</span><span> x[</span></span><span>0</span><span>]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> name, y </span><span>in</span><span> [</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>(</span><span>"cos"</span><span><span>, np.</span><span>cos</span><span>(x)),</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>(</span><span>"sin"</span><span><span>, np.</span><span>sin</span><span>(x)),</span></span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>(</span><span>"3 cos - 2 sin"</span><span>, </span><span>3</span><span><span> </span><span>*</span><span> np.</span><span>cos</span><span>(x) </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(x)),</span></span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>(</span><span>"x"</span><span>, x),</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>]:</span></div></div><div><div><div>16</div></div><div><span>        </span><span># 二阶中心差分近似 y''</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>y_xx </span><span>=</span><span> (y[</span><span>2</span><span><span>:] </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> y[</span></span><span>1</span><span><span>:</span><span>-</span></span><span>1</span><span><span>] </span><span>+</span><span> y[:</span><span>-</span></span><span>2</span><span><span>]) </span><span>/</span><span> h</span><span>**</span></span><span>2</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>residual </span><span>=</span><span> np.</span><span>max</span><span>(np.</span><span>abs</span><span>(y_xx </span><span>+</span><span> y[</span><span>1</span><span><span>:</span><span>-</span></span><span>1</span><span>]))</span></div></div><div><div><div>19</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>:14s</span><span>}</span><span> 的 max|y'' + y| = </span><span>{</span><span>residual</span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># e^{ix} 和 e^{-ix} 在 (cos, sin) 坐标下是 (1, i) 和 (1, -i)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>j</span><span><span>, </span><span>-</span></span><span>1.0</span><span>j</span><span>]])</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"另一组基的行列式："</span><span><span>, np.linalg.</span><span>det</span><span>(E))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"它们也线性无关，秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(E))</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cos            的 max|y'' + y| = 8.2e-07</span></div></div><div><div><div>2</div></div><div><span>sin            的 max|y'' + y| = 8.2e-07</span></div></div><div><div><div>3</div></div><div><span>3 cos - 2 sin  的 max|y'' + y| = 3.0e-06</span></div></div><div><div><div>4</div></div><div><span>x              的 max|y'' + y| = 6.3e+00</span></div></div><div><div><div>5</div></div><div><span>另一组基的行列式： -2j</span></div></div><div><div><div>6</div></div><div><span>它们也线性无关，秩： 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个解的残差只有 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span> 量级，那是差分近似本身的误差（步长约 <span><span>3×10−33 \times 10^{-3}</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，截断误差正比于步长平方）；不是解的 <span><span>y=xy = x</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span></span></span></span> 残差是 6.3，差了七个数量级。行列式 <span><span>−2i-2i</span><span><span><span></span><span>−</span><span>2</span><span>i</span></span></span></span> 不为零，说明 <span><span>(1,i)(1, i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>i</span><span>)</span></span></span></span> 与 <span><span>(1,−i)(1, -i)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>i</span><span>)</span></span></span></span> 线性无关，<span><span>{eix,e−ix}\{e^{ix}, e^{-ix}\}</span><span><span><span></span><span>{</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>i</span><span>x</span></span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>i</span><span>x</span></span></span></span></span></span></span></span></span><span>}</span></span></span></span> 也是基。</p></section>
<section><h2>秩 1 矩阵：一列乘一行<a href="#秩-1-矩阵一列乘一行"><span>#</span></a></h2><p>接下来他回到矩阵的秩，问一个很朴素的问题：秩 1 的 2 × 3 矩阵长什么样？他给第一行 <span><span>(1,4,5)(1, 4, 5)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span>，让大家说第二行。答案是它的倍数，比如 <span><span>(2,8,10)(2, 8, 10)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>8</span><span>,</span><span></span><span>10</span><span>)</span></span></span></span>。这时行空间的基是第一行；列空间的维数也是 1（行秩等于列秩），所有列都是第一列的倍数。他随后写出：</p><span><span><span>[1452810]=[12][145]\begin{bmatrix} 1 &amp; 4 &amp; 5 \\ 2 &amp; 8 &amp; 10 \end{bmatrix}
= \begin{bmatrix} 1 \\ 2 \end{bmatrix}
\begin{bmatrix} 1 &amp; 4 &amp; 5 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p><strong>每个秩 1 矩阵都能写成</strong> <span><span>A=uvTA = \mathbf{u}\mathbf{v}^\mathrm{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>u</span><span><span>v</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：<span><span>u\mathbf{u}</span><span><span><span></span><span>u</span></span></span></span> 是一个列向量，<span><span>vT\mathbf{v}^\mathrm{T}</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是一个行向量。<span><span>u\mathbf{u}</span><span><span><span></span><span>u</span></span></span></span> 张成列空间，<span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 张成行空间。（第 6 版教材把这个形状放在 §1.4 的“列乘行”里，所以我把它列在上面的教材行。）</p><p>他接着说，秩 1 矩阵是所有矩阵的积木：一个 5 × 17 的秩 4 矩阵可以拆成四个秩 1 矩阵之和。（我觉得拿乐高来比最合适，每块只有一个方向，拼够四块才能撑起一个四维的列空间。）</p><p>然后是他很快带过、但容易漏掉的一个问题：秩 4 的 5 × 17 矩阵构成子空间吗？不构成。两个秩 4 矩阵相加，秩可能变成 5。一般的规则是 <span><span>rank⁡(A+B)≤rank⁡A+rank⁡B\operatorname{rank}(A + B) \le \operatorname{rank} A + \operatorname{rank} B</span><span><span><span></span><span><span>rank</span></span><span>(</span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>)</span><span></span><span>≤</span><span></span></span><span><span></span><span><span>rank</span></span><span></span><span>A</span><span></span><span>+</span><span></span></span><span><span></span><span><span>rank</span></span><span></span><span>B</span></span></span></span>，但 5 × 17 矩阵的秩上限是 5，所以 4 + 4 的上界 8 根本用不上。秩 1 矩阵也一样：两个秩 1 矩阵相加，多半是秩 2。（他还补了一句：如果想让零矩阵属于这个集合，得说“秩不超过 4”，但这也救不了加法。）</p><p>我用固定种子随机生成了四个秩 1 矩阵 <span><span>ukvkT\mathbf{u}_k\mathbf{v}_k^\mathrm{T}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，一个一个往上加，看秩怎么涨；再用 SVD（后面第 29 讲才细讲，这里只当作“把矩阵拆成秩 1 矩阵之和”的现成工具）把 <code>B</code> 拆回四项。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 他黑板上的秩 1 矩阵：第二行是第一行的两倍</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>4.0</span><span>, </span><span>5.0</span><span>], [</span><span>2.0</span><span>, </span><span>8.0</span><span>, </span><span>10.0</span><span>]])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>], [</span><span>2.0</span><span>]])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>], [</span><span>4.0</span><span>], [</span><span>5.0</span><span>]])</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"u @ v.T ="</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span><span>(u </span><span>@</span><span> v.T)</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"等于 A："</span><span><span>, np.</span><span>allclose</span><span>(u </span><span>@</span><span> v.T, A))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 用秩 1 矩阵搭出 5 x 17、秩为 4 的矩阵</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>parts </span><span>=</span><span> [</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>1</span><span><span>)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((</span></span><span>1</span><span>, </span><span>17</span><span>))</span></div></div><div><div><div>20</div></div><div><span>        </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> </span><span>sum</span><span>(parts)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>24</div></div><div><span>        </span><span>"四个秩 1 矩阵的秩："</span><span>, [</span><span>int</span><span><span>(np.linalg.</span><span>matrix_rank</span><span>(p)) </span></span><span>for</span><span> p </span><span>in</span><span> parts]</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"累加到第 k 个之后的秩："</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>[</span><span>int</span><span><span>(np.linalg.</span><span>matrix_rank</span><span>(</span></span><span>sum</span><span><span>(parts[: k </span><span>+</span><span> </span></span><span>1</span><span>]))) </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)],</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的形状："</span><span>, B.shape, </span><span>"，秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B))</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 反过来：从 B 里拆出四个秩 1 矩阵（用 SVD，后面会细讲）</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(B)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>rebuilt </span><span>=</span><span> </span><span>sum</span><span><span>(s[k] </span><span>*</span><span> np.</span><span>outer</span><span>(U[:, k], Vt[k]) </span></span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>))</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"SVD 拆出的四项加回来等于 B："</span><span><span>, np.</span><span>allclose</span><span>(rebuilt, B))</span></span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span># 秩 4 的矩阵不构成子空间：两个秩 4 相加，秩可以到 5</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> </span><span>sum</span><span>(</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>1</span><span><span>)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((</span></span><span>1</span><span>, </span><span>17</span><span>))</span></div></div><div><div><div>40</div></div><div><span>        </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>43</div></div><div><span>        </span><span>"rank(B) ="</span><span>,</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(B),</span></div></div><div><div><div>45</div></div><div><span>        </span><span>"，rank(C) ="</span><span>,</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(C),</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>49</div></div><div><span>        </span><span>"rank(B + C) ="</span><span>,</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(B </span><span>+</span><span> C),</span></div></div><div><div><div>51</div></div><div><span>        </span><span>"，上界 4 + 4 = 8，但 5 x 17 最多 5"</span><span>,</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>53</div></div><div><span>    </span><span># 两个秩 1 矩阵相加，同样不再是秩 1</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>p, q </span><span>=</span><span> parts[</span><span>0</span><span>], parts[</span><span>1</span><span>]</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>"rank(p + q) ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(p </span><span>+</span><span> q))</span></span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>秩： 1</span></div></div><div><div><div>2</div></div><div><span>u @ v.T =</span></div></div><div><div><div>3</div></div><div><span>[[ 1.  4.  5.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 2.  8. 10.]]</span></div></div><div><div><div>5</div></div><div><span>等于 A： True</span></div></div><div><div><div>6</div></div><div><span>四个秩 1 矩阵的秩： [1, 1, 1, 1]</span></div></div><div><div><div>7</div></div><div><span>累加到第 k 个之后的秩： [1, 2, 3, 4]</span></div></div><div><div><div>8</div></div><div><span>B 的形状： (5, 17) ，秩： 4</span></div></div><div><div><div>9</div></div><div><span>SVD 拆出的四项加回来等于 B： True</span></div></div><div><div><div>10</div></div><div><span>rank(B) = 4 ，rank(C) = 4</span></div></div><div><div><div>11</div></div><div><span>rank(B + C) = 5 ，上界 4 + 4 = 8，但 5 x 17 最多 5</span></div></div><div><div><div>12</div></div><div><span>rank(p + q) = 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前三行：<code>u @ v.T</code> 恰好还原出他黑板上的矩阵，秩为 1。累加的秩依次是 1、2、3、4：每加一块积木，秩涨 1，加到第四块就是 4。SVD 拆出的四项加回去也等于 <code>B</code>。然后是不构成子空间的证据：两个随机的秩 4 矩阵相加，秩是 5 而不是 4；两个秩 1 矩阵相加，秩是 2 而不是 1。</p></section>
<section><h2>分量之和为零：一个具体的零空间<a href="#分量之和为零一个具体的零空间"><span>#</span></a></h2><p>下面这个例子他拿来把前几讲的东西过一遍：<span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 里分量之和为零的向量，</p><span><span><span>S={b∈R4:b1+b2+b3+b4=0}S = \{\mathbf{b} \in \mathbb{R}^4 : b_1 + b_2 + b_3 + b_4 = 0\}</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>{</span><span>b</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span><span></span><span>:</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>}</span></span></span></span></span><p>它是子空间：乘以 6，分量之和还是 <span><span>6×0=06 \times 0 = 0</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；两个向量相加，分量之和还是 0。那它的维数和基呢？他先让大家猜，三，然后问：这是哪个矩阵的零空间？</p><p>答案是 <span><span>A=[1111]A = \begin{bmatrix} 1 &amp; 1 &amp; 1 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，因为 <span><span>Ab=0A\mathbf{b} = 0</span><span><span><span></span><span>A</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 就是 <span><span>b1+b2+b3+b4=0b_1 + b_2 + b_3 + b_4 = 0</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。这个矩阵的秩是 1，所以零空间的维数是 <span><span>n−r=4−1=3n - r = 4 - 1 = 3</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>。特解的做法和第 7 讲一样：第一列是主列，后三列是自由列；每次给一个自由变量取 1，主变量就取 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，让总和为零：</p><span><span><span>[−1100], [−1010], [−1001]\begin{bmatrix} -1 \\ 1 \\ 0 \\ 0 \end{bmatrix},\
\begin{bmatrix} -1 \\ 0 \\ 1 \\ 0 \end{bmatrix},\
\begin{bmatrix} -1 \\ 0 \\ 0 \\ 1 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span> </span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span> </span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>这就是 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的一组基。（他说不是唯一的三个，但这三个是“特解”。）剩下三个子空间他也顺手说了：行空间是 <span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 里这一行的所有倍数，维数 1；列空间是 <span><span>R1\mathbb{R}^1</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span></span></span></span></span></span></span></span>（因为 <span><span>m=1m = 1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，列只有一个分量）整个，维数 1；左零空间是 <span><span>ATA^\mathrm{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的零空间，只有零向量，他特意花时间讲了零维空间的基是空集，空集里有 0 个向量。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># S = 分量之和为 0 的向量 = 全 1 行矩阵的零空间</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>ones</span><span>((</span><span>1</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>int</span><span><span>(np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"m ="</span><span>, m, </span><span>"，n ="</span><span>, n, </span><span>"，r ="</span><span>, r)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 特解：每个自由变量取 1，主变量取 -1</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>vstack</span><span>([</span><span>-</span><span>np.</span><span>ones</span><span>((</span><span>1</span><span>, </span><span>3</span><span><span>)), np.</span><span>eye</span><span>(</span></span><span>3</span><span>)])</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"N ="</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(N)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ N ="</span><span><span>, A </span><span>@</span><span> N)</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"N 的秩（零空间的维数）："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(N))</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 四个子空间的维数：行空间 r、零空间 n - r、列空间 r、左零空间 m - r</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>dims </span><span>=</span><span> {</span><span>"行空间"</span><span>: r, </span><span>"零空间"</span><span><span>: n </span><span>-</span><span> r, </span></span><span>"列空间"</span><span>: r, </span><span>"左零空间"</span><span><span>: m </span><span>-</span><span> r}</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(dims)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"行空间 + 零空间 = n："</span><span>, dims[</span><span>"行空间"</span><span><span>] </span><span>+</span><span> dims[</span></span><span>"零空间"</span><span><span>] </span><span>==</span><span> n)</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"列空间 + 左零空间 = m："</span><span>, dims[</span><span>"列空间"</span><span><span>] </span><span>+</span><span> dims[</span></span><span>"左零空间"</span><span><span>] </span><span>==</span><span> m)</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m = 1 ，n = 4 ，r = 1</span></div></div><div><div><div>2</div></div><div><span>N =</span></div></div><div><div><div>3</div></div><div><span>[[-1. -1. -1.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 1.  0.  0.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 0.  1.  0.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[ 0.  0.  1.]]</span></div></div><div><div><div>7</div></div><div><span>A @ N = [[0. 0. 0.]]</span></div></div><div><div><div>8</div></div><div><span>N 的秩（零空间的维数）： 3</span></div></div><div><div><div>9</div></div><div><span>{'行空间': 1, '零空间': 3, '列空间': 1, '左零空间': 0}</span></div></div><div><div><div>10</div></div><div><span>行空间 + 零空间 = n： True</span></div></div><div><div><div>11</div></div><div><span>列空间 + 左零空间 = m： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>AN=0A\mathbf{N} = 0</span><span><span><span></span><span>A</span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 验证了三个特解都在 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 里，<span><span>N\mathbf{N}</span><span><span><span></span><span>N</span></span></span></span> 的秩是 3，所以它们线性无关，正好是三维。四个维数是 1、3、1、0，行空间加零空间是 <span><span>1+3=4=n1 + 3 = 4 = n</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>，列空间加左零空间是 <span><span>1+0=1=m1 + 0 = 1 = m</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span></span></span></span>，和上一讲的结论吻合。</p></section>
<section><h2>小世界图：用邻接矩阵的幂数步数<a href="#小世界图用邻接矩阵的幂数步数"><span>#</span></a></h2><p>最后五分钟，他开始介绍图：一个图是一些节点和连接节点的边。他在黑板上画了一个 5 个节点、6 条边的图，说有一个 5 × 6 的矩阵能说明这个图的一切，但把矩阵留到下一讲（关联矩阵，见 <a href="/posts/linear-algebra-12-graphs-and-incidence-matrices/">第 12 讲：图、网络与关联矩阵</a>）。这一讲他只问一个问题：如果每个人是一个节点，朋友之间连一条边，两个人最少隔几步？他用自己和 Clinton 的距离做例子（两步），然后提到“六度分隔”这个说法，并说有几条捷径，所有距离就会大幅下降。</p><p>字幕里没有他给出具体的邻接矩阵，所以下面是我自己的实验。邻接矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的 <span><span>(i,j)(i, j)</span><span><span><span></span><span>(</span><span>i</span><span>,</span><span></span><span>j</span><span>)</span></span></span></span> 项是 1 当且仅当 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 和 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 是朋友。那么 <span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 的 <span><span>(i,j)(i, j)</span><span><span><span></span><span>(</span><span>i</span><span>,</span><span></span><span>j</span><span>)</span></span></span></span> 项数的就是从 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 到 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 的长度恰为 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 的路径条数，所以最小的、使 <span><span>AkA^k</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 的 <span><span>(i,j)(i, j)</span><span><span><span></span><span>(</span><span>i</span><span>,</span><span></span><span>j</span><span>)</span></span></span></span> 项非零的 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span>，就是 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 到 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 的距离。我取 60 个人围成一圈，每人只认识左右两个邻居，再随机加 6 条边，每次都用邻接矩阵的幂算全部 <span><span>60×6060 \times 60</span><span><span><span></span><span>60</span><span></span><span>×</span><span></span></span><span><span></span><span>60</span></span></span></span> 对距离，并和逐层搜索（BFS）的结果对照。（为了不让路径条数溢出，每一步把非零项压成 1。我没有给这 60 个人起名字，名字比代码还难想。）</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> collections </span><span>import</span><span> deque</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>ring</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"""n 个人围成一圈，每人只认识左右两个邻居。"""</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>zeros</span><span>((n, n), </span><span>dtype</span><span>=</span><span>int</span><span>)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>A[i, (i </span><span>+</span><span> </span><span>1</span><span><span>) </span><span>%</span><span> n] </span><span>=</span><span> A[(i </span><span>+</span><span> </span></span><span>1</span><span><span>) </span><span>%</span><span> n, i] </span><span>=</span><span> </span></span><span>1</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> A</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>def</span><span> </span><span>distances</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>"""用邻接矩阵的幂求最短距离：A^k 的 (i, j) 项非零，说明有长 k 的路。"""</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>dist </span><span>=</span><span> np.</span><span>full</span><span>((n, n), </span><span>-</span><span>1</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>np.</span><span>fill_diagonal</span><span>(dist, </span><span>0</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>walks </span><span>=</span><span> np.</span><span>eye</span><span>(n, </span><span>dtype</span><span>=</span><span>int</span><span>)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, n):</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>walks </span><span>=</span><span> (walks </span><span>@</span><span> A </span><span>&gt;</span><span> </span><span>0</span><span><span>).</span><span>astype</span><span>(</span></span><span>int</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>dist[(walks </span><span>&gt;</span><span> </span><span>0</span><span><span>) </span><span>&amp;</span><span> (dist </span><span>&lt;</span><span> </span></span><span>0</span><span><span>)] </span><span>=</span><span> k</span></span></div></div><div><div><div>23</div></div><div><span>        </span><span>if</span><span><span> (dist </span><span>&gt;=</span><span> </span></span><span>0</span><span><span>).</span><span>all</span><span>():</span></span></div></div><div><div><div>24</div></div><div><span>            </span><span>break</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span> dist</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>def</span><span> </span><span>bfs</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>start</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; list[</span><span>int</span><span>]:</span></div></div><div><div><div>29</div></div><div><span>    </span><span>"""逐层搜索，当作对照。"""</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> [</span><span>-</span><span>1</span><span><span>] </span><span>*</span><span> n</span></span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>d[start] </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>queue </span><span>=</span><span> </span><span>deque</span><span>([start])</span></div></div><div><div><div>34</div></div><div><span>    </span><span>while</span><span> queue:</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>i </span><span>=</span><span> queue.</span><span>popleft</span><span>()</span></div></div><div><div><div>36</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span><span> np.</span><span>flatnonzero</span><span>(A[i]):</span></span></div></div><div><div><div>37</div></div><div><span>            </span><span>if</span><span><span> d[j] </span><span>&lt;</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>                </span></span><span>d[j] </span><span>=</span><span> d[i] </span><span>+</span><span> </span><span>1</span></div></div><div><div><div>39</div></div><div><span><span>                </span></span><span>queue.</span><span>append</span><span>(j)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>return</span><span> d</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>60</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> </span><span>ring</span><span>(n)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> </span><span>distances</span><span>(A)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span><span>"圆环：最远距离"</span><span><span>, D.</span><span>max</span><span>(), </span></span><span>"，平均距离"</span><span>, </span><span>round</span><span><span>(D.</span><span>mean</span><span>(), </span></span><span>2</span><span>))</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>    </span><span># 加 6 条随机的“远距离朋友”</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>52</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>6</span><span>):</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>i, j </span><span>=</span><span> rng.</span><span>choice</span><span>(n, </span><span>size</span><span>=</span><span>2</span><span>, </span><span>replace</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>B[i, j] </span><span>=</span><span> B[j, i] </span><span>=</span><span> </span><span>1</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> </span><span>distances</span><span>(B)</span></div></div><div><div><div>56</div></div><div><span>    </span><span>print</span><span>(</span><span>"加 6 条捷径：最远距离"</span><span><span>, E.</span><span>max</span><span>(), </span></span><span>"，平均距离"</span><span>, </span><span>round</span><span><span>(E.</span><span>mean</span><span>(), </span></span><span>2</span><span>))</span></div></div><div><div><div>57</div></div><div><span>    </span><span>print</span><span>(</span><span>"平均距离缩小到原来的"</span><span>, </span><span>round</span><span><span>(E.</span><span>mean</span><span>() </span><span>/</span><span> D.</span><span>mean</span><span>(), </span></span><span>2</span><span>), </span><span>"倍"</span><span>)</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>ok </span><span>=</span><span> </span><span>all</span><span><span>(np.</span><span>array_equal</span><span>(E[i], </span><span>bfs</span><span>(B, i)) </span></span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n))</span></div></div><div><div><div>60</div></div><div><span>    </span><span>print</span><span>(</span><span>"与逐层搜索一致："</span><span>, ok)</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>圆环：最远距离 30 ，平均距离 15.0</span></div></div><div><div><div>2</div></div><div><span>加 6 条捷径：最远距离 17 ，平均距离 7.17</span></div></div><div><div><div>3</div></div><div><span>平均距离缩小到原来的 0.48 倍</span></div></div><div><div><div>4</div></div><div><span>与逐层搜索一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有捷径时，圆环上最远的两个人隔 30 步，平均距离 15.0。加了至多 6 条随机边（相对原来的 60 条边只多 10%）之后，最远距离降到 17，平均距离降到 7.17，缩小到原来的 0.48 倍。矩阵的幂算出的距离和 BFS 逐行一致。这大概就是他说的“几条捷径让距离大幅下降”：边只多了十分之一，平均距离却少了一半多。</p><p>他说的 Clinton 笑话、“six degrees of separation”，我就不细讲了。我猜这个结论的关键是：随机捷径把圆环上本来很远的点直接接到了一起。真实的社交网络比这个模型复杂得多，我没有验证任何关于真实网络的说法。</p><p>下一讲他会用那个 5 × 6 矩阵说明图与线性代数的关系，我猜零空间和左零空间会在图上有很具体的含义。第 12 讲见：<a href="/posts/linear-algebra-12-graphs-and-incidence-matrices/">18.06 第 12 讲：图、网络与关联矩阵</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-10-four-fundamental-subspaces/</id>
      <title type="text">18.06 第 10 讲：四个基本子空间</title>
      <published>2026-10-18T03:00:00.000Z</published>
      <updated>2026-10-18T03:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-10-four-fundamental-subspaces/"/>
      <summary type="text">MIT 18.06 第 10 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 对 Strang 的 3 × 4 矩阵分别用消元和 SVD 求出四个子空间的基，维数是 2、2、2、1，再用 5 个随机造的不同形状矩阵验证 r、n - r、r、m - r 全部成立。</summary>
      <content type="html"><![CDATA[<p>一个 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span>、秩为 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 的矩阵带着四个子空间：列空间、行空间、零空间、左零空间，维数依次是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>、<span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>。Strang 在这一讲里把它们的基和维数全部交代清楚，我用他黑板上的 3 × 4 矩阵从消元和 SVD 两条路各求了一遍，得到的维数都是 2、2、2、1。然后我随手造了 5 个形状和秩各不相同的矩阵，<span><span>r+(n−r)=nr + (n - r) = n</span><span><span><span></span><span>r</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span> 与 <span><span>r+(m−r)=mr + (m - r) = m</span><span><span><span></span><span>r</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span></span></span></span> 一共 10 次检查，10 次都成立。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=nHlE7EgJFds" target="_blank">Lecture 10: The four fundamental subspaces</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-10-the-four-fundamental-subspaces/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §3.5 Dimensions of the Four Subspaces
前置：<a href="/posts/linear-algebra-09-independence-basis-dimension/">第 9 讲：线性无关、基与维数</a>
一句话：<span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span> 里的行空间（<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 维）与零空间（<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 维）加起来是 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 维，<span><span>Rm\mathbb{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span> 里的列空间（<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 维）与左零空间（<span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 维）加起来是 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 维。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几分钟他在纠正上一讲的一个错误，本篇第一节用代码把它重算了一遍，想看他怎么自嘲可以看，想赶进度可以倍速。</li>
<li>他从 <span><span>[A  I][A \; I]</span><span><span><span></span><span>[</span><span>A</span><span></span><span>I</span><span>]</span></span></span></span> 消元出 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span>，再从 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 的最后一行读出左零空间的那一段要认真看，这是本讲唯一不能从 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 直接读出来的东西。</li>
<li>他讲维数的那一段只给了结论，行空间的维数是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 要靠后面用 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的前 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 行来论证，听的时候留意这一步。</li>
<li>最后几分钟他开始讲“矩阵本身也是向量”，只是引子，具体的维数他说留到下一讲。我在最后一节先数了一遍。</li>
</ul></div></div>
<section><h2>先改一个错：三列相关，因为有两行相同<a href="#先改一个错三列相关因为有两行相同"><span>#</span></a></h2><p>上一讲末尾，他为了说明“基不唯一”，取了三个向量 <span><span>(1,1,2)(1, 1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(2,2,5)(2, 2, 5)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span>、<span><span>(3,3,8)(3, 3, 8)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span>，以为第三个会稍微伸出前两个张成的平面。下课后有学生告诉他：不对，这三个向量线性相关。她并没有去解 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 的系数，她只是把这三个向量排成矩阵的三列，然后看了一眼：</p><span><span><span>A=[123123258]A = \begin{bmatrix} 1 &amp; 2 &amp; 3 \\ 1 &amp; 2 &amp; 3 \\ 2 &amp; 5 &amp; 8 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>前两行一模一样，所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 不可逆，三列不可能无关。<strong>为什么看行能知道列的事？</strong> 因为行空间和列空间的维数相同，都等于秩。两行相同，行空间最多 2 维，所以秩最多是 2，三列里最多有两列是独立的。这就是他说的“行告诉了我列的事情”，也是这一讲真正想说的话的预告。</p><p>我把这件事算一遍，包括那个他当时懒得去找的组合：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 第 9 讲的错误：三列 (1,1,2)、(2,2,5)、(3,3,8)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>5.0</span><span>, </span><span>8.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A), </span></span><span>"，det ="</span><span><span>, np.linalg.</span><span>det</span><span>(A))</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的转置的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A.T))</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 学生没有去找组合，而是看到了两行相同；组合其实是 -1 份列一加 2 份列二</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>coef, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A[:, :</span><span>2</span><span>], A[:, </span><span>2</span><span>], </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"第三列 = 列一和列二的组合，系数："</span><span>, coef)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"验证："</span><span><span>, np.</span><span>allclose</span><span>(A[:, :</span></span><span>2</span><span><span>] </span><span>@</span><span> coef, A[:, </span></span><span>2</span><span>]))</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 对照：(3, 3, 7) 本来就是列一加列二，(3, 3, 8) 以为能伸出平面</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> last </span><span>in</span><span> (</span><span>7.0</span><span>, </span><span>8.0</span><span>):</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>B </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>B[:, </span><span>2</span><span><span>] </span><span>=</span><span> [</span></span><span>3.0</span><span>, </span><span>3.0</span><span>, last]</span></div></div><div><div><div>21</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"第三列 (3, 3, </span><span>{</span><span>last</span><span>:.0f</span><span>}</span><span>)：秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B))</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 的秩： 2 ，det = 0.0</span></div></div><div><div><div>2</div></div><div><span>A 的转置的秩： 2</span></div></div><div><div><div>3</div></div><div><span>第三列 = 列一和列二的组合，系数： [-1.  2.]</span></div></div><div><div><div>4</div></div><div><span>验证： True</span></div></div><div><div><div>5</div></div><div><span>第三列 (3, 3, 7)：秩 = 2</span></div></div><div><div><div>6</div></div><div><span>第三列 (3, 3, 8)：秩 = 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的秩都是 2。第三列等于 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 份第一列加 <span><span>22</span><span><span><span></span><span>2</span></span></span></span> 份第二列：<span><span>−(1,1,2)+2(2,2,5)=(3,3,8)-(1, 1, 2) + 2(2, 2, 5) = (3, 3, 8)</span><span><span><span></span><span>−</span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span>，系数 <span><span>(−1,2)(-1, 2)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 是最小二乘返回的，验证为 <code>True</code>。最后两行对比他的两个版本：<span><span>(3,3,7)(3, 3, 7)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span> 和 <span><span>(3,3,8)(3, 3, 8)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span> 的秩都是 2，因为只要前两行相同，第三列无论怎么改都不会让秩升到 3。他当时的直觉是对的（<span><span>(3,3,7)(3, 3, 7)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span> 确实是前两列之和），错在以为改个 8 就能伸出平面，却没看那两行。</p></section>
<section><h2>四个子空间，两个大空间<a href="#四个子空间两个大空间"><span>#</span></a></h2><p><span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的矩阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 对应四个子空间：</p>

<table><thead><tr><th>子空间</th><th>记号</th><th>是什么</th><th>住在</th><th>维数</th></tr></thead><tbody><tr><td>列空间</td><td><span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span></td><td>列的所有组合</td><td><span><span>Rm\mathbb{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span></td><td><span><span>rr</span><span><span><span></span><span>r</span></span></span></span></td></tr><tr><td>零空间</td><td><span><span>N(A)N(A)</span><span><span><span></span><span>N</span><span>(</span><span>A</span><span>)</span></span></span></span></td><td><span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的所有解</td><td><span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span></td><td><span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span></td></tr><tr><td>行空间</td><td><span><span>C(AT)C(A^\mathsf{T})</span><span><span><span></span><span>C</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td>行的所有组合</td><td><span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span></td><td><span><span>rr</span><span><span><span></span><span>r</span></span></span></span></td></tr><tr><td>左零空间</td><td><span><span>N(AT)N(A^\mathsf{T})</span><span><span><span></span><span>N</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的所有解</td><td><span><span>Rm\mathbb{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span></td><td><span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span></td></tr></tbody></table><p><strong>“行空间”为什么写成 <span><span>C(AT)C(A^\mathsf{T})</span><span><span><span></span><span>C</span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span></span></span></span>？</strong> 行是横着躺的，他偏好所有向量都站着，于是把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 转置，行就变成了列：行空间就是 <span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的列空间，数学上什么也没发生，只是让躺着的向量站起来。</p><p>归属的规则很好记：看向量有几个分量。<span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 里的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个分量，所以零空间在 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>；<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的每一行有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个分量，行空间也在 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>；<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的每一列有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 个分量，列空间在 <span><span>Rm\mathbb{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span>；<span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 里的 <span><span>y\mathbf{y}</span><span><span><span></span><span>y</span></span></span></span> 有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 个分量，左零空间也在 <span><span>Rm\mathbb{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span>。（他说第四个子空间一直受着“二等公民”的待遇，不应该被挤到一边。我读到这里替它觉得有点委屈。）</p><p>他先给了维数的“短答案”：</p><ul>
<li>列空间：基是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的主列，维数是秩 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>。</li>
<li>零空间：基是特解，每个自由变量一个，所以维数是 <span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>。</li>
<li>行空间：维数也是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>。</li>
<li>左零空间：<span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是 <span><span>n×mn \times m</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span> 的矩阵，有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 列，同一条规则“主变量加自由变量等于列数”，所以维数是 <span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>。</li>
</ul><p>我觉得最好记的方式是把它看成预算：<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个变量的预算里，<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个被方程占用（主变量），剩下的 <span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 个自由支配（自由变量），总数不变。<span><span>ATA^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 个变量，同样的预算表再用一遍。</p></section>
<section><h2>一次消元，四个基<a href="#一次消元四个基"><span>#</span></a></h2><p>“短答案”只给了数字，接下来他要拿基来证明它们。他用的矩阵是上一讲末尾的 <span><span>3×43 \times 4</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 例子：</p><span><span><span>A=[123111211231]A = \begin{bmatrix} 1 &amp; 2 &amp; 3 &amp; 1 \\ 1 &amp; 1 &amp; 2 &amp; 1 \\ 1 &amp; 2 &amp; 3 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>他做的消元是：第二行减第一行，第三行减第一行，第二行乘 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，再从第一行里减去两份第二行，得到</p><span><span><span>R=[101101100000]R = \begin{bmatrix} 1 &amp; 0 &amp; 1 &amp; 1 \\ 0 &amp; 1 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>主列是第 1、2 列，<span><span>r=2r = 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>。前三个子空间的基就在这里：</p><ul>
<li><strong>列空间</strong>：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的主列，也就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 1、2 列。注意是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的，不是 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的。</li>
<li><strong>行空间</strong>：<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的前 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 行，<span><span>(1,0,1,1)(1, 0, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 和 <span><span>(0,1,1,0)(0, 1, 1, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。</li>
<li><strong>零空间</strong>：特解，第三、第四列是自由列，各给一个特解。</li>
</ul><p>第四个，左零空间，在 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 里看不出来。他想到的办法是回忆 Gauss-Jordan：在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 右边接一个单位矩阵，消元一路做下去，<span><span>[A  I][A \; I]</span><span><span><span></span><span>[</span><span>A</span><span></span><span>I</span><span>]</span></span></span></span> 变成 <span><span>[R  E][R \; E]</span><span><span><span></span><span>[</span><span>R</span><span></span><span>E</span><span>]</span></span></span></span>。<span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 是一个 <span><span>m×mm \times m</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>m</span></span></span></span> 矩阵，记着从 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 到 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的全部行操作，所以 <span><span>EA=REA = R</span><span><span><span></span><span>E</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>R</span></span></span></span>。方阵可逆时 <span><span>R=IR = I</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，<span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 就是 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>；现在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是长方形，<span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 不再是逆，但它仍然是那个把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 变成 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的矩阵。</p><p>下面的脚本自己写了一个 <code>rref</code>（只在前 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 列里找主元，不然接在后面的单位矩阵会被一起“约化”，得到的就不是 Strang 的 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 了），然后一次性读出四个基，并逐个验证：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rref</span><span>(</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>M</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>ncols</span></span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span></div></div><div><div><div>6</div></div><div><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"""对 M 做 Gauss-Jordan 消元，只在前 ncols 列里找主元。"""</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> M.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> R.shape[</span><span>0</span><span>]</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(ncols):</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> row </span><span>==</span><span> m:</span></span></div></div><div><div><div>14</div></div><div><span>            </span><span>break</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>found </span><span>=</span><span> np.</span><span>nonzero</span><span>(np.</span><span>abs</span><span>(R[row:, col]) </span><span>&gt;</span><span> tol)[</span><span>0</span><span>]</span></div></div><div><div><div>16</div></div><div><span>        </span><span>if</span><span><span> found.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>17</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> row </span><span>+</span><span> found[</span><span>0</span><span>]</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>R[[row, p]] </span><span>=</span><span> R[[p, row]]</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>R[row] </span><span>/=</span><span> R[row, col]</span></div></div><div><div><div>21</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>22</div></div><div><span>            </span><span>if</span><span><span> i </span><span>!=</span><span> row:</span></span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>R[i] </span><span>-=</span><span> R[i, col] </span><span>*</span><span> R[row]</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(col)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>row </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span> R, pivots</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>]]</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># [A I] 消元成 [R E]，E 记录了从 A 到 R 的全部行操作</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>RE</span><span>, pivots </span><span>=</span><span> </span><span>rref</span><span>(np.</span><span>hstack</span><span>([A, np.</span><span>eye</span><span>(m)]), n)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>R, E </span><span>=</span><span> </span><span>RE</span><span>[:, :n], </span><span>RE</span><span>[:, n:]</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>len</span><span>(pivots)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"R =</span><span>\n</span><span>"</span><span><span>, R </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"E =</span><span>\n</span><span>"</span><span><span>, E </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>43</div></div><div><span>        </span><span>"主列："</span><span>, pivots, </span><span>"，r ="</span><span>, r, </span><span>"，E @ A == R："</span><span><span>, np.</span><span>allclose</span><span>(E </span><span>@</span><span> A, R)</span></span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>    </span><span># 列空间：A 的主列（是 A 的，不是 R 的）</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>col_basis </span><span>=</span><span> A[:, pivots]</span></div></div><div><div><div>48</div></div><div><span>    </span><span># 行空间：R 的前 r 行</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>row_basis </span><span>=</span><span> R[:r]</span></div></div><div><div><div>50</div></div><div><span>    </span><span># 零空间：每个自由变量一个特解</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>53</div></div><div><span>    </span><span>for</span><span> k, j </span><span>in</span><span> </span><span>enumerate</span><span>(free):</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>N[j, k] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>N[pivots, k] </span><span>=</span><span> </span><span>-</span><span>R[:r, j]</span></div></div><div><div><div>56</div></div><div><span>    </span><span># 左零空间：E 的后 m - r 行</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>left_basis </span><span>=</span><span> E[r:].T</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>    </span><span>print</span><span>(</span><span>"列空间的基（列）：</span><span>\n</span><span>"</span><span>, col_basis)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>print</span><span>(</span><span>"行空间的基（行）：</span><span>\n</span><span>"</span><span><span>, row_basis </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>61</div></div><div><span>    </span><span>print</span><span>(</span><span>"零空间的基（列）：</span><span>\n</span><span>"</span><span>, N)</span></div></div><div><div><div>62</div></div><div><span>    </span><span>print</span><span>(</span><span>"左零空间的基（列）：</span><span>\n</span><span>"</span><span>, left_basis)</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ N == 0："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> N, </span></span><span>0</span><span>))</span></div></div><div><div><div>65</div></div><div><span>    </span><span>print</span><span>(</span><span>"A.T @ y == 0："</span><span><span>, np.</span><span>allclose</span><span>(A.T </span><span>@</span><span> left_basis, </span></span><span>0</span><span>))</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>dims </span><span>=</span><span> [col_basis.shape[</span><span>1</span><span>], row_basis.shape[</span><span>0</span><span>], N.shape[</span><span>1</span><span>]]</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>dims.</span><span>append</span><span>(left_basis.shape[</span><span>1</span><span>])</span></div></div><div><div><div>68</div></div><div><span>    </span><span>print</span><span>(</span><span>"四个维数："</span><span>, dims, </span><span>"，期望："</span><span><span>, [r, r, n </span><span>-</span><span> r, m </span><span>-</span><span> r])</span></span></div></div><div><div><div>69</div></div><div>
</div></div><div><div><div>70</div></div><div><span>    </span><span># 行操作不改变行空间，但改变了列空间</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>same_rows </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>vstack</span><span>([A, R]))</span></div></div><div><div><div>72</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 与 R 的行叠在一起的秩（等于 r 就是同一个行空间）："</span><span>, same_rows)</span></div></div><div><div><div>73</div></div><div><span><span>    </span></span><span>probe </span><span>=</span><span> np.</span><span>column_stack</span><span>([R[:, pivots], A[:, </span><span>0</span><span>]])</span></div></div><div><div><div>74</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>75</div></div><div><span>        </span><span>"A 的第一列放进 R 的列空间后的秩（大于 r 就是跑出去了）："</span><span>,</span></div></div><div><div><div>76</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(probe),</span></div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>78</div></div><div>
</div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>R =</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[[1. 0. 1. 1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 1. 1. 0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[0. 0. 0. 0.]]</span></div></div><div><div><div>5</div></div><div><span>E =</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[[-1.  2.  0.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ 1. -1.  0.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[-1.  0.  1.]]</span></div></div><div><div><div>9</div></div><div><span>主列： [0, 1] ，r = 2 ，E @ A == R： True</span></div></div><div><div><div>10</div></div><div><span>列空间的基（列）：</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[[1. 2.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[1. 1.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[1. 2.]]</span></div></div><div><div><div>14</div></div><div><span>行空间的基（行）：</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[[1. 0. 1. 1.]</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>[0. 1. 1. 0.]]</span></div></div><div><div><div>17</div></div><div><span>零空间的基（列）：</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>[[-1. -1.]</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>[-1.  0.]</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>[ 1.  0.]</span></div></div><div><div><div>21</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>22</div></div><div><span>左零空间的基（列）：</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>[[-1.]</span></div></div><div><div><div>24</div></div><div><span><span> </span></span><span>[ 0.]</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>[ 1.]]</span></div></div><div><div><div>26</div></div><div><span>A @ N == 0： True</span></div></div><div><div><div>27</div></div><div><span>A.T @ y == 0： True</span></div></div><div><div><div>28</div></div><div><span>四个维数： [2, 2, 2, 1] ，期望： [2, 2, 2, 1]</span></div></div><div><div><div>29</div></div><div><span>A 与 R 的行叠在一起的秩（等于 r 就是同一个行空间）： 2</span></div></div><div><div><div>30</div></div><div><span>A 的第一列放进 R 的列空间后的秩（大于 r 就是跑出去了）： 3</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 与他在黑板上得到的完全一样，<span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 也是他写的 <span><span>[−1201−10−101]\begin{bmatrix} -1 &amp; 2 &amp; 0 \\ 1 &amp; -1 &amp; 0 \\ -1 &amp; 0 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span>。<code>E @ A == R</code> 为真，所以 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 确实记录了所有行操作。</p><p><strong>左零空间在哪里？</strong> 它是 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 的最后 <span><span>m−r=1m - r = 1</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 行：<span><span>(−1,0,1)(-1, 0, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。含义是“<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 份第一行加 <span><span>11</span><span><span><span></span><span>1</span></span></span></span> 份第三行等于零行”，第一行和第三行相同，当然如此。脚本里 <code>A.T @ left_basis</code> 全是零，验证了 <span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</p><p><strong>为什么叫“左”零空间？</strong> 把 <span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 两边转置，得到 <span><span>yTA=0T\mathbf{y}^\mathsf{T}A = \mathbf{0}^\mathsf{T}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>0</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：<span><span>yT\mathbf{y}^\mathsf{T}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 是一个行向量，从左边乘在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 上。零空间是“列的哪些组合给出零列”，左零空间是“行的哪些组合给出零行”。他的约定是不写成行向量，仍然写 <span><span>ATy=0A^\mathsf{T}\mathbf{y} = \mathbf{0}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，让向量都站着。</p><p>输出的最后四个维数是 <span><span>[2,2,2,1][2, 2, 2, 1]</span><span><span><span></span><span>[</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>]</span></span></span></span>，和 <span><span>[r,r,n−r,m−r]=[2,2,2,1][r, r, n - r, m - r] = [2, 2, 2, 1]</span><span><span><span></span><span>[</span><span>r</span><span>,</span><span></span><span>r</span><span>,</span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span>,</span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>1</span><span>]</span></span></span></span> 吻合。（这个例子有点不够区分，因为 <span><span>r=n−r=2r = n - r = 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，下面的随机矩阵会补上这个漏洞。）</p></section>
<section><h2>行操作保住的是行空间，不是列空间<a href="#行操作保住的是行空间不是列空间"><span>#</span></a></h2><p>他还特意问了一个问题：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的行空间、列空间各自一样吗？</p><ul>
<li><strong>行空间一样。</strong> 每一步行操作都是“把一行减去另一行的倍数”，结果仍然是原来那些行的组合，所以每一行始终留在行空间里；反过来，每一步可逆，倒着做回去，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的行也都是 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的行的组合。两组行张成同一个空间。所以 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的前 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 行是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的行空间的基：它们属于这个空间，彼此独立，个数恰好是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>。这也就解释了为什么“行空间的维数是 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>”：主元有 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个，非零行就有 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个。</li>
<li><strong>列空间不一样。</strong> <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第一列，但 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的列空间只有前两个分量非零的向量，它够不着 <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。</li>
</ul><p>脚本最后两行用秩验证了这两句话：把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的行叠在一起，秩仍是 2，没有长出新方向，说明行空间相同；把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第一列放进 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的列空间，秩从 2 升到 3，说明它跑出去了。所以“列空间的基用 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的主列，行空间的基用 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的行”这个分工不能搞混：前者必须回到原矩阵，后者必须用化简后的。</p></section>
<section><h2>用 SVD 再算一遍<a href="#用-svd-再算一遍"><span>#</span></a></h2><p>消元是手算时代的做法，数值上有更稳的一条路。奇异值分解 <span><span>A=UΣVTA = U \Sigma V^\mathsf{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>Σ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（第 29 讲才会正式讲，这里只拿来用）把四个子空间的标准正交基一次给出来：<span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的前 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 列是列空间，后 <span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 列是左零空间；<span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 的前 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 列是行空间，后 <span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 列是零空间。<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 就是大于容差的奇异值个数。</p><p>我拿它检验两件事：SVD 的基和消元给出的基是不是张成同一个空间（把两组基并排，秩不增加就说明是），以及维数公式在不同形状、不同秩的矩阵上是不是都成立。后者我用 <code>default_rng(1806)</code> 造了 5 个矩阵，每个都是 <span><span>m×rm \times r</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>r</span></span></span></span> 乘 <span><span>r×nr \times n</span><span><span><span></span><span>r</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的随机矩阵，所以秩是已知的 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>four_subspaces</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>):</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""用 SVD 给出四个子空间的标准正交基，各以列的形式返回。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>int</span><span><span>(np.</span><span>sum</span><span>(s </span><span>&gt;</span><span> tol))</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>V </span><span>=</span><span> Vt.T</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> U[:, :r], V[:, :r], V[:, r:], U[:, r:]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>]]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>col, row, null, left </span><span>=</span><span> </span><span>four_subspaces</span><span>(A)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"SVD 的四个维数："</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>col.shape[</span><span>1</span><span>],</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>row.shape[</span><span>1</span><span>],</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>null.shape[</span><span>1</span><span>],</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>left.shape[</span><span>1</span><span>],</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 和消元法给出的基张成同一个空间吗？把两组基并排，秩不涨就是同一个</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>col_rref </span><span>=</span><span> A[:, [</span><span>0</span><span>, </span><span>1</span><span>]]</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>row_rref </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>null_rref </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]]).T</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>left_rref </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]]).T</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>pairs </span><span>=</span><span> [</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>(</span><span>"列空间"</span><span>, col, col_rref),</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>(</span><span>"行空间"</span><span>, row, row_rref.T),</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>(</span><span>"零空间"</span><span>, null, null_rref),</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>(</span><span>"左零空间"</span><span>, left, left_rref),</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>37</div></div><div><span>    </span><span>for</span><span> name, svd_basis, rref_basis </span><span>in</span><span> pairs:</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>stacked </span><span>=</span><span> np.</span><span>hstack</span><span>([svd_basis, rref_basis])</span></div></div><div><div><div>39</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>name,</span></div></div><div><div><div>41</div></div><div><span>            </span><span>"并排后的秩："</span><span>,</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(stacked),</span></div></div><div><div><div>43</div></div><div><span>            </span><span>"，SVD 的基是标准正交的："</span><span>,</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>np.</span><span>allclose</span><span>(svd_basis.T </span><span>@</span><span> svd_basis, np.</span><span>eye</span><span>(svd_basis.shape[</span><span>1</span><span>])),</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 随机造不同形状、不同秩的矩阵，验证 r、n - r、r、m - r</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span><span>"  m   n   r | 列  行  零  左 | 行+零=n  列+左=m"</span><span>)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>for</span><span> m, n, r </span><span>in</span><span> [(</span><span>3</span><span>, </span><span>6</span><span>, </span><span>2</span><span>), (</span><span>5</span><span>, </span><span>3</span><span>, </span><span>3</span><span>), (</span><span>4</span><span>, </span><span>4</span><span>, </span><span>4</span><span>), (</span><span>6</span><span>, </span><span>5</span><span>, </span><span>1</span><span>), (</span><span>7</span><span>, </span><span>9</span><span>, </span><span>4</span><span>)]:</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((m, r)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((r, n))</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>c, w, z, lf </span><span>=</span><span> </span><span>four_subspaces</span><span>(B)</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>dims </span><span>=</span><span> (c.shape[</span><span>1</span><span>], w.shape[</span><span>1</span><span>], z.shape[</span><span>1</span><span>], lf.shape[</span><span>1</span><span>])</span></div></div><div><div><div>54</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>55</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>m</span><span>:3d</span><span>}</span><span> </span><span>{</span><span>n</span><span>:3d</span><span>}</span><span> </span><span>{</span><span>r</span><span>:3d</span><span>}</span><span> | </span><span>{</span><span>dims[</span><span>0</span><span>]</span><span>:2d</span><span>}</span><span>  </span><span>{</span><span>dims[</span><span>1</span><span>]</span><span>:2d</span><span>}</span><span>  "</span></div></div><div><div><div>56</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>dims[</span><span>2</span><span>]</span><span>:2d</span><span>}</span><span>  </span><span>{</span><span>dims[</span><span>3</span><span>]</span><span>:2d</span><span>}</span><span> | "</span></div></div><div><div><div>57</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>dims[</span><span>1</span><span><span>] </span><span>+</span><span> dims[</span></span><span>2</span><span><span>] </span><span>==</span><span> n</span></span><span>}</span><span>  </span><span>{</span><span>dims[</span><span>0</span><span><span>] </span><span>+</span><span> dims[</span></span><span>3</span><span><span>] </span><span>==</span><span> m</span></span><span>}</span><span>"</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SVD 的四个维数： 2 2 2 1</span></div></div><div><div><div>2</div></div><div><span>列空间 并排后的秩： 2 ，SVD 的基是标准正交的： True</span></div></div><div><div><div>3</div></div><div><span>行空间 并排后的秩： 2 ，SVD 的基是标准正交的： True</span></div></div><div><div><div>4</div></div><div><span>零空间 并排后的秩： 2 ，SVD 的基是标准正交的： True</span></div></div><div><div><div>5</div></div><div><span>左零空间 并排后的秩： 1 ，SVD 的基是标准正交的： True</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>m   n   r | 列  行  零  左 | 行+零=n  列+左=m</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>3   6   2 |  2   2   4   1 | True  True</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>5   3   3 |  3   3   0   2 | True  True</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>4   4   4 |  4   4   0   0 | True  True</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>6   5   1 |  1   1   4   5 | True  True</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>7   9   4 |  4   4   5   3 | True  True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前半段，四个子空间的 SVD 基都和消元的基张成同一个空间：列空间并排后的秩 2，行空间 2，零空间 2，左零空间 1，都恰好等于各自的维数。此外 SVD 给的基还是标准正交的，<code>True</code> 就是这个意思。（消元给的基没有这个性质，这是第 14 讲以后才有用的东西，先记下。）</p><p>后半段是维数的验证，<span><span>(m,n,r)(m, n, r)</span><span><span><span></span><span>(</span><span>m</span><span>,</span><span></span><span>n</span><span>,</span><span></span><span>r</span><span>)</span></span></span></span> 各不相同。<span><span>3×63 \times 6</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span>、秩 2 的矩阵，零空间有 4 维；<span><span>6×56 \times 5</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span>、秩 1 的矩阵，左零空间有 5 维；<span><span>4×44 \times 4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的满秩方阵，零空间和左零空间都是 0 维。五行里“行 + 零 = <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>”与“列 + 左 = <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>”全是 <code>True</code>。我觉得有意思的是 <span><span>6×56 \times 5</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span> 这一行：矩阵看着不小，列空间却只有 1 维，一条直线，剩下 5 个维度全归了左零空间。</p></section>
<section><h2>矩阵也是向量<a href="#矩阵也是向量"><span>#</span></a></h2><p>最后几分钟，他把“向量”的意思推广了一步：所有 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 矩阵组成一个向量空间 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>。能做的运算只有两个，矩阵相加、矩阵乘以数，用不上矩阵乘法。零矩阵是零向量，那八条向量空间公理自然满足。<span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 里有子空间：上三角矩阵、对称矩阵，它们的交集是对角矩阵。</p><p>他当场给了对角子空间的基：三个矩阵，比如 <span><span>diag⁡(3,0,0)\operatorname{diag}(3, 0, 0)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>3</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>diag⁡(0,2,0)\operatorname{diag}(0, 2, 0)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>0</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>diag⁡(0,0,7)\operatorname{diag}(0, 0, 7)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span>（他自己说这个 7 选得不怎么样），所以维数是 3。上三角和对称的维数他留到了下一讲。我先数了一遍，办法是把 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 矩阵拉直成 9 维向量，用秩来数张成的维数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>unit</span><span>(</span><span>i</span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>j</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""只有 (i, j) 位置为 1 的 3 x 3 矩阵。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>E[i, j] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> E</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>dim_span</span><span>(</span><span>mats</span><span>:</span><span> list[np.ndarray]</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"""把矩阵拉直成 9 维向量，张成的空间的维数。"""</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> </span><span>int</span><span><span>(np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>array</span><span>([M.</span><span>ravel</span><span>() </span></span><span>for</span><span> M </span><span>in</span><span> mats])))</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>17</div></div><div><span>    </span><span># 3 x 3 矩阵本身就是向量，整个空间 M 是 9 维的</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>pairs </span><span>=</span><span> [(i, j) </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)]</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>everything </span><span>=</span><span> [</span><span>unit</span><span>(i, j) </span><span>for</span><span> i, j </span><span>in</span><span> pairs]</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"M 的维数："</span><span><span>, </span><span>dim_span</span><span>(everything))</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span># Strang 给的三个对角矩阵（其中有他随手写的 7）</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>diagonal </span><span>=</span><span> [np.</span><span>diag</span><span>(d) </span><span>for</span><span> d </span><span>in</span><span> ([</span><span>3.0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>], [</span><span>0</span><span>, </span><span>2.0</span><span>, </span><span>0</span><span>], [</span><span>0</span><span>, </span><span>0</span><span>, </span><span>7.0</span><span>])]</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"对角子空间 D 的维数："</span><span><span>, </span><span>dim_span</span><span>(diagonal))</span></span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>target </span><span>=</span><span> np.</span><span>diag</span><span>([</span><span>5.0</span><span><span>, </span><span>-</span></span><span>4.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>flat </span><span>=</span><span> np.</span><span>array</span><span>([D.</span><span>ravel</span><span>() </span><span>for</span><span> D </span><span>in</span><span> diagonal]).T</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>coef, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(flat, target.</span><span>ravel</span><span>(), </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>combo </span><span>=</span><span> </span><span>sum</span><span><span>(c </span><span>*</span><span> D </span></span><span>for</span><span> c, D </span><span>in</span><span> </span><span>zip</span><span>(coef, diagonal))</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"对角矩阵的组合系数："</span><span><span>, np.</span><span>round</span><span>(coef, </span></span><span>4</span><span>))</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"还原出 diag(5, -4, 1)："</span><span><span>, np.</span><span>allclose</span><span>(combo, target))</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 他留到下一讲的问题：上三角与对称各是几维？交集呢？</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>upper </span><span>=</span><span> [</span><span>unit</span><span>(i, j) </span><span>for</span><span> i, j </span><span>in</span><span> pairs </span><span>if</span><span><span> i </span><span>&lt;=</span><span> j]</span></span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>sym </span><span>=</span><span> [</span><span>unit</span><span>(i, j) </span><span>+</span><span> </span><span>unit</span><span>(j, i) </span><span>for</span><span> i, j </span><span>in</span><span> pairs </span><span>if</span><span><span> i </span><span>&lt;=</span><span> j]</span></span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>both </span><span>=</span><span> </span><span>dim_span</span><span>(upper </span><span>+</span><span> sym)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"上三角："</span><span><span>, </span><span>dim_span</span><span>(upper), </span></span><span>"，对称："</span><span><span>, </span><span>dim_span</span><span>(sym))</span></span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"二者之和："</span><span>, both, </span><span>"，交集的维数："</span><span>, </span><span>6</span><span><span> </span><span>+</span><span> </span></span><span>6</span><span><span> </span><span>-</span><span> both)</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>M 的维数： 9</span></div></div><div><div><div>2</div></div><div><span>对角子空间 D 的维数： 3</span></div></div><div><div><div>3</div></div><div><span>对角矩阵的组合系数： [ 1.6667 -2.      0.1429]</span></div></div><div><div><div>4</div></div><div><span>还原出 diag(5, -4, 1)： True</span></div></div><div><div><div>5</div></div><div><span>上三角： 6 ，对称： 6</span></div></div><div><div><div>6</div></div><div><span>二者之和： 9 ，交集的维数： 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 是 9 维，对角子空间是 3 维。上三角和对称各 6 维，两个子空间合起来张满整个 9 维的 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>，所以交集的维数是 <span><span>6+6−9=36 + 6 - 9 = 3</span><span><span><span></span><span>6</span><span></span><span>+</span><span></span></span><span><span></span><span>6</span><span></span><span>−</span><span></span></span><span><span></span><span>9</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，恰好等于对角子空间的维数，和他说的“交集是对角矩阵”一致。这里用到了子空间维数公式 <span><span>dim⁡(U∩S)=dim⁡U+dim⁡S−dim⁡(U+S)\dim(U \cap S) = \dim U + \dim S - \dim(U + S)</span><span><span><span></span><span>dim</span><span>(</span><span>U</span><span></span><span>∩</span><span></span></span><span><span></span><span>S</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>dim</span><span></span><span>U</span><span></span><span>+</span><span></span></span><span><span></span><span>dim</span><span></span><span>S</span><span></span><span>−</span><span></span></span><span><span></span><span>dim</span><span>(</span><span>U</span><span></span><span>+</span><span></span></span><span><span></span><span>S</span><span>)</span></span></span></span>，我是从两个子空间的基并排后看秩得到它的（这个公式是我自己加的，Strang 这一讲没有写），在这个例子里它自洽。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>状态方程 <span><span>xk+1=Axk+Buk\mathbf{x}_{k+1} = A\mathbf{x}_k + B\mathbf{u}_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里“系统能不能被控制”，可以直接翻译成这一讲的语言。从零状态出发，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 步之内能到达的所有状态，是能控性矩阵 <span><span>C=[B    AB    A2B    ⋯    An−1B]\mathcal{C} = [B \;\; AB \;\; A^2B \;\; \cdots \;\; A^{n-1}B]</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>B</span><span></span><span></span><span>A</span><span>B</span><span></span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>B</span><span></span><span></span><span></span><span>⋯</span><span></span><span></span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>B</span><span>]</span></span></span></span> 的列的所有组合，也就是 <span><span>C\mathcal{C}</span><span><span><span></span><span>C</span></span></span></span> 的列空间。所以“能控”就是“列空间是整个 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>”，也就是 <span><span>C\mathcal{C}</span><span><span><span></span><span>C</span></span></span></span> 的秩等于 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>，这和第 1 讲“列的组合能不能填满空间”是同一个问题，只是列换成了 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span>、<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span>、<span><span>A2BA^2B</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>B</span></span></span></span>……。</p><p>秩不够的时候，<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 维的“够不着的方向”不在列空间里，而是由左零空间描述：存在 <span><span>y≠0\mathbf{y} \ne \mathbf{0}</span><span><span><span></span><span>y</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> 使得 <span><span>yTC=0T\mathbf{y}^\mathsf{T}\mathcal{C} = \mathbf{0}^\mathsf{T}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span>0</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，意思是状态沿 <span><span>y\mathbf{y}</span><span><span><span></span><span>y</span></span></span></span> 的分量 <span><span>yTx\mathbf{y}^\mathsf{T}\mathbf{x}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span></span></span></span>，无论输入怎么给都推不动。四个子空间里，列空间告诉你够得着哪里，左零空间告诉你够不着哪里，两者的维数加起来是 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>。</p><p>这一节只讲概念，没有跑代码。</p><p>四个子空间到这里讲完了，但它们之间还有一层关系没碰：行空间和零空间都在 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span> 里，维数加起来是 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>，它们到底是怎么摆放的？答案是互相垂直，这是第 14 讲的内容。在那之前，下一讲先把矩阵当成向量继续往下数维数，再看秩为 1 的矩阵长什么样，见 <a href="/posts/linear-algebra-11-matrix-spaces-rank-one/">18.06 第 11 讲：矩阵空间、秩 1 矩阵与小世界图</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-09-independence-basis-dimension/</id>
      <title type="text">18.06 第 9 讲：线性无关、基与维数</title>
      <published>2026-10-18T02:40:00.000Z</published>
      <updated>2026-10-18T02:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-09-independence-basis-dimension/"/>
      <summary type="text">MIT 18.06 第 9 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把线性无关、张成、基和维数都翻译成“秩”和“零空间”来检验，固定种子的随机 100 × 100 矩阵试了 1000 次全部满秩，而人为造的、只差 1e-12 的相关向量却被判成了秩 3。</summary>
      <content type="html"><![CDATA[<p>这一讲 Strang 给四个词下了定义：线性无关、张成、基、维数。我把四个词都翻译成了同一个操作：把向量放进矩阵的列，算秩，找零空间。固定种子取 1000 个随机的 100 × 100 矩阵，1000 个全是满秩，也就是“随机向量几乎总是无关”；而在另一头，我人为造了一组只差 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 的相关向量，<code>matrix_rank</code> 判它们是秩 3，也就是“无关”。这两个数字合起来，就是这一讲的数学定义和浮点数之间的距离。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=yjBerM5jWsc" target="_blank">Lecture 9: Independence, basis, and dimension</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-9-independence-basis-and-dimension/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §3.4 Independence, Basis, and Dimension
前置：<a href="/posts/linear-algebra-08-solving-ax-b/">第 8 讲：解 Ax = b，行最简形 R</a>
一句话：把向量放进矩阵的列，它们无关当且仅当零空间只有零向量；列空间的维数是秩 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>，零空间的维数是 <span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先回顾“未知数比方程多，零空间里一定有非零向量”，这一段是整讲的伏笔，值得认真听。</li>
<li>讲无关的那一段（<span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 和 <span><span>2v2\mathbf{v}</span><span><span><span></span><span>2</span><span>v</span></span></span></span>、含零向量、平面里的三个向量）很短，本篇第一节已经用代码算过，可以倍速。</li>
<li>他在黑板上反复追问“给我另一组基”，那一段看一遍就行，本篇第二、三节有对应的代码。</li>
<li>最后三分钟是零空间的维数，两个特解就是零空间的基，这是下一讲的预告，别漏。</li>
</ul></div></div>
<section><h2>线性无关：看零空间<a href="#线性无关看零空间"><span>#</span></a></h2><p>他先说了一条上一讲留下的事实：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的矩阵，如果 <span><span>n&gt;mn &gt; m</span><span><span><span></span><span>n</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>m</span></span></span></span>（未知数比方程多），那么 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 一定有非零解。理由是消元之后主元最多 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 个，至少剩下 <span><span>n−mn - m</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>m</span></span></span></span> 个自由变量，自由变量可以随便取 1，再回代解出主变量。</p><p><strong>线性无关是什么？</strong> 一堆向量 <span><span>x1,…,xn\mathbf{x}_1, \dots, \mathbf{x}_n</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，如果</p><span><span><span>c1x1+c2x2+⋯+cnxn=0c_1 \mathbf{x}_1 + c_2 \mathbf{x}_2 + \cdots + c_n \mathbf{x}_n = \mathbf{0}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>除了所有 <span><span>cic_i</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都是 0 之外别无解，它们就线性无关；若还有别的 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 能凑出零向量，它们就线性相关。他特地强调：说的是“一堆向量”无关，不说“一个矩阵”无关。</p><p>接着他举了三个例子：<span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 和 <span><span>2v2\mathbf{v}</span><span><span><span></span><span>2</span><span>v</span></span></span></span>（两份第一个减去第二个就是零，相关）、<span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span> 和零向量（零向量那一份随便取几都行，“只要里面有零向量，无关就死了”）、平面里的三个向量（必然相关）。第三个例子里他是这样看的：把三个向量放进 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的三列，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是 <span><span>2×32 \times 3</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span>，列比行多，零空间里一定有非零的 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span>，这个 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span> 就是“往 <span><span>v1\mathbf{v}_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 走几步，往 <span><span>v2\mathbf{v}_2</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 走几步，再往 <span><span>v3\mathbf{v}_3</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 退几步，回到原点”。</p><p>这一下就把定义翻译成了矩阵语言：</p><ul>
<li>列无关 <span><span>  ⟺  \iff</span><span><span><span></span><span></span><span>⟺</span><span></span></span></span></span> <span><span>N(A)={0}N(A) = \{\mathbf{0}\}</span><span><span><span></span><span>N</span><span>(</span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>{</span><span>0</span><span>}</span></span></span></span> <span><span>  ⟺  \iff</span><span><span><span></span><span></span><span>⟺</span><span></span></span></span></span> 没有自由变量 <span><span>  ⟺  \iff</span><span><span><span></span><span></span><span>⟺</span><span></span></span></span></span> 秩 <span><span>r=nr = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>。</li>
<li>列相关 <span><span>  ⟺  \iff</span><span><span><span></span><span></span><span>⟺</span><span></span></span></span></span> 零空间里有非零向量 <span><span>  ⟺  \iff</span><span><span><span></span><span></span><span>⟺</span><span></span></span></span></span> 有自由变量 <span><span>  ⟺  \iff</span><span><span><span></span><span></span><span>⟺</span><span></span></span></span></span> <span><span>r&lt;nr &lt; n</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span>。</li>
</ul><p>我按字幕里他随手画的数取 <span><span>v1=(2,1)\mathbf{v}_1 = (2, 1)</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>、<span><span>v2=(1,2)\mathbf{v}_2 = (1, 2)</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>v3=(2.5,−1)\mathbf{v}_3 = (2.5, -1)</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2.5</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>（字幕是语音转写，“two and a half, minus one”我就照字面取了，他自己也说没画准）。下面的脚本做三件事：判三个向量是否无关，找出那个凑出零的 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span>，再把三种“一眼就相关”的情形跑一遍。零空间向量我用 SVD 的最后一个右奇异向量得到，这个办法后面几讲会解释，现在只当它是“零空间里的一个单位向量”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>null_vector</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回 A 的零空间里的一个单位向量（A 的列相关时才有意义）。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>_, _, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> Vt[</span><span>-</span></span><span>1</span><span>]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 平面里的三个向量：把它们放进 A 的三列，A 是 2 x 3</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>v1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>v2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>v3 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.5</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>column_stack</span><span>([v1, v2, v3])</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的形状："</span><span>, A.shape, </span><span>"，秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"独立的条件是秩 = 列数 3："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A) </span><span>==</span><span> </span></span><span>3</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 列比行多，零空间里一定有非零向量 c</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> </span><span>null_vector</span><span>(A)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"零空间里的 c ="</span><span>, c)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"c1 v1 + c2 v2 + c3 v3 ="</span><span><span>, A </span><span>@</span><span> c)</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 只取前两个向量，秩 = 2 = 列数，独立</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"v1, v2 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A[:, :</span></span><span>2</span><span>]))</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 三种一眼就相关的情形：v 与 2v，含零向量</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>pairs </span><span>=</span><span> {</span></div></div><div><div><div>32</div></div><div><span>        </span><span>"v 与 2v"</span><span><span>: np.</span><span>column_stack</span><span>([v, </span></span><span>2</span><span><span> </span><span>*</span><span> v]),</span></span></div></div><div><div><div>33</div></div><div><span>        </span><span>"v 与零向量"</span><span><span>: np.</span><span>column_stack</span><span>([v, np.</span><span>zeros</span><span>(</span></span><span>2</span><span>)]),</span></div></div><div><div><div>34</div></div><div><span>        </span><span>"v1 与 v2"</span><span><span>: np.</span><span>column_stack</span><span>([v1, v2]),</span></span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> name, M </span><span>in</span><span><span> pairs.</span><span>items</span><span>():</span></span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(M)</span></div></div><div><div><div>38</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：秩 = </span><span>{</span><span>r</span><span>}</span><span>，列数 = </span><span>{</span><span>M.shape[</span><span>1</span><span>]</span><span>}</span><span>，独立：</span><span>{</span><span><span>r </span><span>==</span><span> </span></span><span>2}</span><span>"</span><span>)</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 的形状： (2, 3) ，秩 = 2</span></div></div><div><div><div>2</div></div><div><span>独立的条件是秩 = 列数 3： False</span></div></div><div><div><div>3</div></div><div><span>零空间里的 c = [-0.7428  0.5571  0.3714]</span></div></div><div><div><div>4</div></div><div><span>c1 v1 + c2 v2 + c3 v3 = [ 0. -0.]</span></div></div><div><div><div>5</div></div><div><span>v1, v2 的秩： 2</span></div></div><div><div><div>6</div></div><div><span>v 与 2v：秩 = 1，列数 = 2，独立：False</span></div></div><div><div><div>7</div></div><div><span>v 与零向量：秩 = 1，列数 = 2，独立：False</span></div></div><div><div><div>8</div></div><div><span>v1 与 v2：秩 = 2，列数 = 2，独立：True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>2×32 \times 3</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 的矩阵秩最多是 2，所以三列不可能无关，这是“列比行多”的直接后果。<code>c</code> 的三个分量满足 <span><span>−0.7428 v1+0.5571 v2+0.3714 v3=0-0.7428\,\mathbf{v}_1 + 0.5571\,\mathbf{v}_2 + 0.3714\,\mathbf{v}_3 = \mathbf{0}</span><span><span><span></span><span>−</span><span>0.7428</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>0.5571</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>0.3714</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，右边打印出来的 <code>[ 0. -0.]</code> 就是验证（<code>-0.</code> 是浮点数的负零，不是错误）。把 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span> 同时除以 0.3714，得到 <span><span>(−2,1.5,1)(-2, 1.5, 1)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>1.5</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，你可以手算验一遍：第一个分量 <span><span>−4+1.5+2.5=0-4 + 1.5 + 2.5 = 0</span><span><span><span></span><span>−</span><span>4</span><span></span><span>+</span><span></span></span><span><span></span><span>1.5</span><span></span><span>+</span><span></span></span><span><span></span><span>2.5</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，第二个 <span><span>−2+3−1=0-2 + 3 - 1 = 0</span><span><span><span></span><span>−</span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</p></section>
<section><h2>张成与基：不多不少<a href="#张成与基不多不少"><span>#</span></a></h2><p><strong>张成是什么？</strong> 一堆向量张成一个空间，意思是这个空间就是它们所有线性组合的集合。他说这个词其实早就用过了：矩阵的列张成列空间。他补了一句：这堆向量张成的空间是包含它们的最小空间，因为任何含有这些向量的空间都必须含有它们的所有组合。</p><p>单看张成不够，列可能有冗余；单看无关也不够，向量可能太少。<strong>基是什么？</strong> 同时满足两条：无关，并且张成这个空间。他的说法是“刚刚好”：少一个张不满，多一个就相关。这有点像点菜，少一道吃不饱，多一道吃不完，只有刚好的那一桌才既不饿也不浪费。往后每次问到子空间，他都会问“给我一组基”，因为基就是对这个子空间的完整描述。</p><p>他的第一个例子是 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>。最先想到的基是 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>、<span><span>yy</span><span><span><span></span><span>y</span></span></span></span>、<span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 轴方向的三个单位向量，也就是单位矩阵的三列，零空间只有零向量，所以无关。第二个例子，他从 <span><span>(1,1,2)(1, 1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 和 <span><span>(2,2,5)(2, 2, 5)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span> 开始：两个向量无关，但张不满 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>。要补第三个。补 <span><span>(3,3,7)(3, 3, 7)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span> 是个“失误”，因为它恰好是前两个之和，躺在同一个平面里。补 <span><span>(3,3,8)(3, 3, 8)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span> 呢？他说“希望这个行”，然后问：如果我挑的是别的数，比如 <span><span>(5,7,−14)(5, 7, -14)</span><span><span><span></span><span>(</span><span>5</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>−</span><span>14</span><span>)</span></span></span></span>，我怎么知道？答案是放进矩阵的列，做消元，看有没有自由变量；对方阵来说，等价于矩阵可逆。</p><p>我把这三个候选逐个交给 NumPy，结果和字幕里的猜测不太一样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># R^3 的第一组基：单位矩阵的三列</span></div></div><div><div><div>8</div></div><div><span>    </span><span>print</span><span>(</span><span>"单位矩阵的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span># 只有两个向量：独立，但张不满 R^3</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>B2 </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>5.0</span><span>]])</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"(1,1,2) 与 (2,2,5) 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B2))</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 第三个向量逐个试：秩 = 3 才是基（方阵时等价于 det 非零）</span></div></div><div><div><div>15</div></div><div><span>    </span><span>for</span><span> third </span><span>in</span><span> ([</span><span>3.0</span><span>, </span><span>3.0</span><span>, </span><span>7.0</span><span>], [</span><span>3.0</span><span>, </span><span>3.0</span><span>, </span><span>8.0</span><span>], [</span><span>5.0</span><span>, </span><span>7.0</span><span><span>, </span><span>-</span></span><span>14.0</span><span>]):</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>M </span><span>=</span><span> np.</span><span>column_stack</span><span>([B2, third])</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(M)</span></div></div><div><div><div>18</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"补 </span><span>{</span><span>third</span><span>}</span><span>：秩 = </span><span>{</span><span>r</span><span>}</span><span>，det = </span><span>{</span><span><span>np.linalg.</span><span>det</span><span>(M)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 前两个向量的第一、二个分量总相等，它们张成的平面是 x = y</span></div></div><div><div><div>21</div></div><div><span>    </span><span># (3, 3, 8) 也满足 x = y，所以它还在这个平面里</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"B2 的两列满足 x = y："</span><span><span>, np.</span><span>allclose</span><span>(B2[</span></span><span>0</span><span>], B2[</span><span>1</span><span>]))</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 补 (5, 7, -14) 得到一组基：任意向量都有唯一的坐标</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>good </span><span>=</span><span> np.</span><span>column_stack</span><span>([B2, [</span><span>5.0</span><span>, </span><span>7.0</span><span><span>, </span><span>-</span></span><span>14.0</span><span>]])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>coeff </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(good, x)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"x 在这组基下的坐标："</span><span>, coeff)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"坐标重新组合回 x："</span><span><span>, np.</span><span>allclose</span><span>(good </span><span>@</span><span> coeff, x))</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>单位矩阵的秩： 3</span></div></div><div><div><div>2</div></div><div><span>(1,1,2) 与 (2,2,5) 的秩： 2</span></div></div><div><div><div>3</div></div><div><span>补 [3.0, 3.0, 7.0]：秩 = 2，det = 0.0000</span></div></div><div><div><div>4</div></div><div><span>补 [3.0, 3.0, 8.0]：秩 = 2，det = 0.0000</span></div></div><div><div><div>5</div></div><div><span>补 [5.0, 7.0, -14.0]：秩 = 3，det = -2.0000</span></div></div><div><div><div>6</div></div><div><span>B2 的两列满足 x = y： True</span></div></div><div><div><div>7</div></div><div><span>x 在这组基下的坐标： [ 78.5 -35.   -1.5]</span></div></div><div><div><div>8</div></div><div><span>坐标重新组合回 x： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>(3,3,8)(3, 3, 8)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span> 也是相关的：前两个向量的第一、第二个分量都相等，它们张成的平面就是 <span><span>x=yx = y</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>y</span></span></span></span>，而 <span><span>(3,3,8)(3, 3, 8)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>8</span><span>)</span></span></span></span> 的前两个分量也相等，所以它同样躺在这个平面里。这是 Strang 自己挑错了数字：他说“希望 3 3 8 可以”，课后有同学指出来，他在 <a href="/posts/linear-algebra-10-four-fundamental-subspaces/">第 10 讲</a> 开头更正了。这个“挑数字挑错了”的插曲反而把他想说的事演示了：肉眼挑，十有八九会翻车，要用矩阵检验。（我这里再试一次运气，<span><span>(5,7,−14)(5, 7, -14)</span><span><span><span></span><span>(</span><span>5</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>−</span><span>14</span><span>)</span></span></span></span> 通过，行列式是 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span>。）</p><p>补上 <span><span>(5,7,−14)(5, 7, -14)</span><span><span><span></span><span>(</span><span>5</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>−</span><span>14</span><span>)</span></span></span></span> 之后，方阵 <code>good</code> 可逆，任意 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 都能唯一地写成三列的组合。随手取的 <span><span>x=(1,−2,3)\mathbf{x} = (1, -2, 3)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 在这组基下的坐标是 <span><span>(78.5,−35,−1.5)(78.5, -35, -1.5)</span><span><span><span></span><span>(</span><span>78.5</span><span>,</span><span></span><span>−</span><span>35</span><span>,</span><span></span><span>−</span><span>1.5</span><span>)</span></span></span></span>，系数不小，我猜是因为这三个向量彼此并不“垂直”，拼一个普通的向量得靠大系数互相抵消（我没有算条件数去证实，这只是猜测）。基有好坏之分，后面讲正交基的时候会回来。</p><p>还有一个他强调了两次的事实：基不唯一。任何可逆的 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 矩阵的列都是 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 的一组基，有无穷多组。但所有的基有一个共同点：向量个数相同。</p></section>
<section><h2>维数：基里有几个向量<a href="#维数基里有几个向量"><span>#</span></a></h2><p><strong>维数是什么？</strong> 对一个空间，所有基含有的向量个数相同，这个数就是它的维数。<span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 的每组基都有 3 个向量；某个子空间的一组基有 6 个向量，那它的所有基都是 6 个。他说这是“定义性”的：维数就是基的向量个数，它回答的是“这个空间有多大”。</p><p>这里有一个用词上的提醒，我觉得值得抄下来：</p><ul>
<li>矩阵有秩，没有“维数”。</li>
<li>子空间有维数，没有“秩”。</li>
<li>秩是列空间的维数，所以说“列空间的维数”，不说“<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的维数”。</li>
</ul><p>他说，用词对了，就说明概念对了。</p><p>他用的矩阵是一个 <span><span>3×43 \times 4</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的例子：四列依次是 <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>、<span><span>(2,1,2)(2, 1, 2)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(3,2,3)(3, 2, 3)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>、<span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。第三列是前两列之和，第四列重复第一列。问题是：四列张成列空间，但是否是基？不是，因为 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 份第一列、<span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 份第二列、<span><span>11</span><span><span><span></span><span>1</span></span></span></span> 份第三列、<span><span>00</span><span><span><span></span><span>0</span></span></span></span> 份第四列加起来是零向量，零空间里有非零向量。</p><p>他的回答是：自然的基是主列，也就是第一、第二列。从左往右扫，第一列可以放进基，第二列也可以，第三列不能（它是前两列的组合），第四列也不能。所以秩是 2，主列有 2 个，列空间的维数也是 2：秩、主列个数、列空间维数是同一个数。然后他让大家说“另一组基”：第一、三列，第二、三列，第二、四列都行，甚至不是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列的也行，比如 <span><span>(2,2,2)(2, 2, 2)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 和 <span><span>(7,5,7)(7, 5, 7)</span><span><span><span></span><span>(</span><span>7</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span>（后者是四列之和）。</p><p>最后是一个我觉得很实用的小定理：如果你已经知道空间的维数是 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span>，并且手里有 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 个无关的向量，它们自动就是基，张成是白送的。因为如果张不满，就还缺一个向量来补，那时候就会有 <span><span>d+1d + 1</span><span><span><span></span><span>d</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个无关向量，超过了维数。</p><p>这一节所有的断言，我用一个脚本一并验证。其中的“主列”和“特解”都是手写的（主列：从左到右，加入这一列使秩增加，它就是主列；特解：自由变量取 1，其余自由变量取 0，再解主变量），用 <code>np.linalg.matrix_rank</code> 对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>pivot_columns</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; list[</span><span>int</span><span>]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""从左到右：这一列让秩增加，就是主列。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>1</span><span>]):</span></div></div><div><div><div>8</div></div><div><span>        </span><span>if</span><span><span> np.linalg.</span><span>matrix_rank</span><span>(A[:, pivots </span><span>+</span><span> [j]]) </span><span>&gt;</span><span> </span></span><span>len</span><span>(pivots):</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>pivots.</span><span>append</span><span>(j)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> pivots</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>special_solutions</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>pivots</span></span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>14</div></div><div><span>    </span><span>"""每个自由列取 1、其余自由列取 0，再解出主变量。"""</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>1</span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> k, j </span><span>in</span><span> </span><span>enumerate</span><span>(free):</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>N[j, k] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>N[pivots, k], </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A[:, pivots], </span><span>-</span><span>A[:, j], </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>return</span><span> N</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># Strang 的矩阵：四列依次是 (1,1,1)、(2,1,2)、(3,2,3)、(1,1,1)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"A ="</span><span>)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(A)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"m = </span><span>{</span><span>m</span><span>}</span><span>, n = </span><span>{</span><span>n</span><span>}</span><span>, 秩 r = </span><span>{</span><span>r</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span># 列空间：主列就是一组基，维数 = 秩</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>pivots </span><span>=</span><span> </span><span>pivot_columns</span><span>(A)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>"主列（从 0 数起）："</span><span>, pivots, </span><span>"，列空间的维数 ="</span><span>, </span><span>len</span><span>(pivots))</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span># 零空间：特解就是一组基，维数 = n - r</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> </span><span>special_solutions</span><span>(A, pivots)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"特解矩阵 N（每列一个特解）："</span><span>)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(N)</span></div></div><div><div><div>49</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ N = 0："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> N, </span></span><span>0</span><span>))</span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>51</div></div><div><span>        </span><span>"零空间的维数 n - r ="</span><span><span>, n </span><span>-</span><span> r, </span></span><span>"，N 的秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(N)</span></span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>    </span><span># 他说的第一个组合：-1 份第一列，-1 份第二列，1 份第三列，0 份第四列</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>-</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>56</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ (-1, -1, 1, 0) ="</span><span><span>, A </span><span>@</span><span> c)</span></span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>    </span><span># 另一组基：不是 A 的列，但在列空间里。7 5 7 是四列之和</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>column_stack</span><span>([[</span><span>2</span><span>, </span><span>2</span><span>, </span><span>2</span><span>], [</span><span>7</span><span>, </span><span>5</span><span>, </span><span>7</span><span><span>]]).</span><span>astype</span><span>(</span></span><span>float</span><span>)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>print</span><span>(</span><span>"B 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(B))</span></span></div></div><div><div><div>61</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>62</div></div><div><span>        </span><span>"B 的列都在列空间里："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>hstack</span><span>([A, B])) </span><span>==</span><span> r</span></span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>64</div></div><div><span>    </span><span>print</span><span>(</span><span>"四列之和 ="</span><span><span>, A </span><span>@</span><span> np.</span><span>ones</span><span>(</span></span><span>4</span><span>))</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A =</span></div></div><div><div><div>2</div></div><div><span>[[1. 2. 3. 1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[1. 1. 2. 1.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[1. 2. 3. 1.]]</span></div></div><div><div><div>5</div></div><div><span>m = 3, n = 4, 秩 r = 2</span></div></div><div><div><div>6</div></div><div><span>主列（从 0 数起）： [0, 1] ，列空间的维数 = 2</span></div></div><div><div><div>7</div></div><div><span>特解矩阵 N（每列一个特解）：</span></div></div><div><div><div>8</div></div><div><span>[[-1. -1.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[-1.  0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 1.  0.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>12</div></div><div><span>A @ N = 0： True</span></div></div><div><div><div>13</div></div><div><span>零空间的维数 n - r = 2 ，N 的秩 = 2</span></div></div><div><div><div>14</div></div><div><span>A @ (-1, -1, 1, 0) = [0. 0. 0.]</span></div></div><div><div><div>15</div></div><div><span>B 的秩： 2</span></div></div><div><div><div>16</div></div><div><span>B 的列都在列空间里： True</span></div></div><div><div><div>17</div></div><div><span>四列之和 = [7. 5. 7.]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>零空间的维数是 n - r<a href="#零空间的维数是-n---r"><span>#</span></a></h2><p>最后几分钟他转向零空间。刚才那个非零向量 <span><span>(−1,−1,1,0)(-1, -1, 1, 0)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 只是零空间里的一个向量，零空间里还有别的。自然的选择是：第四列是自由列，令这个自由变量为 1、另一个自由变量（第三列）为 0，问第四列是不是主列的组合。是的，它就是第一列，所以 <span><span>(−1,0,0,1)(-1, 0, 0, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 在零空间里。这两个向量正是上一讲的特解：每个自由变量轮流取 1，其他自由变量取 0。</p><p>它们无关，而且张成整个零空间，所以是零空间的一组基，零空间的维数是 2。更一般地：<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 列里有 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个主列，剩下 <span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 个自由列，每个自由列给一个特解，因此</p><span><span><span>dim⁡C(A)=r,dim⁡N(A)=n−r.\dim C(A) = r, \qquad \dim N(A) = n - r.</span><span><span><span></span><span>dim</span><span></span><span>C</span><span>(</span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span>,</span><span></span><span></span><span>dim</span><span></span><span>N</span><span>(</span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span>.</span></span></span></span></span><p>上面代码块的输出正对着这两个公式：<span><span>m=3m = 3</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>、<span><span>n=4n = 4</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>、<span><span>r=2r = 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，主列是第 0、1 列（从 0 数起），特解矩阵 <code>N</code> 的两列正是 <span><span>(−1,−1,1,0)(-1, -1, 1, 0)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和 <span><span>(−1,0,0,1)(-1, 0, 0, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<code>A @ N</code> 全零，<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的秩是 2。零空间里的向量就是“这些列相关的方式”：每一个特解告诉你，哪几列怎么组合能得零。</p></section>
<section><h2>浮点数里的“几乎无关”<a href="#浮点数里的几乎无关"><span>#</span></a></h2><p>定义是干净的：要么无关，要么相关。但 <code>np.linalg.matrix_rank</code> 判的是数值秩，它拿奇异值和一个阈值比（默认阈值约为 <span><span>σmax⁡⋅max⁡(m,n)⋅ε\sigma_{\max} \cdot \max(m, n) \cdot \varepsilon</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>a</span><span>x</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>max</span><span>(</span><span>m</span><span>,</span><span></span><span>n</span><span>)</span><span></span><span>⋅</span><span></span></span><span><span></span><span>ε</span></span></span></span>，<span><span>ε≈2.2×10−16\varepsilon \approx 2.2 \times 10^{-16}</span><span><span><span></span><span>ε</span><span></span><span>≈</span><span></span></span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>），低于阈值的奇异值当成 0。所以“相关”在浮点数里是有一个模糊带的。下面的脚本做两个实验。第一个：固定种子取随机向量，看它们是不是几乎总是无关（他在第 1 讲说过“MATLAB 的随机矩阵几乎一定可逆”）；并且验证 <span><span>n+1n + 1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 维向量必相关。第二个：第三列取前两列之和，再加上噪声，看噪声多小时 <code>matrix_rank</code> 会被“骗过去”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 随机取 n 个 n 维向量：几乎总是独立（秩 = n）</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> (</span><span>3</span><span>, </span><span>10</span><span>, </span><span>100</span><span>):</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>trials </span><span>=</span><span> </span><span>1000</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>full </span><span>=</span><span> </span><span>sum</span><span>(</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(rng.</span><span>standard_normal</span><span>((n, n))) </span><span>==</span><span> n</span></div></div><div><div><div>13</div></div><div><span>            </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(trials)</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>15</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"n = </span><span>{</span><span>n</span><span>:3d</span><span>}</span><span>：</span><span>{</span><span>trials</span><span>}</span><span> 次里有 </span><span>{</span><span>full</span><span>}</span><span> 次秩为 n"</span><span>)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 取 n + 1 个 n 维向量：必相关，零空间至少一维</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>5</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((n, n </span><span>+</span><span> </span><span>1</span><span>))</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"5 维里取 6 个向量，秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(M))</span></span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>_, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(M)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> Vt[</span><span>-</span><span>1</span><span>]</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"‖M c‖ ="</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(M </span><span>@</span><span> c)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 浮点数里的“相关”：第三列 = 前两列之和，再加一点点噪声</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>base </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>4</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>third </span><span>=</span><span> base[:, </span><span>0</span><span><span>] </span><span>+</span><span> base[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>28</div></div><div><span>    </span><span>for</span><span> eps </span><span>in</span><span> (</span><span>0.0</span><span>, </span><span>1e-16</span><span>, </span><span>1e-12</span><span>, </span><span>1e-8</span><span>, </span><span>1e-4</span><span>):</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>noise </span><span>=</span><span> eps </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>M </span><span>=</span><span> np.</span><span>column_stack</span><span>([base, third </span><span>+</span><span> noise])</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(M, </span><span>compute_uv</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>32</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>33</div></div><div><span>            </span><span>f</span><span>"噪声 </span><span>{</span><span>eps</span><span>:7.0e</span><span>}</span><span>：最小奇异值 = </span><span>{</span><span><span>s[</span><span>-</span></span><span>1</span><span>]</span><span>:.2e</span><span>}</span><span>，"</span></div></div><div><div><div>34</div></div><div><span>            </span><span>f</span><span>"秩 = </span><span>{</span><span><span>np.linalg.</span><span>matrix_rank</span><span>(M)</span></span><span>}</span><span>"</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>n =   3：1000 次里有 1000 次秩为 n</span></div></div><div><div><div>2</div></div><div><span>n =  10：1000 次里有 1000 次秩为 n</span></div></div><div><div><div>3</div></div><div><span>n = 100：1000 次里有 1000 次秩为 n</span></div></div><div><div><div>4</div></div><div><span>5 维里取 6 个向量，秩 = 5</span></div></div><div><div><div>5</div></div><div><span>‖M c‖ = 4.79e-16</span></div></div><div><div><div>6</div></div><div><span>噪声   0e+00：最小奇异值 = 9.16e-17，秩 = 2</span></div></div><div><div><div>7</div></div><div><span>噪声   1e-16：最小奇异值 = 2.18e-16，秩 = 2</span></div></div><div><div><div>8</div></div><div><span>噪声   1e-12：最小奇异值 = 9.42e-13，秩 = 3</span></div></div><div><div><div>9</div></div><div><span>噪声   1e-08：最小奇异值 = 9.92e-09，秩 = 3</span></div></div><div><div><div>10</div></div><div><span>噪声   1e-04：最小奇异值 = 5.54e-05，秩 = 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前三行：随机取向量，在 <span><span>n=3,10,100n = 3, 10, 100</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>100</span></span></span></span> 三种规模下各试 1000 次，每一次都是满秩，随机向量“几乎总是无关”不是夸张。第四、五行：5 维空间里的 6 个向量，秩最多 5，所以必相关，找到的 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span> 让 <span><span>∥Mc∥=4.79×10−16\|M\mathbf{c}\| = 4.79 \times 10^{-16}</span><span><span><span></span><span>∥</span><span>M</span><span>c</span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>4.79</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，也就是舍入误差的水平。</p><p>最后五行是我觉得最有意思的。第三列是前两列的精确之和时（噪声 0），最小奇异值是 <span><span>9.16×10−179.16 \times 10^{-17}</span><span><span><span></span><span>9.16</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>，判相关，对的。噪声只有 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 时最小奇异值是 <span><span>2.18×10−162.18 \times 10^{-16}</span><span><span><span></span><span>2.18</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，还在阈值之下，判相关。可是噪声涨到 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span>，最小奇异值 <span><span>9.42×10−139.42 \times 10^{-13}</span><span><span><span></span><span>9.42</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> 已经高于阈值，<code>matrix_rank</code> 就判秩 3，也就是“无关”了。严格地说，这三列在数学上是无关的（噪声不为零），所以 NumPy 没有错。但如果这个噪声只是测量误差，那么它判的“无关”就没有意义。</p><p>我的结论是：数学里的无关是一个二选一，数值里的“秩”是一个带阈值的判断，阈值怎么取要看这个噪声是不是你关心的东西。后面讲 SVD 时，我会换一个角度处理这件事：奇异值是多少，比“秩是几”携带的信息多得多。（所以我在日常里更愿意看最小奇异值，而不是只看 <code>matrix_rank</code> 的那个整数。）</p><p>这一讲算是把前八讲反复出现的词都落了地：消元找主元，主元是几个，秩就是几；自由变量是几个，零空间就是几维。下一讲要把列空间、零空间再配上它们的两个“转置版本”，凑齐四个基本子空间，并把它们的维数 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span>、<span><span>m−rm - r</span><span><span><span></span><span>m</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 一次说完，见 <a href="/posts/linear-algebra-10-four-fundamental-subspaces/">18.06 第 10 讲：四个基本子空间</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-08-solving-ax-b/</id>
      <title type="text">18.06 第 8 讲：解 Ax = b，行最简形 R</title>
      <published>2026-10-18T02:20:00.000Z</published>
      <updated>2026-10-18T02:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-08-solving-ax-b/"/>
      <summary type="text">MIT 18.06 第 8 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 手写 rref，解出 3 × 4 方程组的完全解 x_p + x_n，再用 1000 个随机右端项检验四种秩的情形，秩小于行数的两种情形有解的比例都是 0.000。</summary>
      <content type="html"><![CDATA[<p>Strang 这一讲把 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 彻底解完：先看有没有解，有的话再找一个特解，然后把零空间里的所有向量加上去。我用他上一讲的 3 × 4 矩阵验证，这样得到的所有 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 都满足方程；而 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘以特解的 2 倍就不行，得到 <span><span>(2,10,12)(2, 10, 12)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>12</span><span>)</span></span></span></span>，不是 <span><span>b=(1,5,6)\mathbf{b} = (1, 5, 6)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>。接着我用 1000 个随机右端项数了一遍四种秩的情形：秩小于行数时，随机的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 有解的比例是 0.000，秩等于行数时是 1.000。他在最后一句话里说，秩告诉你解的个数的一切。我的实验只量了其中“有没有解”这一半：数出来确实如此。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=9Q1q7s1jTzU" target="_blank">Lecture 8: Solving Ax = b: row reduced form R</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-8-solving-ax-b-row-reduced-form-r/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §3.3 The Complete Solution to Ax = b
前置：<a href="/posts/linear-algebra-07-solving-ax-0/">第 7 讲：解 Ax = 0，主变量与特解</a>
一句话：<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 的完全解是一个特解 <span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 加上零空间里的任意向量 <span><span>xn\mathbf{x}_n</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，有没有解、有几个解，只看秩 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 与行数 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、列数 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 的关系。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他用上一讲的矩阵讲“消元怎么发现无解”，我在第一节用代码重做了一遍，这一段可以倍速。</li>
<li>“解集不是子空间，它是一个被平移过的东西”那一段他在黑板上画四维空间，自己都说画得不行。听他说的话比看图重要。</li>
<li>后半段的 <span><span>r=mr = m</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span></span></span></span>、<span><span>r=nr = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>、<span><span>r=m=nr = m = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span> 四个格子是本讲的主角，他边问边答，节奏很快，建议暂停下来自己先填一遍。</li>
<li>他在中途写错过一次 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的形状又自己改了过来，这个小插曲值得看：它说明“<span><span>II</span><span><span><span></span><span>I</span></span></span></span> 在左上角”这件事只在主列恰好排在前面时才成立。</li>
</ul></div></div>
<section><h2>先问：有没有解<a href="#先问有没有解"><span>#</span></a></h2><p>他先不急着解，而是拿上一讲的矩阵</p><span><span><span>A=[1222246836810]A = \begin{bmatrix}
1 &amp; 2 &amp; 2 &amp; 2 \\
2 &amp; 4 &amp; 6 &amp; 8 \\
3 &amp; 6 &amp; 8 &amp; 10
\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>问了一个问题：<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 对哪些 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 有解？第三行等于前两行之和，所以三个方程左边加起来有一个现成的关系：第一行加第二行减第三行是零。如果左边的一个组合给出全零，右边的同一个组合也必须给出零，所以要有 <span><span>b1+b2=b3b_1 + b_2 = b_3</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他举的数是 <span><span>(1,5,17)(1, 5, 17)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>17</span><span>)</span></span></span></span>，不行；<span><span>(1,5,6)(1, 5, 6)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span> 可以。</p><p><strong>消元怎么发现这件事？</strong> 把 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 当作多出来的一列，贴在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 右边，这就是增广矩阵。对增广矩阵做行运算，也就是对方程两边同时做同样的事。消元到最后一行，左边全零，右边剩下 <span><span>b3−b2−b1b_3 - b_2 - b_1</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，方程变成 <span><span>0=b3−b2−b10 = b_3 - b_2 - b_1</span><span><span><span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。这就是可解条件。</p><p>我没有用符号计算，而是在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 右边并排放了一个 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 的单位矩阵，让右边三列记录消元对 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 的三个分量做了什么：做完消元，右边那块就是 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span>，满足 <span><span>EA=REA = R</span><span><span><span></span><span>E</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>R</span></span></span></span>。<span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 的最后一行，就是“把行组合成零行”的那组系数。</p><p>下面的脚本里有一个手写的 <code>rref</code>，带容差，和上一篇的思路一样（每个代码块都要能单独运行，所以会重复贴一遍。贴三遍有点蠢，但我宁愿蠢一点，也不想让你去翻别的文件）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rref</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""化成行最简形，返回 R 和主列下标。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> M.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>rows, cols </span><span>=</span><span> R.shape</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> c </span><span>in</span><span> </span><span>range</span><span>(cols):</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> r </span><span>==</span><span> rows:</span></span></div></div><div><div><div>12</div></div><div><span>            </span><span>break</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> r </span><span>+</span><span> </span><span>int</span><span><span>(np.</span><span>argmax</span><span>(np.</span><span>abs</span><span>(R[r:, c])))</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span> </span><span>abs</span><span><span>(R[p, c]) </span><span>&lt;</span><span> tol:</span></span></div></div><div><div><div>15</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>R[[r, p]] </span><span>=</span><span> R[[p, r]]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>R[r] </span><span>/=</span><span> R[r, c]</span></div></div><div><div><div>18</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(rows):</span></div></div><div><div><div>19</div></div><div><span>            </span><span>if</span><span><span> i </span><span>!=</span><span> r:</span></span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>R[i] </span><span>-=</span><span> R[i, c] </span><span>*</span><span> R[r]</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(c)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>r </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>round</span><span>(R, </span></span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>2</span><span>, </span><span>2</span><span>], [</span><span>2</span><span>, </span><span>4</span><span>, </span><span>6</span><span>, </span><span>8</span><span>], [</span><span>3</span><span>, </span><span>6</span><span>, </span><span>8</span><span>, </span><span>10</span><span>]], </span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 在 A 右边并排放一个单位矩阵：消元对 b 做的事，都记在这三列里</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>R, pivots </span><span>=</span><span> </span><span>rref</span><span>(np.</span><span>hstack</span><span>([A, np.</span><span>eye</span><span>(</span><span>3</span><span>)]))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>R_A</span><span>, E </span><span>=</span><span> R[:, :</span><span>4</span><span>], R[:, </span><span>4</span><span>:]</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"R ="</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span><span>(</span><span>R_A</span><span>)</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的主列："</span><span>, [c </span><span>for</span><span> c </span><span>in</span><span> pivots </span><span>if</span><span><span> c </span><span>&lt;</span><span> </span></span><span>4</span><span>])</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"E @ A 等于 R："</span><span><span>, np.</span><span>allclose</span><span>(E </span><span>@</span><span> A, </span><span>R_A</span><span>))</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span># R 的零行对应的那一行 E，就是“行的组合给出零行”的系数</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> E[</span><span>2</span><span>]</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"y ="</span><span>, y, </span><span>"，y @ A ="</span><span><span>, np.</span><span>round</span><span>(y </span><span>@</span><span> A, </span></span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span>for</span><span> b </span><span>in</span><span> ([</span><span>1.0</span><span>, </span><span>5.0</span><span>, </span><span>6.0</span><span>], [</span><span>1.0</span><span>, </span><span>5.0</span><span>, </span><span>17.0</span><span>]):</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>(b)</span></div></div><div><div><div>44</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"b = </span><span>{</span><span>b</span><span>}</span><span>，y @ b = </span><span>{</span><span><span>y </span><span>@</span><span> b</span></span><span>:.1f</span><span>}</span><span>，可解：</span><span>{</span><span><span>np.</span><span>isclose</span><span>(y </span><span>@</span><span> b, </span></span><span>0</span><span>)</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>R =</span></div></div><div><div><div>2</div></div><div><span>[[ 1.  2.  0. -2.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.  0.  1.  2.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  0.  0.  0.]]</span></div></div><div><div><div>5</div></div><div><span>A 的主列： [0, 2]</span></div></div><div><div><div>6</div></div><div><span>E @ A 等于 R： True</span></div></div><div><div><div>7</div></div><div><span>y = [ 1.  1. -1.] ，y @ A = [0. 0. 0. 0.]</span></div></div><div><div><div>8</div></div><div><span>b = [1. 5. 6.]，y @ b = 0.0，可解：True</span></div></div><div><div><div>9</div></div><div><span>b = [ 1.  5. 17.]，y @ b = -11.0，可解：False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的第三行是零行，主列是第 1 列和第 3 列，所以秩是 2。<span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 的最后一行 <span><span>y=(1,1,−1)\mathbf{y} = (1, 1, -1)</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 满足 <span><span>y⊤A=0\mathbf{y}^\top A = \mathbf{0}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>⊤</span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，也就是他说的“某个行的组合给出零行”。同一个组合作用在 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 上，<span><span>y⊤b=b1+b2−b3\mathbf{y}^\top \mathbf{b} = b_1 + b_2 - b_3</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>⊤</span></span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：对 <span><span>(1,5,6)(1, 5, 6)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span> 是 0，有解；对 <span><span>(1,5,17)(1, 5, 17)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>17</span><span>)</span></span></span></span> 是 <span><span>−11-11</span><span><span><span></span><span>−</span><span>11</span></span></span></span>，无解。和他在黑板上说的一致。</p><p>他把可解条件用了两种语言说出来。一种是列空间的语言：<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 恰好在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间里时有解。另一种是行的语言：若行的某个组合给出零行，则 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 的同一个组合必须是零。他说这两句话等价并不显然，但它们都等价于“方程组有解”，所以必须等价。（这个论证偷了点懒：不证明，只指出两边都等于同一个东西。我猜后面讲四个基本子空间的时候会回到这里。）</p></section>
<section><h2>特解加零空间<a href="#特解加零空间"><span>#</span></a></h2><p>假设 <span><span>b=(1,5,6)\mathbf{b} = (1, 5, 6)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>，有解。怎么找？他的算法只有两步，第一步找一个特解 <span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><ol>
<li>把所有自由变量设为 0。</li>
<li>解主变量。</li>
</ol><p>对这个例子，自由列是第 2 列和第 4 列，所以 <span><span>x2=x4=0x_2 = x_4 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，方程只剩下主列：<span><span>x1+2x3=1x_1 + 2x_3 = 1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 与 <span><span>2x3=32x_3 = 3</span><span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>。回代得 <span><span>x3=32x_3 = \frac32</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，<span><span>x1=1−2⋅32=−2x_1 = 1 - 2 \cdot \frac32 = -2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span>，所以 <span><span>xp=(−2,0,32,0)\mathbf{x}_p = (-2, 0, \frac32, 0)</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。他特意说：做测验时请把它代回原方程验一下，这是好习惯。</p><p>第二步，加上零空间。上一讲的两个特解是 <span><span>(−2,1,0,0)(-2, 1, 0, 0)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和 <span><span>(2,0,−2,1)(2, 0, -2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。他在黑板上一开始没记清，凭方程当场推了一遍，最后确认：第二个特解里的 <span><span>22</span><span><span><span></span><span>2</span></span></span></span>、<span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span>、<span><span>11</span><span><span><span></span><span>1</span></span></span></span> 对得上。所以完全解是</p><span><span><span>x=xp+xn=[−20320]+c1[−2100]+c2[20−21]\mathbf{x} = \mathbf{x}_p + \mathbf{x}_n =
\begin{bmatrix} -2 \\ 0 \\ \frac32 \\ 0 \end{bmatrix} +
c_1 \begin{bmatrix} -2 \\ 1 \\ 0 \\ 0 \end{bmatrix} +
c_2 \begin{bmatrix} 2 \\ 0 \\ -2 \\ 1 \end{bmatrix}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>理由一句话就够：<span><span>Axp=bA\mathbf{x}_p = \mathbf{b}</span><span><span><span></span><span>A</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>，<span><span>Axn=0A\mathbf{x}_n = \mathbf{0}</span><span><span><span></span><span>A</span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，两个加起来 <span><span>A(xp+xn)=b+0=bA(\mathbf{x}_p + \mathbf{x}_n) = \mathbf{b} + \mathbf{0} = \mathbf{b}</span><span><span><span></span><span>A</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>。（零空间对 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 来说像调音台上被静音的旋钮：怎么拧，输出都不变。）</p><p><strong><span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 前面有没有系数 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span>？</strong> 没有。<span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是一个具体的解，我不能把它乘 3，因为 <span><span>A(3xp)=3b≠bA(3\mathbf{x}_p) = 3\mathbf{b} \ne \mathbf{b}</span><span><span><span></span><span>A</span><span>(</span><span>3</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span>b</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>b</span></span></span></span>。零空间里的向量可以随便乘、随便加，因为它们总是给出 <span><span>0\mathbf{0}</span><span><span><span></span><span>0</span></span></span></span>。</p><p>用代码把这几句话都过一遍。我在 <span><span>[A b][A\ \mathbf{b}]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>b</span><span>]</span></span></span></span> 上做消元，最后一列 <span><span>d\mathbf{d}</span><span><span><span></span><span>d</span></span></span></span> 就是消元后的右端项，<span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的主变量直接从 <span><span>d\mathbf{d}</span><span><span><span></span><span>d</span></span></span></span> 里读；特解矩阵 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的每一列由自由列 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span> 取负填进去。然后随机抽几组 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 看 <span><span>xp+Nc\mathbf{x}_p + N\mathbf{c}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>N</span><span>c</span></span></span></span> 是不是解，最后和 <code>np.linalg.lstsq</code> 对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rref</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""化成行最简形，返回 R 和主列下标。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> M.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>rows, cols </span><span>=</span><span> R.shape</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> c </span><span>in</span><span> </span><span>range</span><span>(cols):</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> r </span><span>==</span><span> rows:</span></span></div></div><div><div><div>12</div></div><div><span>            </span><span>break</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> r </span><span>+</span><span> </span><span>int</span><span><span>(np.</span><span>argmax</span><span>(np.</span><span>abs</span><span>(R[r:, c])))</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span> </span><span>abs</span><span><span>(R[p, c]) </span><span>&lt;</span><span> tol:</span></span></div></div><div><div><div>15</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>R[[r, p]] </span><span>=</span><span> R[[p, r]]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>R[r] </span><span>/=</span><span> R[r, c]</span></div></div><div><div><div>18</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(rows):</span></div></div><div><div><div>19</div></div><div><span>            </span><span>if</span><span><span> i </span><span>!=</span><span> r:</span></span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>R[i] </span><span>-=</span><span> R[i, c] </span><span>*</span><span> R[r]</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(c)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>r </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>round</span><span>(R, </span></span><span>10</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>2</span><span>, </span><span>2</span><span>], [</span><span>2</span><span>, </span><span>4</span><span>, </span><span>6</span><span>, </span><span>8</span><span>], [</span><span>3</span><span>, </span><span>6</span><span>, </span><span>8</span><span>, </span><span>10</span><span>]], </span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>5.0</span><span>, </span><span>6.0</span><span>])</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 对 [A b] 做消元，最后一列就是消元后的右端项 d</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>Rb, pivots </span><span>=</span><span> </span><span>rref</span><span>(np.</span><span>hstack</span><span>([A, b[:, </span><span>None</span><span>]]))</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>pivots </span><span>=</span><span> [c </span><span>for</span><span> c </span><span>in</span><span> pivots </span><span>if</span><span><span> c </span><span>&lt;</span><span> n]</span></span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [c </span><span>for</span><span> c </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> c </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> Rb[:, n]</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"[R d] ="</span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(Rb)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"主列："</span><span>, pivots, </span><span>"，自由列："</span><span>, free)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span># 特解：自由变量取 0，主变量直接读 d</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>x_p </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>x_p[pivots] </span><span>=</span><span> d[: </span><span>len</span><span>(pivots)]</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"x_p ="</span><span>, x_p, </span><span>"，A @ x_p = b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x_p, b))</span></span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span># 零空间：每个自由变量取 1，其余自由变量取 0，主变量取 -F 的对应列</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>49</div></div><div><span>    </span><span>for</span><span> k, c </span><span>in</span><span> </span><span>enumerate</span><span>(free):</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>N[c, k] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>N[pivots, k] </span><span>=</span><span> </span><span>-</span><span>Rb[: </span><span>len</span><span>(pivots), c]</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> N </span><span>+</span><span> </span><span>0.0</span></div></div><div><div><div>53</div></div><div><span>    </span><span>print</span><span>(</span><span>"N ="</span><span>)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span>(N)</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ N = 0："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> N, </span></span><span>0</span><span>))</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>    </span><span># 完全解：x_p 加上任意的零空间向量</span></div></div><div><div><div>58</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>):</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>len</span><span>(free))</span></div></div><div><div><div>60</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> x_p </span><span>+</span><span> N </span><span>@</span><span> c</span></div></div><div><div><div>61</div></div><div><span>        </span><span>print</span><span>(</span><span>"c ="</span><span>, c, </span><span>"，A @ x = b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, b))</span></span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>    </span><span># 解集不是子空间：2 * x_p 不再是解</span></div></div><div><div><div>64</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ (2 * x_p) ="</span><span><span>, A </span><span>@</span><span> (</span></span><span>2</span><span><span> </span><span>*</span><span> x_p), </span></span><span>"，不等于 b"</span><span>)</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div><span>    </span><span># 对照：lstsq 给出所有解里范数最小的那一个</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>x_min </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)[</span><span>0</span><span>]</span></div></div><div><div><div>68</div></div><div><span>    </span><span>print</span><span>(</span><span>"lstsq 解 ="</span><span>, x_min, </span><span>"，A @ x = b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x_min, b))</span></span></div></div><div><div><div>69</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"|x_p| = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(x_p)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>70</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"|x_min| = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(x_min)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[R d] =</span></div></div><div><div><div>2</div></div><div><span>[[ 1.   2.   0.  -2.  -2. ]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.   0.   1.   2.   1.5]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.   0.   0.   0.   0. ]]</span></div></div><div><div><div>5</div></div><div><span>主列： [0, 2] ，自由列： [1, 3]</span></div></div><div><div><div>6</div></div><div><span>x_p = [-2.   0.   1.5  0. ] ，A @ x_p = b： True</span></div></div><div><div><div>7</div></div><div><span>N =</span></div></div><div><div><div>8</div></div><div><span>[[-2.  2.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[ 1.  0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 0. -2.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>12</div></div><div><span>A @ N = 0： True</span></div></div><div><div><div>13</div></div><div><span>c = [ 0.6679 -0.2323] ，A @ x = b： True</span></div></div><div><div><div>14</div></div><div><span>c = [-0.284  -0.5004] ，A @ x = b： True</span></div></div><div><div><div>15</div></div><div><span>c = [-0.0221 -1.0333] ，A @ x = b： True</span></div></div><div><div><div>16</div></div><div><span>A @ (2 * x_p) = [ 2. 10. 12.] ，不等于 b</span></div></div><div><div><div>17</div></div><div><span>lstsq 解 = [-0.1379 -0.2759  0.1897  0.6552] ，A @ x = b： True</span></div></div><div><div><div>18</div></div><div><span>|x_p| = 2.5000</span></div></div><div><div><div>19</div></div><div><span>|x_min| = 0.7486</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>[R d][R\ \mathbf{d}]</span><span><span><span></span><span>[</span><span>R</span><span> </span><span>d</span><span>]</span></span></span></span> 的第三行是 <span><span>0=00 = 0</span><span><span><span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以方程没问题；<span><span>d=(−2,1.5,0)\mathbf{d} = (-2, 1.5, 0)</span><span><span><span></span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>1.5</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 的前两个数就是主变量 <span><span>x1x_1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>x3x_3</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的值，<span><span>xp=(−2,0,1.5,0)\mathbf{x}_p = (-2, 0, 1.5, 0)</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1.5</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和黑板上一样。<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的两列是 <span><span>(−2,1,0,0)(-2, 1, 0, 0)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和 <span><span>(2,0,−2,1)(2, 0, -2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，和他当场推的那两个一致，<span><span>AN=0AN = 0</span><span><span><span></span><span>A</span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 通过。三组随机的 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 全部给出解，而 <span><span>2xp2\mathbf{x}_p</span><span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 给出的是 <span><span>(2,10,12)(2, 10, 12)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>10</span><span>,</span><span></span><span>12</span><span>)</span></span></span></span>，不是 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>。</p><p><code>lstsq</code> 也给了一个解，但它是另一个：<span><span>(−0.1379,−0.2759,0.1897,0.6552)(-0.1379, -0.2759, 0.1897, 0.6552)</span><span><span><span></span><span>(</span><span>−</span><span>0.1379</span><span>,</span><span></span><span>−</span><span>0.2759</span><span>,</span><span></span><span>0.1897</span><span>,</span><span></span><span>0.6552</span><span>)</span></span></span></span>。它是所有解里长度最小的那个，<span><span>∥xmin⁡∥=0.7486\|\mathbf{x}_{\min}\| = 0.7486</span><span><span><span></span><span>∥</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>i</span><span>n</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7486</span></span></span></span>，比我手算的 <span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 <span><span>∥xp∥=2.5000\|\mathbf{x}_p\| = 2.5000</span><span><span><span></span><span>∥</span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span>2.5000</span></span></span></span> 短了约 3.3 倍。两者都在同一个“解的平面”上，只相差一个零空间里的向量。选自由变量为 0 只是“最方便”，不是“最短”；这个差别要到第 33 讲讲伪逆的时候才会派上用场。</p><p><strong>解集长什么样？</strong> 他打算画出所有解的集合。四个未知数就是四维空间，黑板只有两维，他先说了一句 Einstein 能画，然后画了四条垂直的轴。结论比图好懂：零空间是 <span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 里的一个二维子空间（两个自由参数），完全解集是把它整体平移，平移到穿过 <span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。它很像子空间，但不过原点（<span><span>x=0\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 不是解，因为 <span><span>A0=0≠bA\mathbf{0} = \mathbf{0} \ne \mathbf{b}</span><span><span><span></span><span>A</span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>b</span></span></span></span>），所以不是子空间。他说着说着把“它是个子空间”改口成“它像个子空间，只是被移开了原点”，我觉得这个改口比任何定义都清楚。</p></section>
<section><h2>秩决定一切<a href="#秩决定一切"><span>#</span></a></h2><p>后半讲他退一步，看一般情形：<span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵，秩为 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>。他先问了 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 与 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 的关系：<span><span>r≤mr \le m</span><span><span><span></span><span>r</span><span></span><span>≤</span><span></span></span><span><span></span><span>m</span></span></span></span>（每行至多一个主元），<span><span>r≤nr \le n</span><span><span><span></span><span>r</span><span></span><span>≤</span><span></span></span><span><span></span><span>n</span></span></span></span>（每列至多一个主元）。然后问最有意思的情形：<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 取到上限，也就是满秩。</p><p><strong>满列秩 <span><span>r=nr = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>。</strong> 每列都有主元，没有自由变量，零空间只有零向量。如果有解，完全解就是 <span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，没有第二项，所以解是唯一的；但不一定有解，因为行数多于列数，会出现零行，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 要满足条件。他的例子是又高又瘦的</p><span><span><span>A=[13216151]A = \begin{bmatrix} 1 &amp; 3 \\ 2 &amp; 1 \\ 6 &amp; 1 \\ 5 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>秩是 2，<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 是上面一个 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的单位矩阵、下面两行零。四个方程两个未知数，随便取一个 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 大概率没解；但如果取成两列之和 <span><span>(4,3,7,6)(4, 3, 7, 6)</span><span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>，那解就是 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，而且是唯一解。</p><p><strong>满行秩 <span><span>r=mr = m</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span></span></span></span>。</strong> 每行都有主元，消元不会出零行，对 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 没有任何条件，所以任何 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 都有解。自由变量有 <span><span>n−r=n−mn - r = n - m</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>m</span></span></span></span> 个，所以有无穷多个解。例子他是把上面那个矩阵转置：</p><span><span><span>A=[12653111]A = \begin{bmatrix} 1 &amp; 2 &amp; 6 &amp; 5 \\ 3 &amp; 1 &amp; 1 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>秩 2，<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 是 <span><span>[I F][I\ F]</span><span><span><span></span><span>[</span><span>I</span><span> </span><span>F</span><span>]</span></span></span></span> 的形状，<span><span>FF</span><span><span><span></span><span>F</span></span></span></span> 是自由列。他在这里卡了一下：主列不一定排在前面，所以 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 和 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span> 可能是交错的，不能总写成左边一块 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>、右边一块 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span>。他发现自己写错了，当场改了过来，这一段比标准答案更有教益。</p><p><strong>满秩的方阵 <span><span>r=m=nr = m = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span>。</strong> 这是第二章的情形：<span><span>R=IR = I</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，零空间只有零向量，对每个 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 恰有一个解，也就是可逆矩阵。他的例子是 <span><span>[1231]\begin{bmatrix} 1 &amp; 2 \\ 3 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。</p><p><strong>其余情形 <span><span>r&lt;mr &lt; m</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span> 且 <span><span>r&lt;nr &lt; n</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span>。</strong> <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 是 <span><span>[IF00]\begin{bmatrix} I &amp; F \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>F</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，有零行，所以 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 要满足条件才有解；有自由列，所以有解就有无穷多个。我们的 3 × 4 例子就属于这类。</p><p>他把四种情形写在黑板上的表，我整理成这样：</p>

<table><thead><tr><th>情形</th><th><span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的形状</th><th>零空间</th><th>解的个数</th></tr></thead><tbody><tr><td><span><span>r=m=nr = m = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span></td><td><span><span>II</span><span><span><span></span><span>I</span></span></span></span></td><td>只有 <span><span>0\mathbf{0}</span><span><span><span></span><span>0</span></span></span></span></td><td>恰好 1 个</td></tr><tr><td><span><span>r=n&lt;mr = n &lt; m</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span></td><td><span><span>[I0]\begin{bmatrix} I \\ 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></td><td>只有 <span><span>0\mathbf{0}</span><span><span><span></span><span>0</span></span></span></span></td><td>0 个或 1 个</td></tr><tr><td><span><span>r=m&lt;nr = m &lt; n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span></td><td><span><span>[IF]\begin{bmatrix} I &amp; F \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>F</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></td><td><span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 维</td><td>无穷多个</td></tr><tr><td><span><span>r&lt;mr &lt; m</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span> 且 <span><span>r&lt;nr &lt; n</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span></td><td><span><span>[IF00]\begin{bmatrix} I &amp; F \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>F</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></td><td><span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 维</td><td>0 个或无穷多个</td></tr></tbody></table><p>四个矩阵都交给 NumPy。对每个矩阵，我抽 1000 个随机的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，数有多少个在列空间里（判据是：把 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 并到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 右边之后秩不变）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>cases </span><span>=</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>"r = m = n（方阵，可逆）"</span><span><span>: np.</span><span>array</span><span>([[</span></span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>3.0</span><span>, </span><span>1.0</span><span>]]),</span></div></div><div><div><div>9</div></div><div><span>        </span><span>"r = n &lt; m（又高又瘦）"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>[[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>6.0</span><span>, </span><span>1.0</span><span>], [</span><span>5.0</span><span>, </span><span>1.0</span><span>]]</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"r = m &lt; n（又矮又胖）"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>6.0</span><span>, </span><span>5.0</span><span>], [</span><span>3.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>]]</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"r &lt; m 且 r &lt; n"</span><span><span>: np.</span><span>array</span><span>(</span></span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>], [</span><span>3.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>, </span><span>10.0</span><span>]]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>),</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>trials </span><span>=</span><span> </span><span>1000</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> name, A </span><span>in</span><span><span> cases.</span><span>items</span><span>():</span></span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>solvable </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>25</div></div><div><span>        </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(trials):</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>b </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(m)</span></div></div><div><div><div>27</div></div><div><span>            </span><span># b 在列空间里，等价于加上 b 之后秩不变</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>aug </span><span>=</span><span> np.</span><span>hstack</span><span>([A, b[:, </span><span>None</span><span>]])</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>solvable </span><span>+=</span><span> np.linalg.</span><span>matrix_rank</span><span>(aug) </span><span>==</span><span> r</span></div></div><div><div><div>30</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：m = </span><span>{</span><span>m</span><span>}</span><span>，n = </span><span>{</span><span>n</span><span>}</span><span>，r = </span><span>{</span><span>r</span><span>}</span><span>，自由变量 </span><span>{</span><span><span>n </span><span>-</span><span> r</span></span><span>}</span><span> 个"</span><span>)</span></div></div><div><div><div>31</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"  随机 b 有解的比例：</span><span>{</span><span><span>solvable </span><span>/</span><span> trials</span></span><span>:.3f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 高瘦矩阵：挑一个在列空间里的 b，解唯一</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> cases[</span><span>"r = n &lt; m（又高又瘦）"</span><span>]</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> A[:, </span><span>0</span><span><span>] </span><span>+</span><span> A[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)[</span><span>0</span><span>]</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"b ="</span><span>, b, </span><span>"，解 x ="</span><span>, x, </span><span>"，A @ x = b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, b))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>r = m = n（方阵，可逆）：m = 2，n = 2，r = 2，自由变量 0 个</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>随机 b 有解的比例：1.000</span></div></div><div><div><div>3</div></div><div><span>r = n &lt; m（又高又瘦）：m = 4，n = 2，r = 2，自由变量 0 个</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>随机 b 有解的比例：0.000</span></div></div><div><div><div>5</div></div><div><span>r = m &lt; n（又矮又胖）：m = 2，n = 4，r = 2，自由变量 2 个</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>随机 b 有解的比例：1.000</span></div></div><div><div><div>7</div></div><div><span>r &lt; m 且 r &lt; n：m = 3，n = 4，r = 2，自由变量 2 个</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>随机 b 有解的比例：0.000</span></div></div><div><div><div>9</div></div><div><span>b = [4. 3. 7. 6.] ，解 x = [1. 1.] ，A @ x = b： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>表里的“0 个或 1 个”“0 个或无穷多个”，在这里变成了两个干净的数字。<span><span>r=mr = m</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span></span></span></span> 的两行（方阵、又矮又胖），随机 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 有解的比例是 1.000；<span><span>r&lt;mr &lt; m</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span> 的两行（又高又瘦、<span><span>r&lt;mr &lt; m</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>m</span></span></span></span> 且 <span><span>r&lt;nr &lt; n</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span>），比例是 0.000。1000 次一次都没有撞上，这并不奇怪：列空间是 <span><span>Rm\mathbb{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span> 里的一个真子空间（4 维空间里的一个平面，或 3 维空间里的一个平面），随机向量落在上面的概率是 0。要想有解，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 必须由我挑，就像最后一行里的 <span><span>b=(4,3,7,6)\mathbf{b} = (4, 3, 7, 6)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>7</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>，解 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 唯一。</p><p>再看自由变量的个数：<span><span>r=nr = n</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span> 的两行是 0 个，<span><span>r&lt;nr &lt; n</span><span><span><span></span><span>r</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>n</span></span></span></span> 的两行都是 2 个。把“有解吗”（看 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 和 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>）与“解几个”（看 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 和 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>）合在一起，就是他在最后一句话里说的：秩告诉你关于解的个数的全部信息，唯一不知道的是解的具体数值，那要回到矩阵本身去算。</p><p>这一讲他说“二维子空间”“两个独立的自由常数”的时候，其实已经在用“维数”这个还没定义的词了，他自己也说下次再讲。下一讲就把“线性无关”“基”“维数”补上，见 <a href="/posts/linear-algebra-09-independence-basis-dimension/">18.06 第 9 讲：线性无关、基与维数</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-07-solving-ax-0/</id>
      <title type="text">18.06 第 7 讲：解 Ax = 0，主变量与特解</title>
      <published>2026-10-18T02:00:00.000Z</published>
      <updated>2026-10-18T02:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-07-solving-ax-0/"/>
      <summary type="text">MIT 18.06 第 7 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 手写了带容差的 rref，把 Strang 的 3 × 4 矩阵化成 R 并读出特解矩阵 N，A @ N 为 0；容差设成 0 时，一个按构造秩为 3 的 6 × 8 矩阵被判成秩 6，零空间只剩 2 维，而不是 5 维。</summary>
      <content type="html"><![CDATA[<p>上一讲 Strang 问的是零空间“是什么”，这一讲他回答“怎么算”：对任意形状的矩阵做消元，遇到 0 主元不要停，数出主元的个数就是秩，剩下的列是自由列，给自由变量依次赋 1 和 0，回代出来的向量叫特解，零空间就是特解的所有组合。我把他黑板上的 3 × 4 矩阵交给一个手写的 <code>rref</code>：秩是 2，两个特解拼成的 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 满足 <span><span>AN=0AN = 0</span><span><span><span></span><span>A</span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。然后我把容差改成 0，一个按构造秩为 3 的 6 × 8 矩阵立刻被判成秩 6，零空间只剩 2 维，而真实答案是 5 维。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=VqP2tREMvt0" target="_blank">Lecture 7: Solving Ax = 0: pivot variables, special solutions</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-7-solving-ax-0-pivot-variables-special-solutions/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §3.2 Computing the Nullspace by Elimination: A = CR
前置：<a href="/posts/linear-algebra-06-column-space-and-nullspace/">第 6 讲：列空间与零空间</a>
一句话：把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 消元到行最简形 <span><span>R=[IF00]R = \begin{bmatrix} I &amp; F \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>F</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，零空间的特解矩阵就是 <span><span>N=[−FI]N = \begin{bmatrix} -F \\ I \end{bmatrix}</span><span><span><span></span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>F</span></span></span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，<span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 个自由变量对应 <span><span>n−rn - r</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span></span></span></span> 个特解。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他拿出 3 × 4 矩阵，先口头指出“第二列是第一列的倍数、第三行是前两行之和”，再开始消元。这一段值得看：他先猜答案，再让消元去验证。</li>
<li>消元的算术很短，本篇第一节已经用代码算过，可以倍速。他在第二列发现主元位置是 0、下面也全是 0，当场不停下来分析、直接往下一列走，这个处理是这一讲的关键，别快进。</li>
<li>“自由变量取 1 和 0、回代求主变量”那一段要认真听，他给了名字：主变量、自由变量、特解。</li>
<li>他写出 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 之后用分块矩阵推导“特解里为什么出现 <span><span>−F-F</span><span><span><span></span><span>−</span><span>F</span></span></span></span>”，是后半段最重要的一段。之后的第二个例子（原矩阵的转置）他自己说是“再练一遍”，本篇第四节已经跑过，可以倍速。</li>
</ul></div></div>
<section><h2>消元不停：遇到 0 就往右走<a href="#消元不停遇到-0-就往右走"><span>#</span></a></h2><p>Strang 的矩阵是 3 × 4 的：</p><span><span><span>A=[1222246836810]A = \begin{bmatrix}
1 &amp; 2 &amp; 2 &amp; 2 \\
2 &amp; 4 &amp; 6 &amp; 8 \\
3 &amp; 6 &amp; 8 &amp; 10
\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>字幕里他念了第二行和第三行（“two four six eight and three six eight ten”），第一行是我根据他的两句观察和后面的消元结果反推的 <span><span>(1,2,2,2)(1, 2, 2, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>：第二列确实是第一列的 2 倍，第三行确实等于前两行之和，消元出来的数也和他念的一致。</p><p><strong>怎么对一个长方形矩阵做消元？</strong> 和以前一样，只是多了一条规则：某一列在主元位置是 0、下面也全是 0，就跳过这一列，在下一列继续。第一步，第二行减去 2 倍第一行，第三行减去 3 倍第一行，两行都变成 <span><span>(0,0,2,4)(0, 0, 2, 4)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>。第二列没有主元可用，跳过。第三列的主元是 2，第三行减去第二行，整行变成 0。结果叫 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>：</p><span><span><span>U=[122200240000]U = \begin{bmatrix}
1 &amp; 2 &amp; 2 &amp; 2 \\
0 &amp; 0 &amp; 2 &amp; 4 \\
0 &amp; 0 &amp; 0 &amp; 0
\end{bmatrix}</span><span><span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>他称它为阶梯形（echelon form，echelon 就是“楼梯”的意思）：非零元沿着楼梯往右下走，每级台阶的高度是一行。主元的个数是 2，这个数就是矩阵的<strong>秩</strong>。</p><p>下面是我的 <code>echelon</code>。它只做向下消元，在当前列从当前行往下找第一个非零元，找不到就跳到下一列。“非零”在浮点数里要靠容差判断，容差的事留到后面再说：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>echelon</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""只做向下消元：遇到没有主元的列就跳过，继续往右。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> U.shape</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> row </span><span>==</span><span> m:</span></span></div></div><div><div><div>12</div></div><div><span>            </span><span>break</span></div></div><div><div><div>13</div></div><div><span>        </span><span># 在 row 行及以下找第一个非零元，必要时换行</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>candidates </span><span>=</span><span> np.</span><span>nonzero</span><span>(np.</span><span>abs</span><span>(U[row:, col]) </span><span>&gt;</span><span> tol)[</span><span>0</span><span>]</span></div></div><div><div><div>15</div></div><div><span>        </span><span>if</span><span><span> candidates.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>U[row:, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>17</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> row </span><span>+</span><span> candidates[</span><span>0</span><span>]</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>U[[row, k]] </span><span>=</span><span> U[[k, row]]</span></div></div><div><div><div>20</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(row </span><span>+</span><span> </span></span><span>1</span><span>, m):</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> U[i, col] </span><span>/</span><span> U[row, col] </span><span>*</span><span> U[row]</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>U[i, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(col)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>row </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span><span> U </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>], [</span><span>3.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>, </span><span>10.0</span><span>]]</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>U, pivots </span><span>=</span><span> </span><span>echelon</span><span>(A)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"U ="</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(U)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"主列（从 0 数起）："</span><span>, pivots, </span><span>"，秩 ="</span><span>, </span><span>len</span><span>(pivots))</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.matrix_rank(A) ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"第三行等于前两行之和："</span><span><span>, np.</span><span>allclose</span><span>(A[</span></span><span>2</span><span>], A[</span><span>0</span><span><span>] </span><span>+</span><span> A[</span></span><span>1</span><span>]))</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span># 消元不改变零空间，但会改变列空间</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>xs </span><span>=</span><span> [np.</span><span>array</span><span>([</span><span>-</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span><span>]), np.</span><span>array</span><span>([</span></span><span>2.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>, </span><span>1.0</span><span>])]</span></div></div><div><div><div>42</div></div><div><span>    </span><span>for</span><span> x </span><span>in</span><span> xs:</span></div></div><div><div><div>43</div></div><div><span>        </span><span>print</span><span>(x, </span><span>"A @ x ="</span><span><span>, A </span><span>@</span><span> x, </span></span><span>"U @ x ="</span><span><span>, U </span><span>@</span><span> x)</span></span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"rank([A U]) ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>hstack</span><span>([A, U])))</span></span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>U =</span></div></div><div><div><div>2</div></div><div><span>[[1. 2. 2. 2.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 0. 2. 4.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[0. 0. 0. 0.]]</span></div></div><div><div><div>5</div></div><div><span>主列（从 0 数起）： [0, 2] ，秩 = 2</span></div></div><div><div><div>6</div></div><div><span>np.linalg.matrix_rank(A) = 2</span></div></div><div><div><div>7</div></div><div><span>第三行等于前两行之和： True</span></div></div><div><div><div>8</div></div><div><span>[-2.  1.  0.  0.] A @ x = [0. 0. 0.] U @ x = [0. 0. 0.]</span></div></div><div><div><div>9</div></div><div><span>[ 2.  0. -2.  1.] A @ x = [0. 0. 0.] U @ x = [0. 0. 0.]</span></div></div><div><div><div>10</div></div><div><span>rank([A U]) = 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有两件事要看。第一，秩是 2，和 <code>np.linalg.matrix_rank</code> 一致；<code>U</code> 的最后一行全零，说明第三行被前两行“吃掉了”，这也印证了 <code>A[2] == A[0] + A[1]</code>。第二，消元<strong>不改变零空间</strong>：他取的两个向量 <span><span>(−2,1,0,0)(-2, 1, 0, 0)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 和 <span><span>(2,0,−2,1)(2, 0, -2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 被 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 都乘成零。但它<strong>改变了列空间</strong>：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间里有 <span><span>(1,2,3)(1, 2, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，<span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的列空间里没有，把两者的列并排放在一起，秩是 3，大于各自的 2。他在视频里特意提醒要留意消元什么不变，我觉得这个提醒在后面几讲反复用得上：解不变，列空间会变。</p></section>
<section><h2>主变量、自由变量与特解<a href="#主变量自由变量与特解"><span>#</span></a></h2><p><strong>哪些变量是自由的？</strong> 有主元的列叫主列，对应的变量叫主变量，这里是 <span><span>x1x_1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>x3x_3</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>；没有主元的列叫自由列，对应的变量是自由变量，这里是 <span><span>x2x_2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>x4x_4</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。自由变量可以随便取值，主变量再由方程回代求出来（有点像做账：自由变量是随便花的零花钱，主变量是被迫补上的差额）。</p><p><span><span>Ux=0U\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>U</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的两个方程是 <span><span>x1+2x2+2x3+2x4=0x_1 + 2x_2 + 2x_3 + 2x_4 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 和 <span><span>2x3+4x4=02x_3 + 4x_4 = 0</span><span><span><span></span><span>2</span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。他的取法是系统的：</p><ul>
<li>取 <span><span>x2=1x_2 = 1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>x4=0x_4 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：第二个方程给出 <span><span>x3=0x_3 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，第一个给出 <span><span>x1=−2x_1 = -2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span>，得到 <span><span>(−2,1,0,0)(-2, 1, 0, 0)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。意思是“<span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 份第一列加 1 份第二列是零向量”，正是他一开始肉眼看出来的那个关系。</li>
<li>取 <span><span>x2=0x_2 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>x4=1x_4 = 1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>：<span><span>x3=−2x_3 = -2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span>，<span><span>x1=2x_1 = 2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，得到 <span><span>(2,0,−2,1)(2, 0, -2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。</li>
</ul><p>这两个向量叫<strong>特解</strong>。特别之处只在于自由变量取的是“一个 1、其余 0”：任何一个解的自由变量取值，都是这些“一个 1、其余 0”的组合，所以每个解都是特解的组合。零空间就是特解的所有组合，<span><span>c1s1+c2s2c_1 \mathbf{s}_1 + c_2 \mathbf{s}_2</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他强调了考试时的写法：如果让你“描述零空间”，要写出这个带 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 的组合，因为你说的是整个空间；如果只要“一组基”，写特解就够了。</p><p>数量也一眼能算：<span><span>m=3m = 3</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span> 行、<span><span>n=4n = 4</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span> 列、秩 <span><span>r=2r = 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，有 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 个主变量，<span><span>n−r=2n - r = 2</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 个自由变量，所以有 2 个特解。这也解释了“三个方程四个未知数”为什么有非零解：看起来是 3 个方程，实际上只有 <span><span>r=2r = 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 个独立的。</p><p>我把回代写成代码，对每个自由变量各做一次，再随机取一组系数，确认组合出来的向量也在零空间里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>echelon</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> U.shape</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> row </span><span>==</span><span> m:</span></span></div></div><div><div><div>11</div></div><div><span>            </span><span>break</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>candidates </span><span>=</span><span> np.</span><span>nonzero</span><span>(np.</span><span>abs</span><span>(U[row:, col]) </span><span>&gt;</span><span> tol)[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> candidates.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>U[row:, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>15</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> row </span><span>+</span><span> candidates[</span><span>0</span><span>]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>U[[row, k]] </span><span>=</span><span> U[[k, row]]</span></div></div><div><div><div>18</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(row </span><span>+</span><span> </span></span><span>1</span><span>, m):</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> U[i, col] </span><span>/</span><span> U[row, col] </span><span>*</span><span> U[row]</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>U[i, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(col)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>row </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span><span> U </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>special_solutions</span><span>(</span><span>U</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>pivots</span></span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>27</div></div><div><span>    </span><span>"""每个自由变量取 1、其余自由变量取 0，回代求主变量。"""</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> U.shape[</span><span>1</span><span>]</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> k, f </span><span>in</span><span> </span><span>enumerate</span><span>(free):</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>x[f] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>34</div></div><div><span>        </span><span>for</span><span> r </span><span>in</span><span> </span><span>reversed</span><span>(</span><span>range</span><span>(</span><span>len</span><span>(pivots))):</span></div></div><div><div><div>35</div></div><div><span><span>            </span></span><span>c </span><span>=</span><span> pivots[r]</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>x[c] </span><span>=</span><span> (</span><span>0.0</span><span><span> </span><span>-</span><span> U[r, c </span><span>+</span><span> </span></span><span>1</span><span><span> :] </span><span>@</span><span> x[c </span><span>+</span><span> </span></span><span>1</span><span><span> :]) </span><span>/</span><span> U[r, c]</span></span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>N[:, k] </span><span>=</span><span> x</span></div></div><div><div><div>38</div></div><div><span>    </span><span>return</span><span> N</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>], [</span><span>3.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>, </span><span>10.0</span><span>]]</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>U, pivots </span><span>=</span><span> </span><span>echelon</span><span>(A)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>1</span><span>]) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(</span><span>"主变量下标："</span><span>, pivots, </span><span>"，自由变量下标："</span><span>, free)</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> </span><span>special_solutions</span><span>(U, pivots)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"特解矩阵 N（每列一个特解）："</span><span>)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(N)</span></div></div><div><div><div>53</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ N ="</span><span>)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span><span>(A </span><span>@</span><span> N)</span></span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>"U @ N 全为 0："</span><span><span>, np.</span><span>allclose</span><span>(U </span><span>@</span><span> N, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>    </span><span># 零空间 = 特解的所有组合：随便取系数，结果仍被 A 消成 0</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> N </span><span>@</span><span> c</span></div></div><div><div><div>61</div></div><div><span>    </span><span>print</span><span>(</span><span>"c ="</span><span>, c, </span><span>"，x = N @ c ="</span><span>, x)</span></div></div><div><div><div>62</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"‖A @ x‖ = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> x)</span></span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>主变量下标： [0, 2] ，自由变量下标： [1, 3]</span></div></div><div><div><div>2</div></div><div><span>特解矩阵 N（每列一个特解）：</span></div></div><div><div><div>3</div></div><div><span>[[-2.  2.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 1.  0.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[ 0. -2.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>7</div></div><div><span>A @ N =</span></div></div><div><div><div>8</div></div><div><span>[[0. 0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[0. 0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0. 0.]]</span></div></div><div><div><div>11</div></div><div><span>U @ N 全为 0： True</span></div></div><div><div><div>12</div></div><div><span>c = [ 0.6679 -0.2323] ，x = N @ c = [-1.8005  0.6679  0.4646 -0.2323]</span></div></div><div><div><div>13</div></div><div><span>‖A @ x‖ = 9.99e-16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的两列和他算的完全一致。随机系数 <span><span>c=(0.6679,−0.2323)c = (0.6679, -0.2323)</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0.6679</span><span>,</span><span></span><span>−</span><span>0.2323</span><span>)</span></span></span></span> 组合出来的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 被 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘出来的范数是 <span><span>10−1510^{-15}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 量级，就是浮点舍入，不是别的。顺带一提，回代那一行 <code>x[c] = (0.0 - ...) / U[r, c]</code> 里的 <code>0.0 -</code> 是为了不打印出难看的 <code>-0.</code>，没有数学意义。</p></section>
<section><h2>进一步清理：行最简形 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span><a href="#进一步清理行最简形-rrr"><span>#</span></a></h2><p><strong><span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 还能再化简吗？</strong> 能。他往上消元，把主元上面的元素也清零，再把每个主元除成 1。第二行除以 2 得到 <span><span>(0,0,1,2)(0, 0, 1, 2)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，第一行减去 2 倍第二行，<span><span>(1,2,2,2)(1, 2, 2, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 变成 <span><span>(1,2,0,−2)(1, 2, 0, -2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span></span></span></span>：</p><span><span><span>R=[120−200120000]R = \begin{bmatrix}
1 &amp; 2 &amp; 0 &amp; -2 \\
0 &amp; 0 &amp; 1 &amp; 2 \\
0 &amp; 0 &amp; 0 &amp; 0
\end{bmatrix}</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>这叫<strong>行最简形</strong>（reduced row echelon form，MATLAB 里是 <code>rref</code>）。他指出里面的几处结构：主行、主列交叉处是一个单位矩阵 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>（这里是 2 × 2）；自由列里剩下的部分叫 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span>；底下是零行。把列重新排一下，<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 就是</p><span><span><span>R=[IF00]R = \begin{bmatrix} I &amp; F \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>F</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>然后是这一讲最漂亮的一步：<strong>特解为什么能直接读出来？</strong> 解 <span><span>Rx=0R\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>R</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，把 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 拆成主变量部分 <span><span>xp\mathbf{x}_p</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和自由变量部分 <span><span>xf\mathbf{x}_f</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：<span><span>xp+Fxf=0\mathbf{x}_p + F\mathbf{x}_f = \mathbf{0}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>F</span><span><span>x</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以 <span><span>xp=−Fxf\mathbf{x}_p = -F\mathbf{x}_f</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>F</span><span><span>x</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。自由变量取单位矩阵的每一列，所有特解就能一次写出来：</p><span><span><span>N=[−FI],RN=−F+F=0N = \begin{bmatrix} -F \\ I \end{bmatrix}, \qquad RN = -F + F = 0</span><span><span><span></span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>F</span></span></span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span>R</span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>F</span><span></span><span>+</span><span></span></span><span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>不用回代。上面算出的 <span><span>(−2,1,0,0)(-2, 1, 0, 0)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>：自由列里 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span> 的第一列是 <span><span>(2,0)(2, 0)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，取负就是 <span><span>(−2,0)(-2, 0)</span><span><span><span></span><span>(</span><span>−</span><span>2</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，放在主变量的位置，<span><span>x2x_2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 位置放 1，完全吻合。他在视频里说这不是魔法，必然如此，我同意。</p><p>我把它写成两个函数：<code>rref</code> 把主元化成 1 并双向清零，<code>null_basis</code> 按下标把 <span><span>−F-F</span><span><span><span></span><span>−</span><span>F</span></span></span></span> 和 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 放回各自的位置。写完用 SVD 给的零空间对照（基不一样，但张成的应该是同一个子空间）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rref</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> R.shape</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> row </span><span>==</span><span> m:</span></span></div></div><div><div><div>11</div></div><div><span>            </span><span>break</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>candidates </span><span>=</span><span> np.</span><span>nonzero</span><span>(np.</span><span>abs</span><span>(R[row:, col]) </span><span>&gt;</span><span> tol)[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> candidates.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>R[row:, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>15</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> row </span><span>+</span><span> candidates[</span><span>0</span><span>]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>R[[row, k]] </span><span>=</span><span> R[[k, row]]</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>R[row] </span><span>/=</span><span> R[row, col]</span></div></div><div><div><div>19</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>20</div></div><div><span>            </span><span>if</span><span><span> i </span><span>!=</span><span> row:</span></span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>R[i] </span><span>-=</span><span> R[i, col] </span><span>*</span><span> R[row]</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(col)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>row </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span><span> R </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>def</span><span> </span><span>null_basis</span><span>(</span><span>R</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>pivots</span></span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> R.shape[</span><span>1</span><span>]</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>N[free, :] </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>len</span><span>(free))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>N[pivots, :] </span><span>=</span><span> </span><span>0.0</span><span><span> </span><span>-</span><span> R[: </span></span><span>len</span><span>(pivots), free]</span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> N</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>], [</span><span>3.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>, </span><span>10.0</span><span>]]</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>R, pivots </span><span>=</span><span> </span><span>rref</span><span>(A)</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> </span><span>len</span><span>(pivots)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>1</span><span>]) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"R ="</span><span>)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(R)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"主列："</span><span>, pivots, </span><span>"，自由列："</span><span>, free)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"I 块（主行、主列）："</span><span>)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>print</span><span>(R[:r, pivots])</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>F </span><span>=</span><span> R[:r, free]</span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span><span>"F 块（主行、自由列）："</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(F)</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>    </span><span># N = [-F; I]：自由行放单位矩阵，主行放 -F</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> </span><span>null_basis</span><span>(R, pivots)</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span><span>"N ="</span><span>)</span></div></div><div><div><div>56</div></div><div><span>    </span><span>print</span><span>(N)</span></div></div><div><div><div>57</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ N 全为 0："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> N, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>58</div></div><div><span>    </span><span>print</span><span>(</span><span>"R @ N 全为 0："</span><span><span>, np.</span><span>allclose</span><span>(R </span><span>@</span><span> N, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>    </span><span># 和 SVD 给的零空间对照：基不同，但张成同一个子空间</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>_, _, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>V_null </span><span>=</span><span> Vt[r:].T</span></div></div><div><div><div>63</div></div><div><span>    </span><span>print</span><span>(</span><span>"SVD 的零空间基维数："</span><span>, V_null.shape[</span><span>1</span><span>])</span></div></div><div><div><div>64</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>65</div></div><div><span>        </span><span>"N 与 SVD 基合在一起的秩："</span><span>,</span></div></div><div><div><div>66</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>hstack</span><span>([N, V_null])),</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div>
</div></div><div><div><div>70</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>R =</span></div></div><div><div><div>2</div></div><div><span>[[ 1.  2.  0. -2.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.  0.  1.  2.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  0.  0.  0.]]</span></div></div><div><div><div>5</div></div><div><span>主列： [0, 2] ，自由列： [1, 3]</span></div></div><div><div><div>6</div></div><div><span>I 块（主行、主列）：</span></div></div><div><div><div>7</div></div><div><span>[[1. 0.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[0. 1.]]</span></div></div><div><div><div>9</div></div><div><span>F 块（主行、自由列）：</span></div></div><div><div><div>10</div></div><div><span>[[ 2. -2.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 0.  2.]]</span></div></div><div><div><div>12</div></div><div><span>N =</span></div></div><div><div><div>13</div></div><div><span>[[-2.  2.]</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[ 1.  0.]</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[ 0. -2.]</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>[ 0.  1.]]</span></div></div><div><div><div>17</div></div><div><span>A @ N 全为 0： True</span></div></div><div><div><div>18</div></div><div><span>R @ N 全为 0： True</span></div></div><div><div><div>19</div></div><div><span>SVD 的零空间基维数： 2</span></div></div><div><div><div>20</div></div><div><span>N 与 SVD 基合在一起的秩： 2</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>RR</span><span><span><span></span><span>R</span></span></span></span>、<span><span>FF</span><span><span><span></span><span>F</span></span></span></span>、<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 都和上面的手算一致。SVD 给出的零空间基有 2 个向量，它和 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 拼在一起，秩仍然是 2，说明两组基张成的是同一个二维子空间。这是“求零空间”的另一条路，后面会说为什么我更信任它。</p><p>两个小注：我的 <code>rref</code> 是一次做完向上、向下消元，而 Strang 先做到 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 再往上清，结果是同一个 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span>（行最简形是唯一的）。另外，它用“第一个非零元”当主元，不选最大的，数值上很天真（我没打算让它活过一个病态矩阵），教学够用。</p></section>
<section><h2>再来一个例子：转置<a href="#再来一个例子转置"><span>#</span></a></h2><p>他的第二个例子是同一个矩阵的转置，<span><span>B=ATB = A^{T}</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span>，一个 4 × 3 的矩阵：</p><span><span><span>B=[1232462682810]B = \begin{bmatrix}
1 &amp; 2 &amp; 3 \\
2 &amp; 4 &amp; 6 \\
2 &amp; 6 &amp; 8 \\
2 &amp; 8 &amp; 10
\end{bmatrix}</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>开始之前他让大家预测：三列里第三列是前两列之和，所以前两列是主列、第三列是自由列，秩是 2。消元时这里会遇到一次行交换：第一列清零之后，第二行变成全零，第三行的第二个元素是 2，要和第二行换位置。我把 <code>rref</code> 直接用在 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 上，同时验证他顺口说的一件事：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>ATA^{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span> 的主列个数相同。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rref</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> R.shape</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> row </span><span>==</span><span> m:</span></span></div></div><div><div><div>11</div></div><div><span>            </span><span>break</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>candidates </span><span>=</span><span> np.</span><span>nonzero</span><span>(np.</span><span>abs</span><span>(R[row:, col]) </span><span>&gt;</span><span> tol)[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> candidates.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>R[row:, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>15</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> row </span><span>+</span><span> candidates[</span><span>0</span><span>]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>R[[row, k]] </span><span>=</span><span> R[[k, row]]</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>R[row] </span><span>/=</span><span> R[row, col]</span></div></div><div><div><div>19</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>20</div></div><div><span>            </span><span>if</span><span><span> i </span><span>!=</span><span> row:</span></span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>R[i] </span><span>-=</span><span> R[i, col] </span><span>*</span><span> R[row]</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(col)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>row </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span><span> R </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>def</span><span> </span><span>null_basis</span><span>(</span><span>R</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>pivots</span></span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> R.shape[</span><span>1</span><span>]</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>N[free, :] </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>len</span><span>(free))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>N[pivots, :] </span><span>=</span><span> </span><span>0.0</span><span><span> </span><span>-</span><span> R[: </span></span><span>len</span><span>(pivots), free]</span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> N</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>4.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>], [</span><span>3.0</span><span>, </span><span>6.0</span><span>, </span><span>8.0</span><span>, </span><span>10.0</span><span>]]</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> A.T  </span><span># 4 x 3，第三列是前两列之和</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>R, pivots </span><span>=</span><span> </span><span>rref</span><span>(B)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> </span><span>null_basis</span><span>(R, pivots)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"R ="</span><span>)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(R)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"主列："</span><span>, pivots, </span><span>"，秩 ="</span><span>, </span><span>len</span><span>(pivots))</span></div></div><div><div><div>47</div></div><div><span>    </span><span>print</span><span>(</span><span>"N ="</span><span>, N[:, </span><span>0</span><span>], </span><span>"，B @ N 全为 0："</span><span><span>, np.</span><span>allclose</span><span>(B </span><span>@</span><span> N, </span></span><span>0.0</span><span>))</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span># A 与 A 转置的主列个数相同，但主列的位置不同</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>_, piv_a </span><span>=</span><span> </span><span>rref</span><span>(A)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的主列："</span><span>, piv_a, </span><span>"，A.T 的主列："</span><span>, pivots)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span><span>"个数相同："</span><span>, </span><span>len</span><span><span>(piv_a) </span><span>==</span><span> </span></span><span>len</span><span>(pivots))</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>R =</span></div></div><div><div><div>2</div></div><div><span>[[1. 0. 1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 1. 1.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[0. 0. 0.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[0. 0. 0.]]</span></div></div><div><div><div>6</div></div><div><span>主列： [0, 1] ，秩 = 2</span></div></div><div><div><div>7</div></div><div><span>N = [-1. -1.  1.] ，B @ N 全为 0： True</span></div></div><div><div><div>8</div></div><div><span>A 的主列： [0, 2] ，A.T 的主列： [0, 1]</span></div></div><div><div><div>9</div></div><div><span>个数相同： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>自由列只有一个，所以零空间是一条直线，<span><span>N=(−1,−1,1)N = (-1, -1, 1)</span><span><span><span></span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，就是“第三列减去前两列等于零”。他要求考试时写 <span><span>c (−1,−1,1)c\,(-1, -1, 1)</span><span><span><span></span><span>c</span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，不要忘了 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span>。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的主列在位置 0 和 2，<span><span>ATA^{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span> 的在 0 和 1，个数都是 2，位置不同。个数一样的原因，他只说这是个了不起的事实，当场没有解释；我猜要等到讲四个基本子空间的那几讲，那时会说清行秩等于列秩。</p></section>
<section><h2>浮点数里的“零”<a href="#浮点数里的零"><span>#</span></a></h2><p><strong>主元是不是零？</strong> 纸上算 <span><span>2−2=02 - 2 = 0</span><span><span><span></span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 是精确的，浮点数里不是。矩阵的秩是 3，但如果元素来自乘法，消元之后本该是 0 的位置会留下 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级的残渣。如果按“严格等于 0”来判断，算法会把这些残渣当成主元，除以它们，整个结果就被放大成垃圾。所以我的函数里每次找主元都带了容差 <code>tol</code>，小于它的当作 0。</p><p>我拿一个按构造秩为 3 的 6 × 8 矩阵（三个 6 维列向量的组合，固定种子），分别用 <code>tol = 0</code> 和 <code>tol = 1e-10</code> 求零空间。奇异值也一并打印出来，看真实的“秩”在哪里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rref</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> R.shape</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> row </span><span>==</span><span> m:</span></span></div></div><div><div><div>11</div></div><div><span>            </span><span>break</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>candidates </span><span>=</span><span> np.</span><span>nonzero</span><span>(np.</span><span>abs</span><span>(R[row:, col]) </span><span>&gt;</span><span> tol)[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> candidates.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>R[row:, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>15</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> row </span><span>+</span><span> candidates[</span><span>0</span><span>]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>R[[row, k]] </span><span>=</span><span> R[[k, row]]</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>R[row] </span><span>/=</span><span> R[row, col]</span></div></div><div><div><div>19</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>20</div></div><div><span>            </span><span>if</span><span><span> i </span><span>!=</span><span> row:</span></span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>R[i] </span><span>-=</span><span> R[i, col] </span><span>*</span><span> R[row]</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(col)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>row </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span><span> R </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>def</span><span> </span><span>null_basis</span><span>(</span><span>R</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>pivots</span></span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> R.shape[</span><span>1</span><span>]</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>N[free, :] </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>len</span><span>(free))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>N[pivots, :] </span><span>=</span><span> </span><span>0.0</span><span><span> </span><span>-</span><span> R[: </span></span><span>len</span><span>(pivots), free]</span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> N</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span># 6 x 8 的矩阵，构造成秩 3：三个 6 维列向量的组合</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>6</span><span>, </span><span>3</span><span><span>)) </span><span>@</span><span> rng.</span><span>standard_normal</span><span>((</span></span><span>3</span><span>, </span><span>8</span><span>))</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A, </span><span>compute_uv</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"奇异值："</span><span><span>, np.</span><span>array2string</span><span>(s, </span></span><span>formatter</span><span><span>=</span><span>{</span></span><span>"float"</span><span>: </span><span>"</span><span><span>{</span><span>:.2e</span><span>}</span></span><span>"</span><span>.format}))</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span>for</span><span> tol </span><span>in</span><span> (</span><span>0.0</span><span>, </span><span>1e-10</span><span>):</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>R, pivots </span><span>=</span><span> </span><span>rref</span><span>(A, </span><span>tol</span><span><span>=</span><span>tol)</span></span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>N </span><span>=</span><span> </span><span>null_basis</span><span>(R, pivots)</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>residual </span><span>=</span><span> np.</span><span>abs</span><span>(A </span><span>@</span><span> N).</span><span>max</span><span>()</span></div></div><div><div><div>48</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>49</div></div><div><span>            </span><span>f</span><span>"tol = </span><span>{</span><span>tol</span><span>:g</span><span>}</span><span>：秩 = </span><span>{</span><span>len</span><span>(pivots)</span><span>}</span><span>，N 有 </span><span>{</span><span>N.shape[</span><span>1</span><span>]</span><span>}</span><span> 列，"</span></div></div><div><div><div>50</div></div><div><span>            </span><span>f</span><span>"max|A @ N| = </span><span>{</span><span>residual</span><span>:.2e</span><span>}</span><span>"</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span><span>"np.linalg.matrix_rank(A) ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>奇异值： [9.91e+00 3.60e+00 2.69e+00 3.91e-16 2.37e-16 1.15e-16]</span></div></div><div><div><div>2</div></div><div><span>tol = 0：秩 = 6，N 有 2 列，max|A @ N| = 8.66e-15</span></div></div><div><div><div>3</div></div><div><span>tol = 1e-10：秩 = 3，N 有 5 列，max|A @ N| = 3.55e-15</span></div></div><div><div><div>4</div></div><div><span>np.linalg.matrix_rank(A) = 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个奇异值在 <span><span>2.692.69</span><span><span><span></span><span>2.69</span></span></span></span> 以上，后三个在 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级，界线分明，真实的秩是 3，零空间是 <span><span>8−3=58 - 3 = 5</span><span><span><span></span><span>8</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 维，<code>matrix_rank</code> 也这么说。<code>tol = 1e-10</code> 的结果对得上：秩 3、<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 有 5 列。<code>tol = 0</code> 的结果让我意外：秩被判成 6，<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 只有 2 列。更有意思的是 <code>max|A @ N|</code> 在两种情况下都是 <span><span>10−1510^{-15}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 量级，也就是说 <code>tol = 0</code> 得到的 2 个向量<strong>没有错</strong>，它们确实在零空间里，只是漏了 3 个。没有任何报错或警告提醒你“这个零空间不完整”，我觉得这比直接算错更危险。</p><p>容差取多少？我取 <span><span>10−1010^{-10}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 是因为它夹在 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 的残渣和 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span> 的真元之间，两边各留了很多个数量级的余量。据我所知，更严格的做法是让容差跟矩阵的尺度走，NumPy 的 <code>matrix_rank</code> 用的是“最大奇异值乘以矩阵尺寸乘以机器精度”。我猜这就是实际工程里大家更愿意直接调 SVD 的原因：它把“哪些是零”的判断放在一排有序的奇异值上，界线比消元里的某个主元清楚得多。手写 <code>rref</code> 是为了看明白算法在干什么，不是为了在病态矩阵上拿来用。</p></section>
<section><h2>在控制里<a href="#在控制里"><span>#</span></a></h2><p>麦轮底盘有四个轮子，只推三个自由度：纵向力、横向力和转矩。这样力和电流之间的关系就是一个 3 × 4 的矩阵 <span><span>A=HTA = H^{T}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span>（<span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 是<a href="/posts/pid-control-08-mimo/">多变量那一篇</a>里的 4 × 3 的轮子几何矩阵），<span><span>AiA\mathbf{i}</span><span><span><span></span><span>A</span><span>i</span></span></span></span> 是四个轮子的电流 <span><span>i\mathbf{i}</span><span><span><span></span><span>i</span></span></span></span> 产生的合力和合力矩。3 × 4 的矩阵，秩 3，<span><span>n−r=1n - r = 1</span><span><span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>：一个自由变量，一个特解，零空间是一条直线。这个形状和今天 Strang 的黑板几乎是同一个题。我把它交给上面的 <code>rref</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>rref</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-10</span></span><span>) -&gt; tuple[np.ndarray, list[</span><span>int</span><span>]]:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> R.shape</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>pivots: list[</span><span>int</span><span><span>] </span><span>=</span><span> []</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> row </span><span>==</span><span> m:</span></span></div></div><div><div><div>11</div></div><div><span>            </span><span>break</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>candidates </span><span>=</span><span> np.</span><span>nonzero</span><span>(np.</span><span>abs</span><span>(R[row:, col]) </span><span>&gt;</span><span> tol)[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> candidates.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>R[row:, col] </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>15</div></div><div><span>            </span><span>continue</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> row </span><span>+</span><span> candidates[</span><span>0</span><span>]</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>R[[row, k]] </span><span>=</span><span> R[[k, row]]</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>R[row] </span><span>/=</span><span> R[row, col]</span></div></div><div><div><div>19</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>20</div></div><div><span>            </span><span>if</span><span><span> i </span><span>!=</span><span> row:</span></span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>R[i] </span><span>-=</span><span> R[i, col] </span><span>*</span><span> R[row]</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>pivots.</span><span>append</span><span>(col)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>row </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span><span> R </span><span>+</span><span> </span></span><span>0.0</span><span>, pivots</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>def</span><span> </span><span>null_basis</span><span>(</span><span>R</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>pivots</span></span><span>:</span><span> list[</span><span>int</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> R.shape[</span><span>1</span><span>]</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>free </span><span>=</span><span> [j </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n) </span><span>if</span><span> j </span><span>not</span><span> </span><span>in</span><span> pivots]</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span><span>len</span><span>(free)))</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>N[free, :] </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>len</span><span>(free))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>N[pivots, :] </span><span>=</span><span> </span><span>0.0</span><span><span> </span><span>-</span><span> R[: </span></span><span>len</span><span>(pivots), free]</span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> N</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>38</div></div><div><span>    </span><span># 麦轮底盘：行依次是左前、右前、左后、右后，L = 0.4</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>H </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>, </span><span>-</span></span><span>0.4</span><span>],</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>0.4</span><span>],</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>0.4</span><span>],</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.4</span><span>],</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> H.T  </span><span># 3 x 4：四个电流映射成 (Fx, Fy, Mz)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>R, pivots </span><span>=</span><span> </span><span>rref</span><span>(A)</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> </span><span>null_basis</span><span>(R, pivots)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span><span>"R ="</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(R)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩 ="</span><span>, </span><span>len</span><span>(pivots), </span><span>"，零空间维数 ="</span><span>, N.shape[</span><span>1</span><span>])</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> N[:, </span><span>0</span><span><span>] </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(N[:, </span></span><span>0</span><span>])</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span>(</span><span>"单位化的零空间向量 n ="</span><span>, n)</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>    </span><span># 想要的合力与合力矩（取个无量纲的值），最小范数电流再加一份 n</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>8.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>])</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>i_min </span><span>=</span><span> np.linalg.</span><span>pinv</span><span>(A) </span><span>@</span><span> w</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>i_shift </span><span>=</span><span> i_min </span><span>+</span><span> </span><span>10.0</span><span><span> </span><span>*</span><span> n</span></span></div></div><div><div><div>60</div></div><div><span>    </span><span>print</span><span>(</span><span>"最小范数电流："</span><span>, i_min, </span><span>f</span><span>"，范数 = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(i_min)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>61</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>62</div></div><div><span>        </span><span>"加上 10 * n 之后："</span><span>,</span></div></div><div><div><div>63</div></div><div><span><span>        </span></span><span>i_shift,</span></div></div><div><div><div>64</div></div><div><span>        </span><span>f</span><span>"，范数 = </span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(i_shift)</span></span><span>:.4f</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>66</div></div><div><span>    </span><span>print</span><span>(</span><span>"两组电流产生同样的 (Fx, Fy, Mz)："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> i_shift, w))</span></span></div></div><div><div><div>67</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"n @ i_min = </span><span>{</span><span><span>n </span><span>@</span><span> i_min</span></span><span>:.4f</span><span>}</span><span>，n @ i_shift = </span><span>{</span><span><span>n </span><span>@</span><span> i_shift</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div>
</div></div><div><div><div>70</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>R =</span></div></div><div><div><div>2</div></div><div><span>[[ 1.  0.  0.  1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.  1.  0.  1.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  0.  1. -1.]]</span></div></div><div><div><div>5</div></div><div><span>秩 = 3 ，零空间维数 = 1</span></div></div><div><div><div>6</div></div><div><span>单位化的零空间向量 n = [-0.5 -0.5  0.5  0.5]</span></div></div><div><div><div>7</div></div><div><span>最小范数电流： [0.875 3.125 1.875 2.125] ，范数 = 4.3084</span></div></div><div><div><div>8</div></div><div><span>加上 10 * n 之后： [-4.125 -1.875  6.875  7.125] ，范数 = 10.8886</span></div></div><div><div><div>9</div></div><div><span>两组电流产生同样的 (Fx, Fy, Mz)： True</span></div></div><div><div><div>10</div></div><div><span>n @ i_min = 0.0000，n @ i_shift = 10.0000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的第四列是 <span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，取负放进 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span>，自由变量位置放 1，得到 <span><span>(−1,−1,1,1)(-1, -1, 1, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>；单位化后是 <span><span>n=(−0.5,−0.5,0.5,0.5)\mathbf{n} = (-0.5, -0.5, 0.5, 0.5)</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>0.5</span><span>,</span><span></span><span>−</span><span>0.5</span><span>,</span><span></span><span>0.5</span><span>,</span><span></span><span>0.5</span><span>)</span></span></span></span>，和那一篇手算的 <span><span>(1,1,−1,−1)/2(1, 1, -1, -1)/2</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span><span>/2</span></span></span></span> 只差一个符号。这就是“一对轮子往一边拧、另一对往另一边拧，合力和合力矩都是零”的电流模式。我把最小范数电流 <span><span>imin⁡\mathbf{i}_{\min}</span><span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>i</span><span>n</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（范数 4.3084）加上 <span><span>10 n10\,\mathbf{n}</span><span><span><span></span><span>10</span><span></span><span>n</span></span></span></span>，合力和合力矩一点没变（<code>np.allclose</code> 为 <code>True</code>），电流向量的范数却涨到 10.8886，是原来的 2.5 倍。多出来的都是白白发热的电流，<span><span>nTi\mathbf{n}^{T}\mathbf{i}</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span><span>T</span></span></span></span></span></span></span></span></span><span>i</span></span></span></span> 从 0 变成 10。</p><p>这就是“零空间电流”的全部数学：给定想要的合力，电流有一条直线的自由度，最小范数的那个解，是这条直线上与 <span><span>n\mathbf{n}</span><span><span><span></span><span>n</span></span></span></span> 垂直的那个点。多变量那一篇里，逐轮截断让这个分量的均方根达到 28.54 A，原因就是截断把电流推向了这条直线的方向。那一篇已经量过，这一篇只补上它的来历：这个方向就是 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的那一列。</p><p>这一讲只做了右端项为零的情况。右端项不为零时，<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 什么时候有解、有几个解，他说留到下一讲。第 6 讲末尾的例子已经显示：解是一个特解加整个零空间。细节见 <a href="/posts/linear-algebra-08-solving-ax-b/">18.06 第 8 讲：解 Ax = b，行最简形 R</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-06-column-space-and-nullspace/</id>
      <title type="text">18.06 第 6 讲：列空间与零空间</title>
      <published>2026-10-18T01:40:00.000Z</published>
      <updated>2026-10-18T01:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-06-column-space-and-nullspace/"/>
      <summary type="text">MIT 18.06 第 6 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 对 Strang 的 4 × 3 矩阵用最小二乘残差判断右端项在不在列空间，再用 SVD 求零空间，1000 个随机右端项里没有一个落在列空间里，而 Ax = b 的全部解是一条到原点距离 0.8165 的直线。</summary>
      <content type="html"><![CDATA[<p>这一讲 Strang 只用一个 4 × 3 的小矩阵，把“解 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>”拆成两个互不相干的子空间：列空间管右端项 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 够不够得着，零空间管解 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 有几个。我用 NumPy 把他心算出来的结论全算了一遍：随机取 1000 个 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，落进列空间的是 0 个；有解的时候，全部解是一条不过原点的直线，到原点的距离是 0.8165。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=8o5Cmfpeo6g" target="_blank">Lecture 6: Column space and nullspace</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-6-column-space-and-nullspace/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §1.3 Matrices and Their Column Spaces；§3.1 Vector Spaces and Subspaces
前置：<a href="/posts/linear-algebra-05-transposes-permutations-spaces/">第 5 讲：转置、置换与向量空间</a>
一句话：<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 有解，当且仅当 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间里；<span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的所有解构成零空间，它是 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span> 里的子空间。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头十来分钟在讲“并集不是子空间、交集是子空间”。他画的平面和直线很直观，我只在第一节做了一次数值检查，你看视频时能跟上就可以倍速。</li>
<li>他让同学心算 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 的那一段（“告诉我一个能解的右端项”）要认真看：这是列空间定义最自然的出场方式。随后他问“哪一列可以扔掉”，主列的说法就是从这里来的。</li>
<li>讲零空间的一段，他让同学报解，从 <span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 一路报到“把所有解都告诉我”，值得完整看一遍。验证零空间是子空间的那几句话很短，本篇第三节用代码逐条对过。</li>
<li>最后他把右端项换成 <span><span>(1,2,3,4)(1, 2, 3, 4)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，指出解集不是子空间，再总结“描述子空间的两种方式”。这一段是全讲的收束，别快进。</li>
</ul></div></div>
<section><h2>并集不是子空间，交集是<a href="#并集不是子空间交集是"><span>#</span></a></h2><p>上一讲留下了子空间的定义：对加法和数乘封闭，合起来就是对线性组合封闭。Strang 先在 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 里画了一个过原点的平面 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 和一条过原点的直线 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span>，问了两个问题。</p><p><strong><span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 与 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的并集是子空间吗？</strong> 不是。平面里取一个向量，直线上取一个向量，加起来一般既不在平面里也不在直线上，加法出了并集。<strong>交集呢？</strong> 交集是子空间，而且对任意两个子空间 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span>、<span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 都成立：<span><span>v\mathbf{v}</span><span><span><span></span><span>v</span></span></span></span>、<span><span>w\mathbf{w}</span><span><span><span></span><span>w</span></span></span></span> 同时在 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 和 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 里，那么 <span><span>v+w\mathbf{v} + \mathbf{w}</span><span><span><span></span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span></span></span></span> 在 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 里（因为 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 是子空间），在 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 里（因为 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 是子空间），所以在交集里；数乘同理。（他举的例子里平面和直线只交于零向量，他问“只有零向量算子空间吗”，答案是算。）</p><p>我用同样的思路各做了一次数值检查。<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 取 <span><span>z=0z = 0</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 平面，<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 取沿 <span><span>(1,1,1)(1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 的直线；交集的例子换成两个平面 <span><span>x+y+z=0x + y + z = 0</span><span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>y</span><span></span><span>+</span><span></span></span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 和 <span><span>x=yx = y</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>y</span></span></span></span>。每组都随机抽 1000 次：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>in_span</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>v</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""判断 v 是否在 M 的列张成的空间里。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>x, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(M, v, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> </span><span>bool</span><span><span>(np.</span><span>allclose</span><span>(M </span><span>@</span><span> x, v))</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 平面 P：z = 0，由 e1、e2 张成；直线 L：沿 (1, 1, 1)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>], [</span><span>1.0</span><span>], [</span><span>1.0</span><span>]])</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 并集：P 里取一个向量，L 里取一个向量，相加后还在 P 或 L 里吗</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>trials </span><span>=</span><span> </span><span>1000</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>stay </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(trials):</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> P </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>w </span><span>=</span><span> L </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> v </span><span>+</span><span> w</span></div></div><div><div><div>25</div></div><div><span>        </span><span>if</span><span><span> </span><span>in_span</span><span>(P, s) </span></span><span>or</span><span><span> </span><span>in_span</span><span>(L, s):</span></span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>stay </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"P 并 L：</span><span>{</span><span>trials</span><span>}</span><span> 次相加，留在并集里的有 </span><span>{</span><span>stay</span><span>}</span><span> 次"</span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span># 交集：S：x + y + z = 0，T：x = y，交集是一条线</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>S_eq </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>T_eq </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>both </span><span>=</span><span> np.</span><span>vstack</span><span>([S_eq, T_eq])</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>_, _, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(both)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> Vt[</span><span>-</span><span>1</span><span><span>] </span><span>/</span><span> Vt[</span><span>-</span></span><span>1</span><span>][</span><span>0</span><span>]  </span><span># 同时满足两个方程的方向</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"S 交 T 的方向："</span><span>, d)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>ok </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>38</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(trials):</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> rng.</span><span>standard_normal</span><span>() </span><span>*</span><span> d</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>w </span><span>=</span><span> rng.</span><span>standard_normal</span><span>() </span><span>*</span><span> d</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> rng.</span><span>standard_normal</span><span>()</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> v </span><span>+</span><span> w</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>t </span><span>=</span><span> c </span><span>*</span><span> v</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>in_both </span><span>=</span><span> np.</span><span>allclose</span><span>(both </span><span>@</span><span> s, </span><span>0</span><span>) </span><span>and</span><span><span> np.</span><span>allclose</span><span>(both </span><span>@</span><span> t, </span></span><span>0</span><span>)</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>ok </span><span>+=</span><span> </span><span>int</span><span>(in_both)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"S 交 T：</span><span>{</span><span>trials</span><span>}</span><span> 次加法与数乘，都还在交集里的有 </span><span>{</span><span>ok</span><span>}</span><span> 次"</span><span>)</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P 并 L：1000 次相加，留在并集里的有 0 次</span></div></div><div><div><div>2</div></div><div><span>S 交 T 的方向： [ 1.  1. -2.]</span></div></div><div><div><div>3</div></div><div><span>S 交 T：1000 次加法与数乘，都还在交集里的有 1000 次</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一组：1000 次相加，没有一次留在并集里（随机浮点数恰好落在平面或直线上的概率是零，这个结果并不意外）。第二组：两个平面的交集是沿 <span><span>(1,1,−2)(1, 1, -2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span></span></span></span> 的一条直线，1000 次加法和数乘全部还在交集里。</p><p>这里的“检查”其实是在复述他那三句话，代码不能代替证明，只是让我确认自己没有读错题。（我也不是很确定把这种检查写成代码有多大必要，但不写的话，这一节就只有字没有数。）</p></section>
<section><h2>列空间：<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 够不够得着<a href="#列空间bmathbfbb-够不够得着"><span>#</span></a></h2><p>接下来是整讲的主角。他的矩阵是</p><span><span><span>A=[112213314415]A = \begin{bmatrix} 1 &amp; 1 &amp; 2 \\ 2 &amp; 1 &amp; 3 \\ 3 &amp; 1 &amp; 4 \\ 4 &amp; 1 &amp; 5 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p><strong>列空间是什么？</strong> <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间 <span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 是三列的所有线性组合。三列各自当然在里面，但三个向量不构成子空间，必须把它们的组合都放进来；这是包含这三列的最小的子空间。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是 4 × 3，每一列有 4 个分量，所以 <span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 是 <span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 的子空间。</p><p>他立刻问：这个子空间会填满整个 <span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 吗？同学说不会。三个向量的组合凑不满四维空间，对应到方程组就是四个方程只有三个未知数，大多数 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 没有解。（他自己替旁观者说了一句“你这个笨蛋，四个方程三个未知数怎么解”，然后话锋一转：有些右端项就是能解。）</p><p><strong>哪些右端项能解？</strong> 他让同学报：全零当然行，<span><span>x=0\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。<span><span>(1,2,3,4)(1, 2, 3, 4)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 行，它是第一列，<span><span>x=(1,0,0)\mathbf{x} = (1, 0, 0)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。<span><span>(1,1,1,1)(1, 1, 1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 行，它是第二列，<span><span>x=(0,1,0)\mathbf{x} = (0, 1, 0)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。然后他给出本讲的第一个结论：</p><span><span><span>Ax=b 有解  ⟺  b∈C(A)A\mathbf{x} = \mathbf{b} \text{ 有解} \iff \mathbf{b} \in C(A)</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span><span> </span><span>有解</span></span><span></span><span></span><span>⟺</span><span></span><span></span></span><span><span></span><span>b</span><span></span><span>∈</span><span></span></span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span></span><p>理由一句话：<span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 的定义就是所有 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 的集合。<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 是列的组合，这个组合的系数就是 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>；不是列的组合，就没有 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>。</p><p>怎么在 NumPy 里判断？我用两种办法。第一种是我在第 1 篇用过的最小二乘残差：<code>np.linalg.lstsq</code> 找离 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 最近的 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span>，残差是 0 就说明 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在列空间里。第二种是把 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 拼到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 右边，看秩有没有涨：秩不变，说明新列没有带来新方向。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>3.0</span><span>, </span><span>1.0</span><span>, </span><span>4.0</span><span>], [</span><span>4.0</span><span>, </span><span>1.0</span><span>, </span><span>5.0</span><span>]]</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"A 是"</span><span>, A.shape[</span><span>0</span><span>], </span><span>"x"</span><span>, A.shape[</span><span>1</span><span>], </span><span>"，秩 ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A)</span></span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"第三列 = 第一列 + 第二列："</span><span><span>, np.</span><span>allclose</span><span>(A[:, </span></span><span>2</span><span>], A[:, </span><span>0</span><span><span>] </span><span>+</span><span> A[:, </span></span><span>1</span><span>])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 扔掉哪一列，列空间都没变</span></div></div><div><div><div>19</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>):</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>rest </span><span>=</span><span> np.</span><span>delete</span><span>(A, j, </span><span>axis</span><span>=</span><span>1</span><span>)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"扔掉第 </span><span>{</span><span><span>j </span><span>+</span><span> </span></span><span>1}</span><span> 列后的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(rest))</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># b 在不在列空间：最小二乘残差 + 秩检验</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>b_cases </span><span>=</span><span> {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>"b = 第一列"</span><span>: A[:, </span><span>0</span><span>],</span></div></div><div><div><div>26</div></div><div><span>        </span><span>"b = (1, 1, 1, 1)"</span><span><span>: np.</span><span>ones</span><span>(</span></span><span>4</span><span>),</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"b = A @ 随机 x"</span><span><span>: A </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(</span></span><span>3</span><span>),</span></div></div><div><div><div>28</div></div><div><span>        </span><span>"b = 随机向量"</span><span><span>: rng.</span><span>standard_normal</span><span>(</span></span><span>4</span><span>),</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>    </span><span>for</span><span> name, b </span><span>in</span><span><span> b_cases.</span><span>items</span><span>():</span></span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>x, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>residual </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> x </span><span>-</span><span> b)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>rank_aug </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(np.</span><span>column_stack</span><span>([A, b]))</span></div></div><div><div><div>34</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：残差 = </span><span>{</span><span>residual</span><span>:.4f</span><span>}</span><span>，[A b] 的秩 = </span><span>{</span><span>rank_aug</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>    </span><span># 随机 b 里有多少个落在列空间里</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>trials </span><span>=</span><span> </span><span>1000</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>inside </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>39</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(trials):</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>b </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>x, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, b, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>inside </span><span>+=</span><span> </span><span>int</span><span><span>(np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, b))</span></span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>trials</span><span>}</span><span> 个随机 b 里，在列空间里的有 </span><span>{</span><span>inside</span><span>}</span><span> 个"</span><span>)</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 是 4 x 3 ，秩 = 2</span></div></div><div><div><div>2</div></div><div><span>第三列 = 第一列 + 第二列： True</span></div></div><div><div><div>3</div></div><div><span>扔掉第 1 列后的秩： 2</span></div></div><div><div><div>4</div></div><div><span>扔掉第 2 列后的秩： 2</span></div></div><div><div><div>5</div></div><div><span>扔掉第 3 列后的秩： 2</span></div></div><div><div><div>6</div></div><div><span>b = 第一列：残差 = 0.0000，[A b] 的秩 = 2</span></div></div><div><div><div>7</div></div><div><span>b = (1, 1, 1, 1)：残差 = 0.0000，[A b] 的秩 = 2</span></div></div><div><div><div>8</div></div><div><span>b = A @ 随机 x：残差 = 0.0000，[A b] 的秩 = 2</span></div></div><div><div><div>9</div></div><div><span>b = 随机向量：残差 = 1.5314，[A b] 的秩 = 3</span></div></div><div><div><div>10</div></div><div><span>1000 个随机 b 里，在列空间里的有 0 个</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个“可解”的右端项残差都是 0.0000，<span><span>[A b][A\ \mathbf{b}]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>b</span><span>]</span></span></span></span> 的秩仍然是 2；随机向量的残差是 1.5314，<span><span>[A b][A\ \mathbf{b}]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>b</span><span>]</span></span></span></span> 的秩涨到 3。这个 1.5314 就是那个随机 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 到 <span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 的距离。最后一行是整个实验里最干脆的数：1000 个随机 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 里，落进列空间的有 0 个。二维的平面在四维空间里测度为零，这不是比喻，是实打实的 0 / 1000。</p><p><strong>为什么秩是 2？</strong> Strang 接着问：三列里能不能扔掉一列、列空间还是原来那个？同学说扔第三列，因为第三列是前两列之和。他承认，其实扔第一列也行，只是约定“从左往右，能留就留”，留下来的叫主列，没留下的以后叫相关列。所以 <span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 是 <span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 里的一个二维子空间。代码里三种扔法的秩全是 2，第三列等于前两列之和也验证了。他在这里很小心地说“独立”“维数”这些词还不能正式用，“你会看出来我们要往那个方向走”。这就是第 9 讲的内容，我先不展开。</p></section>
<section><h2>零空间：<span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 有几个<a href="#零空间xmathbfxx-有几个"><span>#</span></a></h2><p><strong>零空间是什么？</strong> 换一个角度。列空间里装的是右端项 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，零空间 <span><span>N(A)N(A)</span><span><span><span></span><span>N</span><span>(</span><span>A</span><span>)</span></span></span></span> 里装的是未知数 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>：所有满足 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 列，<span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 就有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个分量，所以对 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的矩阵，<span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 在 <span><span>Rm\mathbb{R}^m</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span></span></span></span></span></span></span></span> 里，<span><span>N(A)N(A)</span><span><span><span></span><span>N</span><span>(</span><span>A</span><span>)</span></span></span></span> 在 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span> 里。这个例子里是 <span><span>R4\mathbb{R}^4</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 对 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>。</p><p>他先让同学找解。零向量是解，不用看矩阵就知道。然后有人报 <span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>：第一列加第二列减第三列，正好是零。再要一个，有人报 <span><span>(2,2,−2)(2, 2, -2)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span></span></span></span>。他说“那你干脆告诉我全部”，答案是 <span><span>(c,c,−c)(c, c, -c)</span><span><span><span></span><span>(</span><span>c</span><span>,</span><span></span><span>c</span><span>,</span><span></span><span>−</span><span>c</span><span>)</span></span></span></span>，<span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 取任意数。零向量是 <span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 是 <span><span>c=1c = 1</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。这是 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 里一条过原点的直线。这个例子小，所以零空间一眼就能看出是一条线。</p><p>大一点的矩阵靠看是不行的，他说“官方的办法”是消元，下一讲才讲。我这里用 SVD 代替：奇异值为零的那些右奇异向量就是零空间的一组基。SVD 在第 29 讲才登场，我暂时只把它当成一个黑盒，它给出的向量和他心算的是同一条直线：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>3.0</span><span>, </span><span>1.0</span><span>, </span><span>4.0</span><span>], [</span><span>4.0</span><span>, </span><span>1.0</span><span>, </span><span>5.0</span><span>]]</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>zero </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 他心算出来的：(1, 1, -1) 以及它的倍数</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> c </span><span>in</span><span> [</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>7.0</span><span><span>, </span><span>-</span></span><span>11.0</span><span>]:</span></div></div><div><div><div>15</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"A @ (</span><span>{</span><span>c</span><span>}</span><span> * v) ="</span><span><span>, A </span><span>@</span><span> (c </span><span>*</span><span> v))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 用 SVD 找零空间：奇异值为 0 的右奇异向量</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>_, s, Vt </span><span>=</span><span> np.linalg.</span><span>svd</span><span>(A)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>tol </span><span>=</span><span> s.</span><span>max</span><span>() </span><span>*</span><span> </span><span>max</span><span><span>(A.shape) </span><span>*</span><span> np.</span><span>finfo</span><span>(</span></span><span>float</span><span>).eps</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>rank </span><span>=</span><span> </span><span>int</span><span><span>(np.</span><span>sum</span><span>(s </span><span>&gt;</span><span> tol))</span></span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>N </span><span>=</span><span> Vt[rank:].T  </span><span># 每一列是零空间的一个基向量</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"奇异值："</span><span>, s)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩 ="</span><span>, rank, </span><span>"，零空间维数 ="</span><span>, N.shape[</span><span>1</span><span>])</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"基向量缩放到第一个分量为 1："</span><span>, (N[:, </span><span>0</span><span><span>] </span><span>/</span><span> N[</span></span><span>0</span><span>, </span><span>0</span><span>]))</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"A @ N 的最大绝对值：</span><span>{</span><span><span>np.</span><span>abs</span><span>(A </span><span>@</span><span> N).</span><span>max</span><span>()</span></span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 两条封闭性要求，对零空间里的 v、w 逐条验证</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> </span><span>-</span><span>3.0</span><span><span> </span><span>*</span><span> v</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ (v + w) = A @ v + A @ w："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> (v </span><span>+</span><span> w), zero))</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ (12 * v) = 12 * (A @ v)："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> (</span></span><span>12</span><span><span> </span><span>*</span><span> v), zero))</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 列空间在 R^4，零空间在 R^3：秩 + 零空间维数 = 列数</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩 + 零空间维数 ="</span><span><span>, rank </span><span>+</span><span> N.shape[</span></span><span>1</span><span>], </span><span>"，列数 ="</span><span>, A.shape[</span><span>1</span><span>])</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A @ (1.0 * v) = [0. 0. 0. 0.]</span></div></div><div><div><div>2</div></div><div><span>A @ (2.0 * v) = [0. 0. 0. 0.]</span></div></div><div><div><div>3</div></div><div><span>A @ (7.0 * v) = [0. 0. 0. 0.]</span></div></div><div><div><div>4</div></div><div><span>A @ (-11.0 * v) = [0. 0. 0. 0.]</span></div></div><div><div><div>5</div></div><div><span>奇异值： [9.3441 0.829  0.    ]</span></div></div><div><div><div>6</div></div><div><span>秩 = 2 ，零空间维数 = 1</span></div></div><div><div><div>7</div></div><div><span>基向量缩放到第一个分量为 1： [ 1.  1. -1.]</span></div></div><div><div><div>8</div></div><div><span>A @ N 的最大绝对值：6.7e-16</span></div></div><div><div><div>9</div></div><div><span>A @ (v + w) = A @ v + A @ w： True</span></div></div><div><div><div>10</div></div><div><span>A @ (12 * v) = 12 * (A @ v)： True</span></div></div><div><div><div>11</div></div><div><span>秩 + 零空间维数 = 3 ，列数 = 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前四行是他心算的结果：<span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 的 1、2、7、<span><span>−11-11</span><span><span><span></span><span>−</span><span>11</span></span></span></span> 倍都被 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 映成零向量。奇异值是 9.3441、0.8290 和一个 0，两个非零，所以秩是 2，剩下的一个右奇异向量就是零空间的基，缩放到第一个分量为 1 之后正好是 <span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>。<code>A @ N</code> 的最大绝对值是 6.7e-16，浮点数意义上的零。最后一行：秩 2 加零空间维数 1 等于列数 3，这是第 10 讲会证明的一般结论，我先把它记在这儿。</p><p><strong>凭什么叫“空间”？</strong> 他回到定义，把两条要求检查了一遍。设 <span><span>Av=0A\mathbf{v} = \mathbf{0}</span><span><span><span></span><span>A</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>Aw=0A\mathbf{w} = \mathbf{0}</span><span><span><span></span><span>A</span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，那么</p><span><span><span>A(v+w)=Av+Aw=0+0=0,A(12v)=12 Av=0A(\mathbf{v} + \mathbf{w}) = A\mathbf{v} + A\mathbf{w} = \mathbf{0} + \mathbf{0} = \mathbf{0}, \qquad
A(12\mathbf{v}) = 12\,A\mathbf{v} = \mathbf{0}</span><span><span><span></span><span>A</span><span>(</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>A</span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span></span><span>+</span><span></span></span><span><span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span></span><span>A</span><span>(</span><span>12</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span><span></span><span>A</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>他承认这个证明简单得有点荒谬（说完又收回“荒谬”这个词，说那样讲不太好），而且用的是矩阵乘法的分配律与标量可以提到外面，没有证明就直接用了，“人生有限，这个证明我们跳过”。代码里最后三行对 <span><span>v=(1,1,−1)\mathbf{v} = (1, 1, -1)</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span> 和 <span><span>w=−3v\mathbf{w} = -3\mathbf{v}</span><span><span><span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3</span><span>v</span></span></span></span> 验证了这两条，都是 True。</p></section>
<section><h2>右端项不为零时，解集不是子空间<a href="#右端项不为零时解集不是子空间"><span>#</span></a></h2><p>最后是整讲我最喜欢的一个对比。把右端项换成 <span><span>b=(1,2,3,4)\mathbf{b} = (1, 2, 3, 4)</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>。上面已经知道它有解：<span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。解的全体是不是子空间？</p><p>他先问是或不是，答案是不是，一句话就够：零向量不解这个方程，所以解集不过原点，不可能是子空间。接着同学报了第二个解 <span><span>(0,−1,1)(0, -1, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>：减去第二列、加上第三列，<span><span>−(1,1,1,1)+(2,3,4,5)=(1,2,3,4)-(1, 1, 1, 1) + (2, 3, 4, 5) = (1, 2, 3, 4)</span><span><span><span></span><span>−</span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>5</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>。他说解集“像一条不过原点的直线”。我猜他特意挑 <span><span>(1,2,3,4)(1, 2, 3, 4)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 当右端项，就是因为它恰好是第一列，同学能马上报出解。我把这几件事都算出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>[[</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>3.0</span><span>, </span><span>1.0</span><span>, </span><span>4.0</span><span>], [</span><span>4.0</span><span>, </span><span>1.0</span><span>, </span><span>5.0</span><span>]]</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>4.0</span><span>])</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>x1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])  </span><span># 学生一眼看出来的</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>x2 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>])  </span><span># 第二个解</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])  </span><span># 零空间里的方向</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x1 ="</span><span><span>, A </span><span>@</span><span> x1, </span></span><span>"，是解："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x1, b))</span></span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x2 ="</span><span><span>, A </span><span>@</span><span> x2, </span></span><span>"，是解："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x2, b))</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"x1 - x2 ="</span><span><span>, x1 </span><span>-</span><span> x2, </span></span><span>"，在零空间里："</span><span><span>, np.</span><span>allclose</span><span>(x1 </span><span>-</span><span> x2, n))</span></span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 零向量不是解；两个解相加得到的是 2b，不是 b</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>zero_is_solution </span><span>=</span><span> np.</span><span>allclose</span><span>(A </span><span>@</span><span> np.</span><span>zeros</span><span>(</span><span>3</span><span>), b)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ 0 ="</span><span><span>, A </span><span>@</span><span> np.</span><span>zeros</span><span>(</span></span><span>3</span><span>), </span><span>"，是解："</span><span>, zero_is_solution)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ (x1 + x2) ="</span><span><span>, A </span><span>@</span><span> (x1 </span><span>+</span><span> x2), </span></span><span>"，是 2b："</span><span>, </span><span>end</span><span>=</span><span>" "</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span><span>(np.</span><span>allclose</span><span>(A </span><span>@</span><span> (x1 </span><span>+</span><span> x2), </span></span><span>2</span><span><span> </span><span>*</span><span> b))</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 全部解 = 一个特解加零空间：x1 + c * n，一条不过原点的直线</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>ok </span><span>=</span><span> </span><span>all</span><span><span>(np.</span><span>allclose</span><span>(A </span><span>@</span><span> (x1 </span><span>+</span><span> c </span><span>*</span><span> n), b) </span></span><span>for</span><span> c </span><span>in</span><span><span> np.</span><span>linspace</span><span>(</span><span>-</span></span><span>5</span><span>, </span><span>5</span><span>, </span><span>11</span><span>))</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"x1 + c * n 对 11 个 c 都是解："</span><span>, ok)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>dist </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(x1 </span><span>-</span><span> (x1 </span><span>@</span><span> n) </span><span>/</span><span> (n </span><span>@</span><span> n) </span><span>*</span><span> n)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"这条直线到原点的距离：</span><span>{</span><span>dist</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A @ x1 = [1. 2. 3. 4.] ，是解： True</span></div></div><div><div><div>2</div></div><div><span>A @ x2 = [1. 2. 3. 4.] ，是解： True</span></div></div><div><div><div>3</div></div><div><span>x1 - x2 = [ 1.  1. -1.] ，在零空间里： True</span></div></div><div><div><div>4</div></div><div><span>A @ 0 = [0. 0. 0. 0.] ，是解： False</span></div></div><div><div><div>5</div></div><div><span>A @ (x1 + x2) = [2. 4. 6. 8.] ，是 2b： True</span></div></div><div><div><div>6</div></div><div><span>x1 + c * n 对 11 个 c 都是解： True</span></div></div><div><div><div>7</div></div><div><span>这条直线到原点的距离：0.8165</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个解的差是 <span><span>(1,1,−1)(1, 1, -1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，正好在零空间里；零向量不是解；两个解相加得到的是 <span><span>2b2\mathbf{b}</span><span><span><span></span><span>2</span><span>b</span></span></span></span>，不是 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，加法没有封闭。更一般地，<span><span>x1+c n\mathbf{x}_1 + c\,\mathbf{n}</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span></span><span>n</span></span></span></span> 对 11 个不同的 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 全部都是解。所以解集是“一个特解加上整个零空间”：一条平行于零空间那条线、但整体平移了的直线。这条直线到原点的距离是 0.8165（也就是 <span><span>6/3\sqrt{6}/3</span><span><span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>/3</span></span></span></span>，垂足是 <span><span>x1\mathbf{x}_1</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 去掉沿 <span><span>n\mathbf{n}</span><span><span><span></span><span>n</span></span></span></span> 的分量之后剩下的 <span><span>(23,−13,13)(\tfrac23, -\tfrac13, \tfrac13)</span><span><span><span></span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>)</span></span></span></span>），这就是它没过原点的具体量。<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 的完整解他留到第 8 讲，下一讲先系统地算 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</p></section>
<section><h2>描述子空间的两种方式<a href="#描述子空间的两种方式"><span>#</span></a></h2><p>他用最后几分钟把两个子空间并在一起说：描述一个子空间有两种自然的办法。一种是给几个向量，说“取它们的所有组合”，列空间就是这样；另一种是给一组方程，说“满足这些方程的所有向量”，零空间就是这样。两种办法都能造出子空间，他说这是最重要的两种构造方式。</p><p>我觉得这像一份菜谱的两种写法：一种写“用这些食材随便搭配”，另一种写“只要不含这些忌口的都可以”。（这个类比我就用这一次。）但数学里这两种写法不是对立的，同一个子空间往往两种写法都有，从一种换到另一种就是后面几讲的主题。</p><p>这一讲留下了一个明确的任务：小例子可以靠眼睛看零空间，大例子不行。下一讲他要用消元来系统地算零空间，找出主变量和特解，见 <a href="/posts/linear-algebra-07-solving-ax-0/">18.06 第 7 讲：解 Ax = 0，主变量与特解</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-05-transposes-permutations-spaces/</id>
      <title type="text">18.06 第 5 讲：转置、置换与向量空间</title>
      <published>2026-10-18T01:20:00.000Z</published>
      <updated>2026-10-18T01:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-05-transposes-permutations-spaces/"/>
      <summary type="text">MIT 18.06 第 5 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 数出 4 × 4 置换矩阵共 24 个、手写了带换行的 PA = LU，又在 1e-20 的小主元上看到不换行的解误差是 1.0，换行之后降到 0.0。</summary>
      <content type="html"><![CDATA[<p>这一讲前半段收尾第二章，后半段开始第三章，Strang 说那是“真正的线性代数”的开头：不再只盯着几个向量，而是盯着它们所有线性组合构成的空间。收尾的部分有个我没想到的数字：对 <span><span>[10−20111]\begin{bmatrix} 10^{-20} &amp; 1 \\ 1 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>20</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 这样一个代数上毫无问题的矩阵，不换行消元得到的解 <span><span>(0,1)(0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 离真解 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 差 1.0，先换一次行，误差是 0.0。这就是他随口提到的“Matlab 嫌主元太小”的代价。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=JibVXBElKL0" target="_blank">Lecture 5: Transposes, permutations, spaces R^n</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-5-transposes-permutations-spaces-r-n/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §2.4 Permutations and Transposes；§3.1 Vector Spaces and Subspaces
前置：<a href="/posts/linear-algebra-04-lu-factorization/">第 4 讲：分解成 A = LU</a>
一句话：需要换行时 <span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>；子空间就是对加法和数乘封闭、且必然含有零向量的向量集合，矩阵的列的所有组合就是一个子空间。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头一大段是置换矩阵和转置的收尾，本篇前两节都算过了，倍速即可。他顺口提到 Matlab 会因为主元太小而换行的那几句，我在第三个代码块里做了实验，值得听一遍再看数字。</li>
<li>从“开始讲第三章”往后要认真看：他为什么说“把原点挖掉就不是向量空间”，为什么第一象限不行，为什么不过原点的直线不行。这几个反例比定义本身更有用。</li>
<li>R² 的所有子空间（整个平面、过原点的直线、只有零向量）和 R³ 的类似列表，一边看一边自己默写一遍。</li>
<li>最后几分钟他画列空间：两个列向量的所有组合填满一个过原点的平面。本篇最后一个代码块把这个平面的方程算出来了，看完视频再看。</li>
</ul></div></div>
<section><h2>置换：换行的记账<a href="#置换换行的记账"><span>#</span></a></h2><p>上一讲的 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 有一个隐含的前提：消元过程中主元位置上永远不是 0，不用换行。现实里主元可能是 0，这时从下面找一行不为零的换上来，继续。Strang 的做法是把所有换行集中记在一个矩阵 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 里，于是 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 变成</p><span><span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span></span><p><span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的行先排成“好顺序”，之后消元就不会再遇到零主元，<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 和 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的形状照旧。他说对绝大多数矩阵 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 就是单位矩阵，但总有那么一小撮需要它。对任意可逆的 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，这个分解都存在。</p><p><strong>置换矩阵是什么？</strong> 把单位矩阵的行重新排一遍（包括“原样不动”）。<span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的置换矩阵有 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 个，因为 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 行有 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 种排法。他当场数了 4 × 4 的：24 个，5 × 5 的 120 个，说“把五阶的全列出来就没意思了”。我没有手写，用 <code>itertools.permutations</code> 生成再逐个检验：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> itertools</span></div></div><div><div><div>2</div></div><div><span>import</span><span> math</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>def</span><span> </span><span>perm_matrix</span><span>(</span><span>order</span><span>:</span><span> tuple[</span><span>int</span><span>, </span><span>...</span><span>]</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"""按 order 重排单位矩阵的行。"""</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>eye</span><span>(</span></span><span>len</span><span>(order))[</span><span>list</span><span>(order)]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># n x n 置换矩阵的个数是 n!</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>6</span><span>):</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>count </span><span>=</span><span> </span><span>sum</span><span>(</span><span>1</span><span> </span><span>for</span><span> _ </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(n)))</span></div></div><div><div><div>18</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"n = </span><span>{</span><span>n</span><span>}</span><span>：</span><span>{</span><span>count</span><span>}</span><span> 个，n! = </span><span>{</span><span><span>math.</span><span>factorial</span><span>(n)</span></span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span># 4 x 4 的 24 个置换矩阵：P^T P = 单位矩阵，所以 P^T 就是 P 的逆</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>perms </span><span>=</span><span> [</span><span>perm_matrix</span><span>(p) </span><span>for</span><span> p </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(</span><span>4</span><span>))]</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>24</div></div><div><span>        </span><span>"P^T P = 单位矩阵："</span><span>,</span></div></div><div><div><div>25</div></div><div><span>        </span><span>all</span><span><span>(np.</span><span>allclose</span><span>(P.T </span><span>@</span><span> P, identity) </span></span><span>for</span><span> P </span><span>in</span><span> perms),</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>28</div></div><div><span>        </span><span>"P^T 等于 inv(P)："</span><span>,</span></div></div><div><div><div>29</div></div><div><span>        </span><span>all</span><span><span>(np.</span><span>allclose</span><span>(P.T, np.linalg.</span><span>inv</span><span>(P)) </span></span><span>for</span><span> P </span><span>in</span><span> perms),</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span># 两个置换矩阵相乘，结果还是置换矩阵：24 个里没有跑出去的</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>keys </span><span>=</span><span> {</span><span>tuple</span><span><span>(P.</span><span>flatten</span><span>()) </span></span><span>for</span><span> P </span><span>in</span><span> perms}</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>closed </span><span>=</span><span> </span><span>all</span><span>(</span></div></div><div><div><div>35</div></div><div><span>        </span><span>tuple</span><span><span>((P </span><span>@</span><span> Q).</span><span>flatten</span><span>()) </span></span><span>in</span><span> keys </span><span>for</span><span> P </span><span>in</span><span> perms </span><span>for</span><span> Q </span><span>in</span><span> perms</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"任意两个相乘仍在这 24 个里："</span><span>, closed)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span># 那个绕一圈的置换：1 到 2，2 到 3，3 到 4，4 到 1</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>cycle </span><span>=</span><span> </span><span>perm_matrix</span><span>((</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>0</span><span>))</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(cycle)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>43</div></div><div><span>        </span><span>"连乘 4 次回到单位矩阵："</span><span>,</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(np.linalg.</span><span>matrix_power</span><span>(cycle, </span><span>4</span><span>), identity),</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>n = 1：1 个，n! = 1</span></div></div><div><div><div>2</div></div><div><span>n = 2：2 个，n! = 2</span></div></div><div><div><div>3</div></div><div><span>n = 3：6 个，n! = 6</span></div></div><div><div><div>4</div></div><div><span>n = 4：24 个，n! = 24</span></div></div><div><div><div>5</div></div><div><span>n = 5：120 个，n! = 120</span></div></div><div><div><div>6</div></div><div><span>P^T P = 单位矩阵： True</span></div></div><div><div><div>7</div></div><div><span>P^T 等于 inv(P)： True</span></div></div><div><div><div>8</div></div><div><span>任意两个相乘仍在这 24 个里： True</span></div></div><div><div><div>9</div></div><div><span>[[0. 1. 0. 0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0. 0. 1. 0.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[0. 0. 0. 1.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[1. 0. 0. 0.]]</span></div></div><div><div><div>13</div></div><div><span>连乘 4 次回到单位矩阵： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>置换矩阵的逆就是它的转置：<span><span>PTP=IP^\mathsf{T}P = I</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，因为每一行的那个 1 恰好撞上转置之后同一位置的那个 1。24 个矩阵全部通过，而且 <code>P.T</code> 与 <code>np.linalg.inv(P)</code> 一致。两个置换矩阵相乘还是置换矩阵，24 个里没有一个跑到外面去，他说它们像“一个小群”，位于特殊矩阵的中心。绕一圈的那个置换（1 到 2，2 到 3，3 到 4，4 到 1，他口头举过这个例子）连乘四次回到单位矩阵。</p><p>他还指出满足 <span><span>QTQ=IQ^\mathsf{T}Q = I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 的矩阵不止置换矩阵（旋转矩阵也是），这个话题后面第 17 讲会正式展开。</p><p>一个类比，只用这一次：置换矩阵就像洗牌。每一种洗法对应一个置换矩阵，洗完牌想复原，把洗牌的动作倒过来做一遍就行，而这个“倒过来”恰好就是转置，不需要求逆，不需要任何计算。这也是 <span><span>P−1=PTP^{-1} = P^\mathsf{T}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 在数值计算里受欢迎的原因：它免费。</p></section>
<section><h2>转置与对称<a href="#转置与对称"><span>#</span></a></h2><p><strong>转置怎么算？</strong> 行变列，列变行。他在黑板上取了一个 3 × 2 的矩阵，两列分别是 <span><span>(1,2,4)(1, 2, 4)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 和 <span><span>(3,3,1)(3, 3, 1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，把它记作 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span>（rectangular 的 R）：</p><span><span><span>R=[132341],RT=[124331]R = \begin{bmatrix} 1 &amp; 3 \\ 2 &amp; 3 \\ 4 &amp; 1 \end{bmatrix},\quad
R^\mathsf{T} = \begin{bmatrix} 1 &amp; 2 &amp; 4 \\ 3 &amp; 3 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>4</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>3 × 2 变成 2 × 3，“又矮又宽”。用符号写就是 <span><span>(AT)ij=Aji(A^\mathsf{T})_{ij} = A_{ji}</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：行号和列号对调。</p><p><strong>对称矩阵是什么？</strong> 转置后不变的矩阵，<span><span>AT=AA^\mathsf{T} = A</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span></span></span></span>。比如对角线上随便填，对角线上方有个 1，下方对称的位置就必须也是 1。他说这类矩阵在应用里到处都是，原因是下面这个事实：<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 本身离对称很远（形状都不一样），但 <span><span>RTRR^\mathsf{T}R</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span></span></span></span> 永远是对称的。</p><p>他的验证分两步。先用数字：算出来的 10、11、7 都在对称的位置上重复出现。（字幕里他算的那一排 10、11、7，在我这里是 <span><span>RRTRR^\mathsf{T}</span><span><span><span></span><span>R</span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的第一行，<span><span>RTRR^\mathsf{T}R</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span></span></span></span> 是 2 × 2 的 <span><span>[21131319]\begin{bmatrix} 21 &amp; 13 \\ 13 &amp; 19 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>21</span></span></span><span><span></span><span><span>13</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>13</span></span></span><span><span></span><span><span>19</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。两个都对称，他选的哪一个都不影响结论。）再用符号：转置要反序，</p><span><span><span>(RTR)T=RT(RT)T=RTR(R^\mathsf{T}R)^\mathsf{T} = R^\mathsf{T}(R^\mathsf{T})^\mathsf{T} = R^\mathsf{T}R</span><span><span><span></span><span>(</span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span></span></span></span></span><p>两行就证完了，和逆矩阵的 <span><span>(AB)−1=B−1A−1(AB)^{-1} = B^{-1}A^{-1}</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 是同一种反序。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 他黑板上的 3 x 2 矩阵 R：两列分别是 (1, 2, 4) 和 (3, 3, 1)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>3.0</span><span>], [</span><span>4.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"R 的形状："</span><span>, R.shape, </span><span>"，R^T 的形状："</span><span>, R.T.shape)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(R.T)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># (R^T)[i, j] 就是 R[j, i]</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>flipped </span><span>=</span><span> </span><span>all</span><span><span>(R.T[i, j] </span><span>==</span><span> R[j, i] </span></span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>))</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"行列下标对调："</span><span>, flipped)</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># R^T R 与 R R^T 都是对称的</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"R^T R ="</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span><span>(R.T </span><span>@</span><span> R)</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"R R^T ="</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span><span>(R </span><span>@</span><span> R.T)</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"两个都对称："</span><span><span>, np.</span><span>allclose</span><span>(R.T </span><span>@</span><span> R, (R.T </span><span>@</span><span> R).T), </span></span><span>end</span><span>=</span><span>" "</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span><span>(np.</span><span>allclose</span><span>(R </span><span>@</span><span> R.T, (R </span><span>@</span><span> R.T).T))</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 随机的矩阵也一样：对称性是结构带来的，不是这组数字碰巧</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>7</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>G </span><span>=</span><span> M.T </span><span>@</span><span> M</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"随机 7 x 4 的 M^T M 不对称程度："</span><span><span>, np.</span><span>abs</span><span>(G </span><span>-</span><span> G.T).</span><span>max</span><span>())</span></span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># 转置反序：(AB)^T = B^T A^T，而 A^T B^T 连形状都对不上</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>3</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>2</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"(AB)^T = B^T A^T："</span><span><span>, np.</span><span>allclose</span><span>((A </span><span>@</span><span> B).T, B.T </span><span>@</span><span> A.T))</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>try</span><span>:</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>A.T </span><span>@</span><span> B.T</span></div></div><div><div><div>36</div></div><div><span>    </span><span>except</span><span> </span><span>ValueError</span><span>:</span></div></div><div><div><div>37</div></div><div><span>        </span><span>print</span><span>(</span><span>"A^T B^T：形状 (2, 3) 乘 (5, 2)，乘不了"</span><span>)</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>R 的形状： (3, 2) ，R^T 的形状： (2, 3)</span></div></div><div><div><div>2</div></div><div><span>[[1. 2. 4.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[3. 3. 1.]]</span></div></div><div><div><div>4</div></div><div><span>行列下标对调： True</span></div></div><div><div><div>5</div></div><div><span>R^T R =</span></div></div><div><div><div>6</div></div><div><span>[[21. 13.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[13. 19.]]</span></div></div><div><div><div>8</div></div><div><span>R R^T =</span></div></div><div><div><div>9</div></div><div><span>[[10. 11.  7.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[11. 13. 11.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 7. 11. 17.]]</span></div></div><div><div><div>12</div></div><div><span>两个都对称： True True</span></div></div><div><div><div>13</div></div><div><span>随机 7 x 4 的 M^T M 不对称程度： 0.0</span></div></div><div><div><div>14</div></div><div><span>(AB)^T = B^T A^T： True</span></div></div><div><div><div>15</div></div><div><span>A^T B^T：形状 (2, 3) 乘 (5, 2)，乘不了</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>RTRR^\mathsf{T}R</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span></span></span></span> 和 <span><span>RRTRR^\mathsf{T}</span><span><span><span></span><span>R</span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 都对称。随机 7 × 4 矩阵的 <span><span>MTMM^\mathsf{T}M</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>M</span></span></span></span> 与它自己的转置的最大差在这次运行里是 0.0，不是“近似为 0”，而是逐元素完全相等：<span><span>(MTM)ij(M^\mathsf{T}M)_{ij}</span><span><span><span></span><span>(</span><span><span>M</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>M</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与 <span><span>(MTM)ji(M^\mathsf{T}M)_{ji}</span><span><span><span></span><span>(</span><span><span>M</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>M</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>j</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是同样两列的点积。最后两行是转置反序：<span><span>(AB)T=BTAT(AB)^\mathsf{T} = B^\mathsf{T}A^\mathsf{T}</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 成立，而不反序的 <span><span>ATBTA^\mathsf{T}B^\mathsf{T}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 连形状都对不上，NumPy 直接报错。</p><p>这个“<span><span>RTRR^\mathsf{T}R</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span></span></span></span> 必定对称”的事实后面会反复出现：最小二乘的正规方程、协方差矩阵、奇异值分解，据我所知都是从这个乘积开始的。Strang 的原话大意是：实际应用里的对称矩阵，很多就是这样来的。我觉得这是这一讲前半段最值得记住的一句。</p></section>
<section><h2>换行的另一面：主元太小<a href="#换行的另一面主元太小"><span>#</span></a></h2><p>回到 <span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>。先在一个第一个主元为 0 的矩阵上走一遍：</p><span><span><span>A=[011121279]A = \begin{bmatrix} 0 &amp; 1 &amp; 1 \\ 1 &amp; 2 &amp; 1 \\ 2 &amp; 7 &amp; 9 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>下面的脚本手写了“能不换就不换，主元为零才换”的消元，同一个块里验证 <span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>。</p><p>他在讲这一部分时顺嘴提了一句：Matlab 不光检查主元是不是 0，还检查主元是不是“够大”，因为接近 0 的主元在数值上很糟。代数上不需要这些换行，精度需要。我想看看“很糟”有多糟，就用 <span><span>[10−20111]x=(1,2)\begin{bmatrix} 10^{-20} &amp; 1 \\ 1 &amp; 1 \end{bmatrix}\mathbf{x} = (1, 2)</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>20</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 试了一下：不换行，乘子是 <span><span>102010^{20}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>u22=1−1020u_{22} = 1 - 10^{20}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span></span></span></span> 在双精度里就是 <span><span>−1020-10^{20}</span><span><span><span></span><span>−</span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span></span></span></span>，那个 1 被吃掉了；换行之后乘子是 <span><span>10−2010^{-20}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>20</span></span></span></span></span></span></span></span></span></span></span></span>，什么都没丢。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>lu_with_exchanges</span><span>(</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-12</span></span></div></div><div><div><div>6</div></div><div><span>) -&gt; tuple[np.ndarray, np.ndarray, np.ndarray]:</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"""消元：主元不为零就不换行，为零才向下找一行换上来。"""</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.</span><span>zeros</span><span>((n, n))</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span> </span><span>abs</span><span><span>(U[k, k]) </span><span>&lt;</span><span> tol:</span></span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>below </span><span>=</span><span> k </span><span>+</span><span> </span><span>1</span><span><span> </span><span>+</span><span> </span></span><span>int</span><span><span>(np.</span><span>argmax</span><span>(np.</span><span>abs</span><span>(U[k </span><span>+</span><span> </span></span><span>1</span><span> :, k])))</span></div></div><div><div><div>15</div></div><div><span>            </span><span>for</span><span> M </span><span>in</span><span> (U, L, P):</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>M[[k, below]] </span><span>=</span><span> M[[below, k]]</span></div></div><div><div><div>17</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(k </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>L[i, k] </span><span>=</span><span> U[i, k] </span><span>/</span><span> U[k, k]</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> L[i, k] </span><span>*</span><span> U[k]</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span><span> P, L </span><span>+</span><span> np.</span><span>eye</span><span>(n), U</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span># 第一个主元是 0：不换行就做不下去</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>2.0</span><span>, </span><span>7.0</span><span>, </span><span>9.0</span><span>]])</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>P, L, U </span><span>=</span><span> </span><span>lu_with_exchanges</span><span>(A)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"L ="</span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(L)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"U ="</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(U)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"P A = L U："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> A, L </span><span>@</span><span> U))</span></span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"P^T P = 单位矩阵："</span><span><span>, np.</span><span>allclose</span><span>(P.T </span><span>@</span><span> P, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span># 他提到 Matlab 会嫌主元太小：1e-20 在代数上不是 0，在浮点里很糟</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1e-20</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>x_true </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(B, b)  </span><span># LAPACK 内部做了选主元的换行</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span># 不换行：乘子是 1e20，U[1, 1] = 1 - 1e20 在浮点里吃掉了 1</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> B[</span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>/</span><span> B[</span></span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>u22 </span><span>=</span><span> B[</span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>-</span><span> m </span><span>*</span><span> B[</span></span><span>0</span><span>, </span><span>1</span><span>]</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>y2 </span><span>=</span><span> b[</span><span>1</span><span><span>] </span><span>-</span><span> m </span><span>*</span><span> b[</span></span><span>0</span><span>]</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>x2 </span><span>=</span><span> y2 </span><span>/</span><span> u22</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>x1 </span><span>=</span><span> (b[</span><span>0</span><span><span>] </span><span>-</span><span> B[</span></span><span>0</span><span>, </span><span>1</span><span><span>] </span><span>*</span><span> x2) </span><span>/</span><span> B[</span></span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>x_naive </span><span>=</span><span> np.</span><span>array</span><span>([x1, x2])</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>    </span><span># 先换行再消元：乘子是 1e-20</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>B_swap, b_swap </span><span>=</span><span> B[::</span><span>-</span><span>1</span><span><span>], b[::</span><span>-</span></span><span>1</span><span>]</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> B_swap[</span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>/</span><span> B_swap[</span></span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>u22 </span><span>=</span><span> B_swap[</span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>-</span><span> m </span><span>*</span><span> B_swap[</span></span><span>0</span><span>, </span><span>1</span><span>]</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>x2 </span><span>=</span><span> (b_swap[</span><span>1</span><span><span>] </span><span>-</span><span> m </span><span>*</span><span> b_swap[</span></span><span>0</span><span><span>]) </span><span>/</span><span> u22</span></span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>x1 </span><span>=</span><span> (b_swap[</span><span>0</span><span><span>] </span><span>-</span><span> B_swap[</span></span><span>0</span><span>, </span><span>1</span><span><span>] </span><span>*</span><span> x2) </span><span>/</span><span> B_swap[</span></span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>x_swap </span><span>=</span><span> np.</span><span>array</span><span>([x1, x2])</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>    </span><span>print</span><span>(</span><span>"不换行的解："</span><span>, x_naive, </span><span>"，误差"</span><span><span>, np.linalg.</span><span>norm</span><span>(x_naive </span><span>-</span><span> x_true))</span></span></div></div><div><div><div>60</div></div><div><span>    </span><span>print</span><span>(</span><span>"先换行的解："</span><span>, x_swap, </span><span>"，误差"</span><span><span>, np.linalg.</span><span>norm</span><span>(x_swap </span><span>-</span><span> x_true))</span></span></div></div><div><div><div>61</div></div><div><span>    </span><span>print</span><span>(</span><span>"参考解 x_true："</span><span>, x_true)</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P =</span></div></div><div><div><div>2</div></div><div><span>[[0. 0. 1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 1. 0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[1. 0. 0.]]</span></div></div><div><div><div>5</div></div><div><span>L =</span></div></div><div><div><div>6</div></div><div><span>[[ 1.      0.      0.    ]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ 0.5     1.      0.    ]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[ 0.     -0.6667  1.    ]]</span></div></div><div><div><div>9</div></div><div><span>U =</span></div></div><div><div><div>10</div></div><div><span>[[ 2.      7.      9.    ]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 0.     -1.5    -3.5   ]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[ 0.      0.     -1.3333]]</span></div></div><div><div><div>13</div></div><div><span>P A = L U： True</span></div></div><div><div><div>14</div></div><div><span>P^T P = 单位矩阵： True</span></div></div><div><div><div>15</div></div><div><span>不换行的解： [0. 1.] ，误差 1.0</span></div></div><div><div><div>16</div></div><div><span>先换行的解： [1. 1.] ，误差 0.0</span></div></div><div><div><div>17</div></div><div><span>参考解 x_true： [1. 1.]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>前半部分，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 把第一行和第三行交换，<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的乘子是 0.5 和 <span><span>−0.6667-0.6667</span><span><span><span></span><span>−</span><span>0.6667</span></span></span></span>，<span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 与 <span><span>PTP=IP^\mathsf{T}P = I</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 都为真。后半部分：不换行的解是 <span><span>(0,1)(0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，误差 1.0，也就是误差和解本身一样大；先换行的解 <span><span>(1,1)(1, 1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，误差 0.0。据我所知 <code>np.linalg.solve</code> 背后的 LAPACK 用的就是带选主元换行的 LU，这解释了为什么实际计算里 <span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 才是常态，不带 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 只是把换行拿掉之后最干净的那个版本。（我只是想验证他随口一句话，结果搭进去了一个代码块。）</p></section>
<section><h2>向量空间与子空间<a href="#向量空间与子空间"><span>#</span></a></h2><p><strong>向量空间是什么？</strong> 一批向量，但不是随便一批：必须能做向量该做的两件事，相加和数乘，并且结果仍留在这批向量里。他把这叫“对线性组合封闭”。教材列了八条运算规则，他说那些规则在 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span> 里肯定满足，真正需要检查的是封闭性。</p><p>最大的例子是 <span><span>Rn\mathbb{R}^n</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>，所有有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个实数分量的列向量。<span><span>R2\mathbb{R}^2</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 是 <span><span>xyxy</span><span><span><span></span><span>x</span><span>y</span></span></span></span> 平面，<span><span>(3,2)(3, 2)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(0,0)(0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>(π,e)(\pi, e)</span><span><span><span></span><span>(</span><span>π</span><span>,</span><span></span><span>e</span><span>)</span></span></span></span> 都在里面。注意 <span><span>(3,2,0)(3, 2, 0)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 在 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 而不在 <span><span>R2\mathbb{R}^2</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，它有三个分量，哪怕一个是 0。</p><p>他用三个反例讲封闭性。第一个只需要一句话，后两个我变成了代码：</p><ul>
<li>把 <span><span>xyxy</span><span><span><span></span><span>x</span><span>y</span></span></span></span> 平面里的原点挖掉：不行。<span><span>(3,2)(3, 2)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 乘以 0 得到原点，<span><span>(3,2)+(−3,−2)(3, 2) + (-3, -2)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>−</span><span>3</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span></span></span></span> 也得到原点。每个向量空间都必须含有零向量。</li>
<li>只取第一象限（所有分量不小于 0）：加法没问题，<span><span>(3,2)+(5,6)(3, 2) + (5, 6)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>5</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span> 还在里面，但乘以 <span><span>−5-5</span><span><span><span></span><span>−</span><span>5</span></span></span></span> 就掉出去了。不是向量空间，因为对数乘不封闭。</li>
<li><span><span>R2\mathbb{R}^2</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 里的直线：过原点的才是子空间，不过原点的那条，乘以 0 之后就离开了它。</li>
</ul><p><strong>子空间是什么？</strong> 向量空间里面的一个、自己也是向量空间的部分。<span><span>R2\mathbb{R}^2</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 的子空间只有三类：整个 <span><span>R2\mathbb{R}^2</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，过原点的直线，以及只含零向量的 <span><span>{0}\{\mathbf{0}\}</span><span><span><span></span><span>{</span><span>0</span><span>}</span></span></span></span>（他叫它 <span><span>ZZ</span><span><span><span></span><span>Z</span></span></span></span>）。零向量自己够格：它加它还是它，乘以 17 也还是它。<span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 的子空间是四类：整个空间、过原点的平面、过原点的直线、零向量。（这条直线看起来很像 <span><span>R1\mathbb{R}^1</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span></span></span></span></span></span></span></span>，但它的向量有两个分量，所以不一样，他特别强调了这一点。）</p></section>
<section><h2>列空间：从矩阵里长出子空间<a href="#列空间从矩阵里长出子空间"><span>#</span></a></h2><p>怎么从矩阵里得到子空间？取它的列。他用的矩阵还是那个 3 × 2 的</p><span><span><span>A=[132341]A = \begin{bmatrix} 1 &amp; 3 \\ 2 &amp; 3 \\ 4 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>两列是 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 里的向量。只把这两个向量放进去不够：能乘以数，所以 <span><span>(0,0,0)(0, 0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 要在；能相加，所以 <span><span>(4,5,5)(4, 5, 5)</span><span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span> 要在；一份第一列加三份第二列也要在。所以要把<strong>所有</strong>线性组合都放进去，这个子空间就叫列空间，记作 <span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span>。他说这是这一讲的中心思想：有几个向量还不满足，我们要的是它们的组合构成的空间。</p><p>两个 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span> 里的向量，所有组合是什么？比一条线多，比整个空间少：一个过原点的平面。我用叉乘求出这个平面的法向量，然后检查封闭性：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>in_column_space</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>v</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>x, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, v, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>bool</span><span><span>(np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, v))</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span># 例一：第一象限（所有分量不小于 0）。加法封闭，数乘不封闭</span></div></div><div><div><div>14</div></div><div><span>    </span><span>def</span><span> </span><span>in_quarter</span><span>(</span><span>v</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>15</div></div><div><span>        </span><span>return</span><span> </span><span>bool</span><span><span>(np.</span><span>all</span><span>(v </span><span>&gt;=</span><span> </span></span><span>0</span><span>))</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>u, w </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>3.0</span><span>, </span><span>2.0</span><span><span>]), np.</span><span>array</span><span>([</span></span><span>5.0</span><span>, </span><span>6.0</span><span>])</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"(3, 2) + (5, 6) 还在第一象限："</span><span><span>, </span><span>in_quarter</span><span>(u </span><span>+</span><span> w))</span></span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"-5 * (3, 2) 还在第一象限："</span><span><span>, </span><span>in_quarter</span><span>(</span><span>-</span></span><span>5</span><span><span> </span><span>*</span><span> u))</span></span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 例二：过原点的直线与不过原点的直线</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>def</span><span> </span><span>on_line</span><span>(</span><span>v</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>25</div></div><div><span>        </span><span>return</span><span> </span><span>bool</span><span><span>(np.</span><span>isclose</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> v[</span></span><span>0</span><span><span>] </span><span>-</span><span> v[</span></span><span>1</span><span>], </span><span>0.0</span><span>))</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span>def</span><span> </span><span>on_shifted</span><span>(</span><span>v</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>28</div></div><div><span>        </span><span>return</span><span> </span><span>bool</span><span><span>(np.</span><span>isclose</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> v[</span></span><span>0</span><span><span>] </span><span>-</span><span> v[</span></span><span>1</span><span>], </span><span>1.0</span><span>))</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>t, s </span><span>=</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"过原点的线："</span><span><span>, </span><span>on_line</span><span>(t </span><span>*</span><span> d </span><span>+</span><span> s </span><span>*</span><span> d), </span><span>on_line</span><span>(</span></span><span>0.0</span><span><span> </span><span>*</span><span> d))</span></span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])  </span><span># 平移后的线上的一点</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"平移的线："</span><span><span>, </span><span>on_shifted</span><span>(p), </span></span><span>"，乘 0 之后："</span><span><span>, </span><span>on_shifted</span><span>(</span></span><span>0.0</span><span><span> </span><span>*</span><span> p))</span></span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span># 例三：他的矩阵的列空间，R^3 里的一个平面</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>3.0</span><span>], [</span><span>4.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>37</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> np.</span><span>cross</span><span>(A[:, </span><span>0</span><span>], A[:, </span><span>1</span><span>])  </span><span># 平面的法向量</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"平面的法向量："</span><span>, n)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"列一 + 列二 ="</span><span>, A[:, </span><span>0</span><span><span>] </span><span>+</span><span> A[:, </span></span><span>1</span><span>])</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"列一 + 3 * 列二 ="</span><span>, A[:, </span><span>0</span><span><span>] </span><span>+</span><span> </span></span><span>3</span><span><span> </span><span>*</span><span> A[:, </span></span><span>1</span><span>])</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span># 随机取 1000 对组合，检查加法和数乘是否出了平面</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>worst </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>45</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>1000</span><span>):</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>a </span><span>=</span><span> A </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>b </span><span>=</span><span> A </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> rng.</span><span>standard_normal</span><span>() </span><span>*</span><span> </span><span>10</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>worst </span><span>=</span><span> </span><span>max</span><span>(worst, </span><span>abs</span><span><span>(n </span><span>@</span><span> (a </span><span>+</span><span> b)), </span></span><span>abs</span><span><span>(n </span><span>@</span><span> (c </span><span>*</span><span> a)))</span></span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span><span>"1000 次加法和数乘后离平面的最大偏差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span>worst</span><span>:.1e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(</span><span>"0 向量在列空间里："</span><span><span>, </span><span>in_column_space</span><span>(A, np.</span><span>zeros</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>53</div></div><div><span>        </span><span>"(1, 0, 0) 在列空间里："</span><span><span>, </span><span>in_column_space</span><span>(A, np.</span><span>array</span><span>([</span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>]))</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>55</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>56</div></div><div><span>        </span><span>"(4, 5, 5) 在列空间里："</span><span><span>, </span><span>in_column_space</span><span>(A, np.</span><span>array</span><span>([</span></span><span>4.0</span><span>, </span><span>5.0</span><span>, </span><span>5.0</span><span>]))</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(3, 2) + (5, 6) 还在第一象限： True</span></div></div><div><div><div>2</div></div><div><span>-5 * (3, 2) 还在第一象限： False</span></div></div><div><div><div>3</div></div><div><span>过原点的线： True True</span></div></div><div><div><div>4</div></div><div><span>平移的线： True ，乘 0 之后： False</span></div></div><div><div><div>5</div></div><div><span>A 的秩： 2</span></div></div><div><div><div>6</div></div><div><span>平面的法向量： [-10.  11.  -3.]</span></div></div><div><div><div>7</div></div><div><span>列一 + 列二 = [4. 5. 5.]</span></div></div><div><div><div>8</div></div><div><span>列一 + 3 * 列二 = [10. 11.  7.]</span></div></div><div><div><div>9</div></div><div><span>1000 次加法和数乘后离平面的最大偏差： 5.7e-13</span></div></div><div><div><div>10</div></div><div><span>0 向量在列空间里： True</span></div></div><div><div><div>11</div></div><div><span>(1, 0, 0) 在列空间里： False</span></div></div><div><div><div>12</div></div><div><span>(4, 5, 5) 在列空间里： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>法向量是 <span><span>(−10,11,−3)(-10, 11, -3)</span><span><span><span></span><span>(</span><span>−</span><span>10</span><span>,</span><span></span><span>11</span><span>,</span><span></span><span>−</span><span>3</span><span>)</span></span></span></span>，所以这个平面是 <span><span>−10x+11y−3z=0-10x + 11y - 3z = 0</span><span><span><span></span><span>−</span><span>10</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>11</span><span>y</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。他口头念了 <span><span>(4,5,5)(4, 5, 5)</span><span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>5</span><span>)</span></span></span></span>，它是两列之和；一份第一列加三份第二列是 <span><span>(10,11,7)(10, 11, 7)</span><span><span><span></span><span>(</span><span>10</span><span>,</span><span></span><span>11</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span>（他只说了“一份加三份”，数是我算的），两个都落在这个平面上。随机取 1000 对组合，相加、数乘之后离平面的最大偏差是 <span><span>5.7×10−135.7 \times 10^{-13}</span><span><span><span></span><span>5.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，是舍入误差，没有一次出平面。零向量在，<span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 不在，所以 <span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 不是整个 <span><span>R3\mathbb{R}^3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>。</p><p>他还往 <span><span>R10\mathbb{R}^{10}</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 里推了一步：取五个十维向量，它们的组合不是 <span><span>R5\mathbb{R}^5</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>5</span></span></span></span></span></span></span></span></span></span></span>（向量有十个分量），而是十维空间里一块过原点的、大概五维的“平的东西”。要是五个向量恰好落在同一条直线上，那就只能得到一条线。这取决于列之间的关系，后面几讲会把它说清楚。</p><p>这和第 1 讲其实是同一件事。那一讲里我把第三列改成前两列之和，三列的组合就困在一个平面里，右端项 <span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 够不着，距离是 0.4082。当时我把那个平面叫作“够得着的地方”，现在它有了正式的名字：列空间。同一幅图，换了一套更准确的词。</p><p>我觉得这一讲最好的地方，在于它把“解方程”换成了“找子空间”：<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 有解，当且仅当 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在 <span><span>C(A)C(A)</span><span><span><span></span><span>C</span><span>(</span><span>A</span><span>)</span></span></span></span> 里。他在最后说，下一讲的第一件事就是用这套新语言重新看 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>，并且引入另一个子空间。下一讲继续：<a href="/posts/linear-algebra-06-column-space-and-nullspace/">18.06 第 6 讲：列空间与零空间</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-04-lu-factorization/</id>
      <title type="text">18.06 第 4 讲：分解成 A = LU</title>
      <published>2026-10-18T01:00:00.000Z</published>
      <updated>2026-10-18T01:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-04-lu-factorization/"/>
      <summary type="text">MIT 18.06 第 4 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 手写了不换行的 LU 分解，数出 n = 160 时共 1365280 次乘除，是 n³/3 的 1.000 倍，而每多一个右端项只多 n² 次。</summary>
      <content type="html"><![CDATA[<p>消元的每一步都是一次左乘 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span>，把它们连乘起来是 <span><span>EA=UEA = U</span><span><span><span></span><span>E</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span></span></span></span>，但 Strang 这一讲要你把 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 搬到等号的另一边：<span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>。搬过去以后有一件很舒服的事：消元时用过的乘数，原样坐进 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 里，一个都不会被搅乱，我手写的分解不需要求任何一个逆。他还顺手数了一遍消元要花多少次运算，我在 <span><span>n=160n = 160</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>160</span></span></span></span> 的随机矩阵上实测是 1365280 次乘除，是 <span><span>n3/3=1365333n^3/3 = 1365333</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span><span></span><span>=</span><span></span></span><span><span></span><span>1365333</span></span></span></span> 的 1.000 倍。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=MsIvs_6vC38" target="_blank">Lecture 4: Factorization into A = LU</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-4-factorization-into-a-lu/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §2.3 Matrix Computations and A = LU
前置：<a href="/posts/linear-algebra-03-multiplication-and-inverse/">第 3 讲：矩阵乘法的四种看法与逆矩阵</a>
一句话：<span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 里 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 就是消元乘数的记录本，分解一次的代价是 <span><span>n3/3n^3/3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span></span>，之后每个右端项只要 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他先补了上一讲欠的“乘积的逆”和“转置的逆”，各只有几分钟，本篇第一节用代码验过，可以倍速。</li>
<li>三阶的那段要认真看：他故意取 <span><span>E21E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里是 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span>、<span><span>E32E_{32}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里是 <span><span>−5-5</span><span><span><span></span><span>−</span><span>5</span></span></span></span>，让你亲眼看到 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 的乘积里冒出一个 10，而 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 里没有。这是整讲的要点。</li>
<li>数运算次数那一段，他一边说一边把 <span><span>n2+(n−1)2+⋯+12n^2 + (n-1)^2 + \cdots + 1^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 写出来，再请出微积分。值得听，本篇用计数器重算了一遍。</li>
<li>最后列六个 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 置换矩阵的那段是课堂互动，节奏较慢，本篇最后一节已经枚举过，可以快进。</li>
</ul></div></div>
<section><h2>先还一笔债：乘积的逆与转置的逆<a href="#先还一笔债乘积的逆与转置的逆"><span>#</span></a></h2><p>他的第一件事，是补上一讲留下的问题：知道 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 和 <span><span>B−1B^{-1}</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 的逆是什么？答案是 <span><span>B−1A−1B^{-1}A^{-1}</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>，顺序反过来。验证只要把括号挪一挪：</p><span><span><span>(AB)(B−1A−1)=A(BB−1)A−1=AA−1=I(AB)(B^{-1}A^{-1}) = A(BB^{-1})A^{-1} = AA^{-1} = I</span><span><span><span></span><span>(</span><span>A</span><span>B</span><span>)</span><span>(</span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>(</span><span>B</span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>)</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span></span><p>他说教材里的例子是穿鞋穿袜（先脱鞋再脱袜，所以复原的时候先穿袜再穿鞋），然后为这个“愚蠢的例子”道了歉，还说很遗憾被录进了磁带。我留着它，毕竟这个顺序错了真的会出错。</p><p>转置的逆同样简单：从 <span><span>AA−1=IAA^{-1} = I</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 两边取转置，单位矩阵转置还是单位矩阵，左边顺序又要反过来，得到 <span><span>(A−1)TAT=I(A^{-1})^T A^T = I</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。所以 <span><span>(AT)−1=(A−1)T(A^T)^{-1} = (A^{-1})^T</span><span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：先转置再求逆，和先求逆再转置，是一回事。下面用两个随机的 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 矩阵验证这两句话，顺便看看“忘了反过来”会怎样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>inv_AB </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(A </span><span>@</span><span> B)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>inv_A </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(A)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>inv_B </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(B)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 乘积的逆：顺序要反过来</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"inv(AB) = inv(B) inv(A)："</span><span><span>, np.</span><span>allclose</span><span>(inv_AB, inv_B </span><span>@</span><span> inv_A))</span></span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"inv(AB) = inv(A) inv(B)："</span><span><span>, np.</span><span>allclose</span><span>(inv_AB, inv_A </span><span>@</span><span> inv_B))</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 转置的逆：先转置还是先求逆都一样</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"inv(A.T) = inv(A).T："</span><span><span>, np.</span><span>allclose</span><span>(np.linalg.</span><span>inv</span><span>(A.T), inv_A.T))</span></span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>inv(AB) = inv(B) inv(A)： True</span></div></div><div><div><div>2</div></div><div><span>inv(AB) = inv(A) inv(B)： False</span></div></div><div><div><div>3</div></div><div><span>inv(A.T) = inv(A).T： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>inv(A) @ inv(B)</code> 的那一行是 <code>False</code>。随机矩阵一般不交换，<span><span>A−1B−1A^{-1}B^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 不是 <span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 的逆，顺序是认真的。</p></section>
<section><h2>2 × 2：把 E 的逆放到另一边<a href="#2--2把-e-的逆放到另一边"><span>#</span></a></h2><p>接下来是整讲的核心。设 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 可逆，消元不需要换行，主元位置上也没有 0，那么从 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 一路消元到上三角的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>，两者之间必然有一个矩阵 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 把它们连起来。他先拿 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 练手：</p><span><span><span>A=[2187]A = \begin{bmatrix} 2 &amp; 1 \\ 8 &amp; 7 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>第一个主元是 2，乘数是 4。他特意解释了为什么右下角取 7：如果取 4，第二个主元就没有了，矩阵是奇异的（他的原话是“general screw-up”）。第二行减去 4 倍的第一行，右下角变成 3：</p><span><span><span>E21A=[10−41][2187]=[2103]=UE_{21}A = \begin{bmatrix} 1 &amp; 0 \\ -4 &amp; 1 \end{bmatrix}
\begin{bmatrix} 2 &amp; 1 \\ 8 &amp; 7 \end{bmatrix}
= \begin{bmatrix} 2 &amp; 1 \\ 0 &amp; 3 \end{bmatrix} = U</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>U</span></span></span></span></span><p>现在两边同时左乘 <span><span>E21−1E_{21}^{-1}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 单独留在一边：<span><span>A=E21−1UA = E_{21}^{-1}U</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>U</span></span></span></span>，这个 <span><span>E21−1E_{21}^{-1}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就是 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span>。消元矩阵的逆很好求：把负号换成正号，“减掉的又加回去”。所以 <span><span>L=[1041]L = \begin{bmatrix} 1 &amp; 0 \\ 4 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，<span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 写出来是</p><span><span><span>[2187]=[1041][2103]\begin{bmatrix} 2 &amp; 1 \\ 8 &amp; 7 \end{bmatrix}
= \begin{bmatrix} 1 &amp; 0 \\ 4 &amp; 1 \end{bmatrix}
\begin{bmatrix} 2 &amp; 1 \\ 0 &amp; 3 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>他提到 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的字母来自 lower triangular，对角线上是 1，而 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的对角线上是主元。有时候想让两边“平衡”一点，可以把主元单独提出来，写成 <span><span>A=LDU′A = LDU'</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span>D</span><span><span>U</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span>：<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是主元组成的对角矩阵，<span><span>U′U'</span><span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span> 的对角线也是 1。他说 MATLAB 两种都能给，他自己以后主要用 <span><span>LULU</span><span><span><span></span><span>LU</span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># Strang 的 2 x 2 例子：A = [[2, 1], [8, 7]]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>8.0</span><span>, </span><span>7.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>E21 </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>4.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> E21 </span><span>@</span><span> A</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(E21) </span><span>+</span><span> </span><span>0.0</span><span>  </span><span># 加 0.0 是为了不打印 -0.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"U ="</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(U)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"L ="</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(L)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"L @ U 等于 A："</span><span><span>, np.</span><span>allclose</span><span>(L </span><span>@</span><span> U, A))</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span># 把主元从 U 里提出来：A = L D U'</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>diag</span><span>(np.</span><span>diag</span><span>(U))</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>U1 </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(D) </span><span>@</span><span> U</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"D ="</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(D)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"U' ="</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(U1)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(</span><span>"L @ D @ U' 等于 A："</span><span><span>, np.</span><span>allclose</span><span>(L </span><span>@</span><span> D </span><span>@</span><span> U1, A))</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>U =</span></div></div><div><div><div>2</div></div><div><span>[[2. 1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[0. 3.]]</span></div></div><div><div><div>4</div></div><div><span>L =</span></div></div><div><div><div>5</div></div><div><span>[[1. 0.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[4. 1.]]</span></div></div><div><div><div>7</div></div><div><span>L @ U 等于 A： True</span></div></div><div><div><div>8</div></div><div><span>D =</span></div></div><div><div><div>9</div></div><div><span>[[2. 0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[0. 3.]]</span></div></div><div><div><div>11</div></div><div><span>U' =</span></div></div><div><div><div>12</div></div><div><span>[[1.  0.5]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[0.  1. ]]</span></div></div><div><div><div>14</div></div><div><span>L @ D @ U' 等于 A： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>U′U'</span><span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span> 右上角的 0.5，是把 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 第一行除以主元 2 得到的 <span><span>1/21/2</span><span><span><span></span><span>1/2</span></span></span></span>。第二行除以 3，右上角没有东西可除，所以只剩一个 1。</p></section>
<section><h2>3 × 3：乘数为什么直接落进 L<a href="#3--3乘数为什么直接落进-l"><span>#</span></a></h2><p>二阶看不出什么门道，他自己也说这只是练习，差别只在一个是减号、一个是加号。三阶才开始有意思。三步消元依次是：<span><span>E21E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 清掉 <span><span>(2,1)(2,1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 位置，<span><span>E31E_{31}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 清掉 <span><span>(3,1)(3,1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>E32E_{32}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 清掉 <span><span>(3,2)(3,2)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，合起来 <span><span>E32E31E21A=UE_{32}E_{31}E_{21}A = U</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span></span></span></span>。要把这些放到另一边，每一个都取逆，<strong>而且顺序要反过来</strong>（这就是开头那条规则的用处）：</p><span><span><span>A=E21−1E31−1E32−1 U=LUA = E_{21}^{-1}E_{31}^{-1}E_{32}^{-1}\,U = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span></span><p>为什么要这么麻烦？他取了一个很小的例子来说明。设 <span><span>E21E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里是 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span>，<span><span>E31E_{31}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是单位矩阵（<span><span>(3,1)(3,1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 位置本来就是 0，不用消），<span><span>E32E_{32}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里是 <span><span>−5-5</span><span><span><span></span><span>−</span><span>5</span></span></span></span>。先看 <span><span>E=E32E21E = E_{32}E_{21}</span><span><span><span></span><span>E</span><span></span><span>=</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>E=[1000100−51][100−210001]=[100−21010−51]E = \begin{bmatrix} 1 &amp; 0 &amp; 0 \\ 0 &amp; 1 &amp; 0 \\ 0 &amp; -5 &amp; 1 \end{bmatrix}
\begin{bmatrix} 1 &amp; 0 &amp; 0 \\ -2 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 1 \end{bmatrix}
= \begin{bmatrix} 1 &amp; 0 &amp; 0 \\ -2 &amp; 1 &amp; 0 \\ 10 &amp; -5 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>E</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span><span><span></span><span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>左下角冒出了一个 10。他说他不喜欢这个 10（当然它是对的，他只是不想看到它）：第二行减了 2 倍的第一行，第三行再减 5 倍的新第二行，那么第一行实际上有 <span><span>2×5=102 \times 5 = 10</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 份被“扔”进了第三行。<span><span>E31E_{31}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 本来是单位矩阵，但这个 10 就在 <span><span>(3,1)(3,1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 位置上，和它毫无关系。</p><p>反过来，按逆矩阵的顺序乘：<span><span>L=E21−1E32−1L = E_{21}^{-1}E_{32}^{-1}</span><span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，每个逆里是 <span><span>+2+2</span><span><span><span></span><span>+</span><span>2</span></span></span></span> 和 <span><span>+5+5</span><span><span><span></span><span>+</span><span>5</span></span></span></span>，乘出来是 <span><span>[100210051]\begin{bmatrix} 1 &amp; 0 &amp; 0 \\ 2 &amp; 1 &amp; 0 \\ 0 &amp; 5 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>5</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span>：2 和 5 各在自己的位置上，没有 10。用他的话说，“按正确的顺序，乘数就直接坐在 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 里”。</p><p>所以要得到 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span>，没有任何额外的工作：消元时每用一个乘数 <span><span>ℓij\ell_{ij}</span><span><span><span></span><span><span>ℓ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，就把它记在 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的 <span><span>(i,j)(i,j)</span><span><span><span></span><span>(</span><span>i</span><span>,</span><span></span><span>j</span><span>)</span></span></span></span> 位置，再没有别的。他还补了一句我觉得很实用的话：消元做完第二行以后，你已经造出了 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 的第二行和对应的乘数，这两样都要留下，而 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 自己可以扔了。据我所知，实际的库就是这么存的：<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 和 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 合起来恰好塞进 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 原来的那块内存（<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的对角线是 1，不用存）。</p><p>我造了一个 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，让三个乘数恰好是 2、0、5。下面的脚本先照黑板的写法把 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 乘出来，再手写一个不选主元的 LU：它不求逆，乘数直接记进 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span>，最后用求逆得到的 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 来对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>lu_nopivot</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; tuple[np.ndarray, np.ndarray]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""不换行的消元：乘数直接记进 L，消元结果留在 U。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> np.</span><span>isclose</span><span>(U[k, k], </span></span><span>0.0</span><span>):</span></div></div><div><div><div>11</div></div><div><span>            </span><span>raise</span><span> </span><span>ZeroDivisionError</span><span>(</span><span>f</span><span>"第 </span><span>{</span><span>k</span><span>}</span><span> 个主元是 0，需要换行"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(k </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>L[i, k] </span><span>=</span><span> U[i, k] </span><span>/</span><span> U[k, k]  </span><span># 乘数</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>U[i, k:] </span><span>-=</span><span> L[i, k] </span><span>*</span><span> U[k, k:]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span> L, U</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 构造一个乘数恰好是 2、0、5 的 A（对应 E21 = -2，E31 = 0，E32 = -5）</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>2.0</span><span>, </span><span>5.0</span><span>, </span><span>4.0</span><span>], [</span><span>0.0</span><span>, </span><span>5.0</span><span>, </span><span>11.0</span><span>]])</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 先按黑板上的写法，把三步消元写成三个矩阵</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>E21 </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>E32 </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>5.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> E32 </span><span>@</span><span> E21</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"E32 @ E21 ="</span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(E)</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"E @ A ="</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span><span>(E </span><span>@</span><span> A)</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span># 把它们的逆按相反的顺序乘起来</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>L_from_inverses </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(E21) </span><span>@</span><span> np.linalg.</span><span>inv</span><span>(E32)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"inv(E21) @ inv(E32) ="</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(L_from_inverses)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span># 手写 LU：不求逆，乘数直接落进 L</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>L, U </span><span>=</span><span> </span><span>lu_nopivot</span><span>(A)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"手写 L ="</span><span>)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(L)</span></div></div><div><div><div>42</div></div><div><span>    </span><span>print</span><span>(</span><span>"手写 U ="</span><span>)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>print</span><span>(U)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>print</span><span>(</span><span>"L 与逆矩阵乘出来的一致："</span><span><span>, np.</span><span>allclose</span><span>(L, L_from_inverses))</span></span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"U 与 E @ A 一致："</span><span><span>, np.</span><span>allclose</span><span>(U, E </span><span>@</span><span> A))</span></span></div></div><div><div><div>46</div></div><div><span>    </span><span>print</span><span>(</span><span>"L @ U 等于 A："</span><span><span>, np.</span><span>allclose</span><span>(L </span><span>@</span><span> U, A))</span></span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>    </span><span># 同一个 L、U 反复使用：先 L c = b，再 U x = c</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>3</span><span>, </span><span>4</span><span>))  </span><span># 四个右端项</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(U, np.linalg.</span><span>solve</span><span>(L, B))</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span><span>"四个右端项都解对了："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> X, B))</span></span></div></div><div><div><div>53</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>54</div></div><div><span>        </span><span>"和 np.linalg.solve(A, B) 一致："</span><span>,</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(X, np.linalg.</span><span>solve</span><span>(A, B)),</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>E32 @ E21 =</span></div></div><div><div><div>2</div></div><div><span>[[ 1.  0.  0.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[-2.  1.  0.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[10. -5.  1.]]</span></div></div><div><div><div>5</div></div><div><span>E @ A =</span></div></div><div><div><div>6</div></div><div><span>[[1. 2. 1.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[0. 1. 2.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[0. 0. 1.]]</span></div></div><div><div><div>9</div></div><div><span>inv(E21) @ inv(E32) =</span></div></div><div><div><div>10</div></div><div><span>[[1. 0. 0.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[2. 1. 0.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[0. 5. 1.]]</span></div></div><div><div><div>13</div></div><div><span>手写 L =</span></div></div><div><div><div>14</div></div><div><span>[[1. 0. 0.]</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[2. 1. 0.]</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>[0. 5. 1.]]</span></div></div><div><div><div>17</div></div><div><span>手写 U =</span></div></div><div><div><div>18</div></div><div><span>[[1. 2. 1.]</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>[0. 1. 2.]</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>[0. 0. 1.]]</span></div></div><div><div><div>21</div></div><div><span>L 与逆矩阵乘出来的一致： True</span></div></div><div><div><div>22</div></div><div><span>U 与 E @ A 一致： True</span></div></div><div><div><div>23</div></div><div><span>L @ U 等于 A： True</span></div></div><div><div><div>24</div></div><div><span>四个右端项都解对了： True</span></div></div><div><div><div>25</div></div><div><span>和 np.linalg.solve(A, B) 一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>E32 @ E21</code> 的左下角确实是 10，而手写分解得到的 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 里是 2、0、5，和 <code>inv(E21) @ inv(E32)</code> 一致。<code>U</code> 与 <code>E @ A</code> 一致，<code>L @ U</code> 还原 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>。最后两行演示了分解的用处：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 只分解一次，之后每个右端项只要解 <span><span>Lc=bL\mathbf{c} = \mathbf{b}</span><span><span><span></span><span>L</span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 和 <span><span>Ux=cU\mathbf{x} = \mathbf{c}</span><span><span><span></span><span>U</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span></span></span></span> 两个三角方程。（这里我偷懒用了 <code>np.linalg.solve</code>，它并不知道矩阵是三角的，所以没有省运算。真正省了多少，下一节数给你看。）</p></section>
<section><h2>消元要花多少次运算<a href="#消元要花多少次运算"><span>#</span></a></h2><p>他说今天是“实用的一天”，然后问了一个很朴素的问题：<span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的矩阵，消元要多少步？<span><span>n=1000n = 1000</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>1000</span></span></span></span> 能不能在一秒、一分钟还是一周内解完？更大的 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 呢？他列了几个候选：正比于 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>，<span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，<span><span>n3n^3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>，或者像行列式那样 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span>（他顺手给行列式记了一笔坏账）。</p><p>他的算法是这样想的：把一次“乘一个乘数再减”算作一次运算。第一步，除了第一行以外的所有行都要改，大约 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 个数。第二步，问题缩小成 <span><span>(n−1)×(n−1)(n-1) \times (n-1)</span><span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>，大约 <span><span>(n−1)2(n-1)^2</span><span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。以此类推，总数大约是</p><span><span><span>n2+(n−1)2+⋯+22+12≈13n3n^2 + (n-1)^2 + \cdots + 2^2 + 1^2 \approx \frac{1}{3}n^3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>1</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span></span><p>有 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 项，最大的一项是 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，所以上界是 <span><span>n3n^3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>，但因为后面的项越来越小，实际上只有三分之一。他说这时可以“允许提一下微积分”：求和和积分差不多，<span><span>∫0nx2 dx=n3/3\int_0^n x^2\,dx = n^3/3</span><span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>d</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span></span>（他还补了一句：他不是不喜欢 18.01，不过 18.06 更好）。</p><p>右端项 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 是一个单独的列，所以便宜得多：一次前代和一次回代合起来是 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。他的结论是：如果同一个 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 有很多个右端项，那就一次性付高价 <span><span>n3/3n^3/3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span></span> 做 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>，之后每个右端项只付 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。</p><p>我用计数器验证。为了让每次运算都被算到，我写了一个会数数的 LU：每步消元，除法的次数是剩下的行数，乘法的次数是剩下的子矩阵里元素的个数。再写一个前代加回代，也数运算。随机矩阵加上 <span><span>nInI</span><span><span><span></span><span>n</span><span>I</span></span></span></span> 保证对角占优，不选主元也不会遇到 0：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>lu_count</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; tuple[np.ndarray, np.ndarray, </span><span>int</span><span>]:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""不换行的 LU，同时数乘法和除法的次数。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>count </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>L[k </span><span>+</span><span> </span><span>1</span><span><span> :, k] </span><span>=</span><span> U[k </span><span>+</span><span> </span></span><span>1</span><span><span> :, k] </span><span>/</span><span> U[k, k]</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>count </span><span>+=</span><span> n </span><span>-</span><span> k </span><span>-</span><span> </span><span>1</span><span>  </span><span># 这一步的除法</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>update </span><span>=</span><span> np.</span><span>outer</span><span>(L[k </span><span>+</span><span> </span><span>1</span><span><span> :, k], U[k, k </span><span>+</span><span> </span></span><span>1</span><span> :])</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>U[k </span><span>+</span><span> </span><span>1</span><span><span> :, k </span><span>+</span><span> </span></span><span>1</span><span><span> :] </span><span>-=</span><span> update</span></span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>U[k </span><span>+</span><span> </span><span>1</span><span><span> :, k] </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>count </span><span>+=</span><span> update.size  </span><span># 这一步的乘法</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> L, U, count</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>def</span><span> </span><span>solve_count</span><span>(</span><span>L</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>U</span></span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>21</div></div><div><span>    </span><span>"""前代加回代解 LU x = b，返回乘法和除法的次数。"""</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> L.shape[</span><span>0</span><span>]</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>count </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):  </span><span># L 的对角线是 1，不用除</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>c[i] </span><span>=</span><span> b[i] </span><span>-</span><span> L[i, :i] </span><span>@</span><span> c[:i]</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>count </span><span>+=</span><span> i</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span>):</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>x[i] </span><span>=</span><span> (c[i] </span><span>-</span><span> U[i, i </span><span>+</span><span> </span><span>1</span><span><span> :] </span><span>@</span><span> x[i </span><span>+</span><span> </span></span><span>1</span><span><span> :]) </span><span>/</span><span> U[i, i]</span></span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>count </span><span>+=</span><span> n </span><span>-</span><span> i  </span><span># n - 1 - i 次乘法，加一次除法</span></div></div><div><div><div>32</div></div><div><span>    </span><span>assert</span><span><span> np.</span><span>allclose</span><span>(L </span><span>@</span><span> U </span><span>@</span><span> x, b)</span></span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> count</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>print</span><span>(</span><span>"n, LU 乘除次数, n^3/3, 比值, 每个右端项, 除以 n^2, 相对上一行"</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>previous </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>40</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> [</span><span>10</span><span>, </span><span>20</span><span>, </span><span>40</span><span>, </span><span>80</span><span>, </span><span>160</span><span>]:</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((n, n)) </span><span>+</span><span> n </span><span>*</span><span> np.</span><span>eye</span><span>(n)  </span><span># 不选主元也安全</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>L, U, count </span><span>=</span><span> </span><span>lu_count</span><span>(A)</span></div></div><div><div><div>43</div></div><div><span>        </span><span>assert</span><span><span> np.</span><span>allclose</span><span>(L </span><span>@</span><span> U, A)</span></span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>solve </span><span>=</span><span> </span><span>solve_count</span><span>(L, U, rng.</span><span>standard_normal</span><span>(n))</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>growth </span><span>=</span><span> </span><span>f</span><span>"</span><span>{</span><span><span>count </span><span>/</span><span> previous</span></span><span>:.2f</span><span>}</span><span>"</span><span> </span><span>if</span><span> previous </span><span>else</span><span> </span><span>"-"</span></div></div><div><div><div>46</div></div><div><span>        </span><span>print</span><span>(</span></div></div><div><div><div>47</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>n</span><span>}</span><span>, </span><span>{</span><span>count</span><span>}</span><span>, </span><span>{</span><span><span>n</span><span>**</span></span><span>3</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>:.0f</span><span>}</span><span>, </span><span>{</span><span><span>count </span><span>/</span><span> (n</span><span>**</span></span><span>3</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span>)</span><span>:.3f</span><span>}</span><span>, "</span></div></div><div><div><div>48</div></div><div><span>            </span><span>f</span><span>"</span><span>{</span><span>solve</span><span>}</span><span>, </span><span>{</span><span><span>solve </span><span>/</span><span> n</span><span>**</span></span><span>2</span><span>:.3f</span><span>}</span><span>, </span><span>{</span><span>growth</span><span>}</span><span>"</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>previous </span><span>=</span><span> count</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>n, LU 乘除次数, n^3/3, 比值, 每个右端项, 除以 n^2, 相对上一行</span></div></div><div><div><div>2</div></div><div><span>10, 330, 333, 0.990, 100, 1.000, -</span></div></div><div><div><div>3</div></div><div><span>20, 2660, 2667, 0.998, 400, 1.000, 8.06</span></div></div><div><div><div>4</div></div><div><span>40, 21320, 21333, 0.999, 1600, 1.000, 8.02</span></div></div><div><div><div>5</div></div><div><span>80, 170640, 170667, 1.000, 6400, 1.000, 8.00</span></div></div><div><div><div>6</div></div><div><span>160, 1365280, 1365333, 1.000, 25600, 1.000, 8.00</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>几个数字：</p><ul>
<li><span><span>n=10n = 10</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时 330 次，<span><span>n3/3≈333n^3/3 \approx 333</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span><span></span><span>≈</span><span></span></span><span><span></span><span>333</span></span></span></span>。我算过，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶的精确值是 <span><span>(n3−n)/3(n^3 - n)/3</span><span><span><span></span><span>(</span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>n</span><span>)</span><span>/3</span></span></span></span>，<span><span>n=10n = 10</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时正好是 <span><span>(1000−10)/3=330(1000 - 10)/3 = 330</span><span><span><span></span><span>(</span><span>1000</span><span></span><span>−</span><span></span></span><span><span></span><span>10</span><span>)</span><span>/3</span><span></span><span>=</span><span></span></span><span><span></span><span>330</span></span></span></span>，所以 <span><span>n3/3n^3/3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span></span> 是首项，丢掉的 <span><span>−n/3-n/3</span><span><span><span></span><span>−</span><span>n</span><span>/3</span></span></span></span> 随着 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 变大越来越不重要。到 <span><span>n=160n = 160</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>160</span></span></span></span> 时，实测与 <span><span>n3/3n^3/3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span></span> 的比值已经是 1.000。</li>
<li>每次把 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 加倍，次数乘以 8.00（<span><span>n=10n=10</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 到 <span><span>2020</span><span><span><span></span><span>20</span></span></span></span> 是 8.06，之后是 8.02、8.00、8.00），正是他问“把一百变成两百，是 2 倍、4 倍还是 8 倍”的答案：8 倍。</li>
<li>每个右端项的代价恰好是 <span><span>n2n^2</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>：<span><span>n=160n = 160</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>160</span></span></span></span> 时是 25600。分解一次是 1365280，是它的 53.3 倍（也就是 <span><span>n/3n/3</span><span><span><span></span><span>n</span><span>/3</span></span></span></span>）。所以只要右端项超过 53 个，分解的“高价”就已经不重要了。</li>
</ul><p>他问的 1000 阶，我用这条公式估一下：<span><span>10003/3≈3.3×1081000^3/3 \approx 3.3 \times 10^8</span><span><span><span></span><span>100</span><span><span>0</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span><span></span><span>≈</span><span></span></span><span><span></span><span>3.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span></span></span></span></span></span></span></span> 次乘除，我猜现在的机器一眨眼就算完了，但 1999 年他在教室里问这个问题的时候，大概是真的要想一想。（他没有给确切的时间，这是我的猜测。）</p><p>另外他说明了一个前提：这个计数针对稠密矩阵，也就是假设没有 0。如果矩阵里 0 摆在好的位置上，那些运算可以直接跳过，会快得多。我上面的实验用的是满的随机矩阵，所以数出来的是最坏的情形，<span><span>n3/3n^3/3</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span></span> 要当成稠密矩阵的价目表来读。</p></section>
<section><h2>要换行的时候：置换矩阵<a href="#要换行的时候置换矩阵"><span>#</span></a></h2><p>他在这一讲的最后一段，转向了换行：什么时候要换行？主元位置出现 0 的时候。执行换行的矩阵叫置换矩阵，它们是单位矩阵的行重新排列。他让同学一个一个报出所有 <span><span>3×33 \times 3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 的置换矩阵：单位矩阵，换第 1、2 行，换第 1、3 行，换第 2、3 行，再加两个把三行轮换的，共 6 个。如果问 <span><span>4×44 \times 4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 呢？他让大家猜，答案是 24。</p><p>他还指出几个事实：两个置换矩阵相乘，还是置换矩阵（做完一些换行再做一些换行，总的来说还是换行）；逆也还在这个集合里，所以它们构成一个“群”；换两行的矩阵，逆是它自己；而且 <span><span>P−1=PTP^{-1} = P^T</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，他说这是一个“奇怪的事实”，这一讲里只指出，没有证明。下面用 <code>itertools.permutations</code> 数一遍，并验证这几件事；最后用一个主元为 0 的矩阵，演示不换行的分解会停在第一步，而先乘一个 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 就能做下去：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> itertools</span></div></div><div><div><div>2</div></div><div><span>import</span><span> math</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>def</span><span> </span><span>lu_nopivot</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; tuple[np.ndarray, np.ndarray]:</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span><span> np.</span><span>isclose</span><span>(U[k, k], </span></span><span>0.0</span><span>):</span></div></div><div><div><div>13</div></div><div><span>            </span><span>raise</span><span> </span><span>ZeroDivisionError</span><span>(</span><span>f</span><span>"第 </span><span>{</span><span>k</span><span>}</span><span> 个主元是 0，需要换行"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(k </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>L[i, k] </span><span>=</span><span> U[i, k] </span><span>/</span><span> U[k, k]</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>U[i, k:] </span><span>-=</span><span> L[i, k] </span><span>*</span><span> U[k, k:]</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> L, U</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span># 3 x 3 的置换矩阵：把单位矩阵的行重新排一遍</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>perms </span><span>=</span><span> [identity[</span><span>list</span><span>(p)] </span><span>for</span><span> p </span><span>in</span><span><span> itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(</span><span>3</span><span>))]</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"3 x 3 置换矩阵的个数："</span><span>, </span><span>len</span><span>(perms))</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> n </span><span>in</span><span> [</span><span>4</span><span>, </span><span>5</span><span>, </span><span>6</span><span>]:</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>count </span><span>=</span><span> </span><span>len</span><span>(</span><span>list</span><span><span>(itertools.</span><span>permutations</span><span>(</span></span><span>range</span><span>(n))))</span></div></div><div><div><div>29</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>n</span><span>}</span><span> x </span><span>{</span><span>n</span><span>}</span><span>：</span><span>{</span><span>count</span><span>}</span><span> 个，n! = </span><span>{</span><span><span>math.</span><span>factorial</span><span>(n)</span></span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span># 乘起来还在这个集合里，逆就是转置</span></div></div><div><div><div>32</div></div><div><span>    </span><span>def</span><span> </span><span>in_list</span><span>(</span><span>M</span><span>:</span><span> np.ndarray</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>33</div></div><div><span>        </span><span>return</span><span> </span><span>any</span><span><span>(np.</span><span>array_equal</span><span>(M, P) </span></span><span>for</span><span> P </span><span>in</span><span> perms)</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>closed </span><span>=</span><span> </span><span>all</span><span><span>(</span><span>in_list</span><span>(P </span><span>@</span><span> Q) </span></span><span>for</span><span> P </span><span>in</span><span> perms </span><span>for</span><span> Q </span><span>in</span><span> perms)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>inverse_is_transpose </span><span>=</span><span> </span><span>all</span><span>(</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>np.</span><span>array_equal</span><span>(np.linalg.</span><span>inv</span><span>(P), P.T) </span><span>for</span><span> P </span><span>in</span><span> perms</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>print</span><span>(</span><span>"任意两个相乘仍是置换矩阵："</span><span>, closed)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"P 的逆等于 P 的转置："</span><span>, inverse_is_transpose)</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span># 主元位置出现 0：不换行的消元走不下去，先换行就行</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>1.0</span><span>, </span><span>1.0</span><span>, </span><span>1.0</span><span>], [</span><span>2.0</span><span>, </span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>44</div></div><div><span>    </span><span>try</span><span>:</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>lu_nopivot</span><span>(A)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>except</span><span> </span><span>ZeroDivisionError</span><span> </span><span>as</span><span> error:</span></div></div><div><div><div>47</div></div><div><span>        </span><span>print</span><span>(</span><span>"直接分解："</span><span>, error)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>L, U </span><span>=</span><span> </span><span>lu_nopivot</span><span>(P </span><span>@</span><span> A)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>print</span><span>(</span><span>"L ="</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>print</span><span>(L)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>print</span><span>(</span><span>"U ="</span><span>)</span></div></div><div><div><div>53</div></div><div><span>    </span><span>print</span><span>(U)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>print</span><span>(</span><span>"P A = L U："</span><span><span>, np.</span><span>allclose</span><span>(P </span><span>@</span><span> A, L </span><span>@</span><span> U))</span></span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>3 x 3 置换矩阵的个数： 6</span></div></div><div><div><div>2</div></div><div><span>4 x 4：24 个，n! = 24</span></div></div><div><div><div>3</div></div><div><span>5 x 5：120 个，n! = 120</span></div></div><div><div><div>4</div></div><div><span>6 x 6：720 个，n! = 720</span></div></div><div><div><div>5</div></div><div><span>任意两个相乘仍是置换矩阵： True</span></div></div><div><div><div>6</div></div><div><span>P 的逆等于 P 的转置： True</span></div></div><div><div><div>7</div></div><div><span>直接分解： 第 0 个主元是 0，需要换行</span></div></div><div><div><div>8</div></div><div><span>L =</span></div></div><div><div><div>9</div></div><div><span>[[ 1.   0.   0. ]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 0.   1.   0. ]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 2.  -0.5  1. ]]</span></div></div><div><div><div>12</div></div><div><span>U =</span></div></div><div><div><div>13</div></div><div><span>[[1.  1.  1. ]</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[0.  2.  1. ]</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[0.  0.  1.5]]</span></div></div><div><div><div>16</div></div><div><span>P A = L U： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出的第一行是 6，往下 24、120、720 就是 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span>。他在讲“怎么数运算次数”的时候顺手说过，行列式的代价才是 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span>。这里的 <span><span>n!n!</span><span><span><span></span><span>n</span><span>!</span></span></span></span> 只是置换矩阵的个数，我们并不会逐个去试它们，消元每一步只选一行。最后一段里 <span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 的乘数是 0、2、<span><span>−0.5-0.5</span><span><span><span></span><span>−</span><span>0.5</span></span></span></span>，和上一节是同一个记账法，只是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的行先被 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 换了位置。据我所知，下一讲会把这个形式正式写成 <span><span>PA=LUPA = LU</span><span><span><span></span><span>P</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>。</p><p>下一讲他说会结束第二章，转到第三章，也就是向量空间。转置和置换的部分在第 5 讲里收尾，同时引入子空间，见 <a href="/posts/linear-algebra-05-transposes-permutations-spaces/">18.06 第 5 讲：转置、置换与向量空间</a>。（他在这一讲开头就说过，他担心大家以为这门课全是消元。）</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-03-multiplication-and-inverse/</id>
      <title type="text">18.06 第 3 讲：矩阵乘法的四种看法与逆矩阵</title>
      <published>2026-10-18T00:40:00.000Z</published>
      <updated>2026-10-18T00:40:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-03-multiplication-and-inverse/"/>
      <summary type="text">MIT 18.06 第 3 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把矩阵乘法的五种算法逐一对照 A @ B，再手写 Gauss-Jordan 求逆，把奇异矩阵的 6 改成 6 + 1e-8 之后，逆里最大的元素涨到了 6e+08。</summary>
      <content type="html"><![CDATA[<p>这一讲有两个想法。第一，<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 怎么算都行：按元素、按列、按行、按“列乘行再相加”、按分块，结果相同，但每一种看法回答的问题不一样。第二，求 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 就是同时解 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个方程组，Gauss-Jordan 把它们并排放在一起消元。我把五种乘法算法各写了一遍，和 <code>A @ B</code> 的差都在 <span><span>1.4×10−151.4 \times 10^{-15}</span><span><span><span></span><span>1.4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 以内；再把奇异矩阵 <span><span>[1326]\begin{bmatrix} 1 &amp; 3 \\ 2 &amp; 6 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 的 6 改成 <span><span>6+10−86 + 10^{-8}</span><span><span><span></span><span>6</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>，逆矩阵里最大的元素就从“不存在”变成了 <span><span>6×1086 \times 10^{8}</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=FX4C-JpTFgY" target="_blank">Lecture 3: Multiplication and inverse matrices</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-3-multiplication-and-inverse-matrices/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §1.4 Matrix Multiplication AB and CR；§2.2 Elimination Matrices and Inverse Matrices
前置：<a href="/posts/linear-algebra-02-elimination-with-matrices/">第 2 讲：用矩阵做消元</a>
一句话：<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 的每一列是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列的组合，每一行是 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的行的组合；方阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 没有逆，当且仅当存在非零的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 使 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；对 <span><span>[A I][A\ I]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>I</span><span>]</span></span></span></span> 消元到左边变成 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，右边就是 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头他把 <span><span>c34c_{34}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>34</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的下标公式写了一遍，又讲了形状规则（<span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 乘 <span><span>n×pn \times p</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span> 得 <span><span>m×pm \times p</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span>）。这一段熟悉的人可以倍速，但“按列、按行”两个看法请认真听，后面整门课都在用。</li>
<li>“列乘行”那一段要看：他拿 <span><span>(2,3,4)(2, 3, 4)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 乘 <span><span>(1,6)(1, 6)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span> 做例子，顺便说了“所有行落在同一条直线上”，这是第 11 讲秩 1 矩阵的伏笔。</li>
<li>分块乘法很短，本篇用代码验证过了，倍速即可。</li>
<li>讲逆矩阵时，他给奇异矩阵找了三个理由，其中“有非零向量被送到零”是他最喜欢的。这一段要一字不漏地听。Gauss-Jordan 的消元步骤本篇会重算，看他怎么解释“为什么右边会变成逆”就够了。</li>
</ul></div></div>
<section><h2>元素、列、行：三种看法<a href="#元素列行三种看法"><span>#</span></a></h2><p>先是最熟悉的那种。<span><span>C=ABC = AB</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>B</span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列，是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行与 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 列的点积。他取了 <span><span>c34c_{34}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>34</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做例子，把它写成求和：</p><span><span><span>c34=a31b14+a32b24+⋯+a3nbn4=∑k=1na3kbk4c_{34} = a_{31}b_{14} + a_{32}b_{24} + \cdots + a_{3n}b_{n4}
= \sum_{k=1}^{n} a_{3k}b_{k4}</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span><span>34</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>31</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>24</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>⋯</span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>3</span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>n</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>n</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span>3</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>k</span><span>4</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>求和指标 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 沿着 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的行走，同时沿着 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的列走，所以 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列数必须等于 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的行数。设 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 是 <span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span>，<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 是 <span><span>n×pn \times p</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span>，那么 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 是 <span><span>m×pm \times p</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span>，一共 <span><span>mpmp</span><span><span><span></span><span>m</span><span>p</span></span></span></span> 个元素，每个元素 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 次乘法。这是“大家都这么算”的方式。他自己也这么算，不过他说整门课他几乎不下到元素这一层，因为整列整行的看法更有用。</p><p><strong>按列看是什么样的？</strong> 把 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 看成 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 列并排放着。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘第一列，得到 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第一列；<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘第二列，得到 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第二列，以此类推。矩阵乘向量我们在第 1 讲已经会了，它就是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列的线性组合，所以 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的每一列都是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列的组合，组合系数就是 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 对应那一列里的数。他还留意了一个形状上的事实：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 个分量，<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的列也有 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 个分量，对得上。</p><p><strong>按行看呢？</strong> 完全对称。<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行乘整个 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span>，得到 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行，它是 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的各行的组合，系数是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 行里的数。他的说法是：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 在左边乘，是在“混合”<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的行。</p><p>这两句话可以记成：左乘动的是行，右乘动的是列。上一讲的消元矩阵 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 从左边乘上去，“第二行减去 3 倍第一行”就是行操作，正是因为这个。</p></section>
<section><h2>列乘行与分块<a href="#列乘行与分块"><span>#</span></a></h2><p><strong>第四种看法：列乘行。</strong> 如果 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的一列（<span><span>m×1m \times 1</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span>）乘 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的一行（<span><span>1×p1 \times p</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span>），得到的是一个 <span><span>m×pm \times p</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span> 的整块矩阵，和行乘列得到一个数完全不同。他的例子是列 <span><span>(2,3,4)(2, 3, 4)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 乘行 <span><span>(1,6)(1, 6)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>：</p><span><span><span>[234][16]=[212318424]\begin{bmatrix} 2 \\ 3 \\ 4 \end{bmatrix}
\begin{bmatrix} 1 &amp; 6 \end{bmatrix}
= \begin{bmatrix} 2 &amp; 12 \\ 3 &amp; 18 \\ 4 &amp; 24 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>12</span></span></span><span><span></span><span><span>18</span></span></span><span><span></span><span><span>24</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>这个矩阵的每一列都是 <span><span>(2,3,4)(2, 3, 4)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 的倍数，每一行都是 <span><span>(1,6)(1, 6)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span> 的倍数。所有的行落在同一条直线上，所有的列也落在同一条直线上，他说这是“很极简的矩阵”。第四种看法是：<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 等于 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 列乘 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 行，<span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 从 1 加到 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>。他的例子取 <span><span>A=[273849]A = \begin{bmatrix} 2 &amp; 7 \\ 3 &amp; 8 \\ 4 &amp; 9 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>9</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span>，<span><span>B=[1600]B = \begin{bmatrix} 1 &amp; 6 \\ 0 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，第二项因为 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第二行是零，整个变成零矩阵，乘积就是第一项。我把这两个例子交给 NumPy：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span># 一列乘一行：Strang 的例子，列 (2, 3, 4)，行 (1, 6)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>3.0</span><span>, </span><span>4.0</span><span>])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>6.0</span><span>])</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>outer</span><span>(u, v)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"列 (2, 3, 4) 乘行 (1, 6)："</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(M)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(M))</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"每一列是 u 的倍数，倍数就是 v："</span><span><span>, np.</span><span>allclose</span><span>(M, u[:, </span></span><span>None</span><span><span>] </span><span>*</span><span> v))</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>"第 i 行除以 u[i] 都得到 v："</span><span><span>, np.</span><span>allclose</span><span>(M </span><span>/</span><span> u[:, </span></span><span>None</span><span>], v))</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 两列乘两行：第二项是零矩阵，和为第一项</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>7.0</span><span>], [</span><span>3.0</span><span>, </span><span>8.0</span><span>], [</span><span>4.0</span><span>, </span><span>9.0</span><span>]])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>6.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>first </span><span>=</span><span> np.</span><span>outer</span><span>(A[:, </span><span>0</span><span>], B[</span><span>0</span><span>, :])</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>second </span><span>=</span><span> np.</span><span>outer</span><span>(A[:, </span><span>1</span><span>], B[</span><span>1</span><span>, :])</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"第一列乘第一行："</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(first)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"第二列乘第二行："</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span>(second)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"两项之和等于 A @ B："</span><span><span>, np.</span><span>allclose</span><span>(first </span><span>+</span><span> second, A </span><span>@</span><span> B))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ B 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A </span><span>@</span><span> B))</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>列 (2, 3, 4) 乘行 (1, 6)：</span></div></div><div><div><div>2</div></div><div><span>[[ 2. 12.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 3. 18.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 4. 24.]]</span></div></div><div><div><div>5</div></div><div><span>秩： 1</span></div></div><div><div><div>6</div></div><div><span>每一列是 u 的倍数，倍数就是 v： True</span></div></div><div><div><div>7</div></div><div><span>第 i 行除以 u[i] 都得到 v： True</span></div></div><div><div><div>8</div></div><div><span>第一列乘第一行：</span></div></div><div><div><div>9</div></div><div><span>[[ 2. 12.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 3. 18.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[ 4. 24.]]</span></div></div><div><div><div>12</div></div><div><span>第二列乘第二行：</span></div></div><div><div><div>13</div></div><div><span>[[0. 0.]</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[0. 0.]</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>[0. 0.]]</span></div></div><div><div><div>16</div></div><div><span>两项之和等于 A @ B： True</span></div></div><div><div><div>17</div></div><div><span>A @ B 的秩： 1</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两件事。<span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 的秩是 1，每一列、每一行都是同一个向量的倍数，和他说的一样。而 <span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 的秩也是 1，虽然 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的两列并不平行：右边的 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 把“第二列”的贡献清零了，乘积的列空间就退化成了一条线。“乘积的秩不超过因子的秩”这件事，后面讲秩的时候还会回来。</p><p><strong>分块乘法。</strong> 最后一种看法是把矩阵切成块，块与块之间按“行乘列”的规则乘，只要切法能对上。他取两个 <span><span>20×2020 \times 20</span><span><span><span></span><span>20</span><span></span><span>×</span><span></span></span><span><span></span><span>20</span></span></span></span> 的方阵，各切成四个 <span><span>10×1010 \times 10</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>10</span></span></span></span> 的块，左上角的块是 <span><span>A1B1+A2B3A_1B_1 + A_2B_3</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。他的原话大意是：连 Gauss 也不可能一眼看出这是对的，但检查一遍，五种方法做的其实是同样的乘法，只是记账顺序不同。</p><p>标题写“四种”，是因为他自己数到“第四种”就收住了，分块算顺带的；我把它也算上，一共五种。这个“同样的乘法”可以数出来：<span><span>m×nm \times n</span><span><span><span></span><span>m</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 乘 <span><span>n×pn \times p</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>p</span></span></span></span> 总共 <span><span>mnpmnp</span><span><span><span></span><span>mn</span><span>p</span></span></span></span> 次标量乘法，按元素、按列、按行、按列乘行、分块，一次不多一次不少。区别只在于你把它们分成了什么形状的包。下面我把五种算法各写成一个函数，用一个固定种子的随机 <span><span>4×64 \times 6</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 乘 <span><span>6×46 \times 4</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 矩阵，和 <code>A @ B</code> 对比：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>by_entries</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>B</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""第一种：C[i, j] 是 A 的第 i 行点乘 B 的第 j 列。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>m, n </span><span>=</span><span> A.shape</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> B.shape[</span><span>1</span><span>]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> np.</span><span>zeros</span><span>((m, p))</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(p):</span></div></div><div><div><div>11</div></div><div><span>            </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>C[i, j] </span><span>+=</span><span> A[i, k] </span><span>*</span><span> B[k, j]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> C</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>by_columns</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>B</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>17</div></div><div><span>    </span><span>"""第二种：A 乘 B 的每一列，得到 C 的对应列。"""</span></div></div><div><div><div>18</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>column_stack</span><span>([A </span><span>@</span><span> B[:, j] </span></span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(B.shape[</span><span>1</span><span>])])</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>def</span><span> </span><span>by_rows</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>B</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>22</div></div><div><span>    </span><span>"""第三种：A 的每一行乘 B，得到 C 的对应行。"""</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>vstack</span><span>([A[i, :] </span><span>@</span><span> B </span></span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>0</span><span>])])</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>by_outer</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>B</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>27</div></div><div><span>    </span><span>"""第四种：A 的第 k 列乘 B 的第 k 行，再把 n 个矩阵加起来。"""</span></div></div><div><div><div>28</div></div><div><span>    </span><span>return</span><span> </span><span>sum</span><span><span>(np.</span><span>outer</span><span>(A[:, k], B[k, :]) </span></span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(A.shape[</span><span>1</span><span>]))</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>def</span><span> </span><span>by_blocks</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>B</span></span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>32</div></div><div><span>    </span><span>"""第五种：切成 2 x 2 个块，块和块按行乘列的规则相乘。"""</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>A1, A2 </span><span>=</span><span> A[:</span><span>2</span><span>, :</span><span>3</span><span>], A[:</span><span>2</span><span>, </span><span>3</span><span>:]</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>A3, A4 </span><span>=</span><span> A[</span><span>2</span><span>:, :</span><span>3</span><span>], A[</span><span>2</span><span>:, </span><span>3</span><span>:]</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>B1, B2 </span><span>=</span><span> B[:</span><span>3</span><span>, :</span><span>2</span><span>], B[:</span><span>3</span><span>, </span><span>2</span><span>:]</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>B3, B4 </span><span>=</span><span> B[</span><span>3</span><span>:, :</span><span>2</span><span>], B[</span><span>3</span><span>:, </span><span>2</span><span>:]</span></div></div><div><div><div>37</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>block</span><span>(</span></span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>[A1 </span><span>@</span><span> B1 </span><span>+</span><span> A2 </span><span>@</span><span> B3, A1 </span><span>@</span><span> B2 </span><span>+</span><span> A2 </span><span>@</span><span> B4],</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>[A3 </span><span>@</span><span> B1 </span><span>+</span><span> A4 </span><span>@</span><span> B3, A3 </span><span>@</span><span> B2 </span><span>+</span><span> A4 </span><span>@</span><span> B4],</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>m, n, p </span><span>=</span><span> </span><span>4</span><span>, </span><span>6</span><span>, </span><span>4</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((m, n))</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((n, p))</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> A </span><span>@</span><span> B</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>methods </span><span>=</span><span> [</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>(</span><span>"元素"</span><span>, by_entries),</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>(</span><span>"列"</span><span>, by_columns),</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>(</span><span>"行"</span><span>, by_rows),</span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>(</span><span>"列乘行之和"</span><span>, by_outer),</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>(</span><span>"分块"</span><span>, by_blocks),</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>60</div></div><div><span>    </span><span>for</span><span> name, method </span><span>in</span><span> methods:</span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(</span><span>method</span><span>(A, B) </span><span>-</span><span> C)</span></div></div><div><div><div>62</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>:6s</span><span>}</span><span> 与 A @ B 的差：</span><span>{</span><span>err</span><span>:.2e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>    </span><span># 每一种算法做的标量乘法一样多：m * n * p</span></div></div><div><div><div>65</div></div><div><span>    </span><span>print</span><span>(</span><span>"标量乘法次数 m * n * p ="</span><span><span>, m </span><span>*</span><span> n </span><span>*</span><span> p)</span></span></div></div><div><div><div>66</div></div><div><span>    </span><span>print</span><span>(</span><span>"按列算：C 的第 0 列是 A 的各列的组合，系数是 B 的第 0 列"</span><span>)</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>combo </span><span>=</span><span> </span><span>sum</span><span>(B[k, </span><span>0</span><span><span>] </span><span>*</span><span> A[:, k] </span></span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n))</span></div></div><div><div><div>68</div></div><div><span>    </span><span>print</span><span>(</span><span>"  吻合："</span><span><span>, np.</span><span>allclose</span><span>(combo, C[:, </span></span><span>0</span><span>]))</span></div></div><div><div><div>69</div></div><div><span>    </span><span>print</span><span>(</span><span>"按行算：C 的第 0 行是 B 的各行的组合，系数是 A 的第 0 行"</span><span>)</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>combo </span><span>=</span><span> </span><span>sum</span><span>(A[</span><span>0</span><span><span>, k] </span><span>*</span><span> B[k, :] </span></span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n))</span></div></div><div><div><div>71</div></div><div><span>    </span><span>print</span><span>(</span><span>"  吻合："</span><span><span>, np.</span><span>allclose</span><span>(combo, C[</span></span><span>0</span><span>, :]))</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div>
</div></div><div><div><div>74</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>元素     与 A @ B 的差：1.39e-15</span></div></div><div><div><div>2</div></div><div><span>列      与 A @ B 的差：1.37e-15</span></div></div><div><div><div>3</div></div><div><span>行      与 A @ B 的差：1.13e-15</span></div></div><div><div><div>4</div></div><div><span>列乘行之和  与 A @ B 的差：1.39e-15</span></div></div><div><div><div>5</div></div><div><span>分块     与 A @ B 的差：1.29e-15</span></div></div><div><div><div>6</div></div><div><span>标量乘法次数 m * n * p = 96</span></div></div><div><div><div>7</div></div><div><span>按列算：C 的第 0 列是 A 的各列的组合，系数是 B 的第 0 列</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>吻合： True</span></div></div><div><div><div>9</div></div><div><span>按行算：C 的第 0 行是 B 的各行的组合，系数是 A 的第 0 行</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>吻合： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>五个差都在 <span><span>10−1510^{-15}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 量级，就是双精度浮点的舍入噪声：加法顺序不一样，末位当然不会完全一样。所以我没有用 <code>==</code>，而是看差的范数（这几个数字谁小谁大没有意义，别盯着看）。标量乘法的次数是 <span><span>4×6×4=964 \times 6 \times 4 = 96</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span><span></span><span>=</span><span></span></span><span><span></span><span>96</span></span></span></span>，五种算法都是这个数。最后两行把按列、按行两种看法直接写成“组合”的形式：<span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第 0 列是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的各列按 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 第 0 列的系数加权，第 0 行是 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的各行按 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 第 0 行的系数加权，都是 <code>True</code>。</p><p>我猜 Strang 在第 3 讲就把五种看法摆出来，是因为后面几乎每个定理都挑其中一种来证：证“<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 的列空间包含在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列空间里”用列，证 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span> 是消元的记录用行，列乘行则要到 SVD 才大放异彩。</p></section>
<section><h2>没有逆的矩阵<a href="#没有逆的矩阵"><span>#</span></a></h2><p>接下来是逆矩阵。方阵 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 如果有逆，<span><span>A−1A=IA^{-1}A = I</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，也有 <span><span>AA−1=IAA^{-1} = I</span><span><span><span></span><span>A</span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。他特意说了一句：对方阵，左逆一定也是右逆，这件事不容易证明，但是成立；对长方形矩阵，形状就不允许左逆等于右逆，后面（第 33 讲）会看到有左逆没有右逆的情形。</p><p>最重要的问题是：什么时候没有逆？他先给了一个反例，<span><span>A=[1326]A = \begin{bmatrix} 1 &amp; 3 \\ 2 &amp; 6 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，并要大家给出理由。他一共给了三个，我按他的顺序写下来：</p><ol>
<li>行列式是 <span><span>1×6−3×2=01 \times 6 - 3 \times 2 = 0</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。他说这是“如果你知道行列式的话”，言下之意是他现在还不想用它。</li>
<li>如果 <span><span>AB=IAB = I</span><span><span><span></span><span>A</span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，<span><span>ABAB</span><span><span><span></span><span>A</span><span>B</span></span></span></span> 的每一列都是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列的组合；而 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的两列 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(3,6)(3, 6)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span> 在同一条直线上，<span><span>II</span><span><span><span></span><span>I</span></span></span></span> 的第一列 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 不在这条线上，组合不出来。</li>
<li>存在非零向量 <span><span>x=(3,−1)\mathbf{x} = (3, -1)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，使 <span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，也就是 3 份列一减 1 份列二恰好是零。如果 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 存在，两边左乘它，<span><span>x=A−1Ax=A−10=0\mathbf{x} = A^{-1}A\mathbf{x} = A^{-1}\mathbf{0} = \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，和 <span><span>x≠0\mathbf{x} \ne \mathbf{0}</span><span><span><span></span><span>x</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> 矛盾。</li>
</ol><p>他说第三个理由是自己最喜欢的：“数学里所有关键的方程，右边都是零。”<span><span>Ax=0A\mathbf{x} = \mathbf{0}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的非零解，是奇异矩阵最直接的证据：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 把 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 压成了零，没有任何 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 能把零再变回非零的 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span>。三个理由我都让 NumPy 过了一遍：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>6.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>3.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>print</span><span>(</span><span>"行列式："</span><span><span>, np.linalg.</span><span>det</span><span>(A))</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x ="</span><span><span>, A </span><span>@</span><span> x, </span></span><span>"，而 x 不是零向量"</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"3 * 列一 - 1 * 列二 ="</span><span>, </span><span>3</span><span><span> </span><span>*</span><span> A[:, </span></span><span>0</span><span><span>] </span><span>-</span><span> A[:, </span></span><span>1</span><span>])</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span># 列图：单位矩阵的第一列 (1, 0) 能不能由 A 的列组合出来</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>e1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>coef, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(A, e1, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"最接近 (1, 0) 的列组合："</span><span><span>, A </span><span>@</span><span> coef)</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"差了：</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> coef </span><span>-</span><span> e1)</span></span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>try</span><span>:</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>np.linalg.</span><span>inv</span><span>(A)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>except</span><span> np.linalg.LinAlgError </span><span>as</span><span> err:</span></div></div><div><div><div>23</div></div><div><span>        </span><span>print</span><span>(</span><span>"np.linalg.inv 报错："</span><span>, err)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span># 如果 A 有逆，x 会被 A 的逆拉回零：A 的逆 @ (A @ x) = x</span></div></div><div><div><div>26</div></div><div><span>    </span><span># 但 A @ x 是零向量，零向量乘任何矩阵还是零向量，x 回不来</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"零向量乘以任何矩阵："</span><span><span>, np.</span><span>zeros</span><span>((</span></span><span>2</span><span>, </span><span>2</span><span><span>)) </span><span>@</span><span> (A </span><span>@</span><span> x))</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>行列式： 0.0</span></div></div><div><div><div>2</div></div><div><span>秩： 1</span></div></div><div><div><div>3</div></div><div><span>A @ x = [0. 0.] ，而 x 不是零向量</span></div></div><div><div><div>4</div></div><div><span>3 * 列一 - 1 * 列二 = [0. 0.]</span></div></div><div><div><div>5</div></div><div><span>最接近 (1, 0) 的列组合： [0.2 0.4]</span></div></div><div><div><div>6</div></div><div><span>差了：0.8944</span></div></div><div><div><div>7</div></div><div><span>np.linalg.inv 报错： Singular matrix</span></div></div><div><div><div>8</div></div><div><span>零向量乘以任何矩阵： [0. 0.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>行列式 0.0，秩 1。<span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 确实是零向量而 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 不是。最小二乘能找到的离 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 最近的列组合是 <span><span>(0.2,0.4)(0.2, 0.4)</span><span><span><span></span><span>(</span><span>0.2</span><span>,</span><span></span><span>0.4</span><span>)</span></span></span></span>（也就是 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 在那条线上的投影），差了 0.8944，是 <span><span>2/52/\sqrt{5}</span><span><span><span></span><span>2/</span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。<code>np.linalg.inv</code> 没有试图给出什么答案，直接报 <code>Singular matrix</code>。这里“差了 0.8944”和第 1 讲里的 0.4082 是同一类东西：右端项到列空间的距离。</p></section>
<section><h2>Gauss-Jordan：两个方程组一起解<a href="#gauss-jordan两个方程组一起解"><span>#</span></a></h2><p>换一个能求逆的矩阵。他把刚才的 6 换成了 7（他说 7 是幸运数字）：</p><span><span><span>A=[1327]A = \begin{bmatrix} 1 &amp; 3 \\ 2 &amp; 7 \end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>要找的 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 有四个未知数。他的做法是按列看：<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的第一列，得到 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 的第一列 <span><span>(1,0)(1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>；<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的第二列，得到 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 的第二列 <span><span>(0,1)(0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。所以求逆就是解两个方程组，系数矩阵都是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，只是右端项不同。<span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 矩阵就是 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个方程组。</p><p>Gauss 解一个方程组时，会把右端项作为增广列贴在矩阵旁边。Jordan 的想法是：既然右端项不止一个，把它们一起贴上去，得到 <span><span>[A I][A\ I]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>I</span><span>]</span></span></span></span>，只做一遍消元。他的步骤是：</p><ol>
<li>第二行减去 2 倍第一行，得到 <span><span>[131001−21]\begin{bmatrix} 1 &amp; 3 &amp; 1 &amp; 0 \\ 0 &amp; 1 &amp; -2 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。左半边已经是上三角了，他说 Gauss 到此就收工了。</li>
<li>“Jordan 说继续。”第一行减去 3 倍第二行，消掉上面的 3，得到 <span><span>[107−301−21]\begin{bmatrix} 1 &amp; 0 &amp; 7 &amp; -3 \\ 0 &amp; 1 &amp; -2 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。</li>
</ol><p>左半边变成了 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，所以右半边是 <span><span>A−1=[7−3−21]A^{-1} = \begin{bmatrix} 7 &amp; -3 \\ -2 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>7</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>3</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>。他当场验算了 <span><span>7−6=17 - 6 = 1</span><span><span><span></span><span>7</span><span></span><span>−</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>21−21=021 - 21 = 0</span><span><span><span></span><span>21</span><span></span><span>−</span><span></span></span><span><span></span><span>21</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>−2+2=0-2 + 2 = 0</span><span><span><span></span><span>−</span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>−6+7=1-6 + 7 = 1</span><span><span><span></span><span>−</span><span>6</span><span></span><span>+</span><span></span></span><span><span></span><span>7</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。</p><p>我在 NumPy 里做了三件事：逐列解两个方程组、用上一讲的方式把两步写成 <span><span>E21E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>E12E_{12}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 并乘上去、和 <code>np.linalg.inv</code> 对比。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>7.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>identity </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>2</span><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># 求逆就是解两个方程组：A @ (逆的第 j 列) = 单位矩阵的第 j 列</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>columns </span><span>=</span><span> [np.linalg.</span><span>solve</span><span>(A, identity[:, j]) </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>)]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>stacked </span><span>=</span><span> np.</span><span>column_stack</span><span>(columns)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"逐列解出的 A 的逆："</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(stacked)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># Gauss-Jordan：两个方程组并排放进 [A | I]，一起消元</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>aug </span><span>=</span><span> np.</span><span>hstack</span><span>([A, identity])</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"[A | I] ="</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(aug)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>E21 </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>2.0</span><span>, </span><span>1.0</span><span>]])  </span><span># 第二行减去 2 倍第一行</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>aug </span><span>=</span><span> E21 </span><span>@</span><span> aug</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"向下消元后（上三角，Gauss 到此为止）："</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(aug)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>E12 </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>], [</span><span>0.0</span><span>, </span><span>1.0</span><span>]])  </span><span># 第一行减去 3 倍第二行</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>aug </span><span>=</span><span> E12 </span><span>@</span><span> aug</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"再向上消元后（Jordan 继续）："</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(aug)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>inverse </span><span>=</span><span> aug[:, </span><span>2</span><span>:]</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> E12 </span><span>@</span><span> E21</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"右半边就是 E = E12 @ E21："</span><span><span>, np.</span><span>allclose</span><span>(inverse, E))</span></span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span><span>"E @ A 等于单位矩阵："</span><span><span>, np.</span><span>allclose</span><span>(E </span><span>@</span><span> A, identity))</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>print</span><span>(</span><span>"和 np.linalg.inv 一致："</span><span><span>, np.</span><span>allclose</span><span>(inverse, np.linalg.</span><span>inv</span><span>(A)))</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"和逐列解出的一致："</span><span><span>, np.</span><span>allclose</span><span>(inverse, stacked))</span></span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ 逆 的误差："</span><span><span>, np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> inverse </span><span>-</span><span> identity))</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>逐列解出的 A 的逆：</span></div></div><div><div><div>2</div></div><div><span>[[ 7. -3.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[-2.  1.]]</span></div></div><div><div><div>4</div></div><div><span>[A | I] =</span></div></div><div><div><div>5</div></div><div><span>[[1. 3. 1. 0.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[2. 7. 0. 1.]]</span></div></div><div><div><div>7</div></div><div><span>向下消元后（上三角，Gauss 到此为止）：</span></div></div><div><div><div>8</div></div><div><span>[[ 1.  3.  1.  0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[ 0.  1. -2.  1.]]</span></div></div><div><div><div>10</div></div><div><span>再向上消元后（Jordan 继续）：</span></div></div><div><div><div>11</div></div><div><span>[[ 1.  0.  7. -3.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[ 0.  1. -2.  1.]]</span></div></div><div><div><div>13</div></div><div><span>右半边就是 E = E12 @ E21： True</span></div></div><div><div><div>14</div></div><div><span>E @ A 等于单位矩阵： True</span></div></div><div><div><div>15</div></div><div><span>和 np.linalg.inv 一致： True</span></div></div><div><div><div>16</div></div><div><span>和逐列解出的一致： True</span></div></div><div><div><div>17</div></div><div><span>A @ 逆 的误差： 0.0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四个对比全是 <code>True</code>，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘逆与 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 的误差恰好是 0.0（这几个整数运算没有舍入误差）。有意思的是前两种得到的东西一模一样：逐列解方程组得到的逆，和 <span><span>[A I][A\ I]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>I</span><span>]</span></span></span></span> 消元得到的逆是同一个矩阵，因为它们根本是同一件事的两种写法。</p></section>
<section><h2>为什么右边会变成逆<a href="#为什么右边会变成逆"><span>#</span></a></h2><p><strong>为什么右半边恰好是 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>？</strong> 他在最后几分钟回答了这个问题，我觉得这是这一讲最漂亮的地方。每一步消元都是左乘一个初等矩阵。把所有步骤的乘积记成 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span>，对 <span><span>[A I][A\ I]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>I</span><span>]</span></span></span></span> 做完消元就是</p><span><span><span>E [A I]=[EA  EI]=[I  E]E\,[A\ I] = [EA\ \ EI] = [I\ \ E]</span><span><span><span></span><span>E</span><span></span><span>[</span><span>A</span><span> </span><span>I</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>E</span><span>A</span><span> </span><span> </span><span>E</span><span>I</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>I</span><span> </span><span> </span><span>E</span><span>]</span></span></span></span></span><p>左半边 <span><span>EA=IEA = I</span><span><span><span></span><span>E</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>，所以 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 就是 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>（他问学生，学生答对了）。右半边本来是 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，被同一串操作作用之后变成 <span><span>EI=E=A−1EI = E = A^{-1}</span><span><span><span></span><span>E</span><span>I</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>。我们前面的代码里，<span><span>E=E12E21E = E_{12}E_{21}</span><span><span><span></span><span>E</span><span></span><span>=</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与右半边相等，这就是这句话的具体数值。</p><p>我喜欢把 <span><span>[A I][A\ I]</span><span><span><span></span><span>[</span><span>A</span><span> </span><span>I</span><span>]</span></span></span></span> 想成一本分左右两栏的账：左栏每改一笔，右栏同步记一笔。等左栏平成 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，右栏记下的整本流水，就是 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p>前面写的是 <span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 手算。它没有换行，主元也都是 1。我把这个想法写成能处理 <span><span>n×nn \times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的函数，加上选主元换行，并在同一个脚本里，用 NumPy 做三件事：和 <code>np.linalg.inv</code> 对照；喂给它奇异矩阵，看它在哪里停下；把 6 改成 <span><span>6+ε6 + \varepsilon</span><span><span><span></span><span>6</span><span></span><span>+</span><span></span></span><span><span></span><span>ε</span></span></span></span>，看逆里的数怎么长。（<span><span>2×22 \times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的手算比写这个函数快得多，但手算不会告诉你 <span><span>ε\varepsilon</span><span><span><span></span><span>ε</span></span></span></span> 缩小时会发生什么。）</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>gauss_jordan_inverse</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>) -&gt; np.ndarray:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""对 [A | I] 做 Gauss-Jordan 消元，左半边变成 I 时右半边就是逆。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> A.shape[</span><span>0</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>aug </span><span>=</span><span> np.</span><span>hstack</span><span>([A.</span><span>astype</span><span>(</span><span>float</span><span><span>), np.</span><span>eye</span><span>(n)])</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> col </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>9</div></div><div><span>        </span><span># 选列里绝对值最大的元素做主元，必要时换行</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>pivot_row </span><span>=</span><span> col </span><span>+</span><span> np.</span><span>argmax</span><span>(np.</span><span>abs</span><span>(aug[col:, col]))</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> np.</span><span>isclose</span><span>(aug[pivot_row, col], </span></span><span>0.0</span><span>, </span><span>atol</span><span>=</span><span>1e-12</span><span>):</span></div></div><div><div><div>12</div></div><div><span>            </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"矩阵奇异，找不到 n 个主元"</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>aug[[col, pivot_row]] </span><span>=</span><span> aug[[pivot_row, col]]</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>aug[col] </span><span>/=</span><span> aug[col, col]</span></div></div><div><div><div>15</div></div><div><span>        </span><span># Gauss 向下消，Jordan 向上消：把这一列的其他元素都消成 0</span></div></div><div><div><div>16</div></div><div><span>        </span><span>for</span><span> row </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>17</div></div><div><span>            </span><span>if</span><span><span> row </span><span>!=</span><span> col:</span></span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>aug[row] </span><span>-=</span><span> aug[row, col] </span><span>*</span><span> aug[col]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>return</span><span> aug[:, n:]</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>5</span><span>, </span><span>5</span><span>))</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>inverse </span><span>=</span><span> </span><span>gauss_jordan_inverse</span><span>(A)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>reference </span><span>=</span><span> np.linalg.</span><span>inv</span><span>(A)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"和 np.linalg.inv 的差："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.</span><span>abs</span><span>(inverse </span><span>-</span><span> reference).</span><span>max</span><span>()</span></span><span>:.1e</span><span>}</span><span>"</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>33</div></div><div><span>        </span><span>"A @ 逆 与单位矩阵的差："</span><span>,</span></div></div><div><div><div>34</div></div><div><span>        </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>norm</span><span>(A </span><span>@</span><span> inverse </span><span>-</span><span> np.</span><span>eye</span><span>(</span></span><span>5</span><span>))</span><span>:.1e</span><span>}</span><span>"</span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span># 奇异矩阵：消不出 n 个主元</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>6.0</span><span>]])</span></div></div><div><div><div>39</div></div><div><span>    </span><span>try</span><span>:</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>gauss_jordan_inverse</span><span>(S)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>except</span><span> </span><span>ValueError</span><span> </span><span>as</span><span> err:</span></div></div><div><div><div>42</div></div><div><span>        </span><span>print</span><span>(</span><span>"奇异矩阵："</span><span>, err)</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span># 把 6 换成 6 + eps：eps 越小，越接近奇异，逆里的数越大</span></div></div><div><div><div>45</div></div><div><span>    </span><span>print</span><span>(</span><span>"      eps    逆里最大的元素    条件数"</span><span>)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>for</span><span> eps </span><span>in</span><span> [</span><span>1e-2</span><span>, </span><span>1e-4</span><span>, </span><span>1e-6</span><span>, </span><span>1e-8</span><span>]:</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>T </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>3.0</span><span>], [</span><span>2.0</span><span>, </span><span>6.0</span><span><span> </span><span>+</span><span> eps]])</span></span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>big </span><span>=</span><span> np.</span><span>abs</span><span>(</span><span>gauss_jordan_inverse</span><span>(T)).</span><span>max</span><span>()</span></div></div><div><div><div>49</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>eps</span><span>:9.0e</span><span>}</span><span> </span><span>{</span><span>big</span><span>:16.3e</span><span>}</span><span> </span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(T)</span></span><span>:12.3e</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>和 np.linalg.inv 的差： 4.3e-14</span></div></div><div><div><div>2</div></div><div><span>A @ 逆 与单位矩阵的差： 8.2e-15</span></div></div><div><div><div>3</div></div><div><span>奇异矩阵： 矩阵奇异，找不到 n 个主元</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>eps    逆里最大的元素    条件数</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>1e-02        6.010e+02    5.012e+03</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>1e-04        6.000e+04    5.000e+05</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>1e-06        6.000e+06    5.000e+07</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>1e-08        6.000e+08    5.000e+09</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>随机 <span><span>5×55 \times 5</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span></span></span></span> 矩阵上，手写的 Gauss-Jordan 与 <code>np.linalg.inv</code> 的元素最大差是 <span><span>4.3×10−144.3 \times 10^{-14}</span><span><span><span></span><span>4.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 乘逆与 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 的差是 <span><span>8.2×10−158.2 \times 10^{-15}</span><span><span><span></span><span>8.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，这是舍入噪声的量级。奇异矩阵上，函数在第二列找不到非零主元，就停下了，这对应了“消元得不到 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个主元就没有逆”。</p><p>最后一张表是我最想让你看的。<span><span>ε\varepsilon</span><span><span><span></span><span>ε</span></span></span></span> 从 <span><span>10−210^{-2}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>2</span></span></span></span></span></span></span></span></span></span></span></span> 缩到 <span><span>10−810^{-8}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>，逆里最大的元素从 601 涨到 <span><span>6×1086 \times 10^{8}</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>，涨了一百万倍，刚好是 <span><span>ε\varepsilon</span><span><span><span></span><span>ε</span></span></span></span> 缩小的倍数，也就是逆里最大的元素约为 <span><span>6/ε6/\varepsilon</span><span><span><span></span><span>6/</span><span>ε</span></span></span></span>。条件数同步涨到 <span><span>5×1095 \times 10^{9}</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>。所以奇异与否不是一个开关：矩阵离奇异越近，逆里的数越大，用它解方程时右端项里的一点点误差就被放大得越多。这件事我猜到第 17 讲比较 Gram-Schmidt 的稳定性时会再碰到。</p><p>据我所知，实际工程里几乎没有人真的先算 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 再乘 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>，求解 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 用的是消元（<code>np.linalg.solve</code>）。不过 <span><span>A−1A^{-1}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 这个概念本身是不可替代的，理论里到处都是它。</p><p>下一讲 Strang 要问一个很自然的问题：既然消元是一串 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 的乘积，那 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 本身能不能写成这些 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 的逆的乘积？这就是 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>，下一讲见 <a href="/posts/linear-algebra-04-lu-factorization/">18.06 第 4 讲：分解成 A = LU</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-02-elimination-with-matrices/</id>
      <title type="text">18.06 第 2 讲：用矩阵做消元</title>
      <published>2026-10-18T00:20:00.000Z</published>
      <updated>2026-10-18T00:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-02-elimination-with-matrices/"/>
      <summary type="text">MIT 18.06 第 2 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 手写消元，把每一步写成初等矩阵并验证 E32 @ E21 @ A 恰好等于 U；在 10000 个随机整数 3 × 3 矩阵里，19.30% 需要换行，6.91% 彻底失败。</summary>
      <content type="html"><![CDATA[<p>消元是所有数值软件解方程组的办法，Strang 这一讲要你看到的是：消元的每一步，都是在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的左边再乘一个矩阵。他黑板上的 3 × 3 例子经过两步，<span><span>E32E21AE_{32} E_{21} A</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span></span></span></span> 变成了上三角的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span>，主元是 1、2、5，乘起来就是行列式 10。我还让程序在 10000 个随机的小整数 3 × 3 矩阵上跑了一遍消元：73.79% 一路畅通，19.30% 需要换行，6.91% 在某一步彻底卡死。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=QVKj3LADCnA" target="_blank">Lecture 2: Elimination with matrices</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-2-elimination-with-matrices/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §2.1 Elimination and Back Substitution；§2.2 Elimination Matrices and Inverse Matrices
前置：<a href="/posts/linear-algebra-01-geometry-of-linear-equations/">第 1 讲：方程组的行图与列图</a>
一句话：消元的一步等于左乘一个把单位矩阵改了一个位置的矩阵；整个消元 <span><span>A→UA \to U</span><span><span><span></span><span>A</span><span></span><span>→</span><span></span></span><span><span></span><span>U</span></span></span></span> 就是把这些矩阵按顺序乘在 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 左边。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头到回代结束是他手算消元的那一段，本篇用代码算过同一个矩阵，可以倍速。他中途把一个数写错又自己发现的地方，值得看一眼。</li>
<li>“怎么会失败”那一段要认真看：什么数让主元变成 0 还能用换行救回来，什么数让它彻底没救。我在第二节用同一组数做了实验。</li>
<li>他讲“左乘做行操作，右乘做列操作”和“结合律”的几分钟是全讲的核心，不要倍速。</li>
<li>最后几分钟他开始讲逆矩阵，只举了一个例子，下一篇会系统展开。</li>
</ul></div></div>
<section><h2>手算一遍：从 A 到 U，再回代<a href="#手算一遍从-a-到-u再回代"><span>#</span></a></h2><p>这一讲只有一个方程组，三个方程三个未知数：</p><span><span><span>x+2y+z=23x+8y+z=124y+z=2A=[121381041], b=[2122]\begin{aligned}
x + 2y + z &amp;= 2 \\
3x + 8y + z &amp;= 12 \\
4y + z &amp;= 2
\end{aligned}
\qquad
A = \begin{bmatrix} 1 &amp; 2 &amp; 1 \\ 3 &amp; 8 &amp; 1 \\ 0 &amp; 4 &amp; 1 \end{bmatrix},\
\mathbf{b} = \begin{bmatrix} 2 \\ 12 \\ 2 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>x</span><span></span><span>+</span><span></span><span>2</span><span>y</span><span></span><span>+</span><span></span><span>z</span></span></span><span><span></span><span><span>3</span><span>x</span><span></span><span>+</span><span></span><span>8</span><span>y</span><span></span><span>+</span><span></span><span>z</span></span></span><span><span></span><span><span>4</span><span>y</span><span></span><span>+</span><span></span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>2</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>12</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>8</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span> </span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>12</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>他一开始就说了两件事：方法不是行列式（那是后面的内容），是消元；而且“每个软件包解方程组用的都是它”。我猜这句话并不夸张，LAPACK 里的 LU 分解说到底就是这里的消元加上一点数值上的小心思。</p><p><strong>消元做的是什么？</strong> 拿第一个方程当“主元行”，乘上合适的倍数，从下面的方程里减掉，目的是消去下面方程里的 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>。第一行里的 1 是第一个主元（pivot）。第二行的 3 要被消去，所以乘数是 3：第二行减去 3 倍的第一行，<span><span>3−3×1=03 - 3 \times 1 = 0</span><span><span><span></span><span>3</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>8−3×2=28 - 3 \times 2 = 2</span><span><span><span></span><span>8</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，<span><span>1−3×1=−21 - 3 \times 1 = -2</span><span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span>。第三行的第一个数本来就是 0，乘数是 0，什么也不用做（他特意提了一句，MATLAB 不知道它是 0，也会老老实实走这一步）。</p><p>现在左下角的第一列已经清干净，剩下两个方程、两个未知数 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 和 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>。第二个主元是刚刚出现的 2。第三行的 4 要被消去，乘数是 2：<span><span>4−2×2=04 - 2 \times 2 = 0</span><span><span><span></span><span>4</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>1−2×(−2)=51 - 2 \times (-2) = 5</span><span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>−</span><span>2</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span>。第三个主元是 5。</p><p>他还顺口立了一条规矩：永远说“减去”，不说“加上负的”。这是为了让乘数永远是正数的样子（这里是 3 和 2），而它在矩阵里出现时会带上负号，<span><span>E21E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里的 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span> 就是这么来的。这个约定后面会反复用到：<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 里放的是乘数本身，<span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 里放的是乘数的相反数。（我承认这是个纯属口味的约定，换成“加上负的”数学上毫无区别。）</p><p>右端项要同步更新。做法是把 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 当作第四列粘到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 旁边，这叫增广矩阵（augmented matrix）。“对两边做同样的事”，所以每一步对整行操作，包括最后那一列。我把这个过程原样写成代码，每一步都打印出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>3.0</span><span>, </span><span>8.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>12.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># 增广矩阵 [A | b]：对左边做的每一步，右端项同时被带着走</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>hstack</span><span>([A, b[:, </span><span>None</span><span>]])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>3</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):  </span><span># 第 j 列的主元</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(j </span><span>+</span><span> </span></span><span>1</span><span>, n):  </span><span># 消去主元下面的每一行</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>multiplier </span><span>=</span><span> M[i, j] </span><span>/</span><span> M[j, j]</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>M[i] </span><span>-=</span><span> multiplier </span><span>*</span><span> M[j]</span></div></div><div><div><div>16</div></div><div><span>            </span><span>print</span><span>(</span><span>f</span><span>"第 </span><span>{</span><span><span>i </span><span>+</span><span> </span></span><span>1}</span><span>,</span><span>{</span><span><span>j </span><span>+</span><span> </span></span><span>1}</span><span> 步：乘数 </span><span>{</span><span>multiplier</span><span>:g</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>17</div></div><div><span>            </span><span>print</span><span>(M)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>U, c </span><span>=</span><span> M[:, :n], M[:, n]</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"主元："</span><span><span>, np.</span><span>diag</span><span>(U))</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"主元之积："</span><span><span>, np.</span><span>prod</span><span>(np.</span><span>diag</span><span>(U)), </span></span><span>"，det(A) ="</span><span><span>, np.linalg.</span><span>det</span><span>(A))</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 回代：从最后一个方程往上解</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span>):</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>x[i] </span><span>=</span><span> (c[i] </span><span>-</span><span> U[i, i </span><span>+</span><span> </span><span>1</span><span><span> :] </span><span>@</span><span> x[i </span><span>+</span><span> </span></span><span>1</span><span><span> :]) </span><span>/</span><span> U[i, i]</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"回代得到 x ="</span><span>, x)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"与 np.linalg.solve 一致："</span><span><span>, np.</span><span>allclose</span><span>(x, np.linalg.</span><span>solve</span><span>(A, b)))</span></span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>第 2,1 步：乘数 3</span></div></div><div><div><div>2</div></div><div><span>[[ 1.  2.  1.  2.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.  2. -2.  6.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  4.  1.  2.]]</span></div></div><div><div><div>5</div></div><div><span>第 3,1 步：乘数 0</span></div></div><div><div><div>6</div></div><div><span>[[ 1.  2.  1.  2.]</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>[ 0.  2. -2.  6.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[ 0.  4.  1.  2.]]</span></div></div><div><div><div>9</div></div><div><span>第 3,2 步：乘数 2</span></div></div><div><div><div>10</div></div><div><span>[[  1.   2.   1.   2.]</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>[  0.   2.  -2.   6.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[  0.   0.   5. -10.]]</span></div></div><div><div><div>13</div></div><div><span>主元： [1. 2. 5.]</span></div></div><div><div><div>14</div></div><div><span>主元之积： 10.0 ，det(A) = 9.999999999999998</span></div></div><div><div><div>15</div></div><div><span>回代得到 x = [ 2.  1. -2.]</span></div></div><div><div><div>16</div></div><div><span>与 np.linalg.solve 一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三步的乘数是 3、0、2，和他黑板上的一样。最后的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 是上三角矩阵，右端项变成了 <span><span>c=(2,6,−10)\mathbf{c} = (2, 6, -10)</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>6</span><span>,</span><span></span><span>−</span><span>10</span><span>)</span></span></span></span>（他说 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的归宿，<span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span> 是 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 的归宿）。</p><p><strong>回代是什么？</strong> <span><span>Ux=cU\mathbf{x} = \mathbf{c}</span><span><span><span></span><span>U</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>c</span></span></span></span> 写开就是</p><span><span><span>x+2y+z=22y−2z=65z=−10\begin{aligned}
x + 2y + z &amp;= 2 \\
2y - 2z &amp;= 6 \\
5z &amp;= -10
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>x</span><span></span><span>+</span><span></span><span>2</span><span>y</span><span></span><span>+</span><span></span><span>z</span></span></span><span><span></span><span><span>2</span><span>y</span><span></span><span>−</span><span></span><span>2</span><span>z</span></span></span><span><span></span><span><span>5</span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>2</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>6</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>−</span><span>10</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>从最后一个方程开始：<span><span>z=−2z = -2</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span>。往上一步：<span><span>2y+4=62y + 4 = 6</span><span><span><span></span><span>2</span><span>y</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span>，<span><span>y=1y = 1</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。再往上：<span><span>x+2−2=2x + 2 - 2 = 2</span><span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，<span><span>x=2x = 2</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>。解是 <span><span>(2,1,−2)(2, 1, -2)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span></span></span></span>，和 <code>np.linalg.solve</code> 一致。</p><p>输出里有个小细节：主元之积是 10.0，而 <code>np.linalg.det(A)</code> 打印出 9.999999999999998。两者差约 <span><span>2×10−152 \times 10^{-15}</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，因为 NumPy 的 <code>det</code> 也是做一次 LU 分解再把对角线乘起来，只是中间会选主元、顺序不同，舍入误差就不同。Strang 顺便提了一句：如果你想知道行列式，把主元乘起来就是，虽然“我从来不想知道行列式”。行列式是第 18 讲的内容，这里先记下：三个主元 1、2、5，乘积 10。</p></section>
<section><h2>怎么会失败<a href="#怎么会失败"><span>#</span></a></h2><p>他说看一个算法，最好问它怎么才会失败。这里的“失败”是指凑不够三个主元。</p><p><strong>情形一：主元位置上是 0，下面有非零数。</strong> 他把第二行的 8 换成 6：第一步之后，第二行是 <span><span>(0,0,−2)(0, 0, -2)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span></span></span></span>，第二个主元位置是 0。他强调不会说“零主元”，换行就行：第三行是 <span><span>(0,4,1)(0, 4, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，把第二、三行交换，4 就成了新的主元。这是暂时的失败，可以救。</p><p><strong>情形二：主元位置上是 0，下面全是 0。</strong> 他问把右下角的 1 改成多少会彻底没救。答案是 <span><span>−4-4</span><span><span><span></span><span>−</span><span>4</span></span></span></span>：第三行变成 <span><span>(0,4,−4)(0, 4, -4)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>−</span><span>4</span><span>)</span></span></span></span>，减去 2 倍的第二行 <span><span>(0,2,−2)(0, 2, -2)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>−</span><span>2</span><span>)</span></span></span></span> 得到 <span><span>(0,0,0)(0, 0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。没有第三个主元，没有行可换，矩阵不可逆。</p><p>我把这三个矩阵交给一个手写的 <code>eliminate</code>：主元位置是 0 就往下找第一个非零的行交换，找不到就报失败。（真正的软件会选绝对值最大的数当主元，数值上更稳，这里只按 Strang 的规矩，不是 0 就不换。）</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>eliminate</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-12</span></span><span>):</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""消元到上三角；主元位置是 0 就往下找第一个非零的行交换。</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>返回 (U, 行交换次数, 三个主元是否都找到)。</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> U.shape[</span><span>0</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>swaps </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>nonzero </span><span>=</span><span> np.</span><span>flatnonzero</span><span>(np.</span><span>abs</span><span>(U[j:, j]) </span><span>&gt;</span><span> tol)</span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span><span> nonzero.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>15</div></div><div><span>            </span><span>return</span><span> U, swaps, </span><span>False</span><span>  </span><span># 下面全是 0，没有可换的行</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> j </span><span>+</span><span> </span><span>int</span><span>(nonzero[</span><span>0</span><span>])</span></div></div><div><div><div>17</div></div><div><span>        </span><span>if</span><span><span> k </span><span>!=</span><span> j:</span></span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>U[[j, k]] </span><span>=</span><span> U[[k, j]]</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>swaps </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>20</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(j </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> U[i, j] </span><span>/</span><span> U[j, j] </span><span>*</span><span> U[j]</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span> U, swaps, </span><span>True</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>cases </span><span>=</span><span> {</span></div></div><div><div><div>28</div></div><div><span>        </span><span>"原矩阵"</span><span>: [[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>1</span><span>], [</span><span>3</span><span>, </span><span>8</span><span>, </span><span>1</span><span>], [</span><span>0</span><span>, </span><span>4</span><span>, </span><span>1</span><span>]],</span></div></div><div><div><div>29</div></div><div><span>        </span><span>"8 改成 6"</span><span>: [[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>1</span><span>], [</span><span>3</span><span>, </span><span>6</span><span>, </span><span>1</span><span>], [</span><span>0</span><span>, </span><span>4</span><span>, </span><span>1</span><span>]],</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"1 改成 -4"</span><span>: [[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>1</span><span>], [</span><span>3</span><span>, </span><span>8</span><span>, </span><span>1</span><span>], [</span><span>0</span><span>, </span><span>4</span><span><span>, </span><span>-</span></span><span>4</span><span>]],</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>    </span><span>for</span><span> name, rows </span><span>in</span><span><span> cases.</span><span>items</span><span>():</span></span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>(rows, </span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>U, swaps, ok </span><span>=</span><span> </span><span>eliminate</span><span>(A)</span></div></div><div><div><div>35</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：换行 </span><span>{</span><span>swaps</span><span>}</span><span> 次，三个主元都有：</span><span>{</span><span>ok</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>36</div></div><div><span>        </span><span>print</span><span>(U)</span></div></div><div><div><div>37</div></div><div><span>        </span><span>print</span><span>(</span><span>"  rank ="</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A))</span></span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>原矩阵：换行 0 次，三个主元都有：True</span></div></div><div><div><div>2</div></div><div><span>[[ 1.  2.  1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[ 0.  2. -2.]</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>[ 0.  0.  5.]]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>rank = 3</span></div></div><div><div><div>6</div></div><div><span>8 改成 6：换行 1 次，三个主元都有：True</span></div></div><div><div><div>7</div></div><div><span>[[ 1.  2.  1.]</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>[ 0.  4.  1.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[ 0.  0. -2.]]</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>rank = 3</span></div></div><div><div><div>11</div></div><div><span>1 改成 -4：换行 0 次，三个主元都有：False</span></div></div><div><div><div>12</div></div><div><span>[[ 1.  2.  1.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[ 0.  2. -2.]</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[ 0.  0.  0.]]</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>rank = 2</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个矩阵的结果和他的口头描述一样：原矩阵不换行，主元 1、2、5；改成 6 之后换一次行，主元变成 1、4、<span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span>，仍然有三个；改成 <span><span>−4-4</span><span><span><span></span><span>−</span><span>4</span></span></span></span> 之后第三行全零，只有两个主元，<code>np.linalg.matrix_rank</code> 也说秩是 2。</p><p>这两种失败各有多常见？Strang 没问，我自己好奇，就用固定种子随机生成 10000 个元素取自 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span> 到 <span><span>33</span><span><span><span></span><span>3</span></span></span></span> 的整数 3 × 3 矩阵，分类统计：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>classify</span><span>(</span><span>A</span><span>:</span><span> np.ndarray</span><span>,</span><span><span> </span><span>tol</span></span><span>:</span><span> </span><span>float</span><span> </span><span>=</span><span><span> </span><span>1e-12</span></span><span>) -&gt; </span><span>str</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"""返回直通、换行、失败三者之一。"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> A.</span><span>astype</span><span>(</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> U.shape[</span><span>0</span><span>]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>swapped </span><span>=</span><span> </span><span>False</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>nonzero </span><span>=</span><span> np.</span><span>flatnonzero</span><span>(np.</span><span>abs</span><span>(U[j:, j]) </span><span>&gt;</span><span> tol)</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span><span> nonzero.size </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>12</div></div><div><span>            </span><span>return</span><span> </span><span>"失败"</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> j </span><span>+</span><span> </span><span>int</span><span>(nonzero[</span><span>0</span><span>])</span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span><span> k </span><span>!=</span><span> j:</span></span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>U[[j, k]] </span><span>=</span><span> U[[k, j]]</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>swapped </span><span>=</span><span> </span><span>True</span></div></div><div><div><div>17</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span><span>(j </span><span>+</span><span> </span></span><span>1</span><span>, n):</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>U[i] </span><span>-=</span><span> U[i, j] </span><span>/</span><span> U[j, j] </span><span>*</span><span> U[j]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>return</span><span> </span><span>"换行"</span><span> </span><span>if</span><span> swapped </span><span>else</span><span> </span><span>"直通"</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>trials </span><span>=</span><span> </span><span>10000</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>counts </span><span>=</span><span> {</span><span>"直通"</span><span>: </span><span>0</span><span>, </span><span>"换行"</span><span>: </span><span>0</span><span>, </span><span>"失败"</span><span>: </span><span>0</span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>mismatch </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(trials):</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> rng.</span><span>integers</span><span>(</span><span>-</span><span>3</span><span>, </span><span>4</span><span>, </span><span>size</span><span><span>=</span><span>(</span></span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>result </span><span>=</span><span> </span><span>classify</span><span>(A)</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>counts[result] </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>singular </span><span>=</span><span> np.linalg.</span><span>matrix_rank</span><span>(A) </span><span>&lt;</span><span> </span><span>3</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>mismatch </span><span>+=</span><span> singular </span><span>!=</span><span> (result </span><span>==</span><span> </span><span>"失败"</span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> name, count </span><span>in</span><span><span> counts.</span><span>items</span><span>():</span></span></div></div><div><div><div>34</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：</span><span>{</span><span>count</span><span>}</span><span> / </span><span>{</span><span>trials</span><span>}</span><span> = </span><span>{</span><span><span>count </span><span>/</span><span> trials</span></span><span>:.2%</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>"“失败”与 matrix_rank &lt; 3 不一致的个数："</span><span>, mismatch)</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>直通：7379 / 10000 = 73.79%</span></div></div><div><div><div>2</div></div><div><span>换行：1930 / 10000 = 19.30%</span></div></div><div><div><div>3</div></div><div><span>失败：691 / 10000 = 6.91%</span></div></div><div><div><div>4</div></div><div><span>“失败”与 matrix_rank &lt; 3 不一致的个数： 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>73.79% 的矩阵直通到底，19.30% 需要至少一次换行，6.91% 彻底失败。最后一行是自检：“彻底失败”的矩阵和 <code>matrix_rank &lt; 3</code> 的矩阵逐个对上，一个不差。所以从这个意义上说，消元不只是求解的方法，也是判断矩阵可不可逆的方法：能不能凑齐 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个主元，就是答案。换行救得回来的情形在这批小整数矩阵里比失败的情形常见得多，我猜是因为小整数里零很多；如果换成连续的随机数，两种情形都会变成概率为零的事件。</p></section>
<section><h2>把每一步写成矩阵<a href="#把每一步写成矩阵"><span>#</span></a></h2><p>他在这里先停下来，讲了一个“全局视角”。上一讲讲的是：矩阵乘以列向量，结果是各列的线性组合。现在要的是它的镜像：<strong>行向量乘以矩阵，结果是各行的线性组合。</strong> 他的例子是用 <span><span>(1,2,7)(1, 2, 7)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span> 左乘 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，结果是 <span><span>1×1 \times</span><span><span><span></span><span>1</span><span>×</span></span></span></span> 第一行 <span><span>+2×+ 2 \times</span><span><span><span></span><span>+</span><span>2</span><span>×</span></span></span></span> 第二行 <span><span>+7×+ 7 \times</span><span><span><span></span><span>+</span><span>7</span><span>×</span></span></span></span> 第三行。消元的所有操作都是行操作，所以消元矩阵一定放在左边。</p><p>那么“第二行减去 3 倍的第一行”是哪个矩阵？第一行和第三行不变，所以矩阵的第一行是 <span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，第三行是 <span><span>(0,0,1)(0, 0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>；第二行要取 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span> 份第一行加 1 份第二行：<span><span>(−3,1,0)(-3, 1, 0)</span><span><span><span></span><span>(</span><span>−</span><span>3</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。也就是在单位矩阵的 <span><span>(2,1)(2, 1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 位置填上 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span>：</p><span><span><span>E21=[100−310001],E32=[1000100−21]E_{21} = \begin{bmatrix} 1 &amp; 0 &amp; 0 \\ -3 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 1 \end{bmatrix}, \quad
E_{32} = \begin{bmatrix} 1 &amp; 0 &amp; 0 \\ 0 &amp; 1 &amp; 0 \\ 0 &amp; -2 &amp; 1 \end{bmatrix}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>下标是它负责消去的位置。下面的脚本做了几件事：先验证行向量左乘确实是行的组合，然后构造 <span><span>E21E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>E32E_{32}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，验证 <span><span>E32(E21A)=UE_{32}(E_{21}A) = U</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span></span></span></span>，再把两个矩阵先乘成一个 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span>，验证一次乘到位的结果相同，最后让 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 作用在 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 上，看它是不是 <span><span>c\mathbf{c}</span><span><span><span></span><span>c</span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>3.0</span><span>, </span><span>8.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>4.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>2.0</span><span>, </span><span>12.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>1.0</span><span>], [</span><span>0.0</span><span>, </span><span>2.0</span><span><span>, </span><span>-</span></span><span>2.0</span><span>], [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>5.0</span><span>]])</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span># 左乘行向量：结果是各行的线性组合</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>row </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>7.0</span><span>])</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"[1 2 7] @ A ="</span><span><span>, row </span><span>@</span><span> A)</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"1 * 行一 + 2 * 行二 + 7 * 行三 ="</span><span>, A[</span><span>0</span><span><span>] </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> A[</span></span><span>1</span><span><span>] </span><span>+</span><span> </span></span><span>7</span><span><span> </span><span>*</span><span> A[</span></span><span>2</span><span>])</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># 把消元的每一步写成矩阵：把单位矩阵改一个位置</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>E21 </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>E21[</span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>3.0</span><span>  </span><span># 第二行 减去 3 倍的第一行</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>E32 </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>E32[</span><span>2</span><span>, </span><span>1</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>2.0</span><span>  </span><span># 第三行 减去 2 倍的第二行</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"E21 ="</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(E21)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"E32 ="</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(E32)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"E21 @ A ="</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span><span>(E21 </span><span>@</span><span> A)</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span># 先后左乘两次，等于一次左乘 E = E32 @ E21（结合律）</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>step_by_step </span><span>=</span><span> E32 </span><span>@</span><span> (E21 </span><span>@</span><span> A)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>E </span><span>=</span><span> E32 </span><span>@</span><span> E21</span></div></div><div><div><div>30</div></div><div><span>    </span><span>print</span><span>(</span><span>"E32 @ E21 @ A 等于 U："</span><span><span>, np.</span><span>allclose</span><span>(step_by_step, U))</span></span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"E ="</span><span>)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(E)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>34</div></div><div><span>        </span><span>"(E32 @ E21) @ A 等于 E32 @ (E21 @ A)："</span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(E </span><span>@</span><span> A, step_by_step),</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span># 右端项经历的是同一串行操作，得到 c</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> E </span><span>@</span><span> b</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print</span><span>(</span><span>"c = E @ b ="</span><span>, c)</span></div></div><div><div><div>41</div></div><div><span>    </span><span>print</span><span>(</span><span>"U x = c 的解："</span><span><span>, np.linalg.</span><span>solve</span><span>(U, c))</span></span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[1 2 7] @ A = [ 7. 46. 10.]</span></div></div><div><div><div>2</div></div><div><span>1 * 行一 + 2 * 行二 + 7 * 行三 = [ 7. 46. 10.]</span></div></div><div><div><div>3</div></div><div><span>E21 =</span></div></div><div><div><div>4</div></div><div><span>[[ 1.  0.  0.]</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>[-3.  1.  0.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[ 0.  0.  1.]]</span></div></div><div><div><div>7</div></div><div><span>E32 =</span></div></div><div><div><div>8</div></div><div><span>[[ 1.  0.  0.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[ 0.  1.  0.]</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>[ 0. -2.  1.]]</span></div></div><div><div><div>11</div></div><div><span>E21 @ A =</span></div></div><div><div><div>12</div></div><div><span>[[ 1.  2.  1.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[ 0.  2. -2.]</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[ 0.  4.  1.]]</span></div></div><div><div><div>15</div></div><div><span>E32 @ E21 @ A 等于 U： True</span></div></div><div><div><div>16</div></div><div><span>E =</span></div></div><div><div><div>17</div></div><div><span>[[ 1.  0.  0.]</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>[-3.  1.  0.]</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>[ 6. -2.  1.]]</span></div></div><div><div><div>20</div></div><div><span>(E32 @ E21) @ A 等于 E32 @ (E21 @ A)： True</span></div></div><div><div><div>21</div></div><div><span>c = E @ b = [  2.   6. -10.]</span></div></div><div><div><div>22</div></div><div><span>U x = c 的解： [ 2.  1. -2.]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里有两个点。</p><p>第一，<span><span>E32E21A=UE_{32} E_{21} A = U</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span></span></span></span>。他说，一个小小的式子里装着整讲的内容。</p><p>第二，<span><span>(E32E21)A=E32(E21A)(E_{32} E_{21}) A = E_{32} (E_{21} A)</span><span><span><span></span><span>(</span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>A</span><span>)</span></span></span></span>。这就是结合律，括号可以移动，顺序不能变。他说你可以把“结合律”这个词忘了，但不要忘了这条规则，因为线性代数里很多证明就是把括号挪一挪。把 <span><span>E32E21E_{32} E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 先乘出来就是矩阵 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span>，它一步完成整个消元。</p><p>再看 <span><span>EE</span><span><span><span></span><span>E</span></span></span></span> 本身：</p><span><span><span>E=E32E21=[100−3106−21]E = E_{32} E_{21} = \begin{bmatrix} 1 &amp; 0 &amp; 0 \\ -3 &amp; 1 &amp; 0 \\ 6 &amp; -2 &amp; 1 \end{bmatrix}</span><span><span><span></span><span>E</span><span></span><span>=</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span><span><span></span><span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>左下角出现了 6，而乘数里并没有 6：它是 <span><span>3×23 \times 2</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span>，因为第三行减去的“2 倍第二行”里，第二行本身已经减过 3 倍第一行了。他在这里说了一句“有一个更好的办法”，办法是反过来想，从 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 回到 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，那时乘数会原封不动地落进矩阵里。这是第 4 讲的 <span><span>A=LUA = LU</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>LU</span></span></span></span>。</p></section>
<section><h2>换行矩阵、顺序与逆<a href="#换行矩阵顺序与逆"><span>#</span></a></h2><p>消元里还有一种基本矩阵：换行。他先举二阶的例子，用 <span><span>[abcd]\begin{bmatrix} a &amp; b \\ c &amp; d \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>：要交换两行，就取 0 份第一行加 1 份第二行当新的第一行，1 份第一行加 0 份第二行当新的第二行。得到</p><span><span><span>P=[0110]P = \begin{bmatrix} 0 &amp; 1 \\ 1 &amp; 0 \end{bmatrix}</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>这是置换矩阵（permutation matrix）。找它的捷径是把单位矩阵的两行交换。</p><p>然后是一个好问题：要交换两列怎么办？答案是同一个 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span>，但放在右边。<strong>左乘做行操作，右乘做列操作。</strong> 下面的脚本用 <span><span>[1234]\begin{bmatrix} 1 &amp; 2 \\ 3 &amp; 4 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 代替字母，验证 <span><span>PMPM</span><span><span><span></span><span>P</span><span>M</span></span></span></span> 换行、<span><span>MPMP</span><span><span><span></span><span>M</span><span>P</span></span></span></span> 换列。同一块里还验证了他反复强调的另一条：矩阵乘法没有交换律，<span><span>E32E21≠E21E32E_{32} E_{21} \ne E_{21} E_{32}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。最后算他在最后几分钟举的逆矩阵例子：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>2.0</span><span>], [</span><span>3.0</span><span>, </span><span>4.0</span><span>]])  </span><span># 相当于他黑板上的 [a b; c d]</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span># 把单位矩阵的两行交换，就得到交换两行的矩阵 P</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>2</span><span>)[[</span><span>1</span><span>, </span><span>0</span><span>]]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>print</span><span>(</span><span>"P ="</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(P)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"P @ M（左乘，换行）："</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span><span>(P </span><span>@</span><span> M)</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>"M @ P（右乘，换列）："</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span><span>(M </span><span>@</span><span> P)</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span># 乘法不满足交换律</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>E21 </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>E21[</span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>3.0</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>E32 </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>E32[</span><span>2</span><span>, </span><span>1</span><span><span>] </span><span>=</span><span> </span><span>-</span></span><span>2.0</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"E32 @ E21 ="</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span><span>(E32 </span><span>@</span><span> E21)</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>print</span><span>(</span><span>"E21 @ E32 ="</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>print</span><span><span>(E21 </span><span>@</span><span> E32)</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>print</span><span>(</span><span>"两者相等："</span><span><span>, np.</span><span>allclose</span><span>(E32 </span><span>@</span><span> E21, E21 </span><span>@</span><span> E32))</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span># E21 的逆：把减掉的 3 倍第一行加回去</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>E21_inv </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>E21_inv[</span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>3.0</span></div></div><div><div><div>31</div></div><div><span>    </span><span>print</span><span>(</span><span>"E21_inv @ E21 是单位矩阵："</span><span><span>, np.</span><span>allclose</span><span>(E21_inv </span><span>@</span><span> E21, np.</span><span>eye</span><span>(</span></span><span>3</span><span>)))</span></div></div><div><div><div>32</div></div><div><span>    </span><span>print</span><span>(</span><span>"与 np.linalg.inv 一致："</span><span><span>, np.</span><span>allclose</span><span>(E21_inv, np.linalg.</span><span>inv</span><span>(E21)))</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>P =</span></div></div><div><div><div>2</div></div><div><span>[[0. 1.]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>[1. 0.]]</span></div></div><div><div><div>4</div></div><div><span>P @ M（左乘，换行）：</span></div></div><div><div><div>5</div></div><div><span>[[3. 4.]</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>[1. 2.]]</span></div></div><div><div><div>7</div></div><div><span>M @ P（右乘，换列）：</span></div></div><div><div><div>8</div></div><div><span>[[2. 1.]</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>[4. 3.]]</span></div></div><div><div><div>10</div></div><div><span>E32 @ E21 =</span></div></div><div><div><div>11</div></div><div><span>[[ 1.  0.  0.]</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>[-3.  1.  0.]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>[ 6. -2.  1.]]</span></div></div><div><div><div>14</div></div><div><span>E21 @ E32 =</span></div></div><div><div><div>15</div></div><div><span>[[ 1.  0.  0.]</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>[-3.  1.  0.]</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>[ 0. -2.  1.]]</span></div></div><div><div><div>18</div></div><div><span>两者相等： False</span></div></div><div><div><div>19</div></div><div><span>E21_inv @ E21 是单位矩阵： True</span></div></div><div><div><div>20</div></div><div><span>与 np.linalg.inv 一致： True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><span><span>PMPM</span><span><span><span></span><span>P</span><span>M</span></span></span></span> 把 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(3,4)(3, 4)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 两行对调，<span><span>MPMP</span><span><span><span></span><span>M</span><span>P</span></span></span></span> 把 <span><span>(1,3)(1, 3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>、<span><span>(2,4)(2, 4)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 两列对调。顺序也确实重要：<span><span>E32E21E_{32} E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 <span><span>(3,1)(3, 1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 位置是 6，而 <span><span>E21E32E_{21} E_{32}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 <span><span>(3,1)(3, 1)</span><span><span><span></span><span>(</span><span>3</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span> 位置是 0，两个乘积不相等。</p><p>直觉上也好理解：先“减掉 3 倍第一行”再“从第三行里减 2 倍的新第二行”，和先后颠倒的两步，做的是不同的事，第一种里第三行间接用到了第一行，第二种没有。他说要把矩阵保持在“Gauss 给的顺序”里（这句话我读了两遍才反应过来是在开玩笑）。</p><p>最后，逆。<span><span>E21E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做的是“第二行减去 3 倍第一行”，要撤销它就“把 3 倍第一行加回去”：把 <span><span>−3-3</span><span><span><span></span><span>−</span><span>3</span></span></span></span> 改成 <span><span>+3+3</span><span><span><span></span><span>+</span><span>3</span></span></span></span>。脚本验证了 <span><span>E21−1E21E_{21}^{-1} E_{21}</span><span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>E</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是单位矩阵，也和 <code>np.linalg.inv</code> 的结果一致。如果要找一个类比，逆矩阵就是撤销键。他停在这里：“这是下周一内容的一个小预告。”</p><p>下一讲从这里继续：矩阵到底怎么乘，为什么有四种看法，哪些矩阵有逆，哪些没有。见 <a href="/posts/linear-algebra-03-multiplication-and-inverse/">18.06 第 3 讲：矩阵乘法的四种看法与逆矩阵</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/linear-algebra-01-geometry-of-linear-equations/</id>
      <title type="text">18.06 第 1 讲：方程组的行图与列图</title>
      <published>2026-10-18T00:00:00.000Z</published>
      <updated>2026-10-18T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/linear-algebra-01-geometry-of-linear-equations/"/>
      <summary type="text">MIT 18.06 第 1 讲的配套笔记：我用 Python 3.13 和 NumPy 2.4 把 2 × 2 与 3 × 3 方程组分别按行图、列图和 A @ x 解了一遍，再把第三列换成前两列之和，秩从 3 掉到 2，最近的可达点与 b 差了 0.4082。</summary>
      <content type="html"><![CDATA[<p>Strang 第一讲想让你带走的只有一句话：解 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>，就是问 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 能不能写成 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的各列的线性组合。我把他黑板上的 3 × 3 矩阵的第三列改成前两列之和，行列式从 6 变成 0，秩从 3 掉到 2，右端项 <span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 到最近的可达点的距离是 0.4082：方程组没有解，而且没有解的原因一眼就能在列上看出来。</p>
<div><div><div></div><div>本讲速览</div></div><div><p>视频：<a href="https://www.youtube.com/watch?v=J7DzL2_Na80" target="_blank">Lecture 1: The geometry of linear equations</a>（<a href="https://ocw.mit.edu/courses/18-06-linear-algebra-spring-2010/resources/lecture-1-the-geometry-of-linear-equations/" target="_blank">OCW 页面与字幕</a>）
教材：《Introduction to Linear Algebra》第 6 版 §1.1 Vectors and Linear Combinations；§1.3 Matrices and Their Column Spaces；§2.1 Elimination and Back Substitution
前置：没有前置，这是第一篇。
一句话：<span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的列的线性组合，所以“<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 有没有解”和“<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 在不在列的组合里”是同一个问题。</p></div></div>
<div><div><div></div><div>配合视频</div></div><div><ul>
<li>开头几分钟在介绍教材和课程网页，可以跳过。2 × 2 的行图（两条直线相交）很短，本篇已经算过，倍速即可。</li>
<li>他画列图的那一段要认真看：把两个列向量首尾相接，凑出 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>。这是整门课最重要的一幅图。</li>
<li>3 × 3 的行图他自己都说画不好，可以快进。紧接着的列图和“如果三列落在同一个平面里会怎样”值得一字不漏地听。</li>
<li>最后讲 <span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 按列算、按行算的那段很短，本篇最后一节用代码重算了一遍。</li>
</ul></div></div>
<section><h2>这个系列怎么用<a href="#这个系列怎么用"><span>#</span></a></h2><p>我自己的节奏是这样：先读这一篇（大概 15 分钟），再看视频，遇到 TIP 里写“已经算过”的地方就倍速。视频是他讲，代码是我算，两边对一次，比光看一遍记得牢。</p><p>有一件事要提前说：视频是 1999 年秋天的录像，用的是当时的教材。我手上的《Introduction to Linear Algebra》是第 6 版（2023），它把线性无关和列空间（<span><span>A=CRA = CR</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>R</span></span></span></span>）提到了最前面，录像里没有这个顺序。所以每篇的“教材”一行只告诉你对应哪几节，章节顺序不会一一对上。以视频字幕为准，教材拿来查证明和习题。</p><section><h3>运行环境<a href="#运行环境"><span>#</span></a></h3><p>仓库的 <code>flake.nix</code> 里只有 Node 和 pnpm，我的 PATH 里也没有 <code>python3</code>，所以用 Nix 临时拉一个带 NumPy 和 matplotlib 的 Python：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>nix</span><span> </span><span>shell</span><span> </span><span>--impure</span><span> </span><span>--expr</span><span> </span><span>'(builtins.getFlake "nixpkgs").legacyPackages.x86_64-linux.python313.withPackages (p: [ p.numpy p.matplotlib ])'</span><span> </span><span>--command</span><span> </span><span>python</span><span> </span><span>&lt;script.py&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>打印版本用的是同一个解释器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>nix</span><span> </span><span>shell</span><span> </span><span>--impure</span><span> </span><span>--expr</span><span> </span><span>'(builtins.getFlake "nixpkgs").legacyPackages.x86_64-linux.python313.withPackages (p: [ p.numpy p.matplotlib ])'</span><span> </span><span>--command</span><span> </span><span>python</span><span> </span><span>-c</span><span> </span><span>"import sys, numpy, matplotlib; print(sys.version.split()[0], numpy.__version__, matplotlib.__version__)"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>3.13.15 2.4.4 3.10.9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也就是 Python 3.13.15、NumPy 2.4.4、matplotlib 3.10.9。matplotlib 只用来画文中的图，画图脚本贴在各图之后，其余代码块都不用它。</p></section><section><h3>代码约定<a href="#代码约定"><span>#</span></a></h3><ul>
<li>矩阵用大写单字母 <code>A</code>、<code>L</code>、<code>U</code>、<code>P</code>、<code>Q</code>、<code>R</code>、<code>S</code>，和 Strang 的黑板一致。这是我有意偏离 PEP 8 变量命名的地方。</li>
<li>每个代码块都是能单独运行的完整脚本，只用标准库和 NumPy，不读写文件。块后紧跟的 <code>text</code> 块是它的 stdout，逐字拷贝，不手改。</li>
<li>随机数一律 <code>rng = np.random.default_rng(1806)</code>。浮点比较用 <code>np.allclose</code> 或 <code>np.isclose</code>，不用 <code>==</code>。</li>
<li>画图的脚本放在对应的图后面，需要 matplotlib，不带 <code>text</code> 输出块。图里的数都来自文中的代码块。</li>
</ul></section><section><h3>系列地图<a href="#系列地图"><span>#</span></a></h3><p>全系列 35 篇：第 1 到 34 讲，加上第 24 讲和第 25 讲之间的第 24b 讲（第二次测验复习）。</p>

<table><thead><tr><th>篇</th><th>讲</th><th>标题</th></tr></thead><tbody><tr><td>1</td><td>1</td><td>方程组的行图与列图</td></tr><tr><td>2</td><td>2</td><td><a href="/posts/linear-algebra-02-elimination-with-matrices/">用矩阵做消元</a></td></tr><tr><td>3</td><td>3</td><td><a href="/posts/linear-algebra-03-multiplication-and-inverse/">矩阵乘法的四种看法与逆矩阵</a></td></tr><tr><td>4</td><td>4</td><td><a href="/posts/linear-algebra-04-lu-factorization/">分解成 A = LU</a></td></tr><tr><td>5</td><td>5</td><td><a href="/posts/linear-algebra-05-transposes-permutations-spaces/">转置、置换与向量空间</a></td></tr><tr><td>6</td><td>6</td><td><a href="/posts/linear-algebra-06-column-space-and-nullspace/">列空间与零空间</a></td></tr><tr><td>7</td><td>7</td><td><a href="/posts/linear-algebra-07-solving-ax-0/">解 Ax = 0，主变量与特解</a></td></tr><tr><td>8</td><td>8</td><td><a href="/posts/linear-algebra-08-solving-ax-b/">解 Ax = b，行最简形 R</a></td></tr><tr><td>9</td><td>9</td><td><a href="/posts/linear-algebra-09-independence-basis-dimension/">线性无关、基与维数</a></td></tr><tr><td>10</td><td>10</td><td><a href="/posts/linear-algebra-10-four-fundamental-subspaces/">四个基本子空间</a></td></tr><tr><td>11</td><td>11</td><td><a href="/posts/linear-algebra-11-matrix-spaces-rank-one/">矩阵空间、秩 1 矩阵与小世界图</a></td></tr><tr><td>12</td><td>12</td><td><a href="/posts/linear-algebra-12-graphs-and-incidence-matrices/">图、网络与关联矩阵</a></td></tr><tr><td>13</td><td>13</td><td><a href="/posts/linear-algebra-13-quiz-1-review/">第一次测验复习</a></td></tr><tr><td>14</td><td>14</td><td><a href="/posts/linear-algebra-14-orthogonal-subspaces/">正交向量与正交子空间</a></td></tr><tr><td>15</td><td>15</td><td><a href="/posts/linear-algebra-15-projections/">投影到子空间</a></td></tr><tr><td>16</td><td>16</td><td><a href="/posts/linear-algebra-16-least-squares/">投影矩阵与最小二乘</a></td></tr><tr><td>17</td><td>17</td><td><a href="/posts/linear-algebra-17-orthogonal-matrices-gram-schmidt/">正交矩阵与 Gram-Schmidt</a></td></tr><tr><td>18</td><td>18</td><td><a href="/posts/linear-algebra-18-determinant-properties/">行列式的性质</a></td></tr><tr><td>19</td><td>19</td><td><a href="/posts/linear-algebra-19-determinant-formulas-cofactors/">行列式公式与代数余子式</a></td></tr><tr><td>20</td><td>20</td><td><a href="/posts/linear-algebra-20-cramer-inverse-volume/">Cramer 法则、逆矩阵与体积</a></td></tr><tr><td>21</td><td>21</td><td><a href="/posts/linear-algebra-21-eigenvalues-eigenvectors/">特征值与特征向量</a></td></tr><tr><td>22</td><td>22</td><td><a href="/posts/linear-algebra-22-diagonalization-powers/">对角化与矩阵的幂</a></td></tr><tr><td>23</td><td>23</td><td><a href="/posts/linear-algebra-23-differential-equations-exp-at/">微分方程与 exp(At)</a></td></tr><tr><td>24</td><td>24</td><td><a href="/posts/linear-algebra-24-markov-fourier/">Markov 矩阵与 Fourier 级数</a></td></tr><tr><td>25</td><td>24b</td><td><a href="/posts/linear-algebra-24b-quiz-2-review/">第二次测验复习</a></td></tr><tr><td>26</td><td>25</td><td><a href="/posts/linear-algebra-25-symmetric-positive-definite/">对称矩阵与正定性</a></td></tr><tr><td>27</td><td>26</td><td><a href="/posts/linear-algebra-26-complex-matrices-fft/">复矩阵与快速 Fourier 变换</a></td></tr><tr><td>28</td><td>27</td><td><a href="/posts/linear-algebra-27-positive-definite-minima/">正定矩阵与极小值</a></td></tr><tr><td>29</td><td>28</td><td><a href="/posts/linear-algebra-28-similar-matrices-jordan/">相似矩阵与 Jordan 标准形</a></td></tr><tr><td>30</td><td>29</td><td><a href="/posts/linear-algebra-29-svd/">奇异值分解</a></td></tr><tr><td>31</td><td>30</td><td><a href="/posts/linear-algebra-30-linear-transformations/">线性变换及其矩阵</a></td></tr><tr><td>32</td><td>31</td><td><a href="/posts/linear-algebra-31-change-of-basis-compression/">基变换与图像压缩</a></td></tr><tr><td>33</td><td>32</td><td><a href="/posts/linear-algebra-32-quiz-3-review/">第三次测验复习</a></td></tr><tr><td>34</td><td>33</td><td><a href="/posts/linear-algebra-33-pseudoinverse/">左逆、右逆与伪逆</a></td></tr><tr><td>35</td><td>34</td><td><a href="/posts/linear-algebra-34-final-review/">期末复习</a></td></tr></tbody></table></section></section>
<section><h2>行图：一次看一个方程<a href="#行图一次看一个方程"><span>#</span></a></h2><p>先看 Strang 的第一个例子：</p><span><span><span>2x−y=0−x+2y=3A=[2−1−12], b=[03]\begin{aligned}
2x - y &amp;= 0 \\
-x + 2y &amp;= 3
\end{aligned}
\qquad
A = \begin{bmatrix} 2 &amp; -1 \\ -1 &amp; 2 \end{bmatrix},\
\mathbf{b} = \begin{bmatrix} 0 \\ 3 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>2</span><span>x</span><span></span><span>−</span><span></span><span>y</span></span></span><span><span></span><span><span>−</span><span>x</span><span></span><span>+</span><span></span><span>2</span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>0</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span> </span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p><strong>行图是什么？</strong> 一次只看一行。第一个方程 <span><span>2x−y=02x - y = 0</span><span><span><span></span><span>2</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 是 <span><span>xyxy</span><span><span><span></span><span>x</span><span>y</span></span></span></span> 平面上的一条直线，过原点；第二个方程 <span><span>−x+2y=3-x + 2y = 3</span><span><span><span></span><span>−</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span> 是另一条直线，不过原点。两条直线的交点同时满足两个方程，那就是解。</p><p>他在黑板上找了几个点：第一条线上有 <span><span>(0,0)(0, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>、<span><span>(12,1)(\frac12, 1)</span><span><span><span></span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，第二条线上有 <span><span>(−3,0)(-3, 0)</span><span><span><span></span><span>(</span><span>−</span><span>3</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>(−1,1)(-1, 1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>、<span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>。交点是 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>。下面的脚本把这些点逐个代入方程，然后分别用“列的组合”和 <code>A @ x</code> 验证 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># 行图：每一行是 xy 平面上的一条直线，解是两条直线的交点</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> x, y </span><span>in</span><span> [(</span><span>0.0</span><span>, </span><span>0.0</span><span>), (</span><span>1.0</span><span>, </span><span>2.0</span><span>), (</span><span>0.5</span><span>, </span><span>1.0</span><span>)]:</span></div></div><div><div><div>11</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"(</span><span>{</span><span>x</span><span>}</span><span>, </span><span>{</span><span>y</span><span>}</span><span>) 在第一条直线上："</span><span><span>, np.</span><span>isclose</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> x </span><span>-</span><span> y, </span></span><span>0</span><span>))</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> x, y </span><span>in</span><span><span> [(</span><span>-</span></span><span>3.0</span><span>, </span><span>0.0</span><span><span>), (</span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>), (</span><span>1.0</span><span>, </span><span>2.0</span><span>)]:</span></div></div><div><div><div>13</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"(</span><span>{</span><span>x</span><span>}</span><span>, </span><span>{</span><span>y</span><span>}</span><span>) 在第二条直线上："</span><span><span>, np.</span><span>isclose</span><span>(</span><span>-</span><span>x </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> y, </span></span><span>3</span><span>))</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span># 列图：x 份第一列加 y 份第二列，凑出 b</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A, b)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>combo </span><span>=</span><span> x[</span><span>0</span><span><span>] </span><span>*</span><span> A[:, </span></span><span>0</span><span><span>] </span><span>+</span><span> x[</span></span><span>1</span><span><span>] </span><span>*</span><span> A[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>"解 x ="</span><span>, x)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>"1 * 列一 + 2 * 列二 ="</span><span>, combo)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span># 矩阵形式：A @ x</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x ="</span><span><span>, A </span><span>@</span><span> x, </span></span><span>"等于 b："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, b))</span></span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(0.0, 0.0) 在第一条直线上： True</span></div></div><div><div><div>2</div></div><div><span>(1.0, 2.0) 在第一条直线上： True</span></div></div><div><div><div>3</div></div><div><span>(0.5, 1.0) 在第一条直线上： True</span></div></div><div><div><div>4</div></div><div><span>(-3.0, 0.0) 在第二条直线上： True</span></div></div><div><div><div>5</div></div><div><span>(-1.0, 1.0) 在第二条直线上： True</span></div></div><div><div><div>6</div></div><div><span>(1.0, 2.0) 在第二条直线上： True</span></div></div><div><div><div>7</div></div><div><span>解 x = [1. 2.]</span></div></div><div><div><div>8</div></div><div><span>1 * 列一 + 2 * 列二 = [0. 3.]</span></div></div><div><div><div>9</div></div><div><span>A @ x = [0. 3.] 等于 b： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>六个点都满足各自的方程，而 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span> 同时出现在两个列表里。<code>np.linalg.solve</code> 给出同样的 <span><span>x=(1,2)\mathbf{x} = (1, 2)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>。</p><p>下面这幅图把行图和下一节的列图并排画在一起：</p><p></p><figure><img src="/assets/images/linear-algebra/l01-row-column.svg" alt="左：行图，两条直线交于 (1, 2)。右：列图，一份列一加两份列二首尾相接，到达 b = (0, 3)" /><figcaption>左：行图，两条直线交于 (1, 2)。右：列图，一份列一加两份列二首尾相接，到达 b = (0, 3)</figcaption></figure><p></p><p>上面这张图由下面这个脚本生成（需要 matplotlib，在当前目录写出 SVG；图中的数据与前面的代码块相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> matplotlib</span></div></div><div><div><div>2</div></div><div><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>matplotlib.</span><span>use</span><span>(</span><span>"Agg"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plt.rcParams[</span><span>"svg.hashsalt"</span><span><span>] </span><span>=</span><span> </span></span><span>"la1806"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span>]])</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>3.0</span><span>])</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A, b)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>fig, (ax1, ax2) </span><span>=</span><span> plt.</span><span>subplots</span><span>(</span><span>1</span><span>, </span><span>2</span><span>, </span><span>figsize</span><span><span>=</span><span>(</span></span><span>10</span><span>, </span><span>4</span><span>))</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span># row picture</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>xs </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>-</span><span>4</span><span>, </span><span>3</span><span>, </span><span>50</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(xs, </span><span>2</span><span><span> </span><span>*</span><span> xs, </span></span><span>label</span><span>=</span><span>"2x - y = 0"</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(xs, (</span><span>3</span><span><span> </span><span>+</span><span> xs) </span><span>/</span><span> </span></span><span>2</span><span>, </span><span>label</span><span>=</span><span>"-x + 2y = 3"</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>ax1.</span><span>plot</span><span>(*x, </span><span>"ko"</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>ax1.</span><span>annotate</span><span>(</span><span>"(1, 2)"</span><span>, x, </span><span>textcoords</span><span>=</span><span>"offset points"</span><span>, </span><span>xytext</span><span><span>=</span><span>(</span></span><span>8</span><span><span>, </span><span>-</span></span><span>14</span><span>))</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlim</span><span>(</span><span>-</span><span>4</span><span>, </span><span>3</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylim</span><span>(</span><span>-</span><span>3</span><span>, </span><span>5</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_title</span><span>(</span><span>"Row picture: two lines meet"</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_xlabel</span><span>(</span><span>"x"</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>ax1.</span><span>set_ylabel</span><span>(</span><span>"y"</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>ax1.</span><span>legend</span><span>(</span><span>loc</span><span>=</span><span>"upper left"</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>ax1.</span><span>grid</span><span>(</span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span># column picture</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>c1, c2 </span><span>=</span><span> A[:, </span><span>0</span><span>], A[:, </span><span>1</span><span>]</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>kw </span><span>=</span><span> {</span><span>"angles"</span><span>: </span><span>"xy"</span><span>, </span><span>"scale_units"</span><span>: </span><span>"xy"</span><span>, </span><span>"scale"</span><span>: </span><span>1</span><span>, </span><span>"width"</span><span>: </span><span>0.01</span><span>}</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>ax2.</span><span>quiver</span><span>(</span><span>0</span><span>, </span><span>0</span><span>, *c1, </span><span>color</span><span>=</span><span>"C0"</span><span>, **kw)</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>ax2.</span><span>quiver</span><span>(*c1, *c2, </span><span>color</span><span>=</span><span>"C1"</span><span>, </span><span>alpha</span><span>=</span><span>0.5</span><span>, **kw)</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>ax2.</span><span>quiver</span><span>(*(c1 </span><span>+</span><span> c2), *c2, </span><span>color</span><span>=</span><span>"C1"</span><span>, **kw)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>ax2.</span><span>quiver</span><span>(</span><span>0</span><span>, </span><span>0</span><span>, *b, </span><span>color</span><span>=</span><span>"k"</span><span>, **kw)</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>ax2.</span><span>annotate</span><span>(</span></div></div><div><div><div>38</div></div><div><span>        </span><span>"1 * col1 = (2, -1)"</span><span>,</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>c1 </span><span>/</span><span> </span><span>2</span><span>,</span></div></div><div><div><div>40</div></div><div><span>        </span><span>xytext</span><span><span>=</span><span>(</span><span>-</span></span><span>30</span><span><span>, </span><span>-</span></span><span>22</span><span>),</span></div></div><div><div><div>41</div></div><div><span>        </span><span>textcoords</span><span>=</span><span>"offset points"</span><span>,</span></div></div><div><div><div>42</div></div><div><span>        </span><span>color</span><span>=</span><span>"C0"</span><span>,</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>ax2.</span><span>annotate</span><span>(</span></div></div><div><div><div>45</div></div><div><span>        </span><span>"2 * col2 = (-2, 4)"</span><span>,</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>c1 </span><span>+</span><span> c2,</span></div></div><div><div><div>47</div></div><div><span>        </span><span>xytext</span><span><span>=</span><span>(</span><span>-</span></span><span>135</span><span>, </span><span>6</span><span>),</span></div></div><div><div><div>48</div></div><div><span>        </span><span>textcoords</span><span>=</span><span>"offset points"</span><span>,</span></div></div><div><div><div>49</div></div><div><span>        </span><span>color</span><span>=</span><span>"C1"</span><span>,</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>ax2.</span><span>annotate</span><span>(</span><span>"b = (0, 3)"</span><span>, b, </span><span>xytext</span><span><span>=</span><span>(</span></span><span>8</span><span>, </span><span>0</span><span>), </span><span>textcoords</span><span>=</span><span>"offset points"</span><span>)</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlim</span><span>(</span><span>-</span><span>3</span><span>, </span><span>3</span><span>)</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylim</span><span>(</span><span>-</span><span>2.5</span><span>, </span><span>4</span><span>)</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_aspect</span><span>(</span><span>"equal"</span><span>)</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_title</span><span>(</span><span>"Column picture: 1 * col1 + 2 * col2 = b"</span><span>)</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_xlabel</span><span>(</span><span>"first component"</span><span>)</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>ax2.</span><span>set_ylabel</span><span>(</span><span>"second component"</span><span>)</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>ax2.</span><span>grid</span><span>(</span><span>alpha</span><span>=</span><span>0.3</span><span>)</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>fig.</span><span>tight_layout</span><span>()</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>fig.</span><span>savefig</span><span>(</span></div></div><div><div><div>62</div></div><div><span>        </span><span>"l01-row-column.svg"</span><span>,</span></div></div><div><div><div>63</div></div><div><span>        </span><span>metadata</span><span><span>=</span><span>{</span></span><span>"Date"</span><span>: </span><span>None</span><span>},</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>列图：一次看一列<a href="#列图一次看一列"><span>#</span></a></h2><p><strong>列图是什么？</strong> 把同一个方程组按列写：</p><span><span><span>x[2−1]+y[−12]=[03]x \begin{bmatrix} 2 \\ -1 \end{bmatrix} +
y \begin{bmatrix} -1 \\ 2 \end{bmatrix}
= \begin{bmatrix} 0 \\ 3 \end{bmatrix}</span><span><span><span></span><span>x</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>y</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>方程现在问的是：左边两个向量各取几份，加起来等于右边那个向量？“取若干份再相加”叫线性组合，Strang 说这是整门课最基本的运算。</p><p>他的做法是先偷看答案：取 <span><span>x=1x = 1</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>y=2y = 2</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>。从原点画出第一列 <span><span>(2,−1)(2, -1)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，在箭头尖上接第二列 <span><span>(−1,2)(-1, 2)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，再接一次，落点就是 <span><span>(0,3)(0, 3)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>。上面的图右半边就是这个过程，浅色的箭头是只接了一份第二列的中间位置。两个分量分别看：第一个分量 <span><span>2−2=02 - 2 = 0</span><span><span><span></span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，第二个分量 <span><span>−1+4=3-1 + 4 = 3</span><span><span><span></span><span>−</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>4</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>。</p><p>行图和列图画的是同一个解，但它们回答的问题不一样。行图问“两条直线在哪里相交”，列图问“这些列怎么拼出 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span>”。往后的内容（列空间、零空间、秩、最小二乘）几乎都是列图的问题，我猜 Strang 在第一讲就特别强调列图，是因为这个。</p></section>
<section><h2>三阶：平面看不清，列还看得清<a href="#三阶平面看不清列还看得清"><span>#</span></a></h2><p>接下来是三个方程三个未知数。他选的方程组是：</p><span><span><span>2x−y=0−x+2y−z=−1−3y+4z=4A=[2−10−12−10−34], b=[0−14]\begin{aligned}
2x - y &amp;= 0 \\
-x + 2y - z &amp;= -1 \\
-3y + 4z &amp;= 4
\end{aligned}
\qquad
A = \begin{bmatrix} 2 &amp; -1 &amp; 0 \\ -1 &amp; 2 &amp; -1 \\ 0 &amp; -3 &amp; 4 \end{bmatrix},\
\mathbf{b} = \begin{bmatrix} 0 \\ -1 \\ 4 \end{bmatrix}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>2</span><span>x</span><span></span><span>−</span><span></span><span>y</span></span></span><span><span></span><span><span>−</span><span>x</span><span></span><span>+</span><span></span><span>2</span><span>y</span><span></span><span>−</span><span></span><span>z</span></span></span><span><span></span><span><span>−</span><span>3</span><span>y</span><span></span><span>+</span><span></span><span>4</span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>0</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>−</span><span>1</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>−</span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span> </span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>行图里每个方程是三维空间中的一个平面。前两个平面交成一条直线，第三个平面和这条直线再交成一个点。他取第二个方程 <span><span>−x+2y−z=−1-x + 2y - z = -1</span><span><span><span></span><span>−</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>y</span><span></span><span>−</span><span></span></span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span> 的三个点 <span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>(0,0,1)(0, 0, 1)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>、<span><span>(0,−12,0)(0, -\frac12, 0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 确定平面，然后说自己不是 Rembrandt，图会是弱项。（我的图也没有强多少，所以三维的行图我一张都没画。）</p><p>列图就清楚得多。三个列向量是 <span><span>(2,−1,0)(2, -1, 0)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>、<span><span>(−1,2,−3)(-1, 2, -3)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>−</span><span>3</span><span>)</span></span></span></span>、<span><span>(0,−1,4)(0, -1, 4)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span>，而右端项 <span><span>(0,−1,4)(0, -1, 4)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>4</span><span>)</span></span></span></span> 恰好就是第三列。所以只要取零份第一列、零份第二列、一份第三列：<span><span>x=(0,0,1)\mathbf{x} = (0, 0, 1)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>。三个平面的交点，行图里看不出来，列图里一眼就看出来了。</p><p>他又换了一个右端项：第一列加第二列，得到 <span><span>(1,1,−3)(1, 1, -3)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>3</span><span>)</span></span></span></span>，解显然是 <span><span>(1,1,0)(1, 1, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>。我把两个右端项都交给 NumPy，同时在行图和列图两种意义下验证：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>, </span><span>4.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>4.0</span><span>])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>b_new </span><span>=</span><span> A[:, </span><span>0</span><span><span>] </span><span>+</span><span> A[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> name, rhs </span><span>in</span><span> [(</span><span>"b"</span><span>, b), (</span><span>"b_new"</span><span>, b_new)]:</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(A, rhs)</span></div></div><div><div><div>12</div></div><div><span>        </span><span># 行图：x 同时落在三个平面上，每个平面的残差都是 0</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>row_residuals </span><span>=</span><span> [A[i] </span><span>@</span><span> x </span><span>-</span><span> rhs[i] </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)]</span></div></div><div><div><div>14</div></div><div><span>        </span><span># 列图：三列按 x 的系数加起来</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>combo </span><span>=</span><span> </span><span>sum</span><span><span>(x[j] </span><span>*</span><span> A[:, j] </span></span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>))</span></div></div><div><div><div>16</div></div><div><span>        </span><span>print</span><span>(name, </span><span>"="</span><span>, rhs)</span></div></div><div><div><div>17</div></div><div><span>        </span><span>print</span><span>(</span><span>"  x ="</span><span><span>, np.</span><span>round</span><span>(x, </span></span><span>6</span><span><span>) </span><span>+</span><span> </span></span><span>0.0</span><span>)</span></div></div><div><div><div>18</div></div><div><span>        </span><span>print</span><span>(</span><span>"  三个平面的残差："</span><span><span>, np.</span><span>array</span><span>(row_residuals))</span></span></div></div><div><div><div>19</div></div><div><span>        </span><span>print</span><span>(</span><span>"  列的组合等于右端项："</span><span><span>, np.</span><span>allclose</span><span>(combo, rhs))</span></span></div></div><div><div><div>20</div></div><div><span>        </span><span>print</span><span>(</span><span>"  A @ x 等于右端项："</span><span><span>, np.</span><span>allclose</span><span>(A </span><span>@</span><span> x, rhs))</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>b = [ 0. -1.  4.]</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>x = [0. 0. 1.]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>三个平面的残差： [0. 0. 0.]</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>列的组合等于右端项： True</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>A @ x 等于右端项： True</span></div></div><div><div><div>6</div></div><div><span>b_new = [ 1.  1. -3.]</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>x = [1. 1. 0.]</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>三个平面的残差： [0. 0. 0.]</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>列的组合等于右端项： True</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>A @ x 等于右端项： True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>“三个平面的残差”是 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的每一行点乘 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 再减去右端项的对应分量，三个都是 0 说明 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 同时落在三个平面上。“列的组合”是三列按 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 的分量加权相加。同一个 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 通过了两种检验，因为这两句话本来就是同一个方程组的两种写法。</p></section>
<section><h2>能不能填满整个空间<a href="#能不能填满整个空间"><span>#</span></a></h2><p><strong>这才是第一讲真正的问题：</strong> 保持 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 不变，<span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 取遍三维空间里的所有向量，<span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span> 每次都有解吗？换成列的说法：三列的所有线性组合，能不能填满整个三维空间？</p><p>对 Strang 选的这个 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，答案是能。它的秩是 3，行列式是 6，是他说的“好矩阵”：非奇异，可逆。</p><p>什么时候不行？三个列向量落在同一个平面里。他的例子是第三列等于第一列加第二列：这时三列的所有组合还是困在那个平面里，只有平面里的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 够得着，平面外的 <span><span>b\mathbf{b}</span><span><span><span></span><span>b</span></span></span></span> 够不着。我就用这个办法，把上面的 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 改成奇异矩阵 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span>，看看“够不着”有多远：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1806</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [</span><span>-</span></span><span>1.0</span><span>, </span><span>2.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>3.0</span><span>, </span><span>4.0</span><span>]])</span></div></div><div><div><div>9</div></div><div><span>    </span><span># 奇异的情形：第三列等于第一列加第二列，三列落在同一个平面里</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> A.</span><span>copy</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>S[:, </span><span>2</span><span><span>] </span><span>=</span><span> S[:, </span></span><span>0</span><span><span>] </span><span>+</span><span> S[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>b_in </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> S[:, </span></span><span>0</span><span><span>] </span><span>-</span><span> S[:, </span></span><span>1</span><span>]  </span><span># 在平面里的右端项</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>b_out </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>])  </span><span># 大概率在平面外</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>"A 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(A), </span></span><span>"，det ="</span><span><span>, np.linalg.</span><span>det</span><span>(A))</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"S 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(S), </span></span><span>"，det ="</span><span><span>, np.linalg.</span><span>det</span><span>(S))</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> name, rhs </span><span>in</span><span> [(</span><span>"b_in"</span><span>, b_in), (</span><span>"b_out"</span><span>, b_out)]:</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>x, </span><span>*</span><span>_ </span><span>=</span><span> np.linalg.</span><span>lstsq</span><span>(S, rhs, </span><span>rcond</span><span>=</span><span>None</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>residual </span><span>=</span><span> np.linalg.</span><span>norm</span><span>(S </span><span>@</span><span> x </span><span>-</span><span> rhs)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>：最接近的组合的残差 = </span><span>{</span><span>residual</span><span>:.4f</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span># 九维：随机的 9 x 9 矩阵，与最后一列抄倒数第二列的 9 x 9 矩阵</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>M </span><span>=</span><span> rng.</span><span>standard_normal</span><span>((</span><span>9</span><span>, </span><span>9</span><span>))</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>M_dup </span><span>=</span><span> M.</span><span>copy</span><span>()</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>M_dup[:, </span><span>8</span><span><span>] </span><span>=</span><span> M_dup[:, </span></span><span>7</span><span>]</span></div></div><div><div><div>27</div></div><div><span>    </span><span>print</span><span>(</span><span>"随机 9 x 9 的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(M))</span></span></div></div><div><div><div>28</div></div><div><span>    </span><span>print</span><span>(</span><span>"第九列抄第八列后的秩："</span><span><span>, np.linalg.</span><span>matrix_rank</span><span>(M_dup))</span></span></div></div><div><div><div>29</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"条件数："</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(M)</span></span><span>:.1f</span><span>}</span><span>"</span><span>, </span><span>f</span><span>"</span><span>{</span><span><span>np.linalg.</span><span>cond</span><span>(M_dup)</span></span><span>:.1e</span><span>}</span><span>"</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 的秩： 3 ，det = 6.0</span></div></div><div><div><div>2</div></div><div><span>S 的秩： 2 ，det = 0.0</span></div></div><div><div><div>3</div></div><div><span>b_in：最接近的组合的残差 = 0.0000</span></div></div><div><div><div>4</div></div><div><span>b_out：最接近的组合的残差 = 0.4082</span></div></div><div><div><div>5</div></div><div><span>随机 9 x 9 的秩： 9</span></div></div><div><div><div>6</div></div><div><span>第九列抄第八列后的秩： 8</span></div></div><div><div><div>7</div></div><div><span>条件数： 127.6 3.7e+17</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个数字值得停一下。</p><ul>
<li>秩从 3 掉到 2，行列式从 6.0 掉到 0.0：第三列没有给出任何新方向。</li>
<li>在平面里的右端项 <span><span>bin=2 列一−列二\mathbf{b}_\text{in} = 2\,\text{列一} - \text{列二}</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span>in</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span></span><span><span>列一</span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>列二</span></span></span></span></span>，最小二乘能找到精确组合，残差是 0.0000。</li>
<li>平面外的 <span><span>(1,0,0)(1, 0, 0)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，最好的组合也差 0.4082（恰好是 <span><span>1/61/\sqrt{6}</span><span><span><span></span><span>1/</span><span><span><span><span><span><span></span><span><span>6</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，也就是这个点到平面的距离），再怎么调系数都追不上。</li>
</ul><p>后面几讲会再回到这里：列的所有组合构成的这个平面，叫列空间。现在先把它叫作“够得着的地方”。</p><p>他还往上推了一步：九个方程九个未知数，每一列是九维空间里的向量。如果列是随机取的，几乎一定能填满九维空间（他说用 MATLAB 的随机命令取一个 9 × 9 矩阵，保证可逆）。如果第九列抄了第八列，九个列的组合就只能填满一个八维的“平面”。代码里我用固定种子的随机矩阵验证了这两句：随机矩阵秩是 9，条件数 127.6；抄了一列之后秩是 8，条件数涨到 3.7e+17。后者比 <span><span>1/ε≈4.5×10151/\varepsilon \approx 4.5 \times 10^{15}</span><span><span><span></span><span>1/</span><span>ε</span><span></span><span>≈</span><span></span></span><span><span></span><span>4.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>（双精度的机器精度 <span><span>ε\varepsilon</span><span><span><span></span><span>ε</span></span></span></span> 约为 <span><span>2.2×10−162.2 \times 10^{-16}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>）还大，数值上和奇异没有区别。</p><p>顺便一提一个类比：我觉得这像调色。三管颜料能混出多少种颜色，取决于三管颜料是不是真的各不相同；如果第三管恰好是前两管调出来的，你就没有多一种颜色。</p></section>
<section><h2><span><span>AxA\mathbf{x}</span><span><span><span></span><span>A</span><span>x</span></span></span></span> 是列的组合<a href="#axamathbfxax-是列的组合"><span>#</span></a></h2><p>最后他回到矩阵形式 <span><span>Ax=bA\mathbf{x} = \mathbf{b}</span><span><span><span></span><span>A</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span>，问了一个很小的问题：矩阵乘以向量，怎么乘？他的例子是</p><span><span><span>[2513][12]=1[21]+2[53]=[127]\begin{bmatrix} 2 &amp; 5 \\ 1 &amp; 3 \end{bmatrix}
\begin{bmatrix} 1 \\ 2 \end{bmatrix}
= 1 \begin{bmatrix} 2 \\ 1 \end{bmatrix} +
2 \begin{bmatrix} 5 \\ 3 \end{bmatrix}
= \begin{bmatrix} 12 \\ 7 \end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>5</span></span></span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>12</span></span></span><span><span></span><span><span>7</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>他说有两种算法，他自己喜欢按列：一份第一列加两份第二列，得到 <span><span>(12,7)(12, 7)</span><span><span><span></span><span>(</span><span>12</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span>。也可以按行：每一行和 <span><span>x\mathbf{x}</span><span><span><span></span><span>x</span></span></span></span> 做点积，<span><span>2×1+5×2=122 \times 1 + 5 \times 2 = 12</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>，<span><span>1×1+3×2=71 \times 1 + 3 \times 2 = 7</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>7</span></span></span></span>。结果一样，但看法不一样，往后“列的组合”这个看法要用到底：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>np.</span><span>set_printoptions</span><span>(</span><span>precision</span><span>=</span><span>4</span><span>, </span><span>suppress</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>2.0</span><span>, </span><span>5.0</span><span>], [</span><span>1.0</span><span>, </span><span>3.0</span><span>]])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>2.0</span><span>])</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>by_columns </span><span>=</span><span> x[</span><span>0</span><span><span>] </span><span>*</span><span> A[:, </span></span><span>0</span><span><span>] </span><span>+</span><span> x[</span></span><span>1</span><span><span>] </span><span>*</span><span> A[:, </span></span><span>1</span><span>]</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>by_rows </span><span>=</span><span> np.</span><span>array</span><span>([A[</span><span>0</span><span><span>] </span><span>@</span><span> x, A[</span></span><span>1</span><span><span>] </span><span>@</span><span> x])</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>"按列："</span><span>, by_columns)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>"按行："</span><span>, by_rows)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>"A @ x："</span><span><span>, A </span><span>@</span><span> x)</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"三者相等："</span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>np.</span><span>allclose</span><span>(by_columns, by_rows) </span><span>and</span><span><span> np.</span><span>allclose</span><span>(by_rows, A </span><span>@</span><span> x),</span></span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span># 反过来：右端项是 (12, 7) 时，解就是 (1, 2)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>"解："</span><span><span>, np.linalg.</span><span>solve</span><span>(A, np.</span><span>array</span><span>([</span></span><span>12.0</span><span>, </span><span>7.0</span><span>])))</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>按列： [12.  7.]</span></div></div><div><div><div>2</div></div><div><span>按行： [12.  7.]</span></div></div><div><div><div>3</div></div><div><span>A @ x： [12.  7.]</span></div></div><div><div><div>4</div></div><div><span>三者相等： True</span></div></div><div><div><div>5</div></div><div><span>解： [1. 2.]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>按列、按行、<code>A @ x</code> 三种算法得到同一个向量。最后一行反过来把 <span><span>(12,7)(12, 7)</span><span><span><span></span><span>(</span><span>12</span><span>,</span><span></span><span>7</span><span>)</span></span></span></span> 当作右端项，解出来是 <span><span>(1,2)(1, 2)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>)</span></span></span></span>，和他在这一讲最后一句说的一致。</p><p>下一讲他说要讲系统地求解任意大小方程组的办法：消元。今天我们靠“特意挑过的右端项”和肉眼找到了解，下一讲就不能再靠运气了，我会把消元的每一步写成一个矩阵，见 <a href="/posts/linear-algebra-02-elimination-with-matrices/">18.06 第 2 讲：用矩阵做消元</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-10-embedded/</id>
      <title type="text">上车之前：float32、定点数与 CAN 量化</title>
      <published>2026-10-16T09:00:00.000Z</published>
      <updated>2026-10-16T09:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-10-embedded/"/>
      <summary type="text">PID 控制系列第 10 篇，也是最后一篇：我用 Python 3.13、gcc 15.3、g++ 15.3、rustc 1.95 和 arm-none-eabi-gcc 15.2，把第 5 篇的 PID 搬到 float32 和 Q16.16 定点，再让它在三种上车会遇到的麻烦里各跑一遍：积分器停滞、CAN 的整数量化、采样周期抖动。Q16.16 在三种语言里和 Python 逐位一致（差异 0），C、C++、Rust 的 float32 也逐位一致；float32 的积分器确实会停，但在 M3508 的速度环上只停在 1.04e-3 rpm 的误差上；CAN 量化把 0.8e-3 rpm 的稳态误差变成 0.298 rpm 的锯齿；±20% 的周期抖动用实测周期补偿，IAE 只差 0.53%。M4F 上 PID 的 float32 版是 556 字节。</summary>
      <content type="html"><![CDATA[<p>前九篇所有的控制器都跑在 <code>double</code> 上，这对一台笔记本是理所当然的，对 RoboMaster 的 C 板不是。据我所知（我查了<a href="https://rm-static.djicdn.com/tem/35228/RoboMaster%20Development%20Board%20Type%20C%20User%20Manual.pdf" target="_blank">C 板的用户手册</a>，它的供电框图里把主控标成 STM32F407；<a href="https://www.st.com/en/microcontrollers-microprocessors/stm32f407ig.html" target="_blank">ST 的产品页</a>说这个系列是带 DSP 和 FPU 的 Arm Cortex-M4 内核，168 MHz），C 板上是 Cortex-M4F。据我所知，M4F 的 FPU 只做单精度，一个 <code>double</code> 乘法会掉进软件库。所以这一篇要回答的问题是：<strong>把第 5 篇的 <code>Pid</code> 搬到 <code>float</code>，再搬到 Q16.16 定点，每一步丢了什么？</strong> 我的做法照旧：同一份数据、四种语言、和 Python 的 <code>double</code> 版逐位对，然后在三个上车一定会撞上的麻烦里各跑一遍：一个会停住的积分器、CAN 总线上整数化的反馈和命令、抖动的采样周期。我预料 float32 的积分器会停。<strong>停是停了，但停得很轻，轻到我一开始以为自己的实验是错的。</strong></p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch10</code> 通过，<code>out/ch10/</code> 下 52 个文件（Python 的 5 个浮点算例和 3 个定点算例；C、C++、Rust 各自的 5 个 <code>double</code> 算例、3 个定点算例和 5 个 float32 算例；5 个计时文本），<code>figures/ch10/</code> 下 5 张图；以及 Q16.16 PID、float32 构建、三个“上车麻烦”的实验，和 Cortex-M4F 的代码体积。
新增模块：Python <code>pidlab.pid_q</code>，C <code>pl/pid_q.h</code> 和 <code>src/pid_q.c</code>，C++ <code>pidlab/pid_q.h</code>，Rust <code>pid_q.rs</code>（Q16.16 的乘法、饱和、<code>PidQ</code>）；<code>cpp/tests/no_heap.cc</code> 和 <code>scripts/ch10_checks.sh</code>（四项静态检查）。<code>Pid</code> 本身没有改，只是第一次在 <code>float</code> 下被四种语言同时使用。
前置：<a href="/posts/pid-control-05-practical/">第 5 篇</a>的 <code>Pid</code>（CLAMP 抗饱和、<code>cfg</code> 可在运行时修改）。需要的数学：浮点数的舍入、二进制补码。</p></div></div>
<section><h2>三种数，三种丢法<a href="#三种数三种丢法"><span>#</span></a></h2><p>同一个速度环，我让它在三种数字格式里各算一遍。</p><ul>
<li><strong>float32。</strong> 24 位有效数字（23 位存储加一个隐含位），相对精度 <span><span>2−24≈6×10−82^{-24}\approx6\times10^{-8}</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>−</span><span>24</span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>。数越大，相邻两个数的间隔（ulp）越大：落在 <span><span>[2e,2e+1)[2^e,2^{e+1})</span><span><span><span></span><span>[</span><span><span>2</span><span><span><span><span><span><span></span><span><span>e</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>e</span><span>+</span><span>1</span></span></span></span></span></span></span></span></span><span>)</span></span></span></span> 里的数，ulp 是 <span><span>2e−232^{e-23}</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>e</span><span>−</span><span>23</span></span></span></span></span></span></span></span></span></span></span></span>。</li>
<li><strong>Q16.16 定点。</strong> 一个 <code>int32</code>，高 16 位整数、低 16 位小数，实数值是整数除以 65536。间隔永远是 <span><span>2−16=1.53×10−52^{-16}=1.53\times10^{-5}</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.53</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span>，范围是 <span><span>[−32768, 32768)[-32768,\,32768)</span><span><span><span></span><span>[</span><span>−</span><span>32768</span><span>,</span><span></span><span></span><span>32768</span><span>)</span></span></span></span>。间隔不随数的大小变，代价是范围固定。</li>
<li><strong>CAN 上的整数。</strong> C620 电调的命令是 <span><span>[−16384, 16384][-16384,\,16384]</span><span><span><span></span><span>[</span><span>−</span><span>16384</span><span>,</span><span></span><span></span><span>16384</span><span>]</span></span></span></span> 的整数，对应 <span><span>[−20, 20][-20,\,20]</span><span><span><span></span><span>[</span><span>−</span><span>20</span><span>,</span><span></span><span></span><span>20</span><span>]</span></span></span></span> A，也就是每个单位 <span><span>1.221.22</span><span><span><span></span><span>1.22</span></span></span></span> mA；反馈里的转子转速以 rpm 为单位的整数。这两条我都核对了<a href="https://github.com/RoboGrinder-ECE/Motor_Control/blob/master/Document/RoboMaster%20C620%20Brushless%20DC%20Motor%20Speed%20Controller%20V1.01.pdf" target="_blank">C620 的用户手册</a>（我拿到的是 GitHub 上的一份镜像，不是官网的原件）：“controllable current range is -16384 ~ 0 ~ 16384”对应“-20 ~ 0 ~ 20 A”，反馈报文默认 1 kHz，“rotor speed value unit rpm”。手册没有写反馈转速字段有没有符号，我按有符号 16 位处理，这是我的假设。</li>
</ul><p>这一篇所有的 PID 都用“原始单位”：命令是 raw（<span><span>±16384\pm16384</span><span><span><span></span><span>±</span><span>16384</span></span></span></span>），反馈是<strong>转子</strong>侧的 rpm（手册说的是 rotor speed，所以要把输出轴转速乘上 <span><span>3591/187≈19.23591/187\approx19.2</span><span><span><span></span><span>3591/187</span><span></span><span>≈</span><span></span></span><span><span></span><span>19.2</span></span></span></span> 的减速比，这个比值是 <a href="https://cdn-hz.robomaster.com/robomasters/public/document/RoboMaster%20M3508%20P19%20Brushless%20DC%20Gear%20Motor.pdf" target="_blank">M3508 P19 数据手册</a>里的“Reduction Ratio 3591/187”）。第 5 篇的速度环（<span><span>ωn=30\omega_n=30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span>，<span><span>ζ=0.7\zeta=0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>）换算到原始单位，只需要把 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 乘上“raw 每安培”再除以“rpm 每 rad/s”。<code>python/ch10_embedded.py</code> 里的 <code>raw_gains</code> 就是这几行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>RAW_FULL</span><span> </span><span>=</span><span> </span></span><span>16384.0</span></div></div><div><div><div>2</div></div><div><span><span>AMP_PER_RAW</span><span> </span><span>=</span><span> </span></span><span>20.0</span><span><span> </span><span>/</span><span> </span><span>RAW_FULL</span></span></div></div><div><div><div>3</div></div><div><span><span>RAW_PER_AMP</span><span> </span><span>=</span><span> </span><span>RAW_FULL</span><span> </span><span>/</span><span> </span></span><span>20.0</span></div></div><div><div><div>4</div></div><div><span><span>RPM_PER_RADS</span><span> </span><span>=</span><span> </span></span><span>60.0</span><span><span> </span><span>/</span><span> (</span></span><span>2.0</span><span><span> </span><span>*</span><span> math.pi) </span><span>*</span><span> (</span></span><span>3591.0</span><span><span> </span><span>/</span><span> </span></span><span>187.0</span><span>)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>def</span><span> </span><span>raw_gains</span><span>() -&gt; tuple[</span><span>float</span><span>, </span><span>float</span><span>]:</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"""The ch05 speed PI (wn = 30, zeta = 0.7) in raw units per rpm."""</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> </span><span>MotorSpeedLag</span><span>()</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>kp </span><span>=</span><span> (</span><span>2.0</span><span><span> </span><span>*</span><span> </span><span>ZETA</span><span> </span><span>*</span><span> </span><span>WN</span><span> </span><span>*</span><span> m.j </span><span>-</span><span> m.b) </span><span>/</span><span> m.kt</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>ki </span><span>=</span><span> </span><span>WN</span><span> </span><span>*</span><span> </span><span>WN</span><span> </span><span>*</span><span> m.j </span><span>/</span><span> m.kt</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>scale </span><span>=</span><span> </span><span>RAW_PER_AMP</span><span> </span><span>/</span><span> </span><span>RPM_PER_RADS</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> kp </span><span>*</span><span> scale, ki </span><span>*</span><span> scale</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结果是 <span><span>kp=18.614k_p=18.614</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>18.614</span></span></span></span> raw/rpm，<span><span>ki=402.06k_i=402.06</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>402.06</span></span></span></span> raw/(rpm·s)，<span><span>kits=0.4021k_it_s=0.4021</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.4021</span></span></span></span>。</p><p>这些参数里有一处我没弄明白：数据手册给的转矩常数是 0.3 N·m/A，转速常数是 24.48 rpm/V，后者换成反电势常数是 <span><span>60/(2π⋅24.48)=0.39060/(2\pi\cdot24.48)=0.390</span><span><span><span></span><span>60/</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>24.48</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.390</span></span></span></span> V·s/rad。理想电机里两个应该相等，这里差了 30%。我不知道手册的这两个数各自对应哪个轴、哪种电流定义，所以整个系列照用 0.3，把它当作我取的量级，不当作经过验证的参数。</p></section>
<section><h2>float32：积分器为什么会停，半个 ulp<a href="#float32积分器为什么会停半个-ulp"><span>#</span></a></h2><p>第 5 篇的积分器每个采样做一次 <code>integral += ki*ts*e</code>。设积分项现在是 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，增量是 <span><span>Δ=kitse\Delta=k_it_se</span><span><span><span></span><span>Δ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span></span></span></span>。浮点加法先算精确和再舍入到最近的 float32，所以只要 <span><span>∣Δ∣|\Delta|</span><span><span><span></span><span>∣Δ∣</span></span></span></span> 小于 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 附近 ulp 的一半，<span><span>I+ΔI+\Delta</span><span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span></span></span></span> 就舍入回 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，这一步等于没做。积分器停住了。</p><p>稳态下 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 就是负载电流，在这次实验里大约是 10400 raw，落在 <span><span>[8192,16384)[8192,16384)</span><span><span><span></span><span>[</span><span>8192</span><span>,</span><span></span><span>16384</span><span>)</span></span></span></span>，<span><span>e=13e=13</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>13</span></span></span></span>，ulp <span><span>=2−10=9.77×10−4=2^{-10}=9.77\times10^{-4}</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>9.77</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>。要让积分器动起来，误差至少得是</p><span><span><span>emin⁡=ulp(I)/2kits=4.88×10−40.4021=1.21×10−3 rpme_{\min}=\frac{\text{ulp}(I)/2}{k_it_s}=\frac{4.88\times10^{-4}}{0.4021}=1.21\times10^{-3}\ \text{rpm}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>i</span><span>n</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>ulp</span></span><span>(</span><span>I</span><span>)</span><span>/2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>0.4021</span></span></span><span><span></span><span></span></span><span><span></span><span><span>4.88</span><span></span><span>×</span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.21</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span><span> </span><span><span>rpm</span></span></span></span></span></span><p>也就是说积分器自己把误差“留”在 <span><span>emin⁡e_{\min}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>i</span><span>n</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 以内：误差再小，I 项就不管了。在这个速度环上这是 2000 rpm 的 <span><span>6×10−76\times10^{-7}</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，没有任何意义。但这个公式里有两个旋钮：<span><span>II</span><span><span><span></span><span>I</span></span></span></span> 越大（接近满量程 16384 时 ulp 翻倍）、<span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 越小，<span><span>emin⁡e_{\min}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>i</span><span>n</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 越大。RM 代码里把时间常数折进 <code>ki</code> 的写法（第 1 篇和第 4 篇提过，我猜很常见）会让 <span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 比这里小得多，<span><span>kits=0.01k_it_s=0.01</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span> 对应 0.049 rpm，<span><span>0.0010.001</span><span><span><span></span><span>0.001</span></span></span></span> 对应 0.49 rpm。</p><p>光有公式我不放心，所以 <code>report</code> 里还有一个二分：在真正的 float32 加法里找最小的误差，让 <code>base + gain*e</code> 不等于 <code>base</code>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>f32_deadband</span><span>(</span><span>level</span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>ki_ts</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Smallest error that still moves a float32 integrator at `level`."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>base </span><span>=</span><span> np.</span><span>float32</span><span>(level)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>gain </span><span>=</span><span> np.</span><span>float32</span><span>(ki_ts)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>lo, hi </span><span>=</span><span> </span><span>1e-12</span><span>, </span><span>1e3</span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>200</span><span>):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>mid </span><span>=</span><span> math.</span><span>sqrt</span><span>(lo </span><span>*</span><span> hi)</span></div></div><div><div><div>8</div></div><div><span>        </span><span>if</span><span><span> base </span><span>+</span><span> gain </span><span>*</span><span> np.</span><span>float32</span><span>(mid) </span><span>!=</span><span> base:</span></span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>hi </span><span>=</span><span> mid</span></div></div><div><div><div>10</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>lo </span><span>=</span><span> mid</span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> hi</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>def</span><span> </span><span>half_ulp_deadband</span><span>(</span><span>level</span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>ki_ts</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.</span><span>spacing</span><span>(np.</span><span>float32</span><span>(level))) </span><span>/</span><span> </span></span><span>2.0</span><span><span> </span><span>/</span><span> ki_ts</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个函数对得上，说明“半个 ulp”的推导没有漏掉东西（舍入到偶数的平局不影响二分的结果，因为 <span><span>ee</span><span><span><span></span><span>e</span></span></span></span> 是连续的）。</p></section>
<section><h2>Q16.16：先写 Python 参考<a href="#q1616先写-python-参考"><span>#</span></a></h2><section><h3>乘法<a href="#乘法"><span>#</span></a></h3><p>两个 Q16.16 数的乘积有 32 位小数，要右移 16 位才回到 Q16.16。但乘积本身要 64 位：<span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 Q16.16 表示是 <span><span>18.614×65536=1.22×10618.614\times65536=1.22\times10^6</span><span><span><span></span><span>18.614</span><span></span><span>×</span><span></span></span><span><span></span><span>65536</span><span></span><span>=</span><span></span></span><span><span></span><span>1.22</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span>（21 位），误差 2000 rpm 是 <span><span>1.31×1081.31\times10^8</span><span><span><span></span><span>1.31</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>8</span></span></span></span></span></span></span></span></span></span></span>（27 位），乘起来 48 位，<code>int32</code> 装不下。所以：</p><span><span><span>qmul⁡(a,b)=sat⁡32(⌊(a b+215) / 216⌋)\operatorname{qmul}(a,b)=\operatorname{sat}_{32}\Bigl(\bigl\lfloor (a\,b+2^{15})\,/\,2^{16}\bigr\rfloor\Bigr)</span><span><span><span></span><span><span>qmul</span></span><span>(</span><span>a</span><span>,</span><span></span><span>b</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>sat</span></span><span><span><span><span><span><span></span><span><span><span>32</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>(</span></span><span><span>⌊</span></span><span>(</span><span>a</span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span><span>)</span><span></span><span>/</span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>16</span></span></span></span></span></span></span></span></span><span><span>⌋</span></span><span><span>)</span></span></span></span></span></span><p>加 <span><span>2152^{15}</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 再向下取整等于四舍五入（平局向 <span><span>+∞+\infty</span><span><span><span></span><span>+</span><span>∞</span></span></span></span>），不加的话每次乘法都向 <span><span>−∞-\infty</span><span><span><span></span><span>−</span><span>∞</span></span></span></span> 偏半个最低位，积分起来就是一个固定的漂移。右移用算术移位，负数的取整就是向 <span><span>−∞-\infty</span><span><span><span></span><span>−</span><span>∞</span></span></span></span>。加法也要饱和：<span><span>p+Ip+I</span><span><span><span></span><span>p</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span></span></span></span> 在 <span><span>kpek_pe</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span></span></span></span> 超过 32768 raw 时会溢出 <code>int32</code>。这里 <span><span>kpe=18.6×2000=37228k_pe=18.6\times2000=37228</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>18.6</span><span></span><span>×</span><span></span></span><span><span></span><span>2000</span><span></span><span>=</span><span></span></span><span><span></span><span>37228</span></span></span></span> raw，确实会超，饱和成 32768 raw，但它比输出的钳位 16384 大，所以最后决定输出的还是钳位，积分器的抗饱和判断（<code>v</code> 和 <code>u</code> 的大小关系）也不受影响。</p><p>Python 的整数是任意精度的，不会溢出，所以参考实现必须<strong>主动</strong>模拟 <code>int32</code> 的饱和，否则它描述的就不是硬件。<code>python/pidlab/pid_q.py</code> 的基本操作：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>sat32</span><span>(</span><span>x</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>return</span><span><span> </span><span>INT32_MIN</span><span> </span></span><span>if</span><span><span> x </span><span>&lt;</span><span> </span><span>INT32_MIN</span><span> </span></span><span>else</span><span><span> (</span><span>INT32_MAX</span><span> </span></span><span>if</span><span><span> x </span><span>&gt;</span><span> </span><span>INT32_MAX</span><span> </span></span><span>else</span><span> x)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>def</span><span> </span><span>to_q</span><span>(</span><span>x</span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"""Real to Q16.16: round half away from zero, saturate to int32."""</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> </span><span>sat32</span><span>(</span></span><span>int</span><span><span>(</span><span>round_half_away</span><span>(x </span><span>*</span><span> </span><span>ONE</span><span>)))</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>qmul</span><span>(</span><span>a</span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>11</div></div><div><span>    </span><span>"""(a * b) in Q16.16: int64 product, round half up, saturate."""</span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span><span> </span><span>sat32</span><span>((a </span><span>*</span><span> b </span><span>+</span><span> (</span></span><span>1</span><span><span> </span><span>&lt;&lt;</span><span> (</span><span>FRAC</span><span> </span><span>-</span><span> </span></span><span>1</span><span><span>))) </span><span>&gt;&gt;</span><span> </span><span>FRAC</span><span>)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>to_q</code> 用“四舍五入、平局远离零”（和第 4 篇起所有舍入的约定一样，Python 内置的 <code>round</code> 是银行家舍入，不能用）；而 <code>qmul</code> 里的舍入是“平局向上”，两处不同是有意的：前者是一次性的转换，后者每个采样都发生，必须便宜。</p></section><section><h3><code>update</code><a href="#update"><span>#</span></a></h3><p><code>make_config</code> 把三个增益提前换成 Q16.16：<span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>kd/tsk_d/t_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，之后每个采样里没有除法。<code>update</code> 就是第 5 篇的 CLAMP 规则换成整数：输出被钳位、并且误差把积分往钳位的方向推的时候，积分器不动。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>r_q</span></span><span>:</span><span> </span><span>int</span><span>,</span><span><span> </span><span>y_q</span></span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>2</div></div><div><span>        </span><span>"""One sample; returns the int16 command."""</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> </span><span>self</span><span>.cfg</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>e </span><span>=</span><span> </span><span>sat32</span><span>(r_q </span><span>-</span><span> y_q)</span></div></div><div><div><div>5</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span> </span><span>self</span><span>.primed:</span></div></div><div><div><div>6</div></div><div><span>            </span><span>self</span><span><span>.prev_e </span><span>=</span><span> e</span></span></div></div><div><div><div>7</div></div><div><span>            </span><span>self</span><span><span>.primed </span><span>=</span><span> </span></span><span>True</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> </span><span>qmul</span><span>(c.kp_q, e)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>d </span><span>=</span><span> </span><span>qmul</span><span>(c.kd_over_ts_q, </span><span>sat32</span><span>(e </span><span>-</span><span> </span><span>self</span><span>.prev_e))</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span><span>.prev_e </span><span>=</span><span> e</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> </span><span>sat32</span><span>(</span><span>sat32</span><span>(p </span><span>+</span><span> </span><span>self</span><span><span>.integral) </span><span>+</span><span> d)</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span>(v, c.u_min_q), c.u_max_q)</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span><span>.saturated </span><span>=</span><span> u </span><span>!=</span><span> v</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span> (</span><span>self</span><span>.saturated </span><span>and</span><span><span> (e </span><span>&gt;</span><span> </span></span><span>0</span><span><span>) </span><span>==</span><span> (v </span><span>&gt;</span><span> u) </span></span><span>and</span><span><span> e </span><span>!=</span><span> </span></span><span>0</span><span>):</span></div></div><div><div><div>15</div></div><div><span>            </span><span>self</span><span><span>.integral </span><span>=</span><span> </span><span>sat32</span><span>(</span></span><span>self</span><span><span>.integral </span><span>+</span><span> </span><span>qmul</span><span>(c.ki_ts_q, e))</span></span></div></div><div><div><div>16</div></div><div><span>        </span><span>return</span><span><span> (u </span><span>+</span><span> (</span><span>ONE</span><span> </span><span>&gt;&gt;</span><span> </span></span><span>1</span><span><span>)) </span><span>&gt;&gt;</span><span> </span><span>FRAC</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>每个加减法都过一次 <code>sat32</code>，包括 <span><span>e=r−ye=r-y</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span>：两个 <code>int32</code> 相减也能溢出。最后一行把 Q16.16 的输出舍入成 <code>int16</code> 的命令。<strong>注意这一步</strong>：无论控制器内部用什么格式，C620 收到的命令都是整数，这一步后面会成为 Q16.16 残余误差的来源。</p></section></section>
<section><h2>三种语言的 Q16.16<a href="#三种语言的-q1616"><span>#</span></a></h2><p><code>pid_q</code> 是 Python 版的逐行翻译。区别在这几处。</p><p><strong>C。</strong> 两个基本操作是 <code>static inline</code>，放在头文件里。右移负数在 C 里是“实现定义”，GCC 在<a href="https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html" target="_blank">手册的 Integers implementation 一页</a>里明确写了 “Signed ‘&gt;&gt;’ acts on negative numbers by sign extension”，所以 C 版本的可移植性依赖编译器（C++20 和 Rust 的标准则保证算术移位），我在注释里写了这一点：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* a * b in Q16.16: 64-bit product, round half up, saturate. The */</span></div></div><div><div><div>2</div></div><div><span>/* right shift of a negative value is an arithmetic shift in GCC. */</span></div></div><div><div><div>3</div></div><div><span>static</span><span> </span><span>inline</span><span> </span><span>int32_t</span><span> </span><span>pl_q_mul</span><span>(</span><span>int32_t</span><span><span> </span><span>a</span><span>,</span><span> </span></span><span>int32_t</span><span><span> </span><span>b</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span>  </span><span>return</span><span> </span><span>pl_q_sat</span><span>(((</span><span>int64_t</span><span>)a </span><span>*</span><span> b </span><span>+</span><span> (</span><span>1</span><span> </span><span>&lt;&lt;</span><span> (PL_Q_FRAC </span><span>-</span><span> </span><span>1</span><span>))) </span><span>&gt;&gt;</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>PL_Q_FRAC);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>update</code> 的结构和 Python 相同，只有抗饱和条件写成了更直接的形式：<code>wind_up = (e &gt; 0 &amp;&amp; v &gt; u) || (e &lt; 0 &amp;&amp; v &lt; u)</code>，和 Python 里 <code>saturated and (e &gt; 0) == (v &gt; u) and e != 0</code> 等价（<code>v != u</code> 时 <code>v &gt; u</code> 与 <code>v &lt; u</code> 互斥）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>p </span><span>=</span><span> </span><span>pl_q_mul</span><span><span>(</span><span>c</span></span><span>-&gt;</span><span><span>kp_q</span><span>,</span><span> e</span><span>);</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>d </span><span>=</span><span> </span><span>pl_q_mul</span><span><span>(</span><span>c</span></span><span>-&gt;</span><span><span>kd_over_ts_q</span><span>,</span><span> </span></span><span>pl_q_sat</span><span>((</span><span>int64_t</span><span><span>)</span><span>e </span></span><span>-</span><span> pid</span><span>-&gt;</span><span><span>prev_e</span><span>));</span></span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>pid</span><span>-&gt;</span><span>prev_e </span><span>=</span><span> e;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>v </span><span>=</span><span> </span><span>pl_q_sat</span><span>((</span><span>int64_t</span><span>)</span><span>pl_q_sat</span><span>((</span><span>int64_t</span><span><span>)</span><span>p </span></span><span>+</span><span> pid</span><span>-&gt;</span><span><span>integral</span><span>)</span><span> </span></span><span>+</span><span><span> d</span><span>);</span></span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>u </span><span>=</span><span> v </span><span>&lt;</span><span> c</span><span>-&gt;</span><span>u_min_q </span><span>?</span><span> </span><span>c</span><span>-&gt;</span><span>u_min_q</span><span> </span><span>:</span><span> (v </span><span>&gt;</span><span> c</span><span>-&gt;</span><span>u_max_q </span><span>?</span><span> </span><span>c</span><span>-&gt;</span><span>u_max_q</span><span> </span><span>:</span><span> v);</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>pid</span><span>-&gt;</span><span>saturated </span><span>=</span><span> u </span><span>!=</span><span> v;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>wind_up </span><span>=</span><span> (e </span><span>&gt;</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> v </span><span>&gt;</span><span> u) </span><span>||</span><span> (e </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> v </span><span>&lt;</span><span> u);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>wind_up)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>pid</span><span>-&gt;</span><span>integral </span><span>=</span><span> </span><span>pl_q_sat</span><span>((</span><span>int64_t</span><span><span>)</span><span>pid</span></span><span>-&gt;</span><span>integral </span><span>+</span></div></div><div><div><div>10</div></div><div><span>           </span><span>pl_q_mul</span><span><span>(</span><span>c</span></span><span>-&gt;</span><span><span>ki_ts_q</span><span>,</span><span> e</span><span>));</span></span></div></div><div><div><div>11</div></div><div><span>  </span><span>return</span><span> (</span><span>int16_t</span><span>)((u </span><span>+</span><span> (PL_Q_ONE </span><span>&gt;&gt;</span><span> </span><span>1</span><span>)) </span><span>&gt;&gt;</span><span> PL_Q_FRAC);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>C++。</strong> <code>PidQ</code> 是一个很小的类，没有堆，也没有异常；配置的构造 <code>MakePidQConfig</code> 校验失败时返回 <code>std::nullopt</code>，和第 5 篇的 <code>Pid&lt;T&gt;::Create</code> 同一个思路：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>optional</span><span>&lt;</span><span>PidQConfig</span><span>&gt; </span><span>MakePidQConfig</span><span>(</span><span>T</span><span><span> </span><span>kp</span><span>, </span></span><span>T</span><span><span> </span><span>ki</span><span>, </span></span><span>T</span><span><span> </span><span>kd</span><span>, </span></span><span>T</span><span><span> </span><span>ts</span><span>, </span></span><span>T</span><span><span> </span><span>u_min</span><span>,</span></span></div></div><div><div><div>3</div></div><div><span>                                         </span><span>T</span><span><span> </span><span>u_max</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span> (</span><span>ts</span><span> </span><span>&lt;=</span><span> </span><span>T</span><span>(</span><span>0</span><span>) </span><span>||</span><span> </span><span>u_min</span><span> </span><span>&gt;</span><span> </span><span>u_max</span><span>) </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>if</span><span> (</span><span>u_min</span><span> </span><span>&lt;</span><span> </span><span>-</span><span>kQRawMax</span><span> </span><span>||</span><span> </span><span>u_max</span><span> </span><span>&gt;</span><span> </span><span>kQRawMax</span><span>) </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>return</span><span> </span><span>PidQConfig</span><span>{</span><span>QFromReal</span><span>(</span><span>kp</span><span>), </span><span>QFromReal</span><span>(</span><span>ki</span><span> </span><span>*</span><span> </span><span>ts</span><span>), </span><span>QFromReal</span><span>(</span><span>kd</span><span> </span><span>/</span><span> </span><span>ts</span><span>),</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>QFromReal</span><span>(</span><span>u_min</span><span>), </span><span>QFromReal</span><span>(</span><span>u_max</span><span>)};</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>QMul</code> 里的 <code>static_cast&lt;std::int64_t&gt;</code> 必须写在乘法的前面，写在后面就晚了，<code>int32 * int32</code> 已经溢出。<code>QFromReal</code> 是模板，所以 float32 构建里 <code>T = float</code>，转换在 float 里做。</p><p><strong>Rust。</strong> <code>as i64</code> 是显式的，哪里升到了 64 位一眼就看得出来。漏写编译器不报错：溢出的时候 debug 构建 panic，release 构建回绕。饱和是一行 <code>clamp</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Saturate a 64-bit value to `i32`.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>q_sat</span><span>(</span><span>x</span><span>: </span><span>i64</span><span>) -&gt; </span><span>i32</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>x</span><span>.</span><span>clamp</span><span><span>(</span><span>i32</span><span>::</span></span><span>MIN</span><span> </span><span>as</span><span> </span><span>i64</span><span><span>, </span><span>i32</span><span>::</span></span><span>MAX</span><span> </span><span>as</span><span> </span><span>i64</span><span>) </span><span>as</span><span> </span><span>i32</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/// `a * b` in Q16.16: 64-bit product, round half up, saturate.</span></div></div><div><div><div>7</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>q_mul</span><span>(</span><span>a</span><span>: </span><span>i32</span><span>, </span><span>b</span><span>: </span><span>i32</span><span>) -&gt; </span><span>i32</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>q_sat</span><span>((</span><span>a</span><span> </span><span>as</span><span> </span><span>i64</span><span> * </span><span>b</span><span> </span><span>as</span><span> </span><span>i64</span><span> + (</span><span>1</span><span><span> </span><span>&lt;&lt;</span><span> (</span></span><span>Q_FRAC</span><span> - </span><span>1</span><span><span>))) </span><span>&gt;&gt;</span><span> </span></span><span>Q_FRAC</span><span>)</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>标量类型和没有堆。</strong> 第 5 篇的 <code>Pid</code> 在 C 里是 <code>pl_real</code>（<code>-DPL_USE_FLOAT</code> 时是 <code>float</code>）、C++ 是模板参数 <code>T</code>（这一次用 <code>Pid&lt;float&gt;</code>）、Rust 是 <code>Real</code>（<code>--features f32</code> 时是 <code>f32</code>）。<code>pid_q</code> 不受这个开关影响，永远是整数。三种语言的 PID 都没有堆分配，证据在后面的“静态检查”里。</p></section>
<section><h2>仿真：同一份数据，三个麻烦<a href="#仿真同一份数据三个麻烦"><span>#</span></a></h2><p><strong>CAN 量化：一转的反馈是 18.6 个命令单位。</strong> 反馈取整到 1 rpm 以后，量化误差在 <span><span>±0.5\pm0.5</span><span><span><span></span><span>±</span><span>0.5</span></span></span></span> rpm 之间均匀分布，比例项把它放大 <span><span>kp=18.6k_p=18.6</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>18.6</span></span></span></span> 倍，所以反馈跳一个 rpm，命令就跳 18.6 raw（22.7 mA）。命令取整到 1 raw 还要再加 <span><span>±0.5\pm0.5</span><span><span><span></span><span>±</span><span>0.5</span></span></span></span> raw 的转矩误差。后面的实验里这两个效应会同时出现，而且我能用一个数字把第二个对上。</p><p><strong>周期抖动：测得的周期只比标称值多一个零均值噪声。</strong> 采样周期 <span><span>ts,k=1 ms⋅(1+0.2wk)t_{s,k}=1\text{ ms}\cdot(1+0.2w_k)</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>s</span><span>,</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span> ms</span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>0.2</span><span><span>w</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>wk∼U[−1,1]w_k\sim U[-1,1]</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>U</span><span>[</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>]</span></span></span></span>。控制器在第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 步看到测量 <span><span>yky_k</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，它是上一个周期（<span><span>ts,k−1t_{s,k-1}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>s</span><span>,</span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 长）积分的结果，所以积分项正确的矩形近似是 <span><span>ek ts,k−1e_k\,t_{s,k-1}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>s</span><span>,</span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。标称版本用的是 <span><span>ek⋅1 mse_k\cdot1\text{ ms}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>1</span><span><span> ms</span></span></span></span></span>，两者的差是</p><span><span><span>∑kek (ts,k−1−1 ms)=∑kek⋅0.2 wk−1⋅1 ms\sum_k e_k\,(t_{s,k-1}-1\text{ ms})=\sum_k e_k\cdot0.2\,w_{k-1}\cdot1\text{ ms}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>s</span><span>,</span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span> ms</span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.2</span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>1</span><span><span> ms</span></span></span></span></span></span><p><span><span>ww</span><span><span><span></span><span>w</span></span></span></span> 零均值、互相独立，所以这是一个零均值的噪声项，不是偏差。只有误差 <span><span>eke_k</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 大的时候它才大，也就是设定值跳变的那几十毫秒。我手算的结论是：实测周期的补偿应该有用，但用处很小。</p><p>“用实测周期”在代码里就是每次更新前把 <code>cfg.ts</code> 设成上一个周期，不需要新的接口，第 5 篇已经说过 <code>cfg</code> 是可以在运行时改的。</p><section><h3>数据和仿真循环<a href="#数据和仿真循环"><span>#</span></a></h3><p>数据由 Python 生成，原生程序只读，不用任何随机数发生器。三份：<code>ch10_stall</code> 是 2000 rpm 的阶跃加一个从 1 s 开始、每秒 0.9 N·m、到 3.7 N·m 为止的缓升负载（20 s）；<code>ch10_quant</code> 是同样的阶跃加 1.5 s 处 1 N·m 的负载（3 s）；<code>ch10_jitter</code> 的设定值在 1 s 处从 2000 降到 1000 rpm，2 s 处加 2 N·m 的负载，并带有 <span><span>wkw_k</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（numpy 种子 10）。<code>write_data</code> 里最关键的两行是阶跃和缓升：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>step </span><span>=</span><span> </span><span>lambda</span><span> </span><span>t</span><span>: </span><span>2000.0</span><span><span> </span><span>*</span><span> (t </span><span>&gt;=</span><span> </span></span><span>0.05</span><span>)  </span><span># noqa: E731</span></div></div><div><div><div>2</div></div><div><span>    </span><span># load ramps to 3.7 N*m in 4.1 s, then holds</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>ramp </span><span>=</span><span> </span><span>lambda</span><span> </span><span>t</span><span><span>: np.</span><span>clip</span><span>(</span></span><span>0.9</span><span><span> </span><span>*</span><span> (t </span><span>-</span><span> </span></span><span>1.0</span><span>), </span><span>0.0</span><span>, </span><span>3.7</span><span>)  </span><span># noqa: E731</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>write_csv</span><span>(</span><span>DATA</span><span> </span><span>/</span><span> </span><span>"ch10_stall.csv"</span><span><span>, </span><span>table</span><span>(</span></span><span>20000</span><span>, step, ramp))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>浮点版本的仿真循环，就是第 5 篇的 <code>Pid</code>、一个带电流环的 <code>MotorSpeedLag</code>、和上面的单位换算。每个采样先把真实转速换算成转子 rpm，需要的话取整成“CAN 反馈”，然后（实测模式下）把 <code>cfg.ts</code> 设成上一个周期，更新，需要的话把命令取整，最后用<strong>真实</strong>的周期推进对象（RK4，10 个子步）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(data[</span><span>"t"</span><span>])):</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>r, d, w </span><span>=</span><span> data[</span><span>"r"</span><span>][k], data[</span><span>"d"</span><span>][k], data[</span><span>"w"</span><span>][k]</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>ts_k </span><span>=</span><span> </span><span>TS</span><span> </span><span>*</span><span> (</span><span>1.0</span><span><span> </span><span>+</span><span> </span><span>JITTER</span><span> </span><span>*</span><span> w)</span></span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>ytrue </span><span>=</span><span> x[</span><span>0</span><span><span>] </span><span>*</span><span> </span><span>RPM_PER_RADS</span></span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> </span><span>round_half_away</span><span>(ytrue) </span><span>if</span><span> case.quant </span><span>else</span><span> ytrue</span></div></div><div><div><div>6</div></div><div><span>        </span><span>if</span><span><span> case.jitter </span><span>==</span><span> </span></span><span>2</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>pid.cfg.ts </span><span>=</span><span> prev</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> pid.</span><span>update</span><span>(r, y)</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> case.quant </span><span>or</span><span> case.quant_u:</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>u </span><span>=</span><span> </span><span>round_half_away</span><span>(u)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>t </span><span>=</span><span> t_act </span><span>if</span><span> case.jitter </span><span>else</span><span> data[</span><span>"t"</span><span>][k]</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>([t, r, y, u, ytrue, pid.integral, </span><span>float</span><span>(pid.saturated)])</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>advance</span><span>(plant.deriv, x, [u </span><span>*</span><span> </span><span>AMP_PER_RAW</span><span>, d], ts_k, </span><span>SUBSTEPS</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>t_act </span><span>+=</span><span> ts_k</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>prev </span><span>=</span><span> ts_k</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>t</code> 列在抖动算例里是累加的真实时间。<code>quant_u</code> 是只在 Python 里用的一个开关，只取整命令、不取整反馈，后面解释它为什么存在。</p><p>Q16.16 的循环只多了两件事：把 <code>r</code>、<code>y</code> 用 <code>to_q</code> 转成 Q16.16 再交给 <code>PidQ</code>，以及输出已经是整数命令，直接乘 <code>AMP_PER_RAW</code> 喂给对象。</p><p>C 的循环是同一个形状。<code>pid.cfg.ts = prev</code> 就是“实测周期”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>if</span><span> (c</span><span>-&gt;</span><span>jitter </span><span>==</span><span> </span><span>2</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>pid.cfg.ts </span><span>=</span><span> prev;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> </span><span>pl_pid_update</span><span>(</span><span>&amp;</span><span><span>pid</span><span>,</span><span> </span></span><span>row</span><span><span>[cr]</span><span>,</span><span> y</span><span>,</span><span> </span></span><span>0</span><span><span>,</span><span> </span></span><span>false</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> (c</span><span>-&gt;</span><span>quant)</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>u </span><span>=</span><span> </span><span>PL_ROUND</span><span><span>(</span><span>u</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++ 的版本是函数模板，<code>T</code> 是 <code>double</code> 或 <code>float</code>，<code>RunAll&lt;float&gt;()</code> 用的就是第 5 篇原封不动的 <code>Pid&lt;float&gt;</code>；Rust 的 <code>Real</code> 在 <code>f32</code> 特性下是 <code>f32</code>，同一份源码编译出两个版本，只有定点部分（<code>run_fixed</code>）永远用整数。三个 float32 构建的命令是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-C</span><span> </span><span>c</span><span> </span><span>BUILD=../build/c32</span><span> </span><span>EXTRA_CFLAGS=-DPL_USE_FLOAT</span><span> </span><span>ONLY=ch10</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>rust</span><span> &amp;&amp; </span><span>cargo</span><span> </span><span>build</span><span> </span><span>--release</span><span> </span><span>--features</span><span> </span><span>f32</span><span> </span><span>--bin</span><span> </span><span>ch10</span><span> </span><span>--target-dir</span><span> </span><span>target32</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++ 不需要单独构建，同一个程序里 <code>RunAll&lt;double&gt;()</code> 和 <code>RunAll&lt;float&gt;()</code> 各跑一遍。文件名里 float32 的是 <code>_f32_c.csv</code>、<code>_f32_cpp.csv</code>、<code>_f32_rs.csv</code>。</p><p>增益在 <code>double</code> 里算：三种语言的 <code>raw_gains</code> 都用 <code>double</code> 做算术，输入是对象自己的参数（float32 构建里这些参数本身就是 float32），然后才转成 <code>pl_real</code>、<code>T</code> 或 <code>Real</code>，这样三种语言从同一个数起步。</p></section><section><h3>只在 Python 里存在的一个函数<a href="#只在-python-里存在的一个函数"><span>#</span></a></h3><p>原生的 float32 构建里控制器和对象都是 float32，对象自己的状态也会舍入，两件事混在一起。所以我在 Python 里加了 <code>run_f32_controller</code>：控制器是 numpy 的 float32，对象仍是 float64。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>kp, ki </span><span>=</span><span> (np.</span><span>float32</span><span>(v) </span><span>for</span><span> v </span><span>in</span><span><span> </span><span>raw_gains</span><span>())</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>ts, lim </span><span>=</span><span> np.</span><span>float32</span><span>(</span><span>TS</span><span>), np.</span><span>float32</span><span>(</span><span>RAW_FULL</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>...</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> np.</span><span>float32</span><span>(ytrue)</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> kp </span><span>*</span><span> (r </span><span>-</span><span> y) </span><span>+</span><span> integral</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span><span>(v, </span><span>-</span><span>lim), lim)</span></span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>e </span><span>=</span><span> r </span><span>-</span><span> y</span></div></div><div><div><div>8</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span><span> (u </span><span>!=</span><span> v </span></span><span>and</span><span><span> e </span><span>*</span><span> (v </span><span>-</span><span> u) </span><span>&gt;</span><span> </span></span><span>0</span><span>):</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>integral </span><span>=</span><span> integral </span><span>+</span><span> ki </span><span>*</span><span> ts </span><span>*</span><span> e</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一段是第 5 篇 CLAMP 规则的 float32 版，所有算术都在 <code>np.float32</code> 里。后面讲结果时会看到，为什么要把这两件事分开。</p></section></section>
<section><h2>静态检查<a href="#静态检查"><span>#</span></a></h2><p><code>scripts/ch10_checks.sh</code> 做四件事，都是 <code>make ch10</code> 里自动跑的。</p><ul>
<li><strong>C 没有堆。</strong> 对 <code>build/c/obj/*.o</code>（除了读写 CSV 的 <code>csv.o</code>）跑 <code>nm -u</code>，找不到 <code>malloc</code>、<code>calloc</code>、<code>realloc</code>、<code>free</code>。</li>
<li><strong>C++ 没有堆。</strong> <code>cpp/tests/no_heap.cc</code> 实例化 <code>Pid&lt;float&gt;</code>、<code>Pid&lt;double&gt;</code> 和 <code>PidQ</code>，用 <code>-fno-exceptions -fno-rtti</code> 编译，<code>nm -C -u</code> 里没有 <code>operator new</code> 和 <code>malloc</code>。</li>
<li><strong>Rust 的 <code>no_std</code> 构建。</strong> <code>cargo build --lib --no-default-features --features f32</code> 通过，库本身不依赖 <code>std</code>，也没有 <code>alloc</code>。</li>
<li><strong>Cortex-M4F 的编译。</strong> <code>arm-none-eabi-gcc</code>（15.2.1）编译 <code>pid.c</code> 和 <code>pid_q.c</code>，然后 <code>arm-none-eabi-size</code>。</li>
</ul><p>前两项的核心是一个 <code>nm</code> 过滤，最后一项的关键是 <code>-Wdouble-promotion -Werror</code>：任何一处 float 悄悄升成 double 都会让编译失败。脚本最后还数一下目标文件里引用了几个 <code>__aeabi_d*</code> 软件双精度函数。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> </span><span>o</span><span> </span><span>in</span><span> </span><span>build/c/obj/*.o</span><span>; </span><span>do</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>[ </span><span>"$(</span><span>basename</span><span> "</span><span>$o</span><span>")"</span><span><span> </span><span>=</span><span> csv.o ] &amp;&amp; </span></span><span>continue</span></div></div><div><div><div>3</div></div><div><span>  </span><span>if</span><span> </span><span>nm</span><span> </span><span>-u</span><span> </span><span>"</span><span>$o</span><span>"</span><span> | </span><span>grep</span><span> </span><span>-E</span><span> </span><span>' (malloc|calloc|realloc|free)$'</span><span>; </span><span>then</span></div></div><div><div><div>4</div></div><div><span>    </span><span>echo</span><span> </span><span>"heap symbol in </span><span>$o</span><span>"</span></div></div><div><div><div>5</div></div><div><span>    </span><span>bad</span><span>=</span><span>1</span></div></div><div><div><div>6</div></div><div><span>  </span><span>fi</span></div></div><div><div><div>7</div></div><div><span>done</span></div></div><div><div><div>8</div></div><div><span># ...</span></div></div><div><div><div>9</div></div><div><span>flags</span><span>=</span><span>"-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -O2 -ffunction-sections -fdata-sections -std=c11 -DPL_USE_FLOAT -Wall -Wextra -Wdouble-promotion -Werror -I c/include"</span></div></div><div><div><div>10</div></div><div><span>for</span><span> </span><span>s</span><span> </span><span>in</span><span> </span><span>pid</span><span> </span><span>pid_q</span><span>; </span><span>do</span></div></div><div><div><div>11</div></div><div><span>  </span><span>arm-none-eabi-gcc</span><span> </span><span>$flags</span><span> </span><span>-c</span><span> </span><span>"c/src/</span><span>$s</span><span>.c"</span><span> </span><span>-o</span><span> </span><span>"build/arm/</span><span>$s</span><span>.o"</span></div></div><div><div><div>12</div></div><div><span>done</span></div></div><div><div><div>13</div></div><div><span>arm-none-eabi-size</span><span> </span><span>build/arm/pid.o</span><span> </span><span>build/arm/pid_q.o</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>风格检查：C 用内核的 <code>checkpatch.pl</code>（<code>--no-tree</code>，忽略 SPDX 和 typedef 两项）对 <code>c/include/pl/*.h</code>、<code>c/src/*.c</code>、<code>c/apps/*.c</code> 全部检查，0 errors、0 warnings；C++ 用 clang-format 21.1.8 的 Google 风格（<code>-Werror</code>，现在连 <code>cpp/tests</code> 也在内）；Python 用 ruff 0.15.14（行宽 79，<code>ruff format --check</code> 和 <code>ruff check</code>）；Rust 用 <code>cargo fmt --check</code> 和 <code>cargo clippy --release -- -D warnings</code>（float32 的构建也跑 clippy）。</p></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch10</code> 是第 8 篇那种手写的目标：先 <code>ref</code>，再构建 double 和 float32 的 C、Rust，然后各个程序依次运行，再是静态检查，最后跑计时，最后才是 <code>report</code>。计时不能和别的东西并行，所以整条链是串行的。构建已经缓存的时候整条链大约 10 秒。完整输出在文末的“检查点”里，下面按问题一个个看。</p><section><h3>对得上吗：Q16.16 逐位一致，float32 也是<a href="#对得上吗q1616-逐位一致float32-也是"><span>#</span></a></h3><p>24 个比较文件全部通过。三个 <code>fixed_*</code> 算例的容差是 0，每种语言的每一列，包括 <code>r_q</code>、<code>y_q</code>、<code>u</code>、<code>integral_q</code>、<code>sat</code>，最大差异都是 0（所以三种语言的 Q16.16 和 Python 的整数参考实现逐位一致）。五个 <code>double</code> 算例和 Python 的最大差异也是 0.000e+00，比 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 的容差好得多（这五个算例没有三角函数，Rust 的 <code>libm</code> 和 glibc 的差异不会出现）。</p><p>float32 的构建没有 Python 的 float32 参考，所以我做了两件事：三种语言的 float32 输出两两比较，以及和 Python 的 <code>double</code> 参考比较。<strong>C、C++、Rust 的 float32 输出逐位一致</strong>（我把跨语言的容差设成 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>，断言是“不超过 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>”，实际是 0）。</p><p>这个“逐位一致”不是一次就到的。第一次跑的时候 C 和另外两种语言差了 0.029（积分项），C++ 和 Rust 之间是 0。原因是 <code>sim.c</code> 里的 RK4 写的是 <code>0.5 * h * k1[i]</code> 和 <code>h / 6.0</code>：字面量是 <code>double</code>，在 float32 构建里整个表达式被悄悄提升成 double 再舍回 float，而 C++ 和 Rust 是纯 float 运算。我把这几个字面量改成了 <code>0.5f</code>、<code>6.0f</code>、<code>2.0f</code>（在 double 构建里它们是精确可表示的，所以 double 的结果不变）。<code>freq.c</code> 里的 <code>PL_CABS(1.0 + cur)</code> 在 float32 构建里同样编译不过，改成 <code>(pl_real)1 + cur</code>。这两处都是前面章节的文件，改完以后我重跑了第 1、3、9 篇，都通过；其余章节我没有重跑，只是论证了改动不会改变 double 的结果：<code>0.5f</code>、<code>6.0f</code>、<code>2.0f</code> 都是精确可表示的，<code>(pl_real)1</code> 在 double 构建里就是 <code>1.0</code>。<strong>逐位一致也有前提：</strong> x86-64 的基线指令集没有融合乘加，gcc 和 rustc 都不会把 <code>a*b+c</code> 合并成一条指令。如果换到有 FMA 的目标（据我所知 M4F 的 FPU 有融合乘加指令），我猜三种语言的结果会因为编译器的合并策略不同而分开，没有试过。</p><p>和 <code>double</code> 参考比，float32 在 <code>stall</code> 上的最大差异：转速 0.014 rpm、命令 0.40 raw、积分项 0.38 raw。<code>quant_on</code> 是个例外：转速差 1.0 rpm、命令差 23 raw，因为取整的分支（<code>round(y)</code> 的 0.5 平局）在 float32 和 double 里翻向了不同的一侧，两条轨迹从那个采样起走开了。这是取整这类不连续操作的通病：最后一位上的差异会被放大成一整个量化格。这里翻转发生在 float32 对 double 之间，不是语言之间，所以三种语言互相比较的时候不需要为这个算例放宽容差。</p></section><section><h3>float32 的积分器停了，停在 1 mrpm<a href="#float32-的积分器停了停在-1-mrpm"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch10-float32-stall.svg" alt="积分器停滞：左图是半个 ulp 除以 ki·ts 给出的误差带随积分项大小的阶梯曲线，右图是缓慢加载时 float32 和 Q16.16 的积分项相对 f64 的差，在 5.6 s 之后冻结在约 0.35 和 0.20" /><figcaption>积分器停滞：左图是半个 ulp 除以 ki·ts 给出的误差带随积分项大小的阶梯曲线，右图是缓慢加载时 float32 和 Q16.16 的积分项相对 f64 的差，在 5.6 s 之后冻结在约 0.35 和 0.20</figcaption></figure><p></p><p>左图是 <span><span>emin⁡=ulp(I)/(2kits)e_{\min}=\text{ulp}(I)/(2k_it_s)</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>i</span><span>n</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ulp</span></span><span>(</span><span>I</span><span>)</span><span>/</span><span>(</span><span>2</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 的阶梯，三条线是三个 <span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：这篇用的 0.402、0.01、0.001。每次积分项跨过 2 的幂，误差带翻一倍。黑星是 float32 控制器（对象用 float64）在负载稳定后的实际残余误差。右图是整条回路都在 float32 或 Q16.16 里的积分项和 double 版本的差，大约 5.6 s 之后（负载缓升结束）就冻结了，不再变化。</p><p>数字（<code>report</code> 输出里的表）：</p><ul>
<li>积分项到 10401 raw，超过 <span><span>10410^4</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span>，负载在最后 5 s 恒定。</li>
<li><code>f64</code>：残余误差 <span><span>1.1×10−121.1\times10^{-12}</span><span><span><span></span><span>1.1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> rpm，没有停滞。</li>
<li><code>f32 ctl</code>（float32 控制器，float64 对象）：残余误差 <span><span>1.041×10−31.041\times10^{-3}</span><span><span><span></span><span>1.041</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm，积分项 10401.31（<code>f64</code> 是 10401.29，差 0.02）。</li>
<li>公式 <span><span>emin⁡e_{\min}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>m</span><span>i</span><span>n</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>1.214×10−31.214\times10^{-3}</span><span><span><span></span><span>1.214</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm，实测是它的 0.86 倍。公式是上限（积分器停在误差带的边缘以内），所以这个比值合理。</li>
<li>float32 加法二分出来的误差带，和公式在三个 <span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 上完全一致：0.4021 对 <span><span>1.214×10−31.214\times10^{-3}</span><span><span><span></span><span>1.214</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，0.01 对 <span><span>4.883×10−24.883\times10^{-2}</span><span><span><span></span><span>4.883</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>2</span></span></span></span></span></span></span></span></span></span></span></span>，0.001 对 <span><span>4.883×10−14.883\times10^{-1}</span><span><span><span></span><span>4.883</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> rpm。</li>
</ul><p>所以结论是：<strong>float32 的积分器停滞是真的，大小和半个 ulp 的推导一致，但对这个速度环它是 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm 的事，在 2000 rpm 上不值得操心。</strong> 如果 <span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 小两个数量级，误差带就是 0.5 rpm，开始有意义；我没有在 RM 的真实固件里核实 <span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 通常是多大，所以“RM 代码里 <span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 很小”只是我的猜测，不是结论。</p><p>这里有一个我差点说错的地方。原生的 float32 构建（控制器和对象都是 float32）残余误差是 <span><span>7.32×10−47.32\times10^{-4}</span><span><span><span></span><span>7.32</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rpm，积分项 10401.63，和 <code>double</code> 差 0.34；而 float32 控制器配 float64 对象的积分项只差 0.02。多出来的那 0.32 raw 来自<strong>对象</strong>：float32 的 <code>w</code> 在 10.9 rad/s 附近的 ulp 是 <span><span>9.5×10−79.5\times10^{-7}</span><span><span><span></span><span>9.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，RK4 子步的增量 <span><span>h w˙h\,\dot w</span><span><span><span></span><span>h</span><span></span><span><span><span><span><span><span></span><span>w</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span> 小于一半就丢了，对象自己的状态也在停滞。真实的电机不会这样，所以用 float32 仿真去回答“真车上的 float32 控制器会怎样”会掺进仿真器自己的舍入。我把 <code>f32 ctl</code> 这一行加进了 <code>report</code> 就是为了把它们分开，上面的结论用的是它。</p></section><section><h3>Q16.16 对 float32：不是更准，是误差换了个地方<a href="#q1616-对-float32不是更准是误差换了个地方"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch10-fixed-vs-float.svg" alt="Q16.16 和 float32 的残余误差：左图 10 s 起的 0.1 s，float32 是一条 7.3e-4 rpm 的水平线，Q16.16 是 ±1e-3 rpm 的锯齿，float64 在 0；右图是 Q16.16 命令减 f64 命令，在 -0.29 到 +0.71 raw 之间跳" /><figcaption>Q16.16 和 float32 的残余误差：左图 10 s 起的 0.1 s，float32 是一条 7.3e-4 rpm 的水平线，Q16.16 是 ±1e-3 rpm 的锯齿，float64 在 0；右图是 Q16.16 命令减 f64 命令，在 -0.29 到 +0.71 raw 之间跳</figcaption></figure><p></p><p>负载恒定之后三种格式的残余误差。float32（整条回路）是一条水平线：积分器停了，误差不动。Q16.16 是锯齿：它的最大误差是 <span><span>1.129×10−31.129\times10^{-3}</span><span><span><span></span><span>1.129</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm，比 float32 大，但它不是积分器的问题。Q16.16 的积分项分辨率是 <span><span>1.5×10−51.5\times10^{-5}</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span> raw，比 float32 在 10401 附近的 <span><span>9.8×10−49.8\times10^{-4}</span><span><span><span></span><span>9.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> 细 64 倍，积分器根本不会停。锯齿来自最后一步：<strong>命令取整成 <code>int16</code></strong>。我在 Python 里把 <code>double</code> 版本的命令也取整成整数（不动反馈，不用定点），残余误差是 <span><span>1.124×10−31.124\times10^{-3}</span><span><span><span></span><span>1.124</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm，和 Q16.16 的 <span><span>1.129×10−31.129\times10^{-3}</span><span><span><span></span><span>1.129</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> 差不到 0.5%。<code>report</code> 里断言了这两个数在 10% 以内。右图是 Q16.16 的命令减去 <code>double</code> 命令，只在 <span><span>−0.29-0.29</span><span><span><span></span><span>−</span><span>0.29</span></span></span></span> 和 <span><span>+0.71+0.71</span><span><span><span></span><span>+</span><span>0.71</span></span></span></span> raw 之间跳，就是取整的两侧。</p><p>所以“定点数更准”是错的，更准确的说法是：<strong>定点数的分辨率是常数，float32 的分辨率随数的大小变；这个对象里积分项是大数（1 万以上），定点数的分辨率反而更细，但输出最后要变成 <code>int16</code>，这一步无论用哪种格式都躲不掉。</strong></p></section><section><h3>CAN 量化：不是抖动，是 0.85 秒一个的锯齿<a href="#can-量化不是抖动是-085-秒一个的锯齿"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch10-can-quantization.svg" alt="CAN 量化：负载稳定后的 1 秒，quant_off（蓝）的误差贴着 0，quant_on（橙）的误差从 +0.45 rpm 线性掉到 -0.50 rpm 然后跳回，命令基本不动，在 2.85 s 处有一个单采样的 -19 raw 毛刺" /><figcaption>CAN 量化：负载稳定后的 1 秒，quant_off（蓝）的误差贴着 0，quant_on（橙）的误差从 +0.45 rpm 线性掉到 -0.50 rpm 然后跳回，命令基本不动，在 2.85 s 处有一个单采样的 -19 raw 毛刺</figcaption></figure><p></p><p>指标（<code>report</code> 输出）：</p><ul>
<li>负载稳定后（2 到 3 s）速度误差的均方根：<code>quant_off</code> <span><span>7.96×10−47.96\times10^{-4}</span><span><span><span></span><span>7.96</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rpm，<code>quant_on</code> <span><span>0.2980.298</span><span><span><span></span><span>0.298</span></span></span></span> rpm，是 374 倍。</li>
<li>这 1 s 里命令变了多少次：<code>quant_off</code> 997 次（每个采样都在微调，峰峰值 0.1 raw），<code>quant_on</code> 只有 4 次（峰峰值 37 raw：2.001 s 和 2.002 s 是负载瞬态的尾巴，2.849 s 和 2.850 s 是下面要说的翻档毛刺和恢复）。</li>
</ul><p>我原本以为量化会让命令抖，<strong>实际是反过来的：量化让命令几乎不动，让速度慢慢漂。</strong> 图里 <code>quant_on</code> 的误差从 <span><span>+0.45+0.45</span><span><span><span></span><span>+</span><span>0.45</span></span></span></span> rpm 一路线性掉到 <span><span>−0.50-0.50</span><span><span><span></span><span>−</span><span>0.50</span></span></span></span> rpm，斜率 1.12 rpm/s（我拟合出来是 1.125），到 2.85 s 反馈的整数 rpm 翻了一档，比例项一步把命令拉低了一个 <span><span>kp×1 rpm=18.6k_p\times1\text{ rpm}=18.6</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span> rpm</span></span><span></span><span>=</span><span></span></span><span><span></span><span>18.6</span></span></span></span> raw（图里的毛刺，3010 对 3029），然后回到 3028，误差跳回去，下一轮锯齿开始。</p><p>斜率可以对上：命令是整数 3029，而需要的电流大约对应 3028.5，差半个 raw。半个 raw 的转矩误差是 <span><span>0.5×2016384×0.3=1.83×10−40.5\times\frac{20}{16384}\times0.3=1.83\times10^{-4}</span><span><span><span></span><span>0.5</span><span></span><span>×</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>16384</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>20</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>0.3</span><span></span><span>=</span><span></span></span><span><span></span><span>1.83</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> N·m，除以 <span><span>J=0.03J=0.03</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.03</span></span></span></span> 得 <span><span>6.1×10−36.1\times10^{-3}</span><span><span><span></span><span>6.1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rad/s²，乘 183.4 rpm/(rad/s) 得 1.119 rpm/s。拟合出来的 1.125 rpm/s 对上了。这个对象的转子 rpm 量化（1 rpm）和命令量化（半个 raw）合起来产生了一个 0.85 s 周期、<span><span>±0.5\pm0.5</span><span><span><span></span><span>±</span><span>0.5</span></span></span></span> rpm 幅度的极限环，在 2000 rpm 上是 0.025%。在这个 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 下，它不是抖，是慢速的锯齿；<span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 再大几倍，每次翻档的毛刺就会更明显。这是一个设定值和负载都是常数的算例，我没有扫 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p></section><section><h3>抖动：实测周期只改善了 0.53%<a href="#抖动实测周期只改善了-053"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch10-jitter.svg" alt="周期抖动：左图 1 s 处设定值从 2000 降到 1000 rpm，标称版本和实测版本的转速曲线几乎重合；右图是两个版本的转速差，最大约 8 rpm，出现在设定值跳变之后" /><figcaption>周期抖动：左图 1 s 处设定值从 2000 降到 1000 rpm，标称版本和实测版本的转速曲线几乎重合；右图是两个版本的转速差，最大约 8 rpm，出现在设定值跳变之后</figcaption></figure><p></p><p>指标：</p><ul>
<li>设定值降到 1000 rpm 之后那 1 s 的 IAE：标称 30.947 rpm·s，实测 30.782 rpm·s，差 0.53%。</li>
<li>2 到 3 s（负载 2 N·m 加上以后）：标称 14.796，实测 14.991；也就是说<strong>实测并不总是更好</strong>，这一段它多了 1.3%。</li>
<li>2.5 s 起的稳态里，命令的标准差 0.0335（标称）对 0.0324（实测），误差的标准差 <span><span>2.16×10−32.16\times10^{-3}</span><span><span><span></span><span>2.16</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> 对 <span><span>2.18×10−32.18\times10^{-3}</span><span><span><span></span><span>2.18</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm。</li>
</ul><p>所以前面手算的结论成立：差是一个零均值噪声项，大小取决于误差的大小，设定值跳变的时候最大（图右，最多约 8 rpm 的转速差，占 2000 到 1000 的跳变的 0.8%）。<strong>对这个对象，±20% 的周期抖动用不着补偿。</strong> 这不是说周期抖动永远不用管：这里的抖动是独立同分布、零均值的，真实的抖动（中断被别的任务延迟）往往有偏、有相关，我没有试。</p></section><section><h3>计时：主机上的数，不是 MCU 上的数<a href="#计时主机上的数不是-mcu-上的数"><span>#</span></a></h3><div><div><div></div><div>Warning</div></div><div><p>下面所有计时都是在我的笔记本（AMD Ryzen 7 PRO 6850U with Radeon Graphics，来自 <code>/proc/cpuinfo</code>）上测的，不是 Cortex-M4F。主机有乱序执行、分支预测和大缓存，更新一次只要几个纳秒；M4F 没有这些，同样的代码要多少时钟周期我没有测。<strong>这些数只能比较实现，不能拿来估计 C 板上的耗时。</strong></p></div></div><p>每个程序做 <span><span>10610^6</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 次更新，重复 5 次，取中位数（纳秒/次），输入是一张 4096 点的带噪声测量表：</p>

<table><thead><tr><th>实现</th><th>float64</th><th>float32</th><th>Q16.16</th></tr></thead><tbody><tr><td>C</td><td>4.71</td><td>4.69</td><td>3.79</td></tr><tr><td>C++</td><td>1.87</td><td>2.18</td><td>2.00</td></tr><tr><td>Rust</td><td>4.01</td><td>4.08</td><td>5.41</td></tr></tbody></table><p></p><figure><img src="/assets/images/pid-control/ch10-timing.svg" alt="主机计时：每种实现三种格式每次更新的纳秒数，C++ 最低，Q16.16 在 C 里最快、在 Rust 里最慢" /><figcaption>主机计时：每种实现三种格式每次更新的纳秒数，C++ 最低，Q16.16 在 C 里最快、在 Rust 里最慢</figcaption></figure><p></p><p>这张图能读出的东西很少。语言之间的差（C++ 比 C 快 2 倍多）大于格式之间的差；我猜原因是 C++ 的 <code>Pid</code> 是头文件里的模板，可以内联进测试循环，而 C 的 <code>pl_pid_update</code> 在另一个编译单元里，没有链接时优化，每次是一个真正的函数调用；这是猜测，我没有看汇编。格式之间的差更没有规律：Q16.16 在 C 里最快，在 Rust 里最慢。在 x86-64 上 float32 和 float64 的标量乘加延迟本来就差不多，所以 float32 没有变快也不奇怪。<strong>这些数不能支持“定点比浮点快”或者“float32 比 double 快”。</strong> 另外这是 5 次中位数、一台笔记本、没有锁频，噪声本身也不小。</p><p>在 M4F 上有一个更有意义的数：目标文件的大小。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ arm-none-eabi-size build/arm/pid.o build/arm/pid_q.o</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>text     data      bss      dec      hex  filename</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>556        0        0      556      22c  build/arm/pid.o</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>1332        0        0     1332      534  build/arm/pid_q.o</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>float32 的 <code>pid.o</code> 是 556 字节，Q16.16 的 <code>pid_q.o</code> 是 1332 字节，更大。M4F 有硬件的单精度 FPU，浮点乘加是一条指令；定点这边每次乘法要处理 64 位中间值、舍入和饱和，指令更多。脚本最后数到的软件双精度函数（<code>__aeabi_d*</code>）是 0 个，所以 <code>-Wdouble-promotion -Werror</code> 的意义是实打实的：这两个文件里没有任何一处悄悄用了 double。<strong>这回答了开头的问题：在有单精度 FPU 的 C 板上，float32 的 <code>Pid</code> 比 Q16.16 版本更小，我没有测它更快还是更慢（那需要真板子）。定点数的价值在没有 FPU 的核（Cortex-M0、M3 一类）上，那些我没有编译过，所以这是一个推测。</strong></p></section><section><h3>没有按直觉发生的事<a href="#没有按直觉发生的事"><span>#</span></a></h3><ul>
<li><strong>float32 积分器停滞的大小比我预想的小得多。</strong> 我预料它会成为这篇最显眼的问题，但在这个速度环上它是 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm。我没有为了让它更明显去降低 <span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，那样就不是我手上这个速度环了；二分表里给了 <span><span>kitsk_it_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 小一到两个数量级时的数字。</li>
<li><strong>原生 float32 构建里对象自己也在舍入。</strong> 所以停滞的数字取自 Python 的 <code>f32 ctl</code>（控制器 float32，对象 float64），而不是原生构建的 <span><span>7.3×10−47.3\times10^{-4}</span><span><span><span></span><span>7.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>。这个函数只在 Python 里存在，原生程序不做这个组合。</li>
<li><strong>Q16.16 的残余误差来自 <code>int16</code> 命令，不来自定点。</strong> 用 <code>double</code> 加整数命令复现了同样的数字（<span><span>1.124×10−31.124\times10^{-3}</span><span><span><span></span><span>1.124</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> 对 <span><span>1.129×10−31.129\times10^{-3}</span><span><span><span></span><span>1.129</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>）。这个实验不在原生程序里，只在 <code>report</code> 里，靠 Python 的 <code>quant_u</code> 开关。</li>
<li><strong>CAN 量化的“抖”不存在，存在的是锯齿。</strong> 我预想命令会抖，实测命令几乎不动。我只测了一个 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
<li><strong>周期抖动的补偿只有 0.53%。</strong> 一个算例，一个种子（10）。2 到 3 s 段补偿反而差了 1.3%。</li>
<li><strong>三种语言的 float32 逐位一致依赖 x86-64 没有 FMA。</strong> 换到有 FMA 的目标，我不保证。</li>
</ul></section><section><h3>哪些参数我核实过<a href="#哪些参数我核实过"><span>#</span></a></h3><p>M3508 的转矩常数 0.3 N·m/A、减速比 3591/187、空载 482 rpm 和额定 3 N·m 在<a href="https://cdn-hz.robomaster.com/robomasters/public/document/RoboMaster%20M3508%20P19%20Brushless%20DC%20Gear%20Motor.pdf" target="_blank">数据手册</a>里有；C620 的 <span><span>±16384↔±20\pm16384\leftrightarrow\pm20</span><span><span><span></span><span>±</span><span>16384</span><span></span><span>↔</span><span></span></span><span><span></span><span>±</span><span>20</span></span></span></span> A 和 rpm 反馈在手册里有（镜像文件）；C 板是 STM32F407 来自 C 板手册的框图（PDF 转文字后读到的，图本身我没有看）。<strong>没有核实的</strong>：数据手册里转矩常数对应哪根轴、反馈转速字段是否有符号、负载惯量 0.03 kg·m²（我取的量级）、电流环时间常数 1 ms（我取的量级）。C 的 <code>pid.o</code> 和 <code>pid_q.o</code> 在 M4F 上的体积是真实的编译结果，但没有在真板子上跑过。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在仓库根目录、<code>nix develop</code> 里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch10</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的输出（真实运行；24 行 <code>ok   ...</code> 的比较结果我只留了 6 行，其余形式相同，全部是 0 差异）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch10</span></div></div><div><div><div>2</div></div><div><span>ch10: C (f64) wrote 5 cases to out/ch10</span></div></div><div><div><div>3</div></div><div><span>ch10: C (f32) wrote 5 cases to out/ch10</span></div></div><div><div><div>4</div></div><div><span>ch10: C++ (f64) wrote 5 cases to out/ch10</span></div></div><div><div><div>5</div></div><div><span>ch10: C++ (f32) wrote 5 cases to out/ch10</span></div></div><div><div><div>6</div></div><div><span>ch10: Rust (f64) wrote 5 cases to out/ch10</span></div></div><div><div><div>7</div></div><div><span>ch10: Rust (f32) wrote 5 cases to out/ch10</span></div></div><div><div><div>8</div></div><div><span>== C: heap symbols referenced by the library objects (everything except csv.o)</span></div></div><div><div><div>9</div></div><div><span>ok: no malloc/calloc/realloc/free in 13 library objects</span></div></div><div><div><div>10</div></div><div><span>== C++: heap symbols in the compiled controllers (-fno-exceptions -fno-rtti)</span></div></div><div><div><div>11</div></div><div><span>ok: no operator new / malloc; undefined symbols are:</span></div></div><div><div><div>12</div></div><div><span>== Rust: no_std build of the library (no alloc)</span></div></div><div><div><div>13</div></div><div><span>ok: cargo build --lib --no-default-features --features f32</span></div></div><div><div><div>14</div></div><div><span>== Cortex-M4F code size of the controllers (float, -O2, compile only)</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>text     data      bss      dec      hex  filename</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>556        0        0      556      22c  build/arm/pid.o</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>1332        0        0     1332      534  build/arm/pid_q.o</span></div></div><div><div><div>18</div></div><div><span>soft-double helper symbols (__aeabi_d*, __aeabi_f2d) referenced: 0</span></div></div><div><div><div>19</div></div><div><span>natives vs the Python reference (tolerance 1e-9, Q16.16: 0)</span></div></div><div><div><div>20</div></div><div><span>ok   fixed_stall_c.csv: max diff 0.000e+00 &lt;= 0.0e+00</span></div></div><div><div><div>21</div></div><div><span>ok   fixed_stall_cpp.csv: max diff 0.000e+00 &lt;= 0.0e+00</span></div></div><div><div><div>22</div></div><div><span>ok   fixed_stall_rs.csv: max diff 0.000e+00 &lt;= 0.0e+00</span></div></div><div><div><div>23</div></div><div><span>ok   stall_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>24</div></div><div><span>ok   stall_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>25</div></div><div><span>ok   stall_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>float32 builds: C vs C++ vs Rust, and each vs the f64 reference</span></div></div><div><div><div>28</div></div><div><span>case                 c-cpp      c-rs  f32-f64 y  f32-f64 u f32-f64 int</span></div></div><div><div><div>29</div></div><div><span>stall             0.00e+00  0.00e+00  1.432e-02  3.994e-01   3.772e-01</span></div></div><div><div><div>30</div></div><div><span>quant_off         0.00e+00  0.00e+00  1.432e-02  3.986e-01   3.886e-01</span></div></div><div><div><div>31</div></div><div><span>quant_on          0.00e+00  0.00e+00  1.000e+00  2.300e+01   4.826e+00</span></div></div><div><div><div>32</div></div><div><span>jitter_nominal    0.00e+00  0.00e+00  1.234e-02  3.510e-01   3.238e-01</span></div></div><div><div><div>33</div></div><div><span>jitter_measured   0.00e+00  0.00e+00  1.295e-02  3.451e-01   3.261e-01</span></div></div><div><div><div>34</div></div><div><span>ok   float32 outputs agree across languages within 1e-04</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>speed PI in raw units: kp = 18.614 /rpm, ki = 402.06 /rpm/s, ki*ts = 0.4021</span></div></div><div><div><div>37</div></div><div><span>integrator stall: slow load ramp, last 5 s (load constant)</span></div></div><div><div><div>38</div></div><div><span><span>           </span></span><span>|integral| end  max |e| (rpm)</span></div></div><div><div><div>39</div></div><div><span>(f32 ctl: float32 controller on a float64 plant;</span></div></div><div><div><div>40</div></div><div><span><span> </span></span><span>f64 int u: float64 controller, command rounded to int)</span></div></div><div><div><div>41</div></div><div><span>f64              10401.29      1.137e-12</span></div></div><div><div><div>42</div></div><div><span>f32              10401.63      7.324e-04</span></div></div><div><div><div>43</div></div><div><span>q16              10401.49      1.129e-03</span></div></div><div><div><div>44</div></div><div><span>f32 ctl          10401.31      1.041e-03</span></div></div><div><div><div>45</div></div><div><span>f64 int u        10401.49      1.124e-03</span></div></div><div><div><div>46</div></div><div><span>integral gap vs f64: f32 0.377, q16 0.364 (raw)</span></div></div><div><div><div>47</div></div><div><span>deadband at integral = 10402 (error that still moves it):</span></div></div><div><div><div>48</div></div><div><span><span>   </span></span><span>ki*ts   half ulp / (ki*ts)  float32 bisection</span></div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>0.4021            1.214e-03          1.214e-03</span></div></div><div><div><div>50</div></div><div><span><span>  </span></span><span>0.0100            4.883e-02          4.883e-02</span></div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>0.0010            4.883e-01          4.883e-01</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>CAN quantization: integer rpm feedback, int16 command</span></div></div><div><div><div>54</div></div><div><span><span>           </span></span><span>rms e [1,1.5)  rms e [2,3)  u changes [2,3)  u ptp [2,3)</span></div></div><div><div><div>55</div></div><div><span>quant_off      6.707e-07    7.964e-04              997          0.1</span></div></div><div><div><div>56</div></div><div><span>quant_on       2.731e-01    2.978e-01                4         37.0</span></div></div><div><div><div>57</div></div><div><span>one rpm of feedback is kp = 18.6 raw of command</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>jitter: ts_k = 1 ms * (1 + 0.2 w), w ~ U[-1, 1]</span></div></div><div><div><div>60</div></div><div><span><span>                  </span></span><span>IAE [1,2) rpm s  IAE [2,3) rpm s  late std u  late std e</span></div></div><div><div><div>61</div></div><div><span>jitter_nominal             30.947           14.796      0.0335    2.16e-03</span></div></div><div><div><div>62</div></div><div><span>jitter_measured            30.782           14.991      0.0324    2.18e-03</span></div></div><div><div><div>63</div></div><div><span>IAE difference after the setpoint step: 0.53 %</span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>host timing on: AMD Ryzen 7 PRO 6850U with Radeon Graphics</span></div></div><div><div><div>66</div></div><div><span>ns per update, median of 5 runs of 1e6 (host, NOT the MCU)</span></div></div><div><div><div>67</div></div><div><span><span>            </span></span><span>f64      f32      q16</span></div></div><div><div><div>68</div></div><div><span>c          4.71     4.69     3.79</span></div></div><div><div><div>69</div></div><div><span>cpp        1.87     2.18     2.00</span></div></div><div><div><div>70</div></div><div><span>rs         4.01     4.08     5.41</span></div></div><div><div><div>71</div></div><div><span>figure: figures/ch10/float32-stall.svg</span></div></div><div><div><div>72</div></div><div><span>figure: figures/ch10/fixed-vs-float.svg</span></div></div><div><div><div>73</div></div><div><span>figure: figures/ch10/can-quantization.svg</span></div></div><div><div><div>74</div></div><div><span>figure: figures/ch10/jitter.svg</span></div></div><div><div><div>75</div></div><div><span>figure: figures/ch10/timing.svg</span></div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span>ch10 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>所有文件都在 pid-lab 仓库的 <code>tutorial-v1</code> 标签里。下面的命令都在仓库根目录、<code>nix develop</code> 里运行。</p><p>Python：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid_q.py" target="_blank"><code>python/pidlab/pid_q.py</code></a></td><td><code>make -s ch10</code></td><td>Q16.16 的整数参考，三种语言的 <code>fixed_*</code> 文件与它的差异都是 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch10_embedded.py" target="_blank"><code>python/ch10_embedded.py</code></a></td><td><code>python3 python/ch10_embedded.py ref</code> 然后 <code>report</code></td><td>生成 <code>data/ch10_*.csv</code> 和 Python 参考；<code>report</code> 打印上面的表，保存 5 张图，最后一行 <code>ch10 OK</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid.py" target="_blank"><code>python/pidlab/pid.py</code></a></td><td><code>make -s ch10</code></td><td>第 5 篇的 <code>Pid</code>，这一篇原样使用，24 个比较文件全部 <code>ok</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>python/pidlab/sim.py</code></a></td><td><code>make -s ch10</code></td><td><code>MotorSpeedLag</code> 和 RK4 推进，所有语言共用的仿真契约</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/compare.py" target="_blank"><code>python/compare.py</code></a></td><td><code>make -s ch10</code></td><td>24 个文件，<code>fixed_*</code> 容差 0，其余 1e-9，最大差异都是 0.000e+00</td></tr></tbody></table><p>C、C++、Rust：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/pid_q.h" target="_blank"><code>c/include/pl/pid_q.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid_q.c" target="_blank"><code>c/src/pid_q.c</code></a></td><td><code>make -C c BUILD=../build/c32 EXTRA_CFLAGS=-DPL_USE_FLOAT ONLY=ch10</code></td><td>C 的 Q16.16 和 float32 构建；<code>pid.o</code> 556 字节、<code>pid_q.o</code> 1332 字节（M4F，<code>make ch10</code> 的检查里）</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch10.c" target="_blank"><code>c/apps/ch10.c</code></a></td><td><code>./build/c/ch10 run</code>（<code>make ch10</code> 里自动跑）；<code>./build/c/ch10 bench</code></td><td><code>ch10: C (f64) wrote 5 cases to out/ch10</code>；bench 的 c f64 是 4.71 ns</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/sim.c" target="_blank"><code>c/src/sim.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/freq.c" target="_blank"><code>c/src/freq.c</code></a></td><td><code>make -s ch10</code></td><td>这一篇为 float32 构建改的两个旧文件；C、C++、Rust 的 float32 输出逐位一致</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid_q.h" target="_blank"><code>cpp/include/pidlab/pid_q.h</code></a></td><td><code>make -s ch10</code></td><td>C++ 的 <code>PidQ</code> 和 <code>MakePidQConfig</code>，<code>fixed_*_cpp.csv</code> 差异 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch10.cc" target="_blank"><code>cpp/apps/ch10.cc</code></a></td><td><code>./build/cpp/ch10 run</code>；<code>./build/cpp/ch10 bench</code></td><td><code>RunAll&lt;double&gt;()</code> 和 <code>RunAll&lt;float&gt;()</code> 各写 5 个算例；bench 的 cpp f64 是 1.87 ns</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/tests/no_heap.cc" target="_blank"><code>cpp/tests/no_heap.cc</code></a></td><td><code>./scripts/ch10_checks.sh</code></td><td><code>-fno-exceptions -fno-rtti</code> 下编译，没有 <code>operator new</code> 和 <code>malloc</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid_q.rs" target="_blank"><code>rust/src/pid_q.rs</code></a></td><td><code>cd rust &amp;&amp; cargo build --lib --no-default-features --features f32</code></td><td>Rust 的 <code>PidQ</code>，<code>no_std</code> 构建通过，<code>fixed_*_rs.csv</code> 差异 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch10.rs" target="_blank"><code>rust/src/bin/ch10.rs</code></a></td><td><code>./rust/target/release/ch10 run</code>；<code>cargo build --release --features f32 --bin ch10 --target-dir target32</code></td><td><code>ch10: Rust (f64) wrote 5 cases to out/ch10</code>，f32 构建同样写 5 个算例</td></tr></tbody></table><p>脚本和 Makefile：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/scripts/ch10_checks.sh" target="_blank"><code>scripts/ch10_checks.sh</code></a></td><td><code>./scripts/ch10_checks.sh</code></td><td>四项检查全部 <code>ok</code>，13 个 C 库目标文件无堆符号，<code>__aeabi_d*</code> 引用 0 个</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/Makefile" target="_blank"><code>Makefile</code></a></td><td><code>make -s ch10</code></td><td><code>ch10</code> 目标：<code>ref</code>、双精度与 float32 构建、运行、静态检查、计时、<code>report</code>，最后一行 <code>ch10 OK</code></td></tr></tbody></table></section></section>
<section><h2>外推<a href="#外推"><span>#</span></a></h2><p>这一篇我把第 5 篇的 <code>Pid</code> 搬到了 float32（C、C++、Rust 三种语言逐位一致，积分器会停，但停在 <span><span>1.04×10−31.04\times10^{-3}</span><span><span><span></span><span>1.04</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm），写了 Q16.16 的 <code>PidQ</code>（三种语言和 Python 逐位一致，差异 0），把它们放进三个上车的麻烦里：CAN 量化把 <span><span>0.8×10−30.8\times10^{-3}</span><span><span><span></span><span>0.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rpm 的稳态误差变成 0.298 rpm 的锯齿，周期抖动在实测补偿下 IAE 只变了 0.53%，M4F 上 float32 的 <code>Pid</code> 是 556 字节、Q16.16 的是 1332 字节。系列到这里是十篇：一个电机，加上 P、I、D，加上传递函数和频域，加上离散化和工程上的坑，加上整定、串级和多变量，最后是状态反馈和这一篇的嵌入式。</p><p>下一步不是再写一篇，是把它们装到真的 C 板上。我手上这些控制器（第 5 篇的 <code>Pid</code>、第 7 篇的串级、第 8 篇的矩阵增益、第 9 篇的 LQI）都是 <code>no_std</code>、无堆、可以编译到 M4F 的，而 RoboMaster 的云台还缺一样东西：第 7 篇里的角度和陀螺仪噪声是我从<a href="/posts/kalman-filter-04-attitude-imu/">卡尔曼系列的第 4 篇</a>借来的数（陀螺 0.01 rad/s，角度 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rad）。真的上车时，这两个量应该来自那一篇的姿态估计，而不是 <code>data/</code> 里的随机数：Kalman 的姿态输出作为第 7 篇串级的角度反馈，它的角速度估计作为内环的陀螺。我不知道真车上这个组合的相位裕度还有多少：滤波器自己有延迟，第 3 篇的延迟裕度那一套数就是为了这一刻准备的，我下一步想做的就是把滤波器的群延迟算进去，再看内环还剩几毫秒。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-09-lqr/</id>
      <title type="text">状态反馈：LQR、LQI 与平衡步兵</title>
      <published>2026-10-16T08:00:00.000Z</published>
      <updated>2026-10-16T08:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-09-lqr/"/>
      <summary type="text">PID 控制系列第 9 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95，把小矩阵运算、矩阵指数（缩放平方加 12 阶泰勒）、ZOH 离散化、Riccati 迭代和 LQI 增广各写了四遍，再加一个新对象：平衡步兵的平面拉格朗日模型。同一份仿真里我比较了 LQR、LQI 和“位置环套速度环套直立环”的串级 PID（五个增益交给 Nelder–Mead 整定）。拉格朗日模型的能量漂移是 1.06e-13；LQR 在 10 N 的推力下稳态偏离 0.826 m，LQI 能把它拉回 0，但 5 秒内没拉完，阶跃的超调还是 38.8%（LQR 是 2.3%）；整定好的串级把积分项整定成了 1.5e-12，成了一个 LQR 的 0.87 倍缩放版，代价 6.162 对 6.163；LQI 的增益向量落在串级能表达的范围之外（判别式 −0.0618）。四种语言 42 个对比文件最大差异 5.06e-13。</summary>
      <content type="html"><![CDATA[<p>平衡步兵常见的控制结构（据我所知，开源的代码里大多是这样）是：最里面一个“直立环”，输入是车身倾角和倾角速度，输出轮子的力矩；中间一个“速度环”，输出的是倾角的设定值；最外面再套一个位置环。三个 PID，各自整定，叠在一起能站住。这一篇我想回答一个一直没有人（包括我）认真回答过的问题：<strong>这一串 PID，和直接对四个状态做反馈 <span><span>u=−Kxu=-Kx</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span></span></span></span> 有多大区别？</strong> 我的做法是把对象的拉格朗日模型写出来，线性化，用 LQR 算出 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>，再把一串 PID 展开成它的等价 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>，放到同一份仿真里比。结果有两处和我预想的不一样：整定好的串级 PID 几乎就是 LQR（代价 6.162 对 6.163），而 LQI 的增益落在串级能表达的范围之外，但它在阶跃上的表现比两个对手都差得多。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch09</code> 通过，<code>out/ch09/</code> 下 56 个 CSV（14 个文件，每个 Python、C、C++、Rust 各一份：12 个控制算例、1 个能量自检、1 个增益表），<code>figures/ch09/</code> 下 5 张图；以及平衡步兵的拉格朗日推导、线性化、LQR 和 LQI 的推导与实验。
新增模块：Python <code>pidlab.mat</code> 和 <code>pidlab.lqr</code>，C <code>pl/mat.h</code>、<code>pl/lqr.h</code> 和对应的 <code>src/mat.c</code>、<code>src/lqr.c</code>，C++ <code>pidlab/mat.h</code>、<code>pidlab/lqr.h</code>，Rust <code>mat.rs</code>、<code>lqr.rs</code>（小矩阵运算、矩阵指数、ZOH 离散化、Riccati 迭代、LQR 增益、LQI 增广）。新增对象 <code>Wip</code>（四种语言的 <code>plants</code> 里）。位置环、速度环、直立环仍是第 5 篇的 <code>Pid</code>，没有新的 PID 变体。
前置：<a href="/posts/pid-control-05-practical/">第 5 篇</a>的 <code>Pid</code>、<a href="/posts/pid-control-07-cascade/">第 7 篇</a>的串级接线和 <code>hold_integral</code>、<a href="/posts/pid-control-08-mimo/">第 8 篇</a>的矩阵增益 PID。需要的数学：线性化、状态空间、矩阵指数、特征值。</p></div></div>
<section><h2>对象：平衡步兵的拉格朗日模型<a href="#对象平衡步兵的拉格朗日模型"><span>#</span></a></h2><p>先说我是怎么避免写错符号的，因为这一节是整篇里最容易默默写错的地方：我从拉格朗日量把两条运动方程完整推了一遍，再用三个数值检查核对（能量守恒、功率平衡、对中心差分的线性化），检查的结果放在“结果”那一节。</p><p>对象是平面里的两轮倒立摆：两个轮子合起来当一个轮子（质量 <span><span>mw=2m_w=2</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> kg，转动惯量 <span><span>Iw=0.01I_w=0.01</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span> kg·m²，半径 <span><span>r=0.1r=0.1</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span> m），车身质量 <span><span>M=12M=12</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span> kg，质心在轮轴上方 <span><span>l=0.2l=0.2</span><span><span><span></span><span>l</span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> m，绕质心的转动惯量 <span><span>Ib=0.4I_b=0.4</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.4</span></span></span></span> kg·m²，电机力矩上限 10 N·m（这些都是我取的量级，不是实测）。广义坐标取两个：轮子滚过的距离 <span><span>x=rθwx=r\theta_w</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，和车身从竖直方向向前倾的角度 <span><span>φ\varphi</span><span><span><span></span><span>φ</span></span></span></span>（<span><span>φ&gt;0\varphi&gt;0</span><span><span><span></span><span>φ</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 表示质心跑到 <span><span>+x+x</span><span><span><span></span><span>+</span><span>x</span></span></span></span> 一侧）。</p><p>车身质心的位置是 <span><span>(x+lsin⁡φ, lcos⁡φ)(x+l\sin\varphi,\ l\cos\varphi)</span><span><span><span></span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>l</span><span></span><span>sin</span><span></span><span>φ</span><span>,</span><span> </span><span></span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span>)</span></span></span></span>，速度分量 <span><span>x˙+lcos⁡φ φ˙\dot x+l\cos\varphi\,\dot\varphi</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 和 <span><span>−lsin⁡φ φ˙-l\sin\varphi\,\dot\varphi</span><span><span><span></span><span>−</span><span>l</span><span></span><span>sin</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。动能是轮子的平动加转动、车身质心的平动、车身绕质心的转动：</p><span><span><span>T=12(mw+Iwr2+M)x˙2+Mlcos⁡φ x˙φ˙+12 (Ib+Ml2) φ˙2,V=Mglcos⁡φT=\tfrac12\Bigl(m_w+\tfrac{I_w}{r^2}+M\Bigr)\dot x^2+Ml\cos\varphi\,\dot x\dot\varphi+\tfrac12\,(I_b+Ml^2)\,\dot\varphi^2,
\qquad V=Mgl\cos\varphi</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>(</span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>r</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>I</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span><span><span>)</span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>(</span><span><span>I</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span><span><span>l</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span><span></span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>V</span><span></span><span>=</span><span></span></span><span><span></span><span>M</span><span>g</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span></span></span></span></span><p>记 <span><span>a=mw+Iw/r2+Ma=m_w+I_w/r^2+M</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>r</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span></span></span></span>（数值 15 kg）和 <span><span>c=Ib+Ml2c=I_b+Ml^2</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span><span><span>l</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>（0.88 kg·m²）。<span><span>L=T−VL=T-V</span><span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span>T</span><span></span><span>−</span><span></span></span><span><span></span><span>V</span></span></span></span> 对 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 不显含，所以 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 方程的左边只有 <span><span>ddt∂L∂x˙\frac{d}{dt}\frac{\partial L}{\partial\dot x}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span>t</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>∂</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>∂</span><span>L</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>：</p><span><span><span>ax¨+Mlcos⁡φ φ¨−Mlsin⁡φ φ˙2=Qxa\ddot x+Ml\cos\varphi\,\ddot\varphi-Ml\sin\varphi\,\dot\varphi^2=Q_x</span><span><span><span></span><span>a</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>M</span><span>l</span><span></span><span>sin</span><span></span><span>φ</span><span></span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>φ\varphi</span><span><span><span></span><span>φ</span></span></span></span> 方程里 <span><span>x˙φ˙sin⁡φ\dot x\dot\varphi\sin\varphi</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>sin</span><span></span><span>φ</span></span></span></span> 这一项在 <span><span>ddt∂L∂φ˙\frac{d}{dt}\frac{\partial L}{\partial\dot\varphi}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span>t</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>∂</span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>∂</span><span>L</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 和 <span><span>∂L∂φ\frac{\partial L}{\partial\varphi}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>∂</span><span>φ</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>∂</span><span>L</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 里各出现一次，抵消了：</p><span><span><span>Mlcos⁡φ x¨+c φ¨−Mglsin⁡φ=QφMl\cos\varphi\,\ddot x+c\,\ddot\varphi-Mgl\sin\varphi=Q_\varphi</span><span><span><span></span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>c</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>M</span><span>g</span><span>l</span><span></span><span>sin</span><span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>广义力是符号最容易出错的地方，我用虚功算。电机的力矩 <span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 作用在轮子上，同时有一个反作用力矩 <span><span>−τ-\tau</span><span><span><span></span><span>−</span><span>τ</span></span></span></span> 作用在车身上，所以虚功是 <span><span>τ δθw−τ δφ=τrδx−τ δφ\tau\,\delta\theta_w-\tau\,\delta\varphi=\tfrac{\tau}{r}\delta x-\tau\,\delta\varphi</span><span><span><span></span><span>τ</span><span></span><span>δ</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>τ</span><span></span><span>δ</span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>r</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>τ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>δ</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>τ</span><span></span><span>δ</span><span>φ</span></span></span></span>，于是 <span><span>Qx=τ/rQ_x=\tau/r</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span>/</span><span>r</span></span></span></span>，<span><span>Qφ=−τQ_\varphi=-\tau</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>τ</span></span></span></span>。<strong><span><span>τ&gt;0\tau&gt;0</span><span><span><span></span><span>τ</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 的意思是轮子向前滚，车身被向后推</strong>，和“加速时身体后仰”的日常经验一致。再加一个外力：水平方向的力 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span> 作用在车身质心上（后面的 <code>push</code> 算例要用），虚功是 <span><span>F δ(x+lsin⁡φ)=F δx+Flcos⁡φ δφF\,\delta(x+l\sin\varphi)=F\,\delta x+Fl\cos\varphi\,\delta\varphi</span><span><span><span></span><span>F</span><span></span><span>δ</span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>l</span><span></span><span>sin</span><span></span><span>φ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span></span><span>δ</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>F</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span>δ</span><span>φ</span></span></span></span>，所以 <span><span>QxQ_x</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 多一个 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span>，<span><span>QφQ_\varphi</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 多一个 <span><span>Flcos⁡φFl\cos\varphi</span><span><span><span></span><span>F</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span></span></span></span>。完整的模型是：</p><span><span><span>ax¨+Mlcos⁡φ φ¨−Mlsin⁡φ φ˙2=τ/r+FMlcos⁡φ x¨+c φ¨−Mglsin⁡φ=−τ+Flcos⁡φ\begin{aligned}
a\ddot x+Ml\cos\varphi\,\ddot\varphi-Ml\sin\varphi\,\dot\varphi^2&amp;=\tau/r+F\\
Ml\cos\varphi\,\ddot x+c\,\ddot\varphi-Mgl\sin\varphi&amp;=-\tau+Fl\cos\varphi
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span><span>M</span><span>l</span><span></span><span>sin</span><span></span><span>φ</span><span></span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>c</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span><span>M</span><span>g</span><span>l</span><span></span><span>sin</span><span></span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>τ</span><span>/</span><span>r</span><span></span><span>+</span><span></span><span>F</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span>−</span><span>τ</span><span></span><span>+</span><span></span><span>F</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>每次求导数的时候解一个 2×2：<span><span>[aMlcos⁡φMlcos⁡φc]\begin{bmatrix}a&amp;Ml\cos\varphi\\Ml\cos\varphi&amp;c\end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 乘 <span><span>(x¨,φ¨)(\ddot x,\ddot\varphi)</span><span><span><span></span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span>,</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span></span></span></span> 等于右边，行列式 <span><span>ac−(Mlcos⁡φ)2ac-(Ml\cos\varphi)^2</span><span><span><span></span><span>a</span><span>c</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>M</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 在 <span><span>φ=0\varphi=0</span><span><span><span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 是 7.44，永远不会是 0（<span><span>ac−M2l2cos⁡2φ≥ac−M2l2a c - M^2l^2\cos^2\varphi\ge ac-M^2l^2</span><span><span><span></span><span>a</span><span>c</span><span></span><span>−</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span><span>cos</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>φ</span><span></span><span>≥</span><span></span></span><span><span></span><span>a</span><span>c</span><span></span><span>−</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>），直接用克莱姆法则。<code>plants.py</code> 里的 <code>Wip.deriv</code> 就是这几行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>a, c </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>_coefs</span><span>()</span></span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>ml </span><span>=</span><span> </span><span>self</span><span><span>.m </span><span>*</span><span> </span></span><span>self</span><span>.l</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>s, co </span><span>=</span><span> math.</span><span>sin</span><span>(x[</span><span>2</span><span><span>]), math.</span><span>cos</span><span>(x[</span></span><span>2</span><span>])</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>r1 </span><span>=</span><span> u[</span><span>0</span><span><span>] </span><span>/</span><span> </span></span><span>self</span><span><span>.r </span><span>+</span><span> u[</span></span><span>1</span><span><span>] </span><span>+</span><span> ml </span><span>*</span><span> s </span><span>*</span><span> x[</span></span><span>3</span><span><span>] </span><span>*</span><span> x[</span></span><span>3</span><span>]</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>r2 </span><span>=</span><span> </span><span>-</span><span>u[</span><span>0</span><span><span>] </span><span>+</span><span> u[</span></span><span>1</span><span><span>] </span><span>*</span><span> </span></span><span>self</span><span><span>.l </span><span>*</span><span> co </span><span>+</span><span> ml </span><span>*</span><span> </span></span><span>self</span><span><span>.g </span><span>*</span><span> s</span></span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>b </span><span>=</span><span> ml </span><span>*</span><span> co</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>det </span><span>=</span><span> a </span><span>*</span><span> c </span><span>-</span><span> b </span><span>*</span><span> b</span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> [</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>x[</span><span>1</span><span>],</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>(c </span><span>*</span><span> r1 </span><span>-</span><span> b </span><span>*</span><span> r2) </span><span>/</span><span> det,</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>x[</span><span>3</span><span>],</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>(a </span><span>*</span><span> r2 </span><span>-</span><span> b </span><span>*</span><span> r1) </span><span>/</span><span> det,</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>r1</code> 和 <code>r2</code> 是两个方程的右边（把 <span><span>φ˙2\dot\varphi^2</span><span><span><span></span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 项和重力项移了过去），<code>u</code> 是 <span><span>[τ, F][\tau,\ F]</span><span><span><span></span><span>[</span><span>τ</span><span>,</span><span> </span><span></span><span>F</span><span>]</span></span></span></span>，所以外力是对象的第二个输入，和前几篇“扰动就是多出来的输入”的约定一致。同一个文件里还有 <code>energy</code>，是后面自检要用的动能加势能。</p></section>
<section><h2>线性化：A 和 B 是怎么出来的<a href="#线性化a-和-b-是怎么出来的"><span>#</span></a></h2><p>在 <span><span>φ=0\varphi=0</span><span><span><span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 附近取 <span><span>sin⁡φ≈φ\sin\varphi\approx\varphi</span><span><span><span></span><span>sin</span><span></span><span>φ</span><span></span><span>≈</span><span></span></span><span><span></span><span>φ</span></span></span></span>，<span><span>cos⁡φ≈1\cos\varphi\approx1</span><span><span><span></span><span>cos</span><span></span><span>φ</span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span></span></span></span>，丢掉 <span><span>φ˙2\dot\varphi^2</span><span><span><span></span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，令 <span><span>F=0F=0</span><span><span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：</p><span><span><span>[aMlMlc][x¨φ¨]=[τ/rMgl φ−τ]\begin{bmatrix}a&amp;Ml\\Ml&amp;c\end{bmatrix}\begin{bmatrix}\ddot x\\\ddot\varphi\end{bmatrix}=\begin{bmatrix}\tau/r\\Mgl\,\varphi-\tau\end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>M</span><span>l</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>M</span><span>l</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>τ</span><span>/</span><span>r</span></span></span><span><span></span><span><span>M</span><span>g</span><span>l</span><span></span><span>φ</span><span></span><span>−</span><span></span><span>τ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>记 <span><span>Δ=ac−(Ml)2\Delta=ac-(Ml)^2</span><span><span><span></span><span>Δ</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span>c</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>M</span><span>l</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，把 2×2 求逆：</p><span><span><span>x¨=−Ml⋅MglΔ φ+c/r+MlΔ τ,φ¨=a⋅MglΔ φ−a+Ml/rΔ τ\ddot x=\frac{-Ml\cdot Mgl}{\Delta}\,\varphi+\frac{c/r+Ml}{\Delta}\,\tau,\qquad
\ddot\varphi=\frac{a\cdot Mgl}{\Delta}\,\varphi-\frac{a+Ml/r}{\Delta}\,\tau</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>Δ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>−</span><span>M</span><span>l</span><span></span><span>⋅</span><span></span><span>M</span><span>g</span><span>l</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>φ</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>Δ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>c</span><span>/</span><span>r</span><span></span><span>+</span><span></span><span>M</span><span>l</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>τ</span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>Δ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span></span><span>⋅</span><span></span><span>M</span><span>g</span><span>l</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>φ</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>Δ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>a</span><span></span><span>+</span><span></span><span>M</span><span>l</span><span>/</span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>τ</span></span></span></span></span><p>代入 <span><span>Ml=2.4Ml=2.4</span><span><span><span></span><span>M</span><span>l</span><span></span><span>=</span><span></span></span><span><span></span><span>2.4</span></span></span></span>，<span><span>Mgl=23.544Mgl=23.544</span><span><span><span></span><span>M</span><span>g</span><span>l</span><span></span><span>=</span><span></span></span><span><span></span><span>23.544</span></span></span></span>，<span><span>Δ=15×0.88−2.42=7.44\Delta=15\times0.88-2.4^2=7.44</span><span><span><span></span><span>Δ</span><span></span><span>=</span><span></span></span><span><span></span><span>15</span><span></span><span>×</span><span></span></span><span><span></span><span>0.88</span><span></span><span>−</span><span></span></span><span><span></span><span>2.</span><span><span>4</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>7.44</span></span></span></span>，状态取 <span><span>x=(x,x˙,φ,φ˙)\mathbf x=(x,\dot x,\varphi,\dot\varphi)</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>x</span><span>,</span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>,</span><span></span><span>φ</span><span>,</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span></span></span></span>：</p><span><span><span>A=[010000−7.595000010047.4680],B=[01.5050−5.242]A=\begin{bmatrix}0&amp;1&amp;0&amp;0\\0&amp;0&amp;-7.595&amp;0\\0&amp;0&amp;0&amp;1\\0&amp;0&amp;47.468&amp;0\end{bmatrix},\qquad
B=\begin{bmatrix}0\\1.505\\0\\-5.242\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>7.595</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>47.468</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>,</span><span></span><span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1.505</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>5.242</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>代码把这四个分式原样写出来（只给 <span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 这一列，外力 <span><span>FF</span><span><span><span></span><span>F</span></span></span></span> 只出现在非线性模型里）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>linearize</span><span>(</span><span>self</span><span>) -&gt; tuple[list[list[</span><span>float</span><span>]], list[list[</span><span>float</span><span>]]]:</span></div></div><div><div><div>2</div></div><div><span>        </span><span>"""A, B of x' = A x + B tau at phi = 0 (hand derivation, ch09)."""</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>a, c </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>_coefs</span><span>()</span></span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>ml </span><span>=</span><span> </span><span>self</span><span><span>.m </span><span>*</span><span> </span></span><span>self</span><span>.l</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>mgl </span><span>=</span><span> ml </span><span>*</span><span> </span><span>self</span><span>.g</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>det </span><span>=</span><span> a </span><span>*</span><span> c </span><span>-</span><span> ml </span><span>*</span><span> ml</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>amat </span><span>=</span><span> [</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>],</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span><span>ml </span><span>*</span><span> mgl </span><span>/</span><span> det, </span></span><span>0.0</span><span>],</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>],</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, a </span><span>*</span><span> mgl </span><span>/</span><span> det, </span></span><span>0.0</span><span>],</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>bmat </span><span>=</span><span> [</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>],</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>[(c </span><span>/</span><span> </span><span>self</span><span><span>.r </span><span>+</span><span> ml) </span><span>/</span><span> det],</span></span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>[</span><span>0.0</span><span>],</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>[</span><span>-</span><span>(a </span><span>+</span><span> ml </span><span>/</span><span> </span><span>self</span><span><span>.r) </span><span>/</span><span> det],</span></span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>19</div></div><div><span>        </span><span>return</span><span> amat, bmat</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个观察。第一，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 的特征值是 <span><span>0,0,±47.468=±6.890,0,\pm\sqrt{47.468}=\pm6.89</span><span><span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>±</span><span><span><span><span><span><span></span><span><span>47.468</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>±</span><span>6.89</span></span></span></span> rad/s：一个不稳定极点，倾倒的时间常数 <span><span>1/6.89=0.1451/6.89=0.145</span><span><span><span></span><span>1/6.89</span><span></span><span>=</span><span></span></span><span><span></span><span>0.145</span></span></span></span> s，所以控制器必须比这快得多（1 kHz 对这个来说足够）。第二，<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第二项和第四项<strong>符号相反</strong>：<span><span>τ&gt;0\tau&gt;0</span><span><span><span></span><span>τ</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 让轮子向前加速，同时把车身向后推。要让车向前走，一开始得先向后“退”，让车身前倾，再追上去。这是一个非最小相位的系统，阶跃响应里会有一个先反向的小凹坑，后面的图里能看见，凹坑有多深我后面给数。</p><p>我不信手推的结果，所以 <code>report</code> 把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 和 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 和 <code>Wip.deriv</code> 的中心差分（步长 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span>）比了一次：最大误差 <span><span>4.47×10−114.47\times10^{-11}</span><span><span><span></span><span>4.47</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>11</span></span></span></span></span></span></span></span></span></span></span></span>，是差分本身的截断误差量级。</p></section>
<section><h2>离散化：矩阵指数和 ZOH<a href="#离散化矩阵指数和-zoh"><span>#</span></a></h2><p>控制器每 1 ms 跑一次，输入保持不变（第 4 篇的 ZOH），对象 <span><span>x˙=Ax+Bτ\dot x=Ax+B\tau</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>τ</span></span></span></span> 的精确离散化是</p><span><span><span>xk+1=Adxk+Bdτk,Ad=eAts,Bd=∫0tseAσ dσ Bx_{k+1}=A_dx_k+B_d\tau_k,\qquad A_d=e^{At_s},\quad B_d=\int_0^{t_s}e^{A\sigma}\,d\sigma\,B</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>σ</span></span></span></span></span></span></span></span></span><span></span><span>d</span><span>σ</span><span></span><span>B</span></span></span></span></span><p>两个量可以用一个矩阵指数同时得到：</p><span><span><span>exp⁡([AB00]ts)=[AdBd0I]\exp\Bigl(\begin{bmatrix}A&amp;B\\0&amp;0\end{bmatrix}t_s\Bigr)=\begin{bmatrix}A_d&amp;B_d\\0&amp;I\end{bmatrix}</span><span><span><span></span><span>exp</span><span><span>(</span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>A</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>B</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>验证很短：右边那个矩阵的幂级数里，第二列块是 <span><span>∑k≥1tskk!Ak−1B=∫0tseAσdσ B\sum_{k\ge1}\frac{t_s^k}{k!}A^{k-1}B=\int_0^{t_s}e^{A\sigma}d\sigma\,B</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>k</span><span>≥</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>!</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>A</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>B</span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>σ</span></span></span></span></span></span></span></span></span><span>d</span><span>σ</span><span></span><span>B</span></span></span></span>。</p><p>矩阵指数我自己写，不调库：<strong>缩放平方</strong>。先把矩阵除以 <span><span>2s2^s</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span></span></span></span></span></span></span></span>，直到无穷范数不超过 0.5，对缩小后的矩阵做 12 阶泰勒（截断误差大约是 <span><span>0.513/13!≈2×10−140.5^{13}/13!\approx2\times10^{-14}</span><span><span><span></span><span>0.</span><span><span>5</span><span><span><span><span><span><span></span><span><span><span>13</span></span></span></span></span></span></span></span></span><span>/13</span><span>!</span><span></span><span>≈</span><span></span></span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>），再平方 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 次。Python 版和 <code>scipy.linalg.expm</code> 在 4 个量级（<span><span>0.10.1</span><span><span><span></span><span>0.1</span></span></span></span> 到 <span><span>3030</span><span><span><span></span><span>30</span></span></span></span> 倍的随机 5×5 矩阵）上的最大相对误差是 <span><span>2.51×10−132.51\times10^{-13}</span><span><span><span></span><span>2.51</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，在我设的 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 容差之内。</p><p><code>pidlab/mat.py</code> 是 LQR 要用的那几个矩阵运算（<code>zeros</code>、<code>identity</code>、<code>mul</code>、<code>add</code>、<code>sub</code>、<code>scale</code>、<code>transpose</code>、<code>max_abs</code>）。矩阵是行列表组成的列表（<code>list[list[float]]</code>），没有用 numpy，因为这是原生版本要逐位对上的参考实现，numpy 和 scipy 只出现在 <code>report</code> 里做核对。里面最重要的是矩阵乘，一个朴素的三重循环，累加顺序后面三种语言都照抄：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>mul</span><span>(</span><span>a</span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> Matrix</span><span>) -&gt; Matrix:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""a (n x k) times b (k x m)."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>n, k, m </span><span>=</span><span> </span><span>len</span><span>(a), </span><span>len</span><span>(b), </span><span>len</span><span>(b[</span><span>0</span><span>])</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>out </span><span>=</span><span> </span><span>zeros</span><span>(n, m)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>s </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>8</div></div><div><span>            </span><span>for</span><span> p </span><span>in</span><span> </span><span>range</span><span>(k):</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>s </span><span>+=</span><span> a[i][p] </span><span>*</span><span> b[p][j]</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>out[i][j] </span><span>=</span><span> s</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> out</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pidlab/lqr.py</code> 里的 <code>expm</code> 是上面那段话的直译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>expm</span><span>(</span><span>a</span><span>:</span><span> Matrix</span><span>) -&gt; Matrix:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""exp(a): scale until ||a||_inf &lt;= 0.5, Taylor to order 12, square."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(a)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>nrm </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> row </span><span>in</span><span> a:</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>7</div></div><div><span>        </span><span>for</span><span> x </span><span>in</span><span> row:</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>s </span><span>+=</span><span> </span><span>abs</span><span>(x)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>nrm </span><span>=</span><span> </span><span>max</span><span>(nrm, s)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>squarings </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>11</div></div><div><span>    </span><span>while</span><span><span> nrm </span><span>&gt;</span><span> </span></span><span>0.5</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>nrm </span><span>*=</span><span> </span><span>0.5</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>squarings </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> </span><span>scale</span><span>(a, </span><span>0.5</span><span><span>**</span><span>squarings)</span></span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>term </span><span>=</span><span> </span><span>identity</span><span>(n)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>out </span><span>=</span><span> </span><span>identity</span><span>(n)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span><span>, </span><span>TAYLOR_ORDER</span><span> </span><span>+</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>term </span><span>=</span><span> </span><span>scale</span><span>(</span><span>mul</span><span>(term, h), </span><span>1.0</span><span><span> </span><span>/</span><span> k)</span></span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>out </span><span>=</span><span> </span><span>add</span><span>(out, term)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(squarings):</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>out </span><span>=</span><span> </span><span>mul</span><span>(out, out)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span> out</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>求范数用显式的循环累加，不用 <code>sum()</code>：Python 3.12 起 <code>sum()</code> 对浮点数用补偿求和，和 C 的朴素累加在最后一位上会不同，虽然在这里只影响“要缩放几次”这个判断，我还是把它对齐了。有了 <code>expm</code>，ZOH 就是把 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>、<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 拼成 <span><span>(n+m)×(n+m)(n+m)\times(n+m)</span><span><span><span></span><span>(</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>m</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>m</span><span>)</span></span></span></span> 的块矩阵，乘 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，取指数，再把左上角和右上角切出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>c2d_zoh</span><span>(</span><span>a</span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>ts</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; tuple[Matrix, Matrix]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Zero-order-hold discretization via exp([[A, B], [0, 0]] ts)."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>n, m </span><span>=</span><span> </span><span>len</span><span>(a), </span><span>len</span><span>(b[</span><span>0</span><span>])</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>big </span><span>=</span><span> </span><span>zeros</span><span>(n </span><span>+</span><span> m, n </span><span>+</span><span> m)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>big[i][j] </span><span>=</span><span> a[i][j] </span><span>*</span><span> ts</span></div></div><div><div><div>8</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(m):</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>big[i][n </span><span>+</span><span> j] </span><span>=</span><span> b[i][j] </span><span>*</span><span> ts</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> </span><span>expm</span><span>(big)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>ad </span><span>=</span><span> [row[:n] </span><span>for</span><span> row </span><span>in</span><span> e[:n]]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>bd </span><span>=</span><span> [row[n:] </span><span>for</span><span> row </span><span>in</span><span> e[:n]]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> ad, bd</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>LQR：代价函数、Riccati 方程和 Bryson 权重<a href="#lqr代价函数riccati-方程和-bryson-权重"><span>#</span></a></h2><p>LQR 要的是在无穷时域里最小化</p><span><span><span>J=∑k=0∞(xkTQxk+R uk2)J=\sum_{k=0}^{\infty}\bigl(x_k^TQx_k+R\,u_k^2\bigr)</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>=</span><span>0</span></span></span></span><span><span></span><span><span>∑</span></span></span><span><span></span><span><span><span>∞</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span>(</span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Q</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span></span></span></span></span></span><p>的状态反馈 <span><span>uk=−Kxku_k=-Kx_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。Kalman 在 <a href="https://www.boletin.math.org.mx/pdf/2/5/BSMM%282%29.5.102-119.pdf" target="_blank">1960 年的 “Contributions to the theory of optimal control”</a> 里讨论的就是这一类问题（摘要的原话是设计线性控制系统来最小化某个二次函数的积分）；我这里用的是离散时间的形式，推导我没有去对原文，下面的递推式是标准结果，由 <code>scipy.linalg.solve_discrete_are</code> 核对。</p><p>最优的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 由离散代数 Riccati 方程（DARE）的解 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 给出：</p><span><span><span>K=(R+BdTPBd)−1BdTPAd,P=Q+AdTPAd−AdTPBd (R+BdTPBd)−1BdTPAdK=(R+B_d^TPB_d)^{-1}B_d^TPA_d,\qquad
P=Q+A_d^TPA_d-A_d^TPB_d\,(R+B_d^TPB_d)^{-1}B_d^TPA_d</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>R</span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span><span></span><span>+</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>(</span><span>R</span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>单输入的时候 <span><span>R+BdTPBdR+B_d^TPB_d</span><span><span><span></span><span>R</span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是标量，求逆就是一次除法，所以增益函数很短：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>lqr_gain</span><span>(</span><span>ad</span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>bd</span></span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>p</span></span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>r</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; Matrix:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""K = (R + B'PB)^-1 B'PA as a 1 x n matrix."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>pb </span><span>=</span><span> </span><span>mul</span><span>(p, bd)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> r </span><span>+</span><span> </span><span>mul</span><span>(</span><span>transpose</span><span>(bd), pb)[</span><span>0</span><span>][</span><span>0</span><span>]</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span><span> </span><span>scale</span><span>(</span><span>mul</span><span>(</span><span>transpose</span><span>(pb), ad), </span></span><span>1.0</span><span><span> </span><span>/</span><span> s)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>权重怎么取。</strong> 我用的是 Bryson 法则：每个状态（和输入）的权重取“允许的最大值”的平方的倒数，见 <a href="https://courses.ece.ucsb.edu/ECE147/147B_W13Byl/lecture/Lecture18_annotated.pdf" target="_blank">UCSB 的一份讲义</a>（我没有去翻 Bryson 和 Ho 的原书）。允许值我取：位置 0.5 m、速度 1 m/s、倾角 0.1 rad、倾角速度 1 rad/s、力矩 10 N·m，于是</p><span><span><span>Q=diag⁡(4, 1, 100, 1),R=1102=0.01Q=\operatorname{diag}(4,\ 1,\ 100,\ 1),\qquad R=\frac1{10^2}=0.01</span><span><span><span></span><span>Q</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>4</span><span>,</span><span> </span><span></span><span>1</span><span>,</span><span> </span><span></span><span>100</span><span>,</span><span> </span><span></span><span>1</span><span>)</span><span>,</span><span></span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span></span><p><strong>怎么解 DARE：一个我踩了的坑。</strong> 最直接的做法是从 <span><span>P=QP=Q</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span></span></span></span> 出发反复套上面那个式子，直到不再变化。我先这么写了，结果它不收敛：最大变化量 <span><span>max⁡∣ΔP∣\max|\Delta P|</span><span><span><span></span><span>max</span><span></span><span>∣Δ</span><span>P</span><span>∣</span></span></span></span> 在第 4454 步降到最低的 <span><span>4.2×10−24.2\times10^{-2}</span><span><span><span></span><span>4.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>2</span></span></span></span></span></span></span></span></span></span></span></span>，然后开始变大，到第 20000 步是 <span><span>4.9×10444.9\times10^{44}</span><span><span><span></span><span>4.9</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>44</span></span></span></span></span></span></span></span></span></span></span></span>（<code>report</code> 里把这个原始版本留着，单独跑一遍打印出来）。原因我猜是舍入：<span><span>AdTPAdA_d^TPA_d</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>AdTPBd(⋅)−1BdTPAdA_d^TPB_d(\cdot)^{-1}B_d^TPA_d</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>⋅</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是两个很大、很接近的量相减，而对象有一个不稳定模态，把相减后剩下的舍入误差一步步放大。</p><p>修法是把递推换成一个等价但全是“正半定的项相加”的形式：先用当前的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 算出 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>，再</p><span><span><span>P←Q+KTRK+(Ad−BdK)TP (Ad−BdK)P\leftarrow Q+K^TRK+(A_d-B_dK)^TP\,(A_d-B_dK)</span><span><span><span></span><span>P</span><span></span><span>←</span><span></span></span><span><span></span><span>Q</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>R</span><span>K</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>K</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>P</span><span></span><span>(</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>K</span><span>)</span></span></span></span></span><p>等价性两行就能看出来：展开右边得 <span><span>Q+AdTPAd−2KTBdTPAd+KT(R+BdTPBd)KQ+A_d^TPA_d-2K^TB_d^TPA_d+K^T(R+B_d^TPB_d)K</span><span><span><span></span><span>Q</span><span></span><span>+</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>R</span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>K</span></span></span></span>，而 <span><span>KT(R+BdTPBd)K=KTBdTPAdK^T(R+B_d^TPB_d)K=K^TB_d^TPA_d</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>R</span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（把 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的定义代进去），合并后正是 DARE 的右边。收敛判据是 <span><span>max⁡∣ΔP∣&lt;10−12(1+max⁡∣P∣)\max|\Delta P|&lt;10^{-12}(1+\max|P|)</span><span><span><span></span><span>max</span><span></span><span>∣Δ</span><span>P</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>max</span><span></span><span>∣</span><span>P</span><span>∣</span><span>)</span></span></span></span>，上限 200000 步，超过返回错误：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>dare_iterate</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>ad</span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>bd</span></span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>q</span></span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>r</span></span><span>:</span><span> </span><span>float</span></div></div><div><div><div>3</div></div><div><span>) -&gt; tuple[Matrix, </span><span>int</span><span>]:</span></div></div><div><div><div>4</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> q</span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> it </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span><span>, </span><span>MAX_ITER</span><span> </span><span>+</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>k </span><span>=</span><span> </span><span>lqr_gain</span><span>(ad, bd, p, r)</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>acl </span><span>=</span><span> </span><span>sub</span><span>(ad, </span><span>mul</span><span>(bd, k))</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>kt </span><span>=</span><span> </span><span>transpose</span><span>(k)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>pn </span><span>=</span><span> </span><span>add</span><span>(q, </span><span>scale</span><span>(</span><span>mul</span><span>(kt, k), r))</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>pn </span><span>=</span><span> </span><span>add</span><span>(pn, </span><span>mul</span><span>(</span><span>transpose</span><span>(acl), </span><span>mul</span><span>(p, acl)))</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>done </span><span>=</span><span> </span><span>max_abs</span><span>(</span><span>sub</span><span>(pn, p)) </span><span>&lt;</span><span> </span><span>1e-12</span><span><span> </span><span>*</span><span> (</span></span><span>1.0</span><span><span> </span><span>+</span><span> </span><span>max_abs</span><span>(pn))</span></span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> pn</span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span> done:</span></div></div><div><div><div>15</div></div><div><span>            </span><span>return</span><span> p, it</span></div></div><div><div><div>16</div></div><div><span>    </span><span>raise</span><span> </span><span>RuntimeError</span><span>(</span><span>"DARE iteration did not converge"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意 <code>K</code> 在每一步都重新算，这就是上面数学里的稳定形式。1 kHz 下 LQR 要 13067 步，LQI 要 15178 步（慢的原因是最慢的闭环极点在 <span><span>1−0.000841-0.00084</span><span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>0.00084</span></span></span></span>，也就是 1.2 s 的时间常数，对 1 ms 的步长来说要迭代上万次）。</p><p>这个判据有一个代价：停下来的时候，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 离真解还差大约 <span><span>ΔP/(1−ρ)\Delta P/(1-\rho)</span><span><span><span></span><span>Δ</span><span>P</span><span>/</span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>ρ</span><span>)</span></span></span></span>，<span><span>ρ\rho</span><span><span><span></span><span>ρ</span></span></span></span> 是收敛率，接近 1。实测 Python 的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 和 <code>solve_discrete_are</code> 的相对差（除以 <span><span>max⁡∣P∣\max|P|</span><span><span><span></span><span>max</span><span></span><span>∣</span><span>P</span><span>∣</span></span></span></span>）是 LQR <span><span>6.53×10−106.53\times10^{-10}</span><span><span><span></span><span>6.53</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>、LQI <span><span>4.53×10−104.53\times10^{-10}</span><span><span><span></span><span>4.53</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 是 <span><span>3.07×10−103.07\times10^{-10}</span><span><span><span></span><span>3.07</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 和 <span><span>1.99×10−101.99\times10^{-10}</span><span><span><span></span><span>1.99</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>。都在 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 的容差内，但只有一倍多的余量，这是停止判据决定的，不是 bug。</p></section>
<section><h2>LQI：给位置误差加一个积分状态<a href="#lqi给位置误差加一个积分状态"><span>#</span></a></h2><p>LQR 的 <span><span>u=−Kxu=-Kx</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span></span></span></span> 没有积分：外力持续作用的时候，平衡点会偏。第 1 篇说过，积分项迫使任何稳态的误差为零，LQI 把同一件事放进状态里：增广一个状态 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>，让它积分位置误差，</p><span><span><span>zk+1=zk+ts (xref−xk),[xz]k+1=[Ad0−ts c1][xz]k+[Bd0]uk+[0ts]xrefz_{k+1}=z_k+t_s\,(x_{ref}-x_k),\qquad
\begin{bmatrix}x\\z\end{bmatrix}_{k+1}=\begin{bmatrix}A_d&amp;0\\-t_s\,c&amp;1\end{bmatrix}\begin{bmatrix}x\\z\end{bmatrix}_k+\begin{bmatrix}B_d\\0\end{bmatrix}u_k+\begin{bmatrix}0\\t_s\end{bmatrix}x_{ref}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>c=(1,0,0,0)c=(1,0,0,0)</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span> 取出位置。对这个五阶系统用同一个 DARE 求解，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 多一项 <span><span>qz=4q_z=4</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>，得到 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的五个分量，最后一个是 <span><span>kzk_z</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。增广本身只有几行，<code>c</code> 是参数，换成别的输出行也能用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>lqi_augment</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>ad</span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>bd</span></span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>c</span></span><span>:</span><span> Matrix</span><span>,</span><span><span> </span><span>ts</span></span><span>:</span><span> </span><span>float</span></div></div><div><div><div>3</div></div><div><span>) -&gt; tuple[Matrix, Matrix]:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"""Append z+ = z + ts (x_ref - c x) to the state. c is 1 x n."""</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(ad)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>aa </span><span>=</span><span> </span><span>zeros</span><span>(n </span><span>+</span><span> </span><span>1</span><span><span>, n </span><span>+</span><span> </span></span><span>1</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>ba </span><span>=</span><span> </span><span>zeros</span><span>(n </span><span>+</span><span> </span><span>1</span><span>, </span><span>1</span><span>)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>9</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>aa[i][j] </span><span>=</span><span> ad[i][j]</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>ba[i][</span><span>0</span><span><span>] </span><span>=</span><span> bd[i][</span></span><span>0</span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>aa[n][i] </span><span>=</span><span> </span><span>-</span><span>ts </span><span>*</span><span> c[</span><span>0</span><span>][i]</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>aa[n][n] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span> aa, ba</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把参考也放进了比例项：<span><span>u=−K(x−xrefe0)−kzzu=-K(x-x_{ref}e_0)-k_zz</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span></span></span></span>，等价于把状态平移到参考点（平衡步兵在任何位置都是平衡点，平移不改变动力学）。力矩饱和时冻结 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>（和第 5 篇的 <code>CLAMP</code> 同一个思路）。设计的入口在驱动 <code>ch09_lqr.py</code> 的 <code>design()</code> 里，把上面几个函数串起来：线性化，离散化，LQR 的 4 阶 DARE，LQI 的 5 阶 DARE：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>design</span><span>() -&gt; </span><span>dict</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Discretize the linearization and solve LQR and LQI."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>a, b </span><span>=</span><span> </span><span>Wip</span><span>().</span><span>linearize</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>ad, bd </span><span>=</span><span> </span><span>c2d_zoh</span><span>(a, b, </span><span>TS</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>p4, it4 </span><span>=</span><span> </span><span>dare_iterate</span><span>(ad, bd, </span><span>q_matrix</span><span>(</span><span>4</span><span>), R)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>k4 </span><span>=</span><span> </span><span>lqr_gain</span><span>(ad, bd, p4, R)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>aa, ba </span><span>=</span><span> </span><span>lqi_augment</span><span>(ad, bd, [[</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span><span>]], </span><span>TS</span><span>)</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>p5, it5 </span><span>=</span><span> </span><span>dare_iterate</span><span>(aa, ba, </span><span>q_matrix</span><span>(</span><span>5</span><span>), R)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>k5 </span><span>=</span><span> </span><span>lqr_gain</span><span>(aa, ba, p5, R)</span></div></div><div><div><div>10</div></div><div><span>    </span><span># ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>串级 PID 是一个受限的状态反馈<a href="#串级-pid-是一个受限的状态反馈"><span>#</span></a></h2><p>现在回到开头的问题。串级的三个环，用第 5 篇的 <code>Pid</code> 搭：</p><div><div><div><div><span></span></div><div><span><p>v_ref</p></span></div><div><span><p>φ_ref</p></span></div><div><span><p>τ</p></span></div><div><span><p>x</p></span></div><div><span><p>ẋ</p></span></div><div><span><p>φ, φ̇</p></span></div><div><span><p>x_ref</p></span></div><div><span><p>位置环 P：kx</p></span></div><div><span><p>速度环 PI：kv, ki</p></span></div><div><span><p>直立环 PD：P, D</p></span></div><div><span><p>平衡步兵</p></span></div>
</div><div><div><span></span></div><div><span><p>v_ref</p></span></div><div><span><p>φ_ref</p></span></div><div><span><p>τ</p></span></div><div><span><p>x</p></span></div><div><span><p>ẋ</p></span></div><div><span><p>φ, φ̇</p></span></div><div><span><p>x_ref</p></span></div><div><span><p>位置环 P：kx</p></span></div><div><span><p>速度环 PI：kv, ki</p></span></div><div><span><p>直立环 PD：P, D</p></span></div><div><span><p>平衡步兵</p></span></div>
</div></div></div><p>直立环的符号要说一下。从上面线性化的结果看，倾角 <span><span>φ&gt;0\varphi&gt;0</span><span><span><span></span><span>φ</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>（质心在前）的时候要让轮子向前滚（<span><span>τ&gt;0\tau&gt;0</span><span><span><span></span><span>τ</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>）去追质心，也就是 <span><span>τ=+Pφ+Dφ˙\tau=+P\varphi+D\dot\varphi</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>+</span><span>P</span><span>φ</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，<span><span>P,D&gt;0P,D&gt;0</span><span><span><span></span><span>P</span><span>,</span><span></span><span>D</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>。第 5 篇的 <code>Pid</code> 算的是 <span><span>kp(r−y)k_p(r-y)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>)</span></span></span></span>，所以直立环的 <code>kp</code>、<code>kd</code> 取<strong>负数</strong>：<span><span>−P-P</span><span><span><span></span><span>−</span><span>P</span></span></span></span> 和 <span><span>−D-D</span><span><span><span></span><span>−</span><span>D</span></span></span></span>（<code>Pid</code> 允许负增益，第 8 篇 Wood–Berry 的回路 2 就用过）。D 项作用在测量上（<code>c = 0</code>），所以 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 项是 <span><span>−D⋅ddt(−φ)=+Dφ˙-D\cdot\frac{d}{dt}(-\varphi)=+D\dot\varphi</span><span><span><span></span><span>−</span><span>D</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>d</span><span>t</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>−</span><span>φ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>+</span><span>D</span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。速度环输出倾角设定值：想前进更快，就把车身往前倾，<span><span>φref=kv(vref−x˙)+w\varphi_{ref}=k_v(v_{ref}-\dot x)+w</span><span><span><span></span><span><span>φ</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span></span></span></span>，<span><span>w˙=ki(vref−x˙)\dot w=k_i(v_{ref}-\dot x)</span><span><span><span></span><span><span><span><span><span><span></span><span>w</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>)</span></span></span></span>，位置环 <span><span>vref=kx(xref−x)v_{ref}=k_x(x_{ref}-x)</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>x</span><span>)</span></span></span></span>。接线和第 7 篇一样：外环的输出是内环的设定值，速度环的 <code>hold_integral</code> 是直立环<strong>上一拍</strong>的 <code>saturated</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>Cascade</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Position P -&gt; velocity PI -&gt; pitch PD, all from the ch05 Pid."""</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>g</span></span><span>:</span><span> </span><span>dict</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>lim </span><span>=</span><span> </span><span>Wip</span><span>().tau_max</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.pos </span><span>=</span><span> </span><span>Pid</span><span>(</span><span>PidConfig</span><span>(g[</span></span><span>"kx"</span><span>], </span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>TS</span><span>))</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.vel </span><span>=</span><span> </span><span>Pid</span><span>(</span><span>PidConfig</span><span>(g[</span></span><span>"kv"</span><span>], g[</span><span>"ki"</span><span>], </span><span>0.0</span><span><span>, </span><span>TS</span><span>))</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span># u = P phi + D phid: the pitch loop has negative gains</span></div></div><div><div><div>9</div></div><div><span>        </span><span>self</span><span><span>.pit </span><span>=</span><span> </span><span>Pid</span><span>(</span></span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>PidConfig</span><span>(</span><span>-</span><span>g[</span><span>"p"</span><span>], </span><span>0.0</span><span><span>, </span><span>-</span><span>g[</span></span><span>"d"</span><span><span>], </span><span>TS</span><span>, </span></span><span>u_min</span><span><span>=-</span><span>lim, </span></span><span>u_max</span><span><span>=</span><span>lim)</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> list[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>xref</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>vref </span><span>=</span><span> </span><span>self</span><span><span>.pos.</span><span>update</span><span>(xref, x[</span></span><span>0</span><span>])</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>phiref </span><span>=</span><span> </span><span>self</span><span><span>.vel.</span><span>update</span><span>(vref, x[</span></span><span>1</span><span>], </span><span>0.0</span><span>, </span><span>self</span><span>.pit.saturated)</span></div></div><div><div><div>16</div></div><div><span>        </span><span>return</span><span> </span><span>self</span><span><span>.pit.</span><span>update</span><span>(phiref, x[</span></span><span>2</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>把三层展开，<span><span>τ=Pφ+Dφ˙−Pφref\tau=P\varphi+D\dot\varphi-P\varphi_{ref}</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span>φ</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span><span>φ</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>τ=Pφ+Dφ˙−Pkv(kx(xref−x)−x˙)−Pw\tau=P\varphi+D\dot\varphi-Pk_v\bigl(k_x(x_{ref}-x)-\dot x\bigr)-Pw</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span>φ</span><span></span><span>+</span><span></span></span><span><span></span><span>D</span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>(</span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span><span>)</span></span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span>w</span></span></span></span></span><p>积分 <span><span>w=ki∫(kx(xref−x)−x˙) dt=ki(kxz−(x−x0))w=k_i\int(k_x(x_{ref}-x)-\dot x)\,dt=k_i\bigl(k_xz-(x-x_0)\bigr)</span><span><span><span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>)</span><span></span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>(</span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>)</span></span></span></span></span>（<span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 就是上一节 LQI 的 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>）。整理成 <span><span>τ=−K [x−xref, x˙, φ, φ˙, z]T\tau=-K\,[x-x_{ref},\ \dot x,\ \varphi,\ \dot\varphi,\ z]^T</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span></span><span>[</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span>φ</span><span>,</span><span> </span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>,</span><span> </span><span></span><span>z</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>Kx=−P(kvkx+ki),Kx˙=−Pkv,Kφ=−P,Kφ˙=−D,Kz=PkikxK_x=-P(k_vk_x+k_i),\quad K_{\dot x}=-Pk_v,\quad K_\varphi=-P,\quad K_{\dot\varphi}=-D,\quad K_z=Pk_ik_x</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>P</span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>P</span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>P</span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>D</span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>（差一个只取决于 <span><span>xref−x0x_{ref}-x_0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的常值力矩，tilt 和 push 里它是 0，阶跃里它等价于积分器的初值不同。）这几个等式在代码里就是 <code>cascade_equivalent</code>，五个增益换成 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的五个分量：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>cascade_equivalent</span><span>(</span><span>g</span><span>:</span><span> </span><span>dict</span><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>p, d </span><span>=</span><span> g[</span><span>"p"</span><span>], g[</span><span>"d"</span><span>]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span> [</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>-</span><span>p </span><span>*</span><span> (g[</span><span>"kv"</span><span><span>] </span><span>*</span><span> g[</span></span><span>"kx"</span><span><span>] </span><span>+</span><span> g[</span></span><span>"ki"</span><span>]),</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>-</span><span>p </span><span>*</span><span> g[</span><span>"kv"</span><span>],</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>-</span><span>p,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>-</span><span>d,</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>p </span><span>*</span><span> g[</span><span>"ki"</span><span><span>] </span><span>*</span><span> g[</span></span><span>"kx"</span><span>],</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>一串 PID 就是一个状态反馈，五个增益对应 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的五个分量。</strong> 这是“PID 是 LQI 的一个受限子集”这句话的准确含义。受限在哪？五个参数 <span><span>(P,D,kv,kx,ki)(P,D,k_v,k_x,k_i)</span><span><span><span></span><span>(</span><span>P</span><span>,</span><span></span><span>D</span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 对五个分量，看起来够，但中间有一个二次方程：已知一个目标 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>，<span><span>P=−KφP=-K_\varphi</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>K</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>D=−Kφ˙D=-K_{\dot\varphi}</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>K</span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>kv=Kx˙/Kφk_v=K_{\dot x}/K_\varphi</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 直接得到；剩下 <span><span>KxK_x</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>KzK_z</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 要求</p><span><span><span>kvkx+ki=α≡KxKφ,kikx=β≡−KzKφ⟹kvkx2−αkx+β=0k_vk_x+k_i=\alpha\equiv\frac{K_x}{K_\varphi},\qquad k_ik_x=\beta\equiv-\frac{K_z}{K_\varphi}
\quad\Longrightarrow\quad k_vk_x^2-\alpha k_x+\beta=0</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>α</span><span></span><span>≡</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>β</span><span></span><span>≡</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>α</span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>β</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p><strong>没有实根的时候，这个 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 就是任何一组串级增益都够不着的。</strong> 判别式 <span><span>α2−4kvβ\alpha^2-4k_v\beta</span><span><span><span></span><span><span>α</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>4</span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>β</span></span></span></span> 是 LQI 的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 能不能被串级复现的判据。LQI 的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 是 <span><span>(−46.34, −45.82, −150.39, −25.54, 19.34)(-46.34,\ -45.82,\ -150.39,\ -25.54,\ 19.34)</span><span><span><span></span><span>(</span><span>−</span><span>46.34</span><span>,</span><span> </span><span></span><span>−</span><span>45.82</span><span>,</span><span> </span><span></span><span>−</span><span>150.39</span><span>,</span><span> </span><span></span><span>−</span><span>25.54</span><span>,</span><span> </span><span></span><span>19.34</span><span>)</span></span></span></span>，算出来 <span><span>α=0.3082\alpha=0.3082</span><span><span><span></span><span>α</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3082</span></span></span></span>，<span><span>β=0.1286\beta=0.1286</span><span><span><span></span><span>β</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1286</span></span></span></span>，<span><span>kv=0.3047k_v=0.3047</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3047</span></span></span></span>，判别式是 <span><span>−0.0618&lt;0-0.0618&lt;0</span><span><span><span></span><span>−</span><span>0.0618</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span>。没有实根。离它最近的串级取判别式的顶点，<span><span>kx=α/(2kv)=0.5057k_x=\alpha/(2k_v)=0.5057</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>α</span><span>/</span><span>(</span><span>2</span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.5057</span></span></span></span>，<span><span>ki=α−kvkx=0.1541k_i=\alpha-k_vk_x=0.1541</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>α</span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1541</span></span></span></span>，这样前四个分量和 LQI 完全一样，<span><span>KzK_z</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 只有 11.72（LQI 是 19.34）。<code>nearest_cascade</code> 就是在做这个判断：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>nearest_cascade</span><span>(</span><span>k</span><span>:</span><span> list[</span><span>float</span><span>]</span><span>) -&gt; </span><span>dict</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>p, d </span><span>=</span><span> </span><span>-</span><span>k[</span><span>2</span><span><span>], </span><span>-</span><span>k[</span></span><span>3</span><span>]</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>kv </span><span>=</span><span> k[</span><span>1</span><span><span>] </span><span>/</span><span> k[</span></span><span>2</span><span>]</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>alpha </span><span>=</span><span> k[</span><span>0</span><span><span>] </span><span>/</span><span> k[</span></span><span>2</span><span>]</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>beta </span><span>=</span><span> </span><span>-</span><span>k[</span><span>4</span><span><span>] </span><span>/</span><span> k[</span></span><span>2</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>disc </span><span>=</span><span> alpha </span><span>*</span><span> alpha </span><span>-</span><span> </span><span>4.0</span><span><span> </span><span>*</span><span> kv </span><span>*</span><span> beta</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>kx </span><span>=</span><span> (</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>(alpha </span><span>+</span><span> math.</span><span>sqrt</span><span>(disc)) </span><span>/</span><span> (</span><span>2.0</span><span><span> </span><span>*</span><span> kv)</span></span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> disc </span><span>&gt;=</span><span> </span></span><span>0.0</span></div></div><div><div><div>11</div></div><div><span>        </span><span>else</span><span><span> (alpha </span><span>/</span><span> (</span></span><span>2.0</span><span><span> </span><span>*</span><span> kv))</span></span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> {</span><span>"kx"</span><span>: kx, </span><span>"kv"</span><span>: kv, </span><span>"ki"</span><span><span>: alpha </span><span>-</span><span> kv </span><span>*</span><span> kx, </span></span><span>"p"</span><span>: p, </span><span>"d"</span><span>: d}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>整定。</strong> 五个增益我交给 <code>scipy.optimize.minimize(method="Nelder-Mead")</code>，目标是 <span><span>∑(xTQx+Rτ2) ts\sum(x^TQx+R\tau^2)\,t_s</span><span><span><span></span><span>∑</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>R</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 <code>step</code> 算例里的值（和 LQR 同一个代价，状态取相对参考的偏差），从上面这组“最接近 LQI”的增益出发，五个增益都限制非负，<code>maxiter=4000</code>。优化过程中仿真用每个采样一步 RK4（正式的仿真是每个采样 10 步）。关键就是这一段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>x0 </span><span>=</span><span> [start[n] </span><span>for</span><span> n </span><span>in</span><span> names]</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>res </span><span>=</span><span> </span><span>minimize</span><span>(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>cost,</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>x0,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>method</span><span>=</span><span>"Nelder-Mead"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>bounds</span><span><span>=</span><span>[(</span></span><span>0.0</span><span>, </span><span>None</span><span><span>)] </span><span>*</span><span> </span></span><span>5</span><span>,  </span><span># no negative gains: no positive feedback</span></div></div><div><div><div>7</div></div><div><span>        </span><span>options</span><span><span>=</span><span>{</span></span><span>"maxiter"</span><span>: </span><span>4000</span><span>},</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把算出来的增益写进 <code>data/ch09_cascade_gains.csv</code>，三个原生程序从里面读，不自己重整定。</p></section>
<section><h2>仿真：同一个循环，三个控制器<a href="#仿真同一个循环三个控制器"><span>#</span></a></h2><p>LQR 和 LQI 是同一个类，<code>integral</code> 标志区分：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>StateFeedback</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""u = -K (x - x_ref e0) (- k_z z): LQR when k_z is 0, LQI otherwise."""</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>k</span></span><span>:</span><span> list[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>integral</span></span><span>:</span><span> </span><span>bool</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.k </span><span>=</span><span> k</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.integral </span><span>=</span><span> integral</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.z </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span><span>.lim </span><span>=</span><span> </span><span>Wip</span><span>().tau_max</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> list[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>xref</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>xt </span><span>=</span><span> [x[</span><span>0</span><span><span>] </span><span>-</span><span> xref, x[</span></span><span>1</span><span>], x[</span><span>2</span><span>], x[</span><span>3</span><span>]]</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>):</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>s </span><span>+=</span><span> </span><span>self</span><span><span>.k[i] </span><span>*</span><span> xt[i]</span></span></div></div><div><div><div>15</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.integral:</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>s </span><span>+=</span><span> </span><span>self</span><span>.k[</span><span>4</span><span><span>] </span><span>*</span><span> </span></span><span>self</span><span>.z</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> </span><span>-</span><span>s</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span><span>(v, </span><span>-</span></span><span>self</span><span>.lim), </span><span>self</span><span>.lim)</span></div></div><div><div><div>19</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.integral </span><span>and</span><span><span> u </span><span>==</span><span> v:</span></span></div></div><div><div><div>20</div></div><div><span>            </span><span>self</span><span><span>.z </span><span>-=</span><span> </span><span>TS</span><span> </span><span>*</span><span> xt[</span></span><span>0</span><span>]</span></div></div><div><div><div>21</div></div><div><span>        </span><span>return</span><span> u</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>u == v</code> 就是“没有饱和”，只有没饱和的时候才积分，这是冻结 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 的全部实现。仿真函数本身：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>simulate</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>name</span><span>:</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>gains</span><span>:</span><span> </span><span>dict</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>data</span><span>:</span><span> </span><span>dict</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>substeps</span><span>:</span><span> </span><span>int</span><span> </span><span>=</span><span> </span><span><span>SUBSTEPS</span><span>,</span></span></div></div><div><div><div>6</div></div><div><span>) -&gt; dict[</span><span>str</span><span>, np.ndarray]:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>plant </span><span>=</span><span> </span><span>Wip</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>plant.m </span><span>*=</span><span> </span><span>float</span><span>(data[</span><span>"mscale"</span><span>][</span><span>0</span><span>])</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>ctl </span><span>=</span><span> </span><span>make_controller</span><span>(name, gains)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> [</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>float</span><span>(data[</span><span>"phi0"</span><span>][</span><span>0</span><span>]), </span><span>0.0</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>rows </span><span>=</span><span> []</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(data[</span><span>"t"</span><span>])):</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>tau </span><span>=</span><span> ctl.</span><span>update</span><span>(x, data[</span><span>"xref"</span><span>][k])</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>([data[</span><span>"t"</span><span><span>][k], </span><span>*</span><span>x, tau])</span></span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>advance</span><span>(plant.deriv, x, [tau, data[</span><span>"force"</span><span><span>][k]], </span><span>TS</span><span>, substeps)</span></span></div></div><div><div><div>16</div></div><div><span>        </span><span>if</span><span> </span><span>abs</span><span>(x[</span><span>2</span><span><span>]) </span><span>&gt;</span><span> </span></span><span>3.0</span><span>:  </span><span># fallen over: freeze (the loop is not a model</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>x[</span><span>2</span><span><span>] </span><span>=</span><span> math.</span><span>copysign</span><span>(</span></span><span>3.0</span><span>, x[</span><span>2</span><span>])  </span><span># of lying on the floor)</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>x[</span><span>3</span><span><span>] </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>19</div></div><div><span>    </span><span>return</span><span> </span><span>dict</span><span>(</span><span>zip</span><span><span>(</span><span>COLUMNS</span><span>, np.</span><span>array</span><span>(rows, </span></span><span>dtype</span><span>=</span><span>float</span><span>).T))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>循环顺序和第 1 篇定的约定一样：先用当前状态算控制量，写一行记录（<code>tau</code> 是限幅后实际作用的力矩），再保持力矩和外力不变积分 10 个 RK4 步。<code>mscale</code> 只放大对象的质量，控制器仍然按 <span><span>M=12M=12</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span> kg 设计，<code>mass130</code> 算例就靠它。倾角超过 3 rad 的时候把它冻在 3 rad 上，免得倒下去之后的数值没有意义，本章所有算例都没有走到这一步（<code>report</code> 断言了 <span><span>∣φ∣&lt;0.5|\varphi|&lt;0.5</span><span><span><span></span><span>∣</span><span>φ</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0.5</span></span></span></span>）。</p><p>四个算例（<code>tilt</code>、<code>step</code>、<code>push</code>、<code>mass130</code>）和它们的参考、外力、初始倾角、质量比例都由 Python 的 <code>scenario_table</code> 写进 <code>data/ch09_&lt;case&gt;.csv</code>，三个原生程序只读。</p></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>两个新模块和新对象是 Python 版的逐行翻译，三种语言的 ch09 应用也是 Python 驱动的逐行翻译：同样的算例表、同样的接线，读同一份数据，<strong>自己算 LQR 和 LQI 的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span></strong>，写进 <code>gains_&lt;lang&gt;.csv</code> 和 Python 的对比。区别在这几处：</p><ul>
<li><strong>矩阵的存放。</strong> C 是平铺的行优先数组加维数参数：<code>pl_mat_mul(a, b, n, k, m, out)</code>，调用者提供所有数组，上限 <code>PL_MAX_DIM</code>（8），输出不能和输入重叠；超过上限的 <code>pl_expm</code>、<code>pl_c2d_zoh</code>、<code>pl_dare_iterate</code> 返回 <code>-EINVAL</code>，不收敛返回 <code>-ERANGE</code>。C++ 是 <code>Mat&lt;T, R, C&gt;</code>（<code>std::array&lt;std::array&lt;T, C&gt;, R&gt;</code>）加模板，维数在类型里，写错了编译不过。Rust 是 <code>[[Real; C]; R]</code> 加 const 泛型。三种语言都没有堆分配（<code>csv.c</code> 除外）。</li>
<li><strong>增广一维。</strong> <code>c2d_zoh</code> 要在 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶系统上做 <span><span>(n+1)(n+1)</span><span><span><span></span><span>(</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span> 阶的指数，<code>lqi_augment</code> 要把 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 阶变成 <span><span>n+1n+1</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 阶。C 的数组长度是运行时的，没有问题。C++ 可以直接写 <code>Mat&lt;T, N + 1, N + 1&gt;</code>。<strong>Rust 稳定版不能在类型里写 <code>N + 1</code></strong>（<code>generic_const_exprs</code> 还没稳定），所以这两个函数多一个 const 参数 <code>M</code>，调用的时候写 <code>c2d_zoh::&lt;4, 5&gt;</code>，函数内部检查 <code>M == N + 1</code>，不对就返回 <code>None</code>。我没有找到更干净的写法。</li>
<li><strong>失败的处理。</strong> <code>dare_iterate</code>：C 是 <code>-ERANGE</code>，C++ 是 <code>std::optional&lt;DareResult&lt;T, N&gt;&gt;</code>（结果里有 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 和迭代次数），Rust 是 <code>Option&lt;(Mat&lt;N, N&gt;, usize)&gt;</code>，Python 抛 <code>RuntimeError</code>。</li>
<li><strong>能量自检。</strong> 三种语言的 ch09 应用在做任何别的事之前，先跑 <span><span>τ=0\tau=0</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>F=0F=0</span><span><span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、没有阻尼的 2 s 自由落体（<span><span>φ0=0.3\varphi_0=0.3</span><span><span><span></span><span><span>φ</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span>，RK4，<span><span>h=10−4h=10^{-4}</span><span><span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>），相对能量漂移超过 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span> 就退出码 1。每一步都检查，不只是写进文件的那 201 行。</li>
<li><strong>标量类型。</strong> 这一篇仍是 <code>double</code>（<code>pl_real</code>、<code>T</code>、<code>Real</code>），但 <code>mat.c</code> 和 <code>lqr.c</code> 里的字面量我都写成了跟着 <code>pl_real</code> 走的形式（<code>HALF</code>、<code>ONE</code>、<code>DARE_TOL</code> 三个宏），因为我想看它们在 <code>-DPL_USE_FLOAT -Wdouble-promotion</code> 下过不过。下面“编译和风格检查”里说结果。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p>矩阵乘是整个 LQR 的基础，一个朴素的三重循环，累加顺序和 Python 一致：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>pl_mat_mul</span><span>(</span><span>const</span><span> pl_real </span><span>*</span><span><span>a</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>b</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>k</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>pl_real </span><span>*</span><span><span>out</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span>  </span><span>int</span><span> i, j, p;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> (j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> m; j</span><span>++</span><span>) {</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>pl_real s </span><span>=</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>      </span><span>for</span><span> (p </span><span>=</span><span> </span><span>0</span><span>; p </span><span>&lt;</span><span> k; p</span><span>++</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> </span><span>a</span><span>[i </span><span>*</span><span> k </span><span>+</span><span> p] </span><span>*</span><span> </span><span>b</span><span>[p </span><span>*</span><span> m </span><span>+</span><span> j];</span></div></div><div><div><div>12</div></div><div><span>      </span><span>out</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> j] </span><span>=</span><span> s;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pl_expm</code> 里的缩放因子 <code>f</code> 是在循环里一次次乘 <code>HALF</code> 得到的，不调 <code>pow</code>，所以和 Python 的 <code>0.5**squarings</code> 逐位相同（2 的幂是精确的）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>pl_expm</span><span>(</span><span>const</span><span> pl_real </span><span>*</span><span><span>a</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> pl_real </span></span><span>*</span><span><span>out</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>pl_real </span><span>h</span><span>[MAT_SIZE], </span><span>term</span><span>[MAT_SIZE], </span><span>tmp</span><span>[MAT_SIZE];</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>pl_real nrm </span><span>=</span><span> </span><span>0.0</span><span>, f </span><span>=</span><span> ONE;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>int</span><span> squarings </span><span>=</span><span> </span><span>0</span><span>, i, j, k;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>  </span><span>if</span><span> (n </span><span>&lt;</span><span> </span><span>1</span><span> </span><span>||</span><span> n </span><span>&gt;</span><span> PL_MAX_DIM)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>EINVAL;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>pl_real row </span><span>=</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> (j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>row </span><span>+=</span><span> </span><span>PL_FABS</span><span>(</span><span>a</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j]);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>if</span><span> (row </span><span>&gt;</span><span> nrm)</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>nrm </span><span>=</span><span> row;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>  </span><span>while</span><span> (nrm </span><span>&gt;</span><span> HALF) {</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>nrm </span><span>*=</span><span> HALF;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>f </span><span>*=</span><span> HALF;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>squarings</span><span>++</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>  </span><span>pl_mat_scale</span><span>(a, f, n, n, h);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>pl_mat_identity</span><span>(n, term);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>pl_mat_identity</span><span>(n, out);</span></div></div><div><div><div>25</div></div><div><span>  </span><span>for</span><span> (k </span><span>=</span><span> </span><span>1</span><span>; k </span><span>&lt;=</span><span> TAYLOR_ORDER; k</span><span>++</span><span>) {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>pl_mat_mul</span><span>(term, h, n, n, n, tmp);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pl_mat_scale</span><span>(tmp, </span><span>1.0</span><span> </span><span>/</span><span> k, n, n, term);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>pl_mat_add</span><span>(out, term, n, n, tmp);</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>30</div></div><div><span>      </span><span>out</span><span>[i] </span><span>=</span><span> </span><span>tmp</span><span>[i];</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>  </span><span>for</span><span> (k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> squarings; k</span><span>++</span><span>) {</span></div></div><div><div><div>33</div></div><div><span>    </span><span>pl_mat_mul</span><span>(out, out, n, n, n, tmp);</span></div></div><div><div><div>34</div></div><div><span>    </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>35</div></div><div><span>      </span><span>out</span><span>[i] </span><span>=</span><span> </span><span>tmp</span><span>[i];</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Riccati 迭代拆成两个函数：<code>dare_step</code> 做一步，返回 <span><span>max⁡∣P+−P∣\max|P^+-P|</span><span><span><span></span><span>max</span><span></span><span>∣</span><span><span>P</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span>∣</span></span></span></span>；<code>pl_dare_iterate</code> 管循环和判据。<code>dare_step</code> 里的临时矩阵是栈上的 <span><span>8×88\times8</span><span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span></span></span></span> 数组：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* One step of the stabilized Riccati map; returns max|P+ - P|. */</span></div></div><div><div><div>2</div></div><div><span>static</span><span> pl_real </span><span>dare_step</span><span>(</span><span>const</span><span> pl_real </span><span>*</span><span><span>ad</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>bd</span><span>,</span></span></div></div><div><div><div>3</div></div><div><span>       </span><span>const</span><span> pl_real </span><span>*</span><span><span>q</span><span>,</span><span> pl_real </span><span>r</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>4</div></div><div><span>       </span><span>const</span><span> pl_real </span><span>*</span><span><span>cur</span><span>,</span><span> pl_real </span></span><span>*</span><span><span>nxt</span><span>)</span></span></div></div><div><div><div>5</div></div><div><span>{</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>pl_real </span><span>k</span><span>[PL_MAX_DIM], </span><span>acl</span><span>[MAT_SIZE], </span><span>t1</span><span>[MAT_SIZE], </span><span>t2</span><span>[MAT_SIZE];</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>pl_real </span><span>t3</span><span>[MAT_SIZE];</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>  </span><span>pl_lqr_gain</span><span>(ad, bd, cur, r, n, k);</span></div></div><div><div><div>10</div></div><div><span>  </span><span>pl_mat_mul</span><span>(bd, k, n, </span><span>1</span><span>, n, t1);</span></div></div><div><div><div>11</div></div><div><span>  </span><span>pl_mat_sub</span><span>(ad, t1, n, n, acl);</span><span> /* A - BK */</span></div></div><div><div><div>12</div></div><div><span>  </span><span>pl_mat_transpose</span><span>(k, </span><span>1</span><span>, n, t1);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>pl_mat_mul</span><span>(t1, k, n, </span><span>1</span><span>, n, t2);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>pl_mat_scale</span><span>(t2, r, n, n, t3);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>pl_mat_add</span><span>(q, t3, n, n, t1);</span><span> /* Q + K'RK */</span></div></div><div><div><div>16</div></div><div><span>  </span><span>pl_mat_mul</span><span>(cur, acl, n, n, n, t2);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>pl_mat_transpose</span><span>(acl, n, n, t3);</span></div></div><div><div><div>18</div></div><div><span>  </span><span>pl_mat_mul</span><span>(t3, t2, n, n, n, acl);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>pl_mat_add</span><span>(t1, acl, n, n, nxt);</span></div></div><div><div><div>20</div></div><div><span>  </span><span>pl_mat_sub</span><span>(nxt, cur, n, n, t1);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>return</span><span> </span><span>pl_mat_max_abs</span><span>(t1, n, n);</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>pl_dare_iterate</span><span>(</span><span>const</span><span> pl_real </span><span>*</span><span><span>ad</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>bd</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>q</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>pl_real </span><span>r</span><span>,</span><span> </span><span>int</span><span><span> </span><span>n</span><span>,</span><span> pl_real </span></span><span>*</span><span><span>p</span><span>,</span><span> </span></span><span>int</span><span> </span><span>*</span><span><span>iters</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>pl_real </span><span>cur</span><span>[MAT_SIZE], </span><span>nxt</span><span>[MAT_SIZE], diff;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>int</span><span> it, i;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>  </span><span>if</span><span> (n </span><span>&lt;</span><span> </span><span>1</span><span> </span><span>||</span><span> n </span><span>&gt;</span><span> PL_MAX_DIM)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>EINVAL;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>cur</span><span>[i] </span><span>=</span><span> </span><span>q</span><span>[i];</span></div></div><div><div><div>11</div></div><div><span>  </span><span>for</span><span> (it </span><span>=</span><span> </span><span>1</span><span>; it </span><span>&lt;=</span><span> PL_DARE_MAX_ITER; it</span><span>++</span><span>) {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>diff </span><span>=</span><span> </span><span>dare_step</span><span>(ad, bd, q, r, n, cur, nxt);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>14</div></div><div><span>      </span><span>cur</span><span>[i] </span><span>=</span><span> </span><span>nxt</span><span>[i];</span></div></div><div><div><div>15</div></div><div><span>    </span><span>if</span><span> (diff </span><span>&lt;</span><span> DARE_TOL </span><span>*</span><span> (ONE </span><span>+</span><span> </span><span>pl_mat_max_abs</span><span>(cur, n, n))) {</span></div></div><div><div><div>16</div></div><div><span>      </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>17</div></div><div><span>        </span><span>p</span><span>[i] </span><span>=</span><span> </span><span>cur</span><span>[i];</span></div></div><div><div><div>18</div></div><div><span>      </span><span>*</span><span>iters </span><span>=</span><span> it;</span></div></div><div><div><div>19</div></div><div><span>      </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>  </span><span>return</span><span> </span><span>-</span><span>ERANGE;</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对象在 <code>plants.c</code>，和 Python 的 <code>Wip.deriv</code> 同样的表达式顺序（这是 C 和 Python 逐位一致的前提）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>pl_wip_deriv</span><span>(</span><span>const</span><span> </span><span>void</span><span> </span><span>*</span><span><span>ctx</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>x</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>u</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>pl_real </span><span>*</span><span><span>dx</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span>  </span><span>const</span><span> </span><span>struct</span><span> pl_wip </span><span>*</span><span>p </span><span>=</span><span> ctx;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>pl_real a </span><span>=</span><span> </span><span>p</span><span>-&gt;</span><span>mw</span><span> </span><span>+</span><span> </span><span>p</span><span>-&gt;</span><span>iw</span><span> </span><span>/</span><span> (</span><span>p</span><span>-&gt;</span><span>r</span><span> </span><span>*</span><span> </span><span>p</span><span>-&gt;</span><span>r</span><span>) </span><span>+</span><span> </span><span>p</span><span>-&gt;</span><span>m</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>pl_real c </span><span>=</span><span> </span><span>p</span><span>-&gt;</span><span>ib</span><span> </span><span>+</span><span> </span><span>p</span><span>-&gt;</span><span>m</span><span> </span><span>*</span><span> </span><span>p</span><span>-&gt;</span><span>l</span><span> </span><span>*</span><span> </span><span>p</span><span>-&gt;</span><span>l</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>pl_real ml </span><span>=</span><span> </span><span>p</span><span>-&gt;</span><span>m</span><span> </span><span>*</span><span> </span><span>p</span><span>-&gt;</span><span>l</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>pl_real s </span><span>=</span><span> </span><span>PL_SIN</span><span>(</span><span>x</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>pl_real co </span><span>=</span><span> </span><span>PL_COS</span><span>(</span><span>x</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>pl_real r1 </span><span>=</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>/</span><span> </span><span>p</span><span>-&gt;</span><span>r</span><span> </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>+</span><span> ml </span><span>*</span><span> s </span><span>*</span><span> </span><span>x</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>x</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>pl_real r2 </span><span>=</span><span> </span><span>-</span><span>u</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>p</span><span>-&gt;</span><span>l</span><span> </span><span>*</span><span> co </span><span>+</span><span> ml </span><span>*</span><span> </span><span>p</span><span>-&gt;</span><span>g</span><span> </span><span>*</span><span> s;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>pl_real b </span><span>=</span><span> ml </span><span>*</span><span> co;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>pl_real det </span><span>=</span><span> a </span><span>*</span><span> c </span><span>-</span><span> b </span><span>*</span><span> b;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>  </span><span>dx</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>x</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>16</div></div><div><span>  </span><span>dx</span><span>[</span><span>1</span><span>] </span><span>=</span><span> (c </span><span>*</span><span> r1 </span><span>-</span><span> b </span><span>*</span><span> r2) </span><span>/</span><span> det;</span></div></div><div><div><div>17</div></div><div><span>  </span><span>dx</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>x</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>18</div></div><div><span>  </span><span>dx</span><span>[</span><span>3</span><span>] </span><span>=</span><span> (a </span><span>*</span><span> r2 </span><span>-</span><span> b </span><span>*</span><span> r1) </span><span>/</span><span> det;</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>自检的函数在 <code>apps/ch09.c</code>，每一步都比较：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Energy must stay put with tau = 0, no damping and no force. */</span></div></div><div><div><div>2</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>energy_check</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span>  </span><span>struct</span><span> pl_wip p;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>pl_real </span><span>x</span><span>[PL_MAX_STATES] </span><span>=</span><span> { </span><span>0</span><span>, </span><span>0</span><span>, E_PHI0, </span><span>0</span><span> };</span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> pl_real </span><span>u</span><span>[PL_WIP_M] </span><span>=</span><span> { </span><span>0.0</span><span>, </span><span>0.0</span><span> };</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>pl_real e0, worst </span><span>=</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>double</span><span> </span><span>out</span><span>[E_ROWS </span><span>*</span><span> </span><span>3</span><span>];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>int</span><span> k, ret;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>  </span><span>pl_wip_init</span><span>(</span><span>&amp;</span><span>p);</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>e0 </span><span>=</span><span> </span><span>pl_wip_energy</span><span>(</span><span>&amp;</span><span>p, x);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>for</span><span> (k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;=</span><span> E_STEPS; k</span><span>++</span><span>) {</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>pl_real e_rel </span><span>=</span><span> (</span><span>pl_wip_energy</span><span>(</span><span>&amp;</span><span>p, x) </span><span>-</span><span> e0) </span><span>/</span><span> e0;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> (</span><span>PL_FABS</span><span>(e_rel) </span><span>&gt;</span><span> worst)</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>worst </span><span>=</span><span> </span><span>PL_FABS</span><span>(e_rel);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>/* ... every E_EVERY steps: record t, phi, e_rel ... */</span></div></div><div><div><div>19</div></div><div><span>    </span><span>if</span><span> (k </span><span>&lt;</span><span> E_STEPS)</span></div></div><div><div><div>20</div></div><div><span>      </span><span>pl_rk4_step</span><span>(pl_wip_deriv, </span><span>&amp;</span><span>p, x, u, PL_WIP_N, E_H);</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>  </span><span>printf</span><span>(</span><span>"energy check: max |dE/E0| = </span><span>%.3e</span><span> over </span><span>%g</span><span> s (limit </span><span>%.0e</span><span>)</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>         </span></span><span>(</span><span>double</span><span>)worst, E_STEPS </span><span>*</span><span> E_H, E_TOL);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>if</span><span> (worst </span><span>&gt;=</span><span> E_TOL)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>EDOM;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>/* ... write out/ch09/energy_c.csv ... */</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>完整应用：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch09.c" target="_blank"><code>c/apps/ch09.c</code></a>。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p><code>Mat</code> 是别名模板，<code>Mul</code> 的三个维数参数 <code>N, K, M</code> 由两个实参推出来，维数对不上的乘法编译不过：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// R x C matrix, row-major.</span></div></div><div><div><div>2</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>, std::</span><span>size_t</span><span> </span><span>R</span><span>, std::</span><span>size_t</span><span> </span><span>C</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span>using</span><span> </span><span>Mat</span><span> </span><span>=</span><span> std::</span><span>array</span><span>&lt;std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>C</span><span>&gt;, </span><span>R</span><span>&gt;;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>, std::</span><span>size_t</span><span> </span><span>N</span><span>, std::</span><span>size_t</span><span> </span><span>K</span><span>, std::</span><span>size_t</span><span> </span><span>M</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>M</span><span>&gt; </span><span>Mul</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>K</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>a</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>K</span><span>, </span><span>M</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>b</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>M</span><span>&gt; </span><span>out</span><span>;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>M</span><span>; </span><span>++</span><span>j</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>      </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>11</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>p</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>p</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>++</span><span>p</span><span>) </span><span>s</span><span> </span><span>+=</span><span> </span><span>a</span><span>[</span><span>i</span><span>][</span><span>p</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>p</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>12</div></div><div><span>      </span><span>out</span><span>[</span><span>i</span><span>][</span><span>j</span><span>] </span><span>=</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>  </span><span>return</span><span> </span><span>out</span><span>;</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Expm</code> 和 <code>DareIterate</code> 是 Python 版的逐行翻译，模板参数 <code>T</code>（默认 <code>double</code>）和 <code>N</code>（状态维数）由实参推出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// exp(a): scale until the infinity norm is at most 0.5, Taylor series to</span></div></div><div><div><div>2</div></div><div><span>// order 12, square back.</span></div></div><div><div><div>3</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>, std::</span><span>size_t</span><span> </span><span>N</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt; </span><span>Expm</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>a</span><span>) {</span></span></div></div><div><div><div>5</div></div><div><span>  </span><span>T</span><span> </span><span>nrm</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>6</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>auto&amp;</span><span> </span><span>row</span><span> : </span><span>a</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> (</span><span>T</span><span> </span><span>v</span><span> : </span><span>row</span><span>) </span><span>s</span><span> </span><span>+=</span><span> std::</span><span>fabs</span><span>(</span><span>v</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>nrm</span><span> </span><span>=</span><span> </span><span>s</span><span> </span><span>&gt;</span><span> </span><span>nrm</span><span> </span><span>?</span><span> </span><span>s</span><span> </span><span>:</span><span> </span><span>nrm</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>  </span><span>int</span><span> </span><span>squarings</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>T</span><span> </span><span>f</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>while</span><span> (</span><span>nrm</span><span> </span><span>&gt;</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>)) {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>nrm</span><span> </span><span>*=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>f</span><span> </span><span>*=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>++</span><span>squarings</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt; </span><span>h</span><span> </span><span>=</span><span> </span><span>Scale</span><span>(</span><span>a</span><span>, </span><span>f</span><span>);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt; </span><span>term</span><span> </span><span>=</span><span> </span><span>Identity</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;();</span></div></div><div><div><div>20</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt; </span><span>out</span><span> </span><span>=</span><span> </span><span>Identity</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;();</span></div></div><div><div><div>21</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>1</span><span>; </span><span>k</span><span> </span><span>&lt;=</span><span> </span><span>12</span><span>; </span><span>++</span><span>k</span><span>) {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>term</span><span> </span><span>=</span><span> </span><span>Scale</span><span>(</span><span>Mul</span><span>(</span><span>term</span><span>, </span><span>h</span><span>), </span><span>T</span><span>(</span><span>1</span><span>) </span><span>/</span><span> </span><span>T</span><span>(</span><span>k</span><span>));</span></div></div><div><div><div>23</div></div><div><span>    </span><span>out</span><span> </span><span>=</span><span> </span><span>Add</span><span>(</span><span>out</span><span>, </span><span>term</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>squarings</span><span>; </span><span>++</span><span>k</span><span>) </span><span>out</span><span> </span><span>=</span><span> </span><span>Mul</span><span>(</span><span>out</span><span>, </span><span>out</span><span>);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>return</span><span> </span><span>out</span><span>;</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Iterates P &lt;- Q + K'RK + (A - BK)'P(A - BK) with K from the current P,</span></div></div><div><div><div>2</div></div><div><span>// starting at P = Q, until max|dP| &lt; 1e-12 (1 + max|P|). Returns</span></div></div><div><div><div>3</div></div><div><span>// std::nullopt after kDareMaxIter steps.</span></div></div><div><div><div>4</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>, std::</span><span>size_t</span><span> </span><span>N</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span>std::</span><span>optional</span><span>&lt;</span><span>DareResult</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;&gt; </span><span>DareIterate</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>ad</span><span>,</span></span></div></div><div><div><div>6</div></div><div><span>                                            </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>1</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>bd</span><span>,</span></span></div></div><div><div><div>7</div></div><div><span>                                            </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt; </span><span>p</span><span> </span><span>=</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>1</span><span>; </span><span>it</span><span> </span><span>&lt;=</span><span> </span><span>kDareMaxIter</span><span>; </span><span>++</span><span>it</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>1</span><span>, </span><span>N</span><span>&gt; </span><span>k</span><span> </span><span>=</span><span> </span><span>LqrGain</span><span>(</span><span>ad</span><span>, </span><span>bd</span><span>, </span><span>p</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt; </span><span>acl</span><span> </span><span>=</span><span> </span><span>Sub</span><span>(</span><span>ad</span><span>, </span><span>Mul</span><span>(</span><span>bd</span><span>, </span><span>k</span><span>));</span></div></div><div><div><div>12</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>, </span><span>N</span><span>&gt; </span><span>pn</span><span> </span><span>=</span><span> </span><span>Add</span><span>(</span><span>q</span><span>, </span><span>Scale</span><span>(</span><span>Mul</span><span>(</span><span>Transpose</span><span>(</span><span>k</span><span>), </span><span>k</span><span>), </span><span>r</span><span>));</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pn</span><span> </span><span>=</span><span> </span><span>Add</span><span>(</span><span>pn</span><span>, </span><span>Mul</span><span>(</span><span>Transpose</span><span>(</span><span>acl</span><span>), </span><span>Mul</span><span>(</span><span>p</span><span>, </span><span>acl</span><span>)));</span></div></div><div><div><div>14</div></div><div><span>    </span><span>const</span><span> </span><span>bool</span><span> </span><span>done</span><span> </span><span>=</span><span> </span><span>MaxAbs</span><span>(</span><span>Sub</span><span>(</span><span>pn</span><span>, </span><span>p</span><span>)) </span><span>&lt;</span><span> </span><span>T</span><span>(</span><span><span>1</span><span>e</span></span><span>-</span><span>12</span><span>) </span><span>*</span><span> (</span><span>T</span><span>(</span><span>1</span><span>) </span><span>+</span><span> </span><span>MaxAbs</span><span>(</span><span>pn</span><span>));</span></div></div><div><div><div>15</div></div><div><span>    </span><span>p</span><span> </span><span>=</span><span> </span><span>pn</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> (</span><span>done</span><span>) </span><span>return</span><span> </span><span>DareResult</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>{</span><span>p</span><span>, </span><span>it</span><span>};</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>  </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对象是 <code>Wip&lt;T&gt;</code> 结构体，<code>Deriv</code> 返回 <code>std::array&lt;T, 4&gt;</code>，驱动里用 lambda 包装给 <code>Advance</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>kN</span><span>&gt; </span><span>Deriv</span><span>(</span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>kN</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>x</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span>                        </span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>kM</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>u</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>mw</span><span> </span><span>+</span><span> </span><span>iw</span><span> </span><span>/</span><span> (</span><span>r</span><span> </span><span>*</span><span> </span><span>r</span><span>) </span><span>+</span><span> </span><span>m</span><span>;</span></div></div><div><div><div>4</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>ib</span><span> </span><span>+</span><span> </span><span>m</span><span> </span><span>*</span><span> </span><span>l</span><span> </span><span>*</span><span> </span><span>l</span><span>;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>ml</span><span> </span><span>=</span><span> </span><span>m</span><span> </span><span>*</span><span> </span><span>l</span><span>;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> std::</span><span>sin</span><span>(</span><span>x</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>7</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>co</span><span> </span><span>=</span><span> std::</span><span>cos</span><span>(</span><span>x</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>r1</span><span> </span><span>=</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>/</span><span> </span><span>r</span><span> </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>ml</span><span> </span><span>*</span><span> </span><span>s</span><span> </span><span>*</span><span> </span><span>x</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>x</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>r2</span><span> </span><span>=</span><span> </span><span>-</span><span>u</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>l</span><span> </span><span>*</span><span> </span><span>co</span><span> </span><span>+</span><span> </span><span>ml</span><span> </span><span>*</span><span> </span><span>g</span><span> </span><span>*</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>10</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>ml</span><span> </span><span>*</span><span> </span><span>co</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>det</span><span> </span><span>=</span><span> </span><span>a</span><span> </span><span>*</span><span> </span><span>c</span><span> </span><span>-</span><span> </span><span>b</span><span> </span><span>*</span><span> </span><span>b</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>return</span><span> {</span><span>x</span><span>[</span><span>1</span><span>], (</span><span>c</span><span> </span><span>*</span><span> </span><span>r1</span><span> </span><span>-</span><span> </span><span>b</span><span> </span><span>*</span><span> </span><span>r2</span><span>) </span><span>/</span><span> </span><span>det</span><span>, </span><span>x</span><span>[</span><span>3</span><span>], (</span><span>a</span><span> </span><span>*</span><span> </span><span>r2</span><span> </span><span>-</span><span> </span><span>b</span><span> </span><span>*</span><span> </span><span>r1</span><span>) </span><span>/</span><span> </span><span>det</span><span>};</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>完整应用：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch09.cc" target="_blank"><code>cpp/apps/ch09.cc</code></a>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><p>const 泛型让矩阵运算的签名和数学一致：<code>mul</code> 是 <code>Mat&lt;N, K&gt; * Mat&lt;K, M&gt; -&gt; Mat&lt;N, M&gt;</code>。<code>c2d_zoh</code> 的 <code>M = N + 1</code> 问题前面说过，这是它的样子：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// ZOH discretization of `x' = a x + b u` through `exp([[A, B], [0, 0]] ts)`.</span></div></div><div><div><div>2</div></div><div><span>/// `M` must be `N + 1`.</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>c2d_zoh</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>4</div></div><div><span>    </span><span>a</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>b</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>ts</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>7</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>&gt;)&gt; {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span> </span><span>M</span><span><span> </span><span>!=</span><span> </span></span><span>N</span><span> + </span><span>1</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>big</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>M</span><span>]; </span><span>M</span><span>];</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>big</span><span>[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>a</span><span>[</span><span>i</span><span>][</span><span>j</span><span>] * </span><span>ts</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>        </span><span>big</span><span>[</span><span>i</span><span>][</span><span>N</span><span><span>] </span><span>=</span><span> </span></span><span>b</span><span>[</span><span>i</span><span>][</span><span>0</span><span>] * </span><span>ts</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>e</span><span><span> </span><span>=</span><span> </span></span><span>expm</span><span>(&amp;</span><span>big</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>ad</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>N</span><span>]; </span><span>N</span><span>];</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>bd</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>1</span><span>]; </span><span>N</span><span>];</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>ad</span><span>[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>e</span><span>[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>        </span><span>bd</span><span>[</span><span>i</span><span>][</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>e</span><span>[</span><span>i</span><span>][</span><span>N</span><span>];</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>    </span><span>Some</span><span>((</span><span>ad</span><span>, </span><span>bd</span><span>))</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Iterates `P &lt;- Q + K'RK + (A - BK)'P(A - BK)` with `K` from the current</span></div></div><div><div><div>2</div></div><div><span>/// `P`, starting at `P = Q`, until `max|dP| &lt; 1e-12 (1 + max|P|)`. Returns</span></div></div><div><div><div>3</div></div><div><span>/// `P` and the iteration count, or `None` after `DARE_MAX_ITER` steps.</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>dare_iterate</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>5</div></div><div><span>    </span><span>ad</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>bd</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>q</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>r</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>9</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;, </span><span>usize</span><span>)&gt; {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> *</span></span><span>q</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> </span><span>it</span><span> </span><span>in</span><span> </span><span>1</span><span>..=</span><span>DARE_MAX_ITER</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>lqr_gain</span><span>(</span><span>ad</span><span>, </span><span>bd</span><span>, &amp;</span><span>p</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>acl</span><span><span> </span><span>=</span><span> </span></span><span>sub</span><span>(</span><span>ad</span><span>, &amp;</span><span>mul</span><span>(</span><span>bd</span><span>, &amp;</span><span>k</span><span>));</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>pn</span><span><span> </span><span>=</span><span> </span></span><span>add</span><span>(</span><span>q</span><span>, &amp;</span><span>scale</span><span>(&amp;</span><span>mul</span><span>(&amp;</span><span>transpose</span><span>(&amp;</span><span>k</span><span>), &amp;</span><span>k</span><span>), </span><span>r</span><span>));</span></div></div><div><div><div>15</div></div><div><span>        </span><span>pn</span><span><span> </span><span>=</span><span> </span></span><span>add</span><span>(&amp;</span><span>pn</span><span>, &amp;</span><span>mul</span><span>(&amp;</span><span>transpose</span><span>(&amp;</span><span>acl</span><span>), &amp;</span><span>mul</span><span>(&amp;</span><span>p</span><span>, &amp;</span><span>acl</span><span>)));</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>done</span><span><span> </span><span>=</span><span> </span></span><span>max_abs</span><span>(&amp;</span><span>sub</span><span>(&amp;</span><span>pn</span><span>, &amp;</span><span>p</span><span>)) &lt; </span><span>1</span><span>e-</span><span>12</span><span> * (</span><span>1.0</span><span> + </span><span>max_abs</span><span>(&amp;</span><span>pn</span><span>));</span></div></div><div><div><div>17</div></div><div><span>        </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>pn</span><span>;</span></div></div><div><div><div>18</div></div><div><span>        </span><span>if</span><span> </span><span>done</span><span> {</span></div></div><div><div><div>19</div></div><div><span>            </span><span>return</span><span> </span><span>Some</span><span>((</span><span>p</span><span>, </span><span>it</span><span>));</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>    </span><span>None</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对象 <code>Wip</code> 是一个普通的结构体，<code>deriv</code> 取 <code>&amp;[Real; 4]</code> 和 <code>&amp;[Real; 2]</code>，返回 <code>[Real; 4]</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>deriv</span><span>(&amp;</span><span>self</span><span>, </span><span>x</span><span>: &amp;[</span><span>Real</span><span>; </span><span>4</span><span>], </span><span>u</span><span>: &amp;[</span><span>Real</span><span>; </span><span>2</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>4</span><span>] {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.mw + </span><span>self</span><span>.iw / (</span><span>self</span><span>.r * </span><span>self</span><span>.r) + </span><span>self</span><span>.m;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.ib + </span><span>self</span><span>.m * </span><span>self</span><span>.l * </span><span>self</span><span>.l;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>ml</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.m * </span><span>self</span><span>.l;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>sin</span><span>(</span><span>x</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>co</span><span><span> </span><span>=</span><span> </span></span><span>cos</span><span>(</span><span>x</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>r1</span><span><span> </span><span>=</span><span> </span></span><span>u</span><span>[</span><span>0</span><span>] / </span><span>self</span><span>.r + </span><span>u</span><span>[</span><span>1</span><span>] + </span><span>ml</span><span> * </span><span>s</span><span> * </span><span>x</span><span>[</span><span>3</span><span>] * </span><span>x</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>r2</span><span><span> </span><span>=</span><span> -</span></span><span>u</span><span>[</span><span>0</span><span>] + </span><span>u</span><span>[</span><span>1</span><span>] * </span><span>self</span><span>.l * </span><span>co</span><span> + </span><span>ml</span><span> * </span><span>self</span><span>.g * </span><span>s</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>ml</span><span> * </span><span>co</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>det</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span> * </span><span>c</span><span> - </span><span>b</span><span> * </span><span>b</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>[</span><span>x</span><span>[</span><span>1</span><span>], (</span><span>c</span><span> * </span><span>r1</span><span> - </span><span>b</span><span> * </span><span>r2</span><span>) / </span><span>det</span><span>, </span><span>x</span><span>[</span><span>3</span><span>], (</span><span>a</span><span> * </span><span>r2</span><span> - </span><span>b</span><span> * </span><span>r1</span><span>) / </span><span>det</span><span>]</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>完整应用：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch09.rs" target="_blank"><code>rust/src/bin/ch09.rs</code></a>。</p></section><section><h3>编译和风格检查<a href="#编译和风格检查"><span>#</span></a></h3><p>编译参数和前几篇一样：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code> 加同样的警告，Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>。风格检查：C 用内核的 <code>checkpatch.pl</code>（<code>--no-tree</code>，忽略 SPDX 和 typedef 两项），0 errors、0 warnings；C++ 用 clang-format 的 Google 风格（<code>-Werror</code>）；Python 用 <code>ruff format --check</code> 和 <code>ruff check</code>，行宽 79；Rust 用 <code>cargo fmt --check</code>。四项都在提交前过了。</p><p>两个额外检查。第一，<code>mat.c</code> 和 <code>lqr.c</code> 在 <code>-DPL_USE_FLOAT -Wdouble-promotion -Werror</code> 下能编译（要用 <code>HALF</code> 这几个宏，直接写 <code>0.5</code> 会被报 float 提升成 double）；<code>plants.c</code> 过不了这个检查，因为前面几篇的对象里就有 <code>0.5 * h</code> 这种写法，我没有为它改旧代码。需要说清楚：<strong>能编译不等于能用。</strong> 我另写了一个 15 行的 C 程序，用 <code>-DPL_USE_FLOAT</code> 把 LQR 的 4 阶 DARE 跑了一遍：它在 9134 步停下来（double 是 13067 步），<span><span>P22P_{22}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 17131.5，double 的是 17131.1，相对差 <span><span>2×10−52\times10^{-5}</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span>。<span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 的判据在 float 下达不到，它还是停了，我猜是迭代在 float 的精度上卡住不动了。这个 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的精度我没有量。LQR 的增益本来就应该在 PC 上算好再写进 MCU，上车的代码只需要 <span><span>u=−Kxu=-Kx</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span></span></span></span> 那几次乘加，这是第 10 篇的事。第二，<code>cargo build --lib --features f32</code> 能过：库本身是 <code>no_std</code> 兼容的，<code>mat.rs</code> 和 <code>lqr.rs</code> 没有引入任何新依赖。</p><p><code>checkpatch.pl</code> 这次没有重现第 8 篇的那个误报（它对 <code>pl_real</code> 的第一次使用报间距），因为 <code>mat.h</code> 里 <code>PL_MAX_DIM</code> 的 <code>#define</code> 在 <code>pl_real</code> 的第一次使用之前。这一回是我碰巧按第 8 篇学到的做法排的序，不是修了什么。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch09</code> 一次跑完（Python 的 <code>ref</code> 里有 Nelder–Mead 的约 930 次仿真，加上 DARE，整套大约 100 秒，三个原生程序各约 0.1 到 0.2 秒（含 DARE 和写文件））。14 个文件，每种语言 14 个，42 个对比文件，最大差异 <span><span>5.06×10−135.06\times10^{-13}</span><span><span><span></span><span>5.06</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>（<code>push_cascade_rs.csv</code>），出现在 Rust 的串级里，我猜是 <code>libm</code> 和 glibc 的 <code>sin</code>、<code>cos</code> 在最后一位上的差别在 5 秒的仿真里累积。下面的输出裁掉了中间 36 行 <code>ok</code>；三个原生程序的能量自检、迭代次数完全一样，DARE 的迭代次数（13067 和 15178）在三种语言里一步不差：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch09</span></div></div><div><div><div>2</div></div><div><span>energy check: max |dE/E0| = 1.063e-13 over 2 s (limit 1e-06)</span></div></div><div><div><div>3</div></div><div><span>DARE iterations: LQR 13067, LQI 15178</span></div></div><div><div><div>4</div></div><div><span>ch09: C wrote 12 cases to out/ch09</span></div></div><div><div><div>5</div></div><div><span>energy check: max |dE/E0| = 1.063e-13 over 2 s (limit 1e-06)</span></div></div><div><div><div>6</div></div><div><span>DARE iterations: LQR 13067, LQI 15178</span></div></div><div><div><div>7</div></div><div><span>ch09: C++ wrote 12 cases to out/ch09</span></div></div><div><div><div>8</div></div><div><span>energy check: max |dE/E0| = 1.063e-13 over 2 s (limit 1e-6)</span></div></div><div><div><div>9</div></div><div><span>DARE iterations: LQR 13067, LQI 15178</span></div></div><div><div><div>10</div></div><div><span>ch09: Rust wrote 12 cases to out/ch09</span></div></div><div><div><div>11</div></div><div><span>ok   energy_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>ok   energy_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>13</div></div><div><span>ok   energy_rs.csv: max diff 7.898e-16 &lt;= 1.0e-09</span></div></div><div><div><div>14</div></div><div><span>... （中间 36 行 ok 省略）</span></div></div><div><div><div>15</div></div><div><span>ok   tilt_lqr_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>16</div></div><div><span>ok   tilt_lqr_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>17</div></div><div><span>ok   tilt_lqr_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>Wip: A, B from the Lagrange model; ts = 0.001 s, Q = diag(4.0, 1.0, 100.0, 1.0), R = 0.01, q_z = 4</span></div></div><div><div><div>20</div></div><div><span>energy, tau = 0, h = 0.0001, 2 s: max |dE/E0| = 1.063e-13</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>same run at h = 1e-3 (2000 steps): max = 9.897e-10</span></div></div><div><div><div>22</div></div><div><span>torque and force on, 3 s: max |E - E0 - W| = 3.181e-12 J</span></div></div><div><div><div>23</div></div><div><span>sign check, tau = +1 at rest: xdd = +1.5054, phidd = -5.2419</span></div></div><div><div><div>24</div></div><div><span>A, B vs central differences: max error 4.47e-11</span></div></div><div><div><div>25</div></div><div><span>A =</span></div></div><div><div><div>26</div></div><div><span>[[ 0.          1.          0.          0.        ]</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>[ 0.          0.         -7.59483871  0.        ]</span></div></div><div><div><div>28</div></div><div><span><span> </span></span><span>[ 0.          0.          0.          1.        ]</span></div></div><div><div><div>29</div></div><div><span><span> </span></span><span>[ 0.          0.         47.46774194  0.        ]]</span></div></div><div><div><div>30</div></div><div><span>B^T = [ 0.          1.50537634  0.         -5.24193548]</span></div></div><div><div><div>31</div></div><div><span>open-loop poles (rad/s): [-6.89  0.    0.    6.89]</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>expm vs scipy.linalg.expm: max relative error 2.51e-13</span></div></div><div><div><div>34</div></div><div><span>LQR: 13067 iterations, P vs solve_discrete_are 6.53e-10, K vs scipy 3.07e-10 (relative to the max entry)</span></div></div><div><div><div>35</div></div><div><span>LQI: 15178 iterations, P vs solve_discrete_are 4.53e-10, K vs scipy 1.99e-10 (relative to the max entry)</span></div></div><div><div><div>36</div></div><div><span>textbook form A'PA - A'PB (R + B'PB)^-1 B'PA: max|dP| bottoms out at 4.2e-02 (step 4454), then grows to 4.9e+44 by step 20000; the stabilized form converged in 13067</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>LQR gain K (u = -K x), x = [x, xd, phi, phid]:</span></div></div><div><div><div>39</div></div><div><span>[ -19.3507  -29.8702 -133.6297  -20.8277]</span></div></div><div><div><div>40</div></div><div><span>LQI gain (last entry is k_z):</span></div></div><div><div><div>41</div></div><div><span>[ -46.3429  -45.8187 -150.3896  -25.5436   19.3436]</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>cascade mapping: P = 150.390, D = 25.544, kv = 0.3047, alpha = 0.3082, beta = 0.1286, discriminant alpha^2 - 4 kv beta = -0.0618</span></div></div><div><div><div>44</div></div><div><span>nearest cascade gains: kx = 0.5057, kv = 0.3047, ki = 0.1541, p = 150.3896, d = 25.5436</span></div></div><div><div><div>45</div></div><div><span>Nelder-Mead: 596 iterations, 932 evaluations, cost 6.318 -&gt; 6.162</span></div></div><div><div><div>46</div></div><div><span>tuned cascade gains:   kx = 0.6546, kv = 0.2174, ki = 1.528e-12, p = 119.6, d = 17.76</span></div></div><div><div><div>47</div></div><div><span>equivalent K = [K_x, K_xd, K_phi, K_phid, K_z]:</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>LQI                [ -46.343  -45.819 -150.390  -25.544   19.344]</span></div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>cascade (tuned)    [-1.702e+01 -2.600e+01 -1.196e+02 -1.776e+01  1.196e-10]</span></div></div><div><div><div>50</div></div><div><span><span>  </span></span><span>cascade (nearest)  [ -46.343  -45.819 -150.390  -25.544   11.718]</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>LQR                        max |eig| = 0.999158  slowest decay 0.842 1/s (tau = 1.19 s)</span></div></div><div><div><div>53</div></div><div><span>LQI                        max |eig| = 0.999235  slowest decay 0.765 1/s (tau = 1.31 s)</span></div></div><div><div><div>54</div></div><div><span>cascade (tuned, no I)      max |eig| = 0.999185  slowest decay 0.816 1/s (tau = 1.23 s)</span></div></div><div><div><div>55</div></div><div><span>cascade (nearest to LQI)   max |eig| = 0.999642  slowest decay 0.358 1/s (tau = 2.80 s)</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>tilt: phi0 = 0.15 rad, x_ref = 0</span></div></div><div><div><div>58</div></div><div><span>ctl      |tau| max  sat ms   x max    x end  phi&lt;0.01 at</span></div></div><div><div><div>59</div></div><div><span>cascade      10.00      18   0.082  -0.0020        1.049</span></div></div><div><div><div>60</div></div><div><span>lqr          10.00      18   0.084  -0.0017        1.080</span></div></div><div><div><div>61</div></div><div><span>lqi          10.00      22   0.075  -0.0090        0.973</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>step: x_ref = 1 m at 0.5 s</span></div></div><div><div><div>64</div></div><div><span>ctl      overshoot%   x min   x end  t95 (s)  phi max |tau| max  sat ms</span></div></div><div><div><div>65</div></div><div><span>cascade         2.7  -0.033   1.027     3.52    0.135     10.00      13</span></div></div><div><div><div>66</div></div><div><span>lqr             2.2  -0.032   1.022     3.55    0.134     10.00      14</span></div></div><div><div><div>67</div></div><div><span>lqi            38.8  -0.072   1.132      nan    0.299     10.00      45</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span>mass130: x_ref = 1 m at 0.5 s</span></div></div><div><div><div>70</div></div><div><span>ctl      overshoot%   x min   x end  t95 (s)  phi max |tau| max  sat ms</span></div></div><div><div><div>71</div></div><div><span>cascade         2.1  -0.030   1.021     3.52    0.135     10.00      13</span></div></div><div><div><div>72</div></div><div><span>lqr             1.8  -0.029   1.018     3.56    0.133     10.00      13</span></div></div><div><div><div>73</div></div><div><span>lqi            37.9  -0.065   1.131      nan    0.297     10.00      42</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>push: 10 N on the body COM from 2.0 s</span></div></div><div><div><div>76</div></div><div><span>ctl       x @3 s  x @4 s  x @5 s   x max  phi @5 s  tau @5 s</span></div></div><div><div><div>77</div></div><div><span>cascade    0.293   0.632   0.802   0.802   -0.1469   -1.1813</span></div></div><div><div><div>78</div></div><div><span>lqr        0.292   0.625   0.791   0.791   -0.1462   -1.1748</span></div></div><div><div><div>79</div></div><div><span>lqi        0.248   0.339   0.228   0.343   -0.1296   -1.0185</span></div></div><div><div><div>80</div></div><div>
</div></div><div><div><div>81</div></div><div><span>step-scenario cost sum (x'Qx + R tau^2) ts (substeps = 1)</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>cascade (tuned)                6.162</span></div></div><div><div><div>83</div></div><div><span><span>  </span></span><span>cascade (nearest to LQI)       6.318</span></div></div><div><div><div>84</div></div><div><span><span>  </span></span><span>lqr                            6.163</span></div></div><div><div><div>85</div></div><div><span><span>  </span></span><span>lqi                           10.783</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>push equilibrium for LQR: tau = -1.000 N m, phi = -0.1271 rad, x = 0.8259 m</span></div></div><div><div><div>88</div></div><div><span>cascade      30 s: x(10 s) = +0.8343, x(20 s) = +0.8344, x(30 s) = +0.8344, phi(30 s) = -0.1271</span></div></div><div><div><div>89</div></div><div><span>lqr          30 s: x(10 s) = +0.8260, x(20 s) = +0.8259, x(30 s) = +0.8259, phi(30 s) = -0.1271</span></div></div><div><div><div>90</div></div><div><span>lqi          30 s: x(10 s) = -0.0011, x(20 s) = +0.0000, x(30 s) = -0.0000, phi(30 s) = -0.1271</span></div></div><div><div><div>91</div></div><div><span>cascade_map  30 s: x(10 s) = +0.0446, x(20 s) = +0.0012, x(30 s) = +0.0000, phi(30 s) = -0.1271</span></div></div><div><div><div>92</div></div><div><span>cascade (nearest) vs state feedback with its equivalent K, push: max |dx| = 8.67e-05 m over 5 s</span></div></div><div><div><div>93</div></div><div>
</div></div><div><div><div>94</div></div><div><span>mass sweep (linear, fixed gains, no saturation): stable for s in</span></div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>lqr                        0.2 to 14.8 (contiguous)</span></div></div><div><div><div>96</div></div><div><span><span>  </span></span><span>lqi                        0.2 to 15.2 (contiguous)</span></div></div><div><div><div>97</div></div><div><span><span>  </span></span><span>cascade (tuned, no I)      0.2 to 13.2 (contiguous)</span></div></div><div><div><div>98</div></div><div><span><span>  </span></span><span>cascade (nearest to LQI)   0.2 to 15.2 (contiguous)</span></div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div><span>step, 30 s run (Python only)</span></div></div><div><div><div>101</div></div><div><span><span>  </span></span><span>cascade  peak x = 1.0268 at 5.04 s (overshoot 2.7%), x(30 s) = 1.00000</span></div></div><div><div><div>102</div></div><div><span><span>  </span></span><span>lqr      peak x = 1.0226 at 5.11 s (overshoot 2.3%), x(30 s) = 1.00000</span></div></div><div><div><div>103</div></div><div><span><span>  </span></span><span>lqi      peak x = 1.3884 at 3.19 s (overshoot 38.8%), x(30 s) = 1.00000</span></div></div><div><div><div>104</div></div><div><span>figure: figures/ch09/energy-check.svg</span></div></div><div><div><div>105</div></div><div><span>figure: figures/ch09/step-compare.svg</span></div></div><div><div><div>106</div></div><div><span>figure: figures/ch09/push-offset.svg</span></div></div><div><div><div>107</div></div><div><span>figure: figures/ch09/mass-mismatch.svg</span></div></div><div><div><div>108</div></div><div><span>figure: figures/ch09/gain-structure.svg</span></div></div><div><div><div>109</div></div><div>
</div></div><div><div><div>110</div></div><div><span>ch09 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>模型对得上吗<a href="#模型对得上吗"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch09-energy-check.svg" alt="平衡步兵无力矩自由倒下 2 秒：车身从 0.3 rad 一路摆过去，能量漂移在 h = 1e-4 时最大 1.06e-13，h = 1e-3 时 9.9e-10，都在 1e-6 的红线以下" /><figcaption>平衡步兵无力矩自由倒下 2 秒：车身从 0.3 rad 一路摆过去，能量漂移在 h = 1e-4 时最大 1.06e-13，h = 1e-3 时 9.9e-10，都在 1e-6 的红线以下</figcaption></figure><p></p><p>上图是原生程序启动时的自检（也是 Python 的 <code>energy_run</code>）：<span><span>φ0=0.3\varphi_0=0.3</span><span><span><span></span><span><span>φ</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span> rad，<span><span>τ=0\tau=0</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，没有阻尼，车身倒下去，0.4 s 过了水平，到 2 s 一路摆到了对面：<span><span>φ\varphi</span><span><span><span></span><span>φ</span></span></span></span> 的最大值是 5.9832 rad，正好是 <span><span>2π−0.32\pi-0.3</span><span><span><span></span><span>2</span><span>π</span><span></span><span>−</span><span></span></span><span><span></span><span>0.3</span></span></span></span>，也就是几乎回到起点的高度，这是能量守恒最直观的证据。相对能量漂移最大 <span><span>1.06×10−131.06\times10^{-13}</span><span><span><span></span><span>1.06</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，是舍入量级。步长放大 10 倍（<span><span>h=10−3h=10^{-3}</span><span><span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，等于每个控制周期只积分一步，不做子步）最大漂移是 <span><span>9.9×10−109.9\times10^{-10}</span><span><span><span></span><span>9.9</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，仍然在 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span> 以下，所以对这个对象，每个周期积分 10 步已经是很宽的余量，积分器不会是误差来源。</p><p>能量守恒只能说明 <span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 和 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 对应的运动方程自洽，说明不了广义力的符号。所以还有一个检查：施加 <span><span>τ=3sin⁡7t\tau=3\sin7t</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span><span></span><span>sin</span><span></span><span>7</span><span>t</span></span></span></span> 和 <span><span>F=5cos⁡3tF=5\cos3t</span><span><span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span>5</span><span></span><span>cos</span><span></span><span>3</span><span>t</span></span></span></span> 跑 3 s，把电机和外力做的功 <span><span>∫[τ(x˙/r−φ˙)+F(x˙+lcos⁡φ φ˙)]dt\int[\tau(\dot x/r-\dot\varphi)+F(\dot x+l\cos\varphi\,\dot\varphi)]dt</span><span><span><span></span><span>∫</span><span>[</span><span>τ</span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>/</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>F</span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)]</span><span>d</span><span>t</span></span></span></span> 当作第五个状态一起积分，最大的 <span><span>∣E−E0−W∣|E-E_0-W|</span><span><span><span></span><span>∣</span><span>E</span><span></span><span>−</span><span></span></span><span><span></span><span><span>E</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>W</span><span>∣</span></span></span></span> 是 <span><span>3.18×10−123.18\times10^{-12}</span><span><span><span></span><span>3.18</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> J。<span><span>τ=+1\tau=+1</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>+</span><span>1</span></span></span></span> 静止出发的加速度是 <span><span>x¨=+1.5054\ddot x=+1.5054</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>+</span><span>1.5054</span></span></span></span>、<span><span>φ¨=−5.2419\ddot\varphi=-5.2419</span><span><span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>5.2419</span></span></span></span>，和手推的 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的第二、第四项 <span><span>1.5051.505</span><span><span><span></span><span>1.505</span></span></span></span>、<span><span>−5.242-5.242</span><span><span><span></span><span>−</span><span>5.242</span></span></span></span> 一致，符号也对：向前滚，车身向后。<strong>模型的符号没有错。</strong></p></section><section><h3>增益和闭环极点<a href="#增益和闭环极点"><span>#</span></a></h3><p>LQR 的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>（<span><span>u=−Kxu=-Kx</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span></span></span></span>，状态 <span><span>x,x˙,φ,φ˙x,\dot x,\varphi,\dot\varphi</span><span><span><span></span><span>x</span><span>,</span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>,</span><span></span><span>φ</span><span>,</span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>）是 <span><span>(−19.35, −29.87, −133.63, −20.83)(-19.35,\ -29.87,\ -133.63,\ -20.83)</span><span><span><span></span><span>(</span><span>−</span><span>19.35</span><span>,</span><span> </span><span></span><span>−</span><span>29.87</span><span>,</span><span> </span><span></span><span>−</span><span>133.63</span><span>,</span><span> </span><span></span><span>−</span><span>20.83</span><span>)</span></span></span></span>，LQI 是 <span><span>(−46.34, −45.82, −150.39, −25.54, +19.34)(-46.34,\ -45.82,\ -150.39,\ -25.54,\ +19.34)</span><span><span><span></span><span>(</span><span>−</span><span>46.34</span><span>,</span><span> </span><span></span><span>−</span><span>45.82</span><span>,</span><span> </span><span></span><span>−</span><span>150.39</span><span>,</span><span> </span><span></span><span>−</span><span>25.54</span><span>,</span><span> </span><span></span><span>+</span><span>19.34</span><span>)</span></span></span></span>。倾角分量是 <span><span>−133.6-133.6</span><span><span><span></span><span>−</span><span>133.6</span></span></span></span>：倾 0.1 rad 就要 13 N·m，超过了 10 N·m 的上限，所以后面的 tilt 和 step 一开始都会撞限幅。四个闭环（LQR、LQI、整定后的串级、最接近 LQI 的串级）的离散特征值模都小于 1，最慢的极点：</p>

<table><thead><tr><th>闭环</th><th>最大 <span><span>∣λ∣\lvert\lambda\rvert</span><span><span><span></span><span>∣</span><span>λ</span><span>∣</span></span></span></span></th><th>最慢衰减</th><th>时间常数</th></tr></thead><tbody><tr><td>LQR</td><td>0.999158</td><td>0.842 /s</td><td>1.19 s</td></tr><tr><td>LQI</td><td>0.999235</td><td>0.765 /s</td><td>1.31 s</td></tr><tr><td>串级（整定后，无积分）</td><td>0.999185</td><td>0.816 /s</td><td>1.23 s</td></tr><tr><td>串级（最接近 LQI 的增益）</td><td>0.999642</td><td>0.358 /s</td><td>2.80 s</td></tr></tbody></table><p>最慢极点都在 1 s 以上，和 <span><span>Qx=4Q_x=4</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span>（位置只惩罚了 0.5 m 对应的权重）有关：位置回路在设计上就是慢的，这个时间常数决定了后面几乎所有的“5 秒里没到”。</p></section><section><h3>倾倒、阶跃：三个控制器几乎一样（除了 LQI）<a href="#倾倒阶跃三个控制器几乎一样除了-lqi"><span>#</span></a></h3><p><code>tilt</code>：<span><span>φ0=0.15\varphi_0=0.15</span><span><span><span></span><span><span>φ</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.15</span></span></span></span> rad，位置参考 0。三个控制器都先把力矩打满 10 N·m，然后拉回：</p>

<table><thead><tr><th>控制器</th><th>峰值 <span><span>∣τ∣\lvert\tau\rvert</span><span><span><span></span><span>∣</span><span>τ</span><span>∣</span></span></span></span></th><th>饱和时间</th><th>最大 <span><span>∣x∣\lvert x\rvert</span><span><span><span></span><span>∣</span><span>x</span><span>∣</span></span></span></span></th><th>5 s 末 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span></th><th><span><span>∣φ∣&lt;0.01\lvert\varphi\rvert&lt;0.01</span><span><span><span></span><span>∣</span><span>φ</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0.01</span></span></span></span> 的时刻</th></tr></thead><tbody><tr><td>串级</td><td>10.00 N·m</td><td>18 ms</td><td>0.082 m</td><td>−0.0020 m</td><td>1.049 s</td></tr><tr><td>LQR</td><td>10.00 N·m</td><td>18 ms</td><td>0.084 m</td><td>−0.0017 m</td><td>1.080 s</td></tr><tr><td>LQI</td><td>10.00 N·m</td><td>22 ms</td><td>0.075 m</td><td>−0.0090 m</td><td>0.973 s</td></tr></tbody></table><p>三个都稳。倾 0.15 rad 要 20 N·m 的线性反馈，实际只给 10，饱和了 18 到 22 ms，在不稳定极点 6.89 rad/s 下这点时间还救得回来。</p><p></p><figure><img src="/assets/images/pid-control/ch09-step-compare.svg" alt="阶跃 1 m：串级和 LQR 几乎重合，位置先反向凹下去 3 厘米再慢慢爬到 1；LQI 凹得更深，冲到 1.39 再回来，倾角峰值 0.30 rad，力矩打满的时间 45 ms" /><figcaption>阶跃 1 m：串级和 LQR 几乎重合，位置先反向凹下去 3 厘米再慢慢爬到 1；LQI 凹得更深，冲到 1.39 再回来，倾角峰值 0.30 rad，力矩打满的时间 45 ms</figcaption></figure><p></p><p><code>step</code>：0.5 s 时位置参考从 0 跳到 1 m。上图的三条曲线里串级（绿）和 LQR（蓝）几乎盖在一起：</p>

<table><thead><tr><th>控制器</th><th>超调（5 s 窗口）</th><th>超调（30 s）</th><th>最小 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>（凹坑）</th><th>5 s 末 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span></th><th>进入 ±5%</th><th>峰值 <span><span>∣φ∣\lvert\varphi\rvert</span><span><span><span></span><span>∣</span><span>φ</span><span>∣</span></span></span></span></th><th>饱和时间</th></tr></thead><tbody><tr><td>串级</td><td>2.7%</td><td>2.7%（5.04 s）</td><td>−0.033 m</td><td>1.027 m</td><td>3.52 s</td><td>0.135 rad</td><td>13 ms</td></tr><tr><td>LQR</td><td>2.2%</td><td>2.3%（5.11 s）</td><td>−0.032 m</td><td>1.022 m</td><td>3.55 s</td><td>0.134 rad</td><td>14 ms</td></tr><tr><td>LQI</td><td>38.8%</td><td>38.8%（3.19 s）</td><td>−0.072 m</td><td>1.132 m</td><td>5 s 内没有</td><td>0.299 rad</td><td>45 ms</td></tr></tbody></table><p>两个观察。<strong>凹坑是非最小相位。</strong> 前面推 <span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 的时候说了，<span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 对 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 和 <span><span>φ\varphi</span><span><span><span></span><span>φ</span></span></span></span> 的作用符号相反：LQR 在 0.665 s 的时候 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 凹到 <span><span>−0.032-0.032</span><span><span><span></span><span>−</span><span>0.032</span></span></span></span> m，力矩一开始是 <span><span>−10-10</span><span><span><span></span><span>−</span><span>10</span></span></span></span> N·m，让轮子向后滚、车身向前倾，然后车才往前去。这个凹坑是对象的性质，不是控制器的：任何增益都躲不掉（只能调大小），三个控制器都有。<strong>LQI 的 38.8% 超调是积分器的账。</strong> 我原本以为 LQI 只是 LQR 加了抗扰能力，阶跃成绩应该差不多，结果不是。阶跃之后位置要到 2 s 左右才越过 1 m，这 1.5 s 里位置误差一直是 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span> m，<span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 积了一大块，然后要靠“位置冲过头”来还。30 s 里它也回到了 1.00000，只是 5 秒的窗口看不到。LQI 的阶跃代价（<span><span>∑xTQx+Rτ2\sum x^TQx+R\tau^2</span><span><span><span></span><span>∑</span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>Q</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>R</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>）是 10.783，LQR 是 6.163，<strong>LQI 是 LQR 的 1.75 倍</strong>：积分作用买的是抗扰，不是阶跃的成绩。我没有去扫 <span><span>qzq_z</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 压这个超调，<span><span>qz=4q_z=4</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4</span></span></span></span> 是我设的第一个值，扫它是另一个实验。</p></section><section><h3>推一把：LQR 偏离 0.826 m，LQI 能拉回来，但 5 秒不够<a href="#推一把lqr-偏离-0826-mlqi-能拉回来但-5-秒不够"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch09-push-offset.svg" alt="2.0 秒起 10 N 的推力：上图 5 秒，串级和 LQR 一路漂到 0.8 m，LQI 在 3.81 s 到顶 0.343 m 后往回走；下图 30 秒，LQI 回到 0，串级若取最接近 LQI 的增益也回到 0，LQR 稳在 0.826 m" /><figcaption>2.0 秒起 10 N 的推力：上图 5 秒，串级和 LQR 一路漂到 0.8 m，LQI 在 3.81 s 到顶 0.343 m 后往回走；下图 30 秒，LQI 回到 0，串级若取最接近 LQI 的增益也回到 0，LQR 稳在 0.826 m</figcaption></figure><p></p><p><code>push</code>：2.0 s 起，一个 10 N 的水平力作用在车身质心上。要平衡住，稳态必须满足 <span><span>τ=−rF=−1\tau=-rF=-1</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>r</span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span> N·m（第一个运动方程），倾角由第二个方程 <span><span>Mglsin⁡φ+Flcos⁡φ=τMgl\sin\varphi+Fl\cos\varphi=\tau</span><span><span><span></span><span>M</span><span>g</span><span>l</span><span></span><span>sin</span><span></span><span>φ</span><span></span><span>+</span><span></span></span><span><span></span><span>F</span><span>l</span><span></span><span>cos</span><span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span></span></span></span> 定：<span><span>φ=−0.1271\varphi=-0.1271</span><span><span><span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.1271</span></span></span></span> rad，车身向后仰着顶住这股力。然后 LQR 的 <span><span>u=−Kxu=-Kx</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span></span></span></span> 要产生 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> N·m 的力矩，<span><span>−Kxx−Kφφ=−1-K_x x-K_\varphi\varphi=-1</span><span><span><span></span><span>−</span><span><span>K</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>，解出 <span><span>x=0.8259x=0.8259</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>0.8259</span></span></span></span> m。这个预言在 30 s 的仿真里得到了：LQR 的 <span><span>x(30 s)=0.8259x(30\text{ s})=0.8259</span><span><span><span></span><span>x</span><span>(</span><span>30</span><span><span> s</span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.8259</span></span></span></span>，<span><span>φ=−0.1271\varphi=-0.1271</span><span><span><span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.1271</span></span></span></span>，和预测一致到小数点后四位。</p>

<table><thead><tr><th>控制器</th><th><span><span>xx</span><span><span><span></span><span>x</span></span></span></span>@3 s</th><th><span><span>xx</span><span><span><span></span><span>x</span></span></span></span>@4 s</th><th><span><span>xx</span><span><span><span></span><span>x</span></span></span></span>@5 s</th><th>最大 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span></th><th><span><span>φ\varphi</span><span><span><span></span><span>φ</span></span></span></span>@5 s</th><th><span><span>xx</span><span><span><span></span><span>x</span></span></span></span>@10 s</th><th><span><span>xx</span><span><span><span></span><span>x</span></span></span></span>@30 s</th></tr></thead><tbody><tr><td>串级（整定后）</td><td>0.293</td><td>0.632</td><td>0.802</td><td>0.802</td><td>−0.1469</td><td>0.8343</td><td>0.8344</td></tr><tr><td>LQR</td><td>0.292</td><td>0.625</td><td>0.791</td><td>0.791</td><td>−0.1462</td><td>0.8260</td><td>0.8259</td></tr><tr><td>LQI</td><td>0.248</td><td>0.339</td><td>0.228</td><td>0.343</td><td>−0.1296</td><td>−0.0011</td><td>0.0000</td></tr><tr><td>串级（最接近 LQI）</td><td>无</td><td>无</td><td>无</td><td>无</td><td>无</td><td>0.0446</td><td>0.0000</td></tr></tbody></table><p>最后一行只在 Python 里跑了 30 s，不在 <code>out/ch09/</code> 的 CSV 里。<strong>LQI 确实能去除偏差，但要很久。</strong> 5 秒的窗口里，LQI 的 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 在 3.81 s 到顶 0.343 m，到 5 s 回到 0.228 m，还没有到 0。它最慢的闭环极点的时间常数是 1.31 s，推力在 2.0 s 才来，只剩 3 s，不到 2.3 个时间常数，不够。30 s 的仿真才看见零偏差：10 s 时 <span><span>−0.0011-0.0011</span><span><span><span></span><span>−</span><span>0.0011</span></span></span></span> m，20 s 后 <span><span>0.00000.0000</span><span><span><span></span><span>0.0000</span></span></span></span> m（<code>report</code> 在 30 s 处断言 <span><span>∣x∣&lt;10−3|x|&lt;10^{-3}</span><span><span><span></span><span>∣</span><span>x</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>）。同时 5 s 窗口里我断言了 LQI 的 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 小于 LQR 的 0.35 倍（实际 0.228 对 0.791，是 0.29 倍）。</p><p><strong>整定好的串级 PID 没有消除偏差，和 LQR 一样。</strong> Nelder–Mead 把积分增益 <span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 整定到了 <span><span>1.5×10−121.5\times10^{-12}</span><span><span><span></span><span>1.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span>，等于删掉了积分。原因我想得到：优化的目标是 <code>step</code> 算例的代价，这个场景里没有外力，积分项对代价没有任何好处，只会带来超调，优化器就把它关了。串级的结构有积分，整定的目标把它关了，这是我选目标的问题，不是串级的问题。最后一行是反证：把串级的增益换成“最接近 LQI”的那一组（<span><span>ki=0.1541k_i=0.1541</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1541</span></span></span></span>），它也能把偏差拉回 0（<span><span>x(10 s)=0.0446x(10\text{ s})=0.0446</span><span><span><span></span><span>x</span><span>(</span><span>10</span><span><span> s</span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0446</span></span></span></span>，<span><span>x(30 s)=0.0000x(30\text{ s})=0.0000</span><span><span><span></span><span>x</span><span>(</span><span>30</span><span><span> s</span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0000</span></span></span></span>），比 LQI 慢一点，因为 <span><span>KzK_z</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 只有 11.72。这一组的串级和它的等价状态反馈在 <code>push</code> 上的位置最大差 <span><span>8.67×10−58.67\times10^{-5}</span><span><span><span></span><span>8.67</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span> m（5 秒里最大位移是 0.34 m），验证了上面“串级就是状态反馈”的推导；这点差别来自 D 项：串级里是 1 ms 的后向差分，状态反馈里是真的 <span><span>φ˙\dot\varphi</span><span><span><span></span><span><span><span><span><span><span></span><span>φ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，所以只有在 D 项读陀螺仪的时候两者才严格等价，阶跃场景里积分器的初值还差一个常数（前面推导里说过）。</p></section><section><h3>质量对不上：比我预期的温和得多<a href="#质量对不上比我预期的温和得多"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch09-mass-mismatch.svg" alt="真实质量放大到 1.3 倍：三个控制器的阶跃曲线和标称质量的几乎重合，超调只差 0.4 到 0.9 个百分点" /><figcaption>真实质量放大到 1.3 倍：三个控制器的阶跃曲线和标称质量的几乎重合，超调只差 0.4 到 0.9 个百分点</figcaption></figure><p></p><p><code>mass130</code>：控制器按 <span><span>M=12M=12</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span> kg 设计，对象的车身质量是 <span><span>1.3×1.3\times</span><span><span><span></span><span>1.3</span><span>×</span></span></span></span>。三个控制器的阶跃响应和标称质量几乎重合：串级的超调 2.1%（标称 2.7%），LQR 1.8%（2.3%），LQI 37.9%（38.8%），最小 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 差 0.003 到 0.007 m，5 s 末的 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 差 0.001 到 0.006 m。我预想质量不对会在 LQR 和串级之间分出胜负，<strong>没有分出来</strong>。线性的稳定范围解释了为什么：把真实质量扫成 <span><span>s×12s\times12</span><span><span><span></span><span>s</span><span></span><span>×</span><span></span></span><span><span></span><span>12</span></span></span></span> kg，固定增益，看离散闭环是否稳定，LQR 在 <span><span>s∈[0.2, 14.8]s\in[0.2,\,14.8]</span><span><span><span></span><span>s</span><span></span><span>∈</span><span></span></span><span><span></span><span>[</span><span>0.2</span><span>,</span><span></span><span></span><span>14.8</span><span>]</span></span></span></span> 内稳定，LQI 到 15.2，整定后的串级到 13.2，最接近 LQI 的串级到 15.2。1.3 在这个范围的最近一端的十倍以外。这是线性、无饱和的结论，不是说真车上质量差 10 倍也没事（那时候力矩早就饱和了），我只想说：<span><span>1.3×1.3\times</span><span><span><span></span><span>1.3</span><span>×</span></span></span></span> 离这个设计的失稳边界很远，失配实验在这个强度上没有信息量。</p></section><section><h3>增益的结构：串级够不到 LQI<a href="#增益的结构串级够不到-lqi"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch09-gain-structure.svg" alt="五个增益分量的对比：整定后的串级（绿）的 K_z 是 0，前四个分量约是 LQR 的 0.87 倍；最接近 LQI 的串级（黄）的前四个分量和 LQI（红）完全一样，K_z 只有 11.7 对 19.3" /><figcaption>五个增益分量的对比：整定后的串级（绿）的 K_z 是 0，前四个分量约是 LQR 的 0.87 倍；最接近 LQI 的串级（黄）的前四个分量和 LQI（红）完全一样，K_z 只有 11.7 对 19.3</figcaption></figure><p></p><p>纵轴是 symlog，所以 <span><span>KzK_z</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 看起来矮，其实 19.3 和 <span><span>−150-150</span><span><span><span></span><span>−</span><span>150</span></span></span></span> 是同一个数量级的东西。三根柱子：红的是 LQI 的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>；黄的是“最接近 LQI 的串级”的等价 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>，前四个分量和红的一样，只有 <span><span>KzK_z</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 不同（11.72 对 19.34），因为那个二次方程没有实根（判别式 <span><span>−0.0618-0.0618</span><span><span><span></span><span>−</span><span>0.0618</span></span></span></span>）；绿的是 Nelder–Mead 整定后的串级，<span><span>K=(−17.02, −26.00, −119.6, −17.76, 0)K=(-17.02,\ -26.00,\ -119.6,\ -17.76,\ 0)</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>17.02</span><span>,</span><span> </span><span></span><span>−</span><span>26.00</span><span>,</span><span> </span><span></span><span>−</span><span>119.6</span><span>,</span><span> </span><span></span><span>−</span><span>17.76</span><span>,</span><span> </span><span></span><span>0</span><span>)</span></span></span></span>，和 LQR 的 <span><span>(−19.35, −29.87, −133.63, −20.83)(-19.35,\ -29.87,\ -133.63,\ -20.83)</span><span><span><span></span><span>(</span><span>−</span><span>19.35</span><span>,</span><span> </span><span></span><span>−</span><span>29.87</span><span>,</span><span> </span><span></span><span>−</span><span>133.63</span><span>,</span><span> </span><span></span><span>−</span><span>20.83</span><span>)</span></span></span></span> 的比值是 0.88、0.87、0.90、0.85，<strong>整定后的串级大约是 LQR 的 0.87 倍缩放</strong>。<code>step</code> 的代价 6.162 对 LQR 的 6.163，优化器没有找到比 LQR 更好的东西（在线性、无饱和、无限时域的意义上 LQR 就是最优的，不可能被超过），差的这 0.001 我猜来自力矩饱和和 5 s 的截断，没有去验证。</p><p>所以我对开头那个问题的回答是：<strong>串级 PID 和状态反馈的差别不在“多不多维”，在“增益是怎么来的”。</strong> 展开之后它就是 <span><span>u=−Kx−kzzu=-Kx-k_zz</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span></span></span></span>，五个自由参数；LQR 的好处是增益从一个代价函数里算出来，改一个权重（比如把 <span><span>QφQ_\varphi</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>φ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 放大一倍）重新算一遍就好，串级要重新整定五个互相耦合的增益。这一次实验里串级的表现不比 LQR 差，是因为我让优化器去优化同一个代价。<strong>如果让一个人手调这五个增益，我不知道结果会怎样</strong>，我没有做这个实验。</p></section><section><h3>这些结果的边界<a href="#这些结果的边界"><span>#</span></a></h3><ul>
<li><strong>所有对象参数都是我取的量级。</strong> <span><span>MM</span><span><span><span></span><span>M</span></span></span></span>、<span><span>ll</span><span><span><span></span><span>l</span></span></span></span>、<span><span>IbI_b</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>mwm_w</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>IwI_w</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 和 10 N·m 的力矩上限都是假设，不是实测。没有轮子和地面的摩擦，没有电机的电流环动态（力矩指令即力矩），没有编码器和陀螺仪的噪声，也没有第 7 篇那样的延迟。真车上这些加起来，LQR 的 1 kHz 反馈就未必能站得这么稳。</li>
<li><strong>只有一个推力、一个初始倾角、一个质量失配。</strong> 所有数都是各一个场景，我没有扫。</li>
</ul></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 pid-lab 的根目录运行（<code>nix develop</code> 里，或者你自己装好依赖）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch09</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出的结构就是上面“结果”里那一大段：三个原生程序各打印一次能量自检（漂移 <span><span>1.063×10−131.063\times10^{-13}</span><span><span><span></span><span>1.063</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>）和 DARE 迭代次数（LQR 13067，LQI 15178），然后是 42 行 <code>ok</code>（最大差异 <span><span>5.06×10−135.06\times10^{-13}</span><span><span><span></span><span>5.06</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>），然后是 <code>report</code> 的表，最后一行 <code>ch09 OK</code>。<code>report</code> 里的断言全部通过，包括：能量漂移小于 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span>，<code>expm</code> 对 <code>scipy.linalg.expm</code> 的相对误差小于 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span>，Riccati 迭代对 <code>solve_discrete_are</code> 小于 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，LQI 在 30 s 的 <code>push</code> 里 <span><span>∣x∣&lt;10−3|x|&lt;10^{-3}</span><span><span><span></span><span>∣</span><span>x</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p><strong>这一篇新增或依赖的文件</strong>（<code>data/ch09_*.csv</code> 和 <code>figures/ch09/*.svg</code> 由 <code>make ch09</code> 生成，不在仓库里）：</p><p>Python：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/mat.py" target="_blank"><code>python/pidlab/mat.py</code></a></td><td><code>make -s ch09</code></td><td>小矩阵运算，被 <code>lqr.py</code> 使用</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/lqr.py" target="_blank"><code>python/pidlab/lqr.py</code></a></td><td><code>make -s ch09</code></td><td><code>expm</code> 对 scipy 相对误差 <span><span>2.51×10−132.51\times10^{-13}</span><span><span><span></span><span>2.51</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>；DARE 13067 步（LQI 15178 步），对 <code>solve_discrete_are</code> <span><span>6.53×10−106.53\times10^{-10}</span><span><span><span></span><span>6.53</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a></td><td><code>make -s ch09</code></td><td><code>Wip</code>：能量漂移 <span><span>1.063×10−131.063\times10^{-13}</span><span><span><span></span><span>1.063</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>AA</span><span><span><span></span><span>A</span></span></span></span>、<span><span>BB</span><span><span><span></span><span>B</span></span></span></span> 对中心差分 <span><span>4.47×10−114.47\times10^{-11}</span><span><span><span></span><span>4.47</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>11</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid.py" target="_blank"><code>python/pidlab/pid.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>python/pidlab/sim.py</code></a></td><td><code>make -s ch09</code></td><td>第 5 篇的 <code>Pid</code> 和第 1 篇的 RK4 积分，这一篇没有改</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch09_lqr.py" target="_blank"><code>python/ch09_lqr.py</code></a></td><td><code>python3 python/ch09_lqr.py ref</code> 再 <code>report</code>（<code>make -s ch09</code> 会依次跑）</td><td>写 <code>data/</code> 和 <code>out/ch09/*_py.csv</code>；<span><span>KLQR=(−19.35,−29.87,−133.63,−20.83)K_{LQR}=(-19.35,-29.87,-133.63,-20.83)</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span><span>L</span><span>QR</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>19.35</span><span>,</span><span></span><span>−</span><span>29.87</span><span>,</span><span></span><span>−</span><span>133.63</span><span>,</span><span></span><span>−</span><span>20.83</span><span>)</span></span></span></span>，Nelder–Mead 932 次评估，代价 6.318 到 6.162；5 张图</td></tr></tbody></table><p>C：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/mat.h" target="_blank"><code>c/include/pl/mat.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/mat.c" target="_blank"><code>c/src/mat.c</code></a></td><td><code>make -s ch09</code></td><td>平铺行优先矩阵，维数 <span><span>≤\le</span><span><span><span></span><span>≤</span></span></span></span> <code>PL_MAX_DIM</code>，没有堆</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/lqr.h" target="_blank"><code>c/include/pl/lqr.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/lqr.c" target="_blank"><code>c/src/lqr.c</code></a></td><td><code>make -s ch09</code></td><td>DARE 13067 和 15178 步，与 Python 一步不差</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/plants.h" target="_blank"><code>c/include/pl/plants.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/plants.c" target="_blank"><code>c/src/plants.c</code></a></td><td><code>make -s ch09</code></td><td><code>pl_wip_deriv</code>、<code>pl_wip_energy</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch09.c" target="_blank"><code>c/apps/ch09.c</code></a></td><td><code>make -s ch09</code></td><td><code>ch09: C wrote 12 cases to out/ch09</code>，14 个 <code>ok</code></td></tr></tbody></table><p>C++：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/mat.h" target="_blank"><code>cpp/include/pidlab/mat.h</code></a></td><td><code>make -s ch09</code></td><td><code>Mat&lt;T, R, C&gt;</code>，维数在类型里</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/lqr.h" target="_blank"><code>cpp/include/pidlab/lqr.h</code></a></td><td><code>make -s ch09</code></td><td><code>Expm</code>、<code>C2dZoh</code>、<code>DareIterate</code>、<code>LqiAugment</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/plants.h" target="_blank"><code>cpp/include/pidlab/plants.h</code></a></td><td><code>make -s ch09</code></td><td><code>Wip&lt;T&gt;</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch09.cc" target="_blank"><code>cpp/apps/ch09.cc</code></a></td><td><code>make -s ch09</code></td><td><code>ch09: C++ wrote 12 cases to out/ch09</code>，14 个 <code>ok</code></td></tr></tbody></table><p>Rust：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/mat.rs" target="_blank"><code>rust/src/mat.rs</code></a></td><td><code>make -s ch09</code></td><td><code>Mat&lt;N, M&gt;</code> 加 const 泛型</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/lqr.rs" target="_blank"><code>rust/src/lqr.rs</code></a></td><td><code>make -s ch09</code></td><td><code>c2d_zoh::&lt;4, 5&gt;</code> 和 <code>lqi_augment</code> 多一个 <code>M = N + 1</code> 参数</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/plants.rs" target="_blank"><code>rust/src/plants.rs</code></a></td><td><code>make -s ch09</code></td><td><code>Wip</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch09.rs" target="_blank"><code>rust/src/bin/ch09.rs</code></a></td><td><code>make -s ch09</code></td><td><code>ch09: Rust wrote 12 cases to out/ch09</code>，14 个 <code>ok</code>；最大差异 <span><span>5.06×10−135.06\times10^{-13}</span><span><span><span></span><span>5.06</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> 出现在这里的 <code>push_cascade_rs.csv</code></td></tr></tbody></table><p>构建入口：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/Makefile" target="_blank"><code>Makefile</code></a> 的 <code>ch09</code> 目标，<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/Makefile" target="_blank"><code>c/Makefile</code></a> 和 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/Makefile" target="_blank"><code>cpp/Makefile</code></a> 会自动编译 <code>apps/</code> 下的新文件。</p></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把平衡步兵的拉格朗日模型写成了四种语言里的 <code>Wip</code>（能量漂移 <span><span>1.06×10−131.06\times10^{-13}</span><span><span><span></span><span>1.06</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>），把 LQR 和 LQI 的整条链（矩阵指数、ZOH、Riccati 迭代、增广）写了四遍，三种语言的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 和 Python 逐位一致，DARE 迭代次数一步不差。LQR 在 10 N 的推力下稳态偏离 0.826 m，LQI 在 30 s 内拉回 0（5 s 内没拉完），代价是阶跃超调 38.8%（LQR 2.3%）；串级 PID 被整定成了 LQR 的 0.87 倍缩放（代价 6.162 对 6.163），LQI 的增益向量落在串级能表达的范围之外（判别式 <span><span>−0.0618-0.0618</span><span><span><span></span><span>−</span><span>0.0618</span></span></span></span>）。四种语言 42 个对比文件最大差异 <span><span>5.06×10−135.06\times10^{-13}</span><span><span><span></span><span>5.06</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p>第 10 篇是最后一篇：前九篇所有的控制器都跑在 <code>double</code> 上，真车上的 C 板没有这个余裕。我会把第 5 篇的 <code>Pid</code> 搬到 <code>float</code>，再搬到 Q16.16 定点，让它在一个会停住的积分器、整数化的 CAN 量化、抖动的采样周期下各跑一遍，看每一步丢了什么。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-08-mimo/</id>
      <title type="text">多变量：耦合、RGA、解耦与麦轮底盘</title>
      <published>2026-10-16T07:00:00.000Z</published>
      <updated>2026-10-16T07:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-08-mimo/"/>
      <summary type="text">PID 控制系列第 8 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95，把 2×2 的相对增益阵列（RGA）和静态解耦器、麦轮的运动学（最小范数电流分配、零空间分量）、Wood–Berry 精馏塔和麦轮底盘两个新对象各写了四遍，控制器全用第 5 篇的 Pid。Wood–Berry 的 λ11 = 2.0094，按对角配对的两个 SIMC PI 能用，配成非对角（λ12 = −1.0094）则发散，|y2| 每 13.9 分钟翻一倍；静态解耦把 y2 阶跃的 IAE 从 11.87 降到 7.29，却把 y1 的超调从 17.2% 推到 65.6%。麦轮底盘撞上轮速上限时，逐轮截断让零空间电流 nᵀi 的均方根达到 28.54 A，(vx, vy) 的方向偏了 11.98°，2 s 末位置偏离理想路径 2.18 m；逐轮等比缩放和车体系 PI 的零空间电流只有 3e-13 A 以下，方向偏差 0.001° 以内。四种语言 21 个对比文件最大差异不超过 2.2e-16。</summary>
      <content type="html"><![CDATA[<p>我的底盘代码里有四个 PID，每个轮子一个，互相不认识。四个电机、一个麦轮底盘，我一直这么写，也一直能开。但我从来没问过：<strong>这四个互不相识的 PID，谁在保证“四个轮子别打架”？</strong> 这一篇我去量这个问题的答案。先是一个比底盘更经典的耦合对象，Wood–Berry 精馏塔，用它把“多变量”三个字拆开：RGA 怎么判断两个输入该配哪个输出，静态解耦器能做什么、做不到什么。然后回到麦轮：四个轮子只推三个自由度，多出来的那一维什么也不推，电流照样流。同一份底盘仿真里，我把“轮速超限了怎么办”换成三种写法，其中一种让四个轮子在 1 s 之后平均各吃 14.7 A，另外两种是 4.1 A，而且前者的车根本没有走到目标。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch08</code> 通过，<code>out/ch08/</code> 下 28 个 CSV（7 个算例，每个 Python、C、C++、Rust 各一份），<code>figures/ch08/</code> 下 4 张图；以及 RGA、静态解耦、麦轮运动学和零空间电流的推导与实验。
新增模块：Python <code>pidlab.mimo</code>，C <code>pl/mimo.h</code> 和 <code>src/mimo.c</code>，C++ <code>pidlab/mimo.h</code>，Rust <code>mimo.rs</code>（2×2 的求逆、RGA、静态解耦，麦轮的 H 矩阵、最小范数电流、正运动学、零空间分量）。新增对象 <code>WoodBerry</code> 和 <code>Mecanum</code>（四种语言的 <code>plants</code> 里）。控制器仍然全是第 5 篇的 <code>Pid</code>，整定用第 6 篇的 <code>simc_pi</code>，没有新的 PID 变体。
前置：<a href="/posts/pid-control-06-tuning/">第 6 篇</a>和<a href="/posts/pid-control-07-cascade/">第 7 篇</a>。需要的数学：第 6 篇的 SIMC，第 5 篇的 <code>hold_integral</code>，线性代数里的矩阵求逆、伪逆和零空间。</p></div></div>
<section><h2>Wood–Berry：耦合、RGA 与解耦<a href="#woodberry耦合rga-与解耦"><span>#</span></a></h2><section><h3>两个输入、两个输出：耦合是什么<a href="#两个输入两个输出耦合是什么"><span>#</span></a></h3><p>多变量对象的最小例子是 2×2：</p><span><span><span>[y1y2]=[g11(s)g12(s)g21(s)g22(s)][u1u2]\begin{bmatrix}y_1\\y_2\end{bmatrix}=
\begin{bmatrix}g_{11}(s)&amp;g_{12}(s)\\g_{21}(s)&amp;g_{22}(s)\end{bmatrix}
\begin{bmatrix}u_1\\u_2\end{bmatrix}</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span></span></span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span></span></span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>前七篇的做法放大一倍就是<strong>分散控制</strong>：<span><span>u1=C1(r1−y1)u_1 = C_1(r_1 - y_1)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>r</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>u2=C2(r2−y2)u_2 = C_2(r_2 - y_2)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>r</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，两个 PID 各管各的（出了事互相甩锅）。耦合是 <span><span>g12g_{12}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>g21g_{21}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 不为零：<span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 会推动 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，反过来也一样。类比一下：一个淋浴器有冷水和热水两个龙头，目标是水温和水量，每个龙头都同时改变这两个量，两个人各管一个龙头、各自盯着自己的那一个量，就是分散控制。</p><p>耦合到底会让回路 1 看到什么？看稳态。回路 2 开着的时候，<span><span>y1=g11u1y_1 = g_{11}u_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。回路 2 闭上并且带积分，稳态下 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的偏差被拉回 0，<span><span>0=g21u1+g22u20 = g_{21}u_1 + g_{22}u_2</span><span><span><span></span><span>0</span><span></span><span>=</span><span></span></span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，即 <span><span>u2=−g21u1/g22u_2 = -g_{21}u_1/g_{22}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，代入 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>y1=(g11−g12g21g22)u1=g11(1−κ) u1,κ=g12g21g11g22y_1 = \Bigl(g_{11} - \frac{g_{12}g_{21}}{g_{22}}\Bigr)u_1 = g_{11}(1-\kappa)\,u_1,
\qquad \kappa = \frac{g_{12}g_{21}}{g_{11}g_{22}}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>(</span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>κ</span><span>)</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>κ</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>另一个回路一闭，回路 1 的增益就从 <span><span>g11g_{11}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 变成了 <span><span>g11(1−κ)g_{11}(1-\kappa)</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>κ</span><span>)</span></span></span></span>。你在回路 2 开着的时候整定的 PI，到回路 2 闭上之后用的是另一个对象。</p></section><section><h3>RGA：把这个比值做成一张表<a href="#rga把这个比值做成一张表"><span>#</span></a></h3><p><a href="https://doi.org/10.1109/TAC.1966.1098266" target="_blank">Bristol 1966 年</a>提出的相对增益，就是上面这个比值，<strong>开环增益除以“其余回路闭环时的增益”</strong>：</p><span><span><span>λ11=g11g11(1−κ)=11−κ=g11g22g11g22−g12g21\lambda_{11} = \frac{g_{11}}{g_{11}(1-\kappa)} = \frac{1}{1-\kappa}
= \frac{g_{11}g_{22}}{g_{11}g_{22} - g_{12}g_{21}}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>1</span><span></span><span>−</span><span></span><span>κ</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span>κ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>原文只有两页（IEEE TAC 第 11 卷第 1 期，133 到 134 页），我没有找到能读的全文，所以这里的定义和性质是我自己推的，再用 numpy 核对。2×2 的情况每行每列的和都是 1：<span><span>λ12=λ21=1−λ11=−g12g21/det⁡G\lambda_{12} = \lambda_{21} = 1 - \lambda_{11} = -g_{12}g_{21}/\det G</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span></span><span>det</span><span></span><span>G</span></span></span></span>，<span><span>λ22=λ11\lambda_{22} = \lambda_{11}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。<span><span>n×nn\times n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>n</span></span></span></span> 的一般形式是 <span><span>Λ=G∘(G−1)T\Lambda = G\circ(G^{-1})^T</span><span><span><span></span><span>Λ</span><span></span><span>=</span><span></span></span><span><span></span><span>G</span><span></span><span>∘</span><span></span></span><span><span></span><span>(</span><span><span>G</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（逐元素乘），<code>report</code> 用 <code>numpy.linalg.inv</code> 把它算了一遍，和我手写的 2×2 公式一致。</p><p>怎么读：<span><span>λij\lambda_{ij}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 说的是“<span><span>uju_j</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 配 <span><span>yiy_i</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 这一对，在别的回路都闭上之后，增益变成原来的 <span><span>1/λij1/\lambda_{ij}</span><span><span><span></span><span>1/</span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>”。</p><ul>
<li><span><span>λij≈1\lambda_{ij}\approx1</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span></span></span></span>：别的回路闭不闭，这一对的增益都差不多，可以配。</li>
<li><span><span>λij≫1\lambda_{ij}\gg1</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≫</span><span></span></span><span><span></span><span>1</span></span></span></span>：别的回路一闭，这一对的增益大幅缩水，两个回路在互相牵制。</li>
<li><span><span>λij&lt;0\lambda_{ij}&lt;0</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span>：别的回路一闭，这一对的增益<strong>变号</strong>。据我所知，“别配 RGA 为负的那一对”是这套经验规则里最硬的一条：你按开环符号整定的积分控制器，在另一个回路闭上之后成了正反馈。我不在这里证明它，下面的实验会直接给出结果。</li>
</ul><p>代码就是上面那两个公式。2×2 阵用长度为 4 的行优先列表 <code>[g11, g12, g21, g22]</code>，四种语言布局相同：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>rga2</span><span>(</span><span>g</span><span>:</span><span> list[</span><span>float</span><span>]</span><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Relative gain array [l11, l12, l21, l22] of a 2x2 gain matrix.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>l11 = g11 g22 / det, l12 = -g12 g21 / det; rows and columns sum to 1.</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> </span><span>det2</span><span>(g)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> </span><span>abs</span><span><span>(d) </span><span>&lt;</span><span> </span></span><span>1e-12</span><span>:</span></div></div><div><div><div>8</div></div><div><span>        </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"singular 2x2 matrix"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>l11 </span><span>=</span><span> g[</span><span>0</span><span><span>] </span><span>*</span><span> g[</span></span><span>3</span><span><span>] </span><span>/</span><span> d</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>l12 </span><span>=</span><span> </span><span>-</span><span>g[</span><span>1</span><span><span>] </span><span>*</span><span> g[</span></span><span>2</span><span><span>] </span><span>/</span><span> d</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> [l11, l12, l12, l11]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只算 <span><span>λ11\lambda_{11}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>λ12\lambda_{12}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，另外两项靠“行列和为 1”直接复制。行列式接近 0 的时候没有意义，所以 Python 抛 <code>ValueError</code>，下面的 C、C++、Rust 各用自己的惯用办法报错。</p></section><section><h3>对象：Wood–Berry 精馏塔<a href="#对象woodberry-精馏塔"><span>#</span></a></h3><p><a href="https://ui.adsabs.harvard.edu/abs/1973ChEnS..28.1707W/abstract" target="_blank">Wood 和 Berry 1973 年</a>在一个中试规模的二元精馏塔上做了塔顶、塔底成分的控制研究。后来的教材和论文常拿这个塔的 2×2 传递函数矩阵做多变量控制的例子，时间单位是分钟：</p><span><span><span>G(s)=[12.8e−s16.7s+1−18.9e−3s21s+16.6e−7s10.9s+1−19.4e−3s14.4s+1]G(s)=\begin{bmatrix}
\dfrac{12.8e^{-s}}{16.7s+1} &amp; \dfrac{-18.9e^{-3s}}{21s+1}\\[2ex]
\dfrac{6.6e^{-7s}}{10.9s+1} &amp; \dfrac{-19.4e^{-3s}}{14.4s+1}
\end{bmatrix}</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>16.7</span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>12.8</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>10.9</span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>6.6</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span><span>s</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>21</span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>−</span><span>18.9</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>s</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>14.4</span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>−</span><span>19.4</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span><span>s</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>要交代一件事：这组数是我从常见的转引版本里取的，没有对照 1973 年原文的表格逐项核对。它在这一篇里的角色是“一个 RGA 约等于 2 的耦合对象”，我只要它自洽。</p><p>采样周期取 <span><span>ts=0.1t_s = 0.1</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span> 分钟，四个通道的延迟是 10、30、70、30 个采样。实现上把每个通道拆成“一阶滞后加一条 <code>DelayLine</code>”，一共四个状态、四条延迟线：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>WoodBerry</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>k: </span><span>tuple</span><span><span> </span><span>=</span><span> (</span></span><span>12.8</span><span><span>, </span><span>-</span></span><span>18.9</span><span>, </span><span>6.6</span><span><span>, </span><span>-</span></span><span>19.4</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>tau: </span><span>tuple</span><span><span> </span><span>=</span><span> (</span></span><span>16.7</span><span>, </span><span>21.0</span><span>, </span><span>10.9</span><span>, </span><span>14.4</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>delay: </span><span>tuple</span><span><span> </span><span>=</span><span> (</span></span><span>10</span><span>, </span><span>30</span><span>, </span><span>70</span><span>, </span><span>30</span><span>)  </span><span># samples of 0.1 min</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span> [(</span><span>self</span><span><span>.k[i] </span><span>*</span><span> u[i] </span><span>-</span><span> x[i]) </span><span>/</span><span> </span></span><span>self</span><span>.tau[i] </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)]</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>def</span><span> </span><span>outputs</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>):</span></div></div><div><div><div>11</div></div><div><span>        </span><span>return</span><span> [x[</span><span>0</span><span><span>] </span><span>+</span><span> x[</span></span><span>1</span><span>], x[</span><span>2</span><span><span>] </span><span>+</span><span> x[</span></span><span>3</span><span>]]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>通道顺序是 <span><span>g11,g12,g21,g22g_{11}, g_{12}, g_{21}, g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。延迟不在对象里，由仿真循环用 <code>DelayLine</code> 实现，和第 6 篇的 <code>Flywheel</code> 一样。<span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 同时进两个通道（<span><span>g11g_{11}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>g21g_{21}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>），所以 <span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 要分别推进两条延迟线（延迟 10 和 70），<span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 同理。</p><p>直流增益矩阵 <span><span>G(0)G(0)</span><span><span><span></span><span>G</span><span>(</span><span>0</span><span>)</span></span></span></span> 的行列式是 <span><span>12.8×(−19.4)−(−18.9)(6.6)=−123.5812.8\times(-19.4) - (-18.9)(6.6) = -123.58</span><span><span><span></span><span>12.8</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>−</span><span>19.4</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>−</span><span>18.9</span><span>)</span><span>(</span><span>6.6</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>123.58</span></span></span></span>，所以</p><span><span><span>λ11=12.8×(−19.4)−123.58=2.0094,Λ=[2.0094−1.0094−1.00942.0094]\lambda_{11} = \frac{12.8\times(-19.4)}{-123.58} = 2.0094,\qquad
\Lambda=\begin{bmatrix}2.0094&amp;-1.0094\\-1.0094&amp;2.0094\end{bmatrix}</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>123.58</span></span></span><span><span></span><span></span></span><span><span></span><span><span>12.8</span><span></span><span>×</span><span></span><span>(</span><span>−</span><span>19.4</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.0094</span><span>,</span><span></span><span></span><span>Λ</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2.0094</span></span></span><span><span></span><span><span>−</span><span>1.0094</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1.0094</span></span></span><span><span></span><span><span>2.0094</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>手算 <span><span>λ11=2.0094\lambda_{11}=2.0094</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.0094</span></span></span></span>，<code>report</code> 里断言 <span><span>∣λ11−2.0094∣&lt;10−3|\lambda_{11} - 2.0094| &lt; 10^{-3}</span><span><span><span></span><span>∣</span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>2.0094∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，代码给出的是同一个数。读这张表：对角配对 <span><span>λ11=2.01\lambda_{11} = 2.01</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.01</span></span></span></span>，回路 2 一闭，回路 1 的直流增益从 12.8 掉到 6.37，也就是 <span><span>g11/λ11g_{11}/\lambda_{11}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（<code>report</code> 同时用 <span><span>g11−g12g21/g22g_{11} - g_{12}g_{21}/g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 直接算，两个数相同）；非对角配对 <span><span>λ12=−1.0094\lambda_{12} = -1.0094</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1.0094</span></span></span></span>，<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 <span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的增益在另一个回路闭上之后从 <span><span>−18.9-18.9</span><span><span><span></span><span>−</span><span>18.9</span></span></span></span> 变成 <span><span>+18.72+18.72</span><span><span><span></span><span>+</span><span>18.72</span></span></span></span>，符号反了。</p></section><section><h3>分散 PI：SIMC 逐通道<a href="#分散-pisimc-逐通道"><span>#</span></a></h3><p>对角配对（<span><span>u1→y1u_1\to y_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>→</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>u2→y2u_2\to y_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>→</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）用第 6 篇的 SIMC，<span><span>τc=θ\tau_c = \theta</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>θ</span></span></span></span>。每个通道本身就是一阶加延迟，<span><span>kk</span><span><span><span></span><span>k</span></span></span></span>、<span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span>、<span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 直接读表，<code>simc_pi</code> 算出来：</p>

<table><thead><tr><th>通道</th><th><span><span>kk</span><span><span><span></span><span>k</span></span></span></span></th><th><span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> (min)</th><th><span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> (min)</th><th><span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>TiT_i</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> (min)</th></tr></thead><tbody><tr><td><span><span>g11g_{11}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>12.8</td><td>16.7</td><td>1</td><td>0.6523</td><td>8.0</td></tr><tr><td><span><span>g22g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>−19.4</td><td>14.4</td><td>3</td><td>−0.1237</td><td>14.4</td></tr><tr><td><span><span>g12g_{12}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>−18.9</td><td>21.0</td><td>3</td><td>−0.1852</td><td>21.0</td></tr><tr><td><span><span>g21g_{21}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td>6.6</td><td>10.9</td><td>7</td><td>0.1180</td><td>10.9</td></tr></tbody></table><p>两个对角通道我先手算过：<span><span>Kp=12.8K_p = 12.8</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12.8</span></span></span></span> 的通道是 <span><span>τ/(k(τc+θ))=16.7/(12.8×2)=0.652\tau/(k(\tau_c+\theta)) = 16.7/(12.8\times2) = 0.652</span><span><span><span></span><span>τ</span><span>/</span><span>(</span><span>k</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>θ</span><span>))</span><span></span><span>=</span><span></span></span><span><span></span><span>16.7/</span><span>(</span><span>12.8</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.652</span></span></span></span>，<span><span>Ti=min⁡(16.7, 4×2)=8T_i = \min(16.7,\ 4\times2) = 8</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>(</span><span>16.7</span><span>,</span><span> </span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>8</span></span></span></span>；<span><span>g22g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>14.4/(−19.4×6)=−0.12414.4/(-19.4\times6) = -0.124</span><span><span><span></span><span>14.4/</span><span>(</span><span>−</span><span>19.4</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.124</span></span></span></span>，<span><span>Ti=min⁡(14.4, 24)=14.4T_i = \min(14.4,\ 24) = 14.4</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>(</span><span>14.4</span><span>,</span><span> </span><span></span><span>24</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>14.4</span></span></span></span>。代码给出的 0.6523、8.0、−0.1237、14.4 与此一致，而且是代码算的，没有硬编码。注意 <span><span>g22g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的增益是负的，所以它的 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 也是负的：控制器的符号必须和对象一致，否则第一拍就是正反馈。<code>Pid</code> 不关心符号，写错了它也照样给你一个发散的回路。</p><p>下面两个通道是错配实验用的：<code>wb_wrongpair</code> 里 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 由 <span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 管，用的是 <span><span>g12g_{12}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 那一行的 SIMC 增益，<span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 由 <span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 管，用的是 <span><span>g21g_{21}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 那一行的。SIMC 在这里的意义很窄：它保证每个通道<strong>单独</strong>闭环时是好的，对另一个回路的存在一无所知，这是下面几个实验里被检验的地方。</p><p>四个 <code>Pid</code> 的构造只有这几行，控制器就是第 5 篇的 <code>Pid</code>，<span><span>Kd=0K_d = 0</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>wb_loops</span><span>(</span><span>plant</span><span>:</span><span> WoodBerry</span><span>) -&gt; dict[</span><span>str</span><span>, list[Pid]]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""SIMC PI (tauc = theta) for the two pairings, parallel gains."""</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>def</span><span> </span><span>pid</span><span>(</span><span>i</span><span>:</span><span> </span><span>int</span><span>) -&gt; Pid:</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>theta </span><span>=</span><span> plant.delay[i] </span><span>*</span><span> </span><span>WB_TS</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>g </span><span>=</span><span> </span><span>simc_pi</span><span>(plant.k[i], plant.tau[i], theta)</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span><span> </span><span>Pid</span><span>(</span><span>PidConfig</span><span>(g.kp, g.ki, </span></span><span>0.0</span><span><span>, </span><span>WB_TS</span><span>))</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span># channel order: 0 = g11, 1 = g12, 2 = g21, 3 = g22</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> {</span><span>"diag"</span><span><span>: [</span><span>pid</span><span>(</span></span><span>0</span><span><span>), </span><span>pid</span><span>(</span></span><span>3</span><span>)], </span><span>"off"</span><span><span>: [</span><span>pid</span><span>(</span></span><span>1</span><span><span>), </span><span>pid</span><span>(</span></span><span>2</span><span>)]}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>静态解耦和矩阵增益 PI<a href="#静态解耦和矩阵增益-pi"><span>#</span></a></h3><p>既然耦合在稳态下是 <span><span>G(0)G(0)</span><span><span><span></span><span>G</span><span>(</span><span>0</span><span>)</span></span></span></span> 的非对角项，一个直接的想法是在控制器后面加一个矩阵 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span>，让 <span><span>G(0)DG(0)D</span><span><span><span></span><span>G</span><span>(</span><span>0</span><span>)</span><span>D</span></span></span></span> 变成对角：</p><span><span><span>D=G(0)−1diag⁡(G(0)),G(0)D=diag⁡(12.8, −19.4)D = G(0)^{-1}\operatorname{diag}\bigl(G(0)\bigr),\qquad G(0)D=\operatorname{diag}(12.8,\,-19.4)</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span>G</span><span>(</span><span>0</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span><span>diag</span></span><span><span>(</span></span><span>G</span><span>(</span><span>0</span><span>)</span><span><span>)</span></span><span>,</span><span></span><span></span><span>G</span><span>(</span><span>0</span><span>)</span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>12.8</span><span>,</span><span></span><span></span><span>−</span><span>19.4</span><span>)</span></span></span></span></span><p>Wood–Berry 的 <span><span>D=[2.00942.96700.68362.0094]D = \begin{bmatrix}2.0094&amp;2.9670\\0.6836&amp;2.0094\end{bmatrix}</span><span><span><span></span><span>D</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>2.0094</span></span></span><span><span></span><span><span>0.6836</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>2.9670</span></span></span><span><span></span><span><span>2.0094</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，<code>report</code> 验证了 <span><span>G(0)DG(0)D</span><span><span><span></span><span>G</span><span>(</span><span>0</span><span>)</span><span>D</span></span></span></span> 的非对角项只有 <span><span>4×10−154\times10^{-15}</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。结构是这样的：</p><div><div><div><div><span></span></div><div><span><p>e1, e2</p></span></div><div><span><p>v1, v2</p></span></div><div><span><p>u1, u2</p></span></div><div><span><p>y1, y2</p></span></div><div><span><p>r1, r2</p></span></div><div><span>+ −</span></div><div><span><p>对角 PI：diag(C1, C2)</p></span></div><div><span><p>静态解耦器 D = G(0)⁻¹ diag G(0)</p></span></div><div><span><p>Wood–Berry 对象 G(s)</p></span></div>
</div><div><div><span></span></div><div><span><p>e1, e2</p></span></div><div><span><p>v1, v2</p></span></div><div><span><p>u1, u2</p></span></div><div><span><p>y1, y2</p></span></div><div><span><p>r1, r2</p></span></div><div><span>+ −</span></div><div><span><p>对角 PI：diag(C1, C2)</p></span></div><div><span><p>静态解耦器 D = G(0)⁻¹ diag G(0)</p></span></div><div><span><p>Wood–Berry 对象 G(s)</p></span></div>
</div></div></div><p>两个 PI 是<strong>同一对</strong>对角 PI（用 SIMC 在对角通道上整定的那两个），这是对比的前提。把 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 和 PI 合并看，控制律是 <span><span>u=D [Kpe+Ki ⁣∫e]\mathbf u = D\,[K_p\mathbf e + K_i\!\int\mathbf e]</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>D</span><span></span><span>[</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span></span><span>∫</span><span></span><span>e</span><span>]</span></span></span></span>，也就是一个矩阵增益 PI：</p><span><span><span>u=Kpmat e+Kimat ⁣∫e dt,Kpmat=Ddiag⁡(kp1,kp2),Kimat=Ddiag⁡(ki1,ki2)\mathbf u = K_p^{mat}\,\mathbf e + K_i^{mat}\!\int\mathbf e\,dt,\qquad
K_p^{mat} = D\operatorname{diag}(k_{p1},k_{p2}),\quad
K_i^{mat} = D\operatorname{diag}(k_{i1},k_{i2})</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span><span><span></span><span><span><span>ma</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span><span>ma</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span></span><span>∫</span><span></span><span>e</span><span></span><span>d</span><span>t</span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span><span><span></span><span><span><span>ma</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>D</span><span></span><span><span>diag</span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span><span>ma</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>D</span><span></span><span><span>diag</span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>所谓“多变量 PID”，在这个层面上不过是把标量增益换成矩阵增益，不多不少。第 9 篇会把这个想法推到底。</p><p>静态解耦只管 <span><span>s=0s = 0</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。<span><span>g11g_{11}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 到 <span><span>g22g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的延迟和时间常数各不相同，<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 想抵消的耦合通道和它本该抵消的通道，在动态上并不对齐。稳态耦合是没了，暂态会变好还是变坏，我没法从公式直接看出来，下面直接看数。</p></section><section><h3>四个算例的循环<a href="#四个算例的循环"><span>#</span></a></h3><p>四个算例的循环完全一样，只差“谁管谁”这几行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>y1, y2 </span><span>=</span><span> plant.</span><span>outputs</span><span>(x)</span></span></div></div><div><div><div>2</div></div><div><span>if</span><span><span> name </span><span>==</span><span> </span></span><span>"wb_single1"</span><span>:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>u1, u2 </span><span>=</span><span> loops[</span><span>"diag"</span><span>][</span><span>0</span><span><span>].</span><span>update</span><span>(r1, y1), </span></span><span>0.0</span></div></div><div><div><div>4</div></div><div><span>elif</span><span><span> name </span><span>==</span><span> </span></span><span>"wb_decentral"</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>u1 </span><span>=</span><span> loops[</span><span>"diag"</span><span>][</span><span>0</span><span><span>].</span><span>update</span><span>(r1, y1)</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>u2 </span><span>=</span><span> loops[</span><span>"diag"</span><span>][</span><span>1</span><span><span>].</span><span>update</span><span>(r2, y2)</span></span></div></div><div><div><div>7</div></div><div><span>elif</span><span><span> name </span><span>==</span><span> </span></span><span>"wb_wrongpair"</span><span>:</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>u2 </span><span>=</span><span> loops[</span><span>"off"</span><span>][</span><span>0</span><span><span>].</span><span>update</span><span>(r1, y1)  </span></span><span># y1 is driven by u2</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>u1 </span><span>=</span><span> loops[</span><span>"off"</span><span>][</span><span>1</span><span><span>].</span><span>update</span><span>(r2, y2)  </span></span><span># y2 is driven by u1</span></div></div><div><div><div>10</div></div><div><span>else</span><span>:  </span><span># wb_decoupled</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>v1 </span><span>=</span><span> loops[</span><span>"diag"</span><span>][</span><span>0</span><span><span>].</span><span>update</span><span>(r1, y1)</span></span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>v2 </span><span>=</span><span> loops[</span><span>"diag"</span><span>][</span><span>1</span><span><span>].</span><span>update</span><span>(r2, y2)</span></span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>u1, u2 </span><span>=</span><span> </span><span>mat2_vec</span><span>(d, [v1, v2])</span></div></div><div><div><div>14</div></div><div><span># ...</span></div></div><div><div><div>15</div></div><div><span><span>u </span><span>=</span><span> [lines[</span></span><span>0</span><span><span>].</span><span>push</span><span>(u1), lines[</span></span><span>1</span><span><span>].</span><span>push</span><span>(u2)]</span></span></div></div><div><div><div>16</div></div><div><span><span>u </span><span>+=</span><span> [lines[</span></span><span>2</span><span><span>].</span><span>push</span><span>(u1), lines[</span></span><span>3</span><span><span>].</span><span>push</span><span>(u2)]</span></span></div></div><div><div><div>17</div></div><div><span><span>x </span><span>=</span><span> </span><span>advance</span><span>(plant.deriv, x, u, </span><span>WB_TS</span><span>, </span><span>WB_SUBSTEPS</span><span>)</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三件事值得盯一下。<strong>配对</strong>：<code>wb_wrongpair</code> 里 <code>off[0]</code> 看 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、输出给 <span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<code>off[1]</code> 看 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、输出给 <span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。<strong>四条延迟线</strong>：<code>lines[0]</code> 和 <code>lines[2]</code> 都吃 <span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<code>lines[1]</code> 和 <code>lines[3]</code> 都吃 <span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，分别对应 <span><span>g11,g12,g21,g22g_{11}, g_{12}, g_{21}, g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的延迟。<strong>解耦</strong>：<code>wb_decoupled</code> 先让两个 PI 输出 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span>，再用 <code>mat2_vec(d, [v1, v2])</code> 乘 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span>，记录下来的 <code>u1</code>、<code>u2</code> 是 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 之后、进延迟线之前的量。</p></section></section>
<section><h2>麦轮：四个轮子，三个自由度<a href="#麦轮四个轮子三个自由度"><span>#</span></a></h2><section><h3>从几何推出 H<a href="#从几何推出-h"><span>#</span></a></h3><p>车体系 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 向前、<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 向左、<span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 向上，<span><span>ν=(vx,vy,ωz)\nu = (v_x, v_y, \omega_z)</span><span><span><span></span><span>ν</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 是车体系下的速度。轮子 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 的接地点位置是 <span><span>(xi,yi)(x_i, y_i)</span><span><span><span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>∣xi∣=lx|x_i| = l_x</span><span><span><span></span><span>∣</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>∣yi∣=ly|y_i| = l_y</span><span><span><span></span><span>∣</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，接地点的速度是</p><span><span><span>vi=(vx−ωzyi,  vy+ωzxi)\mathbf v_i = (v_x - \omega_z y_i,\; v_y + \omega_z x_i)</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p><a href="https://en.wikipedia.org/wiki/Mecanum_wheel" target="_blank">麦轮</a>的滚子轴和轮平面成 45°，轮子沿滚子方向可以自由滑动，所以对轮子的转动只有一个约束：轮缘速度 <span><span>rωir\omega_i</span><span><span><span></span><span>r</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 等于接地点速度在“轮面前进方向”和“垂直方向”上的两个分量按滚子斜度（±45°）的组合：</p><span><span><span>r ωi=vi,x+si vi,y,si=±1r\,\omega_i = v_{i,x} + s_i\,v_{i,y},\qquad s_i=\pm1</span><span><span><span></span><span>r</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>±</span><span>1</span></span></span></span></span><p><span><span>sis_i</span><span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 由滚子的装法决定。我取 <span><span>s=(−1,+1,+1,−1)s = (-1, +1, +1, -1)</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>+</span><span>1</span><span>,</span><span></span><span>+</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，轮子依次是左前（FL）、右前（FR）、左后（RL）、右后（RR），位置 <span><span>(xi,yi)=(+lx,+ly),(+lx,−ly),(−lx,+ly),(−lx,−ly)(x_i, y_i) = (+l_x, +l_y), (+l_x, -l_y), (-l_x, +l_y), (-l_x, -l_y)</span><span><span><span></span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>+</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>+</span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span>(</span><span>+</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>−</span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span>(</span><span>−</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>+</span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span>(</span><span>−</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>−</span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。编号和坐标画出来是这样（俯视，<span><span>+y+y</span><span><span><span></span><span>+</span><span>y</span></span></span></span> 指向左边，仿真里的 <code>i1</code> 到 <code>i4</code> 就是这个顺序）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>              </span></span><span>front (+x)</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>(+lx,+ly) 1 FL  ^  2 FR (+lx,-ly)</span></div></div><div><div><div>3</div></div><div><span><span>               </span></span><span>+---|---+</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>(+y) &lt;------|   .   |</span></div></div><div><div><div>5</div></div><div><span><span>               </span></span><span>+-------+</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>(-lx,+ly) 3 RL     4 RR (-lx,-ly)</span></div></div><div><div><div>7</div></div><div><span><span>               </span></span><span>rear (-x)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>代入：</p><span><span><span>rωFL=vx−vy−(lx+ly)ωz,rωFR=vx+vy+(lx+ly)ωzr\omega_{FL} = v_x - v_y - (l_x+l_y)\omega_z,\quad
r\omega_{FR} = v_x + v_y + (l_x+l_y)\omega_z</span><span><span><span></span><span>r</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>F</span><span>L</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>r</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>F</span><span>R</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span><span><span>rωRL=vx+vy−(lx+ly)ωz,rωRR=vx−vy+(lx+ly)ωzr\omega_{RL} = v_x + v_y - (l_x+l_y)\omega_z,\quad
r\omega_{RR} = v_x - v_y + (l_x+l_y)\omega_z</span><span><span><span></span><span>r</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>R</span><span>L</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>r</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>R</span><span>R</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>写成 <span><span>ω=Hν/r\omega = H\nu/r</span><span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>ν</span><span>/</span><span>r</span></span></span></span>，<span><span>L=lx+ly=0.4L = l_x + l_y = 0.4</span><span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.4</span></span></span></span> m：</p><span><span><span>H=[1−1−L11L11−L1−1L]H=\begin{bmatrix}1&amp;-1&amp;-L\\1&amp;1&amp;L\\1&amp;1&amp;-L\\1&amp;-1&amp;L\end{bmatrix}</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>L</span></span></span><span><span></span><span><span>L</span></span></span><span><span></span><span><span>−</span><span>L</span></span></span><span><span></span><span><span>L</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>（<span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 整个反过来的装法，在 <span><span>lx=lyl_x = l_y</span><span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 时每个轮子对 <span><span>ωz\omega_z</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的系数都是 0，底盘转不起来，所以真车不是那样装的。我没有对照真实的 RoboMaster 底盘去核对滚子朝向，这一组 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 是推导里的假设。）<code>report</code> 用 <code>numpy</code> 验证了 <span><span>HTH=diag⁡(4,4,4L2)H^TH=\operatorname{diag}(4,4,4L^2)</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>4</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>4</span><span><span>L</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>：<span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的三列互相正交，这一点后面一直在用。代码里 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 就是 12 个数的行优先列表，轮 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 的那一行在 <code>h[3 * i : 3 * i + 3]</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>mecanum_h</span><span>(</span><span>lx</span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>ly</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Wheel matrix H (4x3): wheel speeds are H nu / r for nu = (vx, vy, wz).</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>Wheel order: front left, front right, rear left, rear right.</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> lx </span><span>+</span><span> ly</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> [</span></div></div><div><div><div>8</div></div><div><span>        </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span>, </span><span>-</span><span>s,</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>1.0</span><span>, </span><span>1.0</span><span>, s,</span></div></div><div><div><div>10</div></div><div><span>        </span><span>1.0</span><span>, </span><span>1.0</span><span><span>, </span><span>-</span><span>s,</span></span></div></div><div><div><div>11</div></div><div><span>        </span><span>1.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>, s,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>]  </span><span># fmt: skip</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>动力学：为什么力矩是 <span><span>KtrHTi\tfrac{K_t}{r}H^Ti</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>r</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span></span></span></span><a href="#动力学为什么力矩是-ktrhtitfrack_trhtirkthti"><span>#</span></a></h3><p>底盘质量 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、转动惯量 <span><span>IzI_z</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，每个轮子折算到轮轴上的转动惯量 <span><span>JwJ_w</span><span><span><span></span><span><span>J</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。动能是</p><span><span><span>T=12νTdiag⁡(m,m,Iz) ν+12Jw∑iωi2=12νT(diag⁡(m,m,Iz)+Jwr2HTH)νT=\tfrac12\nu^T\operatorname{diag}(m,m,I_z)\,\nu+\tfrac12J_w\sum_i\omega_i^2
=\tfrac12\nu^T\Bigl(\operatorname{diag}(m,m,I_z)+\tfrac{J_w}{r^2}H^TH\Bigr)\nu</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>ν</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span><span>diag</span></span><span>(</span><span>m</span><span>,</span><span></span><span>m</span><span>,</span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>ν</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>J</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>ν</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>(</span></span><span><span>diag</span></span><span>(</span><span>m</span><span>,</span><span></span><span>m</span><span>,</span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>r</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>J</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span><span><span>)</span></span><span>ν</span></span></span></span></span><p>电机功率 <span><span>∑iKtiiωi=νT(KtrHTi)\sum_iK_ti_i\omega_i = \nu^T\bigl(\tfrac{K_t}{r}H^Ti\bigr)</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ν</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>(</span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>r</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span><span><span>)</span></span></span></span></span>，所以对 <span><span>ν\nu</span><span><span><span></span><span>ν</span></span></span></span> 的广义力就是 <span><span>KtrHTi\tfrac{K_t}{r}H^Ti</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>r</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span></span></span></span>，与 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 行的符号约定无关，只要 <span><span>ω=Hν/r\omega = H\nu/r</span><span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>ν</span><span>/</span><span>r</span></span></span></span>。再加粘性阻尼 <span><span>−cvν-c_v\nu</span><span><span><span></span><span>−</span><span><span>c</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ν</span></span></span></span> 和体系间的向心耦合（车体系下速度分量的导数不是加速度：<span><span>m(v˙x−ωzvy)=Fxm(\dot v_x - \omega_zv_y)=F_x</span><span><span><span></span><span>m</span><span>(</span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>m(v˙y+ωzvx)=Fym(\dot v_y + \omega_zv_x)=F_y</span><span><span><span></span><span>m</span><span>(</span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>），得到</p><span><span><span>(diag⁡(m,m,Iz)+Jwr2HTH)ν˙=KtrHTi−cvν+[mωzvy−mωzvx0]\Bigl(\operatorname{diag}(m,m,I_z)+\tfrac{J_w}{r^2}H^TH\Bigr)\dot\nu
=\tfrac{K_t}{r}H^Ti-c_v\nu+\begin{bmatrix}m\omega_zv_y\\-m\omega_zv_x\\0\end{bmatrix}</span><span><span><span></span><span><span>(</span></span><span><span>diag</span></span><span>(</span><span>m</span><span>,</span><span></span><span>m</span><span>,</span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>r</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>J</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span><span><span>)</span></span><span><span><span><span><span><span></span><span>ν</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>r</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ν</span><span></span><span>+</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>m</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span>m</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>世界系位姿 <span><span>(X,Y,ψ)(X, Y, \psi)</span><span><span><span></span><span>(</span><span>X</span><span>,</span><span></span><span>Y</span><span>,</span><span></span><span>ψ</span><span>)</span></span></span></span> 由 <span><span>X˙=vxcos⁡ψ−vysin⁡ψ\dot X = v_x\cos\psi - v_y\sin\psi</span><span><span><span></span><span><span><span><span><span><span></span><span>X</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>ψ</span><span></span><span>−</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span>ψ</span></span></span></span>、<span><span>Y˙=vxsin⁡ψ+vycos⁡ψ\dot Y = v_x\sin\psi + v_y\cos\psi</span><span><span><span></span><span><span><span><span><span><span></span><span>Y</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span>ψ</span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>ψ</span></span></span></span>、<span><span>ψ˙=ωz\dot\psi = \omega_z</span><span><span><span></span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 积分。参数是我取的假设值：<span><span>m=20m = 20</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> kg，<span><span>Iz=0.6I_z = 0.6</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.6</span></span></span></span> kg·m²，<span><span>r=0.076r = 0.076</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.076</span></span></span></span> m，<span><span>lx=ly=0.2l_x = l_y = 0.2</span><span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>l</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> m，<span><span>Jw=0.001J_w = 0.001</span><span><span><span></span><span><span>J</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.001</span></span></span></span> kg·m²（每轮），<span><span>cv=0.5c_v = 0.5</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span></span></span></span>，<span><span>Kt=0.3K_t = 0.3</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span> N·m/A（第 1 篇交代过出处和不确定性）。因为 <span><span>HTHH^TH</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span></span></span></span> 是对角的，左边的质量阵也是对角的：<span><span>Meff=diag⁡(20.6925, 20.6925, 0.7108)M_{eff} = \operatorname{diag}(20.6925,\ 20.6925,\ 0.7108)</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>e</span><span>f</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>20.6925</span><span>,</span><span> </span><span></span><span>20.6925</span><span>,</span><span> </span><span></span><span>0.7108</span><span>)</span></span></span></span>。</p><p>向心项值得多说一句。省掉它，对象在车体系下就是三个独立的一阶系统，<span><span>ωz=0\omega_z = 0</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时没错，转起来就不对了。这一篇的指令最后是 <span><span>ωz≈2.2\omega_z\approx2.2</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>2.2</span></span></span></span> rad/s，<span><span>v≈1.46v\approx1.46</span><span><span><span></span><span>v</span><span></span><span>≈</span><span></span></span><span><span></span><span>1.46</span></span></span></span> m/s，向心力在 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 方向的分量 <span><span>mωzvy=20×2.192×1.462=64.1m\omega_zv_y = 20\times2.192\times1.462 = 64.1</span><span><span><span></span><span>m</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>20</span><span></span><span>×</span><span></span></span><span><span></span><span>2.192</span><span></span><span>×</span><span></span></span><span><span></span><span>1.462</span><span></span><span>=</span><span></span></span><span><span></span><span>64.1</span></span></span></span> N，是四个轮子同向推车时推力上限（<span><span>4Kt×20/r≈3164K_t\times20/r\approx316</span><span><span><span></span><span>4</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>20/</span><span>r</span><span></span><span>≈</span><span></span></span><span><span></span><span>316</span></span></span></span> N）的五分之一。我先省掉它跑过一遍，后面“没有按直觉发生的事”给出了两版的数。</p><p>放进代码里，<code>Mecanum.deriv</code> 就是这个方程：先算广义力 <span><span>KtrHTi−cvν\tfrac{K_t}{r}H^Ti-c_v\nu</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>r</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ν</span></span></span></span>，加向心项，再除以对角的 <span><span>MeffM_{eff}</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>e</span><span>f</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，最后接上位姿的运动学：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> </span><span>mecanum_h</span><span>(</span><span>self</span><span>.lx, </span><span>self</span><span>.ly)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>meff </span><span>=</span><span> </span><span>self</span><span><span>.</span><span>inertia</span><span>()</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> []</span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>):</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>wrench </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>7</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>):</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>wrench </span><span>+=</span><span> h[</span><span>3</span><span><span> </span><span>*</span><span> i </span><span>+</span><span> k] </span><span>*</span><span> u[i]</span></span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>f.</span><span>append</span><span>(</span><span>self</span><span><span>.kt </span><span>/</span><span> </span></span><span>self</span><span><span>.r </span><span>*</span><span> wrench </span><span>-</span><span> </span></span><span>self</span><span><span>.cv </span><span>*</span><span> x[k])</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>f[</span><span>0</span><span><span>] </span><span>+=</span><span> </span></span><span>self</span><span><span>.m_body </span><span>*</span><span> x[</span></span><span>2</span><span><span>] </span><span>*</span><span> x[</span></span><span>1</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>f[</span><span>1</span><span><span>] </span><span>-=</span><span> </span></span><span>self</span><span><span>.m_body </span><span>*</span><span> x[</span></span><span>2</span><span><span>] </span><span>*</span><span> x[</span></span><span>0</span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>dv </span><span>=</span><span> [f[k] </span><span>/</span><span> meff[k] </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)]</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>c, s </span><span>=</span><span> math.</span><span>cos</span><span>(x[</span><span>5</span><span><span>]), math.</span><span>sin</span><span>(x[</span></span><span>5</span><span>])</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span> [</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>dv[</span><span>0</span><span>],</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>dv[</span><span>1</span><span>],</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>dv[</span><span>2</span><span>],</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>c </span><span>*</span><span> x[</span><span>0</span><span><span>] </span><span>-</span><span> s </span><span>*</span><span> x[</span></span><span>1</span><span>],</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>s </span><span>*</span><span> x[</span><span>0</span><span><span>] </span><span>+</span><span> c </span><span>*</span><span> x[</span></span><span>1</span><span>],</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>x[</span><span>2</span><span>],</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>状态是 <code>[vx, vy, wz, X, Y, psi]</code>，输入是四个轮子的电流。<code>inertia()</code> 返回对角质量阵：<span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>IzI_z</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 各加上 <span><span>Jwr2\tfrac{J_w}{r^2}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>r</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>J</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span> 乘 <code>mecanum_gram_diag</code>。</p></section><section><h3>零空间：四个轮子，三个自由度<a href="#零空间四个轮子三个自由度"><span>#</span></a></h3><p><span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 是 4×3 的，<span><span>HTH^T</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 有一维零空间。向量 <span><span>n=[1,1,−1,−1]/2n=[1,1,-1,-1]/2</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>]</span><span>/2</span></span></span></span> 满足 <span><span>HTn=0H^Tn=0</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：<span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的第一列是 <span><span>(1,1,1,1)(1,1,1,1)</span><span><span><span></span><span>(</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，和 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 的内积是 <span><span>12(1+1−1−1)=0\tfrac12(1+1-1-1)=0</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；第二列 <span><span>(−1,1,1,−1)(-1,1,1,-1)</span><span><span><span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>−</span><span>1</span><span>)</span></span></span></span>，<span><span>12(−1+1−1+1)=0\tfrac12(-1+1-1+1)=0</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>−</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；第三列 <span><span>(−L,L,−L,L)(-L,L,-L,L)</span><span><span><span></span><span>(</span><span>−</span><span>L</span><span>,</span><span></span><span>L</span><span>,</span><span></span><span>−</span><span>L</span><span>,</span><span></span><span>L</span><span>)</span></span></span></span>，<span><span>12(−L+L+L−L)=0\tfrac12(-L+L+L-L)=0</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>−</span><span>L</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span></span><span>−</span><span></span></span><span><span></span><span>L</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。<code>report</code> 数值核对了 <span><span>HTn=[0,0,0]H^Tn=[0,0,0]</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span>。</p><p>物理含义：<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 是“左前、右前一起往前拧，左后、右后一起往后拧”的电流模式，它产生的合力和合力矩都是 0。四个人抬一张桌子，两个人往前拉、两个人往后拉，桌子不动，但每个人都在出力：零空间电流就是这样的力，不产生任何底盘运动，只是白白发热（我没有在这一篇里算电机热和电池功率，只给电流）。对任意电流 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span>，可以分解成</p><span><span><span>i=irange+(nTi) n,nTi=12(i1+i2−i3−i4)i = i_{range} + (n^Ti)\,n,\qquad n^Ti = \tfrac12(i_1+i_2-i_3-i_4)</span><span><span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>r</span><span>an</span><span>g</span><span>e</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span><span>)</span><span></span><span>n</span><span>,</span><span></span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span><span>i</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p><span><span>nTin^Ti</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span></span></span></span> 就是仿真里记录的 <code>null_i</code>。给定车体系的合力和合力矩 <span><span>w=(Fx,Fy,Tz)w=(F_x,F_y,T_z)</span><span><span><span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>F</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，求满足 <span><span>KtrHTi=w\tfrac{K_t}{r}H^Ti=w</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>r</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>w</span></span></span></span> 的电流，有无穷多解，范数最小的那个是</p><span><span><span>i=rKt H (HTH)−1wi=\frac{r}{K_t}\,H\,(H^TH)^{-1}w</span><span><span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>H</span><span></span><span>(</span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>w</span></span></span></span></span><p>它没有零空间分量。同理由轮速求车体速度的正运动学 <span><span>ν^=rH+ω=r(HTH)−1HTω\hat\nu = rH^+\omega = r(H^TH)^{-1}H^T\omega</span><span><span><span></span><span><span><span><span><span><span></span><span>ν</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span><span>H</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span>(</span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>ω</span></span></span></span>。因为 <span><span>HTHH^TH</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span></span></span></span> 是对角的，这两个式子都不需要矩阵求逆，只要除以 <span><span>(4,4,4L2)(4,4,4L^2)</span><span><span><span></span><span>(</span><span>4</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>4</span><span><span>L</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>mecanum_currents</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>w</span><span>:</span><span> list[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>lx</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>ly</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>r</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>kt</span></span><span>:</span><span> </span><span>float</span></div></div><div><div><div>3</div></div><div><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"""Minimum-norm currents for the body wrench w = (Fx, Fy, Tz).</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>The wrench is (kt / r) H^T i; the solution is</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>i = (r / kt) H (H^T H)^-1 w.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> </span><span>mecanum_h</span><span>(lx, ly)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> </span><span>mecanum_gram_diag</span><span>(lx, ly)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> [w[k] </span><span>/</span><span> g[k] </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)]</span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> [</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>r </span><span>/</span><span> kt </span><span>*</span><span> (h[</span><span>3</span><span><span> </span><span>*</span><span> i] </span><span>*</span><span> a[</span></span><span>0</span><span><span>] </span><span>+</span><span> h[</span></span><span>3</span><span><span> </span><span>*</span><span> i </span><span>+</span><span> </span></span><span>1</span><span><span>] </span><span>*</span><span> a[</span></span><span>1</span><span><span>] </span><span>+</span><span> h[</span></span><span>3</span><span><span> </span><span>*</span><span> i </span><span>+</span><span> </span></span><span>2</span><span><span>] </span><span>*</span><span> a[</span></span><span>2</span><span>])</span></div></div><div><div><div>14</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>def</span><span> </span><span>null_current</span><span>(</span><span>i</span><span>:</span><span> list[</span><span>float</span><span>]</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>"""Component n^T i along the null vector n = [1, 1, -1, -1] / 2."""</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> (i[</span></span><span>0</span><span><span>] </span><span>+</span><span> i[</span></span><span>1</span><span><span>] </span><span>-</span><span> i[</span></span><span>2</span><span><span>] </span><span>-</span><span> i[</span></span><span>3</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>report</code> 把我手写的 <span><span>ν^\hat\nu</span><span><span><span></span><span><span><span><span><span><span></span><span>ν</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 和 <code>numpy.linalg.pinv</code> 的结果比，差 <span><span>2.2×10−162.2\times10^{-16}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，把最小范数电流和 <code>pinv</code> 比，差 <span><span>5.6×10−175.6\times10^{-17}</span><span><span><span></span><span>5.6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>。</p></section><section><h3>饱和的时候，方向比大小重要<a href="#饱和的时候方向比大小重要"><span>#</span></a></h3><p>轮速有上限，<span><span>ωmax=50\omega_{max} = 50</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>ma</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>50</span></span></span></span> rad/s（<a href="https://www.robomaster.com/zh-CN/products/components/general/M3508" target="_blank">官网</a>列的 M3508 空载转速约 482 rpm，也就是 50.5 rad/s），电流有上限 20 A。指令是 <span><span>νref=(2.0, 2.0, 3.0)\nu_{ref}=(2.0,\,2.0,\,3.0)</span><span><span><span></span><span><span>ν</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2.0</span><span>,</span><span></span><span></span><span>2.0</span><span>,</span><span></span><span></span><span>3.0</span><span>)</span></span></span></span>，对应的四个轮速是</p><span><span><span>ωref=Hνref/r=(−15.79, 68.42, 36.84, 15.79) rad/s\omega_{ref}=H\nu_{ref}/r=(-15.79,\ 68.42,\ 36.84,\ 15.79)\ \text{rad/s}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>ν</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>−</span><span>15.79</span><span>,</span><span> </span><span></span><span>68.42</span><span>,</span><span> </span><span></span><span>36.84</span><span>,</span><span> </span><span></span><span>15.79</span><span>)</span><span> </span><span><span>rad/s</span></span></span></span></span></span><p>只有轮 2（FR）超限。三种处理：</p><ul>
<li><strong>逐轮截断</strong>（<code>mec_wheel_clip</code>）：每个轮子的设定值各自截到 <span><span>±50\pm50</span><span><span><span></span><span>±</span><span>50</span></span></span></span>，每个轮子的电流各自截到 <span><span>±20\pm20</span><span><span><span></span><span>±</span><span>20</span></span></span></span> A，每个轮子一个 <code>Pid</code>（带 <span><span>±20\pm20</span><span><span><span></span><span>±</span><span>20</span></span></span></span> A 限幅，窗口是 <code>CLAMP</code>）。</li>
<li><strong>逐轮等比缩放</strong>（<code>mec_wheel_scale</code>）：整个轮速向量乘同一个系数 <span><span>s=50/68.42=0.7308s = 50/68.42 = 0.7308</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>50/68.42</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7308</span></span></span></span>，电流向量也乘同一个系数让 <span><span>max⁡∣i∣≤20\max|i|\le20</span><span><span><span></span><span>max</span><span></span><span>∣</span><span>i</span><span>∣</span><span></span><span>≤</span><span></span></span><span><span></span><span>20</span></span></span></span>。</li>
<li><strong>车体系 PI</strong>（<code>mec_body</code>）：三个 PI 管 <span><span>ν^\hat\nu</span><span><span><span></span><span><span><span><span><span><span></span><span>ν</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>，指令乘同一个 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span>，输出的合力和合力矩用最小范数映射成电流，最后电流向量等比缩放。</li>
</ul><p>截断为什么麻烦？<span><span>ωref\omega_{ref}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 本来在 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的值域里（它是 <span><span>Hνref/rH\nu_{ref}/r</span><span><span><span></span><span>H</span><span><span>ν</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>r</span></span></span></span>），只截轮 2 一个，截出来的向量不再属于值域。值域外的那部分就是零空间分量 <span><span>nTωclip=−9.21n^T\omega_{clip} = -9.21</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>c</span><span>l</span><span>i</span><span>p</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>9.21</span></span></span></span> rad/s，而轮子的实际速度永远在值域里（<span><span>ω=Hν/r\omega = H\nu/r</span><span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>ν</span><span>/</span><span>r</span></span></span></span>），所以这 <span><span>−9.21-9.21</span><span><span><span></span><span>−</span><span>9.21</span></span></span></span> rad/s 的误差<strong>任何底盘运动都消不掉</strong>。四个同样增益的 PI 对它的反应是：比例项单独就是 <span><span>kp×(−9.21)=−38.4k_p\times(-9.21)=-38.4</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>−</span><span>9.21</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>38.4</span></span></span></span> A，沿 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 的方向，也就是每个轮子 <span><span>±19.2\pm19.2</span><span><span><span></span><span>±</span><span>19.2</span></span></span></span> A，已经是 20 A 限幅的 96%，积分项再把它推到限幅。电流的限幅被零空间电流占满，留给真正推车的“值域”部分的余量没了。</p><p>等比缩放的两种写法没有这个问题：缩放后的 <span><span>ωref\omega_{ref}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 仍在值域里，四个 PI 增益相同，值域里的误差只会产生值域里的电流，零空间分量从头到尾是 0（舍入误差除外）。为了让缩放时不积分饱和，缩放这两种的 <code>Pid</code> 不设自己的限幅，驱动把“上一个采样是否缩放了”当作 <code>hold_integral</code> 传给所有积分器，和第 7 篇冻结外环积分是同一个手法。</p><p>三个变体共用一个循环，只差 <code>if</code> 的三支：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>omega </span><span>=</span><span> plant.</span><span>wheel_speeds</span><span>(x)</span></span></div></div><div><div><div>2</div></div><div><span><span>wref </span><span>=</span><span> </span><span>mecanum_wheel_speeds</span><span>(nu_ref, lx, ly, r)</span></span></div></div><div><div><div>3</div></div><div><span>if</span><span><span> name </span><span>==</span><span> </span></span><span>"mec_wheel_clip"</span><span>:</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>wref </span><span>=</span><span> [</span><span>max</span><span><span>(</span><span>-</span><span>OMEGA_MAX</span><span>, </span></span><span>min</span><span><span>(</span><span>OMEGA_MAX</span><span>, w)) </span></span><span>for</span><span> w </span><span>in</span><span> wref]</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>i_cmd </span><span>=</span><span> [pids[i].</span><span>update</span><span>(wref[i], omega[i]) </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)]</span></div></div><div><div><div>6</div></div><div><span>elif</span><span><span> name </span><span>==</span><span> </span></span><span>"mec_wheel_scale"</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>wref, _ </span><span>=</span><span> </span><span>scale_to</span><span>(wref, </span><span>OMEGA_MAX</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> [</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>pids[i].</span><span>update</span><span>(wref[i], omega[i], </span><span>0.0</span><span>, scaled)</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>i_cmd, scaled </span><span>=</span><span> </span><span>scale_to</span><span>(v, plant.i_max)</span></div></div><div><div><div>13</div></div><div><span>else</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>_, hit </span><span>=</span><span> </span><span>scale_to</span><span>(wref, </span><span>OMEGA_MAX</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>OMEGA_MAX</span><span> </span><span>/</span><span> </span><span>max</span><span>(</span><span>abs</span><span>(w) </span><span>for</span><span> w </span><span>in</span><span> wref) </span><span>if</span><span> hit </span><span>else</span><span> </span><span>1.0</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>nu_hat </span><span>=</span><span> </span><span>mecanum_forward</span><span>(omega, lx, ly, r)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> [</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>pids[a].</span><span>update</span><span>(s </span><span>*</span><span> nu_ref[a], nu_hat[a], </span><span>0.0</span><span>, scaled)</span></div></div><div><div><div>19</div></div><div><span>        </span><span>for</span><span> a </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>i_cmd, scaled </span><span>=</span><span> </span><span>scale_to</span><span>(</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>mecanum_currents</span><span>(w, lx, ly, r, kt), plant.i_max</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里有三处要看。<strong>测量</strong>：控制器看到的是 <code>omega</code>（四个轮速，真值，没有噪声），车体系 PI 用 <code>mecanum_forward</code> 从轮速恢复 <span><span>ν^\hat\nu</span><span><span><span></span><span><span><span><span><span><span></span><span>ν</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>。<strong>缩放标志</strong>：<code>scaled</code> 在循环末尾更新、在下一拍的 <code>update</code> 里当 <code>hold_integral</code> 用，读到的是上一拍的值，和第 7 篇一样；逐轮截断这一支不用它，它的 <code>Pid</code> 自己带 <span><span>±20\pm20</span><span><span><span></span><span>±</span><span>20</span></span></span></span> A 限幅，<code>CLAMP</code> 照常工作。<strong>记录</strong>：<code>null_i</code> 是最终发给对象的那一组电流（限幅或缩放之后）的 <span><span>nTin^Ti</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span></span></span></span>，不是 <code>Pid</code> 内部的某个量。</p><p>车体系 PI 的整定在每个轴上一样：<span><span>Meffν˙=F−cvνM_{eff}\dot\nu = F - c_v\nu</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>e</span><span>f</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>ν</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span></span><span>−</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ν</span></span></span></span> 是一阶对象，按第 2 篇的公式放在 <span><span>ωn=30\omega_n = 30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span>、<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>body_pid_configs</span><span>(</span><span>plant</span><span>:</span><span> Mecanum</span><span>) -&gt; list[PidConfig]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""PI placed at WN, ZETA on each body axis: meff nu' = F - cv nu."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>cfgs </span><span>=</span><span> []</span></div></div><div><div><div>4</div></div><div><span>    </span><span>for</span><span> meff </span><span>in</span><span><span> plant.</span><span>inertia</span><span>():</span></span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>kp </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> </span><span>ZETA</span><span> </span><span>*</span><span> </span><span>WN</span><span> </span><span>*</span><span> meff </span><span>-</span><span> plant.cv</span></span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>cfgs.</span><span>append</span><span>(</span><span>PidConfig</span><span>(kp, </span><span>WN</span><span> </span><span>*</span><span> </span><span>WN</span><span> </span><span>*</span><span> meff, </span><span>0.0</span><span><span>, </span><span>MEC_TS</span><span>))</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> cfgs</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>C、C++、Rust 里有什么不同<a href="#ccrust-里有什么不同"><span>#</span></a></h2><p><code>mimo</code> 模块和两个新对象是 Python 版的逐行翻译，三种语言的 ch08 应用也是 Python 驱动的逐行翻译：同样的算例表、同样的整定、同样的接线，读同一份数据。区别在内存和类型上，一共这几处。</p><p><strong>2×2 和 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的存放。</strong> C 用 <code>const pl_real *</code>，调用者给长度 4 和 12 的数组，求逆失败返回 <code>-EINVAL</code>；C++ 是 <code>std::array&lt;T, 4&gt;</code> 和 <code>std::array&lt;T, 12&gt;</code>，求逆和 RGA 返回 <code>std::optional</code>；Rust 是 <code>[Real; 4]</code> 和 <code>[Real; 12]</code>，返回 <code>Option</code>。三种语言都没有堆分配（<code>csv.c</code> 除外）。最小范数电流在 C 里是这样，注意 <span><span>HTHH^TH</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span></span></span></span> 的对角元素直接除，没有矩阵求逆：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>pl_mecanum_currents</span><span><span>(</span><span>pl_real </span><span>lx</span><span>,</span><span> pl_real </span><span>ly</span><span>,</span><span> pl_real </span><span>r</span><span>,</span><span> pl_real </span><span>kt</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span>       </span><span>const</span><span> pl_real </span><span>*</span><span><span>w</span><span>,</span><span> pl_real </span></span><span>*</span><span><span>i</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>pl_real </span><span>h</span><span>[</span><span>12</span><span>], </span><span>g</span><span>[</span><span>3</span><span>], </span><span>a</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>5</div></div><div><span>  </span><span>int</span><span> k;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>  </span><span>pl_mecanum_h</span><span>(lx, ly, h);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>pl_mecanum_gram_diag</span><span>(lx, ly, g);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>a</span><span>[k] </span><span>=</span><span> </span><span>w</span><span>[k] </span><span>/</span><span> </span><span>g</span><span>[k];</span></div></div><div><div><div>11</div></div><div><span>  </span><span>for</span><span> (k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>4</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>i</span><span>[k] </span><span>=</span><span> r </span><span>/</span><span> kt </span><span>*</span><span> (</span><span>h</span><span>[</span><span>3</span><span> </span><span>*</span><span> k] </span><span>*</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>h</span><span>[</span><span>3</span><span> </span><span>*</span><span> k </span><span>+</span><span> </span><span>1</span><span>] </span><span>*</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>+</span></div></div><div><div><div>13</div></div><div><span>         </span><span>h</span><span>[</span><span>3</span><span> </span><span>*</span><span> k </span><span>+</span><span> </span><span>2</span><span>] </span><span>*</span><span> </span><span>a</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C 没有返回数组的办法，输出是调用者传进来的 <code>pl_real *i</code>；<code>pl_rga2</code> 和 <code>pl_static_decoupler</code> 则在行列式过小时返回 <code>-EINVAL</code>，调用者检查。同一个 RGA 在 C++ 里换成 <code>std::optional</code>，把“失败”放进返回类型：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>optional</span><span>&lt;std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>4</span><span>&gt;&gt; </span><span>Rga2</span><span>(</span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>4</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>g</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>d</span><span> </span><span>=</span><span> internal::</span><span>Det2</span><span>(</span><span>g</span><span>);</span></div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span><span> (</span><span>std</span><span>::</span></span><span>abs</span><span>(</span><span>d</span><span>) </span><span>&lt;</span><span> </span><span>T</span><span>(internal::</span><span>kSingular</span><span>)) </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>l11</span><span> </span><span>=</span><span> </span><span>g</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>g</span><span>[</span><span>3</span><span>] </span><span>/</span><span> </span><span>d</span><span>;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>l12</span><span> </span><span>=</span><span> </span><span>-</span><span>g</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>g</span><span>[</span><span>2</span><span>] </span><span>/</span><span> </span><span>d</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>return</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>4</span><span>&gt;</span><span>{</span><span>l11</span><span>, </span><span>l12</span><span>, </span><span>l12</span><span>, </span><span>l11</span><span>};</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Rust 版几乎一样，<code>Option&lt;[Real; 4]&gt;</code>，<code>fabs</code> 经 <code>real.rs</code> 的包装走 <code>libm</code>，所有函数都是自由函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>static_decoupler</span><span>(</span><span>g</span><span>: &amp;[</span><span>Real</span><span>; </span><span>4</span><span>]) -&gt; </span><span>Option</span><span>&lt;[</span><span>Real</span><span>; </span><span>4</span><span>]&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>gi</span><span><span> </span><span>=</span><span> </span></span><span>inv2</span><span>(</span><span>g</span><span>)?;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Some</span><span>([</span><span>gi</span><span>[</span><span>0</span><span>] * </span><span>g</span><span>[</span><span>0</span><span>], </span><span>gi</span><span>[</span><span>1</span><span>] * </span><span>g</span><span>[</span><span>3</span><span>], </span><span>gi</span><span>[</span><span>2</span><span>] * </span><span>g</span><span>[</span><span>0</span><span>], </span><span>gi</span><span>[</span><span>3</span><span>] * </span><span>g</span><span>[</span><span>3</span><span>]])</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>?</code> 在这里替 Python 的 <code>raise</code> 和 C 的 <code>if (ret) return ret</code> 干活：<code>inv2</code> 失败就整个函数返回 <code>None</code>。</p><p><strong>延迟线。</strong> Wood–Berry 要四条 <code>DelayLine</code>。C 是 <code>struct pl_delay_line lines[4]</code>，每条是 <code>PL_MAX_DELAY</code>（256）个 <code>pl_real</code>，<code>pl_delay_init</code> 失败返回 <code>-EINVAL</code>；C++ 是 <code>std::vector&lt;DelayLine&lt;double&gt;&gt;</code>（驱动里的容器不进库），Rust 是 <code>Vec&lt;DelayLine&gt;</code>。最长的延迟是 70 个采样，在 256 以内。C 应用里的初始化和每一拍的推进是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>g0</span><span>[i] </span><span>=</span><span> </span><span>p</span><span>.</span><span>k</span><span>[i];</span></div></div><div><div><div>3</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>ret)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>ret </span><span>=</span><span> </span><span>pl_delay_init</span><span>(</span><span>&amp;</span><span>lines</span><span>[i], </span><span>p</span><span>.</span><span>delay</span><span>[i], </span><span>0.0</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>/* ... */</span></div></div><div><div><div>7</div></div><div><span>in</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>pl_delay_push</span><span>(</span><span>&amp;</span><span>lines</span><span>[</span><span>0</span><span><span>]</span><span>,</span><span> u1</span><span>);</span></span></div></div><div><div><div>8</div></div><div><span>in</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>pl_delay_push</span><span>(</span><span>&amp;</span><span>lines</span><span>[</span><span>1</span><span><span>]</span><span>,</span><span> u2</span><span>);</span></span></div></div><div><div><div>9</div></div><div><span>in</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>pl_delay_push</span><span>(</span><span>&amp;</span><span>lines</span><span>[</span><span>2</span><span><span>]</span><span>,</span><span> u1</span><span>);</span></span></div></div><div><div><div>10</div></div><div><span>in</span><span>[</span><span>3</span><span>] </span><span>=</span><span> </span><span>pl_delay_push</span><span>(</span><span>&amp;</span><span>lines</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> u2</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong><code>Pid</code> 数组。</strong> 逐轮的算例要四个 <code>Pid</code>，车体系要三个。C 是 <code>struct pl_pid pids[4]</code>，用 <code>pl_pid_init</code> 就地初始化；C++ 的 <code>Pid&lt;double&gt;</code> 只能通过 <code>Create</code> 构造，所以先 <code>Create</code>、检查、再 <code>push_back</code> 进 <code>std::vector</code>；Rust 同理，<code>Pid::new</code> 返回 <code>Result</code>，<code>?</code> 向上传：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>auto</span><span> </span><span>pid</span><span> </span><span>=</span><span><span> pidlab::</span><span>Pid</span><span>&lt;</span></span><span>double</span><span>&gt;::</span><span>Create</span><span>(</span><span>cfg</span><span>);</span></div></div><div><div><div>2</div></div><div><span>if</span><span> (</span><span>!</span><span>pid</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>3</div></div><div><span>pids</span><span>.</span><span>push_back</span><span>(</span><span>*</span><span>pid</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>缩放。</strong> <code>scale_to</code> 原地修改向量并返回“是否缩放了”：C 用 <code>static bool scale_to(pl_real *v, int n, pl_real limit)</code>，C++ 是 <code>template &lt;std::size_t N&gt; bool ScaleTo(std::array&lt;double, N&gt;&amp;, double)</code>，Rust 是 <code>fn scale_to(v: &amp;mut [Real], limit: Real) -&gt; bool</code>，Python 返回元组。C 版如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>static</span><span> </span><span>bool</span><span> </span><span>scale_to</span><span><span>(</span><span>pl_real </span></span><span>*</span><span><span>v</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> pl_real </span><span>limit</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>pl_real peak </span><span>=</span><span> </span><span>0.0</span><span>, s;</span></div></div><div><div><div>4</div></div><div><span>  </span><span>int</span><span> i;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> (</span><span>PL_FABS</span><span>(</span><span>v</span><span>[i]) </span><span>&gt;</span><span> peak)</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>peak </span><span>=</span><span> </span><span>PL_FABS</span><span>(</span><span>v</span><span>[i]);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>if</span><span> (peak </span><span>&lt;=</span><span> limit)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>s </span><span>=</span><span> limit </span><span>/</span><span> peak;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>v</span><span>[i] </span><span>=</span><span> </span><span>v</span><span>[i] </span><span>*</span><span> s;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>麦轮循环。</strong> 对象的方程在 <code>plants.c</code>，向心项和 Python 同样的顺序：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* centripetal coupling of the body-frame velocity components */</span></div></div><div><div><div>2</div></div><div><span>f</span><span>[</span><span>0</span><span>] </span><span>+=</span><span> p</span><span>-&gt;</span><span>m </span><span>*</span><span> </span><span>x</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>x</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>3</div></div><div><span>f</span><span>[</span><span>1</span><span>] </span><span>-=</span><span> p</span><span>-&gt;</span><span>m </span><span>*</span><span> </span><span>x</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>x</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>4</div></div><div><span>for</span><span> (k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>dx</span><span>[k] </span><span>=</span><span> </span><span>f</span><span>[k] </span><span>/</span><span> </span><span>meff</span><span>[k];</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应用里三种控制器的最后一支（车体系 PI）在三种语言里是这样的。C：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>2</div></div><div><span>  </span><span>if</span><span> (</span><span>PL_FABS</span><span>(</span><span>wref</span><span><span>[i]</span><span>) </span></span><span>&gt;</span><span> peak)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>peak </span><span>=</span><span> </span><span>PL_FABS</span><span>(</span><span>wref</span><span><span>[i]</span><span>);</span></span></div></div><div><div><div>4</div></div><div><span>if</span><span> (peak </span><span>&gt;</span><span> OMEGA_MAX)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>s </span><span>=</span><span> OMEGA_MAX </span><span>/</span><span> peak;</span></div></div><div><div><div>6</div></div><div><span>pl_mecanum_forward</span><span><span>(</span><span>p.lx</span><span>,</span><span> p.ly</span><span>,</span><span> p.r</span><span>,</span><span> omega</span><span>,</span><span> nu_hat</span><span>);</span></span></div></div><div><div><div>7</div></div><div><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>8</div></div><div><span>  </span><span>w</span><span>[i] </span><span>=</span><span> </span><span>pl_pid_update</span><span>(</span><span>&amp;</span><span>pids</span><span><span>[i]</span><span>,</span><span> s </span></span><span>*</span><span> </span><span>nu_ref</span><span><span>[i]</span><span>,</span></span></div></div><div><div><div>9</div></div><div><span><span>           </span></span><span>nu_hat</span><span><span>[i]</span><span>,</span><span> </span></span><span>0.0</span><span><span>,</span><span> scaled</span><span>);</span></span></div></div><div><div><div>10</div></div><div><span>pl_mecanum_currents</span><span><span>(</span><span>p.lx</span><span>,</span><span> p.ly</span><span>,</span><span> p.r</span><span>,</span><span> p.kt</span><span>,</span><span> w</span><span>,</span><span> icmd</span><span>);</span></span></div></div><div><div><div>11</div></div><div><span>scaled </span><span>=</span><span> </span><span>scale_to</span><span><span>(</span><span>icmd</span><span>,</span><span> </span></span><span>4</span><span><span>,</span><span> p.i_max</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>double</span><span> </span><span>peak</span><span> </span><span>=</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>2</div></div><div><span>for</span><span> (</span><span>double</span><span> </span><span>a</span><span> : </span><span>wref</span><span>) </span><span>peak</span><span> </span><span>=</span><span> std::</span><span>max</span><span>(</span><span>peak</span><span>, std::</span><span>abs</span><span>(</span><span>a</span><span>));</span></div></div><div><div><div>3</div></div><div><span>const</span><span> </span><span>double</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>peak</span><span> </span><span>&gt;</span><span> </span><span>kOmegaMax</span><span> </span><span>?</span><span> </span><span>kOmegaMax</span><span> </span><span>/</span><span> </span><span>peak</span><span> </span><span>:</span><span> </span><span>1.0</span><span>;</span></div></div><div><div><div>4</div></div><div><span>const</span><span> </span><span>auto</span><span> </span><span>nu_hat</span><span> </span><span>=</span><span> pidlab::</span><span>MecanumForward</span><span>(</span><span>p</span><span>.</span><span>lx</span><span>, </span><span>p</span><span>.</span><span>ly</span><span>, </span><span>p</span><span>.</span><span>r</span><span>, </span><span>omega</span><span>);</span></div></div><div><div><div>5</div></div><div><span>std::</span><span>array</span><span>&lt;double</span><span>, </span><span>3</span><span>&gt;</span><span> </span><span>w</span><span>;</span></div></div><div><div><div>6</div></div><div><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>  </span><span>w</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>pids</span><span>[</span><span>i</span><span>].</span><span>Update</span><span>(</span><span>s</span><span> </span><span>*</span><span> </span><span>nu_ref</span><span>[</span><span>i</span><span>], </span><span>nu_hat</span><span>[</span><span>i</span><span>], </span><span>0.0</span><span>, </span><span>scaled</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div><span>icmd</span><span> </span><span>=</span><span> pidlab::</span><span>MecanumCurrents</span><span>(</span><span>p</span><span>.</span><span>lx</span><span>, </span><span>p</span><span>.</span><span>ly</span><span>, </span><span>p</span><span>.</span><span>r</span><span>, </span><span>p</span><span>.</span><span>kt</span><span>, </span><span>w</span><span>);</span></div></div><div><div><div>10</div></div><div><span>scaled</span><span> </span><span>=</span><span> </span><span>ScaleTo</span><span>(</span><span>icmd</span><span>, </span><span>p</span><span>.</span><span>i_max</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Rust：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>peak</span><span><span> </span><span>=</span><span> </span></span><span>wref</span><span>.</span><span>iter</span><span>().</span><span>fold</span><span>(</span><span>0.0</span><span><span>, </span><span>|</span></span><span>m</span><span>: </span><span>Real</span><span>, </span><span>a</span><span><span>|</span><span> </span></span><span>m</span><span>.</span><span>max</span><span>(</span><span>a</span><span>.</span><span>abs</span><span>()));</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>peak</span><span><span> </span><span>&gt;</span><span> </span></span><span>OMEGA_MAX</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>OMEGA_MAX</span><span> / </span><span>peak</span></div></div><div><div><div>4</div></div><div><span>} </span><span>else</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>1.0</span></div></div><div><div><div>6</div></div><div><span>};</span></div></div><div><div><div>7</div></div><div><span>let</span><span> </span><span>nu_hat</span><span><span> </span><span>=</span><span> </span></span><span>mecanum_forward</span><span>(</span><span>p</span><span>.lx, </span><span>p</span><span>.ly, </span><span>p</span><span>.r, &amp;</span><span>omega</span><span>);</span></div></div><div><div><div>8</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>3</span><span>];</span></div></div><div><div><div>9</div></div><div><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>w</span><span>[</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>pids</span><span>[</span><span>i</span><span>].</span><span>update</span><span>(</span><span>s</span><span> * </span><span>nu_ref</span><span>[</span><span>i</span><span>], </span><span>nu_hat</span><span>[</span><span>i</span><span>], </span><span>0.0</span><span>, </span><span>scaled</span><span>);</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div><span>icmd</span><span><span> </span><span>=</span><span> </span></span><span>mecanum_currents</span><span>(</span><span>p</span><span>.lx, </span><span>p</span><span>.ly, </span><span>p</span><span>.r, </span><span>p</span><span>.kt, &amp;</span><span>w</span><span>);</span></div></div><div><div><div>13</div></div><div><span>scaled</span><span><span> </span><span>=</span><span> </span></span><span>scale_to</span><span>(&amp;</span><span>mut</span><span> </span><span>icmd</span><span>, </span><span>p</span><span>.i_max);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>差别是 C 的数组都是调用者给的栈数组，函数写进 <code>out</code> 参数；C++ 和 Rust 的固定长度数组按值返回，赋值就是复制 3 或 4 个 <code>double</code>，没有堆；Rust 的 <code>fold</code> 取最大值对应 C 的 <code>if</code> 循环。<strong>标量类型</strong>这一篇仍是 <code>double</code>（<code>pl_real</code>、<code>T</code>、<code>Real</code>）。</p><section><h3>编译和风格检查<a href="#编译和风格检查"><span>#</span></a></h3><p>编译参数和前几篇一样：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code> 加同样的警告，Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>。风格检查：C 用内核的 <code>checkpatch.pl</code>（<code>--no-tree</code>，忽略 SPDX 和 typedef 两项），0 errors、0 warnings；C++ 用 clang-format 的 Google 风格（<code>-Werror</code>）；Python 用 <code>ruff format --check</code> 和 <code>ruff check</code>，行宽 79；Rust 用 <code>cargo fmt --check</code>。四项都在提交前过了。</p><p>一个小插曲：<code>checkpatch.pl</code> 对 <code>mimo.h</code> 里<strong>第一个</strong>用到 <code>pl_real</code> 的声明报了 “need consistent spacing around ’*’”，因为它不认识这个类型名。在 <code>#include</code> 之后放一行 <code>#define PL_MEC_WHEELS 4</code> 就不报了（它显然是靠文件里已有的内容猜类型）。这个宏是真的在表达“4 个轮子”，我就留着了；更干净的写法我没有找到。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch08</code> 一次跑完。7 个算例，每种语言 7 个文件，21 个对比文件。下面的输出把中间 15 行 <code>ok</code> 裁掉了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch08</span></div></div><div><div><div>2</div></div><div><span>ch08: C wrote 7 cases to out/ch08</span></div></div><div><div><div>3</div></div><div><span>ch08: C++ wrote 7 cases to out/ch08</span></div></div><div><div><div>4</div></div><div><span>ch08: Rust wrote 7 cases to out/ch08</span></div></div><div><div><div>5</div></div><div><span>ok   mec_body_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   mec_body_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   mec_body_rs.csv: max diff 2.220e-16 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>... （中间 15 行 ok 省略）</span></div></div><div><div><div>9</div></div><div><span>ok   wb_wrongpair_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   wb_wrongpair_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   wb_wrongpair_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>Wood-Berry G(0) = [[12.8, -18.9], [6.6, -19.4]], det = -123.580</span></div></div><div><div><div>13</div></div><div><span>RGA = [[2.0094, -1.0094], [-1.0094, 2.0094]] (matches numpy G * inv(G)^T)</span></div></div><div><div><div>14</div></div><div><span>loop 1 gain with loop 2 open 12.800, closed 6.370 (= g11 / lambda11 = 6.370)</span></div></div><div><div><div>15</div></div><div><span>off-diagonal pair (y1 from u2): gain with the other loop open -18.900, closed 18.724 (= g12 / lambda12 = 18.724)</span></div></div><div><div><div>16</div></div><div><span>D = [[2.0094, 2.9670], [0.6836, 2.0094]], G(0) D = diag(12.800, -19.400) (off-diagonal 4.0e-15)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>SIMC PI (tauc = theta), computed by simc_pi:</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>g11: k =   12.8, tau = 16.7, theta = 1.0  -&gt;  Kp =   0.6523, Ti =   8.0</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>g12: k =  -18.9, tau = 21.0, theta = 3.0  -&gt;  Kp =  -0.1852, Ti =  21.0</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>g21: k =    6.6, tau = 10.9, theta = 7.0  -&gt;  Kp =   0.1180, Ti =  10.9</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>g22: k =  -19.4, tau = 14.4, theta = 3.0  -&gt;  Kp =  -0.1237, Ti =  14.4</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>Wood-Berry, 200 min (setpoint y1 +1 at 10 min, y2 +1 at 100 min)</span></div></div><div><div><div>25</div></div><div><span>case              IAE1  max|e2|    IAE2  max|e1|  y1 max   y1 end   y2 end    max|y|</span></div></div><div><div><div>26</div></div><div><span>wb_single1        3.15    0.863   48.44    0.000   1.172    1.000    0.516      1.17</span></div></div><div><div><div>27</div></div><div><span>wb_decentral      4.37    0.859   11.87    0.238   1.172    1.001    0.996      1.24</span></div></div><div><div><div>28</div></div><div><span>wb_wrongpair    677.05   59.499 173824.31 4999.584   0.611 -4998.584 8730.793  8.73e+03</span></div></div><div><div><div>29</div></div><div><span>wb_decoupled     15.58    0.983    7.29    0.467   1.656    1.029    0.996      1.66</span></div></div><div><div><div>30</div></div><div><span>wb_decentral: y2 first leaves +-0.05 at 17.2 min (sign +1), min y2 -0.107</span></div></div><div><div><div>31</div></div><div><span>wb_decoupled: y2 first leaves +-0.05 at 13.1 min (sign -1), min y2 -0.983</span></div></div><div><div><div>32</div></div><div><span>wb_decoupled: max |y1 - 1| in [120, 130) min = 0.139</span></div></div><div><div><div>33</div></div><div><span>wb_decoupled: max |y1 - 1| in [190, 200) min = 0.035</span></div></div><div><div><div>34</div></div><div><span>wb_decoupled: ringing period 4.15 min</span></div></div><div><div><div>35</div></div><div><span>wb_wrongpair: |y2| grows at 0.0498 per min over the last 100 min, doubling every 13.9 min</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>H (rows FL, FR, RL, RR):</span></div></div><div><div><div>38</div></div><div><span>[[ 1.  -1.  -0.4]</span></div></div><div><div><div>39</div></div><div><span><span> </span></span><span>[ 1.   1.   0.4]</span></div></div><div><div><div>40</div></div><div><span><span> </span></span><span>[ 1.   1.  -0.4]</span></div></div><div><div><div>41</div></div><div><span><span> </span></span><span>[ 1.  -1.   0.4]]</span></div></div><div><div><div>42</div></div><div><span>H^T H = diag(4, 4, 0.64)</span></div></div><div><div><div>43</div></div><div><span>max off-diagonal of H^T H = 0.0e+00</span></div></div><div><div><div>44</div></div><div><span>H^T n = [0. 0. 0.] for n = [1, 1, -1, -1] / 2</span></div></div><div><div><div>45</div></div><div><span>forward kinematics: round trip error 4.4e-16, difference to numpy pinv 2.2e-16</span></div></div><div><div><div>46</div></div><div><span>min-norm currents: wrench round trip error 1.1e-16, difference to numpy pinv 5.6e-17, null component -2.8e-17</span></div></div><div><div><div>47</div></div><div><span>M_eff = diag(20.6925, 20.6925, 0.7108)</span></div></div><div><div><div>48</div></div><div><span>commanded wheel speeds (rad/s): -15.79, 68.42, 36.84, 15.79; uniform scale to fit 50: 0.7308</span></div></div><div><div><div>49</div></div><div><span>wheel PI seen from the body axes: wn = 30.1, 30.1, 64.9 rad/s (body PI is placed at 30 on all three)</span></div></div><div><div><div>50</div></div><div><span>clipped setpoints (rad/s): -15.79, 50.00, 36.84, 15.79; twist r H+ w = (1.650, 1.650, 2.125), null part n^T w = -9.21 rad/s, kp * that = -38.4 A</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>case              ang xy  ang 3d  |v_xy|     wz  dev end  dev max</span></div></div><div><div><div>53</div></div><div><span>mec_wheel_clip    11.976  15.898   2.383  1.256   2.1821   2.1821</span></div></div><div><div><div>54</div></div><div><span>mec_wheel_scale    0.000   0.001   2.067  2.192   0.0947   0.2204</span></div></div><div><div><div>55</div></div><div><span>mec_body           0.001   0.001   2.067  2.192   0.2129   0.2290</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>case              rms null  null@.1  null@.5   null@2  min null   at t  mean |i|</span></div></div><div><div><div>58</div></div><div><span>mec_wheel_clip   2.854e+01  -20.000  -32.925  -29.396   -36.081  0.403    14.698</span></div></div><div><div><div>59</div></div><div><span>mec_wheel_scale  2.873e-13   -0.000   -0.000   -0.000    -0.000  1.999     4.145</span></div></div><div><div><div>60</div></div><div><span>mec_body         6.712e-16   -0.000   -0.000    0.000    -0.000  0.146     4.145</span></div></div><div><div><div>61</div></div><div><span>mec_wheel_clip currents at 0.1 s (A): [-20.0, 20.0, 20.0, 20.0]</span></div></div><div><div><div>62</div></div><div><span>mec_body currents at 2 s (A): [-8.291, 0.266, -0.081, -7.944]</span></div></div><div><div><div>63</div></div><div><span>figure: figures/ch08/wood-berry-interaction.svg</span></div></div><div><div><div>64</div></div><div><span>figure: figures/ch08/wood-berry-decoupled.svg</span></div></div><div><div><div>65</div></div><div><span>figure: figures/ch08/mecanum-paths.svg</span></div></div><div><div><div>66</div></div><div><span>figure: figures/ch08/mecanum-null-current.svg</span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>ch08 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>21 个文件里 18 个的最大差异是严格的 <code>0.000e+00</code>。三个例外都是 Rust 的麦轮文件：<code>mec_body_rs</code> 是 2.220e-16，<code>mec_wheel_clip_rs</code> 是 1.110e-16，<code>mec_wheel_scale_rs</code> 是 5.551e-17，全部远低于 1e-9 的容差。和第 7 篇不同，这一篇的循环里有 <code>sin</code> 和 <code>cos</code>（麦轮的位姿），Rust 走的是 <code>libm</code>，C 走 glibc，最后一位不保证相同；Wood–Berry 的四个算例没有三角函数，三种语言的差异严格是 0，连会放大舍入差的发散算例（<code>wb_wrongpair</code>）也是 0，因为三种语言做的是同样顺序的同样运算，没有舍入差可放大。我猜麦轮这三个 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级的差就来自 <code>sin</code> 和 <code>cos</code>，但我没有逐项去验证。</p><section><h3>Wood–Berry：耦合、配对和解耦<a href="#woodberry耦合配对和解耦"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch08-wood-berry-interaction.svg" alt="Wood–Berry 的两个回路：只闭回路 1 时 y2 停在 0.516，两个回路都闭时 y1 和 y2 都回到 1；配成非对角后 y1、y2 一路发散到 −4999 和 8731" /><figcaption>Wood–Berry 的两个回路：只闭回路 1 时 y2 停在 0.516，两个回路都闭时 y1 和 y2 都回到 1；配成非对角后 y1、y2 一路发散到 −4999 和 8731</figcaption></figure><p></p><p>200 分钟，<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的设定值在第 10 分钟阶跃 +1，<span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的设定值在第 100 分钟阶跃 +1。对角配对的 SIMC PI，四个算例的指标（IAE 的单位是成分乘分钟，<code>max|e|</code> 是误差的最大绝对值）：</p>

<table><thead><tr><th>算例</th><th><span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> IAE（10 到 100 分钟）</th><th><span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 最大偏差（10 到 100 分钟）</th><th><span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> IAE（100 到 200 分钟）</th><th><span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 最大偏差（100 到 200 分钟）</th><th><span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 峰值（10 到 100 分钟）</th><th>200 分钟末的 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th></tr></thead><tbody><tr><td><code>wb_single1</code></td><td>3.15</td><td>0.863</td><td>48.44</td><td>0.000</td><td>1.172</td><td>1.000、0.516</td></tr><tr><td><code>wb_decentral</code></td><td>4.37</td><td>0.859</td><td>11.87</td><td>0.238</td><td>1.172</td><td>1.001、0.996</td></tr><tr><td><code>wb_wrongpair</code></td><td>677.05</td><td>59.499</td><td>173824.31</td><td>4999.584</td><td>0.611</td><td>−4998.584、8730.793</td></tr><tr><td><code>wb_decoupled</code></td><td>15.58</td><td>0.983</td><td>7.29</td><td>0.467</td><td>1.656</td><td>1.029、0.996</td></tr></tbody></table><p><strong>回路 2 开环时，<span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 没人管。</strong> <code>wb_single1</code> 只闭回路 1：<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 17.2% 的超调后稳在 1，但 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 被 <span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 顺带拖到 0.516，正好是 <span><span>g21/g11=6.6/12.8=0.516g_{21}/g_{11} = 6.6/12.8 = 0.516</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>6.6/12.8</span><span></span><span>=</span><span></span></span><span><span></span><span>0.516</span></span></span></span>。这就是耦合：一个没人管的输出，被另一个回路推着走。</p><p><strong>两个回路都闭上。</strong> <code>wb_decentral</code> 里 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 被拉回 0，<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的峰值（1.172，超调 17.2%）和单回路的一模一样，说明峰值出现在回路 2 的作用到达 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 之前。<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 IAE 从 3.15 涨到 4.37，多了 38.7%，这是回路 2 反过来搅 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的代价。第 100 分钟 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 阶跃时，<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 被搅动，最大偏差 0.238。最后两个输出都回到设定值（1.001 和 0.996）。</p><p><strong>配成非对角。</strong> <code>wb_wrongpair</code> 把 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 交给 <span><span>u2u_2</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 交给 <span><span>u1u_1</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，每个 PI 单独看都是 SIMC 设计出来的好回路，合起来却发散：200 分钟末 <span><span>y1=−4998.6y_1 = -4998.6</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>4998.6</span></span></span></span>，<span><span>y2=8730.8y_2 = 8730.8</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>8730.8</span></span></span></span>，<span><span>∣y2∣|y_2|</span><span><span><span></span><span>∣</span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span> 在最后 100 分钟的指数增长率是 0.0498 每分钟，<strong>每 13.9 分钟翻一倍</strong>。前面 RGA 那一节的预言兑现了：非对角配对的有效增益从 <span><span>−18.9-18.9</span><span><span><span></span><span>−</span><span>18.9</span></span></span></span> 变成 <span><span>+18.72+18.72</span><span><span><span></span><span>+</span><span>18.72</span></span></span></span>（<code>report</code> 打印了这两个数），积分控制器的符号对不上了。（这也是仿真的好处：对象不会真的爆炸。真实的塔有阀门限幅，发散会被限幅截住，但那也不是你想要的“控制”。）</p><p></p><figure><img src="/assets/images/pid-control/ch08-wood-berry-decoupled.svg" alt="静态解耦加同样的 PI：y2 阶跃后的 IAE 更小，但 y1 在 10 分钟的阶跃上超调到 1.66 并且长时间振荡" /><figcaption>静态解耦加同样的 PI：y2 阶跃后的 IAE 更小，但 y1 在 10 分钟的阶跃上超调到 1.66 并且长时间振荡</figcaption></figure><p></p><p><strong>静态解耦。</strong> 在同一对 PI 前面加 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span>，是这一节里最需要小心的结果。好的一面：<span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在第 100 分钟阶跃的 IAE 从 11.87 降到 7.29（降了 38.6%）。坏的一面：<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的超调从 17.2% 涨到 65.6%，10 到 100 分钟的 IAE 从 4.37 涨到 15.58（3.6 倍），<span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 阶跃时的最大偏差从 0.238 涨到 0.467，而且 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 长时间振荡：振荡周期 4.15 分钟，第 120 到 130 分钟的振幅是 0.139，190 到 200 分钟还有 0.035，到 200 分钟末 <span><span>y1=1.029y_1 = 1.029</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.029</span></span></span></span> 还没有稳下来。</p><p>我对原因的猜测，得到两个数的支持：<span><span>D11=2.0094D_{11}=2.0094</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.0094</span></span></span></span>，所以 <span><span>v1v_1</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 通过 <span><span>g11g_{11}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（延迟 1 分钟）送到 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的增益是原来的两倍，而抵消它的那条通道 <span><span>g12D21g_{12}D_{21}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>D</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 延迟 3 分钟，晚到 2 分钟；一个晚到的抵消，在暂态里等价于回路增益翻倍，相角裕度跟着掉。另一个证据是 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的第一次反向偏移：<code>wb_decoupled</code> 在 13.1 分钟 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 离开 <span><span>±0.05\pm0.05</span><span><span><span></span><span>±</span><span>0.05</span></span></span></span>（符号为负，最低 −0.983），正好是第 10 分钟的阶跃加上 <span><span>g22g_{22}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>22</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 3 分钟延迟；而它本想抵消的耦合 <span><span>g21g_{21}</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span><span>21</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 要到第 17 分钟才到（<code>wb_decentral</code> 里 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 17.2 分钟才离开 <span><span>±0.05\pm0.05</span><span><span><span></span><span>±</span><span>0.05</span></span></span></span>，符号为正，最低 −0.107）。静态解耦器把抵消量提前 4 分钟打出去了。我没有做控制器重新整定的对照，所以“同样的 PI”是故意的：解耦之后应当重新整定，这个实验只是说明：<strong><span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 不是免费的午餐，同一个控制器不能不加检查地接在它后面。</strong></p></section><section><h3>麦轮：三种处理，一份数据<a href="#麦轮三种处理一份数据"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch08-mecanum-paths.svg" alt="麦轮在 0.1 s 收到 (2.0, 2.0, 3.0) 的指令：逐轮等比缩放和车体系 PI 贴着缩放后的理想轨迹转弯，逐轮截断在 0.5 s 之后角速度掉到 1.25 rad/s，轨迹拉直飞出去" /><figcaption>麦轮在 0.1 s 收到 (2.0, 2.0, 3.0) 的指令：逐轮等比缩放和车体系 PI 贴着缩放后的理想轨迹转弯，逐轮截断在 0.5 s 之后角速度掉到 1.25 rad/s，轨迹拉直飞出去</figcaption></figure><p></p><p>0.1 s 收到指令，2 s 的仿真，指标取自 <code>report</code>（“方向误差”是 0.5 s 到 2 s 平均速度和指令的夹角，<span><span>(vx,vy)(v_x,v_y)</span><span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 的夹角看平面内的方向，三维的那一列是 <span><span>(vx,vy,Lωz)(v_x,v_y,L\omega_z)</span><span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>L</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 的夹角，让转动和平动用同一个长度单位；“偏离”是仿真位置和“等比缩放后的指令速度恒定、从 0.1 s 开始”的理想路径的距离，包含了加速段的滞后，所以最好的那条也有 0.1 m 量级）：</p>

<table><thead><tr><th>算例</th><th><span><span>(vx,vy)(v_x,v_y)</span><span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 方向误差</th><th>三维方向误差</th><th><span><span>∣vxy∣\lvert v_{xy}\rvert</span><span><span><span></span><span>∣</span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>x</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span> (m/s)</th><th><span><span>ωz\omega_z</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> (rad/s)</th><th>2 s 末偏离</th><th>最大偏离</th></tr></thead><tbody><tr><td><code>mec_wheel_clip</code></td><td>11.976°</td><td>15.898°</td><td>2.383</td><td>1.256</td><td>2.1821 m</td><td>2.1821 m</td></tr><tr><td><code>mec_wheel_scale</code></td><td>0.000°</td><td>0.001°</td><td>2.067</td><td>2.192</td><td>0.0947 m</td><td>0.2204 m</td></tr><tr><td><code>mec_body</code></td><td>0.001°</td><td>0.001°</td><td>2.067</td><td>2.192</td><td>0.2129 m</td><td>0.2290 m</td></tr></tbody></table><p>缩放后的指令速度是 <span><span>0.7308×(2,2,3)0.7308\times(2,2,3)</span><span><span><span></span><span>0.7308</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span>，<span><span>∣vxy∣=0.7308×2.828=2.067\lvert v_{xy}\rvert = 0.7308\times2.828=2.067</span><span><span><span></span><span>∣</span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>x</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7308</span><span></span><span>×</span><span></span></span><span><span></span><span>2.828</span><span></span><span>=</span><span></span></span><span><span></span><span>2.067</span></span></span></span> m/s，<span><span>ωz=2.192\omega_z=2.192</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.192</span></span></span></span> rad/s，两个“等比”算例都稳稳地走到了；截断那一个，<span><span>vx=2.00v_x=2.00</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.00</span></span></span></span>、<span><span>vy=1.30v_y=1.30</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.30</span></span></span></span>、<span><span>ωz=1.25\omega_z=1.25</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.25</span></span></span></span>：速度大了（<span><span>∣vxy∣=2.383|v_{xy}|=2.383</span><span><span><span></span><span>∣</span><span><span>v</span><span><span><span><span><span><span></span><span><span><span>x</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>2.383</span></span></span></span>），转得慢了，方向偏了 11.98°，2 s 末位置偏离理想路径 2.18 m，是另外两个的 10 倍以上。</p><p>要多说一句，这个偏差不是“截断后的目标本来就变了”这么简单。截断后的轮速设定值，通过 <span><span>rH+rH^+</span><span><span><span></span><span>r</span><span><span>H</span><span><span><span><span><span><span></span><span><span>+</span></span></span></span></span></span></span></span></span></span></span> 变回车体速度，是 <span><span>(1.650, 1.650, 2.125)(1.650,\,1.650,\,2.125)</span><span><span><span></span><span>(</span><span>1.650</span><span>,</span><span></span><span></span><span>1.650</span><span>,</span><span></span><span></span><span>2.125</span><span>)</span></span></span></span>，对 <span><span>(vx,vy)(v_x,v_y)</span><span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 来说方向没变。实际的稳态 <span><span>(2.00, 1.30, 1.25)(2.00,\,1.30,\,1.25)</span><span><span><span></span><span>(</span><span>2.00</span><span>,</span><span></span><span></span><span>1.30</span><span>,</span><span></span><span></span><span>1.25</span><span>)</span></span></span></span> 离这个目标很远，原因是下面这张图。</p><p></p><figure><img src="/assets/images/pid-control/ch08-mecanum-null-current.svg" alt="零空间电流：逐轮截断在 0.1 s 就是 −20 A，0.4 s 到 −36 A，稳态 −29.4 A；另外两种全程为 0。下图是截断算例的四个轮子，左前一直贴着 −20 A" /><figcaption>零空间电流：逐轮截断在 0.1 s 就是 −20 A，0.4 s 到 −36 A，稳态 −29.4 A；另外两种全程为 0。下图是截断算例的四个轮子，左前一直贴着 −20 A</figcaption></figure><p></p><p>零空间电流 <span><span>nTin^Ti</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span></span></span></span>，<code>report</code> 取了几个时刻：</p>

<table><thead><tr><th>算例</th><th>RMS（0.1 s 以后）</th><th>0.1 s</th><th>0.5 s</th><th>2 s</th><th>1 s 之后每轮平均 <span><span>∣i∣\lvert i\rvert</span><span><span><span></span><span>∣</span><span>i</span><span>∣</span></span></span></span></th></tr></thead><tbody><tr><td><code>mec_wheel_clip</code></td><td>28.54 A</td><td>−20.00 A</td><td>−32.93 A</td><td>−29.40 A</td><td>14.698 A</td></tr><tr><td><code>mec_wheel_scale</code></td><td><span><span>2.9×10−132.9\times10^{-13}</span><span><span><span></span><span>2.9</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> A</td><td>0</td><td>0</td><td>0</td><td>4.145 A</td></tr><tr><td><code>mec_body</code></td><td><span><span>6.7×10−166.7\times10^{-16}</span><span><span><span></span><span>6.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> A</td><td>0</td><td>0</td><td>0</td><td>4.145 A</td></tr></tbody></table><p>最后一栏的 14.698 A 和 4.145 A：等比缩放和车体系 PI 在稳态下每个轮子平均 4.145 A，这不是 0，因为向心力 <span><span>mωzvm\omega_zv</span><span><span><span></span><span>m</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>v</span></span></span></span> 要有电流来提供（四个轮子的稳态电流约是 <span><span>(−8.29, 0.27, −0.08, −7.94)(-8.29,\ 0.27,\ -0.08,\ -7.94)</span><span><span><span></span><span>(</span><span>−</span><span>8.29</span><span>,</span><span> </span><span></span><span>0.27</span><span>,</span><span> </span><span></span><span>−</span><span>0.08</span><span>,</span><span> </span><span></span><span>−</span><span>7.94</span><span>)</span></span></span></span> A）。截断那一个平均 14.7 A，每个轮子都吃着零空间的电流，其中左前轮一直贴着 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span> A 的限幅，车却没走到目标。按我在第 1 篇里取的假设，<span><span>Kt=0.3K_t = 0.3</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span> N·m/A，M3508 的额定转矩 3 N·m 约是 10 A，所以这 14.7 A 的平均值是超过额定的，我没有往下算发热。</p></section><section><h3>没有按直觉发生的事<a href="#没有按直觉发生的事"><span>#</span></a></h3><p>下面三件事，我动手之前的直觉都是错的或者只对一半。</p><p><strong>我原以为截断造成的零空间电流会越攒越多。</strong> 理由是四个逐轮积分器互相较劲。结果它不是单调增长：0.1 s 的第一拍四个轮子的比例项都超过了 20 A，各自被截到 <span><span>(−20, +20, +20, +20)(-20,\ +20,\ +20,\ +20)</span><span><span><span></span><span>(</span><span>−</span><span>20</span><span>,</span><span> </span><span></span><span>+</span><span>20</span><span>,</span><span> </span><span></span><span>+</span><span>20</span><span>,</span><span> </span><span></span><span>+</span><span>20</span><span>)</span></span></span></span> A，<span><span>nTi=−20n^Ti=-20</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>20</span></span></span></span> A；最深到 <span><span>−36.08-36.08</span><span><span><span></span><span>−</span><span>36.08</span></span></span></span> A（0.403 s），之后稳在 <span><span>−29.40-29.40</span><span><span><span></span><span>−</span><span>29.40</span></span></span></span> A。比例项单独就占了一大半（<span><span>−38.4-38.4</span><span><span><span></span><span>−</span><span>38.4</span></span></span></span> A），积分项只是再往上加，先冲到最深，再随着整个系统找到另一个平衡点而回来一点。</p><p><strong>我原以为只有车体系控制才能把零空间电流压成 0。</strong> 实际上逐轮等比缩放也是 0。决定零空间电流是不是 0 的，不是控制器在轮子层还是车体层，而是<strong>饱和有没有把信号推出 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 的值域</strong>。四个增益相同的逐轮 PI 吃进值域里的误差，就只吐出值域里的电流。只有逐轮截断（每个轮子看自己的限幅）会破坏这一点。</p><p><strong>车体系 PI 的好处在别处：它让我能分别选三个轴的带宽。</strong> 轮子层的 PI 会悄悄把三个轴整定成不同的带宽：同样增益的轮子 PI，从车体轴看，等效带宽是 <span><span>ωn=(30.1, 30.1, 64.9)\omega_n=(30.1,\,30.1,\,64.9)</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>30.1</span><span>,</span><span></span><span></span><span>30.1</span><span>,</span><span></span><span></span><span>64.9</span><span>)</span></span></span></span> rad/s（<code>report</code> 算的，<span><span>HTH=diag⁡(4,4,4L2)H^TH=\operatorname{diag}(4,4,4L^2)</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>4</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>4</span><span><span>L</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 把转动轴的 <span><span>ωn2\omega_n^2</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 放大了 <span><span>L2Meff,x/Meff,z=4.66L^2M_{eff,x}/M_{eff,z}=4.66</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>e</span><span>f</span><span>f</span><span>,</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>M</span><span><span><span><span><span><span></span><span><span><span>e</span><span>f</span><span>f</span><span>,</span><span>z</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.66</span></span></span></span> 倍）；车体系 PI 我把三个轴都放在 30 rad/s。从图上读，<code>mec_wheel_scale</code> 的 <span><span>ωz\omega_z</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 0.3 s 之前就到位，<code>mec_body</code> 晚约 0.1 s，2 s 末偏离路径也是前者小（0.0947 对 0.2129 m）。车体系 PI 是我有意把转得慢当作选择，不是它更好。</p><p>还有一个差点让我得出错结论的数。前面说过，我先把对象写成车体系下三个独立的一阶系统（没有向心项）跑了一遍：逐轮截断的零空间电流 RMS 是 37.59 A，<span><span>(vx,vy)(v_x,v_y)</span><span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 的方向误差是 0°（指令是 <span><span>vx=vyv_x=v_y</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，只有轮 2 超限，它对 <span><span>vxv_x</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>vyv_y</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的影响恰好对称），2 s 末偏离 0.6071 m。补上向心项之后是 28.54 A，11.976°，2.1821 m。那个“方向误差 0°”如果不细看，会让人以为截断保持方向；它只是这个指令和这个对象的巧合。</p></section><section><h3>这些结果的边界<a href="#这些结果的边界"><span>#</span></a></h3><ul>
<li><strong>逐轮缩放不能在饱和时让积分器自己保持。</strong> 缩放是在 <code>Pid</code> 外面做的，<code>Pid</code> 看不到，所以我用上一拍的缩放标志去冻结积分，这是个粗的、晚一拍的策略，我没有和别的抗饱和策略比较。</li>
<li><strong>解耦的实验只测了“同样的 PI”。</strong> 没有重新整定，也没有做动态解耦，所以“静态解耦让 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 变差”是这个具体实验的结论，不是解耦这件事的结论。</li>
<li><strong>Wood–Berry 的数是转引的。</strong> 没有对照原文，也没有加执行器限幅、噪声和对象失配，四个回路的比较是在理想条件下的。</li>
<li><strong>麦轮的参数和滚子朝向是假设。</strong> <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>、<span><span>IzI_z</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>JwJ_w</span><span><span><span></span><span><span>J</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>cvc_v</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 都是我取的量级，不是实测；滚子的 <span><span>sis_i</span><span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 组合我只做了代数推导，没有对照真车。没有轮子打滑，没有电流环动态（电流指令即电流），没有摩擦的静摩擦部分。</li>
<li><strong>只有一个指令方向。</strong> 所有麦轮的数都是 <span><span>(2,2,3)(2,2,3)</span><span><span><span></span><span>(</span><span>2</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>)</span></span></span></span> 这一个指令，轮 2 是唯一超限的轮子。换一个指令，截断造成的偏差会不同，我没有扫。</li>
</ul></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 pid-lab 根目录（<code>nix develop</code> 里）运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch08</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出就是上面“结果”里贴的那一段，应当以 <code>ch08 OK</code> 结束：21 行 <code>ok</code> 都低于 1e-9，RGA 是 <span><span>[[2.0094,−1.0094],[−1.0094,2.0094]][[2.0094, -1.0094], [-1.0094, 2.0094]]</span><span><span><span></span><span>[[</span><span>2.0094</span><span>,</span><span></span><span>−</span><span>1.0094</span><span>]</span><span>,</span><span></span><span>[</span><span>−</span><span>1.0094</span><span>,</span><span></span><span>2.0094</span><span>]]</span></span></span></span>，两个对角 SIMC PI 的增益是 0.6523 和 −0.1237，<code>wb_wrongpair</code> 每 13.9 分钟翻一倍，<code>mec_wheel_clip</code> 的零空间电流 RMS 是 28.54 A，另外两种在 <span><span>10−1310^{-13}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> A 以下。运行后 <code>out/ch08/</code> 下有 28 个 CSV，<code>figures/ch08/</code> 下有 4 张 SVG，两个目录都不入库。四种语言的 <code>Pid</code> 是第 5 篇的那一份，这一篇没有改它。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/mimo.py" target="_blank"><code>python/pidlab/mimo.py</code></a></td><td><code>make -s ch08</code></td><td>2×2 的求逆、RGA、静态解耦；麦轮的 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span>、最小范数电流、正运动学、<code>null_current</code>；<span><span>λ11=2.0094\lambda_{11}=2.0094</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.0094</span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a></td><td><code>make -s ch08</code></td><td><code>WoodBerry</code>（四个一阶通道）和 <code>Mecanum</code>（含向心项和世界系位姿）</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/tuning.py" target="_blank"><code>python/pidlab/tuning.py</code></a></td><td><code>make -s ch08</code></td><td><code>simc_pi</code> 给出 0.6523 / 8.0 和 −0.1237 / 14.4</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid.py" target="_blank"><code>python/pidlab/pid.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>python/pidlab/sim.py</code></a></td><td><code>make -s ch08</code></td><td>第 5 篇的 <code>Pid</code>（<code>hold_integral</code>）和 <code>DelayLine</code>、<code>advance</code>，原样使用</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch08_mimo.py" target="_blank"><code>python/ch08_mimo.py</code></a></td><td><code>python3 python/ch08_mimo.py ref</code> 或 <code>report</code></td><td><code>ref</code> 写数据和 7 个 <code>_py.csv</code>；<code>report</code> 比 21 个文件、打印上面的指标表、画 4 张图</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/mimo.h" target="_blank"><code>c/include/pl/mimo.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/mimo.c" target="_blank"><code>c/src/mimo.c</code></a></td><td><code>make -s ch08</code></td><td>C 版 <code>pl_inv2</code>、<code>pl_rga2</code>、<code>pl_static_decoupler</code>、<code>pl_mecanum_*</code>；失败返回 <code>-EINVAL</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/plants.h" target="_blank"><code>c/include/pl/plants.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/plants.c" target="_blank"><code>c/src/plants.c</code></a></td><td><code>make -s ch08</code></td><td>C 版 <code>pl_wood_berry_*</code>、<code>pl_mecanum_*</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/mimo.h" target="_blank"><code>cpp/include/pidlab/mimo.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/plants.h" target="_blank"><code>cpp/include/pidlab/plants.h</code></a></td><td><code>make -s ch08</code></td><td>C++ 版，<code>std::optional</code> 和 <code>std::array</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/mimo.rs" target="_blank"><code>rust/src/mimo.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/plants.rs" target="_blank"><code>rust/src/plants.rs</code></a></td><td><code>make -s ch08</code></td><td>Rust 版，<code>Option</code> 和 <code>[Real; N]</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch08.c" target="_blank"><code>c/apps/ch08.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch08.cc" target="_blank"><code>cpp/apps/ch08.cc</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch08.rs" target="_blank"><code>rust/src/bin/ch08.rs</code></a></td><td><code>make -s ch08</code></td><td>各写 7 个 CSV 到 <code>out/ch08/</code>；对比 21 个文件，18 个差异为 0，三个 Rust 麦轮文件在 2.2e-16 以内</td></tr></tbody></table></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把多变量拆成了三块：Wood–Berry 的 RGA（<span><span>λ11=2.0094\lambda_{11}=2.0094</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.0094</span></span></span></span>）告诉我对角配对可以用、非对角配对（<span><span>λ12=−1.0094\lambda_{12}=-1.0094</span><span><span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1.0094</span></span></span></span>）会发散，每 13.9 分钟翻一倍；静态解耦把 <span><span>y2y_2</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 IAE 从 11.87 降到 7.29，但 <span><span>y1y_1</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的超调从 17.2% 涨到 65.6%；麦轮底盘上，轮速超限时逐轮截断让零空间电流的 RMS 达到 28.54 A，<span><span>(vx,vy)(v_x,v_y)</span><span><span><span></span><span>(</span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 方向偏 11.98°，等比缩放和车体系 PI 的零空间电流在 <span><span>10−1310^{-13}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> A 以下。四种语言 21 个对比文件最大差异不超过 2.2e-16。</p><p><a href="/posts/pid-control-09-lqr/">第 9 篇：状态反馈：LQR、LQI 与平衡步兵</a>把“多维”推到底：平衡步兵有位置、速度、车身倾角、倾角速度四个状态，一个力矩输入。我会把常见的“位置环套速度环套直立环”这一串 PID 写成一个结构受限的状态反馈 <span><span>u=−Kxu=-Kx</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>K</span><span>x</span></span></span></span>，再用 LQR 直接算出完整的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>，看这一串 PID 少了什么、多了什么。矩阵增益 PI 在那里变成 LQI。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-07-cascade/</id>
      <title type="text">串级 PID：云台的角度环和速度环</title>
      <published>2026-10-16T06:00:00.000Z</published>
      <updated>2026-10-16T06:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-07-cascade/"/>
      <summary type="text">PID 控制系列第 7 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95，在 GM6020 云台的偏航轴上把单环 PID 和串级 PID（内环速度 PI、外环角度 P）各写了四遍，不新增库模块，只用第 5 篇的 Pid。底盘在 0.5 s 内加速到 6 rad/s 的小陀螺里，单环的云台角度误差峰值是 7.16 mrad，串级是 0.41 mrad（17.5 倍），再加底盘角速度前馈是 0.16 mrad（44.1 倍）；带宽比 1.5 的外环相角裕度 61.7°，比值到 3 以上超过 85°；1.5 rad 的大阶跃让内环饱和时，外环积分一直累加，超调 46.41%，用内环的饱和标志冻结外环积分后降到 1.59%。四种语言 30 个对比文件最大差异为 0。</summary>
      <content type="html"><![CDATA[<p>RoboMaster 步兵开小陀螺的时候，底盘以每秒几圈的速度原地转，云台要在这个转动的底座上把枪口对准一个不动的装甲板。我的云台代码一直是“角度环套速度环”，两层 PID，我见过的开源工程大多这么写，我也一直照着写，但从来没问过：<strong>为什么不用一个 PID 直接管角度？</strong> 这一篇我把同一根偏航轴分别接成单环和串级，放进同一份底盘转动数据里，量出来：底盘在 0.5 s 内加速到 6 rad/s，单环的云台角度误差峰值是 7.16 mrad，串级是 0.41 mrad，差 17.5 倍。然后我拆开这个 17.5 是从哪来的，它比我预想的更“无聊”，也更有用。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch07</code> 通过，<code>out/ch07/</code> 下 40 个 CSV（10 个算例，每个 Python、C、C++、Rust 各一份），<code>figures/ch07/</code> 下 4 张图；以及单环三阶极点配置、串级内外环整定、带宽比分析和内环饱和下外环积分冻结的推导与实验。
新增模块：没有。控制器全部是第 5 篇的 <code>Pid</code>，串级的接线（外环输出变成内环设定值）直接写在每种语言的 ch07 驱动和应用里。对象还是第 1 篇的 <code>GimbalAxis</code>（<code>u = [τ, τ_d, ω_c]</code>，第 1 篇里第三个输入恒为 0，这一篇第一次让它非零），相角裕度用第 3 篇的 <code>freq</code>。
前置：<a href="/posts/pid-control-05-practical/">第 5 篇</a>和<a href="/posts/pid-control-06-tuning/">第 6 篇</a>。需要的数学：第 2 篇的二阶系统和极点配置，第 3 篇的相角裕度，第 5 篇的 <code>hold_integral</code> 参数。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>对象：云台和转动的底盘<a href="#对象云台和转动的底盘"><span>#</span></a></h3><p>云台偏航轴还是第 1 篇的 <code>GimbalAxis</code>，扭矩指令的 GM6020：</p><span><span><span>Jψ¨=τ−τd−bψ˙−bf (ψ˙−ωc)J\ddot\psi = \tau - \tau_d - b\dot\psi - b_f\,(\dot\psi - \omega_c)</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>(</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p><span><span>ψ\psi</span><span><span><span></span><span>ψ</span></span></span></span> 是云台相对世界的角度，<span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 是电机力矩（限幅 <span><span>±1\pm1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> N·m），<span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是底盘的角速度。最后一项是轴承的粘性耦合：底盘转起来，轴承会把云台往同一个方向拖。参数 <span><span>J=0.02J = 0.02</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span> kg·m²、<span><span>b=0.002b = 0.002</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.002</span></span></span></span>、<span><span>bf=0.02b_f = 0.02</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span> 全是我取的量级，不是实测。令 <span><span>ω=ψ˙\omega = \dot\psi</span><span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，<span><span>bt=b+bf=0.022b_t = b + b_f = 0.022</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.022</span></span></span></span>，速度子系统是</p><span><span><span>Jω˙=τ−bt ω+bf ωcJ\dot\omega = \tau - b_t\,\omega + b_f\,\omega_c</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>ω</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>对云台来说，<span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就是一个力矩扰动 <span><span>d=bfωcd = b_f\omega_c</span><span><span><span></span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：底盘到 6 rad/s 时是 <span><span>0.02×6=0.120.02\times6 = 0.12</span><span><span><span></span><span>0.02</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span><span></span><span>=</span><span></span></span><span><span></span><span>0.12</span></span></span></span> N·m，占力矩上限的 12%。这个扰动是一段斜坡：我让 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 1.0 s 到 1.5 s 线性升到 6 rad/s，之后保持。</p><p>测量有两路：角度 <span><span>ψ+na\psi + n_a</span><span><span><span></span><span>ψ</span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>σa=10−4\sigma_a = 10^{-4}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rad；陀螺 <span><span>ψ˙+ng\dot\psi + n_g</span><span><span><span></span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>σg=0.01\sigma_g = 0.01</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span> rad/s。噪声是 Python 用 numpy 种子 7 生成、写进数据 CSV 的，三种原生语言读同一份。这两个数字的出处要交代清楚：<a href="/posts/kalman-filter-04-attitude-imu/">卡尔曼系列第 4 篇</a>的陀螺仿真用的是 0.005 rad/s，那是“典型消费级 MEMS 的量级”；我这里取了它的 2 倍，是为了让噪声看得见，不是那一篇的结论。角度的 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rad（0.0057°）是我假设的姿态估计精度，也不是那一篇测出来的。</p></section><section><h3>单环：三个增益摆三个极点<a href="#单环三个增益摆三个极点"><span>#</span></a></h3><p>先写单环，它是对照组。用第 5 篇的 <code>Pid</code>，只开 P 和 I，D 用陀螺：陀螺读数本身就是角度的导数，不需要对带噪声的角度再差分一次。控制律是</p><span><span><span>τ=kp(r−ψ)+ki ⁣∫(r−ψ) dt−kdg ψ˙gyro\tau = k_p(r-\psi) + k_i\!\int(r-\psi)\,dt - k_{dg}\,\dot\psi_{gyro}</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ψ</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span></span><span>∫</span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ψ</span><span>)</span><span></span><span>d</span><span>t</span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>g</span><span>y</span><span>r</span><span>o</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>陀螺项通过 <code>Pid.update</code> 的 <code>ff</code> 参数送进去（<span><span>ff=−kdgψ˙gyro\mathrm{ff} = -k_{dg}\dot\psi_{gyro}</span><span><span><span></span><span><span>ff</span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>g</span><span>y</span><span>r</span><span>o</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>），它进入限幅前的 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span>，所以抗积分饱和照样看得见它。代入对象，特征多项式是</p><span><span><span>Js3+(bt+kdg) s2+kp s+ki=0J s^3 + (b_t + k_{dg})\,s^2 + k_p\,s + k_i = 0</span><span><span><span></span><span>J</span><span><span>s</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>要的极点是一个实极点 <span><span>−p-p</span><span><span><span></span><span>−</span><span>p</span></span></span></span> 加一个 <span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span> 的共轭对，即 <span><span>J(s+p)(s2+2ζωs+ω2)J(s+p)(s^2 + 2\zeta\omega s + \omega^2)</span><span><span><span></span><span>J</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>p</span><span>)</span><span>(</span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>ζ</span><span>ω</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。对系数得到</p><span><span><span>kdg=J(p+2ζω)−bt,kp=J(ω2+2ζωp),ki=J p ω2k_{dg} = J(p + 2\zeta\omega) - b_t,\qquad
k_p = J(\omega^2 + 2\zeta\omega p),\qquad
k_i = J\,p\,\omega^2</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>J</span><span>(</span><span>p</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>ζ</span><span>ω</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>J</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>ζ</span><span>ω</span><span>p</span><span>)</span><span>,</span><span></span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>J</span><span></span><span>p</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span></span><p>取 <span><span>p=ω=ωo=ωi/5=12p = \omega = \omega_o = \omega_i/5 = 12</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/5</span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span> rad/s（<span><span>ωi=60\omega_i = 60</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>60</span></span></span></span> rad/s 是后面串级内环的带宽，这样两边“外环带宽”相同，对比才公平）：<span><span>kdg=0.554k_{dg} = 0.554</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.554</span></span></span></span>，<span><span>kp=6.912k_p = 6.912</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>6.912</span></span></span></span>，<span><span>ki=34.56k_i = 34.56</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>34.56</span></span></span></span>。极点是 <span><span>−12-12</span><span><span><span></span><span>−</span><span>12</span></span></span></span> 和 <span><span>−8.40±8.57j-8.40\pm8.57\mathrm{j}</span><span><span><span></span><span>−</span><span>8.40</span><span></span><span>±</span><span></span></span><span><span></span><span>8.57</span><span>j</span></span></span></span>（<code>report</code> 打印的）。</p><p>这个单环有一个第 2 篇已经讲过的毛病：闭环传递函数 <span><span>T(s)=(kps+ki)/(…)T(s) = (k_ps + k_i)/(\ldots)</span><span><span><span></span><span>T</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span>(</span><span>…</span><span>)</span></span></span></span> 有一个零点在 <span><span>−ki/kp=−5-k_i/k_p = -5</span><span><span><span></span><span>−</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>5</span></span></span></span> rad/s，比极点慢，所以阶跃会超调，不管阻尼多好。我会在结果里看到这一点。</p></section><section><h3>串级：内环管速度，外环管角度<a href="#串级内环管速度外环管角度"><span>#</span></a></h3><p>串级把这三个增益拆成两层。<strong>内环</strong>是速度 PI，设定值是 <span><span>ωref\omega_{ref}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，测量是陀螺：</p><span><span><span>τ=kp,i(ωref−ωgyro)+ki,i ⁣∫(ωref−ωgyro) dt\tau = k_{p,i}(\omega_{ref} - \omega_{gyro}) + k_{i,i}\!\int(\omega_{ref} - \omega_{gyro})\,dt</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>g</span><span>y</span><span>r</span><span>o</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span></span><span>∫</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>g</span><span>y</span><span>r</span><span>o</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>d</span><span>t</span></span></span></span></span><p>被控对象 <span><span>ω/τ=1/(Js+bt)\omega/\tau = 1/(Js + b_t)</span><span><span><span></span><span>ω</span><span>/</span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 是一阶的，和第 2 篇的电机速度环同一个形状，所以直接套那一篇的极点配置，闭环传递函数是</p><span><span><span>Ti(s)=ωωref=kp,is+ki,iJs2+(bt+kp,i) s+ki,iT_i(s) = \frac{\omega}{\omega_{ref}} = \frac{k_{p,i}s + k_{i,i}}{Js^2 + (b_t + k_{p,i})\,s + k_{i,i}}</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>ω</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>s</span><span></span><span>+</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>令 <span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>、<span><span>ωi=60\omega_i = 60</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>60</span></span></span></span> rad/s：<span><span>ki,i=Jωi2=72k_{i,i} = J\omega_i^2 = 72</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>J</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>72</span></span></span></span>，<span><span>kp,i=2ζωiJ−bt=1.658k_{p,i} = 2\zeta\omega_iJ - b_t = 1.658</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.658</span></span></span></span>。这个 PI 有一个零点在 <span><span>−ki,i/kp,i=−43.4-k_{i,i}/k_{p,i} = -43.4</span><span><span><span></span><span>−</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>43.4</span></span></span></span> rad/s，比 <span><span>ωi\omega_i</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 还低一点，稍后会看到它对外环有用。</p><p><strong>外环</strong>是一个纯 P，输入角度误差，输出内环的速度设定值：<span><span>ωref=ko(r−ψ)\omega_{ref} = k_o(r - \psi)</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ψ</span><span>)</span></span></span></span>，<span><span>ko=ωi/比值k_o = \omega_i/\text{比值}</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>比值</span></span></span></span></span>。比值就是“内环比外环快多少倍”，是这一篇的主要旋钮。整条链路是：</p><div><div><div><div><span></span></div><div><span><p>角度误差</p></span></div><div><span><p>ω_ref</p></span></div><div><span><p>速度误差</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>ψ'</p></span></div><div><span></span></div><div><span><p>ψ</p></span></div><div><span></span></div><div><span><p>inner.saturated</p></span></div><div><span><p>目标角 r</p></span></div><div><span>+ −</span></div><div><span><p>外环 P（或 PI）：k_o</p></span></div><div><span>+ −</span></div><div><span><p>内环 PI：k_p,i 和 k_i,i</p></span></div><div><span><p>前馈 −b_f ω_c</p></span></div><div><span><p>+</p></span></div><div><span><p>限幅 ±1 N·m</p></span></div><div><span><p>云台：J ψ'' = τ − b_t ψ' + b_f ω_c</p></span></div><div><span><p>陀螺（噪声 0.01 rad/s）</p></span></div><div><span><p>角度（噪声 1e-4 rad）</p></span></div>
</div><div><div><span></span></div><div><span><p>角度误差</p></span></div><div><span><p>ω_ref</p></span></div><div><span><p>速度误差</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>ψ'</p></span></div><div><span></span></div><div><span><p>ψ</p></span></div><div><span></span></div><div><span><p>inner.saturated</p></span></div><div><span><p>目标角 r</p></span></div><div><span>+ −</span></div><div><span><p>外环 P（或 PI）：k_o</p></span></div><div><span>+ −</span></div><div><span><p>内环 PI：k_p,i 和 k_i,i</p></span></div><div><span><p>前馈 −b_f ω_c</p></span></div><div><span><p>+</p></span></div><div><span><p>限幅 ±1 N·m</p></span></div><div><span><p>云台：J ψ'' = τ − b_t ψ' + b_f ω_c</p></span></div><div><span><p>陀螺（噪声 0.01 rad/s）</p></span></div><div><span><p>角度（噪声 1e-4 rad）</p></span></div>
</div></div></div><p>虚线是这一篇最后一节才用到的：内环把它有没有饱和告诉外环。</p><p>外环看到的对象不是云台，而是“内环加一个积分器”：<span><span>Lo(s)=ko Ti(s)/sL_o(s) = k_o\,T_i(s)/s</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span><span>/</span><span>s</span></span></span></span>。如果把 <span><span>TiT_i</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 粗略当成一阶的 <span><span>1/(s/ωi+1)1/(s/\omega_i + 1)</span><span><span><span></span><span>1/</span><span>(</span><span>s</span><span>/</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>，外环的特征方程是 <span><span>s2+ωis+ωiko=0s^2 + \omega_i s + \omega_i k_o = 0</span><span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，阻尼比 <span><span>ζo=12ωi/ko=12比值\zeta_o = \tfrac12\sqrt{\omega_i/k_o} = \tfrac12\sqrt{\text{比值}}</span><span><span><span></span><span><span>ζ</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span><span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span><span><span><span></span><span><span><span>比值</span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，比值到 4 就是临界阻尼；相角裕度是 <span><span>90∘−arctan⁡(ωc/ωi)90^\circ - \arctan(\omega_c/\omega_i)</span><span><span><span></span><span>9</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>arctan</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>：内环对外环来说就是一个滞后，比值越大，这个滞后在外环穿越频率处吃掉的相位越少。我不信一个近似，所以 <code>report</code> 用第 3 篇的 <code>freq</code> 在精确的 <span><span>TiT_i</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 上也算一遍（延迟取 ZOH 的半个采样周期）。</p></section><section><h3>串级为什么抗扰：等效 PID<a href="#串级为什么抗扰等效-pid"><span>#</span></a></h3><p>回到 17.5 倍。扰动力矩 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 到角度 <span><span>ψ\psi</span><span><span><span></span><span>ψ</span></span></span></span> 的传递函数，单环和串级是同一个形状（令 <span><span>r=0r = 0</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，把控制律代进去）：</p><span><span><span>ψd=sJs3+(bt+kd) s2+kp s+ki\frac{\psi}{d} = \frac{s}{Js^3 + (b_t + k_d)\,s^2 + k_p\,s + k_i}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span></span></span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span><span><span>s</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span><span>b</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>s</span><span></span><span>+</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>单环的 <span><span>(kd,kp,ki)(k_d, k_p, k_i)</span><span><span><span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 就是 <span><span>(kdg,kp,ki)(k_{dg}, k_p, k_i)</span><span><span><span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>；串级的是<strong>等效增益</strong></p><span><span><span>kd=kp,i,kp=kokp,i+ki,i,ki=ko ki,ik_d = k_{p,i},\qquad k_p = k_ok_{p,i} + k_{i,i},\qquad k_i = k_o\,k_{i,i}</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>代入数字，比值 5（<span><span>ko=12k_o = 12</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>）：</p>

<table><thead><tr><th>回路</th><th><span><span>kdk_d</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th><span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th>极点</th></tr></thead><tbody><tr><td>单环</td><td>0.554</td><td>6.912</td><td>34.56</td><td><span><span>−12.00-12.00</span><span><span><span></span><span>−</span><span>12.00</span></span></span></span>，<span><span>−8.40±8.57j-8.40\pm8.57\mathrm{j}</span><span><span><span></span><span>−</span><span>8.40</span><span></span><span>±</span><span></span></span><span><span></span><span>8.57</span><span>j</span></span></span></span></td></tr><tr><td>串级</td><td>1.658</td><td>91.896</td><td>864.0</td><td><span><span>−11.48-11.48</span><span><span><span></span><span>−</span><span>11.48</span></span></span></span>，<span><span>−36.26±49.47j-36.26\pm49.47\mathrm{j}</span><span><span><span></span><span>−</span><span>36.26</span><span></span><span>±</span><span></span></span><span><span></span><span>49.47</span><span>j</span></span></span></span></td></tr></tbody></table><p>两者最慢的极点几乎在同一个位置（<span><span>−12.00-12.00</span><span><span><span></span><span>−</span><span>12.00</span></span></span></span> 和 <span><span>−11.48-11.48</span><span><span><span></span><span>−</span><span>11.48</span></span></span></span>），所以“外环带宽”确实是一样的；串级多出来的是一对 60 rad/s 量级的快极点，换来了 13.3 倍的 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 25 倍的 <span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。对一段斜率为 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 的力矩斜坡，这个 <span><span>s/(…)s/(\ldots)</span><span><span><span></span><span>s</span><span>/</span><span>(</span><span>…</span><span>)</span></span></span></span> 形式的闭环由终值定理给出稳态角度误差 <span><span>m/kim/k_i</span><span><span><span></span><span>m</span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>ψss=mki,m=bf⋅60.5 s=0.24 N⋅m/s\psi_{ss} = \frac{m}{k_i},\qquad m = \frac{b_f\cdot6}{0.5\ \text{s}} = 0.24\ \text{N·m/s}</span><span><span><span></span><span><span>ψ</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>0.5</span><span> </span><span><span>s</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span><span>6</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.24</span><span> </span><span><span>N⋅m/s</span></span></span></span></span></span><p>单环是 <span><span>0.24/34.56=6.9440.24/34.56 = 6.944</span><span><span><span></span><span>0.24/34.56</span><span></span><span>=</span><span></span></span><span><span></span><span>6.944</span></span></span></span> mrad，串级是 <span><span>0.24/864=0.2780.24/864 = 0.278</span><span><span><span></span><span>0.24/864</span><span></span><span>=</span><span></span></span><span><span></span><span>0.278</span></span></span></span> mrad。换个说法更直观：内环的 PI 先把速度误差压到 <span><span>m/ki,i=0.24/72=3.33m/k_{i,i} = 0.24/72 = 3.33</span><span><span><span></span><span>m</span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.24/72</span><span></span><span>=</span><span></span></span><span><span></span><span>3.33</span></span></span></span> mrad/s，外环再把它除以 <span><span>ko=12k_o = 12</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>，就是 0.278 mrad。<strong>扰动在到达角度之前，已经在速度这一层被消掉了大部分。</strong> 类比的话，这像工厂里班组先把小故障在车间内消化，不往上报给调度：车间的响应快，所以故障还没传远就被吃掉。</p><p>要把话说准：这不是说单环做不到。串级的 <span><span>(kd,kp,ki)=(1.658,91.9,864)(k_d, k_p, k_i) = (1.658, 91.9, 864)</span><span><span><span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1.658</span><span>,</span><span></span><span>91.9</span><span>,</span><span></span><span>864</span><span>)</span></span></span></span> 直接塞进一个单环 PID，特征多项式和扰动响应完全一样。差别是单环用的是我选的“三个极点都放在 12 rad/s”的配方，三个增益被这个配方锁死了；串级的配方是“先把内环做到 60 rad/s，外环按比值再降”，它用同样的三个增益自由度，摆出了更刚的一组极点，并且让整定变成两个有物理含义的旋钮：内环带宽和比值。设定值通道也不同（外环 P 没有积分，没有那个 <span><span>−5-5</span><span><span><span></span><span>−</span><span>5</span></span></span></span> rad/s 的零点）。</p><p><strong>底盘角速度前馈。</strong> 底盘陀螺（底盘上本来就有）给出 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以扰动 <span><span>bfωcb_f\omega_c</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是已知的，可以直接在内环的输出上抵消。符号要小心：云台静止（<span><span>ψ˙=0\dot\psi = 0</span><span><span><span></span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>）时，耦合项对云台的力矩是 <span><span>+bfωc+b_f\omega_c</span><span><span><span></span><span>+</span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以前馈是 <span><span>ff=−bfωc\mathrm{ff} = -b_f\omega_c</span><span><span><span></span><span><span>ff</span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p></section><section><h3>带宽比：多快才算“快得够”<a href="#带宽比多快才算快得够"><span>#</span></a></h3><p>比值太小，内环在外环穿越频率处的滞后就大，相角裕度掉；比值太大，外环慢得没必要，而且你把整条链路的响应速度扔掉了。我在 <span><span>{1.5,3,5,10}\{1.5, 3, 5, 10\}</span><span><span><span></span><span>{</span><span>1.5</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>10</span><span>}</span></span></span></span> 四个比值上扫：<span><span>ko=40,20,12,6k_o = 40, 20, 12, 6</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>40</span><span>,</span><span></span><span>20</span><span>,</span><span></span><span>12</span><span>,</span><span></span><span>6</span></span></span></span> rad/s，内环都不动。</p></section><section><h3>内环饱和：外环怎么知道<a href="#内环饱和外环怎么知道"><span>#</span></a></h3><p>云台要转一个大角度，比如 1.5 rad，外环给出 <span><span>ko×1.5=18k_o\times1.5 = 18</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>1.5</span><span></span><span>=</span><span></span></span><span><span></span><span>18</span></span></span></span> rad/s 的速度设定值。力矩上限 1 N·m 对应的最大角加速度是 <span><span>1/0.02=501/0.02 = 50</span><span><span><span></span><span>1/0.02</span><span></span><span>=</span><span></span></span><span><span></span><span>50</span></span></span></span> rad/s²，加速到 18 rad/s 至少要 0.36 s。这段时间里内环的力矩一直贴在上限，<strong>内环饱和了，而外环自己并没有饱和</strong>：外环输出的是速度设定值，我没有给它限幅。所以第 5 篇的条件积分（<code>CLAMP</code>）在外环看不出任何问题，如果外环带积分（<span><span>ki,o=0.2 ko2=28.8k_{i,o} = 0.2\,k_o^2 = 28.8</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>o</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>28.8</span></span></span></span>），它会一直在 1.5 rad 的误差上累加，最后速度设定值积出来一大块，角度冲过头。</p><p>解法在第 5 篇的 <code>hold_integral</code> 里：驱动把内环<strong>上一个采样</strong>的 <code>saturated</code> 标志传给外环，外环这一拍不更新积分：</p><span><span><span>holdk=inner.saturatedk−1\text{hold}_k = \text{inner.saturated}_{k-1}</span><span><span><span></span><span><span><span>hold</span></span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>inner.saturated</span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>延迟一拍是因为内环要在外环之后才更新；我猜 1 kHz 下一拍的差别可以忽略，但没有单独测。这个策略很粗：内环一饱和就整个冻结外环积分，不看误差方向；第 5 篇的条件积分只在“继续往饱和方向积”时才跳过。我这一篇没有比较两种策略。</p></section></section>
<section><h2>Python：增益、闭环和裕度分析<a href="#python增益闭环和裕度分析"><span>#</span></a></h2><p>不新增库模块，所以这一篇的 Python 全部在驱动 <code>python/ch07_cascade.py</code> 里：算例表、增益、一个闭环仿真、用 <code>freq</code> 的裕度分析和 <code>report</code>。先是两组增益，和上面的公式一一对应：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>inner_gains</span><span>() -&gt; tuple[</span><span>float</span><span>, </span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Rate PI on J s + (b + bf) placed at zeta, WN_I."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> </span><span>GimbalAxis</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>kp </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> </span><span>ZETA</span><span> </span><span>*</span><span> </span><span>WN_I</span><span> </span><span>*</span><span> g.j </span><span>-</span><span> (g.b </span><span>+</span><span> g.bf)</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ki </span><span>=</span><span> </span><span>WN_I</span><span> </span><span>*</span><span> </span><span>WN_I</span><span> </span><span>*</span><span> g.j</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> kp, ki</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>single_gains</span><span>() -&gt; tuple[</span><span>float</span><span>, </span><span>float</span><span>, </span><span>float</span><span>]:</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"""kp, ki and the gyro damping kdg for poles (s + wo)(s^2 + ... wo^2)."""</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> </span><span>GimbalAxis</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>wo </span><span>=</span><span> </span><span>WN_I</span><span> </span><span>/</span><span> </span><span>RATIO</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> wo</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>kdg </span><span>=</span><span> g.j </span><span>*</span><span> (p </span><span>+</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> </span><span>ZETA</span><span> </span><span>*</span><span> wo) </span><span>-</span><span> (g.b </span><span>+</span><span> g.bf)</span></span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>kp </span><span>=</span><span> g.j </span><span>*</span><span> (wo </span><span>*</span><span> wo </span><span>+</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> </span><span>ZETA</span><span> </span><span>*</span><span> wo </span><span>*</span><span> p)</span></span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>ki </span><span>=</span><span> g.j </span><span>*</span><span> p </span><span>*</span><span> wo </span><span>*</span><span> wo</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> kp, ki, kdg</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Case</code> 里的五个字段决定一个算例：<code>kind</code>（单环或串级）、<code>ratio</code>、<code>ff</code>（内环前馈）、<code>outer_i</code>（外环是 PI 还是 P）和 <code>hold</code>（冻结外环积分）。十个算例是：<code>single_step</code>，<code>cascade_r{1.5,3,5,10}_step</code>（0.2 rad 阶跃），<code>single_spin</code>、<code>cascade_spin</code>、<code>cascade_spin_ff</code>（底盘转动），<code>outer_aw_off</code>、<code>outer_aw_on</code>（1.5 rad 大阶跃，外环 PI）。</p><p>数据由 <code>write_data</code> 生成：<code>ch07_step.csv</code>（2000 行，0.05 s 时 <span><span>r=0.2r = 0.2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> rad）、<code>ch07_spin.csv</code>（3000 行，<span><span>r=0r = 0</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 1.0 s 到 1.5 s 线性升到 6 rad/s）、<code>ch07_aw.csv</code>（3000 行，0.05 s 时 <span><span>r=1.5r = 1.5</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1.5</span></span></span></span> rad），每个文件都带 <code>n_gyro</code> 和 <code>n_angle</code> 两列噪声，numpy 种子 7。</p><p>闭环仿真是这一篇的核心，串级的接线就是这几行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>run_case</span><span>(</span><span>case</span><span>:</span><span> Case</span><span>,</span><span><span> </span><span>data</span></span><span>=</span><span>None</span><span>) -&gt; dict[</span><span>str</span><span>, np.ndarray]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""One closed loop; `data` overrides the CSV (used by the linear check)."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>plant </span><span>=</span><span> </span><span>GimbalAxis</span><span>()</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> data </span><span>is</span><span> </span><span>None</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>data </span><span>=</span><span> </span><span>read_csv</span><span>(</span><span>DATA</span><span> </span><span>/</span><span> </span><span>f</span><span>"</span><span>{</span><span>case.data</span><span>}</span><span>.csv"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>u_lim </span><span>=</span><span> plant.u_max</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span><span> case.kind </span><span>==</span><span> </span></span><span>"single"</span><span>:</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>kp, ki, kdg </span><span>=</span><span> </span><span>single_gains</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>outer </span><span>=</span><span> </span><span>Pid</span><span>(</span><span>PidConfig</span><span>(kp, ki, </span><span>0.0</span><span><span>, </span><span>TS</span><span>, </span></span><span>u_min</span><span><span>=-</span><span>u_lim, </span></span><span>u_max</span><span><span>=</span><span>u_lim))</span></span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>inner </span><span>=</span><span> </span><span>None</span></div></div><div><div><div>11</div></div><div><span>    </span><span>else</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>kpi, kii </span><span>=</span><span> </span><span>inner_gains</span><span>()</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>ko </span><span>=</span><span> </span><span>WN_I</span><span> </span><span>/</span><span> case.ratio</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>kio </span><span>=</span><span> </span><span>OUTER_KI</span><span> </span><span>*</span><span> ko </span><span>*</span><span> ko </span><span>if</span><span> case.outer_i </span><span>else</span><span> </span><span>0.0</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>outer </span><span>=</span><span> </span><span>Pid</span><span>(</span><span>PidConfig</span><span>(ko, kio, </span><span>0.0</span><span><span>, </span><span>TS</span><span>))</span></span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>inner </span><span>=</span><span> </span><span>Pid</span><span>(</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>PidConfig</span><span>(</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>kpi,</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>kii,</span></div></div><div><div><div>20</div></div><div><span>                </span><span>0.0</span><span>,</span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>TS</span><span>,</span></div></div><div><div><div>22</div></div><div><span>                </span><span>u_min</span><span><span>=-</span><span>u_lim,</span></span></div></div><div><div><div>23</div></div><div><span>                </span><span>u_max</span><span><span>=</span><span>u_lim,</span></span></div></div><div><div><div>24</div></div><div><span>                </span><span>windup</span><span><span>=</span><span>Windup.</span><span>CLAMP</span><span>,</span></span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> [</span><span>0.0</span><span>, </span><span>0.0</span><span>]</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>rows </span><span>=</span><span> []</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(data[</span><span>"t"</span><span>])):</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>r, wc </span><span>=</span><span> data[</span><span>"r"</span><span>][k], data[</span><span>"wc"</span><span>][k]</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>angle </span><span>=</span><span> x[</span><span>0</span><span><span>] </span><span>+</span><span> data[</span></span><span>"n_angle"</span><span>][k]  </span><span># the angle estimate</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>gyro </span><span>=</span><span> x[</span><span>1</span><span><span>] </span><span>+</span><span> data[</span></span><span>"n_gyro"</span><span>][k]</span></div></div><div><div><div>33</div></div><div><span>        </span><span>if</span><span> inner </span><span>is</span><span> </span><span>None</span><span>:</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>wref </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>35</div></div><div><span><span>            </span></span><span>u </span><span>=</span><span> outer.</span><span>update</span><span>(r, angle, </span><span>-</span><span>kdg </span><span>*</span><span> gyro)</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>sat </span><span>=</span><span> outer.saturated</span></div></div><div><div><div>37</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>38</div></div><div><span><span>            </span></span><span>hold </span><span>=</span><span> case.hold </span><span>and</span><span> inner.saturated  </span><span># last sample's flag</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>wref </span><span>=</span><span> outer.</span><span>update</span><span>(r, angle, </span><span>0.0</span><span>, hold)</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>ff </span><span>=</span><span> </span><span>-</span><span>plant.bf </span><span>*</span><span> wc </span><span>if</span><span> case.ff </span><span>else</span><span> </span><span>0.0</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>u </span><span>=</span><span> inner.</span><span>update</span><span>(wref, gyro, ff)</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>sat </span><span>=</span><span> inner.saturated</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>(</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>[data[</span><span>"t"</span><span>][k], r, wc, x[</span><span>0</span><span>], x[</span><span>1</span><span>], wref, u, outer.integral, sat]</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>advance</span><span>(plant.deriv, x, [u, </span><span>0.0</span><span><span>, wc], </span><span>TS</span><span>, </span><span>SUBSTEPS</span><span>)</span></span></div></div><div><div><div>47</div></div><div><span>    </span><span>return</span><span> </span><span>dict</span><span>(</span><span>zip</span><span><span>(</span><span>COLUMNS</span><span>, np.</span><span>array</span><span>(rows, </span></span><span>dtype</span><span>=</span><span>float</span><span>).T))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三处值得看：</p><ul>
<li><strong>测量。</strong> <code>angle</code> 和 <code>gyro</code> 是真值加噪声列，控制器永远看不到 <code>x</code>。输出 CSV 里的 <code>psi</code>、<code>psid</code> 是真值，所以后面算超调和峰值不受噪声污染。</li>
<li><strong>接线。</strong> 串级先算外环：<code>wref = outer.update(r, angle, 0.0, hold)</code>，再把 <code>wref</code> 当设定值、陀螺当测量喂给内环：<code>inner.update(wref, gyro, ff)</code>。单环只有一次 <code>update</code>，陀螺项走 <code>ff</code>。</li>
<li><strong><code>hold</code>。</strong> 在内环更新之前读 <code>inner.saturated</code>，此时它还是上一拍的值，这就是上面公式里的 <span><span>k−1k-1</span><span><span><span></span><span>k</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>。</li>
</ul><p>裕度分析用第 3 篇的 <code>freq</code>。外环的开环是 <span><span>Lo=koTi/sL_o = k_oT_i/s</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>s</span></span></span></span>，写成多项式比：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>outer_margins</span><span>(</span><span>ratio</span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>first_order</span></span><span>:</span><span> </span><span>bool</span><span> </span><span>=</span><span><span> </span><span>False</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Margins of L_o = k_o T_i(s) / s, the loop opened at the rate setpoint.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>T_i is the exact inner closed loop (PI on J s + b + bf), or the</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>first-order stand-in 1 / (s / wn_i + 1). The delay is the ZOH's ts / 2.</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> </span><span>GimbalAxis</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>kpi, kii </span><span>=</span><span> </span><span>inner_gains</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>ko </span><span>=</span><span> </span><span>WN_I</span><span> </span><span>/</span><span> ratio</span></div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span> first_order:</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>num, den </span><span>=</span><span> [ko], </span><span>poly_mul</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>], [</span><span>1.0</span><span><span> </span><span>/</span><span> </span><span>WN_I</span><span>, </span></span><span>1.0</span><span>])</span></div></div><div><div><div>12</div></div><div><span>    </span><span>else</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>num </span><span>=</span><span> [ko </span><span>*</span><span> kpi, ko </span><span>*</span><span> kii]</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>den </span><span>=</span><span> </span><span>poly_mul</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span><span>], [g.j, g.b </span><span>+</span><span> g.bf </span><span>+</span><span> kpi, kii])</span></span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span><span> </span><span>margins</span><span>(num, den, </span></span><span>0.5</span><span><span> </span><span>*</span><span> </span><span>TS</span><span>, </span><span>log_grid</span><span>())</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>def</span><span> </span><span>disturbance_polys</span><span>() -&gt; dict[</span><span>str</span><span>, list[</span><span>float</span><span>]]:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>"""Denominator of psi / torque disturbance, highest power first.</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>Both loops reduce to J s^3 + (b + bf + kd) s^2 + kp s + ki, with the</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>cascade's equivalent gains kd = kpi, kp = ko kpi + kii, ki = ko kii.</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> </span><span>GimbalAxis</span><span>()</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>bt </span><span>=</span><span> g.b </span><span>+</span><span> g.bf</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>kp, ki, kdg </span><span>=</span><span> </span><span>single_gains</span><span>()</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>kpi, kii </span><span>=</span><span> </span><span>inner_gains</span><span>()</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>ko </span><span>=</span><span> </span><span>WN_I</span><span> </span><span>/</span><span> </span><span>RATIO</span></div></div><div><div><div>29</div></div><div><span>    </span><span>return</span><span> {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>"single"</span><span><span>: [g.j, bt </span><span>+</span><span> kdg, kp, ki],</span></span></div></div><div><div><div>31</div></div><div><span>        </span><span>"cascade"</span><span><span>: [g.j, bt </span><span>+</span><span> kpi, ko </span><span>*</span><span> kpi </span><span>+</span><span> kii, ko </span><span>*</span><span> kii],</span></span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>disturbance_polys</code> 就是上一节的等效增益表，<code>report</code> 用它求根、算斜坡误差公式。<code>outer_margins</code> 里 <code>first_order=True</code> 是“把内环当一阶滞后”的粗近似。单环的开环 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span>（断在对象输入处）也有一个 <code>single_margins</code>：开环有双积分器，<code>freq</code> 的主值相位在起点落在 <span><span>+180∘+180^\circ</span><span><span><span></span><span>+</span><span>18</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span> 的支路上，所以我把算出来的相角裕度折回 <span><span>(−180∘,180∘](-180^\circ, 180^\circ]</span><span><span><span></span><span>(</span><span>−</span><span>18</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>18</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>]</span></span></span></span>，在源码里留了注释。</p><p><code>report</code> 的断言是这一篇的“实验结论”：相角裕度随比值单调增；上升时间随比值单调增；小阶跃（5 mrad，不会碰到力矩限幅）的超调随比值单调降；单环的 <code>ψ</code> 峰值大于串级大于串级加前馈；<code>outer_aw_on</code> 的超调小于 <code>outer_aw_off</code>；斜坡公式的预测值和仿真峰值之比落在 0.9 到 2.0 之间。</p><p>完整驱动（含 <code>report</code> 和画图）：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch07_cascade.py" target="_blank"><code>python/ch07_cascade.py</code></a>。</p></section>
<section><h2>同一套接线，三种语言<a href="#同一套接线三种语言"><span>#</span></a></h2><p>没有新的库模块，三个版本的 ch07 应用都是 Python 驱动的逐行翻译：同样的算例表、同样的增益公式、同样的接线，读同一份数据。区别在这几处：</p><ul>
<li><strong>两个控制器的放法。</strong> C 用 <code>struct pl_pid pid[2]</code>，下标 0 是外环（或单环），下标 1 是内环，初始化放在 <code>make_loops</code> 里，用 <code>pl_pid_init</code> 返回的 <code>-EINVAL</code> 做错误传递；C++ 是两个 <code>std::optional&lt;Pid&lt;double&gt;&gt;</code>，<code>Create</code> 失败就返回 <code>false</code>；Rust 是两个 <code>Pid</code>，<code>Pid::new</code> 返回 <code>Result</code>，用 <code>?</code> 向上传。三种语言都没有堆分配（<code>csv.c</code> 除外）。</li>
<li><strong>“没有限幅”。</strong> Python 的 <code>±math.inf</code>（<code>PidConfig</code> 的默认值）、C 的 <code>INFINITY</code>（<code>math.h</code>）、C++ 的 <code>std::numeric_limits&lt;double&gt;::infinity()</code>、Rust 的 <code>PidConfig::new</code> 默认值，是同一个意思。外环不给限幅，所以外环自己的 <code>saturated</code> 永远是 false，这正是需要 <code>hold_integral</code> 的原因。</li>
<li><strong>读标志。</strong> C 是结构体字段 <code>pid[1].saturated</code>，C++ 是访问器 <code>inner-&gt;saturated()</code>，Rust 是 <code>inner.saturated()</code>，Python 是属性。四种写法读到的都是上一拍的值，因为内环在这一拍还没更新。</li>
<li><strong>标量类型。</strong> 这一篇仍是 <code>double</code>（<code>pl_real</code>、<code>T</code>、<code>Real</code>），不过没有写死，第 10 篇换 float32。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p>构造放在 <code>make_loops</code>，下标约定是 <code>pid[0]</code> 外环（或单环），<code>pid[1]</code> 内环。增益公式和 Python 一行对一行，这里只看内环那一段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>kpi </span><span>=</span><span> </span><span>2.0</span><span> </span><span>*</span><span> ZETA </span><span>*</span><span> WN_I </span><span>*</span><span> g</span><span>-&gt;</span><span>j </span><span>-</span><span> (g</span><span>-&gt;</span><span>b </span><span>+</span><span> g</span><span>-&gt;</span><span>bf);</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>kii </span><span>=</span><span> WN_I </span><span>*</span><span> WN_I </span><span>*</span><span> g</span><span>-&gt;</span><span>j;</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>ko </span><span>=</span><span> WN_I </span><span>/</span><span> c</span><span>-&gt;</span><span>ratio;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kio </span><span>=</span><span> c</span><span>-&gt;</span><span>outer_i </span><span>?</span><span> OUTER_KI </span><span>*</span><span> ko </span><span>*</span><span> ko </span><span>:</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>set_cfg</span><span>(</span><span>&amp;</span><span><span>cfg</span><span>,</span><span> ko</span><span>,</span><span> kio</span><span>);</span></span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>ret </span><span>=</span><span> </span><span>pl_pid_init</span><span>(</span><span>&amp;</span><span>pid</span><span>[</span><span>0</span><span><span>]</span><span>,</span><span> </span></span><span>&amp;</span><span><span>cfg</span><span>);</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>if</span><span> (ret)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> ret;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>set_cfg</span><span>(</span><span>&amp;</span><span><span>cfg</span><span>,</span><span> kpi</span><span>,</span><span> kii</span><span>);</span></span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>cfg.u_min </span><span>=</span><span> </span><span>-</span><span>g</span><span>-&gt;</span><span>u_max;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>cfg.u_max </span><span>=</span><span> g</span><span>-&gt;</span><span>u_max;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>return</span><span> </span><span>pl_pid_init</span><span>(</span><span>&amp;</span><span>pid</span><span>[</span><span>1</span><span><span>]</span><span>,</span><span> </span></span><span>&amp;</span><span><span>cfg</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>闭环里的接线，<code>hold</code> 在内环更新之前读：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>if</span><span> (c</span><span>-&gt;</span><span>kind </span><span>==</span><span> LOOP_SINGLE) {</span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>u </span><span>=</span><span> </span><span>pl_pid_update</span><span>(</span><span>&amp;</span><span>pid</span><span>[</span><span>0</span><span>], </span><span>row</span><span>[cr], angle, </span><span>-</span><span>kdg </span><span>*</span><span> gyro,</span></div></div><div><div><div>3</div></div><div><span>            </span><span>false</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>sat </span><span>=</span><span> </span><span>pid</span><span>[</span><span>0</span><span>].</span><span>saturated</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>/* the inner flag still holds the last sample's value */</span></div></div><div><div><div>7</div></div><div><span>      </span><span>bool</span><span> hold </span><span>=</span><span> </span><span>c</span><span>-&gt;</span><span>hold</span><span> </span><span>&amp;&amp;</span><span> </span><span>pid</span><span>[</span><span>1</span><span>].</span><span>saturated</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>pl_real ff </span><span>=</span><span> </span><span>c</span><span>-&gt;</span><span>ff</span><span> </span><span>?</span><span> </span><span>-</span><span>g</span><span>.</span><span>bf</span><span> </span><span>*</span><span> </span><span>row</span><span>[cw] </span><span>:</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>wref </span><span>=</span><span> </span><span>pl_pid_update</span><span>(</span><span>&amp;</span><span>pid</span><span>[</span><span>0</span><span>], </span><span>row</span><span>[cr], angle, </span><span>0.0</span><span>,</span></div></div><div><div><div>11</div></div><div><span><span>               </span></span><span>hold);</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>u </span><span>=</span><span> </span><span>pl_pid_update</span><span>(</span><span>&amp;</span><span>pid</span><span>[</span><span>1</span><span>], wref, gyro, ff, </span><span>false</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>sat </span><span>=</span><span> </span><span>pid</span><span>[</span><span>1</span><span>].</span><span>saturated</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>完整应用：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch07.c" target="_blank"><code>c/apps/ch07.c</code></a>。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p>外环配置先按单环填好（带 <span><span>±1\pm1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> N·m 限幅），串级时整个换成无限幅的新配置；<code>Create</code> 返回 <code>std::optional</code>，<code>inner-&gt;saturated()</code> 是访问器：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>if</span><span> (</span><span>c</span><span>.</span><span>cascade</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>const</span><span> </span><span>double</span><span> </span><span>ko</span><span> </span><span>=</span><span> </span><span>kWnI</span><span> </span><span>/</span><span> </span><span>c</span><span>.</span><span>ratio</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>outer_cfg</span><span> </span><span>=</span><span> {</span><span>ko</span><span>, </span><span>c</span><span>.</span><span>outer_i</span><span> </span><span>?</span><span> </span><span>kOuterKi</span><span> </span><span>*</span><span> </span><span>ko</span><span> </span><span>*</span><span> </span><span>ko</span><span> </span><span>:</span><span> </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>kTs</span><span>};</span></div></div><div><div><div>4</div></div><div><span>    </span><span>outer_cfg</span><span>.</span><span>u_min</span><span> </span><span>=</span><span> </span><span>-</span><span>inf</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>outer_cfg</span><span>.</span><span>u_max</span><span> </span><span>=</span><span> </span><span>inf</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>  </span><span>auto</span><span> </span><span>outer</span><span> </span><span>=</span><span><span> pidlab::</span><span>Pid</span><span>&lt;</span></span><span>double</span><span>&gt;::</span><span>Create</span><span>(</span><span>outer_cfg</span><span>);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>auto</span><span> </span><span>inner</span><span> </span><span>=</span><span><span> pidlab::</span><span>Pid</span><span>&lt;</span></span><span>double</span><span>&gt;::</span><span>Create</span><span>(</span><span>inner_cfg</span><span>);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>outer</span><span> </span><span>||</span><span> </span><span>!</span><span>inner</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>// ...</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>// the inner flag still holds the last sample's value</span></div></div><div><div><div>13</div></div><div><span>      </span><span>const</span><span> </span><span>bool</span><span> </span><span>hold</span><span> </span><span>=</span><span> </span><span>c</span><span>.</span><span>hold</span><span> </span><span>&amp;&amp;</span><span> </span><span>inner</span><span>-&gt;</span><span>saturated</span><span>();</span></div></div><div><div><div>14</div></div><div><span>      </span><span>const</span><span> </span><span>double</span><span> </span><span>ff</span><span> </span><span>=</span><span> </span><span>c</span><span>.</span><span>ff</span><span> </span><span>?</span><span> </span><span>-</span><span>g</span><span>.</span><span>bf</span><span> </span><span>*</span><span> (</span><span>*</span><span>wc</span><span>)[</span><span>k</span><span>] </span><span>:</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>15</div></div><div><span>      </span><span>wref</span><span> </span><span>=</span><span> </span><span>outer</span><span>-&gt;</span><span>Update</span><span>((</span><span>*</span><span>r</span><span>)[</span><span>k</span><span>], </span><span>angle</span><span>, </span><span>0.0</span><span>, </span><span>hold</span><span>);</span></div></div><div><div><div>16</div></div><div><span>      </span><span>u</span><span> </span><span>=</span><span> </span><span>inner</span><span>-&gt;</span><span>Update</span><span>(</span><span>wref</span><span>, </span><span>gyro</span><span>, </span><span>ff</span><span>);</span></div></div><div><div><div>17</div></div><div><span>      </span><span>sat</span><span> </span><span>=</span><span> </span><span>inner</span><span>-&gt;</span><span>saturated</span><span>();</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>完整应用：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch07.cc" target="_blank"><code>cpp/apps/ch07.cc</code></a>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><p><code>outer_cfg</code> 和 C++ 一样先按单环填限幅，串级时用一个默认无限幅的 <code>PidConfig::new</code> 整个替换：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>if</span><span> </span><span>c</span><span>.cascade {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>ko</span><span><span> </span><span>=</span><span> </span></span><span>WN_I</span><span> / </span><span>c</span><span>.ratio;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>ki</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>c</span><span>.outer_i { </span><span>OUTER_KI</span><span> * </span><span>ko</span><span> * </span><span>ko</span><span> } </span><span>else</span><span> { </span><span>0.0</span><span> };</span></div></div><div><div><div>4</div></div><div><span>        </span><span>outer_cfg</span><span><span> </span><span>=</span><span> </span></span><span>PidConfig</span><span>::</span><span>new</span><span>(</span><span>ko</span><span>, </span><span>ki</span><span>, </span><span>0.0</span><span>, </span><span>TS</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>outer</span><span><span> </span><span>=</span><span> </span></span><span>Pid</span><span>::</span><span>new</span><span>(</span><span>outer_cfg</span><span>).</span><span>map_err</span><span><span>(</span><span>|</span></span><span>e</span><span><span>|</span><span> </span></span><span>format!</span><span>(</span><span>"{e:?}"</span><span>))?;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>inner</span><span><span> </span><span>=</span><span> </span></span><span>Pid</span><span>::</span><span>new</span><span>(</span><span>inner_cfg</span><span>).</span><span>map_err</span><span><span>(</span><span>|</span></span><span>e</span><span><span>|</span><span> </span></span><span>format!</span><span>(</span><span>"{e:?}"</span><span>))?;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>// ...</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> </span><span>c</span><span>.cascade {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>// the inner flag still holds the last sample's value</span></div></div><div><div><div>11</div></div><div><span>            </span><span>let</span><span> </span><span>hold</span><span><span> </span><span>=</span><span> </span></span><span>c</span><span><span>.hold </span><span>&amp;&amp;</span><span> </span></span><span>inner</span><span>.</span><span>saturated</span><span>();</span></div></div><div><div><div>12</div></div><div><span>            </span><span>let</span><span> </span><span>ff</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>c</span><span>.ff { -</span><span>g</span><span>.bf * </span><span>wc</span><span>[</span><span>k</span><span>] } </span><span>else</span><span> { </span><span>0.0</span><span> };</span></div></div><div><div><div>13</div></div><div><span>            </span><span>wref</span><span><span> </span><span>=</span><span> </span></span><span>outer</span><span>.</span><span>update</span><span>(</span><span>r</span><span>[</span><span>k</span><span>], </span><span>angle</span><span>, </span><span>0.0</span><span>, </span><span>hold</span><span>);</span></div></div><div><div><div>14</div></div><div><span>            </span><span>u</span><span><span> </span><span>=</span><span> </span></span><span>inner</span><span>.</span><span>update</span><span>(</span><span>wref</span><span>, </span><span>gyro</span><span>, </span><span>ff</span><span>, </span><span>false</span><span>);</span></div></div><div><div><div>15</div></div><div><span>            </span><span>sat</span><span><span> </span><span>=</span><span> </span></span><span>inner</span><span>.</span><span>saturated</span><span>();</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>完整应用：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch07.rs" target="_blank"><code>rust/src/bin/ch07.rs</code></a>。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch07</code> 一次跑完。10 个算例，每种语言 10 个文件，30 个对比文件。下面的输出把中间 24 行 <code>ok</code> 裁掉了，其余的 <code>max diff</code> 也全是 0：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch07</span></div></div><div><div><div>2</div></div><div><span>ch07: C wrote 10 cases to out/ch07</span></div></div><div><div><div>3</div></div><div><span>ch07: C++ wrote 10 cases to out/ch07</span></div></div><div><div><div>4</div></div><div><span>ch07: Rust wrote 10 cases to out/ch07</span></div></div><div><div><div>5</div></div><div><span>ok   cascade_r1.5_step_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   cascade_r1.5_step_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   cascade_r1.5_step_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>... （中间 24 行 ok 省略）</span></div></div><div><div><div>9</div></div><div><span>ok   single_step_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   single_step_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   single_step_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>inner rate PI at zeta 0.7, wn 60.0: kp = 1.6580, ki = 72.000</span></div></div><div><div><div>13</div></div><div><span>single loop (poles at 12 rad/s): kp = 6.9120, ki = 34.5600, kd_gyro = 0.5540</span></div></div><div><div><div>14</div></div><div><span>outer k_o = 40 (ratio 1.5), 20 (ratio 3), 12 (ratio 5), 6 (ratio 10)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>              </span></span><span>case    rise  overshoot  settle  max|u|  sat ms      PM  PM 1st     wc</span></div></div><div><div><div>17</div></div><div><span><span>       </span></span><span>single_step   0.095     29.89%  0.513s   1.000      14   65.9d       -  28.29</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>cascade_r1.5_step   0.063     15.62%  0.181s   1.000     138   61.7d   59.0d  50.54</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>cascade_r3_step   0.123      0.07%  0.241s   1.000      48   85.4d   71.8d  22.34</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>cascade_r5_step   0.186      0.08%  0.354s   1.000      31   88.7d   78.6d  12.49</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>cascade_r10_step   0.363      0.08%  0.662s   1.000      12   89.6d   84.1d   6.06</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>loop      PM   unclipped overshoot (5 mrad)</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>single   65.9d                        33.946%</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>r = 1.5   61.7d                         4.608%</span></div></div><div><div><div>26</div></div><div><span><span>   </span></span><span>r = 3   85.4d                         0.000%</span></div></div><div><div><div>27</div></div><div><span><span>   </span></span><span>r = 5   88.7d                         0.000%</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>r = 10   89.6d                         0.000%</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>chassis ramp to 6.0 rad/s: disturbance torque bf wc = 0.120 N*m</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>single: J s^3 + 0.576 s^2 + 6.912 s + 34.560; poles -12.00+0.00j, -8.40-8.57j, -8.40+8.57j</span></div></div><div><div><div>32</div></div><div><span><span> </span></span><span>cascade: J s^3 + 1.680 s^2 + 91.896 s + 864.000; poles -36.26-49.47j, -36.26+49.47j, -11.48+0.00j</span></div></div><div><div><div>33</div></div><div><span>torque ramp slope 0.240 N*m/s over ki_eq: single 6.944 mrad, cascade 0.278 mrad</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>case  peak |psi|  rms |psi|      final  rms u (last 0.5 s)</span></div></div><div><div><div>35</div></div><div><span><span>     </span></span><span>single_spin     0.00716    0.00313  -0.000005             0.12008</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>cascade_spin     0.00041    0.00014   0.000029             0.12116</span></div></div><div><div><div>37</div></div><div><span><span> </span></span><span>cascade_spin_ff     0.00016    0.00006   0.000029             0.12116</span></div></div><div><div><div>38</div></div><div><span>cascade cuts the peak by 17.51x, with feedforward by 44.07x</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>single: simulated peak / ramp formula = 1.03</span></div></div><div><div><div>40</div></div><div><span><span> </span></span><span>cascade: simulated peak / ramp formula = 1.47</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span><span>         </span></span><span>case  peak psi  overshoot  max I_out  sat ms  at +1  at -1  settle</span></div></div><div><div><div>43</div></div><div><span><span> </span></span><span>outer_aw_off    2.1962     46.41%      7.329     467    229    238  1.240s</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>outer_aw_on    1.5238      1.59%      0.379     302    182    120  0.329s</span></div></div><div><div><div>45</div></div><div><span>figure: figures/ch07/single-vs-cascade.svg</span></div></div><div><div><div>46</div></div><div><span>figure: figures/ch07/bandwidth-ratio.svg</span></div></div><div><div><div>47</div></div><div><span>figure: figures/ch07/spin-disturbance.svg</span></div></div><div><div><div>48</div></div><div><span>figure: figures/ch07/inner-saturation.svg</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>ch07 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>30 个文件的最大差异都是严格的 <code>0.000e+00</code>，不是“在 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 以内”。这一篇的循环里没有 <code>sin</code>、<code>cos</code>、<code>exp</code>，只有加减乘除和比较，IEEE 754 的基本运算在三种语言里是同一个结果，只要运算顺序一样（我照着 Python 的顺序写的），比特都一样。前面并不总是这样干净：第 6 篇的最大差异是 <span><span>2.2×10−162.2\times10^{-16}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>，我没有追查原因，猜是 <code>libm</code> 函数的末位差别。</p><section><h3>单环对串级：阶跃<a href="#单环对串级阶跃"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch07-single-vs-cascade.svg" alt="0.2 rad 阶跃：单环（三阶极点配置）上冲到约 0.26 rad，串级（比值 5）几乎没有超调，上升慢一倍" /><figcaption>0.2 rad 阶跃：单环（三阶极点配置）上冲到约 0.26 rad，串级（比值 5）几乎没有超调，上升慢一倍</figcaption></figure><p></p><p>0.2 rad 的阶跃，单环超调 29.89%，上升时间 0.095 s，调节时间 0.513 s；串级（比值 5）超调 0.08%，上升时间 0.186 s，调节时间 0.354 s。单环的超调主要来自上面说的 <span><span>−5-5</span><span><span><span></span><span>−</span><span>5</span></span></span></span> rad/s 零点，不是阻尼不足：把阶跃缩小到 5 mrad，不碰力矩限幅，<code>report</code> 另算了一遍，单环超调是 33.95%，比限幅下的还大 4 个百分点（限幅砍掉了一点峰值）。相角裕度 65.9° 一点也没预报这个超调：相角裕度说的是回路的鲁棒性，不是设定值响应的形状，第 2 篇的教训。</p><p>下图的下半部分是力矩：两种回路的第一拍都撞上了 <span><span>±1\pm1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> N·m 限幅，因为 0.2 rad 对单环是 <span><span>6.912×0.2=1.386.912\times0.2 = 1.38</span><span><span><span></span><span>6.912</span><span></span><span>×</span><span></span></span><span><span></span><span>0.2</span><span></span><span>=</span><span></span></span><span><span></span><span>1.38</span></span></span></span> N·m，对串级是 <span><span>12×0.2×1.658=3.9812\times0.2\times1.658 = 3.98</span><span><span><span></span><span>12</span><span></span><span>×</span><span></span></span><span><span></span><span>0.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1.658</span><span></span><span>=</span><span></span></span><span><span></span><span>3.98</span></span></span></span> N·m。单环贴了 14 ms，串级（比值 5）贴了 31 ms。这也意味着这一篇所有 0.2 rad 的阶跃响应都包含了饱和，线性分析要靠小阶跃单独验证。</p></section><section><h3>底盘转动<a href="#底盘转动"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch07-spin-disturbance.svg" alt="底盘角速度在 1.0 到 1.5 s 从 0 升到 6 rad/s：单环的云台角度误差鼓到约 7 mrad，串级不到 0.5 mrad，加前馈后基本是噪声" /><figcaption>底盘角速度在 1.0 到 1.5 s 从 0 升到 6 rad/s：单环的云台角度误差鼓到约 7 mrad，串级不到 0.5 mrad，加前馈后基本是噪声</figcaption></figure><p></p><p>这是实验的主角。三条曲线的峰值：</p>

<table><thead><tr><th>回路</th><th>峰值误差</th><th>区间 RMS</th></tr></thead><tbody><tr><td>单环</td><td>7.16 mrad</td><td>3.13 mrad</td></tr><tr><td>串级（比值 5）</td><td>0.41 mrad</td><td>0.14 mrad</td></tr><tr><td>串级 + 前馈</td><td>0.16 mrad</td><td>0.06 mrad</td></tr></tbody></table><p>串级把峰值砍到 1/17.5，加上底盘角速度前馈砍到 1/44.1。“区间”是 1.0 s 到 3.0 s。斜坡误差公式的预测：单环 6.944 mrad，仿真峰值是它的 1.03 倍；串级预测 0.278 mrad，仿真是 1.47 倍。串级这边偏大，我猜是因为 0.5 s 的斜坡对它的两个快极点够长，但角度测量噪声（<span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rad，也就是 0.1 mrad）已经占了峰值的相当一部分，我没有把噪声和斜坡响应拆开验证。两个回路的公式比值是 25 倍（<span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 之比），仿真峰值的比值是 17.5 倍，差的那部分我猜是噪声，加上 0.5 s 的斜坡对串级的快极点并没有长到稳态。</p><p><strong>代价。</strong> 稳态时三个回路都要用约 0.12 N·m 的力矩顶住 <span><span>bfωcb_f\omega_c</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以最后 0.5 s 力矩的 RMS 差不多：单环 0.12008、串级 0.12116 N·m。把 0.12 在平方和意义下减掉，粗算噪声分量是单环约 0.004、串级约 0.017 N·m，差 3.8 倍。这是我对打印数字的心算，<code>report</code> 没有直接打噪声分量。串级的内环增益 <span><span>kp,i=1.658k_{p,i} = 1.658</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>p</span><span>,</span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.658</span></span></span></span> 把陀螺噪声放大了 3 倍（对比单环的 <span><span>kdg=0.554k_{dg} = 0.554</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.554</span></span></span></span>），这部分是实打实的代价。</p></section><section><h3>带宽比<a href="#带宽比"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch07-bandwidth-ratio.svg" alt="比值越大外环越慢也越稳：比值 1.5 的相角裕度只有 61.7° 并且上冲，比值 3 以上超过 85° 且没有超调；精确内环（实线）的相角裕度比一阶近似（虚线）高" /><figcaption>比值越大外环越慢也越稳：比值 1.5 的相角裕度只有 61.7° 并且上冲，比值 3 以上超过 85° 且没有超调；精确内环（实线）的相角裕度比一阶近似（虚线）高</figcaption></figure><p></p><p>数字来自 <code>report</code> 的两张表。先是 0.2 rad 阶跃（含饱和）和线性分析：</p>

<table><thead><tr><th>回路</th><th>上升 (s)</th><th>超调</th><th>调节 (s)</th><th>饱和 (ms)</th><th>PM 精确</th><th>PM 一阶</th><th><span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> (rad/s)</th></tr></thead><tbody><tr><td>单环</td><td>0.095</td><td>29.89%</td><td>0.513</td><td>14</td><td>65.9°</td><td>无</td><td>28.29</td></tr><tr><td>串级，比值 1.5</td><td>0.063</td><td>15.62%</td><td>0.181</td><td>138</td><td>61.7°</td><td>59.0°</td><td>50.54</td></tr><tr><td>串级，比值 3</td><td>0.123</td><td>0.07%</td><td>0.241</td><td>48</td><td>85.4°</td><td>71.8°</td><td>22.34</td></tr><tr><td>串级，比值 5</td><td>0.186</td><td>0.08%</td><td>0.354</td><td>31</td><td>88.7°</td><td>78.6°</td><td>12.49</td></tr><tr><td>串级，比值 10</td><td>0.363</td><td>0.08%</td><td>0.662</td><td>12</td><td>89.6°</td><td>84.1°</td><td>6.06</td></tr></tbody></table><p>再是 5 mrad 的小阶跃（<code>outer_margins</code> 的相角裕度配上一次不饱和的仿真，这一张表只有 Python 跑，没有跨语言对比）：单环 33.946%，比值 1.5 是 4.608%，比值 3、5、10 都是 0.000%。</p><p>几个结论：</p><ul>
<li><strong>比值 1.5 是唯一出问题的。</strong> 相角裕度 61.7°，不饱和时超调 4.6%，被限幅打断之后（138 ms 贴着上限）超调 15.6%，三倍多。比值 3 以上，线性超调是 0，相角裕度 85° 以上。</li>
<li><strong>比值 3 以后是在用速度换不必要的裕度。</strong> 相角裕度从 85.4° 到 88.7° 到 89.6°，只多了 4.2°，上升时间从 0.123 s 到 0.186 s 到 0.363 s，慢了 2.95 倍。在这些假设下，这个对象用比值 3 到 5 就够了；“内环快十倍”的口诀在这里是浪费。</li>
<li><strong>一阶近似偏保守。</strong> 比值 1.5 时低 2.7°，比值 3 时低 13.6°。我猜是因为内环 PI 的零点（<span><span>−43.4-43.4</span><span><span><span></span><span>−</span><span>43.4</span></span></span></span> rad/s）在外环穿越频率附近带来了超前，一阶近似里没有它，我没有把零点拿掉重算来验证。这是我没想到的：粗近似不是“偏乐观”而是“偏悲观”。</li>
<li><strong>大阶跃下比值 5 到 10 的超调都是 0.07% 到 0.08%</strong>，差别在噪声量级，<code>report</code> 没有在这上面断言。</li>
</ul></section><section><h3>内环饱和<a href="#内环饱和"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch07-inner-saturation.svg" alt="1.5 rad 大阶跃：外环一直积分时角度冲到 2.2 rad（超调 46%），外环积分项涨到 7.3 rad/s；内环饱和时冻结外环积分，只超调 1.6%" /><figcaption>1.5 rad 大阶跃：外环一直积分时角度冲到 2.2 rad（超调 46%），外环积分项涨到 7.3 rad/s；内环饱和时冻结外环积分，只超调 1.6%</figcaption></figure><p></p><p>外环 PI，<span><span>ko=12k_o = 12</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>o</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12</span></span></span></span>、<span><span>ki,o=28.8k_{i,o} = 28.8</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>i</span><span>,</span><span>o</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>28.8</span></span></span></span>，1.5 rad 阶跃：</p>

<table><thead><tr><th>算例</th><th>峰值 ψ (rad)</th><th>超调</th><th>外环积分峰值 (rad/s)</th><th>饱和 (ms)</th><th>调节 (s)</th></tr></thead><tbody><tr><td><code>outer_aw_off</code></td><td>2.1962</td><td>46.41%</td><td>7.329</td><td>467</td><td>1.240</td></tr><tr><td><code>outer_aw_on</code></td><td>1.5238</td><td>1.59%</td><td>0.379</td><td>302</td><td>0.329</td></tr></tbody></table><p>冻结外环积分把超调从 46.41% 降到 1.59%（29 倍），积分峰值从 7.329 降到 0.379 rad/s（19 倍），调节时间从 1.240 s 缩到 0.329 s（3.8 倍）。内环饱和的时间也短了，467 ms 对 302 ms：外环不再把速度设定值积到离谱的高度，少饱和了 165 ms。<code>report</code> 把两种饱和分开数了：<code>outer_aw_off</code> 先贴着 <span><span>+1+1</span><span><span><span></span><span>+</span><span>1</span></span></span></span> N·m 229 ms，再贴着 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> N·m 反向刹车 238 ms；<code>outer_aw_on</code> 是 182 ms 和 120 ms，刹车的那一段短了一半。</p></section><section><h3>这些数字的边界<a href="#这些数字的边界"><span>#</span></a></h3><p>上面的比较有几处要打折，我逐条列出来：</p><ul>
<li><strong>前馈的 44 倍是上限。</strong> 前馈用的是精确的 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，没有给底盘陀螺加噪声和延迟。真车上的底盘陀螺有噪声，而且和云台陀螺不是同一颗，这 44 倍应当打折，打多少折我没测。</li>
<li><strong>单环不是“最好的单环”。</strong> 我选的是三个极点都在 12 rad/s 的配方，它被锁在低增益里；前面已经推过，串级的等效增益塞进单环就是同一个扰动响应。这一篇比较的是“两种整定配方”，不是“单环这种结构”对“串级这种结构”。</li>
<li><strong>单环和串级的 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 不能直接比。</strong> 单环的开环断在对象输入，串级的断在速度设定值，两者的穿越频率是两个不同回路的量。表里放在一起只是为了省地方。</li>
<li><strong>饱和混进了线性分析。</strong> 0.2 rad 的阶跃对所有回路都撞限幅，所以阶跃响应的比较要和 5 mrad 的线性表一起看。</li>
<li><strong>冻结策略没有和条件积分比较。</strong> 见前面“内环饱和：外环怎么知道”一节。</li>
<li><strong>噪声和参数是假设。</strong> <span><span>JJ</span><span><span><span></span><span>J</span></span></span></span>、<span><span>bb</span><span><span><span></span><span>b</span></span></span></span>、<span><span>bfb_f</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和两个噪声标准差都是我取的量级，没有测过。一台真的云台，<span><span>bfb_f</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 很可能比这小一个量级，那样底盘转动的扰动就没有这里这么大。</li>
</ul></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch07</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>通过的标志是 30 个 <code>ok</code> 行最大差异都是 0，<code>report</code> 的断言全过，末尾打印 <code>ch07 OK</code>。编译参数和前几篇一样：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code> 加同样的警告，Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>。风格检查：C 用内核的 <code>checkpatch.pl</code>（<code>--no-tree</code>，忽略 SPDX 和 typedef 两项），0 errors、0 warnings；C++ 用 clang-format 的 Google 风格（<code>-Werror</code>）；Python 用 <code>ruff format --check</code> 和 <code>ruff check</code>，行宽 79；Rust 用 <code>cargo fmt --check</code>。四项都在提交前过了。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch07_cascade.py" target="_blank"><code>python/ch07_cascade.py</code></a></td><td><code>python3 python/ch07_cascade.py ref</code>，再 <code>report</code></td><td>10 个算例的 Python 参考输出；<code>report</code> 断言相角裕度、上升时间、小阶跃超调随比值单调，峰值单环 &gt; 串级 &gt; 串级加前馈，<code>outer_aw_on</code> 超调小于 <code>outer_aw_off</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid.py" target="_blank"><code>python/pidlab/pid.py</code></a></td><td>第 5 篇的 <code>Pid</code>，本篇不改</td><td>单环 1 个实例，串级 2 个实例</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/freq.py" target="_blank"><code>python/pidlab/freq.py</code></a></td><td>第 3 篇的 <code>margins</code></td><td>外环相角裕度：比值 1.5 是 61.7°，3 以上超过 85°</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a>，<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>sim.py</code></a></td><td>第 1 篇的 <code>GimbalAxis</code> 和 RK4</td><td>第三个输入 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 第一次非零</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/pid.h" target="_blank"><code>c/include/pl/pid.h</code></a>，<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid.c" target="_blank"><code>c/src/pid.c</code></a></td><td>第 5 篇的 <code>struct pl_pid</code></td><td>本篇不改</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch07.c" target="_blank"><code>c/apps/ch07.c</code></a></td><td><code>make -s ch07</code></td><td>写 10 个 <code>*_c.csv</code>，与 Python 最大差异 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid.h" target="_blank"><code>cpp/include/pidlab/pid.h</code></a></td><td>第 5 篇的 <code>Pid&lt;T&gt;</code></td><td>本篇不改</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch07.cc" target="_blank"><code>cpp/apps/ch07.cc</code></a></td><td><code>make -s ch07</code></td><td>写 10 个 <code>*_cpp.csv</code>，最大差异 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid.rs" target="_blank"><code>rust/src/pid.rs</code></a></td><td>第 5 篇的 <code>Pid</code></td><td>本篇不改</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch07.rs" target="_blank"><code>rust/src/bin/ch07.rs</code></a></td><td><code>make -s ch07</code></td><td>写 10 个 <code>*_rs.csv</code>，最大差异 0</td></tr><tr><td><code>out/ch07/</code>，<code>figures/ch07/</code>（构建产物，不入库）</td><td><code>make -s ch07</code></td><td>40 个 CSV，4 张 SVG（<code>single-vs-cascade</code>、<code>bandwidth-ratio</code>、<code>spin-disturbance</code>、<code>inner-saturation</code>）</td></tr></tbody></table></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把云台的偏航轴接成了单环和串级，用同一份带噪声的底盘转动数据去比：串级的角度误差峰值是 0.41 mrad，单环 7.16 mrad（17.5 倍），加底盘角速度前馈是 0.16 mrad；原因不是串级这个结构有什么魔法，而是它把三个增益放成了 <span><span>(1.658,91.9,864)(1.658, 91.9, 864)</span><span><span><span></span><span>(</span><span>1.658</span><span>,</span><span></span><span>91.9</span><span>,</span><span></span><span>864</span><span>)</span></span></span></span>，单环的配方放成了 <span><span>(0.554,6.9,34.6)(0.554, 6.9, 34.6)</span><span><span><span></span><span>(</span><span>0.554</span><span>,</span><span></span><span>6.9</span><span>,</span><span></span><span>34.6</span><span>)</span></span></span></span>，两者最慢的极点都在 12 rad/s 附近。带宽比 1.5 时相角裕度 61.7°，3 以上超过 85°；大阶跃让内环饱和时，外环积分冻结把超调从 46.41% 降到 1.59%。四种语言 30 个对比文件最大差异是 0。</p><p><a href="/posts/pid-control-08-mimo/">第 8 篇：多变量：耦合、RGA、解耦与麦轮底盘</a>离开云台，去看一个有四个轮子、三个自由度的底盘：四个电机的电流不是互相独立的，一个轮子的力会让整个底盘的速度和转动一起变。我会用 RGA 判断该怎么配对，用解耦器把耦合拆开，并且看到麦轮的零空间电流：四个轮子互相较劲，不产生任何底盘运动，却白白耗电。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-06-tuning/</id>
      <title type="text">整定：Ziegler–Nichols、继电器自整定与 SIMC</title>
      <published>2026-10-16T05:00:00.000Z</published>
      <updated>2026-10-16T05:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-06-tuning/"/>
      <summary type="text">PID 控制系列第 6 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95 把两点阶跃拟合、继电器自整定、两套 Ziegler–Nichols 规则和 SIMC 各写了四遍，在摩擦轮和 1/(s+1)^4 上比较。两点法在摩擦轮上把 3.00 ms 的等效延迟拟合成 1.39 ms，Ziegler–Nichols 阶跃法因此给出相位裕度 -48.7° 的增益；带 2 rad/s 滞环的继电器读到的 Ku 是 4.81，真值是 12.87，因为它测的不是 -180° 那一点；四阶对象上继电器的 Ku 只比真值低 1.7%；SIMC 的超调 6.8%，Ziegler–Nichols 继电器法 36.2%，代价是负载 IAE 3.75 对 1.38。</summary>
      <content type="html"><![CDATA[<p>手调 PID 最常见的一招是：Ki、Kd 先放着，把 Kp 一点点加大，直到电机开始啸叫，再退回去一半左右。这个手艺有名字，它是 <a href="https://asmedigitalcollection.asme.org/fluidsengineering/article/64/8/759/1155342/Optimum-Settings-for-Automatic-Controllers" target="_blank">Ziegler 和 Nichols 1942 年发表的“临界增益法”</a>的手动版，“一半左右”大致对应他们的 <span><span>0.6Ku0.6K_u</span><span><span><span></span><span>0.6</span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。<strong>这一篇我把“凭手感”拆成三个可以写成函数的步骤：测一条阶跃响应，拟合一个一阶加延迟模型；或者让继电器替我把回路推到临界振荡，读出 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>PuP_u</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>；再用 Ziegler–Nichols 或 <a href="https://skoge.folk.ntnu.no/publications/2003/tuningPID/finalpaper.pdf" target="_blank">Skogestad 的 SIMC 规则</a>算增益。</strong> 对象是第 5 篇的两个：一个带电流环和 2 拍指令延迟的摩擦轮，一个教科书里的 <span><span>1/(s+1)41/(s+1)^4</span><span><span><span></span><span>1/</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span>。结果比我预想的难看：阶跃法在摩擦轮上给出了一组线性回路不稳定的增益，继电器法在摩擦轮上读出的 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 比真值小 2.7 倍，而这个偏小的值恰好给出了最安全的一组增益。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch06</code> 通过，<code>out/ch06/</code> 下 56 个 CSV（两个对象各 7 个算例：<code>step</code>、<code>relay</code>、<code>ident</code>、<code>tuning</code>，以及 <code>eval_&lt;plant&gt;_{zn_step,zn_ultimate,simc}</code>，每个 Python、C、C++、Rust 各一份），<code>figures/ch06/</code> 下 5 张图；以及阶跃拟合、描述函数、Ziegler–Nichols 和 SIMC 的推导。
新增模块：Python 是 <code>pidlab/tuning.py</code>（<code>fopdt_fit</code>、<code>Relay</code>、<code>relay_analyse</code>、<code>zn_step</code>、<code>zn_ultimate</code>、<code>simc_pi</code>），另外在 <code>pidlab/plants.py</code> 里加了 <code>Flywheel</code> 和 <code>Benchmark4</code>；C 是 <code>pl/tuning.h</code> 和 <code>tuning.c</code>（<code>plants.h</code>、<code>plants.c</code> 里加了 <code>pl_flywheel</code>、<code>pl_benchmark4</code>）；C++ 是 <code>pidlab/tuning.h</code>（<code>plants.h</code> 里加了 <code>Flywheel&lt;T&gt;</code>、<code>Benchmark4&lt;T&gt;</code>）；Rust 是 <code>tuning.rs</code>（<code>plants.rs</code> 里加了 <code>Flywheel</code>、<code>Benchmark4</code>）。评估用的是第 5 篇的 <code>Pid</code>，没有新的控制器代码。
前置：<a href="/posts/pid-control-05-practical/">第 5 篇</a>。需要的数学：一阶系统的阶跃响应，第 3 篇的 Bode 图和灵敏度峰值 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，一点点傅里叶级数。</p></div></div>
<p>这一篇的代码都很短，我把它们和对应的推导放在一起，哪一步的数学落成哪几行代码，一眼能看到。完整文件在仓库里，文末的检查点给了链接。</p>
<section><h2>从对象到规则<a href="#从对象到规则"><span>#</span></a></h2><section><h3>两个对象<a href="#两个对象"><span>#</span></a></h3><p><strong>摩擦轮</strong>是这一篇新加的第一个对象，装在转子侧：</p><span><span><span>Jω˙=Kt (i−d)−b ω,τc i˙=icmd−iJ\dot\omega = K_t\,(i - d) - b\,\omega,\qquad \tau_c\,\dot i = i_{cmd} - i</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>(</span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span>d</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span></span><span>ω</span><span>,</span><span></span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span>i</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>i</span></span></span></span></span><p><span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 是折算成电流的负载（A），指令 <span><span>icmdi_{cmd}</span><span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 先过 2 拍（2 ms）的传输延迟，再进电流环。参数是 <span><span>J=4×10−4J = 4\times10^{-4}</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> kg·m²、<span><span>b=2×10−4b = 2\times10^{-4}</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>、<span><span>τc=1\tau_c = 1</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> ms，全是我取的。<span><span>Kt=0.3/(3591/187)=0.01562K_t = 0.3/(3591/187) = 0.01562</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3/</span><span>(</span><span>3591/187</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01562</span></span></span></span> N·m/A：0.3 来自第 1 篇引用的 M3508 P19 手册，3591/187 是我记得的 P19 减速比，<strong>这个比值我没有从手册里核实，当作假设</strong>。所以直流增益 <span><span>Kt/b=78.11K_t/b = 78.11</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>78.11</span></span></span></span> rad/s 每安培，机械时间常数 <span><span>J/b=2.0J/b = 2.0</span><span><span><span></span><span>J</span><span>/</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>2.0</span></span></span></span> s，而电流环加延迟加起来只有 3 ms：一个典型的“滞后主导”对象，<span><span>τ/θ\tau/\theta</span><span><span><span></span><span>τ</span><span>/</span><span>θ</span></span></span></span> 大约 667。</p><p><strong>四阶对象</strong> <span><span>G(s)=1/(s+1)4G(s) = 1/(s+1)^4</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>)</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> 是整定论文里常见的基准，它有 4 个重合的极点，没有延迟，但慢极点串起来的效果很像延迟。它的采样周期 <span><span>Ts=10T_s = 10</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> ms，无限幅。</p><p>两个新对象放在 <code>pidlab/plants.py</code>。摩擦轮把 2 拍延迟留给仿真的 <code>DelayLine</code>（和第 2 篇起的 <code>DialPosition</code> 一样，延迟属于回路不属于 ODE），所以类里只有两个状态 <span><span>[ω,i][\omega, i]</span><span><span><span></span><span>[</span><span>ω</span><span>,</span><span></span><span>i</span><span>]</span></span></span></span> 和两路输入 <span><span>[icmd,d][i_{cmd}, d]</span><span><span><span></span><span>[</span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>d</span><span>]</span></span></span></span>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>Flywheel</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>kt: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>0.3</span><span><span> </span><span>/</span><span> (</span></span><span>3591.0</span><span><span> </span><span>/</span><span> </span></span><span>187.0</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>j: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>4e-4</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>b: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>2e-4</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>tau_c: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>0.001</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>u_max: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>20.0</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>2</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> </span><span>2</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>delay </span><span>=</span><span> </span><span>2</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>15</div></div><div><span>        </span><span>return</span><span> [</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(</span><span>self</span><span><span>.kt </span><span>*</span><span> (x[</span></span><span>1</span><span><span>] </span><span>-</span><span> u[</span></span><span>1</span><span><span>]) </span><span>-</span><span> </span></span><span>self</span><span><span>.b </span><span>*</span><span> x[</span></span><span>0</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.j,</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(u[</span><span>0</span><span><span>] </span><span>-</span><span> x[</span></span><span>1</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.tau_c,</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四阶对象是四个串联的一阶滞后，负载 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 从输入端减去：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>Benchmark4</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>tau: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>4</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> </span><span>2</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>return</span><span> [</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>(u[</span><span>0</span><span><span>] </span><span>-</span><span> u[</span></span><span>1</span><span><span>] </span><span>-</span><span> x[</span></span><span>0</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.tau,</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>(x[</span><span>0</span><span><span>] </span><span>-</span><span> x[</span></span><span>1</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.tau,</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>(x[</span><span>1</span><span><span>] </span><span>-</span><span> x[</span></span><span>2</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.tau,</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>(x[</span><span>2</span><span><span>] </span><span>-</span><span> x[</span></span><span>3</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.tau,</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>一阶加延迟模型<a href="#一阶加延迟模型"><span>#</span></a></h3><p>整定规则几乎都吃同一种模型，FOPDT（first-order plus dead time）：</p><span><span><span>G(s)≈k e−θsτs+1G(s) \approx \frac{k\,e^{-\theta s}}{\tau s + 1}</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>τ</span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>k</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>θ</span><span>s</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 是静态增益，<span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 是时间常数，<span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 是等效延迟。给输入一个阶跃 <span><span>Δu\Delta u</span><span><span><span></span><span>Δ</span><span>u</span></span></span></span>，输出的归一化响应 <span><span>z(t)=(y(t)−y0)/Δyz(t) = (y(t) - y_0)/\Delta y</span><span><span><span></span><span>z</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>y</span><span>(</span><span>t</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/Δ</span><span>y</span></span></span></span>（<span><span>Δy\Delta y</span><span><span><span></span><span>Δ</span><span>y</span></span></span></span> 是最终变化量）在 <span><span>t&gt;θt &gt; \theta</span><span><span><span></span><span>t</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>θ</span></span></span></span> 之后是</p><span><span><span>z(t)=1−e−(t−θ)/τz(t) = 1 - e^{-(t-\theta)/\tau}</span><span><span><span></span><span>z</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>(</span><span>t</span><span>−</span><span>θ</span><span>)</span><span>/</span><span>τ</span></span></span></span></span></span></span></span></span></span></span></span></span><p>只有 <span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 和 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 两个未知数，读两个点就够了。我取 28.3% 和 63.2%：<span><span>z=0.632z = 0.632</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>0.632</span></span></span></span> 时 <span><span>(t−θ)/τ=−ln⁡(1−0.632)=0.9997≈1(t-\theta)/\tau = -\ln(1-0.632) = 0.9997 \approx 1</span><span><span><span></span><span>(</span><span>t</span><span></span><span>−</span><span></span></span><span><span></span><span>θ</span><span>)</span><span>/</span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span></span><span>ln</span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>0.632</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.9997</span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span></span></span></span>；<span><span>z=0.283z = 0.283</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>0.283</span></span></span></span> 时 <span><span>−ln⁡(1−0.283)=0.3327≈1/3-\ln(1-0.283) = 0.3327 \approx 1/3</span><span><span><span></span><span>−</span><span></span><span>ln</span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>0.283</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3327</span><span></span><span>≈</span><span></span></span><span><span></span><span>1/3</span></span></span></span>。记这两个时刻为 <span><span>t63t_{63}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>63</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>t28t_{28}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>28</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，则</p><span><span><span>t63−t28≈23τ,τ=1.5 (t63−t28),θ=t63−τ,k=ΔyΔut_{63} - t_{28} \approx \tfrac{2}{3}\tau,\qquad \tau = 1.5\,(t_{63} - t_{28}),\qquad \theta = t_{63} - \tau,\qquad k = \frac{\Delta y}{\Delta u}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>63</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>28</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>3</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>τ</span><span>,</span><span></span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.5</span><span></span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>63</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>28</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>63</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>τ</span><span>,</span><span></span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>Δ</span><span>u</span></span></span><span><span></span><span></span></span><span><span></span><span><span>Δ</span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><strong>这个拟合的便宜之处是它对噪声不敏感（只用了两个点），代价是它把“1.5”和“0.283、0.632”这些取整的常数都当成精确的。</strong> 当 <span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 比 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 大几百倍的时候，取整误差会把 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 吃掉：精确的常数是 0.9997 和 0.6670×1.5 = 1.0005，<span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 的误差是 <span><span>(0.9997−1.0005)τ=−0.0008τ(0.9997 - 1.0005)\tau = -0.0008\tau</span><span><span><span></span><span>(</span><span>0.9997</span><span></span><span>−</span><span></span></span><span><span></span><span>1.0005</span><span>)</span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.0008</span><span>τ</span></span></span></span>，对 <span><span>τ=2\tau = 2</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> s 就是 1.7 ms。我先写在这里，后面的摩擦轮会撞上它。</p><p>写成代码，这个拟合就是 <code>pidlab/tuning.py</code> 里的 <code>fopdt_fit</code>。它先用线性插值找两个交叉时间，再套上面的三个式子：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>_cross</span><span>(</span><span>z</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>level</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>ts</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>len</span><span>(z)):</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span><span> z[i] </span><span>&gt;=</span><span> level:</span></span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>frac </span><span>=</span><span> (level </span><span>-</span><span> z[i </span><span>-</span><span> </span><span>1</span><span><span>]) </span><span>/</span><span> (z[i] </span><span>-</span><span> z[i </span><span>-</span><span> </span></span><span>1</span><span>])</span></div></div><div><div><div>6</div></div><div><span>            </span><span>return</span><span><span> (i </span><span>-</span><span> </span></span><span>1</span><span><span> </span><span>+</span><span> frac) </span><span>*</span><span> ts</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> math.nan</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>fopdt_fit</span><span>(</span><span>u</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>y</span></span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>ts</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; Fopdt:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>k0 </span><span>=</span><span> </span><span>next</span><span>((i </span><span>for</span><span> i, ui </span><span>in</span><span> </span><span>enumerate</span><span>(u) </span><span>if</span><span><span> ui </span><span>!=</span><span> u[</span></span><span>0</span><span><span>]), </span><span>-</span></span><span>1</span><span>)</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span><span> k0 </span><span>&lt;</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>5</div></div><div><span>        </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"no step in u"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>du </span><span>=</span><span> u[k0] </span><span>-</span><span> u[</span><span>0</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>y0 </span><span>=</span><span> y[k0]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>dy </span><span>=</span><span> y[</span><span>-</span><span>1</span><span><span>] </span><span>-</span><span> y0</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span><span> dy </span><span>==</span><span> </span></span><span>0.0</span><span>:</span></div></div><div><div><div>10</div></div><div><span>        </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"y did not move"</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> [(y[i] </span><span>-</span><span> y0) </span><span>/</span><span> dy </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(k0, </span><span>len</span><span>(y))]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>t28 </span><span>=</span><span> </span><span>_cross</span><span>(z, T28, ts)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>t63 </span><span>=</span><span> </span><span>_cross</span><span>(z, T63, ts)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>tau </span><span>=</span><span> </span><span>1.5</span><span><span> </span><span>*</span><span> (t63 </span><span>-</span><span> t28)</span></span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> t63 </span><span>-</span><span> tau</span></div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> </span><span>not</span><span><span> (tau </span><span>&gt;</span><span> </span></span><span>0.0</span><span> </span><span>and</span><span><span> theta </span><span>&gt;</span><span> </span></span><span>0.0</span><span>):</span></div></div><div><div><div>17</div></div><div><span>        </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"no usable first-order-plus-delay fit"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>return</span><span><span> </span><span>Fopdt</span><span>(dy </span><span>/</span><span> du, tau, theta)</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>插值是必须的。</strong> 采样周期是 1 ms 和 10 ms，<span><span>t28t_{28}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>28</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>t63t_{63}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>63</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 不会正好落在采样点上，不插值会引入最多一个采样周期的误差，对摩擦轮的 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>（1.39 ms）这个误差就是 70%。另外，<code>fopdt_fit</code> 要求记录已经稳定：终值直接取最后一个采样点，不外推。我的摩擦轮阶跃记录 20 s，<span><span>τ=2\tau = 2</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> s，是 10 倍时间常数；四阶对象记录 40 s，足够。</p></section><section><h3>Ziegler–Nichols 两套规则<a href="#zieglernichols-两套规则"><span>#</span></a></h3><p>两套规则都输出 PID 的参数，我统一换成并联形式 <span><span>u=kpe+ki∫e+kde˙u = k_pe + k_i\int e + k_d\dot e</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>e</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span>，其中 <span><span>ki=kp/Tik_i = k_p/T_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>kd=kpTdk_d = k_pT_d</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p><p><strong>阶跃法（reaction curve）：</strong> 用 FOPDT 的三个参数。</p><span><span><span>kp=1.2 τk θ,Ti=2θ,Td=0.5 θk_p = \frac{1.2\,\tau}{k\,\theta},\qquad T_i = 2\theta,\qquad T_d = 0.5\,\theta</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span></span><span>θ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1.2</span><span></span><span>τ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>θ</span><span>,</span><span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span><span></span><span>θ</span></span></span></span></span><p>据我所知原文是用“反应速率”<span><span>R=k/τR = k/\tau</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span>k</span><span>/</span><span>τ</span></span></span></span> 和延迟 <span><span>L=θL = \theta</span><span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span>θ</span></span></span></span> 写的，<span><span>kp=1.2/(RL)k_p = 1.2/(RL)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.2/</span><span>(</span><span>R</span><span>L</span><span>)</span></span></span></span>，两种写法相同。这条规则为什么长这样？拿一个滞后主导的对象，<span><span>τ≫θ\tau \gg \theta</span><span><span><span></span><span>τ</span><span></span><span>≫</span><span></span></span><span><span></span><span>θ</span></span></span></span>，开环近似成积分器加延迟 <span><span>kτse−θs\frac{k}{\tau s}e^{-\theta s}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>τ</span><span>s</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>θ</span><span>s</span></span></span></span></span></span></span></span></span></span></span></span>。<span><span>kpk/τ=1.2/θk_p k/\tau = 1.2/\theta</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>k</span><span>/</span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.2/</span><span>θ</span></span></span></span>，所以回路增益在 <span><span>ωc≈1.2/θ\omega_c \approx 1.2/\theta</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1.2/</span><span>θ</span></span></span></span> 处穿过 1。在这个频率上，延迟贡献 <span><span>−1.2-1.2</span><span><span><span></span><span>−</span><span>1.2</span></span></span></span> rad <span><span>=−68.8°= -68.8°</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>68.8°</span></span></span></span>，积分器 <span><span>−90°-90°</span><span><span><span></span><span>−</span><span>90°</span></span></span></span>，PID 的相位是 <span><span>arctan⁡(ωcTd−1/(ωcTi))=arctan⁡(0.6−0.4167)=+10.4°\arctan(\omega_cT_d - 1/(\omega_cT_i)) = \arctan(0.6 - 0.4167) = +10.4°</span><span><span><span></span><span>arctan</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>1/</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>))</span><span></span><span>=</span><span></span></span><span><span></span><span>arctan</span><span>(</span><span>0.6</span><span></span><span>−</span><span></span></span><span><span></span><span>0.4167</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>+</span><span>10.4°</span></span></span></span>，合计 <span><span>−148.4°-148.4°</span><span><span><span></span><span>−</span><span>148.4°</span></span></span></span>，<strong>相位裕度大约 32°</strong>。我另外跑了一个一次性的脚本核对（没有放进仓库）：<span><span>τ/θ=50\tau/\theta = 50</span><span><span><span></span><span>τ</span><span>/</span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>50</span></span></span></span> 时 32.3°，500 时 31.5°。32° 的相位裕度意味着阶跃响应很振荡，据我所知这是故意的：原文追求的是扰动响应每个周期衰减到四分之一，不是平滑。<a href="https://skoge.folk.ntnu.no/publications/2003/tuningPID/finalpaper.pdf" target="_blank">Skogestad 的论文</a>也把 Ziegler–Nichols 的设定叫做 “rather aggressive”。</p><p><strong>临界法（ultimate cycle）：</strong> 只用 P 控制把回路推到等幅振荡，读出此时的增益 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和周期 <span><span>PuP_u</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>kp=0.6 Ku,Ti=Pu/2,Td=Pu/8k_p = 0.6\,K_u,\qquad T_i = P_u/2,\qquad T_d = P_u/8</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.6</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span><span>,</span><span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/8</span></span></span></span></span><p><span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是奈奎斯特曲线穿过 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 时 <span><span>∣G∣|G|</span><span><span><span></span><span>∣</span><span>G</span><span>∣</span></span></span></span> 的倒数，<span><span>Pu=2π/ωuP_u = 2\pi/\omega_u</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>π</span><span>/</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。<span><span>kp=0.6Kuk_p = 0.6K_u</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.6</span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 意味着只用了 4.4 dB 的增益裕度，再扣掉 PI 加 D 在这一频率附近的相位影响，就是一个不太宽裕的回路。</p><p>两套规则都是几行算术，输出统一的 <code>Gains(kp, ki, kd)</code>，所以后面可以直接喂给第 5 篇的 <code>PidConfig</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>zn_step</span><span>(</span><span>m</span><span>:</span><span> Fopdt</span><span>) -&gt; Gains:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>kp </span><span>=</span><span> </span><span>1.2</span><span><span> </span><span>*</span><span> m.tau </span><span>/</span><span> (m.k </span><span>*</span><span> m.theta)</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span><span> </span><span>Gains</span><span>(kp, kp </span><span>/</span><span> (</span></span><span>2.0</span><span><span> </span><span>*</span><span> m.theta), kp </span><span>*</span><span> (</span></span><span>0.5</span><span><span> </span><span>*</span><span> m.theta))</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>zn_ultimate</span><span>(</span><span>ku</span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>pu</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; Gains:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>kp </span><span>=</span><span> </span><span>0.6</span><span><span> </span><span>*</span><span> ku</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span><span> </span><span>Gains</span><span>(kp, kp </span><span>/</span><span> (pu </span><span>/</span><span> </span></span><span>2.0</span><span><span>), kp </span><span>*</span><span> (pu </span><span>/</span><span> </span></span><span>8.0</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>继电器和描述函数<a href="#继电器和描述函数"><span>#</span></a></h3><p>手动加 Kp 到振荡有两个毛病：要有人盯着，而且真的会把电机推到振荡的边缘。<a href="https://www.sciencedirect.com/science/article/pii/0005109884900141" target="_blank">Åström 和 Hägglund 1984 年的方案</a>是用继电器代替手：控制器输出 <span><span>+d+d</span><span><span><span></span><span>+</span><span>d</span></span></span></span> 或 <span><span>−d-d</span><span><span><span></span><span>−</span><span>d</span></span></span></span>，误差变号就翻转，回路自己会找到一个极限环，极限环的幅值和周期里带着 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>PuP_u</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。<strong>这跟推秋千一样：你不需要知道秋千的固有频率，只要每次在它过最低点的时候推一把，它自己会稳定在它的固有频率上。</strong></p><p>设继电器输入 <span><span>e(t)=asin⁡ωte(t) = a\sin\omega t</span><span><span><span></span><span>e</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span>sin</span><span></span><span>ω</span><span>t</span></span></span></span>，输出是幅值 <span><span>±d\pm d</span><span><span><span></span><span>±</span><span>d</span></span></span></span> 的方波（先令滞环 <span><span>ϵ=0\epsilon = 0</span><span><span><span></span><span>ϵ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>）。方波的基波分量用傅里叶系数算：</p><span><span><span>b1=1π∫02πu(ϑ)sin⁡ϑ dϑ=2π∫0πdsin⁡ϑ dϑ=4dπb_1 = \frac{1}{\pi}\int_0^{2\pi} u(\vartheta)\sin\vartheta\,d\vartheta = \frac{2}{\pi}\int_0^{\pi} d\sin\vartheta\,d\vartheta = \frac{4d}{\pi}</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>2</span><span>π</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>u</span><span>(</span><span>ϑ</span><span>)</span><span></span><span>sin</span><span></span><span>ϑ</span><span></span><span>d</span><span>ϑ</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>π</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>d</span><span></span><span>sin</span><span></span><span>ϑ</span><span></span><span>d</span><span>ϑ</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span></span></span><span><span></span><span></span></span><span><span></span><span><span>4</span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><span><span>cos⁡\cos</span><span><span><span></span><span>cos</span></span></span></span> 项由对称性为 0。<strong>描述函数</strong>把继电器当成一个幅值相关的增益，只保留基波：</p><span><span><span>N(a)=b1a=4dπaN(a) = \frac{b_1}{a} = \frac{4d}{\pi a}</span><span><span><span></span><span>N</span><span>(</span><span>a</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span>4</span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>回路是继电器 <span><span>N(a)N(a)</span><span><span><span></span><span>N</span><span>(</span><span>a</span><span>)</span></span></span></span> 加线性对象 <span><span>G(s)G(s)</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span></span></span></span> 的负反馈。持续振荡要求基波绕回来后自洽（谐波平衡）：<span><span>1+N(a)G(jω)=01 + N(a)G(j\omega) = 0</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>N</span><span>(</span><span>a</span><span>)</span><span>G</span><span>(</span><span>j</span><span>ω</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，即</p><span><span><span>G(jω)=−1N(a)=−πa4dG(j\omega) = -\frac{1}{N(a)} = -\frac{\pi a}{4d}</span><span><span><span></span><span>G</span><span>(</span><span>j</span><span>ω</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>N</span><span>(</span><span>a</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>4</span><span>d</span></span></span><span><span></span><span></span></span><span><span></span><span><span>π</span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>右边是负实轴上的一点，所以 <span><span>ω\omega</span><span><span><span></span><span>ω</span></span></span></span> 就是 <span><span>GG</span><span><span><span></span><span>G</span></span></span></span> 穿过 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 的频率 <span><span>ωu\omega_u</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>Pu=2π/ωuP_u = 2\pi/\omega_u</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>π</span><span>/</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>；幅值条件 <span><span>∣G(jωu)∣=πa/(4d)=1/Ku|G(j\omega_u)| = \pi a/(4d) = 1/K_u</span><span><span><span></span><span>∣</span><span>G</span><span>(</span><span>j</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>π</span><span>a</span><span>/</span><span>(</span><span>4</span><span>d</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 给出</p><span><span><span>Ku=4dπaK_u = \frac{4d}{\pi a}</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span>4</span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><strong>近似的前提是对象是低通的，<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 里的三次、五次谐波可以忽略。</strong> 四阶对象有四个极点，这个假设成立得很好，后面的图里 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 看起来是干净的正弦。</p><p><strong>现实里继电器有滞环 <span><span>ϵ\epsilon</span><span><span><span></span><span>ϵ</span></span></span></span>（也是为了不被噪声反复触发）</strong>：误差上升过 <span><span>+ϵ+\epsilon</span><span><span><span></span><span>+</span><span>ϵ</span></span></span></span> 才翻到 <span><span>+d+d</span><span><span><span></span><span>+</span><span>d</span></span></span></span>，下降过 <span><span>−ϵ-\epsilon</span><span><span><span></span><span>−</span><span>ϵ</span></span></span></span> 才翻到 <span><span>−d-d</span><span><span><span></span><span>−</span><span>d</span></span></span></span>。方波因此相对输入滞后一个角度 <span><span>φ=arcsin⁡(ϵ/a)\varphi = \arcsin(\epsilon/a)</span><span><span><span></span><span>φ</span><span></span><span>=</span><span></span></span><span><span></span><span>arcsin</span><span>(</span><span>ϵ</span><span>/</span><span>a</span><span>)</span></span></span></span>，基波的幅值仍然是 <span><span>4d/π4d/\pi</span><span><span><span></span><span>4</span><span>d</span><span>/</span><span>π</span></span></span></span>，但在原来的相位上多了 <span><span>−φ-\varphi</span><span><span><span></span><span>−</span><span>φ</span></span></span></span> 的滞后。展开：基波是 <span><span>4dπsin⁡(ωt−φ)\frac{4d}{\pi}\sin(\omega t - \varphi)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>π</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>4</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>sin</span><span>(</span><span>ω</span><span>t</span><span></span><span>−</span><span></span></span><span><span></span><span>φ</span><span>)</span></span></span></span>，所以</p><span><span><span>N(a)=4dπa e−jarcsin⁡(ϵ/a)N(a) = \frac{4d}{\pi a}\,e^{-j\arcsin(\epsilon/a)}</span><span><span><span></span><span>N</span><span>(</span><span>a</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span><span>a</span></span></span><span><span></span><span></span></span><span><span></span><span><span>4</span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span></span><span><span>a</span><span>r</span><span>c</span><span>s</span><span>i</span><span>n</span></span><span>(</span><span>ϵ</span><span>/</span><span>a</span><span>)</span></span></span></span></span></span></span></span></span></span></span></span></span><p>（精确的复数形式里实部是 <span><span>4dπa1−ϵ2/a2\frac{4d}{\pi a}\sqrt{1-\epsilon^2/a^2}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>π</span><span>a</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>4</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span>ϵ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/</span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，虚部是 <span><span>−4dπa⋅ϵa-\frac{4d}{\pi a}\cdot\frac{\epsilon}{a}</span><span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>π</span><span>a</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>4</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>ϵ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，模长是 <span><span>4dπa\frac{4d}{\pi a}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>π</span><span>a</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>4</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。）谐波平衡变成</p><span><span><span>G(jω)=−1N(a)=−πa4d e+jarcsin⁡(ϵ/a)G(j\omega) = -\frac{1}{N(a)} = -\frac{\pi a}{4d}\,e^{+j\arcsin(\epsilon/a)}</span><span><span><span></span><span>G</span><span>(</span><span>j</span><span>ω</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>N</span><span>(</span><span>a</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span>4</span><span>d</span></span></span><span><span></span><span></span></span><span><span></span><span><span>π</span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>+</span><span>j</span><span></span><span><span>a</span><span>r</span><span>c</span><span>s</span><span>i</span><span>n</span></span><span>(</span><span>ϵ</span><span>/</span><span>a</span><span>)</span></span></span></span></span></span></span></span></span></span></span></span></span><p>相位是 <span><span>−180°+arcsin⁡(ϵ/a)-180° + \arcsin(\epsilon/a)</span><span><span><span></span><span>−</span><span>180°</span><span></span><span>+</span><span></span></span><span><span></span><span>arcsin</span><span>(</span><span>ϵ</span><span>/</span><span>a</span><span>)</span></span></span></span>。<strong>有滞环的继电器测的不是 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 那一点，而是 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 往回一点的位置</strong>，这个位置比 <span><span>ωu\omega_u</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 低，<span><span>∣G∣|G|</span><span><span><span></span><span>∣</span><span>G</span><span>∣</span></span></span></span> 更大，所以读出来的 <span><span>4d/(πa)4d/(\pi a)</span><span><span><span></span><span>4</span><span>d</span><span>/</span><span>(</span><span>π</span><span>a</span><span>)</span></span></span></span> 比真正的 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 小。后面摩擦轮的实验会用上这个结论。</p><p>代码分两块。继电器本身是一个带状态的小类，初始状态是 <span><span>+d+d</span><span><span><span></span><span>+</span><span>d</span></span></span></span>，只在 <span><span>∣e∣&gt;ϵ|e| &gt; \epsilon</span><span><span><span></span><span>∣</span><span>e</span><span>∣</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>ϵ</span></span></span></span> 时翻转，<span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 和 <span><span>ϵ\epsilon</span><span><span><span></span><span>ϵ</span></span></span></span> 是构造参数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>Relay</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>e</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.state </span><span>&gt;</span><span> </span></span><span>0</span><span> </span><span>and</span><span><span> e </span><span>&lt;</span><span> </span><span>-</span></span><span>self</span><span>.eps:</span></div></div><div><div><div>5</div></div><div><span>            </span><span>self</span><span><span>.state </span><span>=</span><span> </span><span>-</span></span><span>1</span></div></div><div><div><div>6</div></div><div><span>        </span><span>elif</span><span> </span><span>self</span><span><span>.state </span><span>&lt;</span><span> </span></span><span>0</span><span> </span><span>and</span><span><span> e </span><span>&gt;</span><span> </span></span><span>self</span><span>.eps:</span></div></div><div><div><div>7</div></div><div><span>            </span><span>self</span><span><span>.state </span><span>=</span><span> </span></span><span>1</span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span> </span><span>self</span><span><span>.state </span><span>*</span><span> </span></span><span>self</span><span>.d</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后是事后分析一份记录的 <code>relay_analyse</code>。“上升沿”定义成 <code>u</code> 从非正变成正的那一个采样点，时间用 <span><span>e=r−ye = r - y</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span> 穿过 <span><span>+ϵ+\epsilon</span><span><span><span></span><span>+</span><span>ϵ</span></span></span></span> 的线性插值；周期取最后三个周期的平均，幅值 <span><span>aa</span><span><span><span></span><span>a</span></span></span></span> 取最后一个周期里 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 峰峰值的一半：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>relay_analyse</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>r</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>y</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>u</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ts</span><span>:</span><span> </span><span>float</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>eps</span><span>:</span><span> </span><span>float</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>d</span><span>:</span><span> </span><span>float</span><span>,</span></div></div><div><div><div>8</div></div><div><span>) -&gt; RelayResult:</span></div></div><div><div><div>9</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>ups </span><span>=</span><span> []</span></div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>len</span><span>(u)):</span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span><span> u[k] </span><span>&gt;</span><span> </span></span><span>0.0</span><span> </span><span>and</span><span><span> u[k </span><span>-</span><span> </span></span><span>1</span><span><span>] </span><span>&lt;=</span><span> </span></span><span>0.0</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>e0 </span><span>=</span><span> r[k </span><span>-</span><span> </span><span>1</span><span><span>] </span><span>-</span><span> y[k </span><span>-</span><span> </span></span><span>1</span><span>]</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>e1 </span><span>=</span><span> r[k] </span><span>-</span><span> y[k]</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>ups.</span><span>append</span><span>((k, (k </span><span>-</span><span> </span><span>1</span><span><span> </span><span>+</span><span> (eps </span><span>-</span><span> e0) </span><span>/</span><span> (e1 </span><span>-</span><span> e0)) </span><span>*</span><span> ts))</span></span></div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> </span><span>len</span><span><span>(ups) </span><span>&lt;</span><span> </span></span><span>4</span><span>:</span></div></div><div><div><div>17</div></div><div><span>        </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"fewer than four upward switches"</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>pu </span><span>=</span><span> (ups[</span><span>-</span><span>1</span><span>][</span><span>1</span><span><span>] </span><span>-</span><span> ups[</span><span>-</span></span><span>4</span><span>][</span><span>1</span><span><span>]) </span><span>/</span><span> </span></span><span>3.0</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>seg </span><span>=</span><span> y[ups[</span><span>-</span><span>2</span><span>][</span><span>0</span><span><span>] : ups[</span><span>-</span></span><span>1</span><span>][</span><span>0</span><span>]]</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> (</span><span>max</span><span><span>(seg) </span><span>-</span><span> </span></span><span>min</span><span><span>(seg)) </span><span>/</span><span> </span></span><span>2.0</span></div></div><div><div><div>21</div></div><div><span>    </span><span>return</span><span><span> </span><span>RelayResult</span><span>(</span></span><span>4.0</span><span><span> </span><span>*</span><span> d </span><span>/</span><span> (math.pi </span><span>*</span><span> a), pu, a)</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>这个函数不进控制回路</strong>，所以没有状态机，也不需要就地更新，只是读一份已经记录好的数据。</p></section><section><h3>SIMC<a href="#simc"><span>#</span></a></h3><p>SIMC（Skogestad IMC）是从另一个方向想问题：先指定想要的闭环响应，再反推控制器。对 FOPDT 对象 <span><span>g(s)=ke−θs/(τs+1)g(s) = ke^{-\theta s}/(\tau s+1)</span><span><span><span></span><span>g</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>k</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>θ</span><span>s</span></span></span></span></span></span></span></span></span><span>/</span><span>(</span><span>τ</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>，指定设定值到输出的闭环响应保留不可避免的延迟，其余是一阶：</p><span><span><span>yys=e−θsτcs+1\frac{y}{y_s} = \frac{e^{-\theta s}}{\tau_c s + 1}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>y</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>θ</span><span>s</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><span><span>τc\tau_c</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是唯一的整定参数。从 <span><span>T=gc/(1+gc)T = gc/(1+gc)</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>g</span><span>c</span><span>/</span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>g</span><span>c</span><span>)</span></span></span></span> 解出控制器 <span><span>c=1g⋅T1−Tc = \frac{1}{g}\cdot\frac{T}{1-T}</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>g</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>1</span><span>−</span><span>T</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>T</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，<span><span>1−T=(τcs+1−e−θs)/(τcs+1)1 - T = (\tau_cs + 1 - e^{-\theta s})/(\tau_cs+1)</span><span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>θ</span><span>s</span></span></span></span></span></span></span></span></span><span>)</span><span>/</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>，代入得</p><span><span><span>c(s)=τs+1k (τcs+1−e−θs)c(s) = \frac{\tau s + 1}{k\,(\tau_c s + 1 - e^{-\theta s})}</span><span><span><span></span><span>c</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span></span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span>1</span><span></span><span>−</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>θ</span><span>s</span></span></span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>τ</span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>这是个史密斯预估器的形状，没法直接放进 PID。用一阶泰勒 <span><span>e−θs≈1−θse^{-\theta s} \approx 1 - \theta s</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>θ</span><span>s</span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>θ</span><span>s</span></span></span></span>，分母变成 <span><span>k(τc+θ)sk(\tau_c + \theta)s</span><span><span><span></span><span>k</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>θ</span><span>)</span><span>s</span></span></span></span>：</p><span><span><span>c(s)≈τs+1k(τc+θ)s=Kp(1+1τs),Kp=τk(τc+θ),Ti=τc(s) \approx \frac{\tau s + 1}{k(\tau_c + \theta)s} = K_p\left(1 + \frac{1}{\tau s}\right),\qquad K_p = \frac{\tau}{k(\tau_c + \theta)},\quad T_i = \tau</span><span><span><span></span><span>c</span><span>(</span><span>s</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>θ</span><span>)</span><span>s</span></span></span><span><span></span><span></span></span><span><span></span><span><span>τ</span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span>(</span></span><span>1</span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>τ</span><span>s</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span></span><span></span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>θ</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>τ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span></span></span></span></span><p>这是 PI，积分时间等于对象的时间常数：积分项把对象的极点消掉。对设定值很好，对负载扰动，如果 <span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span> 很大（像摩擦轮的 2 s），<span><span>Ti=τT_i = \tau</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span></span></span></span> 会让扰动恢复得很慢。Skogestad 的修正是限制 <span><span>TiT_i</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：把对象近似成积分器 <span><span>k′/sk' / s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>/</span><span>s</span></span></span></span>（<span><span>k′=k/τk' = k/\tau</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>k</span><span>/</span><span>τ</span></span></span></span>），PI 的闭环特征式是</p><span><span><span>s2+k′Kp s+k′KpTi=0s^2 + k'K_p\,s + \frac{k'K_p}{T_i} = 0</span><span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>临界阻尼要求判别式 <span><span>(k′Kp)2=4k′Kp/Ti(k'K_p)^2 = 4k'K_p/T_i</span><span><span><span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，即 <span><span>Ti=4/(k′Kp)T_i = 4/(k'K_p)</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4/</span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。把 <span><span>Kp=1/(k′(τc+θ))K_p = 1/(k'(\tau_c+\theta))</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>θ</span><span>))</span></span></span></span> 代入得 <span><span>Ti=4(τc+θ)T_i = 4(\tau_c + \theta)</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>θ</span><span>)</span></span></span></span>。两个限制取小的：</p><span><span><span>Kp=τk(τc+θ),Ti=min⁡(τ, 4(τc+θ))K_p = \frac{\tau}{k(\tau_c+\theta)},\qquad T_i = \min\big(\tau,\ 4(\tau_c + \theta)\big)</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>k</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>θ</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>τ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span></span><span><span>(</span></span><span>τ</span><span>,</span><span> </span><span></span><span>4</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>θ</span><span>)</span><span><span>)</span></span></span></span></span></span><p>这个推导我对照了 <a href="https://skoge.folk.ntnu.no/publications/2003/tuningPID/finalpaper.pdf" target="_blank">Skogestad 2003</a> 的式（19）到式（24）。<span><span>τc=θ\tau_c = \theta</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>θ</span></span></span></span> 是他推荐的“紧”整定，我默认就用它：<span><span>Kp=τ/(2kθ)K_p = \tau/(2k\theta)</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span>/</span><span>(</span><span>2</span><span>k</span><span>θ</span><span>)</span></span></span></span>，<span><span>Ti=min⁡(τ,8θ)T_i = \min(\tau, 8\theta)</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>(</span><span>τ</span><span>,</span><span></span><span>8</span><span>θ</span><span>)</span></span></span></span>。<strong>要更保守就加大 <span><span>τc\tau_c</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，不用换公式</strong>，这是 SIMC 比 Ziegler–Nichols 好用的地方。同一篇论文还说：数字实现时采样周期 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 对等效延迟贡献约 <span><span>h/2h/2</span><span><span><span></span><span>h</span><span>/2</span></span></span></span>（ZOH 平均要等半个采样周期）；这一点我后面要用。</p><p>对应的代码只有三行有效算术，<code>tauc</code> 默认取 <code>theta</code>，返回的 <code>Gains</code> 里 <span><span>kd=0k_d = 0</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>simc_pi</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>k</span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>tau</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>theta</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>tauc</span></span><span>:</span><span> </span><span>float</span><span> </span><span>|</span><span><span> </span><span>None</span><span> </span></span><span>=</span><span><span> </span><span>None</span></span></div></div><div><div><div>3</div></div><div><span>) -&gt; Gains:</span></div></div><div><div><div>4</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> tauc </span><span>is</span><span> </span><span>None</span><span>:</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>tauc </span><span>=</span><span> theta</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>kp </span><span>=</span><span> tau </span><span>/</span><span> (k </span><span>*</span><span> (tauc </span><span>+</span><span> theta))</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>ti </span><span>=</span><span> </span><span>min</span><span>(tau, </span><span>4.0</span><span><span> </span><span>*</span><span> (tauc </span><span>+</span><span> theta))</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span><span> </span><span>Gains</span><span>(kp, kp </span><span>/</span><span> ti, </span></span><span>0.0</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>鲁棒性与性能<a href="#鲁棒性与性能"><span>#</span></a></h3><p>比较三套规则需要至少两个指标：</p><ul>
<li><strong>性能：</strong> IAE <span><span>=∫∣r−y∣ dt= \int|r - y|\,dt</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>∫</span><span></span><span>∣</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>∣</span><span></span><span>d</span><span>t</span></span></span></span>，设定值阶跃之后算一次（<code>IAE_sp</code>），负载阶跃之后算一次（<code>IAE_ld</code>）；再加设定值阶跃的超调。</li>
<li><strong>鲁棒性：</strong> 第 3 篇的灵敏度峰值 <span><span>Ms=max⁡ω∣1/(1+L(jω))∣M_s = \max_\omega|1/(1+L(j\omega))|</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>max</span><span><span><span><span><span><span></span><span><span>ω</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣1/</span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>(</span><span>j</span><span>ω</span><span>))</span><span>∣</span></span></span></span>，它是奈奎斯特曲线到 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 的最近距离的倒数。<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 1.4 到 2 之间通常算稳妥，更大就是离 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 太近。我用 <code>freq</code> 模块在真实对象（含延迟和第 4 篇的半拍 ZOH）上算。</li>
</ul><p><strong>注意：<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 只在回路稳定时是个有意义的“距离”。</strong> 不稳定的回路 <span><span>∣1+L∣|1+L|</span><span><span><span></span><span>∣1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>∣</span></span></span></span> 的最小值也可以很大；后面摩擦轮的阶跃法就是例子。</p><p>评估用第 5 篇的 <code>Pid</code>：<code>CLAMP</code> 抗饱和，微分滤波 <span><span>tf=Td/10t_f = T_d/10</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/10</span></span></span></span>，输出夹在执行器范围内。设定值阶跃之后，在一半的时间点加一个负载阶跃。</p><p>驱动 <code>python/ch06_tuning.py</code> 里有三条线：开环阶跃、继电器、闭环评估，都走第 5 篇的仿真合同（测量、控制器、限幅、延迟线、保持 10 个 RK4 子步）。<code>simulate</code> 把三种“控制器”收成一个 <code>ctrl(k, r, y) -&gt; u</code> 回调：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>simulate</span><span>(</span><span>spec</span><span>:</span><span> Spec</span><span>,</span><span><span> </span><span>data</span></span><span>:</span><span> </span><span>dict</span><span>,</span><span><span> </span><span>ctrl</span></span><span>) -&gt; dict[</span><span>str</span><span>, np.ndarray]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>plant </span><span>=</span><span> spec.</span><span>plant</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>delay </span><span>=</span><span> </span><span>DelayLine</span><span>(spec.delay)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> [</span><span>0.0</span><span><span>] </span><span>*</span><span> plant.n</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>rows </span><span>=</span><span> []</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(data[</span><span>"t"</span><span>])):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> data[</span><span>"r"</span><span>][k]</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> plant.</span><span>output</span><span>(x) </span><span>+</span><span> data[</span><span>"n"</span><span>][k]</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>ctrl</span><span>(k, r, y)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span><span>(u, </span><span>-</span><span>spec.u_lim), spec.u_lim)</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>([data[</span><span>"t"</span><span>][k], r, y, u])</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>advance</span><span>(</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>plant.deriv, x, [delay.</span><span>push</span><span>(u), data[</span><span>"d"</span><span><span>][k]], spec.ts, </span><span>SUBSTEPS</span></span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> </span><span>dict</span><span>(</span><span>zip</span><span><span>(</span><span>COLUMNS</span><span>, np.</span><span>array</span><span>(rows).T))</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>开环的回调从数据文件读 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>，继电器的回调是 <code>relay.update(r - y)</code>，闭环的回调是 <code>pid.update(r, y)</code>。数据文件由 Python 生成（<code>numpy</code> 种子 6），包含阶跃时刻、负载、继电器测试里 <span><span>σ=0.5\sigma = 0.5</span><span><span><span></span><span>σ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span></span></span></span> rad/s 的测量噪声。闭环评估用的配置把三套规则的增益统一接进第 5 篇的 <code>Pid</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>eval_config</span><span>(</span><span>spec</span><span>:</span><span> Spec</span><span>,</span><span><span> </span><span>g</span></span><span>:</span><span> Gains</span><span>) -&gt; PidConfig:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>return</span><span><span> </span><span>PidConfig</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>g.kp,</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>g.ki,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>g.kd,</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>spec.ts,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>tf</span><span><span>=</span><span>g.kd </span><span>/</span><span> g.kp </span><span>/</span><span> </span></span><span>10.0</span><span>,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>u_min</span><span><span>=-</span><span>spec.u_lim,</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>u_max</span><span><span>=</span><span>spec.u_lim,</span></span></div></div><div><div><div>10</div></div><div><span>        </span><span>windup</span><span><span>=</span><span>Windup.</span><span>CLAMP</span><span>,</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>IAE_sp</code> 用第 2 篇的 <code>step_metrics</code> 在负载阶跃之前截断，<code>IAE_ld</code> 是负载阶跃之后 <span><span>∣r−y∣|r - y|</span><span><span><span></span><span>∣</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>∣</span></span></span></span> 的矩形和：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>evaluate</span><span>(</span><span>spec</span><span>:</span><span> Spec</span><span>,</span><span><span> </span><span>o</span></span><span>:</span><span> </span><span>dict</span><span>) -&gt; tuple[</span><span>float</span><span>, </span><span>float</span><span>, </span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>y, ts </span><span>=</span><span> o[</span><span>"y"</span><span>], spec.ts</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>sm </span><span>=</span><span> </span><span>step_metrics</span><span>(y[: spec.eval_load], ts, spec.eval_sp, spec.eval_r)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>iae_ld </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> yi </span><span>in</span><span> y[spec.eval_load :]:</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>iae_ld </span><span>+=</span><span> </span><span>abs</span><span><span>(spec.eval_r </span><span>-</span><span> yi) </span><span>*</span><span> ts</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> sm.iae, iae_ld, sm.overshoot</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 由 <code>sensitivity</code> 用 <code>freq</code> 模块在真实的对象多项式上算，控制器是 <span><span>C(s)=kp+ki/s+kds/(tfs+1)C(s) = k_p + k_i/s + k_ds/(t_fs + 1)</span><span><span><span></span><span>C</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span> 在第 5 篇定义下的有理形式（<code>loop_polys</code>），延迟用 <code>theta_loop = (delay + 0.5)·ts</code>：传输延迟加半拍 ZOH。</p></section></section>
<section><h2>另外三种语言<a href="#另外三种语言"><span>#</span></a></h2><p>三个版本都是 <code>tuning.py</code> 的逐行翻译，算术顺序一样，所以第 6 篇没有新的数值问题。区别在这几处：</p><ul>
<li><strong>输出参数。</strong> C 没有多返回值，<code>pl_fopdt_fit</code>、<code>pl_relay_analyse</code> 返回 0 或 <code>-EINVAL</code>，结果写进调用方的结构体（<code>struct pl_fopdt</code>、<code>struct pl_relay_result</code>）；三个规则函数返回 <code>void</code>，写进 <code>struct pl_gains</code>。C++ 用 <code>std::optional&lt;Fopdt&lt;T&gt;&gt;</code> 和 <code>std::optional&lt;RelayResult&lt;T&gt;&gt;</code> 返回，规则函数直接返回 <code>Gains&lt;T&gt;</code>；Rust 是 <code>Option&lt;Fopdt&gt;</code> 和 <code>Option&lt;RelayResult&gt;</code>。Python 抛 <code>ValueError</code>，三种原生语言都没有异常，失败条件一样：没有阶跃、<span><span>Δy=0\Delta y = 0</span><span><span><span></span><span>Δ</span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>τθ≤0\tau\theta \le 0</span><span><span><span></span><span>τ</span><span>θ</span><span></span><span>≤</span><span></span></span><span><span></span><span>0</span></span></span></span>、不到四个上升沿、<span><span>a=0a = 0</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</li>
<li><strong>输入是切片。</strong> C 传 <code>const pl_real *</code> 加长度 <code>long n</code>；C++ 用 <code>std::span&lt;const T&gt;</code>；Rust 是 <code>&amp;[Real]</code>。三个函数都不拷贝数据，不分配内存。</li>
<li><strong>“滑动四个上升沿”。</strong> Python 把所有上升沿存进列表再取最后几个；C、C++、Rust 的 <code>relay_analyse</code> 只保留最后四个（<code>idx[4]</code>、<code>tm[4]</code>，每来一个就移位）。这是为了不需要容量未知的数组，结果相同。</li>
<li><strong><code>simc_pi</code> 的默认 <code>tauc</code>。</strong> C 用负数表示“用 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>”（<code>pl_simc_pi(k, tau, theta, -1.0, &amp;g)</code>）；C++ 是 <code>std::optional&lt;T&gt; tauc = std::nullopt</code>；Rust 的 <code>tauc: Option&lt;Real&gt;</code>，传 <code>None</code>。</li>
<li><strong><span><span>π\pi</span><span><span><span></span><span>π</span></span></span></span> 的写法。</strong> C 的 <code>-std=c11 -Wpedantic</code> 没有 <code>M_PI</code>，我写了 <code>PL_PI</code> 常量；Rust 用 <code>core::f64::consts::PI</code> 转 <code>Real</code>。</li>
<li><strong>类型。</strong> <code>pl_real</code>、<code>T</code>、<code>Real</code> 都是标量开关，第 10 篇会用到 float32；这里没有写死 <code>double</code>。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p><code>pl_relay_analyse</code> 是这一篇最长的函数，上升沿用一个移位的 4 元窗口收集：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>pl_relay_analyse</span><span>(</span><span>const</span><span> pl_real </span><span>*</span><span><span>r</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>y</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>u</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span>         </span><span>long</span><span><span> </span><span>n</span><span>,</span><span> pl_real </span><span>ts</span><span>,</span><span> pl_real </span><span>eps</span><span>,</span><span> pl_real </span><span>d</span><span>,</span></span></div></div><div><div><div>3</div></div><div><span>         </span><span>struct</span><span> pl_relay_result </span><span>*</span><span><span>out</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>pl_real </span><span>tm</span><span>[</span><span>4</span><span>] </span><span>=</span><span> { </span><span>0</span><span> }, lo, hi, a;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>long</span><span> </span><span>idx</span><span>[</span><span>4</span><span>] </span><span>=</span><span> { </span><span>0</span><span> }, k;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>int</span><span> cnt </span><span>=</span><span> </span><span>0</span><span>, j;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (k </span><span>=</span><span> </span><span>1</span><span>; k </span><span>&lt;</span><span> n; k</span><span>++</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>pl_real e0, e1;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>(</span><span>u</span><span>[k] </span><span>&gt;</span><span> </span><span>0.0</span><span> </span><span>&amp;&amp;</span><span> </span><span>u</span><span>[k </span><span>-</span><span> </span><span>1</span><span>] </span><span>&lt;=</span><span> </span><span>0.0</span><span>))</span></div></div><div><div><div>13</div></div><div><span>      </span><span>continue</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>e0 </span><span>=</span><span> </span><span>r</span><span>[k </span><span>-</span><span> </span><span>1</span><span>] </span><span>-</span><span> </span><span>y</span><span>[k </span><span>-</span><span> </span><span>1</span><span>];</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>e1 </span><span>=</span><span> </span><span>r</span><span>[k] </span><span>-</span><span> </span><span>y</span><span>[k];</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>      </span><span>idx</span><span>[j] </span><span>=</span><span> </span><span>idx</span><span>[j </span><span>+</span><span> </span><span>1</span><span>];</span></div></div><div><div><div>18</div></div><div><span>      </span><span>tm</span><span>[j] </span><span>=</span><span> </span><span>tm</span><span>[j </span><span>+</span><span> </span><span>1</span><span>];</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>    </span><span>idx</span><span>[</span><span>3</span><span>] </span><span>=</span><span> k;</span></div></div><div><div><div>21</div></div><div><span>    </span><span>tm</span><span>[</span><span>3</span><span>] </span><span>=</span><span> (k </span><span>-</span><span> </span><span>1</span><span> </span><span>+</span><span> (eps </span><span>-</span><span> e0) </span><span>/</span><span> (e1 </span><span>-</span><span> e0)) </span><span>*</span><span> ts;</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>cnt</span><span>++</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>  </span><span>if</span><span> (cnt </span><span>&lt;</span><span> </span><span>4</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>EINVAL;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>/* ... peak-to-peak of y over [idx[2], idx[3]) gives a ... */</span></div></div><div><div><div>27</div></div><div><span>  </span><span>out</span><span>-&gt;</span><span>pu</span><span> </span><span>=</span><span> (</span><span>tm</span><span>[</span><span>3</span><span>] </span><span>-</span><span> </span><span>tm</span><span>[</span><span>0</span><span>]) </span><span>/</span><span> </span><span>3.0</span><span>;</span></div></div><div><div><div>28</div></div><div><span>  </span><span>out</span><span>-&gt;</span><span>a</span><span> </span><span>=</span><span> a;</span></div></div><div><div><div>29</div></div><div><span>  </span><span>out</span><span>-&gt;</span><span>ku</span><span> </span><span>=</span><span> </span><span>4.0</span><span> </span><span>*</span><span> d </span><span>/</span><span> (PL_PI </span><span>*</span><span> a);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>SIMC 只有几行。注意 <code>tauc &lt; 0.0</code> 是“默认”的标记，而 <span><span>τc=0\tau_c = 0</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 是合法值（对应 <span><span>Ti=min⁡(τ,4θ)T_i = \min(\tau, 4\theta)</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>min</span><span>(</span><span>τ</span><span>,</span><span></span><span>4</span><span>θ</span><span>)</span></span></span></span>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>pl_simc_pi</span><span><span>(</span><span>pl_real </span><span>k</span><span>,</span><span> pl_real </span><span>tau</span><span>,</span><span> pl_real </span><span>theta</span><span>,</span><span> pl_real </span><span>tauc</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>struct</span><span> pl_gains </span><span>*</span><span><span>g</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>pl_real ti, lim;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>if</span><span> (tauc </span><span>&lt;</span><span> </span><span>0.0</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>tauc </span><span>=</span><span> theta;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>lim </span><span>=</span><span> </span><span>4.0</span><span> </span><span>*</span><span> (tauc </span><span>+</span><span> theta);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>g</span><span>-&gt;</span><span>kp</span><span> </span><span>=</span><span> tau </span><span>/</span><span> (k </span><span>*</span><span> (tauc </span><span>+</span><span> theta));</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>ti </span><span>=</span><span> tau </span><span>&lt;</span><span> lim </span><span>?</span><span> tau </span><span>:</span><span> lim;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>g</span><span>-&gt;</span><span>ki</span><span> </span><span>=</span><span> </span><span>g</span><span>-&gt;</span><span>kp</span><span> </span><span>/</span><span> ti;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>g</span><span>-&gt;</span><span>kd</span><span> </span><span>=</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>驱动是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch06.c" target="_blank"><code>c/apps/ch06.c</code></a>。<code>simulate</code> 里三种模式用一个 <code>enum mode</code> 和一个 <code>void *ctl</code> 分派，不用函数指针回调；闭环用的 <code>struct pl_pid</code> 和继电器 <code>struct pl_relay</code> 都放在调用方的栈上。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p>继电器是一个有状态的小类，<code>Update(e)</code> 一拍一次；<code>SimcPi</code> 的 <code>tauc</code> 用 <code>std::optional</code>。事后分析的函数是自由函数模板，输入是 <code>std::span</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>2</div></div><div><span>class</span><span> </span><span>Relay</span><span> {</span></div></div><div><div><div>3</div></div><div><span> </span><span>public:</span></div></div><div><div><div>4</div></div><div><span>  </span><span>Relay</span><span>(</span><span>T</span><span><span> </span><span>d</span><span>, </span></span><span>T</span><span><span> </span><span>eps</span><span>) : </span></span><span>d_</span><span>(</span><span>d</span><span>), </span><span>eps_</span><span>(</span><span>eps</span><span>) {}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>// One sample of the relay on the error e = r - y.</span></div></div><div><div><div>7</div></div><div><span>  </span><span>T</span><span> </span><span>Update</span><span>(</span><span>T</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span> (</span><span>state_</span><span> </span><span>&gt;</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>e</span><span> </span><span>&lt;</span><span> </span><span>-</span><span>eps_</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>      </span><span>state_</span><span> </span><span>=</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>state_</span><span> </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>e</span><span> </span><span>&gt;</span><span> </span><span>eps_</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>      </span><span>state_</span><span> </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> </span><span>T</span><span>(</span><span>state_</span><span>) </span><span>*</span><span> </span><span>d_</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span> </span><span>private:</span></div></div><div><div><div>17</div></div><div><span>  </span><span>T</span><span> </span><span>d_</span><span>, </span><span>eps_</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>int</span><span> </span><span>state_</span><span> </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>19</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Skogestad's SIMC PI: Kp = tau / (k (tauc + theta)),</span></div></div><div><div><div>2</div></div><div><span>// Ti = min(tau, 4 (tauc + theta)). tauc defaults to theta.</span></div></div><div><div><div>3</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span>Gains</span><span>&lt;</span><span>T</span><span>&gt; </span><span>SimcPi</span><span>(</span><span>T</span><span><span> </span><span>k</span><span>, </span></span><span>T</span><span><span> </span><span>tau</span><span>, </span></span><span>T</span><span><span> </span><span>theta</span><span>, std::</span></span><span>optional</span><span>&lt;</span><span>T</span><span><span>&gt; </span><span>tauc</span><span> </span></span><span>=</span><span> std::</span><span>nullopt</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>tc</span><span> </span><span>=</span><span> </span><span>tauc</span><span>.</span><span>value_or</span><span>(</span><span>theta</span><span>);</span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>lim</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>4</span><span>) </span><span>*</span><span> (</span><span>tc</span><span> </span><span>+</span><span> </span><span>theta</span><span>);</span></div></div><div><div><div>7</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>kp</span><span> </span><span>=</span><span> </span><span>tau</span><span> </span><span>/</span><span> (</span><span>k</span><span> </span><span>*</span><span> (</span><span>tc</span><span> </span><span>+</span><span> </span><span>theta</span><span>));</span></div></div><div><div><div>8</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>ti</span><span> </span><span>=</span><span> </span><span>tau</span><span> </span><span>&lt;</span><span> </span><span>lim</span><span> </span><span>?</span><span> </span><span>tau</span><span> </span><span>:</span><span> </span><span>lim</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>return</span><span> {</span><span>kp</span><span>, </span><span>kp</span><span> </span><span>/</span><span> </span><span>ti</span><span>, </span><span>T</span><span>(</span><span>0</span><span>)};</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>驱动是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch06.cc" target="_blank"><code>cpp/apps/ch06.cc</code></a>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><p>Rust 版的上升沿窗口用 <code>copy_within(1.., 0)</code> 移位；峰峰值用 <code>fold</code> 而不是 <code>iter().min()</code>，因为 <code>f64</code> 没有全序，不能直接 <code>min</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>relay_analyse</span><span>(</span></div></div><div><div><div>2</div></div><div><span>    </span><span>r</span><span>: &amp;[</span><span>Real</span><span>],</span></div></div><div><div><div>3</div></div><div><span>    </span><span>y</span><span>: &amp;[</span><span>Real</span><span>],</span></div></div><div><div><div>4</div></div><div><span>    </span><span>u</span><span>: &amp;[</span><span>Real</span><span>],</span></div></div><div><div><div>5</div></div><div><span>    </span><span>ts</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>eps</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>d</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>8</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>RelayResult</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>idx</span><span><span> </span><span>=</span><span> [</span></span><span>0</span><span>usize</span><span>; </span><span>4</span><span>];</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>tm</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>4</span><span>];</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>count</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>1</span><span>..</span><span>u</span><span>.</span><span>len</span><span>() {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> </span><span>!</span><span>(</span></span><span>u</span><span>[</span><span>k</span><span><span>] </span><span>&gt;</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>u</span><span>[</span><span>k</span><span> - </span><span>1</span><span><span>] </span><span>&lt;=</span><span> </span></span><span>0.0</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>continue</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>e0</span><span><span> </span><span>=</span><span> </span></span><span>r</span><span>[</span><span>k</span><span> - </span><span>1</span><span>] - </span><span>y</span><span>[</span><span>k</span><span> - </span><span>1</span><span>];</span></div></div><div><div><div>17</div></div><div><span>        </span><span>let</span><span> </span><span>e1</span><span><span> </span><span>=</span><span> </span></span><span>r</span><span>[</span><span>k</span><span>] - </span><span>y</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>18</div></div><div><span>        </span><span>idx</span><span>.</span><span>copy_within</span><span>(</span><span>1</span><span>.., </span><span>0</span><span>);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>tm</span><span>.</span><span>copy_within</span><span>(</span><span>1</span><span>.., </span><span>0</span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>idx</span><span>[</span><span>3</span><span><span>] </span><span>=</span><span> </span></span><span>k</span><span>;</span></div></div><div><div><div>21</div></div><div><span>        </span><span>tm</span><span>[</span><span>3</span><span><span>] </span><span>=</span><span> ((</span></span><span>k</span><span> - </span><span>1</span><span>) </span><span>as</span><span> </span><span>Real</span><span> + (</span><span>eps</span><span> - </span><span>e0</span><span>) / (</span><span>e1</span><span> - </span><span>e0</span><span>)) * </span><span>ts</span><span>;</span></div></div><div><div><div>22</div></div><div><span>        </span><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>    </span><span>if</span><span> </span><span>count</span><span><span> </span><span>&lt;</span><span> </span></span><span>4</span><span> {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>    </span><span>let</span><span> </span><span>seg</span><span><span> </span><span>=</span><span> &amp;</span></span><span>y</span><span>[</span><span>idx</span><span>[</span><span>2</span><span>]..</span><span>idx</span><span>[</span><span>3</span><span>]];</span></div></div><div><div><div>28</div></div><div><span>    </span><span>let</span><span> </span><span>lo</span><span><span> </span><span>=</span><span> </span></span><span>seg</span><span>.</span><span>iter</span><span>().</span><span>fold</span><span>(</span><span>seg</span><span>[</span><span>0</span><span><span>], </span><span>|</span></span><span>m</span><span>, &amp;</span><span>v</span><span><span>|</span><span> </span></span><span>if</span><span> </span><span>v</span><span><span> </span><span>&lt;</span><span> </span></span><span>m</span><span> { </span><span>v</span><span> } </span><span>else</span><span> { </span><span>m</span><span> });</span></div></div><div><div><div>29</div></div><div><span>    </span><span>let</span><span> </span><span>hi</span><span><span> </span><span>=</span><span> </span></span><span>seg</span><span>.</span><span>iter</span><span>().</span><span>fold</span><span>(</span><span>seg</span><span>[</span><span>0</span><span><span>], </span><span>|</span></span><span>m</span><span>, &amp;</span><span>v</span><span><span>|</span><span> </span></span><span>if</span><span> </span><span>v</span><span><span> </span><span>&gt;</span><span> </span></span><span>m</span><span> { </span><span>v</span><span> } </span><span>else</span><span> { </span><span>m</span><span> });</span></div></div><div><div><div>30</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> (</span></span><span>hi</span><span> - </span><span>lo</span><span>) / </span><span>2.0</span><span>;</span></div></div><div><div><div>31</div></div><div><span>    </span><span>// ...</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>simc_pi</code> 的 <code>tauc</code> 是 <code>Option&lt;Real&gt;</code>，<code>None</code> 表示取 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>，其余和 C++ 版一一对应。驱动是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch06.rs" target="_blank"><code>rust/src/bin/ch06.rs</code></a>。</p><p>编译参数和前几篇一样：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code> 加同样的警告，Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>（0.1.95）。风格检查：C 用内核的 <code>checkpatch.pl</code>（<code>--no-tree</code>，忽略 SPDX 和 typedef 两项），0 errors、0 warnings；C++ 用 clang-format 21.1.8 的 Google 风格；Python 用 <code>ruff format --check</code> 和 <code>ruff check</code>，行宽 79；Rust 用 <code>cargo fmt --check</code>。四项都在提交前过了。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch06</code> 一次跑完。每种语言 14 个文件，42 个对比文件；下面的输出把中间的 <code>ok</code> 行裁掉了，只留头尾，其余的 <code>max diff</code> 都是 0 或 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级（最大的一个是 <code>tuning_flywheel_rs.csv</code> 的 <span><span>2.2×10−162.2\times10^{-16}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch06</span></div></div><div><div><div>2</div></div><div><span>ch06: C wrote 2 plants to out/ch06</span></div></div><div><div><div>3</div></div><div><span>ch06: C++ wrote 2 plants to out/ch06</span></div></div><div><div><div>4</div></div><div><span>ch06: Rust wrote 2 plants to out/ch06</span></div></div><div><div><div>5</div></div><div><span>ok   eval_benchmark4_simc_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   eval_benchmark4_simc_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   eval_benchmark4_simc_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>... （中间 33 行 ok 省略）</span></div></div><div><div><div>9</div></div><div><span>ok   tuning_benchmark4_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   tuning_benchmark4_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   tuning_benchmark4_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>ok   tuning_flywheel_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>13</div></div><div><span>ok   tuning_flywheel_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>14</div></div><div><span>ok   tuning_flywheel_rs.csv: max diff 2.220e-16 &lt;= 1.0e-09</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>--- flywheel ---</span></div></div><div><div><div>17</div></div><div><span>step fit  k = 78.11 (Kt/b = 78.11)  tau = 2.0008 s (J/b = 2.0000)  theta = 1.39 ms (delay + current lag = 3.00 ms)</span></div></div><div><div><div>18</div></div><div><span>relay     Ku = 4.805  Pu = 31.63 ms  a = 2.650</span></div></div><div><div><div>19</div></div><div><span>true      Ku = 12.872  Pu = 0.01374 s (freq module)</span></div></div><div><div><div>20</div></div><div><span>harmonic balance: a = |G(jw)| 4d/pi = 2.4552 (measured 2.6496); arg G(jw) = -129.5 deg; -180 + asin(eps/a) = -131.0</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>rule       Kp        Ti        Td   IAE_sp   IAE_ld  over %      Ms     PM</span></div></div><div><div><div>22</div></div><div><span><span>     </span></span><span>zn_step   22.104   0.00278   0.00070  62.7667   0.5093    0.46   1.304  -48.7</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>zn_ultimate    2.883   0.01581   0.00395  62.7824   0.0376    0.78   1.633   61.7</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>simc    9.210   0.01113   0.00000  62.7615   0.0307    0.30  12.405    6.2</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>--- benchmark4 ---</span></div></div><div><div><div>27</div></div><div><span>step fit  k = 1.0000  tau = 2.497 s  theta = 1.855 s</span></div></div><div><div><div>28</div></div><div><span>relay     Ku = 3.913  Pu = 6.340 s  a = 0.3254</span></div></div><div><div><div>29</div></div><div><span>true      Ku = 3.980  Pu = 6.29887 s (freq module)</span></div></div><div><div><div>30</div></div><div><span>harmonic balance: a = |G(jw)| 4d/pi = 0.3241 (measured 0.3254); arg G(jw) = -179.3 deg; -180 + asin(eps/a) = -180.0</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>rule       Kp        Ti        Td   IAE_sp   IAE_ld  over %      Ms     PM</span></div></div><div><div><div>32</div></div><div><span><span>     </span></span><span>zn_step    1.616   3.70909   0.92727   3.0187   2.2958   11.26   1.626   66.7</span></div></div><div><div><div>33</div></div><div><span><span> </span></span><span>zn_ultimate    2.348   3.17000   0.79250   3.3231   1.3772   36.16   2.166   41.3</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>simc    0.673   2.49677   0.00000   4.1331   3.7464    6.80   1.628   62.0</span></div></div><div><div><div>35</div></div><div><span>IAE_sp spread over the three rules: 0.033 % (a 20 A clamp sets the ramp, not the gains)</span></div></div><div><div><div>36</div></div><div><span>zn_step: PM = -48.7 deg, |u| at the 20 A clamp in 41 % of the samples after the load step</span></div></div><div><div><div>37</div></div><div><span>SIMC on the same fit, tuned with different thetas:</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>theta =  1.39 ms (     fitted): Kp =  9.210  Ms = 12.40</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>theta =  3.00 ms (delay + lag): Kp =  4.269  Ms = 1.85</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>theta =  3.50 ms ( + half ZOH): Kp =  3.659  Ms = 1.66</span></div></div><div><div><div>41</div></div><div><span>lowest IAE_sp: zn_step; lowest IAE_ld: zn_ultimate; lowest overshoot: simc; lowest Ms: zn_step</span></div></div><div><div><div>42</div></div><div><span>SIMC detuned to tauc = 2 theta: Kp = 0.449, Ms = 1.373 (tauc = theta: 1.628)</span></div></div><div><div><div>43</div></div><div><span>figure: figures/ch06/relay-oscillation.svg</span></div></div><div><div><div>44</div></div><div><span>figure: figures/ch06/fopdt-fit.svg</span></div></div><div><div><div>45</div></div><div><span>figure: figures/ch06/tuning-compare-flywheel.svg</span></div></div><div><div><div>46</div></div><div><span>figure: figures/ch06/tuning-compare-benchmark4.svg</span></div></div><div><div><div>47</div></div><div><span>figure: figures/ch06/ms-vs-iae.svg</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>ch06 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>阶跃拟合<a href="#阶跃拟合"><span>#</span></a></h3><p>先看拟合。摩擦轮：</p><p></p><figure><img src="/assets/images/pid-control/ch06-fopdt-fit.svg" alt="两点法拟合：摩擦轮的整条阶跃响应几乎和一阶加延迟曲线重合，放大前 20 ms 能看到拟合的延迟比真实响应起步早；四阶对象的拟合在两个取样点之间贴合，两头不贴" /><figcaption>两点法拟合：摩擦轮的整条阶跃响应几乎和一阶加延迟曲线重合，放大前 20 ms 能看到拟合的延迟比真实响应起步早；四阶对象的拟合在两个取样点之间贴合，两头不贴</figcaption></figure><p></p><p>摩擦轮的 <span><span>k=78.11k = 78.11</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>78.11</span></span></span></span>，和 <span><span>Kt/b=78.11K_t/b = 78.11</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>78.11</span></span></span></span> 一致；<span><span>τ=2.0008\tau = 2.0008</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>2.0008</span></span></span></span> s，和 <span><span>J/b=2.0000J/b = 2.0000</span><span><span><span></span><span>J</span><span>/</span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>2.0000</span></span></span></span> 一致（<code>report</code> 里断言相对误差小于 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>）。<strong>延迟对不上：拟合给出 <span><span>θ=1.39\theta = 1.39</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.39</span></span></span></span> ms，而 2 拍传输延迟加 1 ms 电流环是 3.00 ms。</strong> 图下半是放大 20 ms 的视图：真实响应（蓝）在 2 ms 之前几乎不动，然后是缓慢的抛物线起步（电流环的二阶效应），而拟合（橙）在 1.39 ms 就开始直线上升。差的 1.61 ms 和前面估计的取整误差 1.7 ms 是同一个量级，所以我猜这就是主要原因，没有进一步分解（我没有试过用更精确的 0.2833 和 0.6321 重新算，看是不是整整消掉）。</p><p>四阶对象：<span><span>k=1.0000k = 1.0000</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>1.0000</span></span></span></span>，<span><span>τ=2.497\tau = 2.497</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>2.497</span></span></span></span> s，<span><span>θ=1.855\theta = 1.855</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.855</span></span></span></span> s（图右）。真实响应是一条 S 形曲线，FOPDT 在 <span><span>t28t_{28}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>28</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>t63t_{63}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>63</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 两个点之间贴得很好，但头部（拟合有一个硬的延迟，真实曲线是缓慢起步）和尾部（拟合比真实响应慢）都不贴。这是模型本身的限制，不是拟合方法的毛病：四个重合极点用一个极点加延迟代替，必然损失。</p></section><section><h3>继电器<a href="#继电器"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch06-relay-oscillation.svg" alt="继电器实验：上排是测量输出，下排是继电器输出；摩擦轮（左）的输出有噪声，幅值 2.65，周期 31.6 ms；四阶对象（右）的输出是干净的正弦，幅值 0.325，周期 6.34 s，继电器输出是方波" /><figcaption>继电器实验：上排是测量输出，下排是继电器输出；摩擦轮（左）的输出有噪声，幅值 2.65，周期 31.6 ms；四阶对象（右）的输出是干净的正弦，幅值 0.325，周期 6.34 s，继电器输出是方波</figcaption></figure><p></p><p>四阶对象（右）：继电器 <span><span>d=1d = 1</span><span><span><span></span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，没有滞环，没有噪声。输出是干净的正弦，<span><span>a=0.3254a = 0.3254</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3254</span></span></span></span>，<span><span>Pu=6.340P_u = 6.340</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>6.340</span></span></span></span> s，所以 <span><span>Ku=4⋅1/(π⋅0.3254)=3.913K_u = 4\cdot1/(\pi\cdot0.3254) = 3.913</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4</span><span></span><span>⋅</span><span></span></span><span><span></span><span>1/</span><span>(</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.3254</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>3.913</span></span></span></span>。<code>freq</code> 模块在真实对象上算出的真值是 <span><span>Ku=3.980K_u = 3.980</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3.980</span></span></span></span>、<span><span>Pu=6.29887P_u = 6.29887</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>6.29887</span></span></span></span> s。<strong>继电器读的 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 比真值低 1.7%，<span><span>PuP_u</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 高 0.65%。</strong> 谐波平衡自己也能检验：在测得的 <span><span>ω=2π/Pu\omega = 2\pi/P_u</span><span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>π</span><span>/</span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 处算 <span><span>∣G(jω)∣⋅4d/π=0.3241|G(j\omega)|\cdot4d/\pi = 0.3241</span><span><span><span></span><span>∣</span><span>G</span><span>(</span><span>j</span><span>ω</span><span>)</span><span>∣</span><span></span><span>⋅</span><span></span></span><span><span></span><span>4</span><span>d</span><span>/</span><span>π</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3241</span></span></span></span>，实测 <span><span>a=0.3254a = 0.3254</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3254</span></span></span></span>，差 0.4%；<span><span>arg⁡G(jω)=−179.3°\arg G(j\omega) = -179.3°</span><span><span><span></span><span>ar<span>g</span></span><span></span><span>G</span><span>(</span><span>j</span><span>ω</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>179.3°</span></span></span></span>，几乎正好在 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 上。这点偏差来自描述函数只取基波，三次谐波还是有一点点。（<code>report</code> 里这三条都是断言：<span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 差小于 5%，<span><span>PuP_u</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 小于 2%，<span><span>aa</span><span><span><span></span><span>a</span></span></span></span> 小于 2%。）</p><p>摩擦轮（左）：<span><span>d=10d = 10</span><span><span><span></span><span>d</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> A，<span><span>ϵ=2\epsilon = 2</span><span><span><span></span><span>ϵ</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> rad/s，测量噪声 <span><span>σ=0.5\sigma = 0.5</span><span><span><span></span><span>σ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span></span></span></span> rad/s。测到 <span><span>a=2.650a = 2.650</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>2.650</span></span></span></span>，<span><span>Pu=31.63P_u = 31.63</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>31.63</span></span></span></span> ms，所以 <span><span>Ku=4.805K_u = 4.805</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.805</span></span></span></span>。<strong>真值是 <span><span>Ku=12.872K_u = 12.872</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12.872</span></span></span></span>，<span><span>Pu=13.74P_u = 13.74</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>13.74</span></span></span></span> ms：继电器读出的 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 小了 2.7 倍，周期长了 2.3 倍。</strong> 这不是噪声的错，是前面推出的滞环效应：在测得的 <span><span>ω\omega</span><span><span><span></span><span>ω</span></span></span></span> 处，<span><span>∣G∣⋅4d/π=2.4552|G|\cdot4d/\pi = 2.4552</span><span><span><span></span><span>∣</span><span>G</span><span>∣</span><span></span><span>⋅</span><span></span></span><span><span></span><span>4</span><span>d</span><span>/</span><span>π</span><span></span><span>=</span><span></span></span><span><span></span><span>2.4552</span></span></span></span>（实测 2.6496，噪声把峰峰值撑大了一点，这是猜测），<span><span>arg⁡G=−129.5°\arg G = -129.5°</span><span><span><span></span><span>ar<span>g</span></span><span></span><span>G</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>129.5°</span></span></span></span>，而公式 <span><span>−180°+arcsin⁡(ϵ/a)=−131.0°-180° + \arcsin(\epsilon/a) = -131.0°</span><span><span><span></span><span>−</span><span>180°</span><span></span><span>+</span><span></span></span><span><span></span><span>arcsin</span><span>(</span><span>ϵ</span><span>/</span><span>a</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>131.0°</span></span></span></span>。差 1.5°。<strong>继电器测的是 <span><span>−131°-131°</span><span><span><span></span><span>−</span><span>131°</span></span></span></span> 那个点，不是 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 那个点。</strong> <span><span>ϵ/a=2/2.65\epsilon/a = 2/2.65</span><span><span><span></span><span>ϵ</span><span>/</span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>2/2.65</span></span></span></span> 不小，这个效应就大。滞环是为了不被噪声反复触发才加的，2 rad/s 的滞环是 0.5 rad/s 噪声的 4 倍，是我选的，没有找最小可行值。</p></section><section><h3>摩擦轮上的三套规则<a href="#摩擦轮上的三套规则"><span>#</span></a></h3><p><code>tuning_flywheel_*.csv</code> 的结果，<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 PM 是在真实对象上算的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>rule       Kp        Ti        Td   IAE_sp   IAE_ld  over %      Ms     PM</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>zn_step   22.104   0.00278   0.00070  62.7667   0.5093    0.46   1.304  -48.7</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>zn_ultimate    2.883   0.01581   0.00395  62.7824   0.0376    0.78   1.633   61.7</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>simc    9.210   0.01113   0.00000  62.7615   0.0307    0.30  12.405    6.2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch06-tuning-compare-flywheel.svg" alt="摩擦轮的三套整定在设定值阶跃和负载阶跃后的响应：阶跃法的电流在 ±20 A 之间持续来回切换，输出围绕 300 持续振荡；继电器法的输出在负载阶跃后 0.1 s 内回稳；SIMC 衰减振荡约 0.15 s" /><figcaption>摩擦轮的三套整定在设定值阶跃和负载阶跃后的响应：阶跃法的电流在 ±20 A 之间持续来回切换，输出围绕 300 持续振荡；继电器法的输出在负载阶跃后 0.1 s 内回稳；SIMC 衰减振荡约 0.15 s</figcaption></figure><p></p><p>三件事。<strong>第一，<code>IAE_sp</code> 完全没有区别：62.7667、62.7824、62.7615，三者相差 0.033%。</strong> 设定值是 300 rad/s 的阶跃，电流上限 20 A，电机以固定的加速度爬坡，整条上升过程都被限幅占着，增益选什么对 IAE 没有影响（图左上三条线重合）。这个指标在这个对象上没有判别力，我把它留在表里是为了让你看到它。</p><p><strong>第二，阶跃法给的增益让线性回路不稳定。</strong> PM = <span><span>−48.7°-48.7°</span><span><span><span></span><span>−</span><span>48.7°</span></span></span></span>。因为拟合的 <span><span>θ=1.39\theta = 1.39</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.39</span></span></span></span> ms 比真实的等效延迟 3.00 ms 小一半多，<span><span>kp=1.2τ/(kθ)k_p = 1.2\tau/(k\theta)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.2</span><span>τ</span><span>/</span><span>(</span><span>k</span><span>θ</span><span>)</span></span></span></span> 里的 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 小了，<span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 大了：22.104。图里蓝线在负载阶跃之后一直在 <span><span>±20\pm 20</span><span><span><span></span><span>±</span><span>20</span></span></span></span> A 之间切换（限幅限住了发散，变成极限环），<code>report</code> 数出来负载阶跃之后 41% 的采样点上 <span><span>∣u∣|u|</span><span><span><span></span><span>∣</span><span>u</span><span>∣</span></span></span></span> 贴着 20 A。它的 <span><span>Ms=1.304M_s = 1.304</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.304</span></span></span></span> 看起来很漂亮，但是前面说过，回路不稳定的时候 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 不是距离，所以在 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 对 IAE 的图里这个点我画成空心。<code>IAE_ld</code> 是 0.5093，比另两个大 13 到 17 倍。</p><p><strong>第三，SIMC 的负载 IAE 最低（0.0307），但 <span><span>Ms=12.4M_s = 12.4</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12.4</span></span></span></span>，PM 只有 <span><span>6.2°6.2°</span><span><span><span></span><span>6.2°</span></span></span></span>。</strong> 同一个 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 的错误在这里表现成另一种形式：SIMC 的 <span><span>Kp=τ/(2kθ)K_p = \tau/(2k\theta)</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span>/</span><span>(</span><span>2</span><span>k</span><span>θ</span><span>)</span></span></span></span> 同样对 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 很敏感。我用同一个拟合的 <span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span>、<span><span>kk</span><span><span><span></span><span>k</span></span></span></span>，只换 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 再算一遍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SIMC on the same fit, tuned with different thetas:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>theta =  1.39 ms (     fitted): Kp =  9.210  Ms = 12.40</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>theta =  3.00 ms (delay + lag): Kp =  4.269  Ms = 1.85</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>theta =  3.50 ms ( + half ZOH): Kp =  3.659  Ms = 1.66</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用真实的 3.00 ms，<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 12.40 降到 1.85；再加上第 4 篇的半拍 ZOH（0.5 ms，正是 <a href="https://skoge.folk.ntnu.no/publications/2003/tuningPID/finalpaper.pdf" target="_blank">Skogestad 的 <span><span>h/2h/2</span><span><span><span></span><span>h</span><span>/2</span></span></span></span> 规则</a>），<span><span>Ms=1.66M_s = 1.66</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.66</span></span></span></span>，落进这个规则设计的 1.6 到 1.7 的范围。SIMC 没有错，是我喂给它的 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 错了。（<code>report</code> 断言了 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>(1.39 ms) &gt; 5 且 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>(3.5 ms) &lt; 2.5。）</p><p><strong>真正“胜出”的是继电器的临界法：<span><span>kp=2.883k_p = 2.883</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.883</span></span></span></span>，PM <span><span>61.7°61.7°</span><span><span><span></span><span>61.7°</span></span></span></span>，<span><span>Ms=1.633M_s = 1.633</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.633</span></span></span></span>，<code>IAE_ld</code> = 0.0376，比 SIMC 差 22%。</strong> 但这有点讽刺：前面看到继电器读的 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 小了 2.7 倍，所以 <span><span>0.6Ku0.6K_u</span><span><span><span></span><span>0.6</span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 其实只是真值的 0.22 倍（2.883 / 12.872），一个意外地保守的增益，保守恰好救了它。我没有一个办法声称这是“继电器法更好”，它是“滞环给的误差碰巧落在安全的一边”。如果去掉 <span><span>ϵ\epsilon</span><span><span><span></span><span>ϵ</span></span></span></span> 让它读到 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 的点，<span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 会是 <span><span>0.6×12.87=7.70.6\times12.87 = 7.7</span><span><span><span></span><span>0.6</span><span></span><span>×</span><span></span></span><span><span></span><span>12.87</span><span></span><span>=</span><span></span></span><span><span></span><span>7.7</span></span></span></span>，而我没有跑那个实验。</p></section><section><h3>四阶对象上的三套规则<a href="#四阶对象上的三套规则"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>rule       Kp        Ti        Td   IAE_sp   IAE_ld  over %      Ms     PM</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>zn_step    1.616   3.70909   0.92727   3.0187   2.2958   11.26   1.626   66.7</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>zn_ultimate    2.348   3.17000   0.79250   3.3231   1.3772   36.16   2.166   41.3</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>simc    0.673   2.49677   0.00000   4.1331   3.7464    6.80   1.628   62.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch06-tuning-compare-benchmark4.svg" alt="四阶对象的三套整定：阶跃法超调 11%，继电器法超调 36% 但负载恢复最快，SIMC 超调最小但负载下沉最深（到 0.38）、恢复最慢" /><figcaption>四阶对象的三套整定：阶跃法超调 11%，继电器法超调 36% 但负载恢复最快，SIMC 超调最小但负载下沉最深（到 0.38）、恢复最慢</figcaption></figure><p></p><p>这个对象没有限幅，也没有拟合出错，所以三套规则分得开：</p><ul>
<li><strong>设定值：</strong> 阶跃法最快（<code>IAE_sp</code> 3.0187），继电器法次之（3.3231），SIMC 最慢（4.1331）。超调反过来，SIMC 最小（6.80%），阶跃法 11.26%，继电器法 36.16%，是 SIMC 的 5.3 倍。</li>
<li><strong>负载：</strong> 继电器法最好（<code>IAE_ld</code> 1.3772），阶跃法 2.2958，SIMC 3.7464，是继电器法的 2.7 倍。图里 SIMC 在负载阶跃之后下沉到 0.38，最深。</li>
<li><strong>鲁棒性：</strong> 阶跃法和 SIMC 几乎一样（<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 1.626 和 1.628），继电器法最差（2.166）。</li>
</ul><p><code>report</code> 里这四句话是断言：<code>IAE_sp</code> 最低的是阶跃法，<code>IAE_ld</code> 最低的是继电器法，超调最小的是 SIMC，<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 最大的是继电器法。</p><p><strong>我没想到的一点：阶跃法在四阶对象上赢了 SIMC。</strong> 同样的 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（1.626 对 1.628），阶跃法的两个 IAE 都更低（3.02 对 4.13，2.30 对 3.75），只是超调大了 4.5 个百分点。要说公平，阶跃法是 PID，SIMC 这里只是 PI：Td 是 0.927 对 0。我没有跑 SIMC-PID（Skogestad 的论文里对二阶模型有 PID 版本，我这一篇只实现了 PI）。所以这个“赢”只能说是“PID 对 PI”，不能说是规则的胜负。</p><p>SIMC 的可调之处：同一个拟合，<span><span>τc=2θ\tau_c = 2\theta</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>θ</span></span></span></span>（只改了一个参数），<span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 0.673 降到 0.449，<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 1.628 降到 1.373（<code>report</code> 断言它变小）。我没有评估它的 IAE，所以不知道这个更稳的版本贵多少。</p></section><section><h3>鲁棒性和性能<a href="#鲁棒性和性能"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch06-ms-vs-iae.svg" alt="灵敏度峰值对负载 IAE：摩擦轮（左，纵轴对数）阶跃法是空心点（线性不稳定），继电器法在左下角，SIMC 在右下角 M_s = 12.4；四阶对象（右）继电器法 IAE 最低但 M_s 最大，阶跃法和 SIMC M_s 相同，阶跃法 IAE 更低" /><figcaption>灵敏度峰值对负载 IAE：摩擦轮（左，纵轴对数）阶跃法是空心点（线性不稳定），继电器法在左下角，SIMC 在右下角 M_s = 12.4；四阶对象（右）继电器法 IAE 最低但 M_s 最大，阶跃法和 SIMC M_s 相同，阶跃法 IAE 更低</figcaption></figure><p></p><p>把每套规则画成一个点，横轴是 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，纵轴是负载 IAE，越靠左下越好。四阶对象（右图）呈现的是教科书里的权衡：继电器法在最下面（IAE 1.38），也在最右边（<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> = 2.17）；另外两个点在 <span><span>Ms≈1.63M_s \approx 1.63</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1.63</span></span></span></span> 的同一列，阶跃法（2.30）在 SIMC（3.75）下面。所以继电器法拿性能换了鲁棒性，而阶跃法在同样的 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 上压住了 SIMC。</p><p>摩擦轮（左图，纵轴是对数）呈现的则是另一回事：权衡被拟合误差盖住了。左下角的点（继电器法）是被滞环意外保护起来的，右下角的点（SIMC）是被错误的 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 害的，空心点（阶跃法）是不稳定的。<strong>这张图的信息不是哪条规则好，而是：在这个对象上，规则之间的差别小于模型误差带来的差别。</strong></p></section><section><h3>我没有测的东西<a href="#我没有测的东西"><span>#</span></a></h3><p>有几件事我没有去证实，列在这里，免得读者以为它们是结论：</p><ul>
<li>摩擦轮上 FOPDT 拟合的延迟偏小，我估计的取整误差（1.7 ms）和实际差的 1.61 ms 同量级，但没有换精确常数重新拟合来证实。</li>
<li>继电器在摩擦轮上实测的幅值 <span><span>aa</span><span><span><span></span><span>a</span></span></span></span> 比描述函数预测的大 8%，我猜是噪声，没有分解；也没有去掉滞环重做一遍，看 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 能不能回到 12.87 附近。</li>
<li>阶跃法对摩擦轮的 <span><span>Ms=1.304M_s = 1.304</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.304</span></span></span></span> 在回路不稳定时没有意义，它和不稳定同时出现，只是因为我用的函数不检查稳定性。</li>
<li>四阶对象上我把 PID（两套 Ziegler–Nichols）和 PI（SIMC）放在一起比，所以那里的结论是“PID 对 PI”，不是规则之间的胜负。</li>
<li>我没有测 Ziegler–Nichols 说的“四分之一衰减比”，它是我凭记忆写的目标，不是这一篇的实验结果。</li>
<li>继电器的滞环和噪声是我选的，没有扫描。四阶对象上没有滞环，所以那里的 1.7% 误差是“理想”情形。</li>
</ul></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在仓库根目录、<code>nix develop</code> 里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch06</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出就是上面“结果”开头那一段（中间的 <code>ok</code> 行我裁掉了）：每种语言 14 个文件，共 42 个对比文件全部 <code>ok</code>，容差 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，最大差异 <span><span>2.2×10−162.2\times10^{-16}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>；末尾是 <code>ch06 OK</code>，表示 <code>report</code> 里的所有断言都过了。<code>out/ch06/</code> 下有 56 个 CSV，<code>figures/ch06/</code> 下有 5 张图（这两个目录和 <code>data/</code> 一样不进 git，是 <code>make</code> 生成的）。</p><p><strong>这一篇新增的文件：</strong></p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/tuning.py" target="_blank"><code>python/pidlab/tuning.py</code></a></td><td><code>make -s ch06</code></td><td><code>fopdt_fit</code>、<code>Relay</code>、<code>relay_analyse</code>、<code>zn_step</code>、<code>zn_ultimate</code>、<code>simc_pi</code>；Python 参考，其余三种语言与它的最大差异 <span><span>2.2×10−162.2\times10^{-16}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a></td><td><code>make -s ch06</code></td><td>新增 <code>Flywheel</code>、<code>Benchmark4</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/tuning.h" target="_blank"><code>c/include/pl/tuning.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/tuning.c" target="_blank"><code>c/src/tuning.c</code></a></td><td><code>make -s ch06</code></td><td>C 版整定模块，<code>eval_*_c.csv</code> 等 <code>ok</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/plants.h" target="_blank"><code>c/include/pl/plants.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/plants.c" target="_blank"><code>c/src/plants.c</code></a></td><td><code>make -s ch06</code></td><td>新增 <code>pl_flywheel</code>、<code>pl_benchmark4</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/tuning.h" target="_blank"><code>cpp/include/pidlab/tuning.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/plants.h" target="_blank"><code>cpp/include/pidlab/plants.h</code></a></td><td><code>make -s ch06</code></td><td>C++ 版整定模块和 <code>Flywheel&lt;T&gt;</code>、<code>Benchmark4&lt;T&gt;</code>，<code>*_cpp.csv</code> 全部 <code>ok</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/tuning.rs" target="_blank"><code>rust/src/tuning.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/plants.rs" target="_blank"><code>rust/src/plants.rs</code></a></td><td><code>make -s ch06</code></td><td>Rust 版整定模块和 <code>Flywheel</code>、<code>Benchmark4</code>，<code>*_rs.csv</code> 全部 <code>ok</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch06_tuning.py" target="_blank"><code>python/ch06_tuning.py</code></a></td><td><code>python3 python/ch06_tuning.py ref</code>，<code>python3 python/ch06_tuning.py report</code></td><td><code>ref</code> 写数据和 Python 参考输出；<code>report</code> 比对、断言、打印两张表、画 5 张图，结尾 <code>ch06 OK</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch06.c" target="_blank"><code>c/apps/ch06.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch06.cc" target="_blank"><code>cpp/apps/ch06.cc</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch06.rs" target="_blank"><code>rust/src/bin/ch06.rs</code></a></td><td><code>make -s ch06</code></td><td>各写 14 个 CSV，分别打印 <code>ch06: C wrote 2 plants to out/ch06</code>、<code>C++</code>、<code>Rust</code></td></tr></tbody></table><p><strong>这一篇依赖、但没有改动的文件：</strong></p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid.py" target="_blank"><code>python/pidlab/pid.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid.c" target="_blank"><code>c/src/pid.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid.h" target="_blank"><code>cpp/include/pidlab/pid.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid.rs" target="_blank"><code>rust/src/pid.rs</code></a></td><td><code>make -s ch06</code></td><td>第 5 篇的 <code>Pid</code>，评估三套规则时用 <code>CLAMP</code> 和 <span><span>tf=Td/10t_f = T_d/10</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/10</span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/freq.py" target="_blank"><code>python/pidlab/freq.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/metrics.py" target="_blank"><code>python/pidlab/metrics.py</code></a></td><td><code>make -s ch06</code></td><td>第 3 篇的 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和第 2 篇的 <code>step_metrics</code>，<code>report</code> 用来算真值 <span><span>KuK_u</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、IAE</td></tr></tbody></table></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把整定拆成了函数：两点法在摩擦轮上拟合出 <span><span>k=78.11k = 78.11</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>78.11</span></span></span></span>、<span><span>τ=2.0008\tau = 2.0008</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span>2.0008</span></span></span></span> s，但把 3.00 ms 的延迟拟成了 1.39 ms；继电器在四阶对象上读出 <span><span>Ku=3.913K_u = 3.913</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3.913</span></span></span></span>，比真值 3.980 低 1.7%，在带 2 rad/s 滞环的摩擦轮上读出 4.805，真值是 12.872；三套规则里，阶跃法在摩擦轮上线性不稳定（PM <span><span>−48.7°-48.7°</span><span><span><span></span><span>−</span><span>48.7°</span></span></span></span>），SIMC 在同一个拟合上 <span><span>Ms=12.4M_s = 12.4</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>12.4</span></span></span></span>，换成 3.5 ms 的 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 之后降到 1.66；四阶对象上 SIMC 的超调 6.80%，继电器法 36.16%，但负载 IAE 是 3.7464 对 1.3772。四种语言 42 个对比文件最大差异是 <span><span>2.2×10−162.2\times10^{-16}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p><a href="/posts/pid-control-07-cascade/">第 7 篇：串级 PID：云台的角度环和速度环</a>会把这些单回路 PID 接成两层：云台的速度环（内）和角度环（外），内环抗底盘转动带来的扰动。整定用的还是这一篇的 SIMC 和第 2 篇的极点配置，评估用第 5 篇的 <code>Pid</code>，外环怎么知道内环饱和了，要靠 <code>hold_integral</code>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-05-practical/</id>
      <title type="text">工程上的坑：积分饱和、微分冲击与前馈</title>
      <published>2026-10-16T04:00:00.000Z</published>
      <updated>2026-10-16T04:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-05-practical/"/>
      <summary type="text">PID 控制系列第 5 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95 把四种抗饱和、设定值权重、滤波微分、前馈和无扰动切换收成一个 PID 库，写了四遍。40 rad/s 的阶跃上，不处理饱和的积分让超调到 77.4%，条件积分只有 2.2%；D 项直接看误差，一次 0.1 rad 的设定值阶跃就产生 10.43 N·m 的微分冲击，是 1 N·m 限幅的 10 倍；微分滤波时间常数从 0 加到 10 ms，编码器量化引起的力矩抖动小 13.7 倍；重力前馈把正弦跟踪的 rms 误差缩小 1.84 倍；切换增益时调用 reset，输出跳变从 0.162 N·m 降到 0.0267 N·m。</summary>
      <content type="html"><![CDATA[<p>前四篇我一直把 PID 当成一个公式：套上 <span><span>u=kpe+ki∫e+kde˙u = k_p e + k_i\int e + k_d\dot e</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>e</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span>，调几个数，看曲线。这一篇我把它放到一个“会出事”的场景里：电机电流上限 20 A，设定值一次跳到 40 rad/s，积分器在饱和期间照常累加，结果轮速冲到 70.96 rad/s，超调 77.4%；换成条件积分，同一个阶跃只有 2.2%。<strong>饱和、微分冲击、重力和增益切换，是 PID 从课本走到机器人上最先遇到的四个坑，它们各自都有一行修法，但每一行都会碰到别的东西。</strong> 这一篇我把四个坑逐个复现，再把修法收成系列从此以后一直用的 PID 库：<span><span>bb</span><span><span><span></span><span>b</span></span></span></span>、<span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 两个设定值权重，滤波微分，四种抗饱和模式（还有一个让外部掐掉积分的开关），前馈入口和 <code>reset</code>。还有一个我事先用公式算过、看到曲线还是愣了一下的结果：积分限幅是最常见的写法，它在这个负载下留下了 0.79 rad/s 的稳态误差。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch05</code> 通过，<code>out/ch05/</code> 下 60 个 CSV（15 个算例：<code>windup_{none,limit,clamp,backcalc}</code>、<code>kick_c1</code>、<code>kick_c0</code>、<code>weight_b05</code>、<code>dfilter_tf{0,0.002,0.01}</code>、<code>ff_{none,gravity,gravity_acc}</code>、<code>bumpless_{off,on}</code>，每个 Python、C、C++、Rust 各一份），<code>figures/ch05/</code> 下 5 张图；以及积分饱和、设定值权重、微分滤波、前馈、无扰动切换的推导。
新增模块：Python 是 <code>pidlab/pid.py</code>，另外在 <code>pidlab/plants.py</code> 里加了 <code>GimbalPitch</code> 和 <code>round_half_away</code>；C 是 <code>pl/pid.h</code> 和 <code>pid.c</code>（<code>plants.h</code>、<code>plants.c</code> 里加了 <code>pl_gimbal_pitch</code>）；C++ 是 <code>pidlab/pid.h</code>（<code>plants.h</code> 里加了 <code>GimbalPitch&lt;T&gt;</code>）；Rust 是 <code>pid.rs</code>（<code>plants.rs</code> 里加了 <code>GimbalPitch</code>）。<strong>这个 <code>pid</code> 模块的接口从这一篇起固定，第 6 到第 10 篇都直接用它。</strong>
前置：<a href="/posts/pid-control-04-discretization/">第 4 篇</a>。需要的数学：第 2 篇的一阶、二阶系统，第 4 篇的离散化；这一篇没有新的数学工具。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>积分饱和：电梯按钮按了 40 次<a href="#积分饱和电梯按钮按了-40-次"><span>#</span></a></h3><p>执行器有上限：M3508 的电流指令夹在 <span><span>±20\pm 20</span><span><span><span></span><span>±</span><span>20</span></span></span></span> A。控制器想要的输出记作 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span>，实际发出的记作 <span><span>u=sat(v)u = \text{sat}(v)</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>sat</span></span><span>(</span><span>v</span><span>)</span></span></span></span>。<strong>积分器不知道 <span><span>u≠vu \ne v</span><span><span><span></span><span>u</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>v</span></span></span></span></strong>：设定值从 0 跳到 40 rad/s，误差 40 rad/s，比例项 <span><span>kp×40=166.7k_p \times 40 = 166.7</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>40</span><span></span><span>=</span><span></span></span><span><span></span><span>166.7</span></span></span></span> A，电流被夹在 20 A，电机以固定的加速度爬升，这段时间里误差一直为正，积分器照常累加。轮速终于到了 40 rad/s，误差归零，积分器里却攒了几百安培，它要等误差为负足够久才能倒空。这跟反复按电梯按钮一样：电梯没有因为你按了 40 次就来得更快，只是它到了以后，你还得等那 40 次的“欠账”被清掉。</p><p>用 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 记积分项，<span><span>e=r−ye = r - y</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span>，<span><span>vk=pk+Ik+dk+ffkv_k = p_k + I_k + d_k + ff_k</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>f</span><span><span>f</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，我在 PID 库里放四种处理，对应枚举 <code>Windup</code>：</p><ul>
<li><strong>NONE：</strong> <span><span>Ik+1=Ik+kiTsekI_{k+1} = I_k + k_iT_se_k</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，什么都不做。这是对照组。</li>
<li><strong>LIMIT：</strong> 积分之后把 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 夹在 <span><span>±Ilim⁡\pm I_{\lim}</span><span><span><span></span><span>±</span><span><span>I</span><span><span><span><span><span><span></span><span><span><span><span>l</span><span>i</span><span>m</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里。最常见的写法，一行代码。</li>
<li><strong>CLAMP（条件积分）：</strong> 饱和且误差还在把输出往饱和方向推时，这一拍不积分。判据是 <span><span>u≠vu \ne v</span><span><span><span></span><span>u</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>v</span></span></span></span> 且 <span><span>e (v−u)&gt;0e\,(v - u) &gt; 0</span><span><span><span></span><span>e</span><span></span><span>(</span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>u</span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>：<span><span>v−uv - u</span><span><span><span></span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>u</span></span></span></span> 的符号说明饱和在哪一边，<span><span>ee</span><span><span><span></span><span>e</span></span></span></span> 同号就说明积分会让它更饱和。</li>
<li><strong>BACK_CALC（反算）：</strong> 把 <span><span>u−vu - v</span><span><span><span></span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span>v</span></span></span></span> 按跟踪时间常数 <span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 回灌给积分器：</li>
</ul><span><span><span>Ik+1=Ik+kiTsek+TsTt (uk−vk)I_{k+1} = I_k + k_iT_se_k + \frac{T_s}{T_t}\,(u_k - v_k)</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>(</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>连续形式是 <span><span>I˙=kie+(u−v)/Tt\dot I = k_ie + (u - v)/T_t</span><span><span><span></span><span><span><span><span><span><span></span><span>I</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span>v</span><span>)</span><span>/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。饱和时 <span><span>u−vu - v</span><span><span><span></span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span>v</span></span></span></span> 是一个负反馈，把 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 往“让 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 回到边界”的方向拉；平衡点满足 <span><span>v−u=Ttkiev - u = T_tk_ie</span><span><span><span></span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span></span></span></span>。<span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 越小拉得越紧。第 4 篇我推过限幅的增量式累加器就是 <span><span>Tt=TsT_t = T_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的反算，那个 <span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 现在是个参数。我取 <span><span>Tt=0.5 kp/kiT_t = 0.5\,k_p/k_i</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，也就是积分时间 <span><span>Ti=kp/kiT_i = k_p/k_i</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的一半（<span><span>kp=4.1667k_p = 4.1667</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.1667</span></span></span></span>、<span><span>ki=90k_i = 90</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>90</span></span></span></span>，所以 <span><span>Tt=0.0231T_t = 0.0231</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.0231</span></span></span></span> s）。这个 0.5 是我随手取的，没有扫描过。</p><p><strong>LIMIT 有一个不明显的陷阱。</strong> 设负载力矩 <span><span>τd\tau_d</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 稳态下需要的电流 <span><span>iss=(τd+by)/Kti_{ss} = (\tau_d + by)/K_t</span><span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>y</span><span>)</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 比 <span><span>Ilim⁡I_{\lim}</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span><span>l</span><span>i</span><span>m</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 大。积分器最多只能提供 <span><span>Ilim⁡I_{\lim}</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span><span>l</span><span>i</span><span>m</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，剩下的要比例项补：<span><span>kpe+Ilim⁡=issk_pe + I_{\lim} = i_{ss}</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span><span>l</span><span>i</span><span>m</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，而 <span><span>y=r−ey = r - e</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>e</span></span></span></span>，代入电机稳态方程 <span><span>Kti=τd+byK_ti = \tau_d + by</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>y</span></span></span></span> 得</p><span><span><span>ess=(τd+br)/Kt−Ilim⁡kp+b/Kte_{ss} = \frac{(\tau_d + br)/K_t - I_{\lim}}{k_p + b/K_t}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>b</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>b</span><span>r</span><span>)</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span><span>l</span><span>i</span><span>m</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><strong>积分器的“让误差归零”只在它自己没碰到限幅时成立。</strong> 我的算例里 <span><span>τd=2.4\tau_d = 2.4</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.4</span></span></span></span> N·m，<span><span>r=10r = 10</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> rad/s，<span><span>Ilim⁡=5I_{\lim} = 5</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span><span>l</span><span>i</span><span>m</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> A：分子 <span><span>(2.4+0.1)/0.3−5=3.33(2.4 + 0.1)/0.3 - 5 = 3.33</span><span><span><span></span><span>(</span><span>2.4</span><span></span><span>+</span><span></span></span><span><span></span><span>0.1</span><span>)</span><span>/0.3</span><span></span><span>−</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>3.33</span></span></span></span> A，分母 <span><span>4.1667+0.03334.1667 + 0.0333</span><span><span><span></span><span>4.1667</span><span></span><span>+</span><span></span></span><span><span></span><span>0.0333</span></span></span></span>，<span><span>ess=0.7937e_{ss} = 0.7937</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.7937</span></span></span></span> rad/s。<code>report</code> 会把这个公式和仿真的稳态误差比对。</p></section><section><h3>设定值权重和微分冲击<a href="#设定值权重和微分冲击"><span>#</span></a></h3><p>第 2 篇我用过 I-P 形式（比例只看 <span><span>−y-y</span><span><span><span></span><span>−</span><span>y</span></span></span></span>）。把它推广：PID 的比例项和微分项各自带一个设定值权重，</p><span><span><span>U(s)=kp (b R−Y)+kis (R−Y)+kdstfs+1 (c R−Y)U(s) = k_p\,(b\,R - Y) + \frac{k_i}{s}\,(R - Y) + \frac{k_d s}{t_fs + 1}\,(c\,R - Y)</span><span><span><span></span><span>U</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>(</span><span>b</span><span></span><span>R</span><span></span><span>−</span><span></span></span><span><span></span><span>Y</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>(</span><span>R</span><span></span><span>−</span><span></span></span><span><span></span><span>Y</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>(</span><span>c</span><span></span><span>R</span><span></span><span>−</span><span></span></span><span><span></span><span>Y</span><span>)</span></span></span></span></span><p><span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 是比例权重，<span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 是微分权重，积分项永远吃完整的误差 <span><span>R−YR - Y</span><span><span><span></span><span>R</span><span></span><span>−</span><span></span></span><span><span></span><span>Y</span></span></span></span>。这样设定值到输出的通路和扰动到输出的通路是分开的：反馈通路（<span><span>−Y-Y</span><span><span><span></span><span>−</span><span>Y</span></span></span></span> 那一项）不变，决定稳定性和扰动抑制；前馈通路（<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 那一项）由 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span>、<span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 决定，只影响设定值响应。<strong>稳态时 <span><span>e=0e = 0</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，两个权重不起作用，它们只塑造瞬态。</strong></p><p>微分冲击就藏在 <span><span>cc</span><span><span><span></span><span>c</span></span></span></span> 里。<span><span>c=1c = 1</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时微分项吃 <span><span>ee</span><span><span><span></span><span>e</span></span></span></span>，设定值一个阶跃 <span><span>Δr\Delta r</span><span><span><span></span><span>Δ</span><span>r</span></span></span></span> 进来，后向差分在那一拍看到 <span><span>ek−ek−1=Δre_k - e_{k-1} = \Delta r</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Δ</span><span>r</span></span></span></span>，滤波微分递推是 <span><span>dk=a dk−1+kdtf+Ts(ed,k−ed,k−1)d_k = a\,d_{k-1} + \dfrac{k_d}{t_f + T_s}(e_{d,k} - e_{d,k-1})</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>d</span><span>,</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>d</span><span>,</span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，阶跃那一拍 <span><span>dk−1=0d_{k-1} = 0</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、差分是 <span><span>Δr\Delta r</span><span><span><span></span><span>Δ</span><span>r</span></span></span></span>，所以</p><span><span><span>dk=kd Δrtf+Tsd_k = \frac{k_d\,\Delta r}{t_f + T_s}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>Δ</span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>我的云台俯仰 PID 是 <span><span>ωn=15\omega_n = 15</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span>、<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span> 放出来的（下面有增益），<span><span>kd=0.313k_d = 0.313</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.313</span></span></span></span>、<span><span>tf=2t_f = 2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> ms、<span><span>Δr=0.1\Delta r = 0.1</span><span><span><span></span><span>Δ</span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span> rad：<span><span>0.313×0.1/0.003=10.430.313 \times 0.1 / 0.003 = 10.43</span><span><span><span></span><span>0.313</span><span></span><span>×</span><span></span></span><span><span></span><span>0.1/0.003</span><span></span><span>=</span><span></span></span><span><span></span><span>10.43</span></span></span></span> N·m，下一拍乘 <span><span>a=tf/(tf+Ts)=0.667a = t_f/(t_f + T_s) = 0.667</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.667</span></span></span></span> 衰减。力矩限幅是 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> N·m。<span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 之后微分项只吃 <span><span>−y-y</span><span><span><span></span><span>−</span><span>y</span></span></span></span>，<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 是连续的，没有这一脚。</p><p><strong>启用时的第一拍也是同一类问题。</strong> 如果控制器在 <span><span>y≠0y \ne 0</span><span><span><span></span><span>y</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> 的时候第一次被调用，而 <code>prev_ed</code> 还是 0，<span><span>ek−ek−1e_k - e_{k-1}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就是一个假的阶跃。所以第一次 <code>update</code> 把 <code>prev_ed</code> 设成当前的 <span><span>ede_d</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（我把微分通路的输入叫 <span><span>ed=cr−ye_d = cr - y</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>cr</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span>），这是“primed”标志的作用，也是第 4 篇预告的那一脚的修法。</p></section><section><h3>微分滤波和编码器量化<a href="#微分滤波和编码器量化"><span>#</span></a></h3><p>位置传感器是量化的：GM6020 的编码器每圈 8192 个计数，<span><span>q=2π/8192=7.67×10−4q = 2\pi/8192 = 7.67\times 10^{-4}</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>π</span><span>/8192</span><span></span><span>=</span><span></span></span><span><span></span><span>7.67</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rad。角度每变一个台阶，微分项在那一拍就看到一个 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 的跳变。滤波微分的响应是 <span><span>kdq/(tf+Ts)k_dq/(t_f + T_s)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>q</span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>：</p>

<table><thead><tr><th><span><span>tft_f</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th>滤波极点 <span><span>a=tf/(tf+Ts)a = t_f/(t_f+T_s)</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></th><th>单个台阶造成的 D 项</th></tr></thead><tbody><tr><td>0</td><td>0</td><td><span><span>0.313×7.67×10−4/0.001=0.24010.313 \times 7.67\times 10^{-4}/0.001 = 0.2401</span><span><span><span></span><span>0.313</span><span></span><span>×</span><span></span></span><span><span></span><span>7.67</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span><span>/0.001</span><span></span><span>=</span><span></span></span><span><span></span><span>0.2401</span></span></span></span> N·m</td></tr><tr><td>2 ms</td><td>0.667</td><td><span><span>0.08000.0800</span><span><span><span></span><span>0.0800</span></span></span></span> N·m</td></tr><tr><td>10 ms</td><td>0.909</td><td><span><span>0.02180.0218</span><span><span><span></span><span>0.0218</span></span></span></span> N·m</td></tr></tbody></table><p>滤波是有代价的：<span><span>tf=10t_f = 10</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> ms 的极点在 <span><span>1/tf=1001/t_f = 100</span><span><span><span></span><span>1/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>100</span></span></span></span> rad/s，离 <span><span>ωn=15\omega_n = 15</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span> rad/s 不算远，相位多少会被它吃掉一点。我在这个回路里没看到代价（后面的表），但我猜带宽更高的回路上这笔账会显出来，这一点我没有测。</p></section><section><h3>前馈：不要让积分器去背重力<a href="#前馈不要让积分器去背重力"><span>#</span></a></h3><p>俯仰轴有重力矩 <span><span>mglcos⁡θmgl\cos\theta</span><span><span><span></span><span>m</span><span>g</span><span>l</span><span></span><span>cos</span><span></span><span>θ</span></span></span></span>，<span><span>m=1.5m = 1.5</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1.5</span></span></span></span>、<span><span>l=0.03l = 0.03</span><span><span><span></span><span>l</span><span></span><span>=</span><span></span></span><span><span></span><span>0.03</span></span></span></span>、<span><span>g=9.81g = 9.81</span><span><span><span></span><span>g</span><span></span><span>=</span><span></span></span><span><span></span><span>9.81</span></span></span></span>，水平位置是 <span><span>0.44140.4414</span><span><span><span></span><span>0.4414</span></span></span></span> N·m，占 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> N·m 限幅的 44%。PID 要抵消它只能靠误差：比例项需要 <span><span>e=0.4414/kp=0.131e = 0.4414/k_p = 0.131</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>0.4414/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.131</span></span></span></span> rad，或者靠积分器慢慢补，而我按第 2 篇的习惯取 <span><span>ki=0.2 kpk_i = 0.2\,k_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>Ti=5T_i = 5</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> s，积分器补得很慢。</p><p>前馈的想法是：重力是已知的，直接把它加进输出，让反馈只处理剩下的误差：<span><span>u=sat(p+I+d+ff)u = \text{sat}(p + I + d + ff)</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>sat</span></span><span>(</span><span>p</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span><span></span><span>+</span><span></span></span><span><span></span><span>f</span><span>f</span><span>)</span></span></span></span>，<span><span>ff=mglcos⁡rff = mgl\cos r</span><span><span><span></span><span>f</span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span>g</span><span>l</span><span></span><span>cos</span><span></span><span>r</span></span></span></span>。两点：</p><ul>
<li>前馈在限幅之前加进 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span>，所以饱和判据看到的是总输出；</li>
<li>积分器不吃前馈，它仍然只积 <span><span>ee</span><span><span><span></span><span>e</span></span></span></span>，前馈和积分项各管各的。</li>
</ul><p>再往上一层是加速度前馈 <span><span>ff=mglcos⁡r+Jr¨ff = mgl\cos r + J\ddot r</span><span><span><span></span><span>f</span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span>m</span><span>g</span><span>l</span><span></span><span>cos</span><span></span><span>r</span><span></span><span>+</span><span></span></span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>r</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span></span></span></span>。我的参考信号是 <span><span>r=0.3sin⁡(2π⋅0.5 t)r = 0.3\sin(2\pi\cdot 0.5\,t)</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.5</span><span></span><span>t</span><span>)</span></span></span></span>，<span><span>r¨=−0.3π2sin⁡(πt)\ddot r = -0.3\pi^2\sin(\pi t)</span><span><span><span></span><span><span><span><span><span><span></span><span>r</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.3</span><span><span>π</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>sin</span><span>(</span><span>π</span><span>t</span><span>)</span></span></span></span>，<span><span>Jr¨J\ddot r</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>r</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span></span></span></span> 的幅值 <span><span>0.015×0.3π2=0.04440.015 \times 0.3\pi^2 = 0.0444</span><span><span><span></span><span>0.015</span><span></span><span>×</span><span></span></span><span><span></span><span>0.3</span><span><span>π</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.0444</span></span></span></span> N·m，是重力矩的 10%。</p></section><section><h3>无扰动切换：reset 在做什么<a href="#无扰动切换reset-在做什么"><span>#</span></a></h3><p>比如自瞄在“手动”和“自动瞄准”两种模式之间切换，两种模式用两组增益。增益一变，比例项 <span><span>kp(br−y)k_p(br - y)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>b</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>)</span></span></span></span> 立刻变，只要此刻有误差，输出就跳一下。<code>reset(u, r, y)</code> 的做法是让下一拍的输出等于 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>：把积分项改成</p><span><span><span>I←u−kp (b r−y)−dI \leftarrow u - k_p\,(b\,r - y) - d</span><span><span><span></span><span>I</span><span></span><span>←</span><span></span></span><span><span></span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>(</span><span>b</span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>d</span></span></span></span></span><p>也就是“新增益下，比例项和微分项该是多少，积分项就补多少，凑出 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>”。同时把 <code>prev_ed</code> 设成 <span><span>cr−ycr - y</span><span><span><span></span><span>cr</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span>，避免微分又踢一脚。注意 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 是 PID 自己的输出，<strong>不含前馈</strong>：驱动里调 <code>reset(u_last - ff, r, y)</code>。</p><p><strong><code>reset</code> 不是完美的。</strong> 微分状态 <span><span>dd</span><span><span><span></span><span>d</span></span></span></span> 没动，它在下一拍的 <code>update</code> 里会按 <span><span>a=tf/(tf+Ts)a = t_f/(t_f + T_s)</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 衰减，输出就多了一个 <span><span>d(a−1)d(a - 1)</span><span><span><span></span><span>d</span><span>(</span><span>a</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>，大小是 <span><span>∣d∣ Ts/(tf+Ts)|d|\,T_s/(t_f + T_s)</span><span><span><span></span><span>∣</span><span>d</span><span>∣</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。这是一个可以预测的残差，我在结果里对。</p></section><section><h3>更新顺序<a href="#更新顺序"><span>#</span></a></h3><p>库的 <code>update</code> 按固定顺序做七件事（第 6 步里 <code>hold_integral</code> 为真时整个跳过）：</p><ol>
<li><span><span>p=kp(br−y)p = k_p(br - y)</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>b</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>)</span></span></span></span>。</li>
<li><span><span>ed=cr−ye_d = cr - y</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>cr</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span>；若是第一次，<code>prev_ed = e_d</code>。</li>
<li><span><span>d←tftf+Tsd+kdtf+Ts(ed−edprev)d \leftarrow \dfrac{t_f}{t_f + T_s}d + \dfrac{k_d}{t_f + T_s}(e_d - e_d^{\text{prev}})</span><span><span><span></span><span>d</span><span></span><span>←</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>d</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span><span><span>prev</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，然后 <code>prev_ed = e_d</code>。</li>
<li><span><span>v=p+I+d+ffv = p + I + d + ff</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>p</span><span></span><span>+</span><span></span></span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span><span></span><span>+</span><span></span></span><span><span></span><span>f</span><span>f</span></span></span></span>。</li>
<li><span><span>u=sat(v)u = \text{sat}(v)</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>sat</span></span><span>(</span><span>v</span><span>)</span></span></span></span>，<code>saturated = (u != v)</code>。</li>
<li>按窗口模式更新 <span><span>II</span><span><span><span></span><span>I</span></span></span></span>，用 <span><span>e=r−ye = r - y</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span>。</li>
<li>返回 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>。</li>
</ol><p><strong>积分是先用后更新</strong>：第 5 步判饱和要用这一拍的 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 和 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>，所以输出里的 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 是上一拍的，积分的递推滞后一拍（前向欧拉）。第 4 篇的后向积分把当前的 <span><span>ee</span><span><span><span></span><span>e</span></span></span></span> 先加进去，现在换成了这样，相位上多了半拍的滞后。我估计在 1 kHz 下这半拍无足轻重，没有单独量化。</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>v</p></span></div><div><span><p>u</p></span></div><div><span><p>u - v，反算</p></span></div><div><span><p>r</p></span></div><div><span><p>P：kp (b r - y)</p></span></div><div><span><p>y</p></span></div><div><span><p>D：kd s/(tf s + 1) (c r - y)</p></span></div><div><span><p>I：ki/s (r - y)</p></span></div><div><span><p>+</p></span></div><div><span><p>ff</p></span></div><div><span><p>限幅</p></span></div><div><span><p>u</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>v</p></span></div><div><span><p>u</p></span></div><div><span><p>u - v，反算</p></span></div><div><span><p>r</p></span></div><div><span><p>P：kp (b r - y)</p></span></div><div><span><p>y</p></span></div><div><span><p>D：kd s/(tf s + 1) (c r - y)</p></span></div><div><span><p>I：ki/s (r - y)</p></span></div><div><span><p>+</p></span></div><div><span><p>ff</p></span></div><div><span><p>限幅</p></span></div><div><span><p>u</p></span></div>
</div></div></div></section></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><p>模块是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid.py" target="_blank"><code>python/pidlab/pid.py</code></a>，不到 100 行。我只贴承重的部分。先是配置，所有字段都是物理单位的并联增益：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@dataclass</span></div></div><div><div><div>2</div></div><div><span>class</span><span><span> </span><span>PidConfig</span></span><span>:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>kp: </span><span>float</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>ki: </span><span>float</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>kd: </span><span>float</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>ts: </span><span>float</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>tf: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>b: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>c: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>u_min: </span><span>float</span><span><span> </span><span>=</span><span> </span><span>-</span><span>math.inf</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>u_max: </span><span>float</span><span><span> </span><span>=</span><span> math.inf</span></span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>windup: Windup </span><span>=</span><span> Windup.</span><span>CLAMP</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>i_limit: </span><span>float</span><span><span> </span><span>=</span><span> math.inf</span></span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>tt: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>字段顺序是 <code>kp, ki, kd, ts</code> 加一串有默认值的字段。</strong> 默认是 <span><span>b=1b = 1</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>（微分只看测量值）、<code>CLAMP</code>、不限幅、不滤波。第 1 篇的 <code>PidBasic</code> 基本是它的特例：<span><span>c=1c = 1</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<code>tf = 0</code>、<code>LIMIT</code>，区别只在积分先用后更新。</p><p>构造函数拒绝四种配置，并且存的是配置的一份拷贝：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>cfg</span></span><span>:</span><span> PidConfig</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>2</div></div><div><span>        </span><span>if</span><span><span> cfg.ts </span><span>&lt;=</span><span> </span></span><span>0.0</span><span> </span><span>or</span><span><span> cfg.tf </span><span>&lt;</span><span> </span></span><span>0.0</span><span> </span><span>or</span><span><span> cfg.u_min </span><span>&gt;</span><span> cfg.u_max:</span></span></div></div><div><div><div>3</div></div><div><span>            </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"need ts &gt; 0, tf &gt;= 0, u_min &lt;= u_max"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> cfg.windup </span><span>is</span><span><span> Windup.</span><span>BACK_CALC</span><span> </span></span><span>and</span><span><span> cfg.tt </span><span>&lt;=</span><span> </span></span><span>0.0</span><span>:</span></div></div><div><div><div>5</div></div><div><span>            </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"BACK_CALC needs tt &gt; 0"</span><span>)</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.cfg </span><span>=</span><span> </span><span>replace</span><span>(cfg)  </span></span><span># a copy: the gains may be edited later</span></div></div><div><div><div>7</div></div><div><span>        </span><span># ... integral, d_state, prev_ed = 0; primed = saturated = False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四种配置是 <code>ts &lt;= 0</code>、<code>tf &lt; 0</code>、<code>u_min &gt; u_max</code>，以及 <code>BACK_CALC</code> 却 <code>tt &lt;= 0</code>。最后一条是新的，<code>ts / tt</code> 除零会产生 <code>inf</code>，我宁愿它在构造时失败。</p><p><strong><code>self.cfg = replace(cfg)</code> 这一行是被一个 bug 逼出来的。</strong> 我第一版直接存 <code>cfg</code>。<code>bumpless_off</code> 在切换时 <code>pid.cfg.kp *= 2.0</code>，改的是算例表里那份共享的 <code>base</code> 配置，下一个算例 <code>bumpless_on</code> 拿到的就是已经翻了一倍的增益，从第 0 拍起就是 2 倍增益。三种原生实现和 Python 一对，<code>bumpless_on</code> 的 <code>sat</code> 列差了 1.0（<code>u</code> 列差了 0.45 N·m 量级），<code>compare_dir</code> 如实报了 FAIL。原生版本的配置是按值拷进控制器的，从来没有这个问题，是 Python 的对象引用惹的祸。</p><p>接着是 <code>update</code>，上面“更新顺序”的七步一步不差：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> </span><span>self</span><span>.cfg</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>p </span><span>=</span><span> c.kp </span><span>*</span><span> (c.b </span><span>*</span><span> r </span><span>-</span><span> y)</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>ed </span><span>=</span><span> c.c </span><span>*</span><span> r </span><span>-</span><span> y</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span> </span><span>self</span><span>.primed:</span></div></div><div><div><div>5</div></div><div><span>            </span><span>self</span><span><span>.prev_ed </span><span>=</span><span> ed</span></span></div></div><div><div><div>6</div></div><div><span>            </span><span>self</span><span><span>.primed </span><span>=</span><span> </span></span><span>True</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>a </span><span>=</span><span> c.tf </span><span>/</span><span> (c.tf </span><span>+</span><span> c.ts)</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span><span>.d_state </span><span>=</span><span> a </span><span>*</span><span> </span></span><span>self</span><span><span>.d_state </span><span>+</span><span> c.kd </span><span>/</span><span> (c.tf </span><span>+</span><span> c.ts) </span><span>*</span><span> (</span></span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>ed </span><span>-</span><span> </span><span>self</span><span>.prev_ed</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span><span>.prev_ed </span><span>=</span><span> ed</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>v </span><span>=</span><span> p </span><span>+</span><span> </span><span>self</span><span><span>.integral </span><span>+</span><span> </span></span><span>self</span><span><span>.d_state </span><span>+</span><span> ff</span></span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>clamp</span><span>(v, c.u_min, c.u_max)</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span><span>.saturated </span><span>=</span><span> u </span><span>!=</span><span> v</span></span></div></div><div><div><div>15</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span> hold_integral:</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>e </span><span>=</span><span> r </span><span>-</span><span> y</span></div></div><div><div><div>17</div></div><div><span>            </span><span>if</span><span> c.windup </span><span>is</span><span><span> Windup.</span><span>NONE</span><span>:</span></span></div></div><div><div><div>18</div></div><div><span>                </span><span>self</span><span><span>.integral </span><span>+=</span><span> c.ki </span><span>*</span><span> c.ts </span><span>*</span><span> e</span></span></div></div><div><div><div>19</div></div><div><span>            </span><span># ... LIMIT: the same, then clamp(self.integral, +-i_limit)</span></div></div><div><div><div>20</div></div><div><span>            </span><span>elif</span><span> c.windup </span><span>is</span><span><span> Windup.</span><span>CLAMP</span><span>:</span></span></div></div><div><div><div>21</div></div><div><span>                </span><span>if</span><span> </span><span>not</span><span> (</span><span>self</span><span>.saturated </span><span>and</span><span><span> e </span><span>*</span><span> (v </span><span>-</span><span> u) </span><span>&gt;</span><span> </span></span><span>0.0</span><span>):</span></div></div><div><div><div>22</div></div><div><span>                    </span><span>self</span><span><span>.integral </span><span>+=</span><span> c.ki </span><span>*</span><span> c.ts </span><span>*</span><span> e</span></span></div></div><div><div><div>23</div></div><div><span>            </span><span>else</span><span>:</span></div></div><div><div><div>24</div></div><div><span>                </span><span>self</span><span><span>.integral </span><span>+=</span><span> c.ki </span><span>*</span><span> c.ts </span><span>*</span><span> e </span><span>+</span><span> c.ts </span><span>/</span><span> c.tt </span><span>*</span><span> (u </span><span>-</span><span> v)</span></span></div></div><div><div><div>25</div></div><div><span>        </span><span>return</span><span> u</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>要注意三处。<strong>积分器吃的是 <code>e = r - y</code>，不是 <code>p</code> 用的 <code>b * r - y</code>，</strong> 所以 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 不会改变稳态。<strong><code>saturated</code> 是这一拍的 <code>u != v</code>，不含积分更新之后的任何东西，</strong> 这正是第 7 篇外环要读的那个标志。<code>hold_integral</code> 为真时整个第 6 步跳过，包括 BACK_CALC 的回灌。<code>reset</code> 只有三行，就是前面的 <span><span>I←u−kp(br−y)−dI \leftarrow u - k_p(br - y) - d</span><span><span><span></span><span>I</span><span></span><span>←</span><span></span></span><span><span></span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>b</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>d</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>reset</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>u</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>r</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>y</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> </span><span>self</span><span>.cfg</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span><span>.integral </span><span>=</span><span> u </span><span>-</span><span> c.kp </span><span>*</span><span> (c.b </span><span>*</span><span> r </span><span>-</span><span> y) </span><span>-</span><span> </span></span><span>self</span><span>.d_state</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.prev_ed </span><span>=</span><span> c.c </span><span>*</span><span> r </span><span>-</span><span> y</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.primed </span><span>=</span><span> </span></span><span>True</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>俯仰轴和四舍五入<a href="#俯仰轴和四舍五入"><span>#</span></a></h3><p>驱动要一个新植物 <code>GimbalPitch</code>（GM6020 俯仰，带重力和编码器）。它的运动方程是 <span><span>Jθ¨=τ−τd−bθ˙−mglcos⁡θJ\ddot\theta = \tau - \tau_d - b\dot\theta - mgl\cos\theta</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>θ</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span><span><span><span><span><span></span><span>θ</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>m</span><span>g</span><span>l</span><span></span><span>cos</span><span></span><span>θ</span></span></span></span>，状态 <span><span>[θ,θ˙][\theta, \dot\theta]</span><span><span><span></span><span>[</span><span>θ</span><span>,</span><span></span><span><span><span><span><span><span></span><span>θ</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span>]</span></span></span></span>，输入 <span><span>[τ,τd][\tau, \tau_d]</span><span><span><span></span><span>[</span><span>τ</span><span>,</span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>，<span><span>τd\tau_d</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是外部负载力矩，加在 <code>plants.py</code> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>grav </span><span>=</span><span> </span><span>self</span><span><span>.m </span><span>*</span><span> </span></span><span>self</span><span><span>.g </span><span>*</span><span> </span></span><span>self</span><span><span>.l </span><span>*</span><span> math.</span><span>cos</span><span>(x[</span></span><span>0</span><span>])</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>acc </span><span>=</span><span> (u[</span><span>0</span><span><span>] </span><span>-</span><span> u[</span></span><span>1</span><span><span>] </span><span>-</span><span> </span></span><span>self</span><span><span>.b </span><span>*</span><span> x[</span></span><span>1</span><span><span>] </span><span>-</span><span> grav) </span><span>/</span><span> </span></span><span>self</span><span>.j</span></div></div><div><div><div>4</div></div><div><span>        </span><span>return</span><span> [x[</span><span>1</span><span>], acc]</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>def</span><span> </span><span>encoder</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> </span><span>self</span><span>.quantum</span></div></div><div><div><div>8</div></div><div><span>        </span><span>return</span><span><span> </span><span>round_half_away</span><span>(x[</span></span><span>0</span><span><span>] </span><span>/</span><span> q) </span><span>*</span><span> q</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（类里没有输入个数 <code>m</code>，因为 <code>m</code> 在这里是质量，我用了 <code>n_in</code>。）<code>encoder</code> 把角度除以 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span>、舍入、再乘回去，也就是第 1 篇说过的“编码器读数是 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 的整数倍”。<strong>舍入函数是四种语言必须一致的东西。</strong> Python 内置的 <code>round</code> 是银行家舍入（0.5 舍入到偶数），而 C 的 <code>round</code>、Rust 的 <code>f64::round</code> 都是“远离零”，所以我写了一个显式的版本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>round_half_away</span><span>(</span><span>x</span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> </span><span>abs</span><span>(x)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> math.</span><span>floor</span><span>(a)</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span><span> math.</span><span>copysign</span><span>(f </span><span>+</span><span> (</span></span><span>1.0</span><span> </span><span>if</span><span><span> a </span><span>-</span><span> f </span><span>&gt;=</span><span> </span></span><span>0.5</span><span> </span><span>else</span><span> </span><span>0.0</span><span>), x)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C 用 <code>round</code>/<code>roundf</code>，Rust 用 <code>libm::round</code>，C++ 用 <code>std::round</code>。</p></section><section><h3>驱动<a href="#驱动"><span>#</span></a></h3><p>驱动 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch05_practical.py" target="_blank"><code>python/ch05_practical.py</code></a> 的算例表我不贴，用文字说：转速环的 PI 是第 4 篇的 <span><span>ωn=30\omega_n = 30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span>、<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>（<span><span>kp=4.1667k_p = 4.1667</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.1667</span></span></span></span>、<span><span>ki=90k_i = 90</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>90</span></span></span></span>，电流限幅 <span><span>±20\pm 20</span><span><span><span></span><span>±</span><span>20</span></span></span></span> A，四个 <code>windup_*</code> 算例只换 <code>windup</code> 字段，<code>i_limit = 5</code>、<span><span>Tt=0.5 kp/kiT_t = 0.5\,k_p/k_i</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）；俯仰的 PID 是 <span><span>ωn=15\omega_n = 15</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span>、<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>，<span><span>kp=Jωn2=3.375k_p = J\omega_n^2 = 3.375</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>J</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3.375</span></span></span></span>、<span><span>kd=2ζωnJ−b=0.313k_d = 2\zeta\omega_nJ - b = 0.313</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.313</span></span></span></span>、<span><span>ki=0.2 kp=0.675k_i = 0.2\,k_p = 0.675</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.675</span></span></span></span>，<span><span>tf=2t_f = 2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> ms，力矩限幅 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> N·m，微分冲击、滤波和前馈的算例都是在这份 <code>base</code> 上换几个字段。</p><p>仿真循环比前几章更短，因为没有传输延迟，限幅在 PID 里。增益切换是循环里唯一特别的地方：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>if</span><span> case.switch </span><span>and</span><span><span> k </span><span>==</span><span> </span><span>SWITCH_K</span><span>:</span></span></div></div><div><div><div>2</div></div><div><span><span>            </span></span><span>pid.cfg.kp </span><span>*=</span><span> </span><span>2.0</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>pid.cfg.ki </span><span>*=</span><span> </span><span>2.0</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>pid.cfg.kd </span><span>*=</span><span> </span><span>2.0</span></div></div><div><div><div>5</div></div><div><span>            </span><span>if</span><span><span> case.switch </span><span>==</span><span> </span></span><span>2</span><span>:</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>pid.</span><span>reset</span><span>(u </span><span>-</span><span> ff, r, y)</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> pid.</span><span>update</span><span>(r, y, ff)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>switch</code> 为 1 或 2 时在第 1000 拍把三个增益都乘 2，为 2 时再调 <code>reset(u - ff, r, y)</code>，减掉 <code>ff</code> 是因为 <code>reset</code> 的 <code>u</code> 不含前馈。俯仰轴的 <code>y</code> 是编码器读数，<code>ideal=True</code> 时读精确角度，<code>report</code> 用它算“没有量化时的理想输出”。每个算例写 8 列：<code>t,r,y,u,ytrue,integral,d_state,sat</code>，<code>ytrue</code> 是真实输出。数据文件（设定值、负载、前馈三列）由 <code>write_data</code> 一次写好，原生版本只读不算，所以 <code>ff</code> 的重力项也是 Python 算的，三个原生版本不重新算 <span><span>mglcos⁡rmgl\cos r</span><span><span><span></span><span>m</span><span>g</span><span>l</span><span></span><span>cos</span><span></span><span>r</span></span></span></span>。</p></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>三个版本都是 Python 版的逐行翻译，<code>update</code> 里的算术顺序和 Python 一致。区别在这几处：</p><ul>
<li><strong>枚举和配置。</strong> C 是 <code>enum pl_windup</code> 加 <code>struct pl_pid_config</code>，没有 <code>typedef</code>；C++ 是 <code>enum class Windup</code> 和模板结构体 <code>PidConfig&lt;T&gt;</code>，用聚合初始化（<code>PidConfig&lt;double&gt;{kp, ki, kd, ts}</code>，后面的字段有默认值）；Rust 是 <code>enum Windup</code> 和 <code>PidConfig</code>，给了一个 <code>PidConfig::new(kp, ki, kd, ts)</code> 填默认值。</li>
<li><strong>构造的失败方式。</strong> C 的 <code>pl_pid_init</code> 返回 0 或 <code>-EINVAL</code>；C++ 的 <code>Pid&lt;T&gt;::Create</code> 返回 <code>std::optional&lt;Pid&lt;T&gt;&gt;</code>；Rust 的 <code>Pid::new</code> 返回 <code>Result&lt;Pid, PidError&gt;</code>，<code>PidError::InvalidConfig</code> 带一个 <code>&amp;'static str</code>。</li>
<li><strong>运行中改增益。</strong> 第 10 篇还要在运行中改 <code>ts</code>。C 的 <code>pid-&gt;cfg</code> 是公开的结构体字段；C++ 给了 <code>mutable_config()</code>（和只读的 <code>config()</code> 并存）；Rust 的 <code>cfg</code> 是公开字段。这三个是在固定接口之外加的，Python 的 <code>pid.cfg</code> 本来就可写。</li>
<li><strong>内存。</strong> 三种语言的控制器都是栈上的值，没有堆分配。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/pid.h" target="_blank"><code>c/include/pl/pid.h</code></a> 里是两个结构体，配置和状态分开，状态里内嵌一份配置的拷贝：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> pl_pid_config {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>pl_real kp, ki, kd, ts, tf, b, c;</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>pl_real u_min, u_max, i_limit, tt;</span></div></div><div><div><div>4</div></div><div><span>  </span><span>enum</span><span> pl_windup windup;</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>struct</span><span> pl_pid {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>struct</span><span> pl_pid_config cfg;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>pl_real integral, d_state, prev_ed;</span></div></div><div><div><div>10</div></div><div><span>  </span><span>bool</span><span> primed, saturated;</span></div></div><div><div><div>11</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid.c" target="_blank"><code>c/src/pid.c</code></a> 里的 <code>pl_pid_update</code> 是 Python <code>update</code> 的逐行翻译，窗口模式用 <code>switch</code>。从饱和判断开始：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>v </span><span>=</span><span> p </span><span>+</span><span> pid</span><span>-&gt;</span><span>integral </span><span>+</span><span> pid</span><span>-&gt;</span><span>d_state </span><span>+</span><span> ff;</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>u </span><span>=</span><span> </span><span>pl_clamp</span><span><span>(</span><span>v</span><span>,</span><span> c</span></span><span>-&gt;</span><span><span>u_min</span><span>,</span><span> c</span></span><span>-&gt;</span><span><span>u_max</span><span>);</span></span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>pid</span><span>-&gt;</span><span>saturated </span><span>=</span><span> u </span><span>!=</span><span> v;</span></div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span> (hold_integral)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> u;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>e </span><span>=</span><span> r </span><span>-</span><span> y;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>switch</span><span> (</span><span>c</span><span>-&gt;</span><span>windup</span><span>) {</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>/* ... NONE and LIMIT as in Python */</span></div></div><div><div><div>9</div></div><div><span>  </span><span>case</span><span> PL_WINDUP_CLAMP:</span></div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>(pid</span><span>-&gt;</span><span>saturated </span><span>&amp;&amp;</span><span> e </span><span>*</span><span> (v </span><span>-</span><span> u) </span><span>&gt;</span><span> </span><span>0</span><span>))</span></div></div><div><div><div>11</div></div><div><span>      </span><span>pid</span><span>-&gt;</span><span>integral</span><span> </span><span>+=</span><span> </span><span>c</span><span>-&gt;</span><span>ki</span><span> </span><span>*</span><span> </span><span>c</span><span>-&gt;</span><span>ts</span><span> </span><span>*</span><span> e;</span></div></div><div><div><div>12</div></div><div><span>    </span><span>break</span><span>;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>case</span><span> PL_WINDUP_BACK_CALC:</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pid</span><span>-&gt;</span><span>integral</span><span> </span><span>+=</span><span> </span><span>c</span><span>-&gt;</span><span>ki</span><span> </span><span>*</span><span> </span><span>c</span><span>-&gt;</span><span>ts</span><span> </span><span>*</span><span> e </span><span>+</span><span> </span><span>c</span><span>-&gt;</span><span>ts</span><span> </span><span>/</span><span> </span><span>c</span><span>-&gt;</span><span>tt</span><span> </span><span>*</span><span> (u </span><span>-</span><span> v);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>break</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>  </span><span>return</span><span> u;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong><code>-Wdouble-promotion</code> 在这里起作用。</strong> C 的 <code>pid.c</code> 里我用 <code>cfg-&gt;ts &lt;= 0</code> 而不是 <code>&lt;= 0.0</code>：在 <code>-DPL_USE_FLOAT</code> 下，<code>float</code> 和 <code>double</code> 常量比较会被提升成 <code>double</code>，而整数常量不会。我也没有在 <code>pid.c</code> 里写任何 <code>0.5</code> 这样的字面量。</p><p>植物 <code>pl_gimbal_pitch</code> 在 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/plants.c" target="_blank"><code>c/src/plants.c</code></a> 里，导数和编码器和 Python 一一对应，编码器是 <code>PL_ROUND(x[0] / p-&gt;quantum) * p-&gt;quantum</code>。C 的 <code>&lt;math.h&gt;</code> 在 <code>-std=c11 -Wpedantic</code> 下没有 <code>M_PI</code>，所以 <span><span>2π2\pi</span><span><span><span></span><span>2</span><span>π</span></span></span></span> 写成数值字面量 <code>3.141592653589793</code>；<code>PL_ROUND</code> 是新加进 <code>real.h</code> 的宏（<code>double</code> 下是 <code>round</code>，<code>float</code> 下是 <code>roundf</code>）。</p><p>驱动 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch05.c" target="_blank"><code>c/apps/ch05.c</code></a> 把 15 个算例放在一个 <code>struct sim_case</code> 数组里（<code>MAX_CASES 15</code>，我第一次写的是 14，数组越界，段错误，这是这一章唯一一次 <code>make</code> 直接崩）。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid.h" target="_blank"><code>cpp/include/pidlab/pid.h</code></a> 是头文件库。<code>Create</code> 做校验，常量写成 <code>T(0)</code>，让它们跟着标量类型走：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>static</span><span> std::</span><span>optional</span><span>&lt;</span><span>Pid</span><span>&gt; </span><span>Create</span><span>(</span><span>const</span><span> </span><span>PidConfig</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>cfg</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> (</span><span>cfg</span><span>.</span><span>ts</span><span> </span><span>&lt;=</span><span> </span><span>T</span><span>(</span><span>0</span><span>) </span><span>||</span><span> </span><span>cfg</span><span>.</span><span>tf</span><span> </span><span>&lt;</span><span> </span><span>T</span><span>(</span><span>0</span><span>) </span><span>||</span><span> </span><span>cfg</span><span>.</span><span>u_min</span><span> </span><span>&gt;</span><span> </span><span>cfg</span><span>.</span><span>u_max</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>      </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> (</span><span>cfg</span><span>.</span><span>windup</span><span> </span><span>==</span><span> Windup::</span><span>kBackCalc</span><span> </span><span>&amp;&amp;</span><span> </span><span>cfg</span><span>.</span><span>tt</span><span> </span><span>&lt;=</span><span> </span><span>T</span><span>(</span><span>0</span><span>)) {</span></div></div><div><div><div>6</div></div><div><span>      </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>Pid</span><span>(</span><span>cfg</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>构造函数是私有的，<code>Create</code> 是唯一入口，所以拿到一个 <code>Pid</code> 就意味着它通过了校验。<code>Update</code> 的主体和 C 的是同一段，只是成员带下划线、常量带 <code>T()</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>v</span><span> </span><span>=</span><span> </span><span>p</span><span> </span><span>+</span><span> </span><span>integral_</span><span> </span><span>+</span><span> </span><span>d_state_</span><span> </span><span>+</span><span> </span><span>ff</span><span>;</span></div></div><div><div><div>2</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>u</span><span> </span><span>=</span><span> </span><span>Clamp</span><span>(</span><span>v</span><span>, </span><span>cfg_</span><span>.</span><span>u_min</span><span>, </span><span>cfg_</span><span>.</span><span>u_max</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>saturated_</span><span> </span><span>=</span><span> </span><span>u</span><span> </span><span>!=</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> (</span><span>hold_integral</span><span>) </span><span>return</span><span> </span><span>u</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>e</span><span> </span><span>=</span><span> </span><span>r</span><span> </span><span>-</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>switch</span><span> (</span><span>cfg_</span><span>.</span><span>windup</span><span>) {</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>// ... kNone and kLimit</span></div></div><div><div><div>8</div></div><div><span>      </span><span>case</span><span> Windup::</span><span>kClamp</span><span>:</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span>(</span><span>saturated_</span><span> </span><span>&amp;&amp;</span><span> </span><span>e</span><span> </span><span>*</span><span> (</span><span>v</span><span> </span><span>-</span><span> </span><span>u</span><span>) </span><span>&gt;</span><span> </span><span>T</span><span>(</span><span>0</span><span>))) {</span></div></div><div><div><div>10</div></div><div><span>          </span><span>integral_</span><span> </span><span>+=</span><span> </span><span>cfg_</span><span>.</span><span>ki</span><span> </span><span>*</span><span> </span><span>cfg_</span><span>.</span><span>ts</span><span> </span><span>*</span><span> </span><span>e</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>break</span><span>;</span></div></div><div><div><div>13</div></div><div><span>      </span><span>case</span><span> Windup::</span><span>kBackCalc</span><span>:</span></div></div><div><div><div>14</div></div><div><span>        </span><span>integral_</span><span> </span><span>+=</span><span> </span><span>cfg_</span><span>.</span><span>ki</span><span> </span><span>*</span><span> </span><span>cfg_</span><span>.</span><span>ts</span><span> </span><span>*</span><span> </span><span>e</span><span> </span><span>+</span><span> </span><span>cfg_</span><span>.</span><span>ts</span><span> </span><span>/</span><span> </span><span>cfg_</span><span>.</span><span>tt</span><span> </span><span>*</span><span> (</span><span>u</span><span> </span><span>-</span><span> </span><span>v</span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>break</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> </span><span>u</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>限幅函数是类里的一个私有静态 <code>Clamp</code>，没有用 <code>std::clamp</code>：后者在 <code>lo &gt; hi</code> 时是未定义行为，而 <code>i_limit</code> 没有被校验过符号。驱动 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch05.cc" target="_blank"><code>cpp/apps/ch05.cc</code></a> 里的 <code>RunLoop&lt;P&gt;</code> 对植物类型做模板；俯仰轴有 <code>Encoder</code> 成员，用 <code>if constexpr (requires { plant.Encoder(x); })</code> 在编译期选测量方式，别的植物直接用 <code>Output</code>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><p><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid.rs" target="_blank"><code>rust/src/pid.rs</code></a> 里的 <code>Pid</code> 只有 <code>cfg</code> 是公开字段，其余状态是私有的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Pid</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>cfg</span><span>: </span><span>PidConfig</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>integral</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>d_state</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>prev_ed</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>primed</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>saturated</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>update</code> 的后半段，窗口模式用 <code>match</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>let</span><span> </span><span>v</span><span><span> </span><span>=</span><span> </span></span><span>p</span><span> + </span><span>self</span><span>.integral + </span><span>self</span><span>.d_state + </span><span>ff</span><span>;</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>u</span><span><span> </span><span>=</span><span> </span></span><span>clamp</span><span>(</span><span>v</span><span>, </span><span>c</span><span>.u_min, </span><span>c</span><span>.u_max);</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span><span>.saturated </span><span>=</span><span> </span></span><span>u</span><span><span> </span><span>!=</span><span> </span></span><span>v</span><span>;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> </span><span>hold_integral</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>return</span><span> </span><span>u</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>e</span><span><span> </span><span>=</span><span> </span></span><span>r</span><span> - </span><span>y</span><span>;</span></div></div><div><div><div>8</div></div><div><span>        </span><span>match</span><span> </span><span>c</span><span>.windup {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>// ... None and Limit</span></div></div><div><div><div>10</div></div><div><span>            </span><span>Windup</span><span>::</span><span>Clamp</span><span> =&gt; {</span></div></div><div><div><div>11</div></div><div><span>                </span><span>if</span><span><span> </span><span>!</span><span>(</span></span><span>self</span><span><span>.saturated </span><span>&amp;&amp;</span><span> </span></span><span>e</span><span> * (</span><span>v</span><span> - </span><span>u</span><span><span>) </span><span>&gt;</span><span> </span></span><span>0.0</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>                    </span><span>self</span><span><span>.integral </span><span>+=</span><span> </span></span><span>c</span><span>.ki * </span><span>c</span><span>.ts * </span><span>e</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Windup</span><span>::</span><span>BackCalc</span><span> =&gt; </span><span>self</span><span><span>.integral </span><span>+=</span><span> </span></span><span>c</span><span>.ki * </span><span>c</span><span>.ts * </span><span>e</span><span> + </span><span>c</span><span>.ts / </span><span>c</span><span>.tt * (</span><span>u</span><span> - </span><span>v</span><span>),</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>        </span><span>u</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>clamp</code> 是文件里的一个小函数，没有用 <code>f64::clamp</code>：标准库那个在 <code>min &gt; max</code> 或有 NaN 时会 panic，库是 <code>no_std</code> 的，我不想让它 panic。<code>match</code> 是穷尽的，加一种窗口模式编译器会逼着我补这一支。驱动 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch05.rs" target="_blank"><code>rust/src/bin/ch05.rs</code></a> 里的 <code>run_loop&lt;const N: usize&gt;</code> 接受三个闭包：测量、真实输出和导数，转速环（<code>MotorSpeedLag</code>，2 维）和俯仰轴（<code>GimbalPitch</code>，2 维）共用它；<code>Real</code> 是 <code>f64</code>，<code>GimbalPitch::new</code> 里的 <span><span>2π/81922\pi/8192</span><span><span><span></span><span>2</span><span>π</span><span>/8192</span></span></span></span> 用 <code>core::f64::consts::TAU / 8192.0</code> 再转成 <code>Real</code>，这样在 <code>f32</code> 特性下同一行也能编译。</p></section><section><h3>PID 库的接口<a href="#pid-库的接口"><span>#</span></a></h3><p>下面是系列从此以后的“PID 库”，四种语言的公开接口并排放在一起，第 6 到第 10 篇不会再改它：</p>

<table><thead><tr><th></th><th>Python</th><th>C</th><th>C++</th><th>Rust</th></tr></thead><tbody><tr><td>模式</td><td><code>Windup</code></td><td><code>enum pl_windup</code></td><td><code>Windup</code></td><td><code>Windup</code></td></tr><tr><td>配置</td><td><code>PidConfig</code></td><td><code>struct pl_pid_config</code></td><td><code>PidConfig&lt;T&gt;</code></td><td><code>PidConfig</code></td></tr><tr><td>构造</td><td><code>Pid(cfg)</code>，<code>ValueError</code></td><td><code>pl_pid_init</code>，<code>-EINVAL</code></td><td><code>Pid&lt;T&gt;::Create</code>，<code>nullopt</code></td><td><code>Pid::new</code>，<code>PidError</code></td></tr><tr><td>一拍</td><td><code>update(r, y, ff, hold)</code></td><td><code>pl_pid_update</code></td><td><code>Update</code></td><td><code>update</code></td></tr><tr><td>重置</td><td><code>reset(u, r, y)</code></td><td><code>pl_pid_reset</code></td><td><code>Reset</code></td><td><code>reset</code></td></tr><tr><td>饱和标志</td><td><code>saturated</code></td><td><code>pid-&gt;saturated</code></td><td><code>saturated()</code></td><td><code>saturated()</code></td></tr></tbody></table><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>pl_pid_init</span><span>(</span><span>struct</span><span> pl_pid </span><span>*</span><span><span>pid</span><span>,</span><span> </span></span><span>const</span><span> </span><span>struct</span><span> pl_pid_config </span><span>*</span><span><span>cfg</span><span>);</span></span></div></div><div><div><div>2</div></div><div><span>pl_real </span><span>pl_pid_update</span><span>(</span><span>struct</span><span> pl_pid </span><span>*</span><span><span>pid</span><span>,</span><span> pl_real </span><span>r</span><span>,</span><span> pl_real </span><span>y</span><span>,</span><span> pl_real </span><span>ff</span><span>,</span></span></div></div><div><div><div>3</div></div><div><span>          </span><span>bool</span><span><span> </span><span>hold_integral</span><span>);</span></span></div></div><div><div><div>4</div></div><div><span>void</span><span> </span><span>pl_pid_reset</span><span>(</span><span>struct</span><span> pl_pid </span><span>*</span><span><span>pid</span><span>,</span><span> pl_real </span><span>u</span><span>,</span><span> pl_real </span><span>r</span><span>,</span><span> pl_real </span><span>y</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>static</span><span> std::</span><span>optional</span><span>&lt;</span><span>Pid</span><span>&gt; </span><span>Create</span><span>(</span><span>const</span><span> </span><span>PidConfig</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>cfg</span><span>);</span></span></div></div><div><div><div>2</div></div><div><span>T</span><span> </span><span>Update</span><span>(</span><span>T</span><span><span> </span><span>r</span><span>, </span></span><span>T</span><span><span> </span><span>y</span><span>, </span></span><span>T</span><span><span> </span><span>ff</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0</span><span>), </span><span>bool</span><span><span> </span><span>hold_integral</span><span> </span></span><span>=</span><span> </span><span>false</span><span>);</span></div></div><div><div><div>3</div></div><div><span>void</span><span> </span><span>Reset</span><span>(</span><span>T</span><span><span> </span><span>u</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>, </span></span><span>T</span><span><span> </span><span>y</span><span>);</span></span></div></div><div><div><div>4</div></div><div><span>bool</span><span> </span><span>saturated</span><span>() </span><span>const</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>cfg</span><span>: </span><span>PidConfig</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>, </span><span>PidError</span><span>&gt;;</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>r</span><span>: </span><span>Real</span><span>, </span><span>y</span><span>: </span><span>Real</span><span>, </span><span>ff</span><span>: </span><span>Real</span><span>, </span><span>hold_integral</span><span>: </span><span>bool</span><span>) -&gt; </span><span>Real</span><span>;</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>reset</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>u</span><span>: </span><span>Real</span><span>, </span><span>r</span><span>: </span><span>Real</span><span>, </span><span>y</span><span>: </span><span>Real</span><span>);</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>saturated</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>hold_integral</code> 是给第 7 篇的串级用的：内环饱和时，外环可以把这一拍的积分整个冻住，而不用改模式。</p></section><section><h3>编译和风格检查<a href="#编译和风格检查"><span>#</span></a></h3><p>编译参数和前几篇一样：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code> 加同样的警告（15.3.0），Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>（0.1.95）。风格检查：C 用内核的 <code>checkpatch.pl --no-tree --ignore SPDX_LICENSE_TAG,NEW_TYPEDEFS</code>，全部文件 0 errors、0 warnings；C++ 用 <code>clang-format --style=Google</code>（21.1.8）；Python 用 <code>ruff format --line-length 79</code> 和 <code>ruff check</code>（0.15.14）；Rust 用 <code>cargo fmt --check</code>。</p><p>第 10 篇要把这个库放进 float32，我顺手检查了：<code>gcc -DPL_USE_FLOAT -Wdouble-promotion -Werror</code> 编译 <code>pid.c</code> 没有警告；<code>cargo build --lib --no-default-features --features f32</code> 通过；C++ 的 <code>Pid&lt;float&gt;</code> 和 <code>GimbalPitch&lt;float&gt;</code> 能实例化。这三项我只验证了“编译通过”，数值行为留给第 10 篇。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p>下面五组数字来自同一次 <code>make -s ch05</code>：15 个算例乘三种语言，45 个对比文件，输出在后面的检查点里。每个小节先给表，再解释。</p><section><h3>积分饱和：四种模式<a href="#积分饱和四种模式"><span>#</span></a></h3><p>负载和设定值的时间表：0.05 s 设定值 40 rad/s，1.0 s 回到 10 rad/s，2.0 s 加 2.4 N·m 的负载（稳态需要约 8.3 A），电流限幅 <span><span>±20\pm 20</span><span><span><span></span><span>±</span><span>20</span></span></span></span> A，<span><span>kp=4.1667k_p = 4.1667</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.1667</span></span></span></span>、<span><span>ki=90k_i = 90</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>90</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>            </span></span><span>case  peak y  overshoot  max|I|   min y  settle after drop  e under load</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>windup_none   70.96      77.4%   373.7  -11.25             0.558s        0.0000</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>windup_limit   40.58       1.5%     5.0    9.15             0.262s        0.7937</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>windup_clamp   40.89       2.2%     8.7    8.98             0.266s        0.0000</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>windup_backcalc   41.00       2.5%    44.7    8.89             0.266s        0.0000</span></div></div><div><div><div>6</div></div><div><span>LIMIT steady error: formula 0.7937 rad/s, simulated 0.7937 rad/s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch05-windup.svg" alt="四种抗饱和模式下的轮速、电流指令和积分项：不处理的积分器冲到 70.96 rad/s 又反向冲到 -11.25 rad/s，另外三种都紧贴 40 rad/s，而积分限幅在 2 s 加载后停在 10 rad/s 下方" /><figcaption>四种抗饱和模式下的轮速、电流指令和积分项：不处理的积分器冲到 70.96 rad/s 又反向冲到 -11.25 rad/s，另外三种都紧贴 40 rad/s，而积分限幅在 2 s 加载后停在 10 rad/s 下方</figcaption></figure><p></p><p>蓝线是 NONE，最显眼：电流在 20 A 饱和了大约 0.4 s，积分项爬到 373.7 A（限幅的 18.7 倍），轮速冲到 70.96 rad/s，超调 77.4%，在 1.0 s 设定值掉到 10 rad/s 之后又被积分器拖到 <span><span>−11.25-11.25</span><span><span><span></span><span>−</span><span>11.25</span></span></span></span> rad/s，回到 <span><span>±0.2\pm 0.2</span><span><span><span></span><span>±</span><span>0.2</span></span></span></span> rad/s 以内花了 0.558 s。<strong>条件积分的超调是 2.2%，回稳 0.266 s：超调缩小 35 倍，回稳快 2.1 倍。</strong> 反算 BACK_CALC 是 2.5% 和 0.266 s，和 CLAMP 几乎一样；它的积分项绝对值最大是 44.7 A，比 CLAMP 的 8.7 A 大，因为它把积分拉向“<span><span>u−p−du - p - d</span><span><span><span></span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span>p</span><span></span><span>−</span><span></span></span><span><span></span><span>d</span></span></span></span>”，在饱和的头几毫秒里这个数很负（图里下图红线）。这个大数不要紧，因为它是被主动拉过去的，不是累出来的。</p><p><strong>LIMIT 的结果和我预想的有出入，也是我最想让你看到的一个。</strong> 5 A 的积分限幅把超调压到 1.5%（最低），回稳 0.262 s（最快），看起来是赢家，但 2.0 s 加上 2.4 N·m 的负载之后，积分项停在 5.0 A，稳态需要的电流是 8.3 A，剩下的比例项补，最后留下 0.7937 rad/s 的稳态误差。公式给 0.7937，仿真给 0.7937，吻合。如果我把 <code>i_limit</code> 取成 10 A（大于 8.3 A），误差会消失，但那时候积分限幅对暂态的保护也就弱了：<code>i_limit</code> 要同时比“稳态需要的电流”大、比“饱和时累积的电流”小，两头夹。我没有对 <code>i_limit</code> 做扫描，这个说法来自公式，不是实验。条件积分和反算没有这个问题，因为它们只在饱和时介入。</p></section><section><h3>微分冲击<a href="#微分冲击"><span>#</span></a></h3><p>三个算例，俯仰轴，0.05 s 设定值阶跃 0.1 rad，<span><span>tf=2t_f = 2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> ms，力矩限幅 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> N·m。<code>kick_c1</code>：<span><span>b=1b = 1</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>c=1c = 1</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（D 看误差）；<code>kick_c0</code>：<span><span>b=1b = 1</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>（D 只看测量）；<code>weight_b05</code>：<span><span>b=0.5b = 0.5</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span></span></span></span>、<span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。重力由前馈补掉（<code>ch05_step</code> 数据里 <code>ff = mgl cos r</code>，这样三个算例不用先对付 0.131 rad 的下垂）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>       </span></span><span>case  peak D  max|u| 5ms  overshoot  y at 1.45 s    rise  sat 50 ms</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>kick_c1   10.43       1.000      5.48%       0.1015   0.135          9</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>kick_c0    0.24       0.777      5.63%       0.1016   0.139          0</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>weight_b05    0.14       0.608      0.00%       0.0625     nan          0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch05-derivative-kick.svg" alt="三种设定值权重下的 D 项、力矩指令和俯仰角：D 看误差时 D 项在阶跃那一拍冲到 10.43 N·m 并把力矩夹在 1 N·m，D 只看测量时没有这个尖峰；b 取 0.5 时响应慢得多" /><figcaption>三种设定值权重下的 D 项、力矩指令和俯仰角：D 看误差时 D 项在阶跃那一拍冲到 10.43 N·m 并把力矩夹在 1 N·m，D 只看测量时没有这个尖峰；b 取 0.5 时响应慢得多</figcaption></figure><p></p><p>上图的 D 项是对数刻度（<span><span>±0.1\pm 0.1</span><span><span><span></span><span>±</span><span>0.1</span></span></span></span> 以内线性）：蓝线在阶跃那一拍直接是 10.43 N·m，和我前面算的 <span><span>kdΔr/(tf+Ts)=10.43k_d\Delta r/(t_f + T_s) = 10.43</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>r</span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>10.43</span></span></span></span> 一样，下一拍乘 0.667。中图是力矩指令：<strong>D 看误差时，力矩在上限 1 N·m 上停了 9 拍（<code>sat 50 ms</code> 那一列）；D 只看测量时，同一时刻是 0.777 N·m，一拍都没有饱和。</strong> 0.777 基本就是重力前馈 <span><span>mglcos⁡0.1=0.4392mgl\cos 0.1 = 0.4392</span><span><span><span></span><span>m</span><span>g</span><span>l</span><span></span><span>cos</span><span></span><span>0.1</span><span></span><span>=</span><span></span></span><span><span></span><span>0.4392</span></span></span></span> 加比例项 <span><span>kp×0.1=0.3375k_p \times 0.1 = 0.3375</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>0.1</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3375</span></span></span></span>，合 0.7767，剩下 0.0003 是微分项和积分项。</p><p><strong>但我没能看到这一脚对响应本身有什么坏处。</strong> 超调是 5.48% 对 5.63%，上升时间 0.135 s 对 0.139 s，基本一样，蓝线甚至快一点点，我猜是力矩被夹在上限的那 9 拍把角度推得更快。它的代价在执行器上：一个短暂的满力矩冲击，实机上对应一次 CAN 上的大指令、齿轮的一次撞击。我没有量化这个代价，也不知道 GM6020 的内部限幅会不会先把它削掉。</p><p><strong><code>weight_b05</code> 是我想看“只看误差的一半”会怎样，结果出人意料地差。</strong> 超调是 0%，但 1.45 s 时角度才 0.0625 rad，离 0.1 rad 还差 37.5%，<code>rise</code> 是 NaN：它一直没到 90%。原因在 <span><span>Ti=kp/ki=5T_i = k_p/k_i = 5</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> s：<span><span>b=0.5b = 0.5</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span></span></span></span> 让比例项只吃一半设定值，平衡点由积分器去补，而积分器的时间常数在秒的量级。<span><span>b&lt;1b &lt; 1</span><span><span><span></span><span>b</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span> 要配一个快的积分才有意义，否则就是把响应拖慢。我不会在后面的章节里用 <span><span>b&lt;1b &lt; 1</span><span><span><span></span><span>b</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span>。</p><p><strong>第一拍的 prime。</strong> <code>report</code> 里有一个单独的检查：<span><span>c=1c = 1</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>r=0.1r = 0.1</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span>、<span><span>y=0y = 0</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 的情况下第一次调用 <code>update</code>，D 项是 0.000（不 prime 的话会是 10.433 N·m），而输出恰好是 <span><span>kp×0.1k_p \times 0.1</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>0.1</span></span></span></span>。这条是断言，不通过 <code>report</code> 会退出。</p></section><section><h3>微分滤波和量化抖动<a href="#微分滤波和量化抖动"><span>#</span></a></h3><p>同一个回路，<span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，换三个 <span><span>tft_f</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（0、2 ms、10 ms）。<code>report</code> 把有量化的实际输出和<strong>同一个控制器读精确角度</strong>时的理想输出做差，0.8 s 之后取 rms：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>            </span></span><span>case  rms(u - u_ideal)  overshoot  settle  D per quantum</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>dfilter_tf0           0.16672      6.26%  0.509s         0.2401</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>dfilter_tf0.002           0.04447      5.63%  0.509s         0.0800</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>dfilter_tf0.01           0.01221      3.28%  0.499s         0.0218</span></div></div><div><div><div>5</div></div><div><span>chatter ratio tf = 0 over tf = 0.01: 13.7x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch05-d-filter-noise.svg" alt="三种微分滤波时间常数下的力矩指令和编码器读数偏差：tf 为 0 时力矩有很大的毛刺，tf 为 10 ms 时几乎平滑" /><figcaption>三种微分滤波时间常数下的力矩指令和编码器读数偏差：tf 为 0 时力矩有很大的毛刺，tf 为 10 ms 时几乎平滑</figcaption></figure><p></p><p><strong><span><span>tft_f</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 0 加到 10 ms，量化造成的力矩 rms 从 0.16672 N·m 降到 0.01221 N·m，小 13.7 倍。</strong> 最后一列是前面“单个台阶”的预测值，0.2401、0.0800、0.0218，比值 11.0；实测 rms 的比值是 13.7，不完全一致，因为 rms 是很多个台阶叠加的结果，不是一个台阶的峰值，我没有进一步拆。<strong>滤波没有拖慢响应：</strong> 回稳 0.509、0.509、0.499 s，超调反而从 6.26% 降到 3.28%（我猜是滤掉了高频的同时，微分的有效相位超前少了一点，抑制了响应的尖峰，但没有验证）。这个回路上看不到滤波的代价，因为 <span><span>1/tf=1001/t_f = 100</span><span><span><span></span><span>1/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>100</span></span></span></span> rad/s 比 <span><span>ωn=15\omega_n = 15</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span> 高一个量级。</p></section><section><h3>重力前馈<a href="#重力前馈"><span>#</span></a></h3><p><span><span>r=0.3sin⁡(2π⋅0.5 t)r = 0.3\sin(2\pi\cdot 0.5\,t)</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.5</span><span></span><span>t</span><span>)</span></span></span></span>，3 s，同一个 PID（<span><span>b=1b = 1</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>tf=2t_f = 2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> ms）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m g l = 0.4414 N*m, J r'' amplitude = 0.0444 N*m</span></div></div><div><div><div>2</div></div><div><span><span>           </span></span><span>case    rms e   max|e|</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>ff_none   0.1151   0.2095</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>ff_gravity   0.0626   0.0911</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>ff_gravity_acc   0.0620   0.0904</span></div></div><div><div><div>6</div></div><div><span>gravity ff cuts the rms error by 1.84x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch05-gravity-ff.svg" alt="三种前馈下的俯仰角和跟踪误差：没有前馈时误差最大，约 0.21 rad；加重力前馈后降到 0.09 rad；再加加速度前馈几乎看不出差别" /><figcaption>三种前馈下的俯仰角和跟踪误差：没有前馈时误差最大，约 0.21 rad；加重力前馈后降到 0.09 rad；再加加速度前馈几乎看不出差别</figcaption></figure><p></p><p><strong>重力前馈把 rms 误差从 0.1151 rad 降到 0.0626 rad，缩小 1.84 倍，峰值从 0.2095 降到 0.0911。</strong> 加速度前馈 <span><span>Jr¨J\ddot r</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>r</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span></span></span></span> 把 rms 再降到 0.0620，只有 1%。它的幅值是重力的 10%，但剩下的 0.06 rad 误差主要不是加速度造成的：我猜它是速度项（<span><span>br˙b\dot r</span><span><span><span></span><span>b</span><span><span><span><span><span><span></span><span>r</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span> 和 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的有限带宽造成的相位滞后）和积分器太慢，这两样加速度前馈都不管。我没有试速度前馈，也没有把 <span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 调大来看，所以这只是个猜测。</p></section><section><h3>无扰动切换<a href="#无扰动切换"><span>#</span></a></h3><p>俯仰轴，设定值 0.1 rad，0.5 s 加 0.15 N·m 的恒定负载，重力前馈开着；1.0 s 把三个增益都乘 2（自瞄切换模式的意思）。积分器很慢（<span><span>Ti=5T_i = 5</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> s），到 1.0 s 时误差还有 0.0402 rad（表里最后一列），比例项承担了大部分负载，所以增益一翻倍输出就会跳：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>         </span></span><span>case  u before  u after      |du|  D before  e before</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>bumpless_off    0.5122   0.6745  1.62e-01   -0.0800    0.0402</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>bumpless_on    0.5122   0.5388  2.67e-02   -0.0800    0.0402</span></div></div><div><div><div>4</div></div><div><span>residual jump predicted from the D state: 2.67e-02</span></div></div><div><div><div>5</div></div><div><span>reset cuts the jump by 6.1x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch05-bumpless.svg" alt="切换增益前后的力矩指令和俯仰角：直接改增益时力矩从 0.512 跳到 0.675 N·m，之后角度被拉向目标；加 reset 的力矩只动了 0.027 N·m，角度几乎不动" /><figcaption>切换增益前后的力矩指令和俯仰角：直接改增益时力矩从 0.512 跳到 0.675 N·m，之后角度被拉向目标；加 reset 的力矩只动了 0.027 N·m，角度几乎不动</figcaption></figure><p></p><p><strong>直接改增益：力矩从 0.5122 跳到 0.6745 N·m，跳了 0.162 N·m。调用 <code>reset</code> 之后：0.5388，跳了 0.0267 N·m，小 6.1 倍。</strong> 剩下的 0.0267 就是前面预测的残差：切换前的 D 项是 <span><span>−0.0800-0.0800</span><span><span><span></span><span>−</span><span>0.0800</span></span></span></span> N·m，<span><span>∣d∣ Ts/(tf+Ts)=0.0800/3=0.0267|d|\,T_s/(t_f + T_s) = 0.0800/3 = 0.0267</span><span><span><span></span><span>∣</span><span>d</span><span>∣</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0800/3</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0267</span></span></span></span>。<code>report</code> 把这个预测和实测的差限制在 20% 以内，实际吻合到三位小数。</p><p>两条线之后的路径也很不一样（图下半）：直接改增益的角度被突然变大的力矩拉向目标，<code>bumpless_on</code> 的角度在这 0.15 s 里几乎不动，只是后面微分的量化抖动更明显了一些（图上半橙线后半段的尖峰，增益翻倍后 <span><span>kdk_d</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 也翻倍，同一个量化台阶造成的 D 项也翻倍）。我不会说 <code>reset</code> 是“更好”：它保证了输出连续，代价是这 0.15 s 里响应更钝，要不要这一脚取决于你是想要平滑还是想要快。</p></section><section><h3>没有按直觉发生的事<a href="#没有按直觉发生的事"><span>#</span></a></h3><p>有几件事和我动手前的直觉不一样，我把它们列在一起，每一条前面的小节里都有数据：</p><ul>
<li>LIMIT 是超调最低、回稳最快的一个，同时也是唯一一个留下稳态误差（0.79 rad/s）的，所以只看暂态指标会选错。</li>
<li>微分冲击把力矩夹在上限 9 拍，但超调和上升时间都没有变差，我没能量化它的坏处。</li>
<li><code>weight_b05</code> 在 <span><span>Ti=5T_i = 5</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> s 的回路里是慢的，不是我在教科书里读到的“减小超调的好办法”。</li>
<li>加速度前馈只让 rms 误差降了 1%，我对“剩下的误差是什么”只有猜测。</li>
<li>微分滤波在这个回路里没有任何代价，但这是因为 <span><span>ωn\omega_n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 低；回路更快时我估计会不一样，这一点我没测。</li>
<li><code>reset</code> 之后还剩 0.0267 N·m 的跳变，是 D 状态衰减，不是零。</li>
</ul></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在仓库根目录（<code>nix develop</code> 里）运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch05</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出一共一百来行，我只留下有代表性的几行：每个文件的对比结果是一行 <code>ok</code>，下面截取了 Rust 版里差值非零的三个文件和每种算例各一个；中间的指标表就是前面各小节贴过的那些，这里省略。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch05</span></div></div><div><div><div>2</div></div><div><span>ch05: C wrote 15 cases to out/ch05</span></div></div><div><div><div>3</div></div><div><span>ch05: C++ wrote 15 cases to out/ch05</span></div></div><div><div><div>4</div></div><div><span>ch05: Rust wrote 15 cases to out/ch05</span></div></div><div><div><div>5</div></div><div><span>ok   bumpless_off_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   bumpless_on_rs.csv: max diff 6.939e-18 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   dfilter_tf0_rs.csv: max diff 2.776e-17 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>ok   kick_c1_rs.csv: max diff 1.388e-17 &lt;= 1.0e-09</span></div></div><div><div><div>9</div></div><div><span>ok   windup_backcalc_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>(... 其余 40 个 ok 行，差值全是 0.000e+00 ...)</span></div></div><div><div><div>11</div></div><div><span>speed PI at zeta 0.7, wn 30.0: kp = 4.1667, ki = 90.0000</span></div></div><div><div><div>12</div></div><div><span>pitch PID at zeta 0.7, wn 15.0: kp = 3.3750, ki = 0.6750, kd = 0.3130</span></div></div><div><div><div>13</div></div><div><span>constructor rejects 4 of 4 invalid configs</span></div></div><div><div><div>14</div></div><div><span>first update with r = 0.1, y = 0, c = 1: D = 0.000 (unprimed would be 10.433 N*m)</span></div></div><div><div><div>15</div></div><div><span>(... 五张指标表 ...)</span></div></div><div><div><div>16</div></div><div><span>figure: figures/ch05/windup.svg</span></div></div><div><div><div>17</div></div><div><span>figure: figures/ch05/derivative-kick.svg</span></div></div><div><div><div>18</div></div><div><span>figure: figures/ch05/d-filter-noise.svg</span></div></div><div><div><div>19</div></div><div><span>figure: figures/ch05/gravity-ff.svg</span></div></div><div><div><div>20</div></div><div><span>figure: figures/ch05/bumpless.svg</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>ch05 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>45 个文件和 Python 的最大差值，绝大多数是 <code>0.000e+00</code>，逐位一致；有三个是 <span><span>10−1710^{-17}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>10−1810^{-18}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>18</span></span></span></span></span></span></span></span></span></span></span></span> 的量级（Rust 版的 <code>dfilter_tf0</code> 是 <span><span>2.8×10−172.8\times 10^{-17}</span><span><span><span></span><span>2.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>，<code>kick_c1</code> 是 <span><span>1.4×10−171.4\times 10^{-17}</span><span><span><span></span><span>1.4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>，<code>bumpless_on</code> 是 <span><span>6.9×10−186.9\times 10^{-18}</span><span><span><span></span><span>6.9</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>18</span></span></span></span></span></span></span></span></span></span></span></span>）。这三个都是俯仰轴的算例，我猜原因是 Rust 的 <code>cos</code> 来自 <code>libm</code> crate，C、C++ 和 Python 用的是 glibc，两个实现偶尔会差最后一位，我没有去核对。这一章的非线性（限幅、<code>round</code>、条件积分）都是比较和舍入，我担心过量化会在最后一位上翻一次台阶，结果没有，所以不需要任何算例的放宽容差。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>文件都在 <a href="https://github.com/bfmhno3/pid-lab/tree/tutorial-v1" target="_blank">pid-lab</a> 仓库的 <code>tutorial-v1</code> 标签里。下面的命令都在仓库根目录运行；<code>make -s ch05</code> 会依次生成 Python 参考输出、编译并运行三个原生程序，再做对比和画图，所以表里“结果”一列对应的是这一条命令的整体输出。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid.py" target="_blank"><code>python/pidlab/pid.py</code></a></td><td><code>make -s ch05</code></td><td>PID 库的 Python 参考实现，构造函数拒绝 4 种非法配置（4 of 4）</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a></td><td><code>make -s ch05</code></td><td>新增 <code>GimbalPitch</code> 和 <code>round_half_away</code>，编码器量化 <span><span>q=2π/8192q = 2\pi/8192</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>π</span><span>/8192</span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>python/pidlab/sim.py</code></a></td><td><code>make -s ch05</code></td><td>前几篇的 RK4 积分，这一章直接复用</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch05_practical.py" target="_blank"><code>python/ch05_practical.py</code></a></td><td><code>python3 python/ch05_practical.py ref</code>，<code>make -s ch05</code></td><td><code>ref</code> 写数据和 15 个 <code>_py.csv</code>；<code>report</code> 对比 45 个文件、断言 LIMIT 稳态误差公式与仿真都是 0.7937 rad/s，画 5 张图，最后打印 <code>ch05 OK</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/pid.h" target="_blank"><code>c/include/pl/pid.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid.c" target="_blank"><code>c/src/pid.c</code></a></td><td><code>make -s ch05</code></td><td>C 版 PID 库，15 个 <code>_c.csv</code> 全部 <code>ok</code>，差值 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/plants.h" target="_blank"><code>c/include/pl/plants.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/plants.c" target="_blank"><code>c/src/plants.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/real.h" target="_blank"><code>c/include/pl/real.h</code></a></td><td><code>make -s ch05</code></td><td><code>pl_gimbal_pitch</code> 和 <code>PL_ROUND</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid.h" target="_blank"><code>cpp/include/pidlab/pid.h</code></a></td><td><code>make -s ch05</code></td><td>C++ 版，<code>Pid&lt;T&gt;::Create</code>，15 个 <code>_cpp.csv</code> 全部 <code>ok</code>，差值 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/plants.h" target="_blank"><code>cpp/include/pidlab/plants.h</code></a></td><td><code>make -s ch05</code></td><td><code>GimbalPitch&lt;T&gt;</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid.rs" target="_blank"><code>rust/src/pid.rs</code></a></td><td><code>make -s ch05</code></td><td>Rust 版，<code>Pid::new</code>，15 个 <code>_rs.csv</code> 全部 <code>ok</code>，最大差值 <span><span>2.8×10−172.8\times 10^{-17}</span><span><span><span></span><span>2.8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/plants.rs" target="_blank"><code>rust/src/plants.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/real.rs" target="_blank"><code>rust/src/real.rs</code></a></td><td><code>make -s ch05</code></td><td><code>GimbalPitch</code>，<code>Real</code> 标量别名</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch05.c" target="_blank"><code>c/apps/ch05.c</code></a></td><td><code>make -s ch05</code></td><td>读 <code>data/</code>，写 <code>out/ch05/</code>，输出 <code>C wrote 15 cases</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch05.cc" target="_blank"><code>cpp/apps/ch05.cc</code></a></td><td><code>make -s ch05</code></td><td>输出 <code>C++ wrote 15 cases</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch05.rs" target="_blank"><code>rust/src/bin/ch05.rs</code></a></td><td><code>make -s ch05</code></td><td>输出 <code>Rust wrote 15 cases</code></td></tr><tr><td><code>figures/ch05/</code>（5 张 SVG，被忽略的构建产物）</td><td><code>make -s ch05</code></td><td><code>windup</code>、<code>derivative-kick</code>、<code>d-filter-noise</code>、<code>gravity-ff</code>、<code>bumpless</code>，同步到本站的 <code>ch05-*.svg</code></td></tr></tbody></table></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把 PID 的四个“坑”做成了数：不处理的积分在 40 rad/s 的阶跃上超调 77.4%，条件积分 2.2%，反算 2.5%，积分限幅 1.5% 却在 2.4 N·m 的负载下留下 0.79 rad/s 的稳态误差；D 看误差，0.1 rad 的设定值阶跃给出 10.43 N·m 的微分冲击；<span><span>tft_f</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 取 10 ms 让量化抖动小了 13.7 倍；重力前馈让正弦跟踪的 rms 误差缩小了 1.84 倍；<code>reset</code> 让增益切换的输出跳变小了 6.1 倍。四种语言的 45 个对比文件逐位一致或差在 <span><span>10−1710^{-17}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p><a href="/posts/pid-control-06-tuning/">第 6 篇：整定：Ziegler–Nichols、继电器自整定与 SIMC</a>会用这个 PID 库去回答“这些 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>kdk_d</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从哪来”：阶跃法拟合一阶加延迟模型，继电器法直接测出临界增益和周期，再用 Ziegler–Nichols 和 SIMC 两套规则算增益，放在摩擦轮和一个四阶对象上比较。评估用的就是这一篇的 <code>Pid</code>，<code>CLAMP</code> 模式，<span><span>tf=Td/10t_f = T_d/10</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/10</span></span></span></span>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-04-discretization/</id>
      <title type="text">离散化：位置式、增量式与采样率</title>
      <published>2026-10-16T03:00:00.000Z</published>
      <updated>2026-10-16T03:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-04-discretization/"/>
      <summary type="text">PID 控制系列第 4 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95 把位置式、增量式和三种 s 到 z 映射写了四遍，把采样周期从 1 ms 拉到 20 ms，相位裕度从 59.0° 掉到 -21.7°，回路在 20 ms 上失稳；限幅后的位置式积分器让超调到 29.2%、回到零要 5.07 s，增量式累加器限幅后只有 0.4% 和 0.53 s；Tustin 微分让输出的 rms 电流从 0.86 A 涨到 2.83 A。</summary>
      <content type="html"><![CDATA[<p>上一篇我手算了一个“半拍”：ZOH 让相位多滞后半个采样周期，这半拍恰好决定了延迟裕度的整数拍答案。那时采样周期是 1 ms，半拍小到几乎可以忽略。这一篇我把采样周期拉长，看它到底有多小：同一个 PI 速度环（<span><span>ωn=30\omega_n = 30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span>、<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>，加一拍传输延迟），采样周期取 1、5、10、20 ms，相位裕度依次是 59.0°、44.0°、24.0° 和 <span><span>−21.7-21.7</span><span><span><span></span><span>−</span><span>21.7</span></span></span></span>°，超调从 23.4% 一路涨到 70.8%，到 20 ms 回路干脆失稳。<strong>采样周期不是一个“无关紧要的实现细节”，它是回路里一个真实的延迟。</strong> 除了这条线，我还把位置式和增量式 PID 放在一起比：不限幅时两者的输出差 <span><span>4×10−154\times 10^{-15}</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，限幅时一个积分器卷绕到 1328 A，另一个只有 0.4% 的超调。最后是微分项的两种离散化：后向差分和 Tustin，同一份噪声下，Tustin 的电流 rms 是后向的 3.3 倍。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch04</code> 通过，<code>out/ch04/</code> 下 44 个 CSV（<code>ts_{1,5,10,20}ms</code>、<code>form_{pos,inc}</code>、<code>sat_{pos,inc,inc_raw}</code>、<code>d_{backward,tustin}</code> 共 11 个算例，每个 Python、C、C++、Rust 各一份），<code>figures/ch04/</code> 下 4 张图；以及 ZOH、三种 s 到 z 映射、滤波微分的离散化、位置式和增量式的等价与抗饱和的完整推导。
新增模块：Python 是 <code>pidlab/pid_discrete.py</code>；C 是 <code>pl/pid_discrete.h</code> 和 <code>pid_discrete.c</code>；C++ 是 <code>pidlab/pid_discrete.h</code>；Rust 是 <code>pid_discrete.rs</code>。
前置：<a href="/posts/pid-control-03-frequency-domain/">第 3 篇</a>。需要的数学：复指数、第 3 篇的相位裕度和延迟裕度；z 变换这一篇现推。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>采样、零阶保持和那个半拍<a href="#采样零阶保持和那个半拍"><span>#</span></a></h3><p>数字控制器每隔 <span><span>TsT_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 读一次传感器，算出 <span><span>uku_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，然后在下一个采样点之前让它保持不变：零阶保持（ZOH）。保持器的传递函数是</p><span><span><span>H(s)=1−e−sTssH(s) = \frac{1 - e^{-sT_s}}{s}</span><span><span><span></span><span>H</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>代入 <span><span>s=jωs = j\omega</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>j</span><span>ω</span></span></span></span>，把分子拆成 <span><span>e−jωTs/2e^{-j\omega T_s/2}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span></span></span></span> 乘一个正弦：</p><span><span><span>H(jω)=e−jωTs/2(ejωTs/2−e−jωTs/2)jω=Ts sin⁡(ωTs/2)ωTs/2 e−jωTs/2H(j\omega) = \frac{e^{-j\omega T_s/2}\left(e^{j\omega T_s/2} - e^{-j\omega T_s/2}\right)}{j\omega} = T_s\,\frac{\sin(\omega T_s/2)}{\omega T_s/2}\,e^{-j\omega T_s/2}</span><span><span><span></span><span>H</span><span>(</span><span>j</span><span>ω</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>j</span><span>ω</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span><span></span><span><span><span>(</span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>j</span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span><span><span>)</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>sin</span><span>(</span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span></span></span></span></span><p><strong>相位是精确的 <span><span>−ωTs/2-\omega T_s/2</span><span><span><span></span><span>−</span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span>，幅值是 <span><span>TsT_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 乘一个 sinc。</strong> 所以保持器在相位上就是一个半拍的纯延迟，没有近似。幅值那一项在回路的穿越频率 <span><span>ωc≈46\omega_c \approx 46</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>46</span></span></span></span> rad/s、<span><span>Ts=1T_s = 1</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> ms 处是 <span><span>sinc(0.023)≈0.9999\text{sinc}(0.023) \approx 0.9999</span><span><span><span></span><span><span>sinc</span></span><span>(</span><span>0.023</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>0.9999</span></span></span></span>，可以忽略。我的仿真里命令从算出来到进入电机还要多过一拍（<code>DelayLine(1)</code>，第 3 篇的 <code>TRANSPORT</code>），所以回路的有效延迟是</p><span><span><span>θ=1.5 Ts,ΔPM≈−ωc⋅1.5 Ts⋅180π 度\theta = 1.5\,T_s, \qquad \Delta\text{PM} \approx -\omega_c \cdot 1.5\,T_s \cdot \frac{180}{\pi}\ \text{度}</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.5</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>Δ</span><span><span>PM</span></span><span></span><span>≈</span><span></span></span><span><span></span><span>−</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>1.5</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>π</span></span></span><span><span></span><span></span></span><span><span></span><span><span>180</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span> </span><span><span>度</span></span></span></span></span></span><p>这是我要检验的第一个预测：用无延迟回路的 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>1.5Ts1.5T_s</span><span><span><span></span><span>1.5</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 估计相位裕度的损失，再和精确的采样模型对一对。</p></section><section><h3><span><span>z=esTsz = e^{sT_s}</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span> 和三种近似<a href="#zestsz--est_szests-和三种近似"><span>#</span></a></h3><p>连续系统的极点 <span><span>s=ps = p</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>p</span></span></span></span>，采样后变成离散系统的极点 <span><span>z=epTsz = e^{pT_s}</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>p</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>。这是精确的对应，但控制器里的 <span><span>1/s1/s</span><span><span><span></span><span>1/</span><span>s</span></span></span></span>（积分）和 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span>（微分）要用 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 的有理函数实现，而 <span><span>s=ln⁡z/Tss = \ln z / T_s</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>ln</span><span></span><span>z</span><span>/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 不是有理函数。所以要近似。三种常见的近似，对应 <span><span>esTse^{sT_s}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span> 的三种展开：</p><ul>
<li><strong>前向欧拉：</strong> <span><span>z=esTs≈1+sTsz = e^{sT_s} \approx 1 + sT_s</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，即 <span><span>s≈z−1Tss \approx \dfrac{z - 1}{T_s}</span><span><span><span></span><span>s</span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。</li>
<li><strong>后向欧拉：</strong> <span><span>z−1=e−sTs≈1−sTsz^{-1} = e^{-sT_s} \approx 1 - sT_s</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，即 <span><span>s≈z−1Tszs \approx \dfrac{z - 1}{T_s z}</span><span><span><span></span><span>s</span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span></span></span><span><span></span><span></span></span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。</li>
<li><strong>Tustin（双线性）：</strong> <span><span>esTs=esTs/2e−sTs/2≈1+sTs/21−sTs/2e^{sT_s} = \dfrac{e^{sT_s/2}}{e^{-sT_s/2}} \approx \dfrac{1 + sT_s/2}{1 - sT_s/2}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，即 <span><span>s≈2Tsz−1z+1s \approx \dfrac{2}{T_s}\dfrac{z - 1}{z + 1}</span><span><span><span></span><span>s</span><span></span><span>≈</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>z</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>。</li>
</ul><p>用它们离散积分器 <span><span>I(s)=ki/sI(s) = k_i/s</span><span><span><span></span><span>I</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>s</span></span></span></span>，得到三条递推：</p>

<table><thead><tr><th>方法</th><th><span><span>I(z)I(z)</span><span><span><span></span><span>I</span><span>(</span><span>z</span><span>)</span></span></span></span></th><th>递推</th></tr></thead><tbody><tr><td>前向</td><td><span><span>kiTs/(z−1)k_i T_s/(z - 1)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span></td><td><span><span>Ik=Ik−1+kiTsek−1I_k = I_{k-1} + k_i T_s e_{k-1}</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td></tr><tr><td>后向</td><td><span><span>kiTsz/(z−1)k_i T_s z/(z - 1)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span><span>/</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span></td><td><span><span>Ik=Ik−1+kiTsekI_k = I_{k-1} + k_i T_s e_k</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td></tr><tr><td>Tustin</td><td><span><span>kiTs2z+1z−1\dfrac{k_i T_s}{2}\dfrac{z + 1}{z - 1}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>z</span><span></span><span>+</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td><span><span>Ik=Ik−1+kiTs2(ek+ek−1)I_k = I_{k-1} + \dfrac{k_i T_s}{2}(e_k + e_{k-1})</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></td></tr></tbody></table><p>我在第 1 篇写的 <code>PidBasic</code>（先 <code>integral += e * ts</code> 再输出）就是后向那一行。区别只在于 <span><span>IkI_k</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 里有没有包含当前这一拍的误差。</p><p><strong>三种近似在频率响应上的差别。</strong> 取 <span><span>z=ejθz = e^{j\theta}</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>j</span><span>θ</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>θ=ωTs\theta = \omega T_s</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，把三个积分器的 <span><span>1/(jω)1/(j\omega)</span><span><span><span></span><span>1/</span><span>(</span><span>j</span><span>ω</span><span>)</span></span></span></span> 提出来：</p><span><span><span>前向：Tsz−1=1jω⋅θ/2sin⁡(θ/2) e−jθ/2后向：Tszz−1=1jω⋅θ/2sin⁡(θ/2) e+jθ/2Tustin：Ts2z+1z−1=1jω⋅θ/2tan⁡(θ/2)\begin{aligned}
\text{前向：}&amp;\quad \frac{T_s}{z - 1} = \frac{1}{j\omega}\cdot\frac{\theta/2}{\sin(\theta/2)}\,e^{-j\theta/2}\\[2pt]
\text{后向：}&amp;\quad \frac{T_s z}{z - 1} = \frac{1}{j\omega}\cdot\frac{\theta/2}{\sin(\theta/2)}\,e^{+j\theta/2}\\[2pt]
\text{Tustin：}&amp;\quad \frac{T_s}{2}\frac{z + 1}{z - 1} = \frac{1}{j\omega}\cdot\frac{\theta/2}{\tan(\theta/2)}
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span>前向：</span></span></span></span><span><span></span><span><span><span>后向：</span></span></span></span><span><span></span><span><span><span>Tustin</span><span>：</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>j</span><span>ω</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>sin</span><span>(</span><span>θ</span><span>/2</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>θ</span><span>/2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>θ</span><span>/2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>j</span><span>ω</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>sin</span><span>(</span><span>θ</span><span>/2</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>θ</span><span>/2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>+</span><span>j</span><span>θ</span><span>/2</span></span></span></span></span></span></span></span></span></span></span><span><span></span><span><span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>z</span><span></span><span>+</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>j</span><span>ω</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>tan</span><span>(</span><span>θ</span><span>/2</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>θ</span><span>/2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>前向多出半拍滞后，后向多出半拍超前，Tustin 的相位是精确的 <span><span>−90°-90°</span><span><span><span></span><span>−</span><span>90°</span></span></span></span>，只有幅值有一点偏差。这半拍和 ZOH 的半拍符号相反：后向的积分器刚好抵消一部分保持器的滞后，前向则叠加上去。我猜这就是 <code>PidBasic</code> 那种写法在 1 kHz 下“怎么都能用”的一半原因，另一半是 1 ms 本来就很小。</p><p><strong>稳定区域。</strong> <span><span>z=esTsz = e^{sT_s}</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span> 把左半平面映到单位圆内。三种近似各自的“映到单位圆内的 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 区域”是：</p><ul>
<li>前向：<span><span>∣1+sTs∣&lt;1|1 + sT_s| &lt; 1</span><span><span><span></span><span>∣1</span><span></span><span>+</span><span></span></span><span><span></span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span>，是以 <span><span>−1/Ts-1/T_s</span><span><span><span></span><span>−</span><span>1/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为圆心、<span><span>1/Ts1/T_s</span><span><span><span></span><span>1/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为半径的圆盘。<strong>一个稳定的连续极点落在圆外，离散化之后会变成不稳定。</strong></li>
<li>后向：<span><span>∣1−sTs∣&gt;1|1 - sT_s| &gt; 1</span><span><span><span></span><span>∣1</span><span></span><span>−</span><span></span></span><span><span></span><span>s</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>1</span></span></span></span>，是以 <span><span>+1/Ts+1/T_s</span><span><span><span></span><span>+</span><span>1/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为圆心的圆的外部。<strong>一个不稳定的连续极点落在圆内，离散化之后反而稳定。</strong></li>
<li>Tustin：恰好是左半平面，稳定性一一对应。代价是频率轴被压缩，模拟频率 <span><span>ωa\omega_a</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和数字频率 <span><span>Ω\Omega</span><span><span><span></span><span>Ω</span></span></span></span> 的关系是 <span><span>ωa=2Tstan⁡ΩTs2\omega_a = \dfrac{2}{T_s}\tan\dfrac{\Omega T_s}{2}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>tan</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>Ω</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，模拟的无穷远被折到 Nyquist 频率。</li>
</ul><p></p><figure><img src="/assets/images/pid-control/ch04-s-to-z.svg" alt="三种映射在 z 平面上的像和各自映到单位圆内的 s 区域：前向是一条竖线和左边的圆盘，后向是小圆和右边圆的外部，Tustin 精确对应左半平面" /><figcaption>三种映射在 z 平面上的像和各自映到单位圆内的 s 区域：前向是一条竖线和左边的圆盘，后向是小圆和右边圆的外部，Tustin 精确对应左半平面</figcaption></figure><p></p><p>左图是虚轴 <span><span>s=jωs = j\omega</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>j</span><span>ω</span></span></span></span> 在三种映射下的像：前向是 <span><span>Re z=1\text{Re}\,z = 1</span><span><span><span></span><span><span>Re</span></span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 这条竖线，永远在单位圆外；后向是以 0.5 为圆心的小圆，完全在单位圆内；Tustin 精确地落在单位圆上。右图是反过来看，哪些 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 被映进了单位圆。最后这个区别后面会直接用到：Tustin 把 <span><span>s→−∞s \to -\infty</span><span><span><span></span><span>s</span><span></span><span>→</span><span></span></span><span><span></span><span>−</span><span>∞</span></span></span></span> 映到 <span><span>z=−1z = -1</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>。</p></section><section><h3>滤波微分的离散化<a href="#滤波微分的离散化"><span>#</span></a></h3><p>纯微分 <span><span>KdsK_d s</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span></span></span></span> 在高频增益无穷大，实际一定要接一个一阶滤波，<span><span>D(s)=Kds/(tfs+1)D(s) = K_d s/(t_f s + 1)</span><span><span><span></span><span>D</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>。分别代入后向和 Tustin。</p><p><strong>后向：</strong> <span><span>s=z−1Tszs = \dfrac{z - 1}{T_s z}</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span></span></span><span><span></span><span></span></span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，</p><span><span><span>D(z)=Kd(z−1)tf(z−1)+Tsz=Kd(z−1)(tf+Ts)z−tfD(z) = \frac{K_d (z - 1)}{t_f (z - 1) + T_s z} = \frac{K_d (z - 1)}{(t_f + T_s) z - t_f}</span><span><span><span></span><span>D</span><span>(</span><span>z</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>z</span><span></span><span>−</span><span></span><span>1</span><span>)</span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>z</span><span></span><span>−</span><span></span><span>1</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>z</span><span></span><span>−</span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>z</span><span></span><span>−</span><span></span><span>1</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>分子分母同除以 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>，交叉相乘，得到递推</p><span><span><span>dk=tftf+Ts dk−1+Kdtf+Ts (ek−ek−1)d_k = \frac{t_f}{t_f + T_s}\,d_{k-1} + \frac{K_d}{t_f + T_s}\,(e_k - e_{k-1})</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p><strong>Tustin：</strong> <span><span>s=2Tsz−1z+1s = \dfrac{2}{T_s}\dfrac{z - 1}{z + 1}</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>z</span><span></span><span>+</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>z</span><span></span><span>−</span><span></span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，分子分母同乘 <span><span>Ts(z+1)T_s(z + 1)</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>z</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>：</p><span><span><span>D(z)=2Kd(z−1)(2tf+Ts)z−(2tf−Ts)D(z) = \frac{2K_d (z - 1)}{(2t_f + T_s) z - (2t_f - T_s)}</span><span><span><span></span><span>D</span><span>(</span><span>z</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>z</span><span></span><span>−</span><span></span><span>(</span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>z</span><span></span><span>−</span><span></span><span>1</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><span><span><span>dk=2tf−Ts2tf+Ts dk−1+2Kd2tf+Ts (ek−ek−1)d_k = \frac{2t_f - T_s}{2t_f + T_s}\,d_{k-1} + \frac{2K_d}{2t_f + T_s}\,(e_k - e_{k-1})</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>两条递推的形状一样，<span><span>dk=a dk−1+b (ek−ek−1)d_k = a\,d_{k-1} + b\,(e_k - e_{k-1})</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，区别只在极点 <span><span>aa</span><span><span><span></span><span>a</span></span></span></span> 和增益 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span>。连续滤波器的极点在 <span><span>s=−1/tfs = -1/t_f</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，精确的离散极点是 <span><span>e−Ts/tfe^{-T_s/t_f}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>；两种近似给出的是</p><span><span><span>a后向=tftf+Ts,aTustin=2tf−Ts2tf+Tsa_{\text{后向}} = \frac{t_f}{t_f + T_s}, \qquad a_{\text{Tustin}} = \frac{2t_f - T_s}{2t_f + T_s}</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>后向</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span><span><span>Tustin</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>取 <span><span>tf=0.1 Tst_f = 0.1\,T_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（滤波器比采样还快，我选它是因为这是“只想去掉一点点毛刺”时最容易写出来的设置），精确值是 <span><span>e−10=4.5×10−5e^{-10} = 4.5\times 10^{-5}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span>，后向给 <span><span>0.0910.091</span><span><span><span></span><span>0.091</span></span></span></span>，Tustin 给 <span><span>−0.667-0.667</span><span><span><span></span><span>−</span><span>0.667</span></span></span></span>。<strong>后向的极点是正的，误差落在“还是一个平滑的衰减”那一侧；Tustin 的极点是负的，每一拍符号翻转。</strong> 前向欧拉在这里是 <span><span>z=1−Ts/tf=−9z = 1 - T_s/t_f = -9</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>9</span></span></span></span>，在单位圆外，所以我的实现直接拒绝 <code>FORWARD</code> 作为微分方法。</p><p>Tustin 为什么会这样：它把 <span><span>s→−∞s \to -\infty</span><span><span><span></span><span>s</span><span></span><span>→</span><span></span></span><span><span></span><span>−</span><span>∞</span></span></span></span>（滤波器的高频极限）映到 <span><span>z=−1z = -1</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span>，正好是 Nyquist 点。采样把快速旋转的车轮拍成倒转，这是同一类现象（电影里的 wagon-wheel 错觉）：映射把一个“快得没法表示”的模式折叠到 Nyquist 频率。在 <span><span>z=−1z = -1</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span></span></span></span> 处两个滤波器的增益是</p><span><span><span>∣D后向(−1)∣=2Kd2tf+Ts,∣DTustin(−1)∣=Kdtf|D_{\text{后向}}(-1)| = \frac{2K_d}{2t_f + T_s}, \qquad |D_{\text{Tustin}}(-1)| = \frac{K_d}{t_f}</span><span><span><span></span><span>∣</span><span><span>D</span><span><span><span><span><span><span></span><span><span><span><span>后向</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>−</span><span>1</span><span>)</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span>∣</span><span><span>D</span><span><span><span><span><span><span></span><span><span><span><span>Tustin</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>−</span><span>1</span><span>)</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>比值是 <span><span>(2tf+Ts)/(2tf)=6(2t_f + T_s)/(2t_f) = 6</span><span><span><span></span><span>(</span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span>(</span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>6</span></span></span></span>。这是第二个预测：同样的噪声，Tustin 的 D 项应该在 Nyquist 附近放大得更多，并且符号交替。</p></section><section><h3>位置式和增量式<a href="#位置式和增量式"><span>#</span></a></h3><p><strong>位置式</strong>保存积分，每拍直接算出输出：</p><span><span><span>uk=kpek+Ik+dk,Ik=Ik−1+ΔIku_k = k_p e_k + I_k + d_k, \qquad I_k = I_{k-1} + \Delta I_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>其中 <span><span>ΔIk\Delta I_k</span><span><span><span></span><span>Δ</span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是上一节表里右边的增量（前向、后向或 Tustin）。<strong>增量式</strong>保存的是上一拍的输出，每拍只算变化量：</p><span><span><span>uk=uk−1+kp(ek−ek−1)+ΔIk+(dk−dk−1)u_k = u_{k-1} + k_p (e_k - e_{k-1}) + \Delta I_k + (d_k - d_{k-1})</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>两者在没有限幅时等价：把位置式写成 <span><span>uk−uk−1u_k - u_{k-1}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>kpekk_p e_k</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的差分是 <span><span>kp(ek−ek−1)k_p(e_k - e_{k-1})</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>Ik−Ik−1=ΔIkI_k - I_{k-1} = \Delta I_k</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Δ</span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>dk−dk−1d_k - d_{k-1}</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 原样保留，就是增量式的右边。起点也要一致：我让 <span><span>e−1=0e_{-1} = 0</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>d−1=0d_{-1} = 0</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>u−1=0u_{-1} = 0</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以没有“第一拍不算微分”的特殊处理（第 5 篇的最终 PID 才会加上）。</p><p><strong>限幅时它们就不等价了，原因是各自的状态不同。</strong> 位置式有两个量：积分 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 和被限幅的输出 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>。限幅只动 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>，<span><span>II</span><span><span><span></span><span>I</span></span></span></span> 照样累加，账本和钱包就对不上了：执行器只能给 5 A，账上却记着 1300 A。增量式只有一个量，上一拍的输出 <span><span>uk−1u_{k-1}</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，限幅作用在它身上，状态本身就被夹住了。</p><p><strong>但这个好处依赖一个条件：限幅要加在累加器上。</strong> 如果增量式只把“要发给电机的值”限幅，累加器仍然是无限制地累加，那它在数学上就是位置式，一样卷绕。我会跑这个算例来验证（<code>sat_inc_raw</code>）。</p><p><strong>限幅后的增量式到底是什么？</strong> 令 <span><span>Ik:=uk−kpek−dkI_k := u_k - k_p e_k - d_k</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>:=</span><span></span></span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（把输出里不属于积分的部分减掉）。不限幅时代入增量式，<span><span>Ik=Ik−1+ΔIkI_k = I_{k-1} + \Delta I_k</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，就是位置式的积分。限幅时 <span><span>uku_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 被夹在边界上，<span><span>IkI_k</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 被重新定义成“让输出刚好等于边界的那个值”。设位置式的未限幅输出是 <span><span>vk=kpek+Ik−1+ΔIk+dkv_k = k_p e_k + I_{k-1} + \Delta I_k + d_k</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，那么</p><span><span><span>Ik=uk−kpek−dk=Ik−1+ΔIk+(uk−vk)I_k = u_k - k_p e_k - d_k = I_{k-1} + \Delta I_k + (u_k - v_k)</span><span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span><span><span>I</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>这正是<strong>反算抗饱和（back-calculation）</strong>，跟踪时间常数取 <span><span>Tt=TsT_t = T_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，也就是一个采样周期就把积分拉回去。第 5 篇会把 <span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做成可调参数；这里我先在 <code>report</code> 里验证这个等价（下面“限幅”一节）。</p></section><section><h3>ts 为什么必须显式出现<a href="#ts-为什么必须显式出现"><span>#</span></a></h3><p>连续的 <span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的量纲是 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 除以时间，<span><span>kdk_d</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 乘时间。离散化之后每拍加的是 <span><span>kiTsekk_i T_s e_k</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，微分是 <span><span>kd(ek−ek−1)/Tsk_d (e_k - e_{k-1})/T_s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。据我所知，不少 RM 开源代码把 <span><span>TsT_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 直接折进增益里：<code>ki_code = ki * ts</code>、<code>kd_code = kd / ts</code>，循环里只写 <code>integral += ki_code * e</code>。这在固定采样率下完全正确，但这样做之后，<strong><span><span>TsT_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 变了代码里的增益不会跟着变</strong>。比如 <span><span>ki=90k_i = 90</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>90</span></span></span></span>，从 1 ms 改到 5 ms，如果 <code>ki_code</code> 还是 <span><span>90×0.00190 \times 0.001</span><span><span><span></span><span>90</span><span></span><span>×</span><span></span></span><span><span></span><span>0.001</span></span></span></span>，等效积分增益就是 <span><span>0.09/0.005=180.09/0.005 = 18</span><span><span><span></span><span>0.09/0.005</span><span></span><span>=</span><span></span></span><span><span></span><span>18</span></span></span></span>，只剩 1/5。我没有逐个翻那些仓库，这一条是从常见写法推的，不是审计结论。我自己的做法是把 <code>ts</code> 放进配置、让 <code>update</code> 每拍用 <code>ts</code> 乘，这样改采样率只改一个数。</p></section></section>
<section><h2>写进代码<a href="#写进代码"><span>#</span></a></h2><section><h3>Python：一个 <code>update</code>，三个开关<a href="#python一个-update三个开关"><span>#</span></a></h3><p>模块是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid_discrete.py" target="_blank"><code>pidlab/pid_discrete.py</code></a>。我先把上面的三个选择做成配置里的三个独立字段，而不是十二个类：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>Form</span><span>(</span><span>Enum</span><span>)</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>POSITIONAL</span><span> </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>INCREMENTAL</span><span> </span><span>=</span><span> </span><span>1</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>class</span><span><span> </span><span>Method</span><span>(</span><span>Enum</span><span>)</span></span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>FORWARD</span><span> </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>BACKWARD</span><span> </span><span>=</span><span> </span><span>1</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>TUSTIN</span><span> </span><span>=</span><span> </span><span>2</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>@dataclass</span></div></div><div><div><div>13</div></div><div><span>class</span><span><span> </span><span>DiscreteConfig</span></span><span>:</span></div></div><div><div><div>14</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>kp: </span><span>float</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>ki: </span><span>float</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>kd: </span><span>float</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>ts: </span><span>float</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>tf: </span><span>float</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>u_min: </span><span>float</span><span><span> </span><span>=</span><span> </span><span>-</span><span>math.inf</span></span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>u_max: </span><span>float</span><span><span> </span><span>=</span><span> math.inf</span></span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>form: Form </span><span>=</span><span> Form.</span><span>POSITIONAL</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>i_method: Method </span><span>=</span><span> Method.</span><span>BACKWARD</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>d_method: Method </span><span>=</span><span> Method.</span><span>BACKWARD</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三种积分、两种微分、两种形式，一共 12 个组合，每个组合是 <code>update</code> 里的一个分支。构造函数拒绝五种配置：<code>ts &lt;= 0</code>、<code>tf &lt; 0</code>、<code>u_min &gt; u_max</code>，还有两条针对微分的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>if</span><span> cfg.d_method </span><span>is</span><span><span> Method.</span><span>FORWARD</span><span>:</span></span></div></div><div><div><div>2</div></div><div><span>            </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"derivative has no forward method"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>        </span><span>if</span><span> cfg.d_method </span><span>is</span><span><span> Method.</span><span>TUSTIN</span><span> </span></span><span>and</span><span><span> cfg.tf </span><span>&lt;=</span><span> </span></span><span>0.0</span><span>:</span></div></div><div><div><div>4</div></div><div><span>            </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"Tustin derivative needs tf &gt; 0"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前者是上面算出来的 <span><span>z=−9z = -9</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>9</span></span></span></span>，后者是 <span><span>tf=0t_f = 0</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时 Tustin 的极点落在 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>，每拍翻符号永不衰减。</p><p>核心是 <code>update</code>。<strong>先算微分的递推，再算积分增量 <code>di</code>，最后按形式拼出输出：</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>r</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>y</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> </span><span>self</span><span>.cfg</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>e </span><span>=</span><span> r </span><span>-</span><span> y</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>de </span><span>=</span><span> e </span><span>-</span><span> </span><span>self</span><span>.e_prev</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>d_old </span><span>=</span><span> </span><span>self</span><span>.d_state</span></div></div><div><div><div>6</div></div><div><span>        </span><span>if</span><span> c.d_method </span><span>is</span><span><span> Method.</span><span>BACKWARD</span><span>:</span></span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>a </span><span>=</span><span> c.tf </span><span>/</span><span> (c.tf </span><span>+</span><span> c.ts)</span></div></div><div><div><div>8</div></div><div><span>            </span><span>self</span><span><span>.d_state </span><span>=</span><span> a </span><span>*</span><span> d_old </span><span>+</span><span> c.kd </span><span>/</span><span> (c.tf </span><span>+</span><span> c.ts) </span><span>*</span><span> de</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>a </span><span>=</span><span> (</span><span>2.0</span><span><span> </span><span>*</span><span> c.tf </span><span>-</span><span> c.ts) </span><span>/</span><span> (</span></span><span>2.0</span><span><span> </span><span>*</span><span> c.tf </span><span>+</span><span> c.ts)</span></span></div></div><div><div><div>11</div></div><div><span>            </span><span>self</span><span><span>.d_state </span><span>=</span><span> a </span><span>*</span><span> d_old </span><span>+</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> c.kd </span><span>/</span><span> (</span></span><span>2.0</span><span><span> </span><span>*</span><span> c.tf </span><span>+</span><span> c.ts) </span><span>*</span><span> de</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span> c.i_method </span><span>is</span><span><span> Method.</span><span>FORWARD</span><span>:</span></span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>di </span><span>=</span><span> c.ki </span><span>*</span><span> c.ts </span><span>*</span><span> </span><span>self</span><span>.e_prev</span></div></div><div><div><div>14</div></div><div><span>        </span><span>elif</span><span> c.i_method </span><span>is</span><span><span> Method.</span><span>BACKWARD</span><span>:</span></span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>di </span><span>=</span><span> c.ki </span><span>*</span><span> c.ts </span><span>*</span><span> e</span></div></div><div><div><div>16</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>di </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> c.ki </span><span>*</span><span> c.ts </span><span>*</span><span> (e </span><span>+</span><span> </span></span><span>self</span><span>.e_prev)</span></div></div><div><div><div>18</div></div><div><span>        </span><span>if</span><span> c.form </span><span>is</span><span><span> Form.</span><span>POSITIONAL</span><span>:</span></span></div></div><div><div><div>19</div></div><div><span>            </span><span>self</span><span><span>.integral </span><span>+=</span><span> di</span></span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>v </span><span>=</span><span> c.kp </span><span>*</span><span> e </span><span>+</span><span> </span><span>self</span><span><span>.integral </span><span>+</span><span> </span></span><span>self</span><span>.d_state</span></div></div><div><div><div>21</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>v </span><span>=</span><span> </span><span>self</span><span><span>.u_prev </span><span>+</span><span> c.kp </span><span>*</span><span> de </span><span>+</span><span> di </span><span>+</span><span> (</span></span><span>self</span><span><span>.d_state </span><span>-</span><span> d_old)</span></span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>clamp</span><span>(v, c.u_min, c.u_max)</span></div></div><div><div><div>24</div></div><div><span>        </span><span>self</span><span><span>.u_prev </span><span>=</span><span> u</span></span></div></div><div><div><div>25</div></div><div><span>        </span><span>self</span><span><span>.e_prev </span><span>=</span><span> e</span></span></div></div><div><div><div>26</div></div><div><span>        </span><span>return</span><span> u</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这段和数学一节的公式是一一对应的：<code>d_state</code> 是 <span><span>dkd_k</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<code>di</code> 是前面表里右边的增量，两个分支就是位置式和增量式。注意两处：</p><ul>
<li><strong><code>u_min</code>、<code>u_max</code> 的含义随形式而变。</strong> 位置式里它们夹的是输出 <code>v</code>，<code>integral</code> 不受影响；增量式里 <code>u_prev</code> 存的就是夹过的输出，所以夹的是累加器。<code>self.u_prev = u</code> 在位置式里没有用到，写上它是为了让两种形式共用一个出口。</li>
<li><strong>没有“第一拍不算微分”。</strong> 第 1 篇的 <code>PidBasic</code> 有 <code>primed</code> 标志，第一拍的微分为 0。这里 <code>e_prev</code> 从 0 开始，第一拍看到的是一个从 0 跳到 <span><span>e0e_0</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的阶跃，微分会踢一脚（后面 Tustin 的算例里这一脚是 11.1 A）。我这么做是为了让位置式和增量式从第 0 拍起逐位一致；第 5 篇的 PID 会处理微分冲击。</li>
</ul></section><section><h3>仿真循环和精确的采样模型<a href="#仿真循环和精确的采样模型"><span>#</span></a></h3><p>驱动 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch04_discretization.py" target="_blank"><code>python/ch04_discretization.py</code></a> 的仿真循环和第 3 篇的差别只有两处：控制器换成 <code>PidDiscrete</code>，以及一个只在 <code>report</code> 里用的 <code>back_calc</code> 开关，用来验证“限幅增量式等价于反算”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>len</span><span>(data[</span><span>"t"</span><span>])):</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> data[</span><span>"r"</span><span>][k]</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> plant.</span><span>output</span><span>(x) </span><span>+</span><span> data[</span><span>"n"</span><span>][k]</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> pid.</span><span>update</span><span>(r, y)</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>ua </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span><span>(u, </span><span>-</span><span>case.act_limit), case.act_limit)</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>if</span><span> back_calc:</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>pid.integral </span><span>+=</span><span> ua </span><span>-</span><span> u</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>([data[</span><span>"t"</span><span>][k], r, y, u, ua, pid.d_state])</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>advance</span><span>(</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>plant.deriv, x, [delay.</span><span>push</span><span>(ua), data[</span><span>"d"</span><span><span>][k]], ts, </span><span>SUBSTEPS</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>u</code> 是控制器的输出，<code>ua</code> 是夹过执行器限幅后真正发给电机的命令，两列都写进 CSV。<code>back_calc</code> 那一行就是教科书的反算：把被夹掉的 <code>ua - u</code> 加回积分。</p><p>要回答“相位裕度到底掉了多少”，只用连续模型加延迟是近似。<code>sampled_pm</code> 把电机（连同电流环）用 <code>scipy.linalg.expm</code> 做精确的 ZOH 离散化，控制器用 <span><span>C(z)=kp+kiTsz/(z−1)C(z) = k_p + k_i T_s z/(z-1)</span><span><span><span></span><span>C</span><span>(</span><span>z</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span><span>/</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>（后向积分），再乘一拍延迟 <span><span>z−1z^{-1}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>em </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>em[:</span><span>2</span><span>, :</span><span>2</span><span><span>] </span><span>=</span><span> a</span></span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>em[:</span><span>2</span><span>, </span><span>2</span><span><span>] </span><span>=</span><span> bv</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> </span><span>expm</span><span>(em </span><span>*</span><span> ts)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ad, bd </span><span>=</span><span> e[:</span><span>2</span><span>, :</span><span>2</span><span>], e[:</span><span>2</span><span>, </span><span>2</span><span>]</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>linspace</span><span>(</span><span>1.0</span><span><span>, math.pi </span><span>/</span><span> ts </span><span>*</span><span> </span></span><span>0.999</span><span>, </span><span>20000</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> np.</span><span>exp</span><span>(</span><span>1</span><span>j</span><span><span> </span><span>*</span><span> w </span><span>*</span><span> ts)</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> np.</span><span>empty_like</span><span>(z)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> i, zi </span><span>in</span><span> </span><span>enumerate</span><span>(z):</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>g[i] </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(zi </span><span>*</span><span> np.</span><span>eye</span><span>(</span><span>2</span><span><span>) </span><span>-</span><span> ad, bd)[</span></span><span>0</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>lz </span><span>=</span><span> (kp </span><span>+</span><span> ki </span><span>*</span><span> ts </span><span>*</span><span> z </span><span>/</span><span> (z </span><span>-</span><span> </span><span>1.0</span><span><span>)) </span><span>*</span><span> g </span><span>/</span><span> z</span><span>**</span><span>TRANSPORT</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>expm</code> 作用在增广矩阵 <span><span>[AB00]Ts\begin{bmatrix}A &amp; B\\0 &amp; 0\end{bmatrix}T_s</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>A</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>B</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 上，左上角是 <span><span>Ad=eATsA_d = e^{AT_s}</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>，右上角是 <span><span>BdB_d</span><span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，这是 ZOH 离散化的标准技巧。然后在 <span><span>[1, 0.999π/Ts][1,\ 0.999\pi/T_s]</span><span><span><span></span><span>[</span><span>1</span><span>,</span><span> </span><span></span><span>0.999</span><span>π</span><span>/</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span> 上扫频率，在 <span><span>∣L∣|L|</span><span><span><span></span><span>∣</span><span>L</span><span>∣</span></span></span></span> 穿过 1 的那个区间里线性插值出 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和相位裕度。另一个函数 <code>sampled_radius</code> 把同一个回路写成四维状态（角速度、电流、积分器、在途的命令）的离散状态矩阵，取特征值的最大模，大于 1 就是不稳定。</p><p>这两个函数是分析用的，不是被检验的对象：它们只用 scipy 的 <code>expm</code> 和 numpy 的特征值，C、C++、Rust 不需要重写。<code>report</code> 里的对比表、抗饱和等价性验证、D 项统计和 4 张图都在同一个驱动里。</p></section><section><h3>C、C++、Rust：逐行翻译，差别在边角<a href="#ccrust逐行翻译差别在边角"><span>#</span></a></h3><p>三个版本是 Python 版的逐行翻译，<code>update</code> 里的算术顺序也和 Python 一致，所以后面才能逐位对上。差别在这几处：</p><ul>
<li><strong>枚举和配置。</strong> C 是 <code>enum pl_form</code>、<code>enum pl_method</code> 加 <code>struct pl_pid_discrete_config</code>，没有 <code>typedef</code>；C++ 是 <code>enum class Form</code>、<code>enum class Method</code> 和模板结构体 <code>PidDiscreteConfig&lt;T&gt;</code>；Rust 是 <code>enum Form</code>、<code>enum Method</code> 和 <code>PidDiscreteConfig</code>，另外给了一个 <code>PidDiscreteConfig::new(kp, ki, kd, ts)</code> 填上默认值（不限幅、后向积分、后向微分），算例里只改需要改的字段。</li>
<li><strong>构造的失败方式。</strong> C 的 <code>pl_pid_discrete_init</code> 返回 0 或 <code>-EINVAL</code>；C++ 的 <code>PidDiscrete&lt;T&gt;::Create</code> 返回 <code>std::optional</code>，失败是 <code>std::nullopt</code>，没有异常；Rust 的 <code>PidDiscrete::new</code> 返回 <code>Result&lt;Self, PidDiscreteError&gt;</code>。</li>
<li><strong>无穷。</strong> 不限幅的 <code>u_min</code>、<code>u_max</code> 在 C 里是 <code>-INFINITY</code> 和 <code>INFINITY</code>（<code>&lt;math.h&gt;</code>），C++ 是 <code>std::numeric_limits&lt;T&gt;::infinity()</code>，Rust 是 <code>Real::INFINITY</code>；Python 是 <code>math.inf</code>。<code>clamp</code> 对无穷边界没有特殊处理，<code>x &lt; -inf</code> 永远为假。</li>
<li><strong>内存。</strong> 三种语言里控制器都是栈上的值：C 的 <code>struct pl_pid_discrete</code> 把配置拷贝进来，C++ 和 Rust 的对象也是值语义，没有堆分配。</li>
</ul><section><h4>C<a href="#c"><span>#</span></a></h4><p>文件是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/pid_discrete.h" target="_blank"><code>c/include/pl/pid_discrete.h</code></a> 和 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid_discrete.c" target="_blank"><code>c/src/pid_discrete.c</code></a>。<code>pl_pid_discrete_update</code> 开头一次取出 <code>c = &amp;pid-&gt;cfg</code>，后面都走它。积分增量和两种形式这一段是这样的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>if</span><span> (c</span><span>-&gt;</span><span>i_method </span><span>==</span><span> PL_METHOD_FORWARD)</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>di </span><span>=</span><span> c</span><span>-&gt;</span><span>ki </span><span>*</span><span> c</span><span>-&gt;</span><span>ts </span><span>*</span><span> pid</span><span>-&gt;</span><span>e_prev;</span></div></div><div><div><div>3</div></div><div><span>  </span><span>else</span><span> </span><span>if</span><span> (c</span><span>-&gt;</span><span>i_method </span><span>==</span><span> PL_METHOD_BACKWARD)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>di </span><span>=</span><span> c</span><span>-&gt;</span><span>ki </span><span>*</span><span> c</span><span>-&gt;</span><span>ts </span><span>*</span><span> e;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>else</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>di </span><span>=</span><span> </span><span>0.5</span><span> </span><span>*</span><span> c</span><span>-&gt;</span><span>ki </span><span>*</span><span> c</span><span>-&gt;</span><span>ts </span><span>*</span><span> (e </span><span>+</span><span> pid</span><span>-&gt;</span><span>e_prev);</span></div></div><div><div><div>7</div></div><div><span>  </span><span>if</span><span> (c</span><span>-&gt;</span><span>form </span><span>==</span><span> PL_FORM_POSITIONAL) {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>pid</span><span>-&gt;</span><span>integral</span><span> </span><span>+=</span><span> di;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> </span><span>c</span><span>-&gt;</span><span>kp</span><span> </span><span>*</span><span> e </span><span>+</span><span> </span><span>pid</span><span>-&gt;</span><span>integral</span><span> </span><span>+</span><span> </span><span>pid</span><span>-&gt;</span><span>d_state</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>v </span><span>=</span><span> </span><span>pid</span><span>-&gt;</span><span>u_prev</span><span> </span><span>+</span><span> </span><span>c</span><span>-&gt;</span><span>kp</span><span> </span><span>*</span><span> de </span><span>+</span><span> di </span><span>+</span><span> (</span><span>pid</span><span>-&gt;</span><span>d_state</span><span> </span><span>-</span><span> d_old);</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>u </span><span>=</span><span> </span><span>pl_clamp</span><span><span>(</span><span>v</span><span>,</span><span> c</span></span><span>-&gt;</span><span><span>u_min</span><span>,</span><span> c</span></span><span>-&gt;</span><span><span>u_max</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>状态是四个 <code>pl_real</code>（<code>integral</code>、<code>u_prev</code>、<code>d_state</code>、<code>e_prev</code>），<code>struct pl_pid_discrete</code> 里还嵌着一份配置，所以复制一个控制器就是一次结构体赋值。驱动是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch04.c" target="_blank"><code>c/apps/ch04.c</code></a>：11 个算例放在一个 <code>struct sim_case</code> 数组里，<code>run_case</code> 读对应的数据文件，按 <code>dial</code> 标志选 <code>MotorSpeedLag</code> 还是 <code>DialPosition</code>，写 <code>out/ch04/&lt;case&gt;_c.csv</code>。</p></section><section><h4>C++<a href="#c-1"><span>#</span></a></h4><p>文件是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid_discrete.h" target="_blank"><code>cpp/include/pidlab/pid_discrete.h</code></a>。构造用 <code>Create</code>，失败返回 <code>std::nullopt</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>static</span><span> std::</span><span>optional</span><span>&lt;</span><span>PidDiscrete</span><span>&gt; </span><span>Create</span><span>(</span><span>const</span><span> </span><span>PidDiscreteConfig</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>cfg</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> (</span><span>cfg</span><span>.</span><span>ts</span><span> </span><span>&lt;=</span><span> </span><span>T</span><span>(</span><span>0</span><span>) </span><span>||</span><span> </span><span>cfg</span><span>.</span><span>tf</span><span> </span><span>&lt;</span><span> </span><span>T</span><span>(</span><span>0</span><span>) </span><span>||</span><span> </span><span>cfg</span><span>.</span><span>u_min</span><span> </span><span>&gt;</span><span> </span><span>cfg</span><span>.</span><span>u_max</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>      </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> (</span><span>cfg</span><span>.</span><span>d_method</span><span> </span><span>==</span><span> Method::</span><span>kForward</span><span>) </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span> (</span><span>cfg</span><span>.</span><span>d_method</span><span> </span><span>==</span><span> Method::</span><span>kTustin</span><span> </span><span>&amp;&amp;</span><span> </span><span>cfg</span><span>.</span><span>tf</span><span> </span><span>&lt;=</span><span> </span><span>T</span><span>(</span><span>0</span><span>)) {</span></div></div><div><div><div>7</div></div><div><span>      </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> </span><span>PidDiscrete</span><span>(</span><span>cfg</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>真正的构造函数是私有的，<code>Create</code> 是唯一的入口，所以拿到一个 <code>PidDiscrete</code> 就意味着它通过了校验。代价是对象只能通过 <code>std::optional</code> 取出来，驱动里用 <code>pid-&gt;Update(...)</code>。<code>Update</code> 里的常数写成 <code>T(2)</code>、<code>T(0.5)</code>，让它们跟着标量类型走，第 10 篇换成 <code>float</code> 时不用改这里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>      </span><span>const</span><span> </span><span>T</span><span> </span><span>a</span><span> </span><span>=</span><span> (</span><span>T</span><span>(</span><span>2</span><span>) </span><span>*</span><span> </span><span>cfg_</span><span>.</span><span>tf</span><span> </span><span>-</span><span> </span><span>cfg_</span><span>.</span><span>ts</span><span>) </span><span>/</span><span> (</span><span>T</span><span>(</span><span>2</span><span>) </span><span>*</span><span> </span><span>cfg_</span><span>.</span><span>tf</span><span> </span><span>+</span><span> </span><span>cfg_</span><span>.</span><span>ts</span><span>);</span></div></div><div><div><div>2</div></div><div><span>      </span><span>d_state_</span><span> </span><span>=</span><span> </span><span>a</span><span> </span><span>*</span><span> </span><span>d_old</span><span> </span><span>+</span><span> </span><span>T</span><span>(</span><span>2</span><span>) </span><span>*</span><span> </span><span>cfg_</span><span>.</span><span>kd</span><span> </span><span>/</span><span> (</span><span>T</span><span>(</span><span>2</span><span>) </span><span>*</span><span> </span><span>cfg_</span><span>.</span><span>tf</span><span> </span><span>+</span><span> </span><span>cfg_</span><span>.</span><span>ts</span><span>) </span><span>*</span><span> </span><span>de</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>驱动是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch04.cc" target="_blank"><code>cpp/apps/ch04.cc</code></a>。<code>RunLoop&lt;P&gt;</code> 是以植物类型为模板参数的一个函数，<code>DialPosition</code> 和 <code>MotorSpeedLag</code> 各实例化一份。</p></section><section><h4>Rust<a href="#rust"><span>#</span></a></h4><p>文件是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid_discrete.rs" target="_blank"><code>rust/src/pid_discrete.rs</code></a>。<code>match</code> 把三种积分方法写成一个表达式，编译器检查是否穷尽：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>let</span><span> </span><span>di</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>c</span><span>.i_method {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>Method</span><span>::</span><span>Forward</span><span> =&gt; </span><span>c</span><span>.ki * </span><span>c</span><span>.ts * </span><span>self</span><span>.e_prev,</span></div></div><div><div><div>3</div></div><div><span>            </span><span>Method</span><span>::</span><span>Backward</span><span> =&gt; </span><span>c</span><span>.ki * </span><span>c</span><span>.ts * </span><span>e</span><span>,</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Method</span><span>::</span><span>Tustin</span><span> =&gt; </span><span>0.5</span><span> * </span><span>c</span><span>.ki * </span><span>c</span><span>.ts * (</span><span>e</span><span> + </span><span>self</span><span>.e_prev),</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>v</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>c</span><span><span>.form </span><span>==</span><span> </span></span><span>Form</span><span>::</span><span>Positional</span><span> {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>self</span><span><span>.integral </span><span>+=</span><span> </span></span><span>di</span><span>;</span></div></div><div><div><div>8</div></div><div><span>            </span><span>c</span><span>.kp * </span><span>e</span><span> + </span><span>self</span><span>.integral + </span><span>self</span><span>.d_state</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>self</span><span>.u_prev + </span><span>c</span><span>.kp * </span><span>de</span><span> + </span><span>di</span><span> + (</span><span>self</span><span>.d_state - </span><span>d_old</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>u</span><span><span> </span><span>=</span><span> </span></span><span>v</span><span>.</span><span>clamp</span><span>(</span><span>c</span><span>.u_min, </span><span>c</span><span>.u_max);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这段前面有一句 <code>let c = &amp;self.cfg;</code>，之后又修改 <code>self.d_state</code>，借用检查没有抱怨：<code>c</code> 只借了 <code>self.cfg</code> 这一个字段，Rust 认为字段之间是不相交的借用。Python 版的 <code>ValueError</code> 在这里对应 <code>PidDiscreteError::InvalidConfig(&amp;'static str)</code>，错误信息是静态字符串，不需要分配。驱动是 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch04.rs" target="_blank"><code>rust/src/bin/ch04.rs</code></a>：<code>run_loop&lt;const N: usize&gt;</code> 接受两个闭包，一个取输出、一个取导数，两个植物的状态维数不同（2 和 3）正好用常量泛型表示。</p></section><section><h4>编译和风格检查<a href="#编译和风格检查"><span>#</span></a></h4><p>编译参数和前几篇一样：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code> 加同样的警告（15.3.0），Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>（0.1.95）。风格检查：C 用内核的 <code>checkpatch.pl --no-tree --ignore SPDX_LICENSE_TAG,NEW_TYPEDEFS</code>，全部文件 0 errors、0 warnings；C++ 用 <code>clang-format --style=Google</code>（21.1.8）；Python 用 <code>ruff format --line-length 79</code> 和 <code>ruff check</code>（0.15.14）；Rust 用 <code>cargo fmt --check</code>。</p></section></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch04</code> 一次跑完。11 个算例乘三种语言，33 个对比文件，输出很长，我只留头尾和每组的一个例子（中间 30 行 <code>ok</code> 的最大差值同样全是 <code>0.000e+00</code>，省略）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch04</span></div></div><div><div><div>2</div></div><div><span>ch04: C wrote 11 cases to out/ch04</span></div></div><div><div><div>3</div></div><div><span>ch04: C++ wrote 11 cases to out/ch04</span></div></div><div><div><div>4</div></div><div><span>ch04: Rust wrote 11 cases to out/ch04</span></div></div><div><div><div>5</div></div><div><span>ok   d_backward_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   d_backward_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   d_backward_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div><div><div><div>9</div></div><div><span>ok   ts_5ms_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   ts_5ms_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   ts_5ms_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>speed PI at zeta 0.7, wn 30.0: kp = 4.1667, ki = 90.0000</span></div></div><div><div><div>13</div></div><div><span>dial PD: kp = 15.0000, kd = 0.6667</span></div></div><div><div><div>14</div></div><div><span>continuous loop (current lag, no delay): wc = 45.98 rad/s, PM = 62.62 deg</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>ts  est loss true loss  PM cont  PM exact  wc exact     rho  overshoot  max|u|  n&gt;20A</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>1ms     3.95°     3.60°   58.67°    59.02°     46.33  0.9778     23.41%   21.87      7</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>5ms    19.76°    18.59°   42.86°    44.03°     47.66  0.8608     36.40%   25.33      4</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>10ms    39.52°    38.61°   23.10°    24.02°     49.45  0.8123     70.81%   29.83      3</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>20ms    79.04°    84.34°  -16.42°   -21.72°     54.09  1.1555    150.42%   42.10     30</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>form_pos vs form_inc (no saturation): max|dy| = 4.44e-16, max|du| = 4.00e-15, max|u| = 15.18 A</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>case  peak y  overshoot   max|u|  settle after drop</span></div></div><div><div><div>24</div></div><div><span><span>     </span></span><span>sat_pos   38.77      29.2%      5.0             5.067s</span></div></div><div><div><div>25</div></div><div><span><span>     </span></span><span>sat_inc   30.12       0.4%      5.0             0.532s</span></div></div><div><div><div>26</div></div><div><span><span> </span></span><span>sat_inc_raw   38.77      29.2%   1328.4             5.067s</span></div></div><div><div><div>27</div></div><div><span>max|y_pos - y_inc_raw| = 1.146e-13 (same arithmetic)</span></div></div><div><div><div>28</div></div><div><span>max|y_inc - y_positional_backcalc(tt = ts)| = 3.553e-15 (clamped accumulator is back-calculation)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span><span>       </span></span><span>case    pole    rms u    rms d  lag-1 of d  hf share</span></div></div><div><div><div>31</div></div><div><span><span> </span></span><span>d_backward  +0.091    0.8597    0.8482   -0.4790    0.8079</span></div></div><div><div><div>32</div></div><div><span><span>   </span></span><span>d_tustin  -0.667    2.8303    2.8239   -0.8261    0.9647</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>       </span></span><span>case              D step response, samples 0..4  |D| at Nyquist  max|u|  rms y err</span></div></div><div><div><div>35</div></div><div><span><span> </span></span><span>d_backward    606.1     55.1      5.0      0.5      0.0          1111.1    6.29   1.04e-03</span></div></div><div><div><div>36</div></div><div><span><span>   </span></span><span>d_tustin   1111.1   -740.7    493.8   -329.2    219.5          6666.7   11.10   1.04e-03</span></div></div><div><div><div>37</div></div><div><span>figure: figures/ch04/s-to-z.svg</span></div></div><div><div><div>38</div></div><div><span>figure: figures/ch04/sample-rate.svg</span></div></div><div><div><div>39</div></div><div><span>figure: figures/ch04/positional-vs-incremental.svg</span></div></div><div><div><div>40</div></div><div><span>figure: figures/ch04/tustin-ringing.svg</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>ch04 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>33 个文件和 Python 的最大差值全部是 <code>0.000e+00</code>，逐位一致。这一章只有加减乘除和比较，没有 <code>atan2</code>、没有复数、没有 <code>exp</code>，三种语言的 <code>double</code> 运算顺序又完全一致，这个结果在意料之中。唯一值得提一句的是积分器卷绕到 1328 A 的两个算例：数值大不代表舍入误差大，位置式和 <code>sat_inc_raw</code> 之间的差只有 <span><span>1.1×10−131.1\times 10^{-13}</span><span><span><span></span><span>1.1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>。</p><section><h3>采样率：预测对了一半<a href="#采样率预测对了一半"><span>#</span></a></h3><p>连续回路（电流环加 <span><span>ωn=30\omega_n = 30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span> 的 PI，没有延迟）的穿越频率是 45.98 rad/s，相位裕度 62.62°。表里“est loss”列是前面的预测 <span><span>ωc⋅1.5Ts\omega_c \cdot 1.5T_s</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>1.5</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，“true loss”列是 62.62° 减去精确采样模型（<code>sampled_pm</code>）的相位裕度，“PM cont”是用第 3 篇的 <code>freq</code> 模块给连续模型加 <span><span>1.5Ts1.5T_s</span><span><span><span></span><span>1.5</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 延迟算的：</p>

<table><thead><tr><th><span><span>TsT_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></th><th>est loss</th><th>true loss</th><th>PM cont</th><th>PM exact</th><th><span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> exact</th><th><span><span>ρ\rho</span><span><span><span></span><span>ρ</span></span></span></span></th><th>超调</th></tr></thead><tbody><tr><td>1 ms</td><td>3.95°</td><td>3.60°</td><td>58.67°</td><td>59.02°</td><td>46.33</td><td>0.9778</td><td>23.41%</td></tr><tr><td>5 ms</td><td>19.76°</td><td>18.59°</td><td>42.86°</td><td>44.03°</td><td>47.66</td><td>0.8608</td><td>36.40%</td></tr><tr><td>10 ms</td><td>39.52°</td><td>38.61°</td><td>23.10°</td><td>24.02°</td><td>49.45</td><td>0.8123</td><td>70.81%</td></tr><tr><td>20 ms</td><td>79.04°</td><td>84.34°</td><td>-16.42°</td><td>-21.72°</td><td>54.09</td><td>1.1555</td><td>150.42%</td></tr></tbody></table><p>表里 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> exact 的单位是 rad/s，<span><span>ρ\rho</span><span><span><span></span><span>ρ</span></span></span></span> 是精确采样闭环的谱半径。</p><p></p><figure><img src="/assets/images/pid-control/ch04-sample-rate.svg" alt="采样周期取 1、5、10、20 ms 的阶跃响应，和相位裕度随采样周期下降的曲线：20 ms 的响应是一个发散的振荡，相位裕度已经为负" /><figcaption>采样周期取 1、5、10、20 ms 的阶跃响应，和相位裕度随采样周期下降的曲线：20 ms 的响应是一个发散的振荡，相位裕度已经为负</figcaption></figure><p></p><p><strong>1、5、10 ms 上，预测在 1.2° 以内。</strong> 估计值（3.95°、19.76°、39.52°）和真实损失（3.60°、18.59°、38.61°）每一档都略大一点，差 0.35°、1.17°、0.91°。我猜这个同号的偏差来自上面推的后向欧拉积分器的半拍超前，加上 sinc 幅值的微小下降，它们都没进 <span><span>ωc⋅1.5Ts\omega_c \cdot 1.5T_s</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>1.5</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的估计；“PM exact”总比“PM cont”高 0.35 到 1.17°，方向对得上，但我没有逐项把这两个修正量分离出来，所以这只是一个符号上吻合的解释。</p><p><strong>20 ms 上预测失效。</strong> 估计损失 79.04°，真实损失 84.34°，差了 5.3°，更重要的是相位裕度已经是 <span><span>−21.72-21.72</span><span><span><span></span><span>−</span><span>21.72</span></span></span></span>°，闭环特征值的谱半径 <span><span>ρ=1.1555\rho = 1.1555</span><span><span><span></span><span>ρ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.1555</span></span></span></span>，每拍增长 15.5%。估计公式用的是无延迟回路的 <span><span>ωc=45.98\omega_c = 45.98</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>45.98</span></span></span></span> rad/s，而采样模型里真实的穿越点随 <span><span>TsT_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在往上走：46.33、47.66、49.45，到 20 ms 是 54.09 rad/s，延迟拉长又推高穿越频率，滞后就比估计的多。这一步我只是从表上看出的趋势，没有单独验证。</p><p><strong>超调和相位裕度同向变化：23.4%、36.4%、70.8%。</strong> 最后一行的 150.42% 不是超调：电流命令碰到 ±20 A 限幅（30 拍超过 20 A，控制器输出最大 42.10 A），响应变成一个幅值固定的极限环，图里的峰值在 12 rad/s 左右。（别被那个百分比骗了，它只是 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 峰值除以 5 再减 1。）</p><p><strong>1 ms 那一行也不是完全线性的。</strong> 阶跃一来比例项就是 <span><span>kp×5=20.83k_p \times 5 = 20.83</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>20.83</span></span></span></span> A，超过 20 A 限幅，最大控制器输出 21.87 A，有 7 拍被夹住（表里最后一列）。这对 23.41% 的超调贡献很小，但表面上看起来“纯线性”的数字里混进了一点限幅，我写出来是为了你复现的时候不要奇怪。</p></section><section><h3>位置式、增量式：不限幅时相等<a href="#位置式增量式不限幅时相等"><span>#</span></a></h3><p><code>form_pos</code> 和 <code>form_inc</code> 是同一个 PID（<span><span>kp=4.17k_p = 4.17</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.17</span></span></span></span>、<span><span>ki=90k_i = 90</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>90</span></span></span></span>、<span><span>kd=0.02k_d = 0.02</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span>、<span><span>tf=5t_f = 5</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> ms），1 ms，设定值 2 rad/s，0.5 s 加 0.3 N·m 负载，控制器输出最大 15.18 A，没有触及任何限幅。输出的最大差：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>form_pos vs form_inc (no saturation): max|dy| = 4.44e-16, max|du| = 4.00e-15, max|u| = 15.18 A</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>差值是浮点舍入的量级。这是两条递推在数学上相等的最直接的证据，也是阈值 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 的来源：我没有给它留余量，因为真实差值比阈值小三个数量级。</p></section><section><h3>限幅：增量式只有在累加器限幅时才抗饱和<a href="#限幅增量式只有在累加器限幅时才抗饱和"><span>#</span></a></h3><p>三个算例，同一个 PI（1 ms），执行器限幅 ±5 A，设定值 0.1 s 起跳到 30 rad/s，1.0 s 回到 0，一共跑 8 s：</p><ul>
<li><code>sat_pos</code>：位置式，输出夹在 ±5 A，积分不限。</li>
<li><code>sat_inc</code>：增量式，累加器夹在 ±5 A。</li>
<li><code>sat_inc_raw</code>：增量式，累加器不限（<code>u_min = -inf</code>、<code>u_max = inf</code>），只有执行器（仿真循环里的 <code>ua</code>）限幅。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>case  peak y  overshoot   max|u|  settle after drop</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>sat_pos   38.77      29.2%      5.0             5.067s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>sat_inc   30.12       0.4%      5.0             0.532s</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>sat_inc_raw   38.77      29.2%   1328.4             5.067s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch04-positional-vs-incremental.svg" alt="位置式和不限幅的增量式曲线完全重合：轮速冲到 38.8 rad/s，再反向冲到约 -33 rad/s，来回摆动；限幅累加器的增量式在 30 rad/s 附近平稳停住，下降后 0.53 s 归零" /><figcaption>位置式和不限幅的增量式曲线完全重合：轮速冲到 38.8 rad/s，再反向冲到约 -33 rad/s，来回摆动；限幅累加器的增量式在 30 rad/s 附近平稳停住，下降后 0.53 s 归零</figcaption></figure><p></p><p>上图是轮速，中图是送到电机的电流，下图是增量式累加器的值（对数刻度，<span><span>±5\pm 5</span><span><span><span></span><span>±</span><span>5</span></span></span></span> A 以内是线性的）。<strong><code>sat_pos</code> 和 <code>sat_inc_raw</code> 的轮速曲线重合：两者的最大差是 <span><span>1.146×10−131.146\times 10^{-13}</span><span><span><span></span><span>1.146</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>。</strong> 它们的区别只是名字：把增量式的累加器放开，它就变成了位置式。蓝线和绿色虚线在图上是同一条线，我在图例里特意画了一条虚线才看得出有两条。</p><p>对位置式，轮速冲到 38.77 rad/s（超调 29.2%），因为从 0.1 s 到 1.0 s 电流一直被夹在 5 A，积分器却在一直累加：<code>sat_inc_raw</code> 里的累加器（等于位置式未限幅的和）绝对值最大到 1328.4 A，是执行器 5 A 限幅的 266 倍。设定值回到 0 之后，积分要先花时间“倒空”，期间执行器一直饱和在反方向，轮速反向冲到约 <span><span>−33-33</span><span><span><span></span><span>−</span><span>33</span></span></span></span> rad/s，再来回摆，直到设定值回零之后 5.067 s 才稳定在 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> rad/s 以内。</p><p>对限幅累加器的增量式，轮速到 30.12 rad/s（超调 0.4%）就停住了，下降后 0.532 s 就进入 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> rad/s：<strong>超调 29.2% 对 0.4%，回稳时间 5.067 s 对 0.532 s，差 9.5 倍。</strong></p><p><strong>限幅累加器就是反算抗饱和，<span><span>Tt=TsT_t = T_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</strong> 我在数学一节推了这个等价，这里验证：驱动里 <code>run_case(..., back_calc=True)</code> 让位置式 PID 每拍执行 <code>pid.integral += ua - u</code>（把夹掉的部分加回积分），这是教科书的反算，<span><span>Tt=TsT_t = T_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。它和 <code>sat_inc</code> 的轮速最大差：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>max|y_inc - y_positional_backcalc(tt = ts)| = 3.553e-15 (clamped accumulator is back-calculation)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>浮点舍入的量级。所以“增量式不会卷绕”这句话，更准确的说法是：<strong>限幅的增量式是跟踪时间常数最短的反算。</strong> 这也有代价：<span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 取一拍，意味着控制器一退出饱和，积分立刻被重置到“刚好让输出等于边界”，比 <span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 取几个采样周期更激进，我猜在有测量噪声的回路里会让输出在边界附近抖。我没有测这一点，第 5 篇的 <code>BACK_CALC</code> 会把 <span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 做成参数。</p></section><section><h3>Tustin 微分的振铃<a href="#tustin-微分的振铃"><span>#</span></a></h3><p><code>d_backward</code> 和 <code>d_tustin</code>：<code>DialPosition</code>（拨弹盘）上的 PD，<span><span>kp=15k_p = 15</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span>、<span><span>kd=0.667k_d = 0.667</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.667</span></span></span></span>，<span><span>tf=0.1Ts=0.1t_f = 0.1T_s = 0.1</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span> ms，测量噪声 <span><span>σ=10−3\sigma = 10^{-3}</span><span><span><span></span><span>σ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rad（<code>numpy</code> 种子 4，来自数据文件），设定值 0.01 rad。0.3 s 之后的统计：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>       </span></span><span>case    pole    rms u    rms d  lag-1 of d  hf share</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>d_backward  +0.091    0.8597    0.8482   -0.4790    0.8079</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>d_tustin  -0.667    2.8303    2.8239   -0.8261    0.9647</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>       </span></span><span>case              D step response, samples 0..4  |D| at Nyquist  max|u|  rms y err</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>d_backward    606.1     55.1      5.0      0.5      0.0          1111.1    6.29   1.04e-03</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>d_tustin   1111.1   -740.7    493.8   -329.2    219.5          6666.7   11.10   1.04e-03</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch04-tustin-ringing.svg" alt="D 项对单位误差阶跃的响应：后向是单调衰减，Tustin 每拍符号翻转、衰减得很慢；中图是同一份噪声下两种 D 项的时间序列，右图是它们的幅度谱" /><figcaption>D 项对单位误差阶跃的响应：后向是单调衰减，Tustin 每拍符号翻转、衰减得很慢；中图是同一份噪声下两种 D 项的时间序列，右图是它们的幅度谱</figcaption></figure><p></p><p>左图是 D 项对单位误差阶跃的响应（对数刻度，保留符号）：后向是 606.1、55.1、5.0、0.5，每拍乘 0.091，两拍之内就衰减了两个数量级；Tustin 是 1111.1、<span><span>−740.7-740.7</span><span><span><span></span><span>−</span><span>740.7</span></span></span></span>、493.8、<span><span>−329.2-329.2</span><span><span><span></span><span>−</span><span>329.2</span></span></span></span>、219.5，每拍乘 <span><span>−0.667-0.667</span><span><span><span></span><span>−</span><span>0.667</span></span></span></span>，符号翻转，衰减缓慢，<strong>第 4 拍（从 0 数起）还有 219.5</strong>。</p><p>第二行表里的 <span><span>∣D∣|D|</span><span><span><span></span><span>∣</span><span>D</span><span>∣</span></span></span></span> at Nyquist 是 1111.1 和 6666.7，比值 6.0：这就是前面推导的 <span><span>(2tf+Ts)/(2tf)(2t_f + T_s)/(2t_f)</span><span><span><span></span><span>(</span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span>(</span><span>2</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。同一份噪声下，Tustin 的 D 项 rms 是后向的 <span><span>2.8239/0.8482=3.332.8239/0.8482 = 3.33</span><span><span><span></span><span>2.8239/0.8482</span><span></span><span>=</span><span></span></span><span><span></span><span>3.33</span></span></span></span> 倍，lag-1 自相关从 <span><span>−0.479-0.479</span><span><span><span></span><span>−</span><span>0.479</span></span></span></span> 到 <span><span>−0.826-0.826</span><span><span><span></span><span>−</span><span>0.826</span></span></span></span>（纯白噪声的差分是 <span><span>−0.5-0.5</span><span><span><span></span><span>−</span><span>0.5</span></span></span></span>，所以后向这一项基本就是“噪声的差分”；Tustin 是它加上负极点带来的翻转），功率落在上半频段（高于 <span><span>fs/4f_s/4</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/4</span></span></span></span>）的比例从 80.8% 涨到 96.5%。中图和右图是同样的结论，频谱上 Tustin 的曲线整体高一截，尤其在 400 Hz 以上。</p><p><strong>设定值阶跃的那一脚也不一样：</strong> 最大的控制器输出出现在阶跃那一拍，后向是 6.29 A，Tustin 是 11.10 A，对应表里的 <code>max|u|</code>。</p><p><strong>但我没能从输出里看到它对轮子的影响。</strong> 两行最后一列的 rms 跟踪误差都是 <span><span>1.04×10−31.04\times 10^{-3}</span><span><span><span></span><span>1.04</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rad，和噪声 <span><span>σ=10−3\sigma = 10^{-3}</span><span><span><span></span><span>σ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> 几乎一样，因为记录的 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 是含测量噪声的读数，不是真实角度。这个指标被噪声底限住了，区分不出两者。D 项振铃的代价是在 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 上：电流指令的 rms 高 3.3 倍，在每拍 <span><span>±\pm</span><span><span><span></span><span>±</span></span></span></span> 几安培的翻转里；M3508 的电流环带宽比 1 kHz 低得多，所以我猜电机本身不会跟着抖，但发热和 CAN 上的指令噪声是真实的代价。这是推测，我没有在真电机上测。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 pid-lab 仓库根目录、<code>nix develop</code> 里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch04</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出就是上一节那一大块：最后一行是 <code>ch04 OK</code>，前面是 33 行 <code>ok</code>（11 个算例乘 C、C++、Rust，容差 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，实际最大差全是 <code>0.000e+00</code>）和四张表。<code>out/ch04/</code> 下有 44 个 CSV（11 个算例乘 Python、C、C++、Rust），<code>figures/ch04/</code> 下有 4 张 SVG。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇新增的是 <code>pid_discrete</code> 模块和 <code>ch04</code> 驱动；仿真循环、植物和 CSV 比对沿用前几篇。链接指向 tag <code>tutorial-v1</code>。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid_discrete.py" target="_blank"><code>python/pidlab/pid_discrete.py</code></a></td><td><code>make -s ch04</code></td><td>参考实现，<code>form_pos</code> 与 <code>form_inc</code> 的 <code>max|du| = 4.00e-15</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/pid_discrete.h" target="_blank"><code>c/include/pl/pid_discrete.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid_discrete.c" target="_blank"><code>c/src/pid_discrete.c</code></a></td><td><code>make -s ch04</code></td><td>11 行 <code>ok ..._c.csv</code>，最大差 <code>0.000e+00</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid_discrete.h" target="_blank"><code>cpp/include/pidlab/pid_discrete.h</code></a></td><td><code>make -s ch04</code></td><td>11 行 <code>ok ..._cpp.csv</code>，最大差 <code>0.000e+00</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid_discrete.rs" target="_blank"><code>rust/src/pid_discrete.rs</code></a></td><td><code>make -s ch04</code></td><td>11 行 <code>ok ..._rs.csv</code>，最大差 <code>0.000e+00</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch04_discretization.py" target="_blank"><code>python/ch04_discretization.py</code></a></td><td><code>python3 python/ch04_discretization.py report</code>（<code>ref</code> 之后）</td><td>采样率表、限幅表、D 项表，4 张图，最后一行 <code>ch04 OK</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch04.c" target="_blank"><code>c/apps/ch04.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch04.cc" target="_blank"><code>cpp/apps/ch04.cc</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch04.rs" target="_blank"><code>rust/src/bin/ch04.rs</code></a></td><td><code>make -s ch04</code></td><td>各写 11 个算例：<code>ch04: C wrote 11 cases to out/ch04</code>，C++、Rust 同理</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>python/pidlab/sim.py</code></a></td><td><code>make -s ch04</code></td><td>沿用：<code>MotorSpeedLag</code>、<code>DialPosition</code>、<code>DelayLine</code></td></tr><tr><td><code>figures/ch04/</code>（生成物，不进仓库）</td><td><code>ls figures/ch04</code></td><td><code>s-to-z.svg</code>、<code>sample-rate.svg</code>、<code>positional-vs-incremental.svg</code>、<code>tustin-ringing.svg</code>，由 <code>report</code> 写出</td></tr></tbody></table></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把采样周期这个“实现细节”变成了几个数：1 ms 到 20 ms，相位裕度从 59.0° 降到 <span><span>−21.7-21.7</span><span><span><span></span><span>−</span><span>21.7</span></span></span></span>°，20 ms 上回路失稳，估计公式 <span><span>ωc⋅1.5Ts\omega_c \cdot 1.5T_s</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>1.5</span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 10 ms 以内误差不超过 1.2°。位置式和增量式在不限幅时差 <span><span>4×10−154\times 10^{-15}</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，限幅累加器的增量式等价于 <span><span>Tt=TsT_t = T_s</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的反算抗饱和（轮速差 <span><span>3.6×10−153.6\times 10^{-15}</span><span><span><span></span><span>3.6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>），不限幅的增量式则完全是位置式（差 <span><span>1.1×10−131.1\times 10^{-13}</span><span><span><span></span><span>1.1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>）。Tustin 的微分在 <span><span>tf≪Tst_f \ll T_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≪</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 时每拍翻转符号，换来 3.3 倍的电流 rms。</p><p><a href="/posts/pid-control-05-practical/">第 5 篇：工程上的坑：积分饱和、微分冲击与前馈</a>会把这些零散的写法收成一个 PID 库：位置式，四种抗饱和（无、限幅、条件积分、反算，这一篇的“一拍反算”变成可调的 <span><span>TtT_t</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>），设定值权重和微分只看测量值来去掉微分冲击，重力前馈，以及切换增益时的无扰动处理。这一篇留下的那一脚（第一拍微分看到一个从 0 的阶跃）会在那里被处理掉。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-03-frequency-domain/</id>
      <title type="text">频域：Bode、Nyquist 与稳定裕度</title>
      <published>2026-10-16T02:00:00.000Z</published>
      <updated>2026-10-16T02:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-03-frequency-domain/"/>
      <summary type="text">PID 控制系列第 3 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95 把频率响应、相位展开和裕度求解写了四遍，给第 2 篇的 PI 速度环算出 63.3° 相位裕度和 72.9 ms 延迟裕度，再把延迟加到裕度的 0.9 倍和 1.1 倍去仿真：0.9 倍的包络以 -1.06/s 衰减，1.1 倍以 +0.81/s 发散，精确的 1 kHz 采样模型把稳定边界放在 72 和 73 拍之间。</summary>
      <content type="html"><![CDATA[<p>上一篇结尾我留了一个难看的事实：Routh 判据给出的临界增益，在 1 kHz 采样下少了 20%，而我只能靠“把对象重新离散化一遍再求特征值”去量它。这一篇我换一个问法：<strong>这个回路还能再吃多少延迟？</strong> 频域给的答案是一个数，<span><span>Dm=PM/ωcD_m = \text{PM}/\omega_c</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>PM</span></span><span>/</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。第 2 篇的 PI 速度环（<span><span>ωn=10\omega_n = 10</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>、<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>）加一个采样周期的纯延迟，我算出 <span><span>Dm=72.875D_m = 72.875</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>72.875</span></span></span></span> ms，也就是 72.875 拍。然后我把额外延迟设成这个数的 0.9 倍（66 拍）和 1.1 倍（80 拍）去仿真：0.9 倍的偏差包络以 <span><span>−1.063-1.063</span><span><span><span></span><span>−</span><span>1.063</span></span></span></span>/s 衰减，1.1 倍以 <span><span>+0.806+0.806</span><span><span><span></span><span>+</span><span>0.806</span></span></span></span>/s 增长，直到电流限幅把它夹成一个极限环。把延迟逐拍加上去的精确 1 kHz 离散模型说，最后一个稳定的额外延迟是 72 拍，73 拍就不稳了。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch03</code> 通过，<code>out/ch03/</code> 下 16 个 CSV（<code>bode</code>、<code>margins</code>、<code>delay_0.9</code>、<code>delay_1.1</code> 四组，每组 Python、C、C++、Rust 各一份），<code>figures/ch03/</code> 下 5 张图；以及频率响应、Bode 渐近线、Nyquist 判据、三种裕度、灵敏度函数和 Bode 灵敏度积分的完整推导。
新增模块：Python 是 <code>pidlab/freq.py</code>；C 是 <code>pl/freq.h</code> 和 <code>freq.c</code>，<code>pl/real.h</code> 多了 <code>PL_LOG10</code>；C++ 是 <code>pidlab/freq.h</code>；Rust 是 <code>freq.rs</code>，<code>real.rs</code> 多了 <code>log10</code>。
前置：<a href="/posts/pid-control-02-transfer-functions/">第 2 篇</a>。需要的数学：复数的模和辐角、欧拉公式、第 2 篇的传递函数和极点配置公式。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>正弦稳态：频率响应是什么<a href="#正弦稳态频率响应是什么"><span>#</span></a></h3><p>设 <span><span>G(s)G(s)</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span></span></span></span> 是稳定的线性系统，脉冲响应为 <span><span>g(t)g(t)</span><span><span><span></span><span>g</span><span>(</span><span>t</span><span>)</span></span></span></span>，输入 <span><span>u(t)=ejωtu(t) = e^{j\omega t}</span><span><span><span></span><span>u</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>j</span><span>ω</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>。卷积给出</p><span><span><span>y(t)=∫0tg(τ) ejω(t−τ) dτ=ejωt∫0tg(τ) e−jωτ dτy(t) = \int_0^t g(\tau)\,e^{j\omega(t-\tau)}\,d\tau = e^{j\omega t}\int_0^t g(\tau)\,e^{-j\omega\tau}\,d\tau</span><span><span><span></span><span>y</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>g</span><span>(</span><span>τ</span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>j</span><span>ω</span><span>(</span><span>t</span><span>−</span><span>τ</span><span>)</span></span></span></span></span></span></span></span></span><span></span><span>d</span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>j</span><span>ω</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>g</span><span>(</span><span>τ</span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span>τ</span></span></span></span></span></span></span></span></span><span></span><span>d</span><span>τ</span></span></span></span></span><p>积分上限 <span><span>t→∞t \to \infty</span><span><span><span></span><span>t</span><span></span><span>→</span><span></span></span><span><span></span><span>∞</span></span></span></span> 时（系统稳定，<span><span>gg</span><span><span><span></span><span>g</span></span></span></span> 衰减，暂态消失），右边的积分就是拉普拉斯变换 <span><span>G(s)=∫0∞g(τ)e−sτdτG(s) = \int_0^\infty g(\tau)e^{-s\tau}d\tau</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>g</span><span>(</span><span>τ</span><span>)</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>τ</span></span></span></span></span></span></span></span></span><span>d</span><span>τ</span></span></span></span> 在 <span><span>s=jωs = j\omega</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>j</span><span>ω</span></span></span></span> 处的值：</p><span><span><span>yss(t)=G(jω) ejωty_{ss}(t) = G(j\omega)\,e^{j\omega t}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>G</span><span>(</span><span>j</span><span>ω</span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>j</span><span>ω</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>gg</span><span><span><span></span><span>g</span></span></span></span> 是实函数，所以对 <span><span>u=sin⁡ωtu = \sin\omega t</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>sin</span><span></span><span>ω</span><span>t</span></span></span></span> 取虚部，得到 <span><span>yss=∣G(jω)∣sin⁡(ωt+∠G(jω))y_{ss} = |G(j\omega)|\sin(\omega t + \angle G(j\omega))</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>∣</span><span>G</span><span>(</span><span>j</span><span>ω</span><span>)</span><span>∣</span><span></span><span>sin</span><span>(</span><span>ω</span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>∠</span><span>G</span><span>(</span><span>j</span><span>ω</span><span>))</span></span></span></span>。<strong>频率响应就是传递函数沿虚轴取值：</strong> 幅值是放大倍数，辐角是相位滞后。我之前把 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 当成复数平面上的任意点去找极点，现在只看一条线。</p><p>纯延迟 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 的传递函数是 <span><span>e−sθe^{-s\theta}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>θ</span></span></span></span></span></span></span></span></span></span></span></span>，在虚轴上 <span><span>∣e−jωθ∣=1|e^{-j\omega\theta}| = 1</span><span><span><span></span><span>∣</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span>θ</span></span></span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，相位是 <span><span>−ωθ-\omega\theta</span><span><span><span></span><span>−</span><span>ω</span><span>θ</span></span></span></span>。幅值不变，相位随频率线性往下掉。画在对数频率轴上，这条直线会越弯越陡，这是下面 Bode 图里蓝线和橙线分开的原因。</p><p>频率响应的乘法有一个很好用的性质：开环 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 是一串因子的积，所以</p><span><span><span>20log⁡10∣L∣=∑i20log⁡10∣Li∣,∠L=∑i∠Li20\log_{10}|L| = \sum_i 20\log_{10}|L_i|, \qquad \angle L = \sum_i \angle L_i</span><span><span><span></span><span>20</span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span>L</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>20</span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span><span>L</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span>,</span><span></span><span></span><span>∠</span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>∠</span><span><span>L</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>在对数幅值和线性相位下，因子是加起来的。这就是 Bode 图的全部发明。</p></section><section><h3>本篇的回路<a href="#本篇的回路"><span>#</span></a></h3><p>对象是带电流环的 M3508 速度模型（第 2 篇的 <code>MotorSpeedLag</code>）：<span><span>Jω˙=Kti−bωJ\dot\omega = K_t i - b\omega</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span>bω</span></span></span></span>，<span><span>τci˙=icmd−i\tau_c\dot i = i_{cmd} - i</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>i</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>i</span></span></span></span>。从电流指令到轮速的传递函数是</p><span><span><span>G(s)=Kt(Js+b)(τcs+1)G(s) = \frac{K_t}{(Js + b)(\tau_c s + 1)}</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span><span>b</span><span>)</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span>1</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>控制器是第 2 篇极点配置出来的 PI，<span><span>C(s)=kp+ki/sC(s) = k_p + k_i/s</span><span><span><span></span><span>C</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>s</span></span></span></span>，其中 <span><span>kp=(2ζωnJ−b)/Ktk_p = (2\zeta\omega_n J - b)/K_t</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>ki=ωn2J/Ktk_i = \omega_n^2 J/K_t</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，取 <span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>、<span><span>ωn=10\omega_n = 10</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> rad/s，得到 <span><span>kp=1.3667k_p = 1.3667</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.3667</span></span></span></span>、<span><span>ki=10k_i = 10</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>。（第 2 篇里比例项对设定值的权重只影响从 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 到 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 的通路，不进入开环 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span>，所以这里不出现。）</p><p>延迟有两部分。一部分是我故意加的：指令先进 <code>DelayLine</code>，延迟 1 个采样周期，也就是 1 ms。另一部分是零阶保持器（ZOH）自己带的：控制器每 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 才更新一次指令，在 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 内保持不变，它的传递函数是</p><span><span><span>1−e−stssts=e−sts/2 sinh⁡(sts/2)sts/2≈e−sts/2\frac{1 - e^{-st_s}}{st_s} = e^{-st_s/2}\,\frac{\sinh(st_s/2)}{st_s/2} \approx e^{-st_s/2}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>sinh</span><span>(</span><span>s</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span></span></span></span></span></span></span></span></span></span><p>低频下相当于半个采样周期的延迟。所以我取总延迟 <span><span>θ=1.5 ts=1.5\theta = 1.5\,t_s = 1.5</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.5</span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.5</span></span></span></span> ms：</p><span><span><span>L(s)=C(s) G(s) e−sθ=Kt(kps+ki)s (Js+b)(τcs+1) e−sθL(s) = C(s)\,G(s)\,e^{-s\theta} = \frac{K_t(k_p s + k_i)}{s\,(Js + b)(\tau_c s + 1)}\,e^{-s\theta}</span><span><span><span></span><span>L</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>s</span><span>)</span><span></span><span>G</span><span>(</span><span>s</span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>θ</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span><span></span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span><span>b</span><span>)</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span>1</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>θ</span></span></span></span></span></span></span></span></span></span></span></span></span><p>我一开始只放了 1 个采样周期的传输延迟，做实验时才发现这半拍有区别（见下面“结果”一节），所以这里直接按 1.5 拍写。</p></section><section><h3>Bode 渐近线：不用算也能读<a href="#bode-渐近线不用算也能读"><span>#</span></a></h3><p><span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的因子有一个积分器 <span><span>1/s1/s</span><span><span><span></span><span>1/</span><span>s</span></span></span></span>、PI 的零点 <span><span>s=−ki/kps = -k_i/k_p</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、两个极点 <span><span>s=−b/Js = -b/J</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>b</span><span>/</span><span>J</span></span></span></span> 和 <span><span>s=−1/τcs = -1/\tau_c</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1/</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。幅值渐近线是折线：每过一个极点，斜率下降 20 dB/dec，每过一个零点，斜率上升 20 dB/dec。转折频率是（手算）：</p><ul>
<li><span><span>b/J=0.01/0.03=0.333b/J = 0.01/0.03 = 0.333</span><span><span><span></span><span>b</span><span>/</span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01/0.03</span><span></span><span>=</span><span></span></span><span><span></span><span>0.333</span></span></span></span> rad/s，机械极点。</li>
<li><span><span>ki/kp=10/1.3667=7.32k_i/k_p = 10/1.3667 = 7.32</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10/1.3667</span><span></span><span>=</span><span></span></span><span><span></span><span>7.32</span></span></span></span> rad/s，PI 零点。</li>
<li><span><span>1/τc=10001/\tau_c = 1000</span><span><span><span></span><span>1/</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1000</span></span></span></span> rad/s，电流环极点。</li>
</ul><p>所以斜率从低频的 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span>（积分器）到 0.333 之后变成 <span><span>−40-40</span><span><span><span></span><span>−</span><span>40</span></span></span></span>，到 7.32 又回到 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span>，到 1000 以后是 <span><span>−40-40</span><span><span><span></span><span>−</span><span>40</span></span></span></span>。PI 的零点把斜率拉回了 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span>，而增益穿越频率会落在 7.32 和 1000 之间的 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span> dB/dec 段上：这是一个经验规则，穿越点的斜率是 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span> 时，相位通常离 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 还远，相位裕度是健康的。下面的数据会验证这一点：<span><span>ωc=15.17\omega_c = 15.17</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15.17</span></span></span></span> rad/s，确实在这一段里。</p></section><section><h3>Nyquist 判据：为什么 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 这个点特别<a href="#nyquist-判据为什么-1-11-这个点特别"><span>#</span></a></h3><p>闭环特征方程是 <span><span>1+L(s)=01 + L(s) = 0</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。<a href="https://onlinelibrary.wiley.com/doi/10.1002/j.1538-7305.1932.tb02344.x" target="_blank">Nyquist 在 1932 年</a>发表《Regeneration Theory》，那是 Bell System Technical Journal 第 11 卷第 1 期 126 到 147 页，问题是电子管放大器的反馈什么时候会振荡。他的判据用现在的话说就是辐角原理：取 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 平面上包住整个右半平面的封闭曲线（沿虚轴从 <span><span>−j∞-j\infty</span><span><span><span></span><span>−</span><span>j</span><span>∞</span></span></span></span> 走到 <span><span>+j∞+j\infty</span><span><span><span></span><span>+</span><span>j</span><span>∞</span></span></span></span>，再沿无穷大半圆绕回来），<span><span>1+L(s)1 + L(s)</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>(</span><span>s</span><span>)</span></span></span></span> 沿这条曲线的像绕原点顺时针转的圈数 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 等于曲线内部的零点数 <span><span>ZZ</span><span><span><span></span><span>Z</span></span></span></span> 减去极点数 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span>。零点就是闭环极点，极点就是开环极点，所以</p><span><span><span>N=Z−P⟹Z=N+PN = Z - P \quad\Longrightarrow\quad Z = N + P</span><span><span><span></span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>Z</span><span></span><span>−</span><span></span></span><span><span></span><span>P</span><span></span><span></span><span>⟹</span><span></span><span></span></span><span><span></span><span>Z</span><span></span><span>=</span><span></span></span><span><span></span><span>N</span><span></span><span>+</span><span></span></span><span><span></span><span>P</span></span></span></span></span><p><span><span>1+L1 + L</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span></span></span></span> 绕原点，等价于 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 绕点 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。开环稳定（<span><span>P=0P = 0</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>）时，闭环稳定当且仅当 <span><span>L(jω)L(j\omega)</span><span><span><span></span><span>L</span><span>(</span><span>j</span><span>ω</span><span>)</span></span></span></span> 的完整曲线（<span><span>ω\omega</span><span><span><span></span><span>ω</span></span></span></span> 从 <span><span>−∞-\infty</span><span><span><span></span><span>−</span><span>∞</span></span></span></span> 到 <span><span>+∞+\infty</span><span><span><span></span><span>+</span><span>∞</span></span></span></span>）不绕 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。负频率那一半是正频率的复共轭，所以只画 <span><span>ω&gt;0\omega &gt; 0</span><span><span><span></span><span>ω</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>，再镜像。</p><p>我的 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 在原点有一个积分极点，它在虚轴上，不属于开右半平面，处理办法是把曲线在原点附近往右半平面绕出去一个小半圆，把它排除在外。代价是 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的图像在 <span><span>ω→0\omega \to 0</span><span><span><span></span><span>ω</span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span> 时会在无穷远处画一段大圆弧，这是 Nyquist 图里看起来“多出来一块”的原因，我这里只用它的直观版本，不推导严格的绕圈数。</p></section><section><h3>增益裕度、相位裕度、延迟裕度<a href="#增益裕度相位裕度延迟裕度"><span>#</span></a></h3><p>“不绕 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>”是一个二元判断，现实里要问的是<strong>离 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 有多远</strong>。三个裕度各取一种方向：</p><ul>
<li><strong>增益裕度</strong>（GM）。<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 的相位到 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 的那个频率 <span><span>ω180\omega_{180}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>180</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 处，<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 穿过负实轴，位置是 <span><span>−1/GM-1/\text{GM}</span><span><span><span></span><span>−</span><span>1/</span><span><span>GM</span></span></span></span></span>。增益还能放大 <span><span>GM=1/∣L(jω180)∣\text{GM} = 1/|L(j\omega_{180})|</span><span><span><span></span><span><span>GM</span></span><span></span><span>=</span><span></span></span><span><span></span><span>1/∣</span><span>L</span><span>(</span><span>j</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>180</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>∣</span></span></span></span> 倍才碰到 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>。</li>
<li><strong>相位裕度</strong>（PM）。<span><span>∣L∣=1|L| = 1</span><span><span><span></span><span>∣</span><span>L</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的频率 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 处，<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 穿过单位圆，相位还剩 <span><span>PM=180°+∠L(jωc)\text{PM} = 180° + \angle L(j\omega_c)</span><span><span><span></span><span><span>PM</span></span><span></span><span>=</span><span></span></span><span><span></span><span>180°</span><span></span><span>+</span><span></span></span><span><span></span><span>∠</span><span>L</span><span>(</span><span>j</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 才转到 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span>。</li>
<li><strong>延迟裕度</strong>。额外加一个延迟 <span><span>τ\tau</span><span><span><span></span><span>τ</span></span></span></span>，相当于乘 <span><span>e−jωτe^{-j\omega\tau}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span>τ</span></span></span></span></span></span></span></span></span></span></span></span>，幅值不变，所以 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 不动，只是相位多滞后 <span><span>ωcτ\omega_c\tau</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>τ</span></span></span></span>。当 <span><span>ωcτ\omega_c\tau</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>τ</span></span></span></span> 等于 PM（换成弧度）时，曲线正好压到 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 上：</li>
</ul><span><span><span>Dm=PMradωcD_m = \frac{\text{PM}_{rad}}{\omega_c}</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>PM</span></span><span><span><span><span><span><span></span><span><span><span>r</span><span>a</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>这个公式只在 <span><span>∣L∣|L|</span><span><span><span></span><span>∣</span><span>L</span><span>∣</span></span></span></span> 恰好穿越 1 一次时精确；如果有多个穿越点，要取每个穿越点算出来的最小值。我的 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 在这一点上没有问题（图里能看到）。</p><p>我选这个回路的原因也在这里：<span><span>Dm≈73D_m \approx 73</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>73</span></span></span></span> ms 比采样周期 1 ms 大两个数量级，所以“把延迟加到 <span><span>DmD_m</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 0.9 倍和 1.1 倍”可以用整数拍表示，舍入误差不会盖住那 10%。</p></section><section><h3>灵敏度函数、噪声和 <span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><a href="#灵敏度函数噪声和-msm_sms"><span>#</span></a></h3><p>定义灵敏度 <span><span>S=1/(1+L)S = 1/(1+L)</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>)</span></span></span></span> 和补灵敏度 <span><span>T=L/(1+L)T = L/(1+L)</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span>/</span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>)</span></span></span></span>，显然 <span><span>S+T=1S + T = 1</span><span><span><span></span><span>S</span><span></span><span>+</span><span></span></span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 是从扰动到输出的传递函数，<span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 是从设定值到输出的传递函数，也是从测量噪声到输出的传递函数（符号相反）：低频要 <span><span>∣S∣|S|</span><span><span><span></span><span>∣</span><span>S</span><span>∣</span></span></span></span> 小（抗扰动、无稳态误差），高频要 <span><span>∣T∣|T|</span><span><span><span></span><span>∣</span><span>T</span><span>∣</span></span></span></span> 小（不放大噪声），而两者之和是 1，所以两个目标没法同时在同一个频率上做到。</p><p><span><span>Ms=max⁡ω∣S(jω)∣M_s = \max_\omega|S(j\omega)|</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>max</span><span><span><span><span><span><span></span><span><span>ω</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span>S</span><span>(</span><span>j</span><span>ω</span><span>)</span><span>∣</span></span></span></span> 是 <span><span>∣1+L∣|1 + L|</span><span><span><span></span><span>∣1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>∣</span></span></span></span> 最小值的倒数，几何上是 Nyquist 曲线到 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 的最近距离的倒数。这给了一个比 GM、PM 更严格的稳健性指标，因为它考虑了整条曲线，不只是两个穿越点。它和另外两个裕度有关，推导很短：<span><span>∣S∣≤Ms|S| \le M_s</span><span><span><span></span><span>∣</span><span>S</span><span>∣</span><span></span><span>≤</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 意味着曲线始终在以 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 为圆心、半径 <span><span>1/Ms1/M_s</span><span><span><span></span><span>1/</span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的圆外。穿越单位圆的那一点离 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 的距离是弦长 <span><span>2sin⁡(PM/2)2\sin(\text{PM}/2)</span><span><span><span></span><span>2</span><span></span><span>sin</span><span>(</span><span><span>PM</span></span><span>/2</span><span>)</span></span></span></span>，穿越负实轴的那一点离 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 的距离是 <span><span>1−1/GM1 - 1/\text{GM}</span><span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>1/</span><span><span>GM</span></span></span></span></span>，它们都不能小于 <span><span>1/Ms1/M_s</span><span><span><span></span><span>1/</span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>PM≥2arcsin⁡12Ms,GM≥MsMs−1\text{PM} \ge 2\arcsin\frac{1}{2M_s}, \qquad \text{GM} \ge \frac{M_s}{M_s - 1}</span><span><span><span></span><span><span>PM</span></span><span></span><span>≥</span><span></span></span><span><span></span><span>2</span><span></span><span>arcsin</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>GM</span></span><span></span><span>≥</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>噪声方面，PID 里的微分项是问题。<span><span>C(s)=kp+ki/s+kdsC(s) = k_p + k_i/s + k_d s</span><span><span><span></span><span>C</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span></span></span></span> 的幅值在高频是 <span><span>kdωk_d\omega</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ω</span></span></span></span>，随频率无限增长。<span><span>kdsk_d s</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span></span></span></span> 在 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 附近提供的相位超前是 <span><span>arctan⁡(ωkd/kp)\arctan(\omega k_d/k_p)</span><span><span><span></span><span>arctan</span><span>(</span><span>ω</span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>（对 PD 而言），小于 90°，这是好处；代价是 <span><span>∣kdjω∣|k_d j\omega|</span><span><span><span></span><span>∣</span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>j</span><span>ω</span><span>∣</span></span></span></span> 在测量噪声的频段里被放大。工程上的办法是给微分加一个一阶滤波，<span><span>Cd(s)=kds/(tfs+1)C_d(s) = k_d s/(t_f s + 1)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span>/</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span></span></span></span>，高频增益被压到 <span><span>kd/tfk_d/t_f</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。滤波器的转折频率 <span><span>1/tf1/t_f</span><span><span><span></span><span>1/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 如果比 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 高得多，那在穿越频率附近几乎没有额外的相位代价，第 5 篇会把它放进 <code>Pid</code> 里。</p></section><section><h3>Bode 灵敏度积分：水床<a href="#bode-灵敏度积分水床"><span>#</span></a></h3><p><span><span>∣S∣|S|</span><span><span><span></span><span>∣</span><span>S</span><span>∣</span></span></span></span> 在低频压低一块，就一定会在别的频率抬高一块，据我所知这个结论属于 Bode，来自他 1945 年的《Network Analysis and Feedback Amplifier Design》（<a href="https://archive.org/details/dli.ernet.15701" target="_blank">书在 Internet Archive</a>，<a href="https://ntrs.nasa.gov/api/citations/20180001190/downloads/20180001190.pdf" target="_blank">NASA 的一篇论文</a>说这个积分定理首次出现在 1945 年）。形式是：设 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 在开右半平面没有极点，并且比较分母和分子的阶次，分母至少高两阶（相对阶数不小于 2），那么</p><span><span><span>∫0∞ln⁡∣S(jω)∣ dω=0\int_0^\infty \ln|S(j\omega)|\,d\omega = 0</span><span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>ln</span><span></span><span>∣</span><span>S</span><span>(</span><span>j</span><span>ω</span><span>)</span><span>∣</span><span></span><span>d</span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>这是<a href="https://en.wikipedia.org/wiki/Bode%27s_sensitivity_integral" target="_blank">维基百科 Bode’s sensitivity integral 词条</a>里的表述，它还给出了有右半平面极点时的一般式 <span><span>π∑Re(pk)−π2lim⁡s→∞sL(s)\pi\sum\text{Re}(p_k) - \frac{\pi}{2}\lim_{s\to\infty}sL(s)</span><span><span><span></span><span>π</span><span></span><span>∑</span><span></span><span><span>Re</span></span><span>(</span><span><span>p</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>π</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>lim</span><span><span><span><span><span><span></span><span><span><span>s</span><span>→</span><span>∞</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>s</span><span>L</span><span>(</span><span>s</span><span>)</span></span></span></span>。常被叫做“水床效应”：压下去一块，另一块就鼓起来。我的 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 有一个原点处的极点，不在开右半平面里；相对阶数是 3 减 1 等于 2；延迟项 <span><span>e−sθe^{-s\theta}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>θ</span></span></span></span></span></span></span></span></span></span></span></span> 在高频不影响收敛。条件看起来都满足，但我没有去查带积分器和延迟的情形是否被原始证明覆盖，所以下面我直接做数值检验，而不是把它当作已知。</p></section></section>
<section><h2>把公式写成代码<a href="#把公式写成代码"><span>#</span></a></h2><p>数学写完了，剩下的是两件容易写错的事：相位怎么展开，穿越点怎么找。我先写 Python，再逐行翻成 C、C++、Rust。</p><section><h3>频率响应和相位展开<a href="#频率响应和相位展开"><span>#</span></a></h3><p><span><span>L(jω)L(j\omega)</span><span><span><span></span><span>L</span><span>(</span><span>j</span><span>ω</span><span>)</span></span></span></span> 就是分子除以分母再乘延迟。多项式求值用第 2 篇的 <code>poly_eval</code>（Horner，复数点），所以 <code>loop_eval</code> 只有三行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>loop_eval</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>num</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>den</span></span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>theta</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>w</span></span><span>:</span><span> </span><span>float</span></div></div><div><div><div>3</div></div><div><span>) -&gt; </span><span>complex</span><span>:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"""L(jw) for L(s) = num(s) / den(s) * exp(-s theta)."""</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>complex</span><span>(</span><span>0.0</span><span>, w)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>lag </span><span>=</span><span> </span><span>complex</span><span><span>(math.</span><span>cos</span><span>(w </span><span>*</span><span> theta), </span><span>-</span><span>math.</span><span>sin</span><span>(w </span><span>*</span><span> theta))</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span><span> </span><span>poly_eval</span><span>(num, s) </span><span>/</span><span> </span><span>poly_eval</span><span>(den, s) </span><span>*</span><span> lag</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>延迟项我写成 <span><span>cos⁡ωθ−jsin⁡ωθ\cos\omega\theta - j\sin\omega\theta</span><span><span><span></span><span>cos</span><span></span><span>ω</span><span>θ</span><span></span><span>−</span><span></span></span><span><span></span><span>j</span><span></span><span>sin</span><span></span><span>ω</span><span>θ</span></span></span></span>，不调 <code>cmath.exp</code>，这样四种语言用的是同样的几个初等函数。</p><p><code>atan2</code> 返回的辐角在 <span><span>(−π,π](-\pi, \pi]</span><span><span><span></span><span>(</span><span>−</span><span>π</span><span>,</span><span></span><span>π</span><span>]</span></span></span></span> 里，曲线转过 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 的时候会从 <span><span>−π-\pi</span><span><span><span></span><span>−</span><span>π</span></span></span></span> 跳到 <span><span>+π+\pi</span><span><span><span></span><span>+</span><span>π</span></span></span></span>。我的办法是逐点展开：把相邻两个采样点的辐角差折回 <span><span>(−π,π](-\pi, \pi]</span><span><span><span></span><span>(</span><span>−</span><span>π</span><span>,</span><span></span><span>π</span><span>]</span></span></span></span>，再累加。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>wrap</span><span>(</span><span>d</span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Fold a phase difference of (-2 pi, 2 pi) into (-pi, pi]."""</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span><span> d </span><span>&gt;</span><span> math.pi:</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>return</span><span><span> d </span><span>-</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> math.pi</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span><span> d </span><span>&lt;=</span><span> </span><span>-</span><span>math.pi:</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>return</span><span><span> d </span><span>+</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> math.pi</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> d</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>bode</span><span>(</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>num</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>den</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>theta</span><span>:</span><span> </span><span>float</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>ws</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span></div></div><div><div><div>15</div></div><div><span>) -&gt; tuple[list[</span><span>float</span><span>], list[</span><span>float</span><span>]]:</span></div></div><div><div><div>16</div></div><div><span>    </span><span>"""Magnitude in dB and unwrapped phase in degrees on the grid ws."""</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>mag, phase </span><span>=</span><span> [], []</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>ph </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>prev </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> i, w </span><span>in</span><span> </span><span>enumerate</span><span>(ws):</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>lw </span><span>=</span><span> </span><span>loop_eval</span><span>(num, den, theta, w)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>a </span><span>=</span><span> math.</span><span>atan2</span><span>(lw.imag, lw.real)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>ph </span><span>=</span><span> a </span><span>if</span><span><span> i </span><span>==</span><span> </span></span><span>0</span><span> </span><span>else</span><span><span> ph </span><span>+</span><span> </span><span>wrap</span><span>(a </span><span>-</span><span> prev)</span></span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>prev </span><span>=</span><span> a</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>mag.</span><span>append</span><span>(</span><span>20.0</span><span><span> </span><span>*</span><span> math.</span><span>log10</span><span>(</span></span><span>abs</span><span>(lw)))</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>phase.</span><span>append</span><span>(math.</span><span>degrees</span><span>(ph))</span></div></div><div><div><div>27</div></div><div><span>    </span><span>return</span><span> mag, phase</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>这要求相邻两点之间相位变化小于 <span><span>π\pi</span><span><span><span></span><span>π</span></span></span></span>。</strong> 网格是 2000 个点、<span><span>[0.1,104][0.1, 10^4]</span><span><span><span></span><span>[</span><span>0.1</span><span>,</span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span><span>]</span></span></span></span> rad/s 对数均匀，相邻两点的比值是 <span><span>105/1999=1.005810^{5/1999} = 1.0058</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>5/1999</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.0058</span></span></span></span>；延迟项在 <span><span>10410^4</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> rad/s 处的相位是 <span><span>ωθ=15\omega\theta = 15</span><span><span><span></span><span>ω</span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span> rad，相邻两点的差是 <span><span>15×0.0058=0.0915 \times 0.0058 = 0.09</span><span><span><span></span><span>15</span><span></span><span>×</span><span></span></span><span><span></span><span>0.0058</span><span></span><span>=</span><span></span></span><span><span></span><span>0.09</span></span></span></span> rad，离 <span><span>π\pi</span><span><span><span></span><span>π</span></span></span></span> 差得远。</p></section><section><h3>穿越点：网格定区间，二分求精<a href="#穿越点网格定区间二分求精"><span>#</span></a></h3><p>网格只告诉我 <span><span>∣L∣|L|</span><span><span><span></span><span>∣</span><span>L</span><span>∣</span></span></span></span> 在哪个区间从 <span><span>≥1\ge 1</span><span><span><span></span><span>≥</span><span></span></span><span><span></span><span>1</span></span></span></span> 变成 <span><span>&lt;1&lt; 1</span><span><span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span>。区间宽度大约 <span><span>0.006 ω0.006\,\omega</span><span><span><span></span><span>0.006</span><span></span><span>ω</span></span></span></span>，直接用端点会差 0.3%。所以我在区间里做 60 次二分，区间缩到比 <code>float64</code> 的分辨率还小。四种语言取同一个区间、同一个判据、同一个步数，就会收敛到同一个浮点数（最后几位除外）。<code>margins</code> 里增益穿越这一段是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>if</span><span><span> math.</span><span>isnan</span><span>(wc) </span></span><span>and</span><span> </span><span>abs</span><span><span>(prev) </span><span>&gt;=</span><span> </span></span><span>1.0</span><span><span> </span><span>&gt;</span><span> </span></span><span>abs</span><span>(cur):</span></div></div><div><div><div>2</div></div><div><span><span>            </span></span><span>lo, hi </span><span>=</span><span> ws[i </span><span>-</span><span> </span><span>1</span><span>], ws[i]</span></div></div><div><div><div>3</div></div><div><span>            </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span><span>(</span><span>BISECT_STEPS</span><span>):</span></span></div></div><div><div><div>4</div></div><div><span><span>                </span></span><span>mid </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> (lo </span><span>+</span><span> hi)</span></span></div></div><div><div><div>5</div></div><div><span>                </span><span>if</span><span> </span><span>abs</span><span><span>(</span><span>loop_eval</span><span>(num, den, theta, mid)) </span><span>&gt;=</span><span> </span></span><span>1.0</span><span>:</span></div></div><div><div><div>6</div></div><div><span><span>                    </span></span><span>lo </span><span>=</span><span> mid</span></div></div><div><div><div>7</div></div><div><span>                </span><span>else</span><span>:</span></div></div><div><div><div>8</div></div><div><span><span>                    </span></span><span>hi </span><span>=</span><span> mid</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>wc </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> (lo </span><span>+</span><span> hi)</span></span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>ph_c </span><span>=</span><span> </span><span>_phase_at</span><span>(num, den, theta, wc, prev, ph)</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>pm </span><span>=</span><span> math.pi </span><span>+</span><span> ph_c</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>dm </span><span>=</span><span> pm </span><span>/</span><span> wc</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>_phase_at</code> 是“从已知点 <code>prev</code> 把相位延拓到 <code>wc</code>”，相位裕度 <code>pm</code> 和延迟裕度 <code>dm = pm / wc</code> 都从它来。<span><span>ω180\omega_{180}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>180</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是同一个结构，判据换成展开后的相位 <span><span>&gt;−π&gt; -\pi</span><span><span><span></span><span>&gt;</span><span></span></span><span><span></span><span>−</span><span>π</span></span></span></span>，然后 <span><span>GM=−20log⁡10∣L(jω180)∣\text{GM} = -20\log_{10}|L(j\omega_{180})|</span><span><span><span></span><span><span>GM</span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>20</span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span>L</span><span>(</span><span>j</span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>180</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>∣</span></span></span></span>。</p><p><strong><code>margins</code> 是一遍扫完的。</strong> 它不存数组，边走边记上一个点的复数值和展开相位，穿越时就地二分，并持续更新 <span><span>Ms=max⁡1/∣1+L∣M_s = \max 1/|1 + L|</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>max</span><span></span><span>1/∣1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>∣</span></span></span></span>，所以 C 版本不需要大缓冲区。网格里没有穿越点时，对应字段是 NaN，不是 0：0 会被误读成“零裕度”。</p></section><section><h3>回路和延迟仿真<a href="#回路和延迟仿真"><span>#</span></a></h3><p>驱动 <code>python/ch03_frequency_domain.py</code> 先把第 2 篇的 PI 设计和 <span><span>L(s)L(s)</span><span><span><span></span><span>L</span><span>(</span><span>s</span><span>)</span></span></span></span> 的分子分母拼出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>design</span><span>() -&gt; tuple[MotorSpeedLag, </span><span>float</span><span>, </span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Plant and the PI gains (kp, ki) placed at ZETA, WN as in ch02."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>m </span><span>=</span><span> </span><span>MotorSpeedLag</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>kp </span><span>=</span><span> (</span><span>2.0</span><span><span> </span><span>*</span><span> </span><span>ZETA</span><span> </span><span>*</span><span> </span><span>WN</span><span> </span><span>*</span><span> m.j </span><span>-</span><span> m.b) </span><span>/</span><span> m.kt</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ki </span><span>=</span><span> </span><span>WN</span><span> </span><span>*</span><span> </span><span>WN</span><span> </span><span>*</span><span> m.j </span><span>/</span><span> m.kt</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> m, kp, ki</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>def</span><span> </span><span>loop_polys</span><span>(</span><span>m</span><span>:</span><span> MotorSpeedLag</span><span>,</span><span><span> </span><span>kp</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>ki</span></span><span>:</span><span> </span><span>float</span><span>):</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"""L(s) = Kt (kp s + ki) / (s (J s + b) (tau_c s + 1))."""</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>num </span><span>=</span><span> </span><span>poly_scale</span><span>([kp, ki], m.kt)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>den </span><span>=</span><span> </span><span>poly_mul</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span><span>], </span><span>poly_mul</span><span>([m.j, m.b], [m.tau_c, </span></span><span>1.0</span><span>]))</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> num, den</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>THETA = (TRANSPORT + 0.5) * TS</code> 就是上面的 1.5 拍。检验延迟裕度的闭环仿真是第 2 篇的循环顺序：测量、控制器、限幅、进 <code>DelayLine</code>、<code>advance</code> 十步 RK4。唯一的区别是延迟线的长度，我可以拿 <code>extra</code> 去逐拍调：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>run_delay</span><span>(</span><span>extra</span><span>:</span><span> </span><span>int</span><span>) -&gt; dict[</span><span>str</span><span>, np.ndarray]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Closed loop with `extra` more samples of command delay."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>m, kp, ki </span><span>=</span><span> </span><span>design</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>data </span><span>=</span><span> </span><span>read_csv</span><span>(</span><span>DATA</span><span> </span><span>/</span><span> </span><span>"ch03_step.csv"</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>delay </span><span>=</span><span> </span><span>DelayLine</span><span>(</span><span>TRANSPORT</span><span> </span><span>+</span><span> extra)</span></div></div><div><div><div>6</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span><span>(</span><span>STEPS</span><span>):</span></span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> data[</span><span>"r"</span><span>][k]</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> m.</span><span>output</span><span>(x) </span><span>+</span><span> data[</span><span>"n"</span><span>][k]</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>e </span><span>=</span><span> r </span><span>-</span><span> y</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> kp </span><span>*</span><span> (r </span><span>-</span><span> y) </span><span>+</span><span> integ</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span><span>(u, </span><span>-</span><span>m.u_max), m.u_max)</span></span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>([data[</span><span>"t"</span><span>][k], r, y, u])</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>integ </span><span>+=</span><span> ki </span><span>*</span><span> </span><span>TS</span><span> </span><span>*</span><span> e</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>advance</span><span>(m.deriv, x, [delay.</span><span>push</span><span>(u), data[</span><span>"d"</span><span><span>][k]], </span><span>TS</span><span>, </span><span>SUBSTEPS</span><span>)</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>精确的采样模型：用来当裁判<a href="#精确的采样模型用来当裁判"><span>#</span></a></h3><p>0.9 倍和 1.1 倍的仿真只说“这两个点一个稳一个不稳”，我还想知道真正的边界在哪一拍。ZOH 离散化之后，闭环是线性定常的离散系统，状态是 <span><span>[ω,i,z,uk−1,…,uk−D][\omega, i, z, u_{k-1}, \dots, u_{k-D}]</span><span><span><span></span><span>[</span><span>ω</span><span>,</span><span></span><span>i</span><span>,</span><span></span><span>z</span><span>,</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>D</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>（积分器状态和还在延迟线里的 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 个指令），谱半径小于 1 就稳定：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>sampled_radius</span><span>(</span><span>extra</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Spectral radius of the exact 1 kHz ZOH loop with `extra` samples."""</span></div></div><div><div><div>3</div></div><div><span>    </span><span>from</span><span> scipy.linalg </span><span>import</span><span> expm</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>m, kp, ki </span><span>=</span><span> </span><span>design</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> </span><span>TRANSPORT</span><span> </span><span>+</span><span> extra</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>a </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>m.b </span><span>/</span><span> m.j, m.kt </span><span>/</span><span> m.j], [</span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span><span> </span><span>/</span><span> m.tau_c]])</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>em </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span>, </span><span>3</span><span>))</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>em[:</span><span>2</span><span>, :</span><span>2</span><span><span>] </span><span>=</span><span> a</span></span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>em[:</span><span>2</span><span>, </span><span>2</span><span><span>] </span><span>=</span><span> [</span></span><span>0.0</span><span>, </span><span>1.0</span><span><span> </span><span>/</span><span> m.tau_c]</span></span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> </span><span>expm</span><span>(em </span><span>*</span><span> </span><span>TS</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span># state: w, i, integrator, then the d commands still in flight</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span><span> </span><span>+</span><span> d, </span></span><span>3</span><span><span> </span><span>+</span><span> d))</span></span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>f[:</span><span>2</span><span>, :</span><span>2</span><span><span>] </span><span>=</span><span> e[:</span></span><span>2</span><span>, :</span><span>2</span><span>]</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>f[:</span><span>2</span><span>, </span><span>2</span><span><span> </span><span>+</span><span> d] </span><span>=</span><span> e[:</span></span><span>2</span><span>, </span><span>2</span><span>]  </span><span># oldest command reaches the plant</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>f[</span><span>2</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> </span><span>-</span><span>ki </span><span>*</span><span> </span><span>TS</span></span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>f[</span><span>2</span><span>, </span><span>2</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>f[</span><span>3</span><span>, </span><span>0</span><span><span>] </span><span>=</span><span> </span><span>-</span><span>kp</span></span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>f[</span><span>3</span><span>, </span><span>2</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> q </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, d):</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>f[</span><span>3</span><span><span> </span><span>+</span><span> q, </span></span><span>2</span><span><span> </span><span>+</span><span> q] </span><span>=</span><span> </span></span><span>1.0</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.</span><span>abs</span><span>(np.linalg.</span><span>eigvals</span><span>(f)).</span><span>max</span><span>())</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这个模型不含任何频域近似，只有 <code>expm</code> 和矩阵。<code>sampled_critical_extra</code> 在 <span><span>[0,200][0, 200]</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>200</span><span>]</span></span></span></span> 里对额外拍数做 12 步二分，找最大的稳定拍数。</p><p>仿真里的增长率用偏差包络量：取 <span><span>∣y−r∣|y - r|</span><span><span><span></span><span>∣</span><span>y</span><span></span><span>−</span><span></span></span><span><span></span><span>r</span><span>∣</span></span></span></span> 的局部峰值，只取到电流限幅第一次起作用为止（之后是极限环，不再是线性增长），对峰值取对数做线性回归，斜率就是增长率（1/s）。这是 <code>ch03_frequency_domain.py</code> 里的 <code>envelope</code> 和 <code>measured_growth</code>。</p></section></section>
<section><h2>C、C++、Rust<a href="#ccrust"><span>#</span></a></h2><p>三个版本是同一份结构：<code>Loop</code>（分子、分母、延迟）、<code>bode</code>、<code>margins</code>，加一个驱动。区别主要在这几处：</p><ul>
<li><strong>复数。</strong> C 用第 2 篇引入的 <code>pl_cplx</code>，虚数单位是 <code>&lt;complex.h&gt;</code> 的 <code>I</code>，<code>PL_CABS</code>、<code>PL_CIMAG</code>、<code>PL_CREAL</code> 取模和分量；C++ 用 <code>std::complex&lt;T&gt;</code>；Rust 用第 2 篇写的 <code>Complex</code>，这一篇没有给它加方法，辐角直接用 <code>atan2(im, re)</code>。</li>
<li><strong>数组的存放。</strong> C 的 <code>struct pl_loop</code> 里是两个 <code>PL_MAX_POLY</code> 长度的固定数组加两个长度，不分配内存；C++ 的 <code>Loop&lt;T, NN, ND&gt;</code> 把分子分母的长度放进类型；Rust 的 <code>Loop&lt;'a&gt;</code> 借用调用者的切片，生命周期参数就是“这个回路不拥有系数”。</li>
<li><strong>NaN。</strong> C 用 <code>NAN</code> 和 <code>isnan</code>，C++ 用 <code>quiet_NaN()</code> 和 <code>std::isnan</code>，Rust 用 <code>Real::NAN</code> 和 <code>is_nan()</code>。三种写法都在没有穿越点时留下 NaN，<code>compare.py</code> 把两边同为 NaN 算作相等。</li>
<li><strong>取整。</strong> 延迟拍数 <span><span>round(f⋅Dm/ts)\text{round}(f\cdot D_m/t_s)</span><span><span><span></span><span><span>round</span></span><span>(</span><span>f</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>：C 是 <code>lround</code>，C++ 是 <code>std::lround</code>，Rust 是 <code>f64::round</code>，Python 是我自己写的 <code>round_half_away</code>（Python 内置的 <code>round</code> 是银行家舍入）。0.9 倍是 65.59，1.1 倍是 80.16，离 .5 都很远，这里谁舍入都一样，但我还是统一了。</li>
<li><strong>数学函数。</strong> C 和 C++ 走 glibc 的 <code>atan2</code>、<code>log10</code>、<code>sin</code>、<code>cos</code>，Rust 的 <code>no_std</code> 库走 <code>libm</code> crate（第 1 篇已经这么做了），<code>real.rs</code> 里多包了一个 <code>log10</code>。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p><code>struct pl_loop</code> 和 <code>pl_margins</code> 是 Python 版的逐行翻译，一遍扫完，不开缓冲区。回路是固定容量的数组，没有 <code>malloc</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Polynomials are highest degree first, at most PL_MAX_POLY coefficients. */</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> pl_loop {</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>pl_real </span><span>num</span><span>[PL_MAX_POLY];</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>pl_real </span><span>den</span><span>[PL_MAX_POLY];</span></div></div><div><div><div>5</div></div><div><span>  </span><span>int</span><span> nnum;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>int</span><span> nden;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>pl_real theta;</span><span> /* pure delay, seconds */</span></div></div><div><div><div>8</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pl_margins</code> 的主循环：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>pl_margins</span><span>(</span><span>const</span><span> </span><span>struct</span><span> pl_loop </span><span>*</span><span><span>l</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>w</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span>         </span><span>struct</span><span> pl_margins </span><span>*</span><span><span>m</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>/* ... */</span></div></div><div><div><div>5</div></div><div><span>  </span><span>m</span><span>-&gt;</span><span>wc</span><span> </span><span>=</span><span> </span><span>m</span><span>-&gt;</span><span>pm_deg</span><span> </span><span>=</span><span> </span><span>m</span><span>-&gt;</span><span>w180</span><span> </span><span>=</span><span> </span><span>m</span><span>-&gt;</span><span>gm_db</span><span> </span><span>=</span><span> </span><span>m</span><span>-&gt;</span><span>dm</span><span> </span><span>=</span><span> NAN;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>prev </span><span>=</span><span> </span><span>pl_loop_eval</span><span>(l, </span><span>w</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>ph </span><span>=</span><span> </span><span>arg_of</span><span>(prev);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>m</span><span>-&gt;</span><span>ms</span><span> </span><span>=</span><span> </span><span>1.0</span><span> </span><span>/</span><span> </span><span>PL_CABS</span><span>(</span><span>1.0</span><span> </span><span>+</span><span> prev);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>1</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>cur </span><span>=</span><span> </span><span>pl_loop_eval</span><span>(l, </span><span>w</span><span>[i]);</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>ph_new </span><span>=</span><span> </span><span>phase_at</span><span>(l, </span><span>w</span><span>[i], prev, ph);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>if</span><span> (</span><span>1.0</span><span> </span><span>/</span><span> </span><span>PL_CABS</span><span>(</span><span>1.0</span><span> </span><span>+</span><span> cur) </span><span>&gt;</span><span> </span><span>m</span><span>-&gt;</span><span>ms</span><span>)</span></div></div><div><div><div>13</div></div><div><span>      </span><span>m</span><span>-&gt;</span><span>ms</span><span> </span><span>=</span><span> </span><span>1.0</span><span> </span><span>/</span><span> </span><span>PL_CABS</span><span>(</span><span>1.0</span><span> </span><span>+</span><span> cur);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>if</span><span> (</span><span>isnan</span><span>(</span><span>m</span><span>-&gt;</span><span>wc</span><span>) </span><span>&amp;&amp;</span><span> </span><span>PL_CABS</span><span>(prev) </span><span>&gt;=</span><span> </span><span>1.0</span><span> </span><span>&amp;&amp;</span><span> </span><span>PL_CABS</span><span>(cur) </span><span>&lt;</span><span> </span><span>1.0</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>      </span><span>m</span><span>-&gt;</span><span>wc</span><span> </span><span>=</span><span> </span><span>bisect_gain</span><span>(l, </span><span>w</span><span>[i </span><span>-</span><span> </span><span>1</span><span>], </span><span>w</span><span>[i]);</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>pm </span><span>=</span><span> PL_PI </span><span>+</span><span> </span><span>phase_at</span><span>(l, </span><span>m</span><span>-&gt;</span><span>wc</span><span>, prev, ph);</span></div></div><div><div><div>17</div></div><div><span>      </span><span>m</span><span>-&gt;</span><span>pm_deg</span><span> </span><span>=</span><span> pm </span><span>*</span><span> (</span><span>180.0</span><span> </span><span>/</span><span> PL_PI);</span></div></div><div><div><div>18</div></div><div><span>      </span><span>m</span><span>-&gt;</span><span>dm</span><span> </span><span>=</span><span> pm </span><span>/</span><span> </span><span>m</span><span>-&gt;</span><span>wc</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>/* ... the w180 crossing, same shape ... */</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>prev </span><span>=</span><span> cur;</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>ph </span><span>=</span><span> ph_new;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pl_loop_eval</code> 里 <code>w * I</code> 是一个复数，<code>pl_poly_eval</code> 是第 2 篇的 Horner。<code>phase_at</code> 把“从已知点往前延拓相位”单独拎出来，<code>pl_margins</code> 里的穿越点和二分都靠它，<code>wrap</code> 则是它和 <code>pl_bode</code> 共用的。输出是调用者给的 <code>struct pl_margins *</code>，函数只返回 0 或 <code>-EINVAL</code>（网格少于两个点）。</p><p>驱动 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch03.c" target="_blank"><code>c/apps/ch03.c</code></a> 从 <code>data/ch03_w.csv</code> 读频率网格，从 <code>data/ch03_step.csv</code> 读设定值，自己用 <code>pl_poly_mul</code> 拼出分母，调 <code>pl_bode</code> 和 <code>pl_margins</code>，再 <code>lround</code> 出延迟拍数。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p>模板参数是标量类型 <code>T</code> 和两个多项式的长度，不满足条件的调用在编译期就错了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>, std::</span><span>size_t</span><span> </span><span>NN</span><span>, std::</span><span>size_t</span><span> </span><span>ND</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Loop</span><span> {</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>NN</span><span>&gt; </span><span>num</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>ND</span><span>&gt; </span><span>den</span><span>;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>T</span><span> </span><span>theta</span><span>;</span><span>  // pure delay, seconds</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>// L(jw).</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt; </span><span>Eval</span><span>(</span><span>T</span><span><span> </span><span>w</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>const</span><span> std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt; </span><span>s</span><span>(</span><span>T</span><span>(</span><span>0</span><span>), </span><span>w</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>const</span><span> std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt; </span><span>lag</span><span><span>(</span><span>std</span><span>::</span></span><span>cos</span><span>(</span><span>w</span><span> </span><span>*</span><span> </span><span>theta</span><span>), </span><span>-</span><span><span>std</span><span>::</span></span><span>sin</span><span>(</span><span>w</span><span> </span><span>*</span><span> </span><span>theta</span><span>));</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> </span><span>PolyEval</span><span>(</span><span>num</span><span>, </span><span>s</span><span>) </span><span>/</span><span> </span><span>PolyEval</span><span>(</span><span>den</span><span>, </span><span>s</span><span>) </span><span>*</span><span> </span><span>lag</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>ComputeMargins</code> 的主循环（增益穿越一段；<span><span>ω180\omega_{180}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>180</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 那段省略）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>// ...</span></div></div><div><div><div>2</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>1</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>w</span><span>.</span><span>size</span><span>(); </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt; </span><span>cur</span><span> </span><span>=</span><span> </span><span>l</span><span>.</span><span>Eval</span><span>(</span><span>w</span><span>[</span><span>i</span><span>]);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>ph_new</span><span> </span><span>=</span><span> </span><span>PhaseAt</span><span>(</span><span>l</span><span>, </span><span>w</span><span>[</span><span>i</span><span>], </span><span>prev</span><span>, </span><span>ph</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>m</span><span>.</span><span>ms</span><span> </span><span>=</span><span> std::</span><span>max</span><span>(</span><span>m</span><span>.</span><span>ms</span><span>, </span><span>T</span><span>(</span><span>1</span><span>) </span><span>/</span><span> std::</span><span>abs</span><span>(</span><span>T</span><span>(</span><span>1</span><span>) </span><span>+</span><span> </span><span>cur</span><span>));</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span><span> (</span><span>std</span><span>::</span></span><span>isnan</span><span>(</span><span>m</span><span>.</span><span>wc</span><span>) </span><span>&amp;&amp;</span><span> std::</span><span>abs</span><span>(</span><span>prev</span><span>) </span><span>&gt;=</span><span> </span><span>T</span><span>(</span><span>1</span><span>) </span><span>&amp;&amp;</span><span> std::</span><span>abs</span><span>(</span><span>cur</span><span>) </span><span>&lt;</span><span> </span><span>T</span><span>(</span><span>1</span><span>)) {</span></div></div><div><div><div>7</div></div><div><span>      </span><span>T</span><span> </span><span>lo</span><span> </span><span>=</span><span> </span><span>w</span><span>[</span><span>i</span><span> </span><span>-</span><span> </span><span>1</span><span>], </span><span>hi</span><span> </span><span>=</span><span> </span><span>w</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>8</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>s</span><span> </span><span>&lt;</span><span> internal::</span><span>kBisectSteps</span><span>; </span><span>++</span><span>s</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>const</span><span> </span><span>T</span><span> </span><span>mid</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> (</span><span>lo</span><span> </span><span>+</span><span> </span><span>hi</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span><span> (</span><span>std</span><span>::</span></span><span>abs</span><span>(</span><span>l</span><span>.</span><span>Eval</span><span>(</span><span>mid</span><span>)) </span><span>&gt;=</span><span> </span><span>T</span><span>(</span><span>1</span><span>)) {</span></div></div><div><div><div>11</div></div><div><span>          </span><span>lo</span><span> </span><span>=</span><span> </span><span>mid</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>13</div></div><div><span>          </span><span>hi</span><span> </span><span>=</span><span> </span><span>mid</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>      </span><span>m</span><span>.</span><span>wc</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> (</span><span>lo</span><span> </span><span>+</span><span> </span><span>hi</span><span>);</span></div></div><div><div><div>17</div></div><div><span>      </span><span>const</span><span> </span><span>T</span><span> </span><span>pm</span><span> </span><span>=</span><span> </span><span>kPi</span><span> </span><span>+</span><span> </span><span>PhaseAt</span><span>(</span><span>l</span><span>, </span><span>m</span><span>.</span><span>wc</span><span>, </span><span>prev</span><span>, </span><span>ph</span><span>);</span></div></div><div><div><div>18</div></div><div><span>      </span><span>m</span><span>.</span><span>pm_deg</span><span> </span><span>=</span><span> </span><span>pm</span><span> </span><span>*</span><span> (</span><span>T</span><span>(</span><span>180</span><span>) </span><span>/</span><span> </span><span>kPi</span><span>);</span></div></div><div><div><div>19</div></div><div><span>      </span><span>m</span><span>.</span><span>dm</span><span> </span><span>=</span><span> </span><span>pm</span><span> </span><span>/</span><span> </span><span>m</span><span>.</span><span>wc</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>// ... the w180 crossing, same shape ...</span></div></div><div><div><div>22</div></div><div><span>    </span><span>prev</span><span> </span><span>=</span><span> </span><span>cur</span><span>;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>ph</span><span> </span><span>=</span><span> </span><span>ph_new</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Bode</code> 接受 <code>std::span</code>，调用者传 <code>std::vector</code> 或数组都行，长度不一致返回 <code>false</code>，不抛异常。<code>ComputeMargins</code> 对不足两个点的网格返回默认构造的结果（全 NaN、<code>ms = 0</code>）。驱动：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch03.cc" target="_blank"><code>cpp/apps/ch03.cc</code></a>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>margins</span><span>(</span><span>l</span><span>: &amp;</span><span>Loop</span><span>, </span><span>w</span><span>: &amp;[</span><span>Real</span><span>]) -&gt; </span><span>Option</span><span>&lt;</span><span>Margins</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> </span><span>w</span><span>.</span><span>len</span><span>() &lt; </span><span>2</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>    </span><span>// ...</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>dist</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>z</span><span>: </span><span>Complex</span><span><span>|</span><span> </span></span><span>Complex</span><span>::</span><span>new</span><span>(</span><span>1.0</span><span> + </span><span>z</span><span>.re, </span><span>z</span><span>.im).</span><span>abs</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>prev</span><span><span> </span><span>=</span><span> </span></span><span>l</span><span>.</span><span>eval</span><span>(</span><span>w</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>ph</span><span><span> </span><span>=</span><span> </span></span><span>arg</span><span>(</span><span>prev</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>m</span><span><span>.ms </span><span>=</span><span> </span></span><span>1.0</span><span> / </span><span>dist</span><span>(</span><span>prev</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>1</span><span>..</span><span>w</span><span>.</span><span>len</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>cur</span><span><span> </span><span>=</span><span> </span></span><span>l</span><span>.</span><span>eval</span><span>(</span><span>w</span><span>[</span><span>i</span><span>]);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>ph_new</span><span><span> </span><span>=</span><span> </span></span><span>l</span><span>.</span><span>phase_at</span><span>(</span><span>w</span><span>[</span><span>i</span><span>], </span><span>prev</span><span>, </span><span>ph</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>m</span><span><span>.ms </span><span>=</span><span> </span></span><span>m</span><span>.ms.</span><span>max</span><span>(</span><span>1.0</span><span> / </span><span>dist</span><span>(</span><span>cur</span><span>));</span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span> </span><span>m</span><span>.wc.</span><span>is_nan</span><span><span>() </span><span>&amp;&amp;</span><span> </span></span><span>prev</span><span>.</span><span>abs</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>1.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>cur</span><span>.</span><span>abs</span><span>() &lt; </span><span>1.0</span><span> {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>let</span><span> (</span><span>mut</span><span> </span><span>lo</span><span>, </span><span>mut</span><span> </span><span>hi</span><span><span>) </span><span>=</span><span> (</span></span><span>w</span><span>[</span><span>i</span><span> - </span><span>1</span><span>], </span><span>w</span><span>[</span><span>i</span><span>]);</span></div></div><div><div><div>16</div></div><div><span>            </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>BISECT_STEPS</span><span> {</span></div></div><div><div><div>17</div></div><div><span>                </span><span>let</span><span> </span><span>mid</span><span><span> </span><span>=</span><span> </span></span><span>0.5</span><span> * (</span><span>lo</span><span> + </span><span>hi</span><span>);</span></div></div><div><div><div>18</div></div><div><span>                </span><span>if</span><span> </span><span>l</span><span>.</span><span>eval</span><span>(</span><span>mid</span><span>).</span><span>abs</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>1.0</span><span> {</span></div></div><div><div><div>19</div></div><div><span>                    </span><span>lo</span><span><span> </span><span>=</span><span> </span></span><span>mid</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>hi</span><span><span> </span><span>=</span><span> </span></span><span>mid</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>            </span><span>m</span><span><span>.wc </span><span>=</span><span> </span></span><span>0.5</span><span> * (</span><span>lo</span><span> + </span><span>hi</span><span>);</span></div></div><div><div><div>25</div></div><div><span>            </span><span>let</span><span> </span><span>pm</span><span><span> </span><span>=</span><span> </span></span><span>PI</span><span> + </span><span>l</span><span>.</span><span>phase_at</span><span>(</span><span>m</span><span>.wc, </span><span>prev</span><span>, </span><span>ph</span><span>);</span></div></div><div><div><div>26</div></div><div><span>            </span><span>m</span><span><span>.pm_deg </span><span>=</span><span> </span></span><span>pm</span><span> * (</span><span>180.0</span><span> / </span><span>PI</span><span>);</span></div></div><div><div><div>27</div></div><div><span>            </span><span>m</span><span><span>.dm </span><span>=</span><span> </span></span><span>pm</span><span> / </span><span>m</span><span>.wc;</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>        </span><span>// ... the w180 crossing, same shape ...</span></div></div><div><div><div>30</div></div><div><span>        </span><span>prev</span><span><span> </span><span>=</span><span> </span></span><span>cur</span><span>;</span></div></div><div><div><div>31</div></div><div><span>        </span><span>ph</span><span><span> </span><span>=</span><span> </span></span><span>ph_new</span><span>;</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>    </span><span>Some</span><span>(</span><span>m</span><span>)</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Loop&lt;'a&gt;</code> 借用分子分母的切片，<code>margins</code> 返回 <code>Option&lt;Margins&gt;</code>，网格短于两个点时是 <code>None</code>。复数的除法是第 2 篇写的朴素公式 <span><span>(abˉ)/∣b∣2(a\bar b)/|b|^2</span><span><span><span></span><span>(</span><span>a</span><span><span><span><span><span><span></span><span>b</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span>)</span><span>/∣</span><span>b</span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，C 和 C++ 走编译器和标准库的实现，这是下面 <code>bode_rs.csv</code> 比另外两个多出 <span><span>5.684×10−135.684\times 10^{-13}</span><span><span><span></span><span>5.684</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> 的一个可能原因，另一个是 <code>libm</code> 的 <code>sin</code>、<code>cos</code>、<code>atan2</code> 和 glibc 末位舍入不同。我没有去追究到底是哪一个，<span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 的容差留了四个数量级的余地。驱动：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch03.rs" target="_blank"><code>rust/src/bin/ch03.rs</code></a>。</p></section><section><h3>编译和风格检查<a href="#编译和风格检查"><span>#</span></a></h3><p>编译参数和前两篇一样：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code> 加同样的警告（15.3.0），Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>（0.1.95）。风格检查：C 用内核的 <code>checkpatch.pl --no-tree --ignore SPDX_LICENSE_TAG,NEW_TYPEDEFS</code>，全部文件 0 errors、0 warnings；C++ 用 <code>clang-format --style=Google</code>（21.1.8）；Python 用 <code>ruff format --line-length 79</code> 和 <code>ruff check</code>（0.15.14）；Rust 用 <code>cargo fmt --check</code>。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch03</code> 一次跑完。12 个对比文件（4 组乘 C、C++、Rust）全部列在这里，后面是 Python 的 <code>report</code> 打印的表：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch03</span></div></div><div><div><div>2</div></div><div><span>ch03: C wrote bode, margins and 2 delay runs to out/ch03</span></div></div><div><div><div>3</div></div><div><span>ch03: C++ wrote bode, margins and 2 delay runs to out/ch03</span></div></div><div><div><div>4</div></div><div><span>ch03: Rust wrote bode, margins and 2 delay runs to out/ch03</span></div></div><div><div><div>5</div></div><div><span>ok   bode_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   bode_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   bode_rs.csv: max diff 5.684e-13 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>ok   delay_0.9_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>9</div></div><div><span>ok   delay_0.9_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   delay_0.9_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   delay_1.1_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>ok   delay_1.1_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>13</div></div><div><span>ok   delay_1.1_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>14</div></div><div><span>ok   margins_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>15</div></div><div><span>ok   margins_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>16</div></div><div><span>ok   margins_rs.csv: max diff 3.553e-15 &lt;= 1.0e-09</span></div></div><div><div><div>17</div></div><div><span>vs scipy.signal.freqresp: max |mag diff| 1.42e-14 dB, max |phase diff| 7.96e-13 deg</span></div></div><div><div><div>18</div></div><div><span>PI placed at zeta 0.7, wn 10.0: kp = 1.3667, ki = 10.0000</span></div></div><div><div><div>19</div></div><div><span><span>       </span></span><span>wc       PM      w180       GM      Ms        Dm</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>15.1683  63.334°   653.971  35.14dB  1.0357  72.875ms</span></div></div><div><div><div>21</div></div><div><span>Dm / ts = 72.875 samples, theta = 1.5 ms</span></div></div><div><div><div>22</div></div><div><span>without the ZOH half sample (theta = 1.0 ms): PM 63.769°, Dm / ts = 73.375</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>continuous prediction: Dm / ts = 72.87 extra samples; exact 1 kHz sampled loop goes unstable at 73 extra samples (last stable: 72)</span></div></div><div><div><div>25</div></div><div><span><span>      </span></span><span>case  extra  ln(rho)/ts  slope 1/s  max|u|</span></div></div><div><div><div>26</div></div><div><span><span> </span></span><span>delay_0.9     66      -1.064     -1.063   10.20</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>delay_1.1     80      +0.851     +0.806   20.00</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>Bode integral of ln|S|: +0.00002 = +24.8890 (S &gt; 1) -24.8890 (S &lt; 1)</span></div></div><div><div><div>30</div></div><div><span>min |1 + L| = 0.9655 = 1/Ms, Ms = 1.0357 (0.31 dB), peak |T| = 1.2728</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>PID with kd = 0.02, kp = 1.3667, ki = 10.0</span></div></div><div><div><div>33</div></div><div><span><span>     </span></span><span>tf  |C(j wc)|  lead at wc  |C| at Nyq</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>0.000     1.4122      11.16°      62.844</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>0.001     1.4167      11.20°      20.363</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>0.005     1.4348      11.32°       5.356</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>0.020     1.5003      11.02°       2.366</span></div></div><div><div><div>38</div></div><div><span>tf = 0 at 3142 rad/s: 46.0 x kp</span></div></div><div><div><div>39</div></div><div><span>figure: figures/ch03/bode.svg</span></div></div><div><div><div>40</div></div><div><span>figure: figures/ch03/nyquist.svg</span></div></div><div><div><div>41</div></div><div><span>figure: figures/ch03/delay-margin-check.svg</span></div></div><div><div><div>42</div></div><div><span>figure: figures/ch03/sensitivity.svg</span></div></div><div><div><div>43</div></div><div><span>figure: figures/ch03/d-term-noise-gain.svg</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>ch03 OK</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>12 个文件里，除了两个 Rust 文件，其余 10 个和 Python 的最大差值都是 <code>0.000e+00</code>，逐位一致。<code>bode_rs.csv</code> 差 <span><span>5.684×10−135.684\times 10^{-13}</span><span><span><span></span><span>5.684</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>（单位是 dB 和度，相位的数值在几百的量级，相当于末几位），<code>margins_rs.csv</code> 差 <span><span>3.553×10−153.553\times 10^{-15}</span><span><span><span></span><span>3.553</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>。两组延迟仿真在三种语言里共 6 个文件都是 <code>0.000e+00</code>：仿真只有加减乘除，不碰 <code>atan2</code>，也不碰复数。Python 的 <code>freq.py</code> 另外对照了 <code>scipy.signal.freqresp</code>：幅值最大差 <span><span>1.42×10−141.42\times 10^{-14}</span><span><span><span></span><span>1.42</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span> dB，相位最大差 <span><span>7.96×10−137.96\times 10^{-13}</span><span><span><span></span><span>7.96</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> 度。</p><section><h3>裕度<a href="#裕度"><span>#</span></a></h3><p>PI 回路的穿越点和裕度（表里 <code>Dm</code> 在 <code>ts = 1 ms</code> 下就是拍数）：</p><p></p><figure><img src="/assets/images/pid-control/ch03-bode.svg" alt="PI 速度环的 Bode 图：穿越频率 15.2 rad/s、相位裕度 63.3 度、增益裕度 35.1 dB，延迟让相位在 650 rad/s 以后急剧下掉" /><figcaption>PI 速度环的 Bode 图：穿越频率 15.2 rad/s、相位裕度 63.3 度、增益裕度 35.1 dB，延迟让相位在 650 rad/s 以后急剧下掉</figcaption></figure><p></p><p>增益穿越频率 <span><span>ωc=15.17\omega_c = 15.17</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15.17</span></span></span></span> rad/s，在上面渐近线分析说的 7.32 到 1000 rad/s 的 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span> dB/dec 段里，相位裕度 63.33°，<span><span>ω180=653.97\omega_{180} = 653.97</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>180</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>653.97</span></span></span></span> rad/s，增益裕度 35.14 dB（线性值 57.2 倍），<span><span>Ms=1.0357M_s = 1.0357</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.0357</span></span></span></span>（0.31 dB），<span><span>Dm=72.875D_m = 72.875</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>72.875</span></span></span></span> ms。</p><p><strong>半拍的账。</strong> 图上橙色虚线是没有延迟的 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span>，蓝线是 <span><span>θ=1.5\theta = 1.5</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>1.5</span></span></span></span> ms。两条幅值曲线重合（延迟不改幅值），相位在 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 附近几乎重合，在 100 rad/s 以后才分开，在 650 rad/s 处蓝线已经穿过 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span>，橙线直到 <span><span>10410^4</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span> rad/s 都还在 <span><span>−175°-175°</span><span><span><span></span><span>−</span><span>175°</span></span></span></span> 附近。</p><p>我一开始只放了 1 ms 的传输延迟（没有算 ZOH 的半拍）：相位裕度 63.769°，<span><span>Dm=73.375D_m = 73.375</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>73.375</span></span></span></span> 拍。算上半拍之后是 63.334° 和 72.875 拍，半拍在 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 处值 <span><span>15.17 rad/s×0.5 ms=0.007615.17\ \text{rad/s} \times 0.5\ \text{ms} = 0.0076</span><span><span><span></span><span>15.17</span><span> </span><span><span>rad/s</span></span><span></span><span>×</span><span></span></span><span><span></span><span>0.5</span><span> </span><span><span>ms</span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.0076</span></span></span></span> rad <span><span>=0.43°= 0.43°</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span>0.43°</span></span></span></span>，相位裕度减少 0.435°，延迟裕度少 0.5 拍，也就是 0.68%。第 2 篇里同样的半拍加电流环的滞后吃掉了 Routh 临界增益的 20%，这里只吃掉延迟裕度的 0.7%：那个拨弹盘回路在临界增益附近的穿越点在 31.6 rad/s，相位裕度趋近于零，这个速度环的穿越点在 15.2 rad/s、相位裕度 63°，同样的半拍落在富裕得多的地方。</p><p>但这 0.5 拍恰好决定了整数拍的答案。看下一节。</p></section><section><h3>延迟裕度：0.9 倍衰减，1.1 倍发散<a href="#延迟裕度09-倍衰减11-倍发散"><span>#</span></a></h3><p>额外延迟取 <span><span>round(0.9×72.875)=66\text{round}(0.9 \times 72.875) = 66</span><span><span><span></span><span><span>round</span></span><span>(</span><span>0.9</span><span></span><span>×</span><span></span></span><span><span></span><span>72.875</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>66</span></span></span></span> 拍和 <span><span>round(1.1×72.875)=80\text{round}(1.1 \times 72.875) = 80</span><span><span><span></span><span><span>round</span></span><span>(</span><span>1.1</span><span></span><span>×</span><span></span></span><span><span></span><span>72.875</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>80</span></span></span></span> 拍，设定值阶跃到 5 rad/s，跑 3 秒：</p><p></p><figure><img src="/assets/images/pid-control/ch03-delay-margin-check.svg" alt="额外延迟取裕度的 0.9 倍时响应衰减，1.1 倍时发散到电流限幅；下图是偏差峰值的包络和回归直线，斜率 -1.06/s 对 +0.81/s" /><figcaption>额外延迟取裕度的 0.9 倍时响应衰减，1.1 倍时发散到电流限幅；下图是偏差峰值的包络和回归直线，斜率 -1.06/s 对 +0.81/s</figcaption></figure><p></p><p>上图是轮速。蓝线（0.9 倍，+66 拍）震荡着收敛，偏差峰值从 0.48 s 处的 4.86 一路降到 2.88 s 处的 0.38，12.8 倍。橙线（1.1 倍，+80 拍）的振幅逐周期增大，大约 0.96 s 时电流指令碰到 20 A 限幅，之后变成一个幅值固定的极限环：偏差的峰值停在 18.2 附近，轮速在 <span><span>−13-13</span><span><span><span></span><span>−</span><span>13</span></span></span></span> 到 <span><span>+23+23</span><span><span><span></span><span>+</span><span>23</span></span></span></span> rad/s 之间摆。下图把偏差峰值画成对数，回归直线的斜率是 <span><span>−1.06-1.06</span><span><span><span></span><span>−</span><span>1.06</span></span></span></span>/s 和 <span><span>+0.81+0.81</span><span><span><span></span><span>+</span><span>0.81</span></span></span></span>/s。</p><p>对照上面输出里的三列：</p><ul>
<li><code>ln(rho)/ts</code>：精确的 1 kHz 离散模型的谱半径取对数除以 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，是理论增长率：<span><span>−1.064-1.064</span><span><span><span></span><span>−</span><span>1.064</span></span></span></span> 和 <span><span>+0.851+0.851</span><span><span><span></span><span>+</span><span>0.851</span></span></span></span>。</li>
<li><code>slope</code>：仿真里量出来的：<span><span>−1.063-1.063</span><span><span><span></span><span>−</span><span>1.063</span></span></span></span> 和 <span><span>+0.806+0.806</span><span><span><span></span><span>+</span><span>0.806</span></span></span></span>。0.9 倍的两个数只差 0.001；1.1 倍低了 5%，我猜是因为线性区只有三个峰值（0.31 s、0.53 s、0.74 s），回归点太少，斜率对第一个峰里残留的快模态很敏感。</li>
<li><code>max|u|</code>：0.9 倍时 10.20 A，没有碰限幅，断言要求这一点；1.1 倍时 20.00 A，限幅起了作用。</li>
</ul><p><strong>连续模型的 <span><span>Dm=72.875D_m = 72.875</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>72.875</span></span></span></span> 这个数是准的吗？</strong> 精确模型说：额外 72 拍稳定，73 拍不稳。72.875 落在 72 和 73 之间，预测对了整数。如果我只用 1 ms 的 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>，<span><span>Dm=73.375D_m = 73.375</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>73.375</span></span></span></span>，会预测“73 拍稳定”，这是错的。半拍决定了对错。</p></section><section><h3>Nyquist 图：离 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 很远<a href="#nyquist-图离-1-11-很远"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch03-nyquist.svg" alt="Nyquist 图：曲线离 -1 很远，最近距离 0.9655 出现在 59 rad/s 附近，放大后曲线在那里和半径 1/Ms 的红圈擦边" /><figcaption>Nyquist 图：曲线离 -1 很远，最近距离 0.9655 出现在 59 rad/s 附近，放大后曲线在那里和半径 1/Ms 的红圈擦边</figcaption></figure><p></p><p>左图是整条曲线（实线 <span><span>ω&gt;0\omega &gt; 0</span><span><span><span></span><span>ω</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>，虚线是镜像）。<span><span>ω\omega</span><span><span><span></span><span>ω</span></span></span></span> 很小时 <span><span>L≈−jkiKt/(bω)L \approx -j k_i K_t/(b\omega)</span><span><span><span></span><span>L</span><span></span><span>≈</span><span></span></span><span><span></span><span>−</span><span>j</span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span>bω</span><span>)</span></span></span></span>，幅值 3000 倍的量级，所以曲线从下方无穷远处进来，向原点收拢，在 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 处穿过单位圆（左图里黑色点线），然后在延迟的作用下绕着原点螺旋收缩。它离 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 一直很远。</p><p>最近距离是 <span><span>min⁡∣1+L∣=0.9655\min|1 + L| = 0.9655</span><span><span><span></span><span>min</span><span></span><span>∣1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>0.9655</span></span></span></span>，等于 <span><span>1/Ms1/M_s</span><span><span><span></span><span>1/</span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。它出现在 <span><span>ω≈59\omega \approx 59</span><span><span><span></span><span>ω</span><span></span><span>≈</span><span></span></span><span><span></span><span>59</span></span></span></span> rad/s，那里 <span><span>L=−0.061−0.224jL = -0.061 - 0.224j</span><span><span><span></span><span>L</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.061</span><span></span><span>−</span><span></span></span><span><span></span><span>0.224</span><span>j</span></span></span></span>（我另外用 20 万点的细网格查到的，图里用 2000 点网格标出的是 59.4 rad/s）。这不是穿越单位圆的那一点（<span><span>ωc=15\omega_c = 15</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span>），而是曲线收向原点的途中，在相位快到 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span> 的区域里（右图：红色圆是以 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 为圆心、半径 <span><span>1/Ms1/M_s</span><span><span><span></span><span>1/</span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的圆，黑点是曲线和它擦边的位置）。前面的两个不等式给出 <span><span>PM≥57.7°\text{PM} \ge 57.7°</span><span><span><span></span><span><span>PM</span></span><span></span><span>≥</span><span></span></span><span><span></span><span>57.7°</span></span></span></span> 和 <span><span>GM≥29.0\text{GM} \ge 29.0</span><span><span><span></span><span><span>GM</span></span><span></span><span>≥</span><span></span></span><span><span></span><span>29.0</span></span></span></span>（29.2 dB），实测是 63.3° 和 57.2（35.1 dB）：不等式成立，PM 比下界多了 5.6°，GM 多了约 6 dB。<span><span>MsM_s</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 这个全局量把两个局部量夹在了里面。</p></section><section><h3>灵敏度和水床<a href="#灵敏度和水床"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch03-sensitivity.svg" alt="上图是 |S| 和 |T| 的 dB 值，下图是 ω ln|S|，红色面积和蓝色面积相等" /><figcaption>上图是 |S| 和 |T| 的 dB 值，下图是 ω ln|S|，红色面积和蓝色面积相等</figcaption></figure><p></p><p>上图：<span><span>∣S∣|S|</span><span><span><span></span><span>∣</span><span>S</span><span>∣</span></span></span></span> 在 1 rad/s 处是 <span><span>−39.5-39.5</span><span><span><span></span><span>−</span><span>39.5</span></span></span></span> dB，再往下越来越低，<span><span>∣T∣|T|</span><span><span><span></span><span>∣</span><span>T</span><span>∣</span></span></span></span> 在 8.0 rad/s 附近有一个 2.1 dB 的小峰（<span><span>max⁡∣T∣=1.2728\max|T| = 1.2728</span><span><span><span></span><span>max</span><span></span><span>∣</span><span>T</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>1.2728</span></span></span></span>，这是第 2 篇说的 PI 零点造成的）。<span><span>Ms=1.0357M_s = 1.0357</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.0357</span></span></span></span> 低得几乎看不出来，红色虚线只比 0 dB 线高 0.31 dB。</p><p>下图是 <span><span>ωln⁡∣S∣\omega\ln|S|</span><span><span><span></span><span>ω</span><span></span><span>ln</span><span></span><span>∣</span><span>S</span><span>∣</span></span></span></span> 对 <span><span>ln⁡ω\ln\omega</span><span><span><span></span><span>ln</span><span></span><span>ω</span></span></span></span>。这个量在对数横轴下的面积正好是 <span><span>∫ln⁡∣S∣ dω\int\ln|S|\,d\omega</span><span><span><span></span><span>∫</span><span></span><span>ln</span><span></span><span>∣</span><span>S</span><span>∣</span><span></span><span>d</span><span>ω</span></span></span></span>（因为 <span><span>dω=ω dln⁡ωd\omega = \omega\,d\ln\omega</span><span><span><span></span><span>d</span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span>ω</span><span></span><span>d</span><span></span><span>ln</span><span></span><span>ω</span></span></span></span>）。蓝色是 <span><span>∣S∣&lt;1|S| &lt; 1</span><span><span><span></span><span>∣</span><span>S</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span> 的部分，红色是 <span><span>∣S∣&gt;1|S| &gt; 1</span><span><span><span></span><span>∣</span><span>S</span><span>∣</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>1</span></span></span></span> 的部分。我做了数值积分（横轴从 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>10710^7</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span></span></span></span> rad/s，40 万个点）：</p><ul>
<li>红色面积 <span><span>+24.8890+24.8890</span><span><span><span></span><span>+</span><span>24.8890</span></span></span></span>，蓝色面积 <span><span>−24.8890-24.8890</span><span><span><span></span><span>−</span><span>24.8890</span></span></span></span>，总和 <span><span>+0.00002+0.00002</span><span><span><span></span><span>+</span><span>0.00002</span></span></span></span>。</li>
</ul><p>所以积分是 0，Bode 的积分在这个带积分器、带延迟的回路上成立，这是我想检验的。（准确说是红蓝相消到小数点后 5 位，剩下的误差来自有限的积分区间和离散化。）</p><p>这张图里有两件事我事先没想到。第一，红色那一块<strong>很高也很宽</strong>：<span><span>∣S∣|S|</span><span><span><span></span><span>∣</span><span>S</span><span>∣</span></span></span></span> 只比 1 大 3.5%，但在 18.8 到 1446 rad/s 的近两个数量级里都是正的（我从细网格里找到的两个过零点），<span><span>ωln⁡∣S∣\omega\ln|S|</span><span><span><span></span><span>ω</span><span></span><span>ln</span><span></span><span>∣</span><span>S</span><span>∣</span></span></span></span> 的峰值是 11.8，在 567 rad/s 处。低频压得越狠（蓝），高频就要用越宽的频带去还。第二，1446 rad/s 以后的几个小起伏是延迟造成的：<span><span>e−jωθe^{-j\omega\theta}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>j</span><span>ω</span><span>θ</span></span></span></span></span></span></span></span></span></span></span></span> 在高频让 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 绕着 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 的邻域转圈，<span><span>∣1+L∣|1 + L|</span><span><span><span></span><span>∣1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span><span>∣</span></span></span></span> 一会儿大于 1 一会儿小于 1，每个起伏的面积都在衰减。</p></section><section><h3>D 项：相位几乎白拿，噪声增益不是<a href="#d-项相位几乎白拿噪声增益不是"><span>#</span></a></h3><p>PID 的幅频特性在高频由 <span><span>kdk_d</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 决定。我拿同一个 PI 加 <span><span>kd=0.02k_d = 0.02</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span>（假设值），对比滤波时间常数 <span><span>tft_f</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><p></p><figure><img src="/assets/images/pid-control/ch03-d-term-noise-gain.svg" alt="PID 的增益幅值：不加滤波时随频率线性增长，奈奎斯特频率 3142 rad/s 处是 62.8，加 tf 之后被压成一条水平线" /><figcaption>PID 的增益幅值：不加滤波时随频率线性增长，奈奎斯特频率 3142 rad/s 处是 62.8，加 tf 之后被压成一条水平线</figcaption></figure><p></p><p>表里的列是穿越频率处的增益和相位超前（相对于纯 PI），以及采样的奈奎斯特频率 <span><span>π/ts=3142\pi/t_s = 3142</span><span><span><span></span><span>π</span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>3142</span></span></span></span> rad/s 处的增益：</p><ul>
<li><span><span>tf=0t_f = 0</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：穿越点处增益 1.4122、超前 11.16°，奈奎斯特处增益 62.844，是 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 46.0 倍。</li>
<li><span><span>tf=0.001t_f = 0.001</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.001</span></span></span></span>：1.4167、11.20°，奈奎斯特处 20.363（高频极限 <span><span>kp+kd/tf=21.367k_p + k_d/t_f = 21.367</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>21.367</span></span></span></span>）。</li>
<li><span><span>tf=0.005t_f = 0.005</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.005</span></span></span></span>：1.4348、11.32°，奈奎斯特处 5.356。</li>
<li><span><span>tf=0.02t_f = 0.02</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span>：1.5003、11.02°，奈奎斯特处 2.366。</li>
</ul><p><strong>噪声增益从 62.8 降到 20.4（3.1 倍），在穿越频率处的相位超前只动了 0.04°。</strong> 原因是 <span><span>ωc=15\omega_c = 15</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15</span></span></span></span> rad/s 比 <span><span>1/tf=10001/t_f = 1000</span><span><span><span></span><span>1/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1000</span></span></span></span> rad/s 低得多，滤波器在穿越点附近几乎没有相位代价。我猜这是实际调 D 项最容易被忽视的免费午餐：只要 <span><span>1/tf1/t_f</span><span><span><span></span><span>1/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 比 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 高一个数量级以上，滤波不会伤到相位裕度。<span><span>tft_f</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 增大到 0.02 s（<span><span>1/tf=501/t_f = 50</span><span><span><span></span><span>1/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>50</span></span></span></span> rad/s，离 <span><span>ωc\omega_c</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 只有 3 倍）时超前降到 11.02°，开始比 0.005 s 时低，但变化仍然很小；这里的 <span><span>kdk_d</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 本来就小，相位超前只有 11° 的量级，D 项在这个回路上不是主角。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 pid-lab 根目录（<code>nix develop</code> 里）运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch03</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出见上一节：12 个 <code>ok</code> 对比行（<code>bode</code>、<code>margins</code>、<code>delay_0.9</code>、<code>delay_1.1</code> 各对 C、C++、Rust），最大差值 0 到 <span><span>5.684×10−135.684\times 10^{-13}</span><span><span><span></span><span>5.684</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，容差 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>；然后是裕度表（<span><span>ωc=15.1683\omega_c = 15.1683</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15.1683</span></span></span></span>，PM 63.334°，GM 35.14 dB，<span><span>Ms=1.0357M_s = 1.0357</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.0357</span></span></span></span>，<span><span>Dm=72.875D_m = 72.875</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>72.875</span></span></span></span> ms）、延迟检验（0.9 倍斜率 <span><span>−1.063-1.063</span><span><span><span></span><span>−</span><span>1.063</span></span></span></span>/s，1.1 倍 <span><span>+0.806+0.806</span><span><span><span></span><span>+</span><span>0.806</span></span></span></span>/s）、灵敏度积分和 D 项增益表，最后一行是 <code>ch03 OK</code>。<code>report</code> 里的断言（0.9 倍衰减且没碰限幅，1.1 倍增长且碰限幅，Bode 积分为零，精确采样模型的边界落在 72 和 73 拍之间）有任何一条不成立都会退出 1。</p><p><strong>本篇新增的文件</strong>（tag <code>tutorial-v1</code>，本篇还会用到第 1、2 篇的 <code>sim</code>、<code>plants</code>、<code>poly</code>、<code>complex</code>，它们没有变化）：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/freq.py" target="_blank"><code>python/pidlab/freq.py</code></a></td><td><code>make -s ch03</code></td><td>频率响应、相位展开、<code>margins</code>；对 <code>scipy.signal.freqresp</code> 幅值差 <span><span>1.42×10−141.42\times 10^{-14}</span><span><span><span></span><span>1.42</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span> dB</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/freq.h" target="_blank"><code>c/include/pl/freq.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/freq.c" target="_blank"><code>c/src/freq.c</code></a></td><td><code>make -s ch03</code></td><td><code>bode_c.csv</code>、<code>margins_c.csv</code>、两个延迟文件与 Python 最大差 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/real.h" target="_blank"><code>c/include/pl/real.h</code></a></td><td>同上</td><td>多了 <code>PL_LOG10</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/freq.h" target="_blank"><code>cpp/include/pidlab/freq.h</code></a></td><td><code>make -s ch03</code></td><td><code>*_cpp.csv</code> 与 Python 最大差 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/freq.rs" target="_blank"><code>rust/src/freq.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/real.rs" target="_blank"><code>rust/src/real.rs</code></a></td><td><code>make -s ch03</code></td><td><code>bode_rs.csv</code> 差 <span><span>5.684×10−135.684\times 10^{-13}</span><span><span><span></span><span>5.684</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，<code>margins_rs.csv</code> 差 <span><span>3.553×10−153.553\times 10^{-15}</span><span><span><span></span><span>3.553</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>；<code>real.rs</code> 多了 <code>log10</code></td></tr></tbody></table><p>驱动和应用：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch03_frequency_domain.py" target="_blank"><code>python/ch03_frequency_domain.py</code></a></td><td><code>python3 python/ch03_frequency_domain.py ref</code> 或 <code>report</code></td><td><code>ref</code> 写数据和 Python 参考输出；<code>report</code> 比对、打印表、断言、画 5 张图</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch03.c" target="_blank"><code>c/apps/ch03.c</code></a></td><td><code>make -s ch03</code></td><td><code>ch03: C wrote bode, margins and 2 delay runs to out/ch03</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch03.cc" target="_blank"><code>cpp/apps/ch03.cc</code></a></td><td><code>make -s ch03</code></td><td><code>ch03: C++ wrote ...</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch03.rs" target="_blank"><code>rust/src/bin/ch03.rs</code></a></td><td><code>make -s ch03</code></td><td><code>ch03: Rust wrote ...</code></td></tr></tbody></table><p><code>figures/ch03/</code> 下是 <code>bode</code>、<code>nyquist</code>、<code>delay-margin-check</code>、<code>sensitivity</code>、<code>d-term-noise-gain</code> 5 张 SVG，它们是 <code>report</code> 在本地生成的（<code>figures/</code> 不进仓库）。</p></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把“稳定的边界”变成了几个能读出来的数：63.3° 的相位裕度、35.1 dB 的增益裕度、72.875 ms 的延迟裕度，和一个把三者统一起来的 <span><span>Ms=1.036M_s = 1.036</span><span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.036</span></span></span></span>。延迟裕度的预测经受住了 0.9 和 1.1 倍的检验，包络斜率 <span><span>−1.063-1.063</span><span><span><span></span><span>−</span><span>1.063</span></span></span></span>/s 和 <span><span>+0.806+0.806</span><span><span><span></span><span>+</span><span>0.806</span></span></span></span>/s 跟精确离散模型的 <span><span>−1.064-1.064</span><span><span><span></span><span>−</span><span>1.064</span></span></span></span>/s 和 <span><span>+0.851+0.851</span><span><span><span></span><span>+</span><span>0.851</span></span></span></span>/s 一致。同时我也发现一个不起眼的细节会决定整数拍的答案：ZOH 的半个采样周期。</p><p><a href="/posts/pid-control-04-discretization/">第 4 篇：离散化：位置式、增量式与采样率</a>会把这个半拍放到台面上来。第 2 篇的 PI（这一篇取 <span><span>ωn=30\omega_n = 30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span>），我把采样周期从 1 ms 拉到 5、10、20 ms，看相位裕度掉多少、超调涨多少；再推位置式和增量式 PID 的等价关系，对比后向差分和 Tustin 两种 D 项离散化的振铃。这一篇手算的“半拍”，到时候会变成一张表。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-02-transfer-functions/</id>
      <title type="text">传递函数、极点与根轨迹</title>
      <published>2026-10-16T01:00:00.000Z</published>
      <updated>2026-10-16T01:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-02-transfer-functions/"/>
      <summary type="text">PID 控制系列第 2 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95 把多项式求根、阶跃响应指标和两个新对象写了四遍，用拉普拉斯变换推出二阶原型和极点配置公式，再跑 14 组仿真验证：同样的极点配置下 PI 超调 20.29%、I-P 只有 4.70%，Routh 判据给出的临界增益在 1 kHz 采样下少了 20%。</summary>
      <content type="html"><![CDATA[<p>上一篇留下了两笔债。第一笔：PI 速度环的超调实测 28.02%，二阶公式预测 15.0%，我当时只能说“大概是零点”。第二笔：<span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KiK_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KdK_d</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 全是靠手感试出来的。这一篇还这两笔债，工具是拉普拉斯变换。同一组 <span><span>Kp=1K_p = 1</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>Ki=10K_i = 10</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>，把比例项改成只作用在 <span><span>−y-y</span><span><span><span></span><span>−</span><span>y</span></span></span></span> 上之后，超调从 28.29% 降到 15.30%，而二阶公式预测的是 15.02%；反过来把闭环零点算进公式，预测是 27.94%。债还清之后，<span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>KiK_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就不用试了，给定 <span><span>ωn\omega_n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 直接算。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch02</code> 通过，<code>out/ch02/</code> 下 68 个 CSV（14 组仿真、一份 <code>metrics</code>、两条根轨迹，每组 Python、C、C++、Rust 各一份），<code>figures/ch02/</code> 下 4 张图；以及拉普拉斯变换、二阶原型、极点配置、Routh 判据、根轨迹的完整推导，和一个对着 <code>numpy.roots</code> 验证过的手写求根器。
新增模块：Python 是 <code>pidlab/poly.py</code>（多项式与求根）、<code>pidlab/metrics.py</code>（阶跃指标），<code>pidlab/plants.py</code> 里多了 <code>MotorSpeedLag</code> 和 <code>DialPosition</code>；C 是 <code>pl/poly.h</code>、<code>pl/metrics.h</code> 及对应的 <code>.c</code>，<code>pl/plants.h</code> 里多了两个对象，<code>pl/real.h</code> 多了复数类型 <code>pl_cplx</code>；C++ 是 <code>pidlab/poly.h</code>、<code>pidlab/metrics.h</code>，<code>pidlab/plants.h</code> 里多了两个对象；Rust 是 <code>complex.rs</code>、<code>poly.rs</code>、<code>metrics.rs</code>，<code>plants.rs</code> 里多了两个对象。
前置：<a href="/posts/pid-control-01-p-i-d/">第 1 篇</a>。需要的数学：复数、一元多项式、一阶和二阶常微分方程，会解一元二次方程。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>拉普拉斯变换：把微分方程变成代数方程<a href="#拉普拉斯变换把微分方程变成代数方程"><span>#</span></a></h3><p>函数 <span><span>f(t)f(t)</span><span><span><span></span><span>f</span><span>(</span><span>t</span><span>)</span></span></span></span>（<span><span>t≥0t \ge 0</span><span><span><span></span><span>t</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>）的拉普拉斯变换定义为</p><span><span><span>F(s)=L{f}(s)=∫0∞f(t) e−st dtF(s) = \mathcal{L}\{f\}(s) = \int_0^\infty f(t)\,e^{-st}\,dt</span><span><span><span></span><span>F</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span>{</span><span>f</span><span>}</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>f</span><span>(</span><span>t</span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>d</span><span>t</span></span></span></span></span><p><span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 是复数。我在这里只需要三条性质，每一条都值得自己推一遍。</p><p><strong>微分变成乘以 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span>。</strong> 分部积分：</p><span><span><span>L{f˙}=∫0∞f˙(t) e−st dt=[f(t)e−st]0∞+s∫0∞f(t) e−st dt=sF(s)−f(0)\mathcal{L}\{\dot f\} = \int_0^\infty \dot f(t)\,e^{-st}\,dt = \Big[f(t)e^{-st}\Big]_0^\infty + s\int_0^\infty f(t)\,e^{-st}\,dt = sF(s) - f(0)</span><span><span><span></span><span>L</span><span>{</span><span><span><span><span><span><span></span><span>f</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>}</span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span>f</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span><span>[</span></span><span>f</span><span>(</span><span>t</span><span>)</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>t</span></span></span></span></span></span></span></span></span><span><span><span>]</span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>s</span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>f</span><span>(</span><span>t</span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>s</span><span>F</span><span>(</span><span>s</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>f</span><span>(</span><span>0</span><span>)</span></span></span></span></span><p>初值为零时，时域里的 <span><span>d/dtd/dt</span><span><span><span></span><span>d</span><span>/</span><span>d</span><span>t</span></span></span></span> 就是 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 域里的乘以 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span>。这一条的用处是把 <span><span>Jω˙+bω=KtiJ\dot\omega + b\omega = K_t i</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>bω</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span></span></span></span> 这种常微分方程变成 <span><span>(Js+b) Ω(s)=KtI(s)(Js + b)\,\Omega(s) = K_t I(s)</span><span><span><span></span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>)</span><span></span><span>Ω</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>I</span><span>(</span><span>s</span><span>)</span></span></span></span>，求解变成除法。</p><p><strong>两个基本变换。</strong> 单位阶跃 <span><span>1(t)1(t)</span><span><span><span></span><span>1</span><span>(</span><span>t</span><span>)</span></span></span></span> 的变换是 <span><span>∫0∞e−stdt=1/s\int_0^\infty e^{-st}dt = 1/s</span><span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>s</span><span>t</span></span></span></span></span></span></span></span></span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>1/</span><span>s</span></span></span></span>；指数 <span><span>e−ate^{-at}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>a</span><span>t</span></span></span></span></span></span></span></span></span></span></span></span> 的变换是 <span><span>1/(s+a)1/(s + a)</span><span><span><span></span><span>1/</span><span>(</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>a</span><span>)</span></span></span></span>。有了它们和线性性质，反变换基本靠部分分式，把 <span><span>F(s)F(s)</span><span><span><span></span><span>F</span><span>(</span><span>s</span><span>)</span></span></span></span> 拆成这两种形式的和。</p><p><strong>终值定理。</strong> 如果 <span><span>sF(s)sF(s)</span><span><span><span></span><span>s</span><span>F</span><span>(</span><span>s</span><span>)</span></span></span></span> 的极点全在左半平面（<span><span>ff</span><span><span><span></span><span>f</span></span></span></span> 真的收敛），那么 <span><span>f(∞)=lim⁡s→0sF(s)f(\infty) = \lim_{s \to 0} sF(s)</span><span><span><span></span><span>f</span><span>(</span><span>∞</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>lim</span><span><span><span><span><span><span></span><span><span><span>s</span><span>→</span><span>0</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>s</span><span>F</span><span>(</span><span>s</span><span>)</span></span></span></span>。推导：对 <span><span>f˙\dot f</span><span><span><span></span><span><span><span><span><span><span></span><span>f</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 的变换令 <span><span>s→0s \to 0</span><span><span><span></span><span>s</span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span>，左边是 <span><span>∫0∞f˙ dt=f(∞)−f(0)\int_0^\infty \dot f\,dt = f(\infty) - f(0)</span><span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span>f</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span>∞</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>f</span><span>(</span><span>0</span><span>)</span></span></span></span>，右边是 <span><span>lim⁡sF(s)−f(0)\lim sF(s) - f(0)</span><span><span><span></span><span>lim</span><span></span><span>s</span><span>F</span><span>(</span><span>s</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>f</span><span>(</span><span>0</span><span>)</span></span></span></span>，两边消掉 <span><span>f(0)f(0)</span><span><span><span></span><span>f</span><span>(</span><span>0</span><span>)</span></span></span></span>。条件不能省：对不稳定的系统这个公式仍然会给你一个漂亮的有限数，只是和真实响应毫无关系。后面的 Routh 判据就是用来先回答“它到底收敛吗”。</p><p>用终值定理把第 1 篇的静差重算一遍，当作对工具的检验。纯 P 控制的速度环，开环是 <span><span>G(s)=Kt/(Js+b)G(s) = K_t/(Js + b)</span><span><span><span></span><span>G</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>)</span></span></span></span>，闭环 <span><span>T=KtKp/(Js+b+KtKp)T = K_tK_p/(Js + b + K_tK_p)</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，阶跃 <span><span>R=r/sR = r/s</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span>/</span><span>s</span></span></span></span>，所以</p><span><span><span>ω(∞)=lim⁡s→0s⋅KtKpJs+b+KtKp⋅rs=KtKpb+KtKp r\omega(\infty) = \lim_{s\to 0} s\cdot\frac{K_tK_p}{Js + b + K_tK_p}\cdot\frac{r}{s} = \frac{K_tK_p}{b + K_tK_p}\,r</span><span><span><span></span><span>ω</span><span>(</span><span>∞</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>s</span><span>→</span><span>0</span></span></span></span><span><span></span><span><span>lim</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>s</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span><span>s</span><span></span><span>+</span><span></span><span>b</span><span></span><span>+</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span></span></span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>b</span><span></span><span>+</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>r</span></span></span></span></span><p><span><span>ess=r−ω(∞)=b r/(b+KtKp)e_{ss} = r - \omega(\infty) = b\,r/(b + K_tK_p)</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ω</span><span>(</span><span>∞</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>r</span><span>/</span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，和第 1 篇令导数为零得到的结果一致（没有负载的情形）。</p></section><section><h3>传递函数：每个对象写成一个分式<a href="#传递函数每个对象写成一个分式"><span>#</span></a></h3><p>初值为零时，输出的变换除以输入的变换叫传递函数。这一篇和后面几篇用到的对象是（参数沿用第 1 篇，新增的两个我标明）：</p>

<table><thead><tr><th>对象</th><th>传递函数</th><th>参数</th></tr></thead><tbody><tr><td><code>MotorSpeed</code></td><td><span><span>ΩI=KtJs+b\dfrac{\Omega}{I} = \dfrac{K_t}{Js + b}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>I</span></span></span><span><span></span><span></span></span><span><span></span><span><span>Ω</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span><span>s</span><span></span><span>+</span><span></span><span>b</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td><span><span>Kt=0.3K_t = 0.3</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span>，<span><span>J=0.03J = 0.03</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.03</span></span></span></span>，<span><span>b=0.01b = 0.01</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span></td></tr><tr><td><code>MotorSpeedLag</code>（新）</td><td><span><span>ΩIcmd=Kt(Js+b)(τcs+1)\dfrac{\Omega}{I_{cmd}} = \dfrac{K_t}{(Js + b)(\tau_c s + 1)}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>Ω</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span><span>b</span><span>)</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span>1</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td>同上，电流环时间常数 <span><span>τc=0.001\tau_c = 0.001</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.001</span></span></span></span> s（我取的）</td></tr><tr><td><code>GimbalAxis</code>（<span><span>ωc=0\omega_c = 0</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>）</td><td><span><span>ΨT=1s (Js+b+bf)\dfrac{\Psi}{T} = \dfrac{1}{s\,(Js + b + b_f)}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>T</span></span></span><span><span></span><span></span></span><span><span></span><span><span>Ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span><span></span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span><span>b</span><span></span><span>+</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td><span><span>J=0.02J = 0.02</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span>，<span><span>b=0.002b = 0.002</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.002</span></span></span></span>，<span><span>bf=0.02b_f = 0.02</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span></td></tr><tr><td><code>DialPosition</code>（新）</td><td><span><span>ΘIcmd=Kts (Js+b)(τcs+1)\dfrac{\Theta}{I_{cmd}} = \dfrac{K_t}{s\,(Js + b)(\tau_c s + 1)}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>I</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>Θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span><span></span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span><span>b</span><span>)</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span>1</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></td><td><span><span>Kt=0.3K_t = 0.3</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span>，<span><span>J=0.005J = 0.005</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.005</span></span></span></span>，<span><span>b=0.01b = 0.01</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span>，<span><span>τc=0.002\tau_c = 0.002</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.002</span></span></span></span> s</td></tr></tbody></table><p><code>MotorSpeedLag</code> 多出来的一阶因子是电流环：电调不会瞬间把电流拉到指令值，我把它建成 <span><span>τc di/dt=icmd−i\tau_c\,di/dt = i_{cmd} - i</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>d</span><span>i</span><span>/</span><span>d</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>i</span></span></span></span>。<code>DialPosition</code> 是拨弹盘上的 M3508，状态是角度、角速度和电流，输出是角度。它的 <span><span>J=0.005J = 0.005</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.005</span></span></span></span> 和 <span><span>τc=0.002\tau_c = 0.002</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.002</span></span></span></span> s 都是我取的数（量级合理，没有实测），所以下面所有关于它的数字都应该读成“这个形状”，不是“你车上的数”。</p><p>PID 控制器是 <span><span>C(s)=Kp+Ki/s+KdsC(s) = K_p + K_i/s + K_ds</span><span><span><span></span><span>C</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span></span></span></span>。把控制器和对象首尾相连，开环 <span><span>L(s)=C(s)G(s)L(s) = C(s)G(s)</span><span><span><span></span><span>L</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>s</span><span>)</span><span>G</span><span>(</span><span>s</span><span>)</span></span></span></span>，闭环有两个值得命名的函数：</p><span><span><span>T(s)=L1+L,S(s)=11+L,T+S=1T(s) = \frac{L}{1 + L},\qquad S(s) = \frac{1}{1 + L},\qquad T + S = 1</span><span><span><span></span><span>T</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>L</span></span></span><span><span></span><span></span></span><span><span></span><span><span>L</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span>S</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>L</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span>T</span><span></span><span>+</span><span></span></span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span></span><p><span><span>TT</span><span><span><span></span><span>T</span></span></span></span> 管设定值到输出，<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 管误差。第 1 篇的负载 <span><span>τd\tau_d</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从对象输入端进来，等价于电流扰动 <span><span>τd/Kt\tau_d/K_t</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以 <span><span>Y=T R−S G τd/KtY = T\,R - S\,G\,\tau_d/K_t</span><span><span><span></span><span>Y</span><span></span><span>=</span><span></span></span><span><span></span><span>T</span><span></span><span>R</span><span></span><span>−</span><span></span></span><span><span></span><span>S</span><span></span><span>G</span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。两个函数的分母都是 <span><span>1+L1 + L</span><span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>L</span></span></span></span> 的分子，也就是<strong>特征多项式</strong>，它的根叫闭环极点。系统的所有自由运动都是极点的组合：每个极点 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 贡献一项 <span><span>epte^{pt}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>pt</span></span></span></span></span></span></span></span></span></span></span></span>。（类比一下琴弦：拨的位置和力度只决定每个音的音量，响的永远是弦自己的音高，极点就是系统的音高。）这一篇所有的事归结为一句话：<strong>极点在哪，响应就长什么样；<span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KiK_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KdK_d</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的作用是把极点搬到我要的地方。</strong></p></section><section><h3>二阶原型：阶跃响应的完整推导<a href="#二阶原型阶跃响应的完整推导"><span>#</span></a></h3><p>很多闭环最后都化成同一个形状，我把它叫原型：</p><span><span><span>T0(s)=ωn2s2+2ζωns+ωn2T_0(s) = \frac{\omega_n^2}{s^2 + 2\zeta\omega_n s + \omega_n^2}</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><span><span>ωn\omega_n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是固有频率，<span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 是阻尼比。<span><span>0&lt;ζ&lt;10 &lt; \zeta &lt; 1</span><span><span><span></span><span>0</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>ζ</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span></span></span></span> 时极点是一对共轭复数 <span><span>−σ±jωd-\sigma \pm j\omega_d</span><span><span><span></span><span>−</span><span>σ</span><span></span><span>±</span><span></span></span><span><span></span><span>j</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>σ=ζωn\sigma = \zeta\omega_n</span><span><span><span></span><span>σ</span><span></span><span>=</span><span></span></span><span><span></span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>ωd=ωn1−ζ2\omega_d = \omega_n\sqrt{1 - \zeta^2}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span>ζ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。单位阶跃响应的变换是 <span><span>Y0=T0/sY_0 = T_0/s</span><span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>s</span></span></span></span>，拆成部分分式：</p><span><span><span>Y0(s)=1s−s+2σ(s+σ)2+ωd2=1s−s+σ(s+σ)2+ωd2−σωd⋅ωd(s+σ)2+ωd2Y_0(s) = \frac{1}{s} - \frac{s + 2\sigma}{(s + \sigma)^2 + \omega_d^2} = \frac{1}{s} - \frac{s + \sigma}{(s + \sigma)^2 + \omega_d^2} - \frac{\sigma}{\omega_d}\cdot\frac{\omega_d}{(s + \sigma)^2 + \omega_d^2}</span><span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>s</span><span></span><span>+</span><span></span><span>σ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>s</span><span></span><span>+</span><span></span><span>2</span><span>σ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>s</span><span></span><span>+</span><span></span><span>σ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>s</span><span></span><span>+</span><span></span><span>σ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>σ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>s</span><span></span><span>+</span><span></span><span>σ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>后两项是 <span><span>e−σtcos⁡ωdte^{-\sigma t}\cos\omega_d t</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>σ</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span> 和 <span><span>e−σtsin⁡ωdte^{-\sigma t}\sin\omega_d t</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>σ</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span> 的变换，所以</p><span><span><span>y0(t)=1−e−σt(cos⁡ωdt+σωdsin⁡ωdt)y_0(t) = 1 - e^{-\sigma t}\left(\cos\omega_d t + \frac{\sigma}{\omega_d}\sin\omega_d t\right)</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>σ</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span><span><span>(</span></span><span>cos</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>σ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>sin</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span><span><span>)</span></span></span></span></span></span></span><p>对 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span> 求导，<span><span>y˙0(t)=ωn1−ζ2 e−σtsin⁡ωdt\dot y_0(t) = \dfrac{\omega_n}{\sqrt{1-\zeta^2}}\,e^{-\sigma t}\sin\omega_d t</span><span><span><span></span><span><span><span><span><span><span><span></span><span>y</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span>ζ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>σ</span><span>t</span></span></span></span></span></span></span></span></span><span></span><span>sin</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span>（这也是原型的脉冲响应）。令它为零，第一个解是 <span><span>ωdt=π\omega_d t = \pi</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>π</span></span></span></span>：</p><span><span><span>tp=πωd,y0(tp)=1+e−σπ/ωd  ⇒  Mp=e−πζ/1−ζ2t_p = \frac{\pi}{\omega_d},\qquad y_0(t_p) = 1 + e^{-\sigma\pi/\omega_d} \;\Rightarrow\; M_p = e^{-\pi\zeta/\sqrt{1 - \zeta^2}}</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>π</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>σ</span><span>π</span><span>/</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span></span><span></span><span>⇒</span><span></span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>π</span><span>ζ</span><span>/</span><span><span><span><span><span><span></span><span><span>1</span><span>−</span><span><span>ζ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><p>这就是第 1 篇一直在用的超调公式。调节时间看包络线：<span><span>e−σt/1−ζ2e^{-\sigma t}/\sqrt{1-\zeta^2}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>σ</span><span>t</span></span></span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span>ζ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 降到 0.02 以下的时间大约是 <span><span>4/σ4/\sigma</span><span><span><span></span><span>4/</span><span>σ</span></span></span></span>，所以 <span><span>ts≈4/(ζωn)t_s \approx 4/(\zeta\omega_n)</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>4/</span><span>(</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>（一条经验公式，<span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 很小或接近 1 时会偏）。<span><span>ζ=1\zeta = 1</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 是临界阻尼，<span><span>y0=1−(1+ωnt)e−ωnty_0 = 1 - (1 + \omega_n t)e^{-\omega_n t}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span><span>)</span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>t</span></span></span></span></span></span></span></span></span></span></span></span>，没有超调；<span><span>ζ&gt;1\zeta &gt; 1</span><span><span><span></span><span>ζ</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>1</span></span></span></span> 是两个实极点，也没有超调，只是更慢。上升时间没有这么干净的闭式，我让仿真去量。</p></section><section><h3>极点配置：把 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KdK_d</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 变成算出来的数<a href="#极点配置把-kpk_pkpkdk_dkd-变成算出来的数"><span>#</span></a></h3><p><strong>云台。</strong> 力矩指令的云台，<span><span>Jψ¨=τ−(b+bf)ψ˙J\ddot\psi = \tau - (b + b_f)\dot\psi</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。我这次不用第 1 篇的误差微分，而是用角速度反馈：<span><span>τ=Kp(r−ψ)−Kdψ˙meas\tau = K_p(r - \psi) - K_d\dot\psi_{meas}</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ψ</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>m</span><span>e</span><span>a</span><span>s</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（这样设定值的跳变不会被微分成脉冲，第 5 篇会专门处理这件事）。代入：</p><span><span><span>Jψ¨+(b+bf+Kd)ψ˙+Kpψ=Kpr  ⇒  T(s)=Kp/Js2+b+bf+KdJs+KpJJ\ddot\psi + (b + b_f + K_d)\dot\psi + K_p\psi = K_p r \;\Rightarrow\; T(s) = \frac{K_p/J}{s^2 + \dfrac{b + b_f + K_d}{J}s + \dfrac{K_p}{J}}</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ψ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>r</span><span></span><span></span><span>⇒</span><span></span><span></span></span><span><span></span><span>T</span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span></span></span><span><span></span><span></span></span><span><span></span><span><span>b</span><span></span><span>+</span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>s</span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>J</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>这恰好是原型，没有任何零点。对照 <span><span>2ζωn=(b+bf+Kd)/J2\zeta\omega_n = (b + b_f + K_d)/J</span><span><span><span></span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span>J</span></span></span></span> 和 <span><span>ωn2=Kp/J\omega_n^2 = K_p/J</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>J</span></span></span></span>：</p><span><span><span>Kp=Jωn2,Kd=2ζωnJ−(b+bf)K_p = J\omega_n^2,\qquad K_d = 2\zeta\omega_n J - (b + b_f)</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>J</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></span><p>注意括号里是 <span><span>b+bfb + b_f</span><span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：底盘不动时，轴承耦合 <span><span>bfb_f</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 对云台就是一个额外的阻尼。只减 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 的话，得到的 <span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 会比目标少一点点。<span><span>ωn=20\omega_n = 20</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> rad/s、<span><span>J=0.02J = 0.02</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span> 时 <span><span>Kp=8K_p = 8</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>8</span></span></span></span>，<span><span>ζ=0.2, 0.4, 0.7, 1\zeta = 0.2,\ 0.4,\ 0.7,\ 1</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span><span>,</span><span> </span><span></span><span>0.4</span><span>,</span><span> </span><span></span><span>0.7</span><span>,</span><span> </span><span></span><span>1</span></span></span></span> 对应 <span><span>Kd=0.1380, 0.2980, 0.5380, 0.7780K_d = 0.1380,\ 0.2980,\ 0.5380,\ 0.7780</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1380</span><span>,</span><span> </span><span></span><span>0.2980</span><span>,</span><span> </span><span></span><span>0.5380</span><span>,</span><span> </span><span></span><span>0.7780</span></span></span></span>。</p><p><strong>速度环 PI。</strong> <span><span>u=Kpe+Ki∫e dtu = K_p e + K_i\int e\,dt</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span></span><span>e</span><span></span><span>d</span><span>t</span></span></span></span>，对象 <span><span>Kt/(Js+b)K_t/(Js + b)</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>)</span></span></span></span>。特征多项式是 <span><span>s(Js+b)+Kt(Kps+Ki)s(Js + b) + K_t(K_ps + K_i)</span><span><span><span></span><span>s</span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>：</p><span><span><span>Js2+(b+KtKp) s+KtKi=J(s2+2ζωns+ωn2)Js^2 + (b + K_tK_p)\,s + K_tK_i = J\left(s^2 + 2\zeta\omega_n s + \omega_n^2\right)</span><span><span><span></span><span>J</span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>J</span><span></span><span><span><span>(</span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span></span></span></span></span></span></span><span><span><span>Kp=2ζωnJ−bKt,Ki=ωn2JKtK_p = \frac{2\zeta\omega_n J - b}{K_t},\qquad K_i = \frac{\omega_n^2 J}{K_t}</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span></span><span>−</span><span></span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p><span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>、<span><span>ωn=10\omega_n = 10</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> rad/s 给出 <span><span>Kp=1.367K_p = 1.367</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.367</span></span></span></span>、<span><span>Ki=10K_i = 10</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>；<span><span>ωn=30\omega_n = 30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span> rad/s 给出 <span><span>Kp=4.167K_p = 4.167</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>4.167</span></span></span></span>、<span><span>Ki=90K_i = 90</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>90</span></span></span></span>。</p></section><section><h3>闭环零点：PI 为什么超调<a href="#闭环零点pi-为什么超调"><span>#</span></a></h3><p>极点对了不代表响应是原型，因为分子还没看。PI 的闭环传递函数是</p><span><span><span>TPI(s)=Kt(Kps+Ki)Js2+(b+KtKp)s+KtKi=ωn2 1+s/zs2+2ζωns+ωn2,z=KiKpT_{PI}(s) = \frac{K_t(K_ps + K_i)}{Js^2 + (b + K_tK_p)s + K_tK_i} = \omega_n^2\,\frac{1 + s/z}{s^2 + 2\zeta\omega_n s + \omega_n^2},\qquad z = \frac{K_i}{K_p}</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span><span>P</span><span>I</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>s</span><span></span><span>+</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>+</span><span></span><span>s</span><span>/</span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>分子 <span><span>Kps+KiK_ps + K_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 <span><span>s=−zs = -z</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>z</span></span></span></span> 处为零，这是闭环零点。它的效果很容易算：<span><span>TPI=(1+s/z) T0T_{PI} = (1 + s/z)\,T_0</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span><span>P</span><span>I</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>s</span><span>/</span><span>z</span><span>)</span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以 <span><span>YPI=Y0+sY0/zY_{PI} = Y_0 + sY_0/z</span><span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span><span>P</span><span>I</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>Y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>s</span><span><span>Y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>z</span></span></span></span>，而 <span><span>sY0sY_0</span><span><span><span></span><span>s</span><span><span>Y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 正好是 <span><span>y˙0\dot y_0</span><span><span><span></span><span><span><span><span><span><span><span></span><span>y</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的变换（<span><span>y0(0)=0y_0(0) = 0</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>0</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>），时域里就是</p><span><span><span>yPI(t)=y0(t)+1z y˙0(t)y_{PI}(t) = y_0(t) + \frac{1}{z}\,\dot y_0(t)</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>P</span><span>I</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>z</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span><span></span><span>y</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span></span></span></span></span><p>原型的 <span><span>y˙0\dot y_0</span><span><span><span></span><span><span><span><span><span><span><span></span><span>y</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在到达峰值之前一直是正的，所以零点把一份“速度”加到了位置上，让输出更早、更猛地冲向设定值，超调自然变大。<span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 越靠近 <span><span>ωn\omega_n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 影响越大。</p><p>如果我只想要积分器给的极点、不想要零点，办法是让比例项只作用在测量值上：<span><span>u=−Kpy+Ki∫(r−y) dtu = -K_p y + K_i\int(r - y)\,dt</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>y</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>)</span><span></span><span>d</span><span>t</span></span></span></span>，我叫它 I-P。这时 <span><span>U=−KpY+Ki(R−Y)/sU = -K_pY + K_i(R - Y)/s</span><span><span><span></span><span>U</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>R</span><span></span><span>−</span><span></span></span><span><span></span><span>Y</span><span>)</span><span>/</span><span>s</span></span></span></span>，代进对象得 <span><span>TIP=KtKi/(Js2+(b+KtKp)s+KtKi)T_{IP} = K_tK_i/(Js^2 + (b + K_tK_p)s + K_tK_i)</span><span><span><span></span><span><span>T</span><span><span><span><span><span><span></span><span><span><span>I</span><span>P</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span>J</span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，分子是常数，<strong>正好是原型</strong>，超调只由 <span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 决定。代价是设定值进来的路径少了 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 那一份，响应会慢。第 5 篇会把这个做成一个参数 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span>（设定值权重，<span><span>b=1b = 1</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 是 PI，<span><span>b=0b = 0</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 是 I-P）。</p><p>先把第 1 篇的账算清楚：<span><span>Kp=1K_p = 1</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>Ki=10K_i = 10</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时 <span><span>ωn=10\omega_n = 10</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>、<span><span>ζ=0.517\zeta = 0.517</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.517</span></span></span></span>、<span><span>z=10z = 10</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>。原型公式给 15.02%，带上这个零点是 27.94%，第 1 篇实测 28.02%。三个数对得上，<strong>那 13 个百分点就是零点。</strong></p></section><section><h3>Routh–Hurwitz：三阶系统稳定的条件<a href="#routhhurwitz三阶系统稳定的条件"><span>#</span></a></h3><p>拨弹盘用 P 控制 <span><span>u=Keu = K e</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>K</span><span>e</span></span></span></span>，<span><span>e=r−θe = r - \theta</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>θ</span></span></span></span>，特征多项式是 <span><span>s(Js+b)(τcs+1)+KKts(Js + b)(\tau_cs + 1) + KK_t</span><span><span><span></span><span>s</span><span>(</span><span>J</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>)</span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>Jτc s3+(J+bτc) s2+b s+KKt=0J\tau_c\,s^3 + (J + b\tau_c)\,s^2 + b\,s + KK_t = 0</span><span><span><span></span><span>J</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>J</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>一般的三阶 <span><span>a3s3+a2s2+a1s+a0a_3s^3 + a_2s^2 + a_1s + a_0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（系数全正），它在稳定与不稳定之间的边界上有一对极点正好落在虚轴上。令 <span><span>s=jωs = j\omega</span><span><span><span></span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span>j</span><span>ω</span></span></span></span>：</p><span><span><span>(a0−a2ω2)+j (a1ω−a3ω3)=0  ⇒  ω2=a1a3,a0=a2ω2=a2a1a3(a_0 - a_2\omega^2) + j\,(a_1\omega - a_3\omega^3) = 0 \;\Rightarrow\; \omega^2 = \frac{a_1}{a_3},\quad a_0 = a_2\omega^2 = \frac{a_2a_1}{a_3}</span><span><span><span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>j</span><span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ω</span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span></span><span></span><span>⇒</span><span></span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>所以边界是 <span><span>a2a1=a3a0a_2a_1 = a_3a_0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，稳定条件是 <span><span>a2a1&gt;a3a0a_2a_1 &gt; a_3a_0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。这就是 Routh 阵列第一列第三项 <span><span>(a2a1−a3a0)/a2(a_2a_1 - a_3a_0)/a_2</span><span><span><span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 为正的条件（阵列的前两行是 <span><span>a3,a1a_3, a_1</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>a2,a0a_2, a_0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，第三行 <span><span>(a2a1−a3a0)/a2(a_2a_1 - a_3a_0)/a_2</span><span><span><span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，第四行 <span><span>a0a_0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，第一列不变号就稳定）。代入拨弹盘：</p><span><span><span>Kcrit=(J+bτc) bJτcKt,ωc=bJτcK_{crit} = \frac{(J + b\tau_c)\,b}{J\tau_c K_t},\qquad \omega_c = \sqrt{\frac{b}{J\tau_c}}</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span><span>cr</span><span>i</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>J</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>J</span><span></span><span>+</span><span></span><span>b</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span>J</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><p>数值是 <span><span>Kcrit=16.7333K_{crit} = 16.7333</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span><span>cr</span><span>i</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>16.7333</span></span></span></span> A/rad，穿越频率 <span><span>ωc=31.623\omega_c = 31.623</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>31.623</span></span></span></span> rad/s。K 超过它，一对极点跑到右半平面，响应是一个越来越大的振荡。</p><p>但这个结论有一个前提：连续时间。我的控制器是 1 kHz 的采样系统，输出要保持一个周期。第 1 篇说过零阶保持平均相当于 <span><span>ts/2t_s/2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span> 的延迟，这个延迟对稳定边界的影响，我这次用精确的离散模型算出来，不用近似。拨弹盘的状态方程 <span><span>x˙=Ax+Bu\dot x = Ax + Bu</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>u</span></span></span></span>，保持 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 一个周期后 <span><span>xk+1=Adxk+Bdukx_{k+1} = A_dx_k + B_du_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，其中</p><span><span><span>[AdBd01]=exp⁡([AB00]ts)\begin{bmatrix} A_d &amp; B_d \\ 0 &amp; 1 \end{bmatrix} = \exp\left(\begin{bmatrix} A &amp; B \\ 0 &amp; 0 \end{bmatrix} t_s\right)</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>exp</span><span></span><span><span><span>(</span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>A</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>B</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span></span></span></span></span></span></span><p><span><span>uk=K(r−θk)u_k = K(r - \theta_k)</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>K</span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 时，<span><span>Ad−BdK [1 0 0]A_d - B_dK\,[1\ 0\ 0]</span><span><span><span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>B</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>K</span><span></span><span>[</span><span>1</span><span> </span><span>0</span><span> </span><span>0</span><span>]</span></span></span></span> 的特征值模长是 1 的地方就是真正的边界，等效的增长率是 <span><span>ln⁡∣λ∣max/ts\ln|\lambda|_{max}/t_s</span><span><span><span></span><span>ln</span><span></span><span>∣</span><span>λ</span><span><span>∣</span><span><span><span><span><span><span></span><span><span><span>ma</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。这一步我用 <code>scipy.linalg.expm</code>（第 9 篇会手写）和 <code>numpy</code> 求特征值，不用手写的参考，因为它只是用来解释仿真，不是四种语言要对齐的东西。</p></section><section><h3>根轨迹：极点作为 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的函数<a href="#根轨迹极点作为-kkk-的函数"><span>#</span></a></h3><p>画根轨迹的传统是用一套规则手画。我不画，我直接算：对 400 个对数间隔的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>（0.01 到 1000），每个求一次特征多项式的根。定义很简单：控制器是 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 乘一个固定的 <span><span>Nc(s)N_c(s)</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>s</span><span>)</span></span></span></span> 时，特征多项式是 <span><span>D(s)+K N(s)D(s) + K\,N(s)</span><span><span><span></span><span>D</span><span>(</span><span>s</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span></span><span>N</span><span>(</span><span>s</span><span>)</span></span></span></span>（<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是对象分母，<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 是对象分子乘 <span><span>NcN_c</span><span><span><span></span><span><span>N</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的分子），根随 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 连续移动，画出来就是根轨迹。<span><span>K→0K \to 0</span><span><span><span></span><span>K</span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span> 时根趋向 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 的根（开环极点），<span><span>K→∞K \to \infty</span><span><span><span></span><span>K</span><span></span><span>→</span><span></span></span><span><span></span><span>∞</span></span></span></span> 时趋向 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的根（开环零点）或无穷远。</p><p>两条要算的轨迹，都是拨弹盘：</p><ul>
<li><strong>P：</strong> <span><span>D+KKtD + KK_t</span><span><span><span></span><span>D</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，开环极点在 <span><span>00</span><span><span><span></span><span>0</span></span></span></span>、<span><span>−b/J=−2-b/J = -2</span><span><span><span></span><span>−</span><span>b</span><span>/</span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span></span></span></span> 和 <span><span>−1/τc=−500-1/\tau_c = -500</span><span><span><span></span><span>−</span><span>1/</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>500</span></span></span></span>。</li>
<li><strong>PD：</strong> 控制器 <span><span>K(1+s/20)K(1 + s/20)</span><span><span><span></span><span>K</span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>s</span><span>/20</span><span>)</span></span></span></span>，多了一个零点 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span>，特征多项式是 <span><span>Jτcs3+(J+bτc)s2+(b+KKt/20)s+KKtJ\tau_cs^3 + (J + b\tau_c)s^2 + (b + KK_t/20)s + KK_t</span><span><span><span></span><span>J</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>J</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/20</span><span>)</span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
</ul><p>PD 的 Routh 条件可以直接写出来：<span><span>(J+bτc)(b+KKt/20)&gt;JτcKKt(J + b\tau_c)(b + KK_t/20) &gt; J\tau_c KK_t</span><span><span><span></span><span>(</span><span>J</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/20</span><span>)</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>J</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，整理成</p><span><span><span>(J+bτc) b+KKt[J+bτc20−Jτc]&gt;0(J + b\tau_c)\,b + KK_t\left[\frac{J + b\tau_c}{20} - J\tau_c\right] &gt; 0</span><span><span><span></span><span>(</span><span>J</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span></span><span><span><span><span><span><span></span><span><span>20</span></span></span><span><span></span><span></span></span><span><span></span><span><span>J</span><span></span><span>+</span><span></span><span>b</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>−</span><span></span><span>J</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>方括号里是 <span><span>0.00502/20−0.00001=2.41×10−4&gt;00.00502/20 - 0.00001 = 2.41\times 10^{-4} &gt; 0</span><span><span><span></span><span>0.00502/20</span><span></span><span>−</span><span></span></span><span><span></span><span>0.00001</span><span></span><span>=</span><span></span></span><span><span></span><span>2.41</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span>，所以对任何 <span><span>K&gt;0K &gt; 0</span><span><span><span></span><span>K</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 都成立：<strong>这个 PD 对任何增益都稳定。</strong> 一个零点就把边界拆掉了，这是后面反复出现的主题：D 项给相位超前，系统才敢用高增益。</p></section><section><h3>数值求根：Durand–Kerner<a href="#数值求根durandkerner"><span>#</span></a></h3><p>三阶多项式有求根公式，但我不想写它：公式在有重根时数值很差，也不好推广到后面的四阶和五阶。我用 Durand–Kerner 迭代：把多项式化成首一，同时维护 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个根的猜测 <span><span>z1,…,znz_1, \dots, z_n</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，每个按</p><span><span><span>zi←zi−p(zi)∏j≠i(zi−zj)z_i \leftarrow z_i - \frac{p(z_i)}{\prod_{j \ne i}(z_i - z_j)}</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>∏</span><span><span><span><span><span><span></span><span><span><span>j</span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span>i</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>p</span><span>(</span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>更新（用最新的 <span><span>zjz_j</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，也就是原地更新）。初值是 <span><span>R (0.4+0.9j)kR\,(0.4 + 0.9j)^k</span><span><span><span></span><span>R</span><span></span><span>(</span><span>0.4</span><span></span><span>+</span><span></span></span><span><span></span><span>0.9</span><span>j</span><span><span>)</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span>，<span><span>k=0,…,n−1k = 0, \dots, n-1</span><span><span><span></span><span>k</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>，<span><span>R=1+max⁡∣ci∣R = 1 + \max|c_i|</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>max</span><span></span><span>∣</span><span><span>c</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span> 是 Cauchy 界（所有根的模长都小于它）；这些点落在一条螺线上，互不相同，不会出现分母为零。收敛判据是所有更新量都小于 <span><span>10−14(1+∣z∣)10^{-14}(1 + |z|)</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>∣</span><span>z</span><span>∣</span><span>)</span></span></span></span>，最多 1000 轮。</p><p>之后做 3 步 Newton 抛光。重根附近 <span><span>p′p'</span><span><span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span> 接近零，Newton 一步可能飞出去，所以我加了保护：只在 <span><span>∣p∣|p|</span><span><span><span></span><span>∣</span><span>p</span><span>∣</span></span></span></span> 变小时才接受这一步。最后做两件事保证四种语言的输出可以逐列对比：虚部小于 <span><span>10−9max⁡(1,∣z∣)10^{-9}\max(1, |z|)</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span><span></span><span>max</span><span>(</span><span>1</span><span>,</span><span></span><span>∣</span><span>z</span><span>∣</span><span>)</span></span></span></span> 的根强制成实数；然后按实部从小到大排序，实部相差小于 <span><span>10−9(1+max⁡∣Re∣)10^{-9}(1 + \max|\mathrm{Re}|)</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>max</span><span></span><span>∣</span><span><span>Re</span></span><span>∣</span><span>)</span></span></span></span> 的当作并列，并列的按虚部从小到大。共轭对因此永远是“负虚部在前”。</p><p>我原先担心重根附近 Durand–Kerner 的精度只有 <span><span>ϵ≈10−8\sqrt\epsilon \approx 10^{-8}</span><span><span><span></span><span><span><span><span><span><span></span><span>ϵ</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span> 量级，四种语言在根轨迹的分叉点附近会差出一点，打算给这两个文件把容差放宽到 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span>。结果下面会看到：没用上。</p></section><section><h3>阶跃响应指标<a href="#阶跃响应指标"><span>#</span></a></h3><p>仿真的响应是一串采样值，不是公式，所以指标要有离散的定义。阶跃在第 <span><span>k0k_0</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 个采样点施加，起始值取 <span><span>y[k0]y[k_0]</span><span><span><span></span><span>y</span><span>[</span><span><span>k</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>，目标值 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span>，归一化 <span><span>zi=(yi−y[k0])/(r−y[k0])z_i = (y_i - y[k_0])/(r - y[k_0])</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>[</span><span><span>k</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>])</span><span>/</span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span><span>[</span><span><span>k</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>])</span></span></span></span>，时间从 <span><span>k0k_0</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 起算：</p><ul>
<li><strong>上升时间：</strong> <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 第一次达到 0.1 和 0.9 的时刻（相邻两个采样点之间线性插值）之差。</li>
<li><strong>超调：</strong> <span><span>max⁡(0,max⁡z−1)×100%\max(0, \max z - 1)\times 100\%</span><span><span><span></span><span>max</span><span>(</span><span>0</span><span>,</span><span></span><span>max</span><span></span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>100%</span></span></span></span>。</li>
<li><strong>峰值时间：</strong> 第一个满足 <span><span>z≥max⁡z−10−9z \ge \max z - 10^{-9}</span><span><span><span></span><span>z</span><span></span><span>≥</span><span></span></span><span><span></span><span>max</span><span></span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 的采样点。直接取 argmax 的话，没有超调的响应在末尾会因为最后一位舍入而挑出不同的“峰”，四种语言可能挑得不一样；加一个 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 的容差，有超调的响应不受影响（峰顶是平的，差 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 对应不到一个采样点），单调的响应也有了确定的答案。</li>
<li><strong>2% 调节时间：</strong> 最后一次 <span><span>∣z−1∣&gt;0.02|z - 1| &gt; 0.02</span><span><span><span></span><span>∣</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>1∣</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0.02</span></span></span></span> 的采样点之后的第一个时刻。</li>
<li><strong>IAE：</strong> <span><span>∑∣r−yi∣ ts\sum|r - y_i|\,t_s</span><span><span><span></span><span>∑</span><span></span><span>∣</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，矩形积分。</li>
</ul></section></section>
<section><h2>把它们写成代码<a href="#把它们写成代码"><span>#</span></a></h2><p>数学部分到此为止，下面是实现。我先写 Python，再把同一套算法搬到 C、C++、Rust。每个模块只贴承载解释的那几行，完整文件在最后的检查点里逐个链接。</p><section><h3>多项式<a href="#多项式"><span>#</span></a></h3><p>多项式就是系数列表，最高次在前，和 <code>numpy.roots</code> 同一个约定。需要的运算很少：乘、加、数乘、求值。<code>poly_add</code> 要把常数项对齐，这样“三阶特征多项式加上一个常数 <span><span>KKtKK_t</span><span><span><span></span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>”可以直接写成 <code>poly_add(plant, [k * kt])</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>poly_add</span><span>(</span><span>a</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Sum of two polynomials; the constant terms line up."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>max</span><span>(</span><span>len</span><span>(a), </span><span>len</span><span>(b))</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>out </span><span>=</span><span> [</span><span>0.0</span><span><span>] </span><span>*</span><span> n</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> i, ai </span><span>in</span><span> </span><span>enumerate</span><span>(a):</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>out[n </span><span>-</span><span> </span><span>len</span><span><span>(a) </span><span>+</span><span> i] </span><span>+=</span><span> ai</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> i, bi </span><span>in</span><span> </span><span>enumerate</span><span>(b):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>out[n </span><span>-</span><span> </span><span>len</span><span><span>(b) </span><span>+</span><span> i] </span><span>+=</span><span> bi</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> out</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>def</span><span> </span><span>poly_eval</span><span>(</span><span>a</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>z</span></span><span>:</span><span> </span><span>complex</span><span>) -&gt; </span><span>complex</span><span>:</span></div></div><div><div><div>13</div></div><div><span>    </span><span>"""Horner evaluation at a complex point."""</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>acc </span><span>=</span><span> </span><span>complex</span><span>(</span><span>0.0</span><span>, </span><span>0.0</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>for</span><span> ai </span><span>in</span><span> a:</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>acc </span><span>=</span><span> acc </span><span>*</span><span> z </span><span>+</span><span> ai</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> acc</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>求值用 Horner 法，输入点是复数，因为求根的时候每个猜测都是复数。<code>poly_mul</code> 是两层循环的卷积，<code>poly_scale</code> 是列表推导，没什么好解释的。</p></section><section><h3>Durand–Kerner 求根<a href="#durandkerner-求根"><span>#</span></a></h3><p>上一节的算法翻译成 Python，主体就是两个嵌套循环加一步 Newton。先看 Durand–Kerner 的那一段：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> [ai </span><span>/</span><span> a[</span><span>0</span><span>] </span><span>for</span><span> ai </span><span>in</span><span> a]</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>radius </span><span>=</span><span> </span><span>1.0</span><span><span> </span><span>+</span><span> </span></span><span>max</span><span>(</span><span>abs</span><span>(ci) </span><span>for</span><span> ci </span><span>in</span><span> c[</span><span>1</span><span>:])</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>seed </span><span>=</span><span> </span><span>complex</span><span>(</span><span>0.4</span><span>, </span><span>0.9</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> [radius </span><span>*</span><span> seed</span><span>**</span><span>k </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n)]</span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span><span>(</span><span>MAX_ITER</span><span>):</span></span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>done </span><span>=</span><span> </span><span>True</span></div></div><div><div><div>7</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>den </span><span>=</span><span> </span><span>complex</span><span>(</span><span>1.0</span><span>, </span><span>0.0</span><span>)</span></div></div><div><div><div>9</div></div><div><span>            </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>10</div></div><div><span>                </span><span>if</span><span><span> j </span><span>!=</span><span> i:</span></span></div></div><div><div><div>11</div></div><div><span><span>                    </span></span><span>den </span><span>*=</span><span> z[i] </span><span>-</span><span> z[j]</span></div></div><div><div><div>12</div></div><div><span>            </span><span>if</span><span><span> den </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>13</div></div><div><span>                </span><span>continue</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>step </span><span>=</span><span> </span><span>poly_eval</span><span>(c, z[i]) </span><span>/</span><span> den</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>z[i] </span><span>-=</span><span> step</span></div></div><div><div><div>16</div></div><div><span>            </span><span>if</span><span> </span><span>abs</span><span><span>(step) </span><span>&gt;</span><span> </span><span>DK_TOL</span><span> </span><span>*</span><span> (</span></span><span>1.0</span><span><span> </span><span>+</span><span> </span></span><span>abs</span><span>(z[i])):</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>done </span><span>=</span><span> </span><span>False</span></div></div><div><div><div>18</div></div><div><span>        </span><span>if</span><span> done:</span></div></div><div><div><div>19</div></div><div><span>            </span><span>break</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里的 <code>n</code> 是次数。<code>seed**k</code> 就是螺线上的第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个点；<code>z[i] -= step</code> 在循环里原地更新，所以后面的根用到的是已经更新过的 <code>z[j]</code>，和上一节写的“用最新的 <span><span>zjz_j</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>”一致。</p><p>重根附近 <span><span>p′p'</span><span><span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span> 接近零，Newton 抛光那一步我只在 <span><span>∣p∣|p|</span><span><span><span></span><span>∣</span><span>p</span><span>∣</span></span></span></span> 变小时才接受：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>_newton</span><span>(</span><span>a</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>da</span></span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>z</span></span><span>:</span><span> </span><span>complex</span><span>) -&gt; </span><span>complex</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""One safeguarded Newton step: keep it only if |p| shrinks."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>p </span><span>=</span><span> </span><span>poly_eval</span><span>(a, z)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>dp </span><span>=</span><span> </span><span>poly_eval</span><span>(da, z)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span><span> dp </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>6</div></div><div><span>        </span><span>return</span><span> z</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>z_new </span><span>=</span><span> z </span><span>-</span><span> p </span><span>/</span><span> dp</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> z_new </span><span>if</span><span> </span><span>abs</span><span><span>(</span><span>poly_eval</span><span>(a, z_new)) </span><span>&lt;</span><span> </span></span><span>abs</span><span>(p) </span><span>else</span><span> z</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后的规范化也是几行：虚部够小的强制成实数，然后用插入排序按实部排（并列时按虚部）。我没有用 <code>sorted</code>，而是写了插入排序，四种语言可以写成同一个样子，比较函数带着这个容差：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>_after</span><span>(</span><span>a</span><span>:</span><span> </span><span>complex</span><span>,</span><span><span> </span><span>b</span></span><span>:</span><span> </span><span>complex</span><span>,</span><span><span> </span><span>tie</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>bool</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""True when a must come after b."""</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span> </span><span>abs</span><span><span>(a.real </span><span>-</span><span> b.real) </span><span>&lt;=</span><span> tie:</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>return</span><span><span> a.imag </span><span>&gt;</span><span> b.imag</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span><span> a.real </span><span>&gt;</span><span> b.real</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>poly_roots</code> 在首项为零或次数小于 1 时抛 <code>ValueError</code>。</p></section><section><h3>阶跃指标<a href="#阶跃指标"><span>#</span></a></h3><p><code>step_metrics</code> 把上面五条定义直接翻成代码。先把响应归一化，再找各个时刻：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>y0 </span><span>=</span><span> y[k0]</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>amp </span><span>=</span><span> r </span><span>-</span><span> y0</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> [(y[i] </span><span>-</span><span> y0) </span><span>/</span><span> amp </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(k0, n)]</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>t10 </span><span>=</span><span> </span><span>_cross</span><span>(z, </span><span>0.1</span><span>, ts)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>t90 </span><span>=</span><span> </span><span>_cross</span><span>(z, </span><span>0.9</span><span>, ts)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>zmax </span><span>=</span><span> </span><span>max</span><span>(z)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>k_peak </span><span>=</span><span> </span><span>next</span><span>(i </span><span>for</span><span> i, zi </span><span>in</span><span> </span><span>enumerate</span><span>(z) </span><span>if</span><span><span> zi </span><span>&gt;=</span><span> zmax </span><span>-</span><span> </span><span>PEAK_TOL</span><span>)</span></span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>last_out </span><span>=</span><span> </span><span>-</span><span>1</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> i, zi </span><span>in</span><span> </span><span>enumerate</span><span>(z):</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> </span><span>abs</span><span><span>(zi </span><span>-</span><span> </span></span><span>1.0</span><span><span>) </span><span>&gt;</span><span> </span><span>BAND</span><span>:</span></span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>last_out </span><span>=</span><span> i</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调节时间是 <code>(last_out + 1) * ts</code>，超调是 <code>max(0, zmax - 1) * 100</code>。线性插值的穿越时刻单独成一个函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>_cross</span><span>(</span><span>z</span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>level</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>ts</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""First time z reaches level, linearly interpolated; NaN if never."""</span></div></div><div><div><div>3</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, </span><span>len</span><span>(z)):</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span><span> z[i] </span><span>&gt;=</span><span> level:</span></span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>frac </span><span>=</span><span> (level </span><span>-</span><span> z[i </span><span>-</span><span> </span><span>1</span><span><span>]) </span><span>/</span><span> (z[i] </span><span>-</span><span> z[i </span><span>-</span><span> </span></span><span>1</span><span>])</span></div></div><div><div><div>6</div></div><div><span>            </span><span>return</span><span><span> (i </span><span>-</span><span> </span></span><span>1</span><span><span> </span><span>+</span><span> frac) </span><span>*</span><span> ts</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> math.nan</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有穿过的时候返回 NaN 而不是抛异常，这样“没有到达 0.9”也能写进 CSV，四种语言的 NaN 在对比时算相等。</p></section><section><h3>两个新对象<a href="#两个新对象"><span>#</span></a></h3><p><code>MotorSpeedLag</code> 的状态是 <span><span>[ω,i][\omega, i]</span><span><span><span></span><span>[</span><span>ω</span><span>,</span><span></span><span>i</span><span>]</span></span></span></span>，<code>DialPosition</code> 的状态是 <span><span>[θ,ω,i][\theta, \omega, i]</span><span><span><span></span><span>[</span><span>θ</span><span>,</span><span></span><span>ω</span><span>,</span><span></span><span>i</span><span>]</span></span></span></span>。接口和第 1 篇的对象一样，<code>deriv(x, u)</code> 和 <code>output(x)</code>，输入是 <code>[指令, 负载]</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span>        </span><span>return</span><span> [</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>x[</span><span>1</span><span>],</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>(</span><span>self</span><span><span>.kt </span><span>*</span><span> x[</span></span><span>2</span><span><span>] </span><span>-</span><span> </span></span><span>self</span><span><span>.b </span><span>*</span><span> x[</span></span><span>1</span><span><span>] </span><span>-</span><span> u[</span></span><span>1</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.j,</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>(u[</span><span>0</span><span><span>] </span><span>-</span><span> x[</span></span><span>2</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.tau_c,</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 <code>DialPosition.deriv</code>：角度的导数是角速度，角速度按 <span><span>Jω˙=Kti−bω−τdJ\dot\omega = K_ti - b\omega - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span>bω</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，电流按 <span><span>τci˙=icmd−i\tau_c\dot i = i_{cmd} - i</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>i</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>i</span><span><span><span><span><span><span></span><span><span><span>c</span><span>m</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>i</span></span></span></span>。<code>MotorSpeedLag.deriv</code> 去掉第一行。它们是两个类而不是一个带开关的类：状态维数在 C、C++、Rust 里是编译期常数，一个对象不能有时是 1 维有时是 2 维。</p></section><section><h3>驱动：实验表、设计公式和回路<a href="#驱动实验表设计公式和回路"><span>#</span></a></h3><p>驱动 <code>python/ch02_transfer_functions.py</code> 还是 <code>ref</code> 和 <code>report</code> 两个子命令。每个实验是一个 <code>Case</code>，<code>kind</code> 说明控制器的种类，<code>p1</code>、<code>p2</code> 的含义随 <code>kind</code> 变，<code>bw</code> 是比例项里设定值的权重（1 是 PI，0 是 I-P）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>CASES</span><span> </span><span>=</span><span> {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>"pd_z0.2"</span><span><span>: </span><span>Case</span><span>(</span><span>PD</span><span>, </span></span><span>0.2</span><span><span>, </span><span>PD_WN</span><span>, </span></span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.05</span><span>),</span></div></div><div><div><div>3</div></div><div><span>    </span><span># ... pd_z0.4, pd_z0.7, pd_z1</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"pi_place_w10"</span><span><span>: </span><span>Case</span><span>(</span><span>PI_PLACE</span><span>, </span></span><span>0.7</span><span>, </span><span>10.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>5.0</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"pi_place_w10_ip"</span><span><span>: </span><span>Case</span><span>(</span><span>PI_PLACE</span><span>, </span></span><span>0.7</span><span>, </span><span>10.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>5.0</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span># ... w30, w30_ip, w30_lag, pi_ch01, pi_ch01_ip</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"routh_0.7"</span><span><span>: </span><span>Case</span><span>(</span><span>ROUTH</span><span>, </span></span><span>0.7</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.05</span><span>),</span></div></div><div><div><div>8</div></div><div><span>    </span><span># ... routh_0.9, routh_1.1</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一共 14 组。<code>design</code> 就是上一节推出的两组公式，外加 Routh 临界增益：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>if</span><span><span> c.kind </span><span>==</span><span> </span><span>PD</span><span>:</span></span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>g </span><span>=</span><span> </span><span>GimbalAxis</span><span>()</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>kp </span><span>=</span><span> g.j </span><span>*</span><span> c.p2 </span><span>*</span><span> c.p2</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>kd </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> c.p1 </span><span>*</span><span> c.p2 </span><span>*</span><span> g.j </span><span>-</span><span> (g.b </span><span>+</span><span> g.bf)</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>return</span><span> g, kp, kd</span></div></div><div><div><div>6</div></div><div><span>    </span><span># ...</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span><span> c.kind </span><span>==</span><span> </span><span>PI_PLACE</span><span>:</span></span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>kp </span><span>=</span><span> (</span><span>2.0</span><span><span> </span><span>*</span><span> c.p1 </span><span>*</span><span> c.p2 </span><span>*</span><span> m.j </span><span>-</span><span> m.b) </span><span>/</span><span> m.kt</span></span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>ki </span><span>=</span><span> c.p2 </span><span>*</span><span> c.p2 </span><span>*</span><span> m.j </span><span>/</span><span> m.kt</span></div></div><div><div><div>10</div></div><div><span>        </span><span>return</span><span> m, kp, ki</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>critical_gain</span><span>(</span><span>d</span><span>:</span><span> DialPosition</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>14</div></div><div><span>    </span><span>"""Routh limit of P control on the dial: (J + b tc) b / (J tc Kt)."""</span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span><span> (d.j </span><span>+</span><span> d.b </span><span>*</span><span> d.tau_c) </span><span>*</span><span> d.b </span><span>/</span><span> (d.j </span><span>*</span><span> d.tau_c </span><span>*</span><span> d.kt)</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>run_case</code> 的循环顺序和第 1 篇完全一样（测量、控制、限幅、延迟线、保持、记录），区别在控制律那几行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>e </span><span>=</span><span> r </span><span>-</span><span> y</span></div></div><div><div><div>2</div></div><div><span>        </span><span>if</span><span><span> c.kind </span><span>==</span><span> </span><span>PD</span><span>:</span></span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>u </span><span>=</span><span> k1 </span><span>*</span><span> e </span><span>-</span><span> k2 </span><span>*</span><span> x[</span><span>1</span><span>]  </span><span># rate feedback from the gyro</span></div></div><div><div><div>4</div></div><div><span>        </span><span>elif</span><span><span> c.kind </span><span>==</span><span> </span><span>ROUTH</span><span>:</span></span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>u </span><span>=</span><span> k1 </span><span>*</span><span> e</span></div></div><div><div><div>6</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>u </span><span>=</span><span> k1 </span><span>*</span><span> (c.bw </span><span>*</span><span> r </span><span>-</span><span> y) </span><span>+</span><span> integ</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span><span>(u, </span><span>-</span><span>plant.u_max), plant.u_max)</span></span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>([data[</span><span>"t"</span><span>][k], r, y, u])</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> c.kind </span><span>in</span><span><span> (</span><span>PI_PLACE</span><span>, </span><span>PI_GAINS</span><span>):</span></span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>integ </span><span>+=</span><span> k2 </span><span>*</span><span> </span><span>TS</span><span> </span><span>*</span><span> e</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>PD</code> 用 <code>x[1]</code> 当“陀螺仪读数”做角速度反馈；<code>c.bw * r - y</code> 就是 I-P 和 PI 的唯一区别；积分在算完 <code>u</code> 之后才更新，这个顺序我会在结果里用到。根轨迹那边，特征多项式就是用 <code>poly_mul</code>、<code>poly_add</code>、<code>poly_scale</code> 拼出来的，和上面的数学一一对应：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>locus_poly</span><span>(</span><span>pd_zero</span><span>:</span><span> </span><span>bool</span><span>,</span><span><span> </span><span>k</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Characteristic polynomial of the dial loop at gain k."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> </span><span>DialPosition</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>plant </span><span>=</span><span> </span><span>poly_mul</span><span>(</span><span>poly_mul</span><span>([</span><span>1.0</span><span>, </span><span>0.0</span><span>], [d.j, d.b]), [d.tau_c, </span><span>1.0</span><span>])</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ctrl </span><span>=</span><span> [</span><span>1.0</span><span><span> </span><span>/</span><span> </span><span>PD_ZONE</span><span>, </span></span><span>1.0</span><span>] </span><span>if</span><span> pd_zero </span><span>else</span><span> [</span><span>1.0</span><span>]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span><span> </span><span>poly_add</span><span>(plant, </span><span>poly_scale</span><span>(ctrl, k </span><span>*</span><span> d.kt))</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>report</code> 里的对比、指标表、断言和画图在驱动的 <code>report</code> 子命令里，下面“结果”一节引用的数字都来自它。</p></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>三个版本是同一份结构：<code>poly</code>、<code>metrics</code>、两个新对象，加一个驱动。区别主要在这几处：</p><ul>
<li><strong>复数。</strong> C 用 C99 的 <code>_Complex</code>，但 <code>pl_real complex</code> 这种写法不合法（<code>pl_real</code> 是 typedef，不能再和 <code>complex</code> 组合），所以 <code>real.h</code> 里和 <code>pl_real</code> 一起多了一个 <code>pl_cplx</code>，同样由 <code>-DPL_USE_FLOAT</code> 切换，还有配套的 <code>PL_CABS</code>、<code>PL_CREAL</code>、<code>PL_CIMAG</code>。C++ 用 <code>std::complex&lt;T&gt;</code>。Rust 的 <code>no_std</code> 里没有复数，我写了一个 84 行的 <code>complex.rs</code>，只实现 <code>+ - * /</code>、模长和乘实数。</li>
<li><strong>多项式的存放。</strong> C 用 <code>PL_MAX_POLY = 8</code> 的固定数组，调用者传长度；C++ 用 <code>std::array&lt;T, N&gt;</code>，<code>PolyMul</code> 的返回类型 <code>std::array&lt;T, A + B - 1&gt;</code> 让长度进入类型，不会越界；Rust 用切片加调用者给的输出缓冲区（稳定版 Rust 还不能写 <code>[T; N + M - 1]</code>）。三者都不分配内存。</li>
<li><strong>出错。</strong> 首项为零之类的错：C 返回 <code>-EINVAL</code>，C++ 返回 <code>std::optional</code>，Rust 返回 <code>Result</code>。</li>
<li><strong>求模。</strong> C 的 <code>cabs</code> 和 Python 的 <code>abs</code> 都是 <code>hypot</code>；Rust 里我也调 <code>libm::hypot</code>，没有写 <span><span>x2+y2\sqrt{x^2 + y^2}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，免得和另外两个差一个舍入。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p>求根函数是 Python 版的逐行翻译。<code>pl_cplx</code> 的乘除由编译器处理，我没有手写。Durand–Kerner 循环：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>for</span><span> (it </span><span>=</span><span> </span><span>0</span><span>; it </span><span>&lt;</span><span> MAX_ITER; it</span><span>++</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>bool</span><span> done </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> deg; i</span><span>++</span><span>) {</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>pl_cplx den </span><span>=</span><span> </span><span>1.0</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>pl_cplx step;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>      </span><span>for</span><span> (j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> deg; j</span><span>++</span><span>)</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> (j </span><span>!=</span><span> i)</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>den </span><span>*=</span><span> </span><span>roots</span><span>[i] </span><span>-</span><span> </span><span>roots</span><span>[j];</span></div></div><div><div><div>11</div></div><div><span>      </span><span>if</span><span> (den </span><span>==</span><span> </span><span>0.0</span><span>)</span></div></div><div><div><div>12</div></div><div><span>        </span><span>continue</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>step </span><span>=</span><span> </span><span>pl_poly_eval</span><span>(c, n, </span><span>roots</span><span>[i]) </span><span>/</span><span> den;</span></div></div><div><div><div>14</div></div><div><span>      </span><span>roots</span><span>[i] </span><span>-=</span><span> step;</span></div></div><div><div><div>15</div></div><div><span>      </span><span>if</span><span> (</span><span>PL_CABS</span><span>(step) </span><span>&gt;</span><span> DK_TOL </span><span>*</span><span> (</span><span>1.0</span><span> </span><span>+</span><span> </span><span>PL_CABS</span><span>(</span><span>roots</span><span>[i])))</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>done </span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>    </span><span>if</span><span> (done)</span></div></div><div><div><div>19</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>返回值和出错都是内核风格：<code>n &lt; 2</code>、<code>n &gt; PL_MAX_POLY</code> 或首项为零时返回 <code>-EINVAL</code>，结果写进调用者给的 <code>roots</code> 数组。C 里只有 <code>poly.c</code> 用到复数，别处都是 <code>pl_real</code>。<code>PL_FABS(PL_CREAL(...))</code> 看着啰嗦，但它是 float 构建（<code>-DPL_USE_FLOAT</code>）下 <code>fabsf</code>、<code>crealf</code> 都匹配的最简单写法，我编译过 float 版确认没有警告（第 10 篇才会真的跑它）。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p><code>PolyRoots</code> 是一个函数模板，<code>N</code> 是系数个数，返回 <code>std::optional&lt;std::array&lt;std::complex&lt;T&gt;, N - 1&gt;&gt;</code>，根的个数在类型里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>, std::</span><span>size_t</span><span> </span><span>N</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>optional</span><span>&lt;std::</span><span>array</span><span>&lt;std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt;, </span><span>N</span><span> </span><span>-</span><span> </span><span>1</span><span>&gt;&gt; </span><span>PolyRoots</span><span>(</span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>a</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>  </span><span>static_assert</span><span>(</span><span>N</span><span> </span><span>&gt;=</span><span> </span><span>2</span><span>, </span><span>"need degree &gt;= 1"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>  </span><span>constexpr</span><span> std::</span><span>size_t</span><span> </span><span>kDeg</span><span> </span><span>=</span><span> </span><span>N</span><span> </span><span>-</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>if</span><span> (</span><span>a</span><span>[</span><span>0</span><span>] </span><span>==</span><span> </span><span>T</span><span>(</span><span>0</span><span>)) </span><span>return</span><span> std::</span><span>nullopt</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>// ... normalize, Cauchy radius, spiral start</span></div></div><div><div><div>8</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>it</span><span> </span><span>&lt;</span><span> internal::</span><span>kMaxIter</span><span>; </span><span>++</span><span>it</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>bool</span><span> </span><span>done</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kDeg</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt; </span><span>den</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>12</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>kDeg</span><span>; </span><span>++</span><span>j</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span> (</span><span>j</span><span> </span><span>!=</span><span> </span><span>i</span><span>) </span><span>den</span><span> </span><span>*=</span><span> </span><span>z</span><span>[</span><span>i</span><span>] </span><span>-</span><span> </span><span>z</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>      </span><span>if</span><span> (</span><span>den</span><span> </span><span>==</span><span> std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt;(</span><span>0</span><span>)) </span><span>continue</span><span>;</span></div></div><div><div><div>16</div></div><div><span>      </span><span>const</span><span> std::</span><span>complex</span><span>&lt;</span><span>T</span><span>&gt; </span><span>step</span><span> </span><span>=</span><span> </span><span>PolyEval</span><span>(</span><span>c</span><span>, </span><span>z</span><span>[</span><span>i</span><span>]) </span><span>/</span><span> </span><span>den</span><span>;</span></div></div><div><div><div>17</div></div><div><span>      </span><span>z</span><span>[</span><span>i</span><span>] </span><span>-=</span><span> </span><span>step</span><span>;</span></div></div><div><div><div>18</div></div><div><span>      </span><span>if</span><span><span> (</span><span>std</span><span>::</span></span><span>abs</span><span>(</span><span>step</span><span>) </span><span>&gt;</span><span> </span><span>T</span><span>(internal::</span><span>kDkTol</span><span>) </span><span>*</span><span> (</span><span>T</span><span>(</span><span>1</span><span>) </span><span>+</span><span> std::</span><span>abs</span><span>(</span><span>z</span><span>[</span><span>i</span><span>]))) {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>done</span><span> </span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>    </span><span>if</span><span> (</span><span>done</span><span>) </span><span>break</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>// ... Newton polish, canonical sort</span></div></div><div><div><div>25</div></div><div><span>  </span><span>return</span><span> </span><span>z</span><span>;</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>std::complex</code> 的运算符和 C 的 <code>_Complex</code> 行为一样，所以循环体几乎逐字相同。Newton 和比较函数放在 <code>internal</code> 命名空间里，不对外。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><p>没有现成的复数，所以先写 <code>complex.rs</code>。四则运算用运算符重载，这样 <code>poly_roots</code> 的循环读起来和 Python 一样。除法用的是教科书公式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Div</span><span> </span><span>for</span><span> </span><span>Complex</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Complex</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>div</span><span>(</span><span>self</span><span>, </span><span>o</span><span>: </span><span>Complex</span><span>) -&gt; </span><span>Complex</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>den</span><span><span> </span><span>=</span><span> </span></span><span>o</span><span>.re * </span><span>o</span><span>.re + </span><span>o</span><span>.im * </span><span>o</span><span>.im;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Complex</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>(</span><span>self</span><span>.re * </span><span>o</span><span>.re + </span><span>self</span><span>.im * </span><span>o</span><span>.im) / </span><span>den</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>(</span><span>self</span><span>.im * </span><span>o</span><span>.re - </span><span>self</span><span>.re * </span><span>o</span><span>.im) / </span><span>den</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>于是 <code>poly_roots</code> 里的核心循环和前面三个版本几乎一模一样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>MAX_ITER</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>done</span><span><span> </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>deg</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>den</span><span><span> </span><span>=</span><span> </span></span><span>Complex</span><span>::</span><span>ONE</span><span>;</span></div></div><div><div><div>5</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>deg</span><span> {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>if</span><span> </span><span>j</span><span><span> </span><span>!=</span><span> </span></span><span>i</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>den</span><span><span> </span><span>*=</span><span> </span></span><span>z</span><span>[</span><span>i</span><span>] - </span><span>z</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>            </span><span>if</span><span> </span><span>den</span><span><span> </span><span>==</span><span> </span></span><span>Complex</span><span>::</span><span>ZERO</span><span> {</span></div></div><div><div><div>11</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>            </span><span>let</span><span> </span><span>step</span><span><span> </span><span>=</span><span> </span></span><span>poly_eval</span><span>(</span><span>c</span><span>, </span><span>z</span><span>[</span><span>i</span><span>]) / </span><span>den</span><span>;</span></div></div><div><div><div>14</div></div><div><span>            </span><span>z</span><span>[</span><span>i</span><span><span>] </span><span>-=</span><span> </span></span><span>step</span><span>;</span></div></div><div><div><div>15</div></div><div><span>            </span><span>if</span><span> </span><span>step</span><span>.</span><span>abs</span><span>() &gt; </span><span>DK_TOL</span><span> * (</span><span>1.0</span><span> + </span><span>z</span><span>[</span><span>i</span><span>].</span><span>abs</span><span>()) {</span></div></div><div><div><div>16</div></div><div><span>                </span><span>done</span><span><span> </span><span>=</span><span> </span></span><span>false</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>        </span><span>if</span><span> </span><span>done</span><span> {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>break</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>poly_roots</code> 要求调用者给一个足够长的 <code>&amp;mut [Complex]</code>，这是在没有分配器的情况下把“返回一组根”表达出来的方式；次数不对返回 <code>PolyError::Degenerate</code>，缓冲区或系数个数超限返回 <code>PolyError::Size</code>。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch02</code> 一次跑完。<code>ok</code> 行比第 1 篇多，这里只贴头尾：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch02</span></div></div><div><div><div>2</div></div><div><span>ch02: C wrote 14 cases, metrics and 2 loci to out/ch02</span></div></div><div><div><div>3</div></div><div><span>ch02: C++ wrote 14 cases, metrics and 2 loci to out/ch02</span></div></div><div><div><div>4</div></div><div><span>ch02: Rust wrote 14 cases, metrics and 2 loci to out/ch02</span></div></div><div><div><div>5</div></div><div><span>ok   metrics_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   metrics_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   metrics_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>... (trimmed: 42 more ok lines)</span></div></div><div><div><div>9</div></div><div><span>ok   rlocus_p_c.csv: max diff 2.842e-14 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   rlocus_p_cpp.csv: max diff 2.842e-14 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   rlocus_p_rs.csv: max diff 2.842e-14 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>ok   rlocus_pd_c.csv: max diff 7.816e-14 &lt;= 1.0e-09</span></div></div><div><div><div>13</div></div><div><span>ok   rlocus_pd_cpp.csv: max diff 7.816e-14 &lt;= 1.0e-09</span></div></div><div><div><div>14</div></div><div><span>ok   rlocus_pd_rs.csv: max diff 1.386e-13 &lt;= 1.0e-09</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>51 个对比文件（17 组乘 C、C++、Rust）里每一个，和 Python 的最大差值都小于 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>。仿真的 14 组和 <code>metrics</code> 是 <code>0.000e+00</code>，逐位一致，原因和第 1 篇一样（只有加减乘除，运算顺序写成了一样，x86-64 上 gcc 默认不生成 FMA）。真正有看头的是根轨迹：<code>rlocus_p</code> 最大差 <span><span>2.842×10−142.842\times 10^{-14}</span><span><span><span></span><span>2.842</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>，<code>rlocus_pd</code> 对 C 和 C++ 是 <span><span>7.816×10−147.816\times 10^{-14}</span><span><span><span></span><span>7.816</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>，对 Rust 是 <span><span>1.386×10−131.386\times 10^{-13}</span><span><span><span></span><span>1.386</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>。我打算放宽到的 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span> 一点没用到，差了七个数量级。这大概率是这两条轨迹的网格点没有正好落在重根上，不是 Durand–Kerner 在重根处突然变准了：重根附近它的精度仍然是 <span><span>ϵ\sqrt\epsilon</span><span><span><span></span><span><span><span><span><span><span></span><span>ϵ</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 量级，我没有去构造一个正好重根的 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 来逼它现原形。所以两个根轨迹文件和别的文件一样用 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 的容差。</p><p>Python 自己的求根还要对照 <code>numpy.roots</code>，对 800 个多项式（两条轨迹各 400 个 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch02</span></div></div><div><div><div>2</div></div><div><span>... (trimmed)</span></div></div><div><div><div>3</div></div><div><span>Durand-Kerner vs numpy.roots over 800 polys: max root distance 1.02e-12</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最大距离 <span><span>1.02×10−121.02\times 10^{-12}</span><span><span><span></span><span>1.02</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span>，断言阈值是 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span>。</p><section><h3>二阶原型：公式和仿真<a href="#二阶原型公式和仿真"><span>#</span></a></h3><p>云台 PD，<span><span>ωn=20\omega_n = 20</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> rad/s，<span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 取 0.2、0.4、0.7、1：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch02</span></div></div><div><div><div>2</div></div><div><span>... (trimmed)</span></div></div><div><div><div>3</div></div><div><span>Gimbal PD, wn = 20 rad/s: simulation vs second-order formulas</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>zeta     Kp      Kd    Mp %   sim %   tp (s)      sim  4/(z wn)   sim ts    rise</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>0.2   8.00  0.1380   52.66   53.49   0.1603   0.1600     1.000    0.988  0.0598</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>0.4   8.00  0.2980   25.38   25.75   0.1714   0.1700     0.500    0.419  0.0726</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>0.7   8.00  0.5380    4.60    4.61   0.2200   0.2180     0.286    0.297  0.1056</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>1.0   8.00  0.7780    0.00    0.00               n/a     0.200    0.292  0.1674</span></div></div><div><div><div>9</div></div><div><span>(Kd = 2 zeta wn J - (b + bf), b + bf = 0.022)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch02-second-order-family.svg" alt="ζ 从 0.2 增到 1，云台阶跃响应的超调从 53.49% 降到 0，仿真曲线与二阶公式几乎重合" /><figcaption>ζ 从 0.2 增到 1，云台阶跃响应的超调从 53.49% 降到 0，仿真曲线与二阶公式几乎重合</figcaption></figure><p></p><p>左图的实线是仿真，黑色虚线是上一节的闭式 <span><span>y0(t)y_0(t)</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span></span></span></span>，两者几乎叠在一起，右图是对应的极点：<span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 越小，极点越靠近虚轴，越往上（频率不变，所有极点都在半径为 20 的圆上）。数字上看：<span><span>ζ=0.2\zeta = 0.2</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> 时公式给 52.66%，仿真 53.49%，差 0.83 个百分点；峰值时间 0.1603 s 对 0.1600 s。<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span> 时公式 4.60%，仿真 4.61%。差距随 <span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 变小而变大，而且永远是仿真更大，我认为这是零阶保持的延迟（大约 <span><span>ts/2t_s/2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span>）在悄悄削掉一点相位，等效地减小了阻尼。我没有单独验证这个解释，但第 3 篇会把这个延迟变成相位裕度，到时候能直接算。</p><p><span><span>ζ=1\zeta = 1</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的调节时间（0.292 s）比 <span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>（0.297 s）还短，不是哪里算错了：<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span> 有 4.6% 的超调，要进 2% 的带子得先冲出去再绕回来，临界阻尼从下面单调爬进去，反而更早进带子。<span><span>4/(ζωn)4/(\zeta\omega_n)</span><span><span><span></span><span>4/</span><span>(</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 对 <span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span> 的预测是 0.286 s（实测 0.297 s），对 <span><span>ζ=1\zeta = 1</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 是 0.200 s（实测 0.292 s），偏得很多：这个公式本来就只是包络线的粗略估计，<span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 接近 1 时没有振荡包络可言。</p></section><section><h3>极点配置与闭环零点<a href="#极点配置与闭环零点"><span>#</span></a></h3><p>速度环 PI 用公式 <span><span>Kp=(2ζωnJ−b)/KtK_p = (2\zeta\omega_n J - b)/K_t</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span>ζ</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>Ki=ωn2J/KtK_i = \omega_n^2J/K_t</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 配到 <span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>，每个 <span><span>ωn\omega_n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 各跑一次 PI 和一次 I-P（<code>_ip</code> 后缀）。<code>pi_ch01</code> 和 <code>pi_ch01_ip</code> 是第 1 篇的增益，最后两行是对它的核对：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch02</span></div></div><div><div><div>2</div></div><div><span>... (trimmed)</span></div></div><div><div><div>3</div></div><div><span>Speed PI vs I-P: closed-loop zero at -Ki/Kp</span></div></div><div><div><div>4</div></div><div><span><span>             </span></span><span>case      Kp      Ki     wn   zeta    zero  proto %  +zero %   sim %  sat u</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>pi_place_w10   1.367   10.00  10.00  0.700   -7.32     4.60    20.06   20.29     no</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>pi_place_w10_ip   1.367   10.00  10.00  0.700   -7.32     4.60    20.06    4.70     no</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>pi_place_w30   4.167   90.00  30.00  0.700  -21.60     4.60    20.70   21.48    yes</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>pi_place_w30_ip   4.167   90.00  30.00  0.700  -21.60     4.60    20.70    4.92     no</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>pi_ch01   1.000   10.00  10.00  0.517  -10.00    15.02    27.94   28.29     no</span></div></div><div><div><div>10</div></div><div><span><span>       </span></span><span>pi_ch01_ip   1.000   10.00  10.00  0.517  -10.00    15.02    27.94   15.30     no</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>             </span></span><span>case  rise (s)  settle (s)      IAE   sim %</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>pi_place_w10    0.0868       0.488   0.4784   20.29</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>pi_place_w10_ip    0.2111       0.597   0.8070    4.70</span></div></div><div><div><div>15</div></div><div><span><span>     </span></span><span>pi_place_w30    0.0280       0.161   0.1633   21.48</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>pi_place_w30_ip    0.0694       0.199   0.2703    4.92</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>pi_place_w30_lag    0.0267       0.158   0.1678   22.58</span></div></div><div><div><div>18</div></div><div><span><span>          </span></span><span>pi_ch01    0.0953       0.745   1.2680   28.29</span></div></div><div><div><div>19</div></div><div><span><span>       </span></span><span>pi_ch01_ip    0.1659       0.791   1.7011   15.30</span></div></div><div><div><div>20</div></div><div><span>ch01 gains (Kp=1, Ki=10): PI 28.29 %, I-P 15.30 %, prototype formula 15.02 %, with the zero 27.94 %</span></div></div><div><div><div>21</div></div><div><span>pi_ch01 with the integrator updated before u (ch01's order): 28.02 %</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p></p><figure><img src="/assets/images/pid-control/ch02-pole-placement-zero.svg" alt="同样的极点配置，PI 超调 20.29%，I-P 只有 4.70%；第三格是第 1 篇的增益，28.29% 对 15.30%" /><figcaption>同样的极点配置，PI 超调 20.29%，I-P 只有 4.70%；第三格是第 1 篇的增益，28.29% 对 15.30%</figcaption></figure><p></p><p>极点配置的 <span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 是 0.7，不带零点的原型应该给 4.60%。PI 的实测是 20.29%（<span><span>ωn=10\omega_n = 10</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>）和 21.48%（<span><span>ωn=30\omega_n = 30</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span>），零点公式预测 20.06% 和 20.70%，仿真比公式高 0.23 和 0.78 个百分点，同样是采样保持的方向。<strong>零点把 4.6% 变成了 20%，是 4.4 倍。</strong> I-P 的实测是 4.70% 和 4.92%，和原型 4.60% 只差 0.10 和 0.32。第 1 篇的增益也是同一个故事：PI 28.29%，零点公式 27.94%，I-P 15.30%，原型 15.02%。</p><p>我把这件事看成一笔交易：I-P 把超调压到原型水平，上升时间从 0.0868 s 变成 0.2111 s（2.4 倍），IAE 从 0.4784 变成 0.8070（1.7 倍）。零点是在拿超调换速度。哪个对，取决于这个速度环后面接什么；如果它是串级里的内环（第 7 篇），超调往往不是问题，慢才是。</p><p>债还清了，但有三处要交代。第一，第 1 篇报告的是 28.02%，这里是 28.29%，差 0.27。不是实验不稳：这一篇的驱动在算完 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 之后才更新积分，第 1 篇的 <code>PidBasic</code> 是先更新积分再算 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>，积分项等于提前了一个采样点。我在 <code>report</code> 里把 <code>pi_ch01</code> 按第 1 篇的顺序重跑了一次，输出最后一行的 28.02% 就是它，和第 1 篇对上了。第二，<code>pi_place_w30</code> 的 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 在第一个采样点就是 <span><span>Kpr=4.167×5=20.83K_p r = 4.167\times 5 = 20.83</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>4.167</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>20.83</span></span></span></span> A，超过 20 A 的限幅，表里 <code>sat u</code> 标 yes。限幅只持续了几个采样点，所以 21.48% 里有一小部分不是线性公式能解释的，我没有把它拆出来。第三，<code>pi_place_w30_lag</code> 是同一个控制器接到带 1 ms 电流环的对象上：超调 22.58%，比没有电流环的 21.48% 多 1.1 个百分点，上升时间 0.0267 s。这就是电流环的代价，也是后面几篇都要带着 <code>MotorSpeedLag</code> 的原因。</p></section><section><h3>Routh：临界增益比算出来的低 20%<a href="#routh临界增益比算出来的低-20"><span>#</span></a></h3><p>拨弹盘 P 控制，增益取 Routh 临界增益的 0.7、0.9、1.1 倍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch02</span></div></div><div><div><div>2</div></div><div><span>... (trimmed)</span></div></div><div><div><div>3</div></div><div><span>Routh: K_crit = 16.7333 A/rad, crossing at w = sqrt(b/(J tau_c)) = 31.623 rad/s</span></div></div><div><div><div>4</div></div><div><span>exact 1 kHz ZOH loop: critical gain 13.3776 A/rad = 0.7995 K_crit</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>case       K   pole re  ZOH pred  sim slope  max|u|</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>routh_0.7  11.713   -0.2983   -0.1236    -0.1236    0.59</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>routh_0.9  15.060   -0.0993   +0.1247    +0.1247    0.95</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>routh_1.1  18.407   +0.0991   +0.3721    +0.3721    1.86</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch02-routh-boundary.svg" alt="Routh 临界增益 16.73 与采样后的临界增益 13.38；0.9 倍的增益在 1 kHz 采样下已经不稳" /><figcaption>Routh 临界增益 16.73 与采样后的临界增益 13.38；0.9 倍的增益在 1 kHz 采样下已经不稳</figcaption></figure><p></p><p>这是这一篇里和我预期不符的地方。我原本打算只用 0.9 和 1.1 倍的 <span><span>KcritK_{crit}</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span><span>cr</span><span>i</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，预期 0.9 衰减、1.1 发散。实测不是这样：<code>routh_0.9</code> 和 <code>routh_1.1</code> 的响应都在增长，只有我后来加的 <code>routh_0.7</code> 是衰减的。</p><p>原因在表的 <code>ZOH pred</code> 一列：我用上面那个精确离散模型预测的增长率，和仿真里把偏差包络的峰值取对数、做线性回归得到的斜率，在 0.7、0.9、1.1 倍三个增益上分别是 <span><span>−0.1236-0.1236</span><span><span><span></span><span>−</span><span>0.1236</span></span></span></span>、<span><span>+0.1247+0.1247</span><span><span><span></span><span>+</span><span>0.1247</span></span></span></span>、<span><span>+0.3721+0.3721</span><span><span><span></span><span>+</span><span>0.3721</span></span></span></span>，<strong>小数点后四位完全一致</strong>。也就是说仿真没问题，我的“连续时间”预测错了：连续模型说 0.9 倍时最右的极点实部是 <span><span>−0.0993-0.0993</span><span><span><span></span><span>−</span><span>0.0993</span></span></span></span>（稳定），而带保持器的 1 kHz 系统里这个模式在以每秒 <span><span>e0.1247e^{0.1247}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>0.1247</span></span></span></span></span></span></span></span></span></span></span></span> 的速率增长。精确离散模型算出来的临界增益是 13.3776 A/rad，是连续 Routh 值的 0.7995 倍：<strong>一个 1 ms 的采样周期，吃掉了 20% 的增益裕度。</strong> 图下半部分的红线（16.73，连续）和绿线（13.38，采样后）就是这两个边界。</p><p>我很想说这是意外，但回头看是合理的。在 <span><span>ωc=31.623\omega_c = 31.623</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>31.623</span></span></span></span> rad/s 处，对象的相位是 <span><span>−90°-90°</span><span><span><span></span><span>−</span><span>90°</span></span></span></span>（积分器）<span><span>−arctan⁡(ωcJ/b)=−86.4°-\arctan(\omega_c J/b) = -86.4°</span><span><span><span></span><span>−</span><span></span><span>arctan</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>J</span><span>/</span><span>b</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>86.4°</span></span></span></span>（<span><span>JJ</span><span><span><span></span><span>J</span></span></span></span>、<span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 那一极点）<span><span>−arctan⁡(ωcτc)=−3.6°-\arctan(\omega_c\tau_c) = -3.6°</span><span><span><span></span><span>−</span><span></span><span>arctan</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>3.6°</span></span></span></span>（电流环），加起来正好 <span><span>−180°-180°</span><span><span><span></span><span>−</span><span>180°</span></span></span></span>。也就是说它几乎是个双积分器，电流环只给了 3.6° 的滞后，边界附近的相位裕度就薄到这个量级。保持器和采样再加上 <span><span>ωcts/2=0.016\omega_c t_s/2 = 0.016</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span><span></span><span>=</span><span></span></span><span><span></span><span>0.016</span></span></span></span> rad，约 0.91°，相对 3.6° 不是小数，所以临界增益掉得很明显。这是我手算的估计，不是仿真输出，但方向和量级对得上。第 3 篇会用频域把“还剩多少相位和延迟”变成一个直接可算的数，不用每次靠重新离散化去试。</p></section><section><h3>根轨迹<a href="#根轨迹"><span>#</span></a></h3><p>两条轨迹的关键位置，都来自 <code>rlocus_p</code> 和 <code>rlocus_pd</code> 的 CSV：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch02</span></div></div><div><div><div>2</div></div><div><span>... (trimmed)</span></div></div><div><div><div>3</div></div><div><span>Root locus over 400 log-spaced K in [0.01, 1000]</span></div></div><div><div><div>4</div></div><div><span>breakaway estimate b^2 / (4 J Kt) = 0.0167 (lag ignored)</span></div></div><div><div><div>5</div></div><div><span>rlocus_p: a pair leaves the real axis between K = 0.0163 and 0.0168</span></div></div><div><div><div>6</div></div><div><span>rlocus_p: rightmost pole crosses the imaginary axis between K = 16.6170 and 17.1034 (Routh: 16.7333)</span></div></div><div><div><div>7</div></div><div><span>rlocus_p: largest real part over the grid +42.6398 at K = 1000.0000</span></div></div><div><div><div>8</div></div><div><span>rlocus_pd: a pair leaves the real axis between K = 0.0173 and 0.0178</span></div></div><div><div><div>9</div></div><div><span>rlocus_pd: a pair returns to the real axis between K = 22.8242 and 23.4924</span></div></div><div><div><div>10</div></div><div><span>rlocus_pd: a pair leaves the real axis between K = 44.3217 and 45.6192</span></div></div><div><div><div>11</div></div><div><span>rlocus_pd: largest real part over the grid -0.3594 at K = 0.0100</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/pid-control/ch02-root-locus.svg" alt="P 对象的根轨迹穿过虚轴进入右半平面，加了 PD 零点后整条轨迹都留在左半平面" /><figcaption>P 对象的根轨迹穿过虚轴进入右半平面，加了 PD 零点后整条轨迹都留在左半平面</figcaption></figure><p></p><p>上排是整条轨迹的全貌，下排是原点附近的放大。左列（P）：一对极点在 <span><span>K=0.0163K = 0.0163</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0163</span></span></span></span> 到 <span><span>0.01680.0168</span><span><span><span></span><span>0.0168</span></span></span></span> 之间离开实轴，我用忽略电流环的二阶近似 <span><span>b2−4JKKt=0b^2 - 4JKK_t = 0</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>4</span><span>J</span><span>K</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 估计的是 0.0167，落在这个区间里。然后轨迹向上弯，在 <span><span>K=16.62K = 16.62</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>16.62</span></span></span></span> 到 <span><span>17.1017.10</span><span><span><span></span><span>17.10</span></span></span></span> 之间穿过虚轴（图里的红星是 <span><span>±31.623j\pm 31.623j</span><span><span><span></span><span>±</span><span>31.623</span><span>j</span></span></span></span>），Routh 值 16.7333 在这个区间里，这是对 Routh 推导的一次独立检验。<span><span>K=1000K = 1000</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>1000</span></span></span></span> 时最右的极点实部是 <span><span>+42.64+42.64</span><span><span><span></span><span>+</span><span>42.64</span></span></span></span>，早就不稳了。</p><p>右列（PD）：开环零点在 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span>（空心圆）。轨迹也先离开实轴（<span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 在 0.0173 到 0.0178 之间），但随后绕了一个半圆，在 <span><span>K=22.82K = 22.82</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>22.82</span></span></span></span> 到 <span><span>23.4923.49</span><span><span><span></span><span>23.49</span></span></span></span> 之间回到实轴，一个根被零点吸住（最后贴着 <span><span>−20-20</span><span><span><span></span><span>−</span><span>20</span></span></span></span>），另一个往左，然后在 <span><span>K=44.32K = 44.32</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>44.32</span></span></span></span> 到 <span><span>45.6245.62</span><span><span><span></span><span>45.62</span></span></span></span> 之间和 <span><span>−500-500</span><span><span><span></span><span>−</span><span>500</span></span></span></span> 那一支碰头，再次变成复数对，沿着实部约 <span><span>−240-240</span><span><span><span></span><span>−</span><span>240</span></span></span></span> 的竖线往上跑（图里上排右）。<strong>整个网格上最大的实部是 <span><span>−0.3594-0.3594</span><span><span><span></span><span>−</span><span>0.3594</span></span></span></span>，在 <span><span>K=0.01K = 0.01</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span>。</strong> 上一节 Routh 推出的“PD 对所有 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 稳定”在数据里没有被推翻。（我只试了 <span><span>K≤1000K \le 1000</span><span><span><span></span><span>K</span><span></span><span>≤</span><span></span></span><span><span></span><span>1000</span></span></span></span>，这不是证明，证明是上面方括号那一行。）</p><p>PD 轨迹中间那一段几乎是个圆，圆心在零点。两个极点加一个零点的根轨迹有一个经典结论：复数段是以零点为圆心的圆，半径是零点到两个极点距离的几何平均，这里是 <span><span>20×18=18.97\sqrt{20\times 18} = 18.97</span><span><span><span></span><span><span><span><span><span><span></span><span><span>20</span><span></span><span>×</span><span></span><span>18</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>18.97</span></span></span></span>，回到实轴的位置因此在 <span><span>−20−18.97=−38.97-20 - 18.97 = -38.97</span><span><span><span></span><span>−</span><span>20</span><span></span><span>−</span><span></span></span><span><span></span><span>18.97</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>38.97</span></span></span></span>。我没有在这里推这个结论，只是拿图来对：下排右图里圆的左端在 <span><span>−40-40</span><span><span><span></span><span>−</span><span>40</span></span></span></span> 附近，和 <span><span>−38.97-38.97</span><span><span><span></span><span>−</span><span>38.97</span></span></span></span> 对得上，<span><span>−500-500</span><span><span><span></span><span>−</span><span>500</span></span></span></span> 的电流环极点让它略微变形。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 <code>pid-lab</code> 的根目录、<code>nix develop</code> 里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>ch02</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出就是上面“结果”里贴的那些：三行 <code>wrote 14 cases, metrics and 2 loci</code>，51 行 <code>ok</code>（每个对比文件 C、C++、Rust 各一行，最大差都小于 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>），然后是指标表和 <code>Durand-Kerner vs numpy.roots over 800 polys: max root distance 1.02e-12</code>。<code>out/ch02/</code> 下应该有 68 个 CSV，<code>figures/ch02/</code> 下 4 张图。</p><p>风格和编译检查，参数和第 1 篇一样。C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>（15.3.0），C++ 是 <code>g++ -std=c++20</code>（15.3.0）加同样的警告，Rust 是 rustc 1.95.0、edition 2024，<code>cargo clippy --release -- -D warnings</code>（0.1.95）。C 用内核的 <code>checkpatch.pl --no-tree --ignore SPDX_LICENSE_TAG,NEW_TYPEDEFS</code>，全部文件 0 errors、0 warnings；C++ 用 <code>clang-format --style=Google</code>（21.1.8）；Python 用 <code>ruff format --line-length 79</code> 和 <code>ruff check</code>（0.15.14）；Rust 用 <code>cargo fmt --check</code>。另外我手工用 <code>-DPL_USE_FLOAT</code> 把 <code>poly.c</code>、<code>metrics.c</code>、<code>plants.c</code> 编了一遍，用 <code>cargo build --lib --no-default-features --features f32</code> 编了 Rust 库，都没有警告。</p><section><h3>文件清单<a href="#文件清单"><span>#</span></a></h3><p>这一篇新增的文件，以及它依赖的前一篇的文件（tag <code>tutorial-v1</code>）。模块：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/poly.py" target="_blank"><code>python/pidlab/poly.py</code></a></td><td><code>make -s ch02</code></td><td>多项式运算与 Durand–Kerner，对 <code>numpy.roots</code> 最大距离 <span><span>1.02×10−121.02\times 10^{-12}</span><span><span><span></span><span>1.02</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/metrics.py" target="_blank"><code>python/pidlab/metrics.py</code></a></td><td><code>make -s ch02</code></td><td>五个阶跃指标，<code>metrics_*.csv</code> 四份逐位一致</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a></td><td><code>make -s ch02</code></td><td>新增 <code>MotorSpeedLag</code>、<code>DialPosition</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/poly.h" target="_blank"><code>c/include/pl/poly.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/poly.c" target="_blank"><code>c/src/poly.c</code></a></td><td><code>make -s ch02</code></td><td><code>rlocus_p_c.csv</code> 最大差 <span><span>2.842×10−142.842\times 10^{-14}</span><span><span><span></span><span>2.842</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/metrics.h" target="_blank"><code>c/include/pl/metrics.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/metrics.c" target="_blank"><code>c/src/metrics.c</code></a></td><td><code>make -s ch02</code></td><td><code>metrics_c.csv</code> 最大差 0</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/plants.h" target="_blank"><code>c/include/pl/plants.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/plants.c" target="_blank"><code>c/src/plants.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/real.h" target="_blank"><code>c/include/pl/real.h</code></a></td><td><code>make -s ch02</code></td><td>两个新对象与 <code>pl_cplx</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/poly.h" target="_blank"><code>cpp/include/pidlab/poly.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/metrics.h" target="_blank"><code>cpp/include/pidlab/metrics.h</code></a></td><td><code>make -s ch02</code></td><td><code>rlocus_pd_cpp.csv</code> 最大差 <span><span>7.816×10−147.816\times 10^{-14}</span><span><span><span></span><span>7.816</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/plants.h" target="_blank"><code>cpp/include/pidlab/plants.h</code></a></td><td><code>make -s ch02</code></td><td>两个新对象</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/complex.rs" target="_blank"><code>rust/src/complex.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/poly.rs" target="_blank"><code>rust/src/poly.rs</code></a></td><td><code>make -s ch02</code></td><td><code>rlocus_pd_rs.csv</code> 最大差 <span><span>1.386×10−131.386\times 10^{-13}</span><span><span><span></span><span>1.386</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/metrics.rs" target="_blank"><code>rust/src/metrics.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/plants.rs" target="_blank"><code>rust/src/plants.rs</code></a></td><td><code>make -s ch02</code></td><td>指标与两个新对象</td></tr></tbody></table><p>驱动、应用，以及这一篇用到的第 1 篇的文件：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch02_transfer_functions.py" target="_blank"><code>python/ch02_transfer_functions.py</code></a></td><td><code>python3 python/ch02_transfer_functions.py ref</code>，然后 <code>report</code></td><td>写数据和 Python 参考输出；<code>report</code> 比对、打印指标表、画 4 张图</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch02.c" target="_blank"><code>c/apps/ch02.c</code></a></td><td><code>make -s ch02</code></td><td><code>ch02: C wrote 14 cases, metrics and 2 loci to out/ch02</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch02.cc" target="_blank"><code>cpp/apps/ch02.cc</code></a></td><td><code>make -s ch02</code></td><td><code>ch02: C++ wrote 14 cases, metrics and 2 loci to out/ch02</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch02.rs" target="_blank"><code>rust/src/bin/ch02.rs</code></a></td><td><code>make -s ch02</code></td><td><code>ch02: Rust wrote 14 cases, metrics and 2 loci to out/ch02</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>python/pidlab/sim.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/sim.c" target="_blank"><code>c/src/sim.c</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/sim.h" target="_blank"><code>cpp/include/pidlab/sim.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/sim.rs" target="_blank"><code>rust/src/sim.rs</code></a></td><td>第 1 篇已有</td><td>RK4 与延迟线，循环顺序不变</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/compare.py" target="_blank"><code>python/compare.py</code></a></td><td><code>make -s ch02</code></td><td>51 行 <code>ok</code>，容差 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span></td></tr></tbody></table></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把极点从“调参的结果”变成了“可以直接放的东西”：<span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KiK_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KdK_d</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 由 <span><span>ωn\omega_n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>ζ\zeta</span><span><span><span></span><span>ζ</span></span></span></span> 算出来，Routh 判据和根轨迹给出稳定的增益范围，I-P 把零点带来的超调拿掉。同时我也发现，连续时间的稳定边界在 1 kHz 采样下少了 20%，这个损失来自保持器和电流环叠加出来的延迟，而我只能靠“重新算一遍离散模型”去量它。</p><p><a href="/posts/pid-control-03-frequency-domain/">第 3 篇：频域：Bode、Nyquist 与稳定裕度</a>会换一个视角：不再问极点在哪，而是问回路对不同频率的正弦信号放大多少、延迟多少相位。相位裕度和延迟裕度会把这一篇里的 20% 变成一个直接可算的数；同样的 PI（<span><span>ωn=10\omega_n = 10</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>、<span><span>ζ=0.7\zeta = 0.7</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.7</span></span></span></span>）加上 <code>MotorSpeedLag</code> 和一个采样周期的纯延迟，我会拿它做“把延迟加到裕度的 0.9 倍和 1.1 倍，看它衰减还是发散”的实验，这次的预测来自裕度，不来自重新离散化。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/pid-control-01-p-i-d/</id>
      <title type="text">P、I、D 各自在干什么</title>
      <published>2026-10-16T00:00:00.000Z</published>
      <updated>2026-10-16T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/pid-control-01-p-i-d/"/>
      <summary type="text">PID 控制系列第 1 篇：我用 Python 3.13、gcc 15.3、g++ 15.3 和 rustc 1.95 把同一个位置式 PID 写了四遍，在仿真的 M3508 转速环和 GM6020 云台上跑 9 组实验，四种语言的输出逐位一致，再逐项看 P、I、D 各自改变了什么。</summary>
      <content type="html"><![CDATA[<p>1922 年，Nicolas Minorsky 在《Naval Engineers Journal》上发表了 <a href="https://onlinelibrary.wiley.com/journal/15593584" target="_blank">Directional stability of automatically steered bodies</a>（1922 年 5 月，第 280 到 309 页），分析怎么让舵机替舵手稳住一条船的航向。据我所知，今天说的“比例加积分加微分”三项控制，常被追溯到这篇论文和他在美国海军舰船上的实验（<a href="http://ieeexplore.ieee.org/iel5/37/24267/01104827.pdf?arnumber=1104827" target="_blank">Bennett 1984 的历史回顾</a>有详细经过）。一百年之后，我在 RoboMaster 里用的还是同一个东西：我写过很多年 PID，但只会一种写法，位置式、单变量、一个积分限幅、一组 <code>kp ki kd</code>，调参靠手感加运气。</p>
<p>这个系列我想把它拆开：从一个电机出发，一路走到状态反馈和 LQR，每一步都自己推公式、自己写代码。这一篇只做最小的事：在仿真的 M3508 转速环和 GM6020 云台上让 P、I、D 一项一项出场，每加一项就看一个数字变化。同一个云台，只用 P 时超调 90.76%，加上 D 降到 15.61%；再加上 I，0.1 N·m 的负载造成的 0.02 rad 静差，平均值降到 0.0007 rad。后面九篇都建立在这一篇搭好的仿真平台上。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：一份能跑的伴生仓库 <code>pid-lab</code>，<code>make ch01</code> 通过，生成 <code>out/ch01/</code> 下 36 个 CSV（9 组实验，每组 Python、C、C++、Rust 各一份）和 <code>figures/ch01/</code> 下 4 张图；以及 P、I、D 三项的完整推导。
新增模块：Python 是 <code>pidlab/sim.py</code>（RK4 与延迟线）、<code>pidlab/plants.py</code>（<code>MotorSpeed</code>、<code>GimbalAxis</code>）、<code>pidlab/pid_basic.py</code>；C 是 <code>pl/sim.h</code>、<code>pl/plants.h</code>、<code>pl/pid_basic.h</code> 及对应的 <code>.c</code>；C++ 是 <code>pidlab/sim.h</code>、<code>pidlab/plants.h</code>、<code>pidlab/pid_basic.h</code>；Rust 是 <code>sim.rs</code>、<code>plants.rs</code>、<code>pid_basic.rs</code>。
这是第 1 篇，没有前置章节。需要的数学：一阶和二阶常微分方程，知道“阻尼比”是什么就行。</p></div></div>
<section><h2>这个系列怎么读<a href="#这个系列怎么读"><span>#</span></a></h2><p>下面是整个系列的地图：</p>

<table><thead><tr><th>篇</th><th>标题</th><th>主要内容</th></tr></thead><tbody><tr><td>1</td><td>P、I、D 各自在干什么</td><td>仿真平台、四种语言的 PID、静差与超调</td></tr><tr><td>2</td><td><a href="/posts/pid-control-02-transfer-functions/">传递函数、极点与根轨迹</a></td><td>拉普拉斯变换、二阶原型、极点配置、Routh 判据、根轨迹</td></tr><tr><td>3</td><td><a href="/posts/pid-control-03-frequency-domain/">频域：Bode、Nyquist 与稳定裕度</a></td><td>相位裕度、延迟裕度、灵敏度函数</td></tr><tr><td>4</td><td><a href="/posts/pid-control-04-discretization/">离散化：位置式、增量式与采样率</a></td><td>三种离散化、位置式与增量式的等价、采样率的代价</td></tr><tr><td>5</td><td><a href="/posts/pid-control-05-practical/">工程上的坑：积分饱和、微分冲击与前馈</a></td><td>抗饱和、微分滤波、前馈、无扰切换，以及系列的 PID 库</td></tr><tr><td>6</td><td><a href="/posts/pid-control-06-tuning/">整定：Ziegler–Nichols、继电器自整定与 SIMC</a></td><td>阶跃法、继电器法、SIMC 规则</td></tr><tr><td>7</td><td><a href="/posts/pid-control-07-cascade/">串级 PID：云台的角度环和速度环</a></td><td>内外环、带宽分离、内环饱和</td></tr><tr><td>8</td><td><a href="/posts/pid-control-08-mimo/">多变量：耦合、RGA、解耦与麦轮底盘</a></td><td>RGA、解耦、麦轮的零空间电流</td></tr><tr><td>9</td><td><a href="/posts/pid-control-09-lqr/">状态反馈：LQR、LQI 与平衡步兵</a></td><td>LQR、LQI、PID 是状态反馈的一个子集</td></tr><tr><td>10</td><td><a href="/posts/pid-control-10-embedded/">上车之前：float32、定点数与 CAN 量化</a></td><td>float32、Q16.16、量化、采样抖动</td></tr></tbody></table><p><strong>四种语言。</strong> 每个控制器和每个仿真对象我都写四遍：Python（numpy，只用来当参考实现和画图，仿真本身是手写的）、C（C11，不使用 <code>malloc</code>）、C++（C++20，模板，纯头文件）、Rust（edition 2024，<code>no_std</code> 兼容）。Python 版承担推导和完整讲解；C、C++、Rust 的核心函数我会贴出来，文字只讲它们和 Python 版的区别：内存归谁、尺寸在哪里确定、标量类型怎么切换、语言本身的惯用法。文中只贴关键的几行，完整文件在仓库里，文末的检查点列了链接。</p><p><strong>同一份数据。</strong> 所有设定值、扰动和测量噪声都由 Python 生成，写成 CSV，四种语言读同一份文件，没有任何一种语言自己造随机数。这一篇的噪声是 0，但文件里已经留了噪声这一列，后面几篇会用到。这恰好也是上车之后的工作流：日志进来，控制器处理。</p><p><strong>真实输出。</strong> 文中所有终端输出、数值和图，都来自我在这台机器上跑 <code>make ch01</code> 的结果。每个浮点数在 CSV 里都写成 17 位有效数字，C、C++、Rust 的输出和 Python 参考的最大差值要在 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 以内才算通过。</p></section>
<section><h2>装环境<a href="#装环境"><span>#</span></a></h2><p>代码在 <a href="https://github.com/bfmhno3/pid-lab/tree/tutorial-v1" target="_blank">pid-lab</a>，本系列固定在 <code>tutorial-v1</code> 标签。仓库里有一个 <code>flake.nix</code>，<code>nix develop</code> 进去就是我用的全部工具：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>https://github.com/bfmhno3/pid-lab.git</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>pid-lab</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>checkout</span><span> </span><span>tutorial-v1</span></div></div><div><div><div>4</div></div><div><span>nix</span><span> </span><span>develop</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pid-lab dev shell: Python 3.13.15, gcc (GCC) 15.3.0, rustc 1.95.0 (59807616e 2026-04-14) (built from a source tarball)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有 Nix 的话，自己装这几样就行：Python 3 加 <code>numpy</code>、<code>matplotlib</code>、<code>scipy</code>；gcc 13 或更新（要支持 C11 和 C++20）；用 rustup 装的 Rust stable，外加 <code>clippy</code>。画图时中文标签用的字体通过环境变量 <code>PL_CJK_FONT</code> 指定（<code>flake.nix</code> 已经设好），找不到字体时图的标签自动退回英文。</p><p>所有命令都在仓库根目录跑。<code>make ch01</code> 做五件事：用 Python 生成数据并跑参考实现；编译 C、C++、Rust 三个版本；各自读同一份数据并输出；用 <code>python/compare.py</code> 逐列对比；最后打印指标并画图。</p></section>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>回路、对象和扰动<a href="#回路对象和扰动"><span>#</span></a></h3><p>先把整个回路画出来，后面九篇都是在这张图上加东西：</p><div><div><div><div><span></span></div><div><span><p>e = r − y</p></span></div><div><span><p>u</p></span></div><div><span></span></div><div><span></span></div><div><span><p>状态 x</p></span></div><div><span><p>y</p></span></div><div><span><p>设定值 r</p></span></div><div><span>+ −</span></div><div><span><p>PID 控制器</p></span></div><div><span><p>限幅</p></span></div><div><span><p>对象：电机或云台</p></span></div><div><span><p>扰动 d：负载力矩</p></span></div><div><span><p>测量 y = h(x) + n</p></span></div>
</div><div><div><span></span></div><div><span><p>e = r − y</p></span></div><div><span><p>u</p></span></div><div><span></span></div><div><span></span></div><div><span><p>状态 x</p></span></div><div><span><p>y</p></span></div><div><span><p>设定值 r</p></span></div><div><span>+ −</span></div><div><span><p>PID 控制器</p></span></div><div><span><p>限幅</p></span></div><div><span><p>对象：电机或云台</p></span></div><div><span><p>扰动 d：负载力矩</p></span></div><div><span><p>测量 y = h(x) + n</p></span></div>
</div></div></div><p>这一篇有两个对象，都取 RoboMaster 的量级。我先说明哪些是查到的，哪些是我自己取的：</p>

<table><thead><tr><th>对象</th><th>方程</th><th>参数</th></tr></thead><tbody><tr><td><code>MotorSpeed</code>（M3508 轮子，输出轴侧）</td><td><span><span>Jω˙=Kti−bω−τdJ\dot\omega = K_t i - b\omega - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span>bω</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></td><td><span><span>Kt=0.3K_t = 0.3</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span> N·m/A，<span><span>J=0.03J = 0.03</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.03</span></span></span></span> kg·m²，<span><span>b=0.01b = 0.01</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span> N·m·s/rad，<span><span>∣i∣≤20\lvert i\rvert \le 20</span><span><span><span></span><span>∣</span><span>i</span><span>∣</span><span></span><span>≤</span><span></span></span><span><span></span><span>20</span></span></span></span> A</td></tr><tr><td><code>GimbalAxis</code>（GM6020 yaw，力矩指令）</td><td><span><span>Jψ¨=τ−τd−bψ˙−bf(ψ˙−ωc)J\ddot\psi = \tau - \tau_d - b\dot\psi - b_f(\dot\psi - \omega_c)</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span></td><td><span><span>J=0.02J = 0.02</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span>，<span><span>b=0.002b = 0.002</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0.002</span></span></span></span>，<span><span>bf=0.02b_f = 0.02</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span>，<span><span>∣τ∣≤1\lvert\tau\rvert \le 1</span><span><span><span></span><span>∣</span><span>τ</span><span>∣</span><span></span><span>≤</span><span></span></span><span><span></span><span>1</span></span></span></span> N·m，这一篇令底盘角速度 <span><span>ωc=0\omega_c = 0</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></td></tr></tbody></table><p><span><span>KtK_t</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是查到的：<a href="https://cdn-hz.robomaster.com/robomasters/public/document/RoboMaster%20M3508%20P19%20Brushless%20DC%20Gear%20Motor%20V1.0.pdf" target="_blank">M3508 P19 的用户手册</a>在特征参数表里写转矩常数 0.3 N·m/A，额定转矩 3 N·m。手册没有说这个常数折算在转子侧还是输出轴侧，我把它当输出轴侧的值用，这是我的假设（3 N·m 除以 0.3 N·m/A 得 10 A，和手册的额定电流 10 A 在数量级上吻合，但手册说那是电调输入端的电流，所以我不拿它当证据）。电流上限 20 A 来自 <a href="https://www.robomaster.com/zh-CN/products/components/general/M3508" target="_blank">RoboMaster 官网上 C620 电调的参数</a>：最大持续电流 20 A。</p><p><span><span>JJ</span><span><span><span></span><span>J</span></span></span></span> 和 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 是我取的，我把 <span><span>J=0.03J = 0.03</span><span><span><span></span><span>J</span><span></span><span>=</span><span></span></span><span><span></span><span>0.03</span></span></span></span> 当成四分之一台 20 kg 的车压在半径 76 mm 的轮子上：<span><span>5×0.0762≈0.0295\times 0.076^2 \approx 0.029</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>0.07</span><span><span>6</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>0.029</span></span></span></span>。云台的 <span><span>JJ</span><span><span><span></span><span>J</span></span></span></span>、<span><span>bb</span><span><span><span></span><span>b</span></span></span></span>、<span><span>bfb_f</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、力矩上限也全是我取的，只是量级合理的数。第 7 篇会解释 <span><span>bfb_f</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是什么（轴承把底盘的转动带给云台）。你换一组自己车上辨识出来的参数，所有结论的形状不变，数字会变。</p><p>采样周期 <span><span>ts=1t_s = 1</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> ms，我选它是因为据我所知 RM 的 CAN 电机反馈大约是这个频率，没有逐个核实。对象用 RK4 积分，每个控制周期切成 10 小步。</p></section><section><h3>速度环是一阶系统<a href="#速度环是一阶系统"><span>#</span></a></h3><p>电机在常数电流 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 和常数负载 <span><span>τd\tau_d</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 下是一阶线性方程 <span><span>Jω˙+bω=Kti−τdJ\dot\omega + b\omega = K_t i - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>bω</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，解是</p><span><span><span>ω(t)=ω∞(1−e−t/T),T=Jb,ω∞=Kti−τdb\omega(t) = \omega_\infty\left(1 - e^{-t/T}\right),\quad T = \frac{J}{b},\quad \omega_\infty = \frac{K_t i - \tau_d}{b}</span><span><span><span></span><span>ω</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span>(</span></span><span>1</span><span></span><span>−</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>t</span><span>/</span><span>T</span></span></span></span></span></span></span></span></span><span><span>)</span></span></span><span></span><span>,</span><span></span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span></span></span><span><span></span><span><span>J</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>b</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>代进去 <span><span>T=0.03/0.01=3T = 0.03/0.01 = 3</span><span><span><span></span><span>T</span><span></span><span>=</span><span></span></span><span><span></span><span>0.03/0.01</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span> s。开环的轮子要 3 秒才爬到终值的 63%，这就是需要闭环的原因：控制器的第一件工作是把 3 秒压下来。</p></section><section><h3>P：比例，永远留一个误差<a href="#p比例永远留一个误差"><span>#</span></a></h3><p>令 <span><span>u=Kpeu = K_p e</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span></span></span></span>，<span><span>e=r−ωe = r - \omega</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ω</span></span></span></span>。代进模型：</p><span><span><span>Jω˙=KtKp(r−ω)−bω−τd  ⇒  Jω˙+(b+KtKp) ω=KtKpr−τdJ\dot\omega = K_t K_p (r - \omega) - b\omega - \tau_d \;\Rightarrow\; J\dot\omega + (b + K_t K_p)\,\omega = K_t K_p r - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ω</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>bω</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span></span><span>⇒</span><span></span><span></span></span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>还是一阶系统，时间常数从 <span><span>J/bJ/b</span><span><span><span></span><span>J</span><span>/</span><span>b</span></span></span></span> 变成 <span><span>J/(b+KtKp)J/(b + K_t K_p)</span><span><span><span></span><span>J</span><span>/</span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。<span><span>Kp=1K_p = 1</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 时是 <span><span>0.03/0.31=0.09680.03/0.31 = 0.0968</span><span><span><span></span><span>0.03/0.31</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0968</span></span></span></span> s，比开环快了 31 倍。令 <span><span>ω˙=0\dot\omega = 0</span><span><span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 得稳态转速，误差是</p><span><span><span>ess=r−ω∞=b r+τdb+KtKpe_{ss} = r - \omega_\infty = \frac{b\,r + \tau_d}{b + K_t K_p}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>b</span><span></span><span>+</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>b</span><span></span><span>r</span><span></span><span>+</span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>这个式子里 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 只在分母上，所以大 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 能压小误差，但分子永远不为零：只要 <span><span>r≠0r \ne 0</span><span><span><span></span><span>r</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span>，轮子的黏性阻尼 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 就需要一个非零的电流来克服，而 P 控制器只有在 <span><span>e≠0e \ne 0</span><span><span><span></span><span>e</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>0</span></span></span></span> 时才会输出电流。负载 <span><span>τd\tau_d</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 也是同样的道理。P 控制的误差不是调参能解决的，是结构性的。</p></section><section><h3>I：积分，强制误差归零<a href="#i积分强制误差归零"><span>#</span></a></h3><p>令 <span><span>u=Kpe+Ki∫e dtu = K_p e + K_i \int e\,dt</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span></span><span>e</span><span></span><span>d</span><span>t</span></span></span></span>，把积分记成 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>（<span><span>z˙=e\dot z = e</span><span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>e</span></span></span></span>）。假设闭环最终停在某个平衡点，所有量都不再变化。那么 <span><span>z˙=e\dot z = e</span><span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>e</span></span></span></span> 也必须是 0，否则 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 在变，<span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 在变，就不是平衡点。<strong>积分器是一个只有在 <span><span>e=0e = 0</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时才能停下来的东西</strong>，这就是它的全部作用。（类比一下：浴缸里的水位。只要进水口和出水口流量不等，水位就在变；水位不变，必然流量相等。积分器的“水位”就是 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 里那一份，它会一直调节到“出入相等”，也就是误差为零。）</p><p>同时看看它对动态的影响。对 <span><span>Jω˙=Kt(Kpe+Kiz)−bω−τdJ\dot\omega = K_t(K_p e + K_i z) - b\omega - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>z</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>bω</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 求一次导，再用 <span><span>z˙=e\dot z = e</span><span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>e</span></span></span></span> 和 <span><span>e˙=r˙−ω˙\dot e = \dot r - \dot\omega</span><span><span><span></span><span><span><span><span><span><span></span><span>e</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>r</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>Jω¨+(b+KtKp) ω˙+KtKi ω=KtKp r˙+KtKi r−τ˙dJ\ddot\omega + (b + K_t K_p)\,\dot\omega + K_t K_i\,\omega = K_t K_p\,\dot r + K_t K_i\,r - \dot\tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>¨</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span>r</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>τ</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>变成二阶系统。<span><span>Kp=1K_p = 1</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，<span><span>Ki=10K_i = 10</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 时：<span><span>ωn=KtKi/J=10\omega_n = \sqrt{K_t K_i / J} = 10</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>J</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> rad/s，阻尼比 <span><span>ζ=(b+KtKp)/(2Jωn)=0.517\zeta = (b + K_t K_p)/(2J\omega_n) = 0.517</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span>(</span><span>2</span><span>J</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.517</span></span></span></span>。不带零点的二阶系统在这个阻尼比下超调应该是 <span><span>e−πζ/1−ζ2=15.0%e^{-\pi\zeta/\sqrt{1-\zeta^2}} = 15.0\%</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>π</span><span>ζ</span><span>/</span><span><span><span><span><span><span></span><span><span>1</span><span>−</span><span><span>ζ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15.0%</span></span></span></span>。后面我会拿实测去对比，剧透：对不上。</p></section><section><h3>D：微分，给二阶对象加阻尼<a href="#d微分给二阶对象加阻尼"><span>#</span></a></h3><p>云台和轮子的区别是它从力矩到角度是两次积分（再加一点点阻尼）。令底盘不转，<span><span>Jψ¨=τ−τd−(b+bf)ψ˙J\ddot\psi = \tau - \tau_d - (b + b_f)\dot\psi</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。只用 P：<span><span>τ=Kp(r−ψ)\tau = K_p(r - \psi)</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ψ</span><span>)</span></span></span></span>，得</p><span><span><span>Jψ¨+(b+bf)ψ˙+Kpψ=Kpr−τdJ\ddot\psi + (b + b_f)\dot\psi + K_p\psi = K_p r - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ψ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>这是弹簧质量阻尼器，<span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是弹簧，<span><span>b+bfb + b_f</span><span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是阻尼。固有频率 <span><span>ωn=Kp/J=5/0.02=15.81\omega_n = \sqrt{K_p/J} = \sqrt{5/0.02} = 15.81</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>J</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>5/0.02</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15.81</span></span></span></span> rad/s，阻尼比 <span><span>ζ=(b+bf)/(2JKp)=0.022/0.6325=0.035\zeta = (b+b_f)/(2\sqrt{JK_p}) = 0.022/0.6325 = 0.035</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span>(</span><span>2</span><span><span><span><span><span><span></span><span><span>J</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.022/0.6325</span><span></span><span>=</span><span></span></span><span><span></span><span>0.035</span></span></span></span>。这个阻尼比几乎是零：云台会像一根没有减震器的弹簧一样来回晃。超调公式给出 <span><span>e−πζ/1−ζ2=89.6%e^{-\pi\zeta/\sqrt{1-\zeta^2}} = 89.6\%</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>π</span><span>ζ</span><span>/</span><span><span><span><span><span><span></span><span><span>1</span><span>−</span><span><span>ζ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>89.6%</span></span></span></span>。</p><p>加上 D：<span><span>τ=Kpe+Kde˙\tau = K_p e + K_d \dot e</span><span><span><span></span><span>τ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>e</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span>，其中 <span><span>e˙≈−ψ˙\dot e \approx -\dot\psi</span><span><span><span></span><span><span><span><span><span><span></span><span>e</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>−</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>（设定值不动的时候）。D 项贡献的是一个正比于速度的力矩，和 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 一样是阻尼：</p><span><span><span>Jψ¨+(b+bf+Kd) ψ˙+Kpψ=Kpr−τdJ\ddot\psi + (b + b_f + K_d)\,\dot\psi + K_p\psi = K_p r - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ψ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>Kd=0.3K_d = 0.3</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span></span></span></span> 时 <span><span>ζ=0.322/0.6325=0.509\zeta = 0.322/0.6325 = 0.509</span><span><span><span></span><span>ζ</span><span></span><span>=</span><span></span></span><span><span></span><span>0.322/0.6325</span><span></span><span>=</span><span></span></span><span><span></span><span>0.509</span></span></span></span>，超调预期降到 15.6%。<strong>D 项就是人为加上的阻尼。</strong>（飞机的偏航阻尼器也叫这个名字，同一回事：测角速度，反向加力矩。）</p><p>静差：令导数全为零，<span><span>Kp(r−ψ)=τdK_p(r - \psi) = \tau_d</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>ψ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以 <span><span>ess=τd/Kp=0.1/5=0.02e_{ss} = \tau_d / K_p = 0.1/5 = 0.02</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1/5</span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span> rad。D 对它没有任何帮助，因为稳态时 <span><span>e˙=0\dot e = 0</span><span><span><span></span><span><span><span><span><span><span></span><span>e</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。要消掉这个静差还是得 I。三项都加上就是三阶：</p><span><span><span>Jψ...+(b+bf+Kd)ψ¨+Kpψ˙+Kiψ=…J\dddot\psi + (b + b_f + K_d)\ddot\psi + K_p\dot\psi + K_i\psi = \dots</span><span><span><span></span><span>J</span><span><span><span><span><span><span><span><span></span><span><span>ψ</span></span></span><span><span></span><span><span><span><span><span><span><span></span><span><span>...</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>ψ</span><span></span><span>=</span><span></span></span><span><span></span><span>…</span></span></span></span></span><p>三阶系统稳定需要 <span><span>(b+bf+Kd)Kp&gt;JKi(b + b_f + K_d)K_p &gt; J K_i</span><span><span><span></span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>J</span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，这里是 <span><span>0.322×5=1.61&gt;0.02×20=0.40.322\times 5 = 1.61 &gt; 0.02\times 20 = 0.4</span><span><span><span></span><span>0.322</span><span></span><span>×</span><span></span></span><span><span></span><span>5</span><span></span><span>=</span><span></span></span><span><span></span><span>1.61</span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0.02</span><span></span><span>×</span><span></span></span><span><span></span><span>20</span><span></span><span>=</span><span></span></span><span><span></span><span>0.4</span></span></span></span>，满足。这是 Routh 判据的一个特例，第 2 篇会从头推。</p></section><section><h3>采样：回路里还有一个保持器<a href="#采样回路里还有一个保持器"><span>#</span></a></h3><p>控制器每 1 ms 才看一次 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span>，输出 <span><span>uku_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在下一个采样点之前保持不变（零阶保持，ZOH）。一个保持器平均相当于 <span><span>ts/2t_s/2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span> 的延迟，再加上“测量到输出”本身晚一拍，大约是一个半采样周期的纯延迟。<span><span>ts=1t_s = 1</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> ms 时它远小于对象的时间常数，可以忽略；但我后面会遇到它：下面 <span><span>esse_{ss}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的验证里，我得把预测向后移一个采样周期才对得上。第 3、4 篇会把它变成裕度和采样率的数字。</p><p>最后是我要先打个预防针的一点：<strong>很多 RM 代码里 <code>ki</code> 和 <code>kd</code> 是把 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 吸收进去的</strong>，比如积分写成 <code>integral += error</code> 而不乘 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，微分写成 <code>error - last_error</code> 而不除 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。据我所知这很常见，但我没有统计过。这样写的增益数值和我这里的 <span><span>KiK_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KdK_d</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 差一个 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 因子，换采样率就要重调。我这里把 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 显式留在公式里，第 4 篇会专门算这笔账。</p></section></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><section><h3>积分器、延迟线和对象<a href="#积分器延迟线和对象"><span>#</span></a></h3><p>对象的方程要变成代码，先要一个积分器。我用最朴素的经典 RK4：状态是 <code>list[float]</code>，对象给出 <code>deriv(x, u)</code>，<code>rk4_step</code> 在一步里调用它四次，输入 <code>u</code> 在这一步里保持不变：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>rk4_step</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>f</span><span>:</span><span> Deriv</span><span>,</span><span><span> </span><span>x</span></span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>u</span></span><span>:</span><span> Sequence[</span><span>float</span><span>]</span><span>,</span><span><span> </span><span>h</span></span><span>:</span><span> </span><span>float</span></div></div><div><div><div>3</div></div><div><span>) -&gt; list[</span><span>float</span><span>]:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"""Advance dx/dt = f(x, u) by h with classic RK4, u held constant."""</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(x)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>k1 </span><span>=</span><span> </span><span>f</span><span>(x, u)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>k2 </span><span>=</span><span> </span><span>f</span><span>([x[i] </span><span>+</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> h </span><span>*</span><span> k1[i] </span></span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n)], u)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>k3 </span><span>=</span><span> </span><span>f</span><span>([x[i] </span><span>+</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> h </span><span>*</span><span> k2[i] </span></span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n)], u)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>k4 </span><span>=</span><span> </span><span>f</span><span>([x[i] </span><span>+</span><span> h </span><span>*</span><span> k3[i] </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n)], u)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> [</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>x[i] </span><span>+</span><span> h </span><span>/</span><span> </span><span>6.0</span><span><span> </span><span>*</span><span> (k1[i] </span><span>+</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> k2[i] </span><span>+</span><span> </span></span><span>2.0</span><span><span> </span><span>*</span><span> k3[i] </span><span>+</span><span> k4[i])</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个控制周期里 <code>advance</code> 把它调 <code>substeps</code> 次（我取 10），每次步长 <code>ts / substeps</code>。指令的传输延迟（整数个采样周期）用 <code>DelayLine</code>：一个 <code>deque</code> 当环形缓冲区，<code>push(u)</code> 存进去，取出 <code>delay</code> 次之前存的值，<code>delay = 0</code> 直接透传。这一篇的延迟是 0，但后面几篇都要用，所以现在就放进回路里。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>push</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>u</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span>        </span><span>"""Store u, return the value pushed `delay` calls ago."""</span></div></div><div><div><div>3</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span> </span><span>self</span><span>.buf:</span></div></div><div><div><div>4</div></div><div><span>            </span><span>return</span><span> u</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>out </span><span>=</span><span> </span><span>self</span><span><span>.buf.</span><span>popleft</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.buf.</span><span>append</span><span>(u)</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span> out</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对象只负责回答“在状态 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>、输入 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span> 下导数是多少”。输入向量约定为 <code>u = [指令, 扰动...]</code>，所以扰动只是 <code>u</code> 的额外条目，数据文件里写什么，对象就收到什么。这个约定后面九篇一直用。转速环和云台各一个方法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span>        </span><span>return</span><span> [(</span><span>self</span><span><span>.kt </span><span>*</span><span> u[</span></span><span>0</span><span><span>] </span><span>-</span><span> </span></span><span>self</span><span><span>.b </span><span>*</span><span> x[</span></span><span>0</span><span><span>] </span><span>-</span><span> u[</span></span><span>1</span><span><span>]) </span><span>/</span><span> </span></span><span>self</span><span>.j]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>deriv</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x</span></span><span>,</span><span><span> </span><span>u</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>acc </span><span>=</span><span> (u[</span><span>0</span><span><span>] </span><span>-</span><span> u[</span></span><span>1</span><span><span>] </span><span>-</span><span> </span></span><span>self</span><span><span>.b </span><span>*</span><span> x[</span></span><span>1</span><span><span>] </span><span>-</span><span> </span></span><span>self</span><span><span>.bf </span><span>*</span><span> (x[</span></span><span>1</span><span><span>] </span><span>-</span><span> u[</span></span><span>2</span><span><span>])) </span><span>/</span><span> </span></span><span>self</span><span>.j</span></div></div><div><div><div>3</div></div><div><span>        </span><span>return</span><span> [x[</span><span>1</span><span>], acc]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上面是 <code>MotorSpeed</code> 和 <code>GimbalAxis</code> 的 <code>deriv</code>，分别就是 <span><span>Jω˙=Kti−bω−τdJ\dot\omega = K_t i - b\omega - \tau_d</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>i</span><span></span><span>−</span><span></span></span><span><span></span><span>bω</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>Jψ¨=τ−τd−bψ˙−bf(ψ˙−ωc)J\ddot\psi = \tau - \tau_d - b\dot\psi - b_f(\dot\psi - \omega_c)</span><span><span><span></span><span>J</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>¨</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>τ</span><span></span><span>−</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 解出导数的样子。云台的状态是 <span><span>[ψ,ψ˙][\psi, \dot\psi]</span><span><span><span></span><span>[</span><span>ψ</span><span>,</span><span></span><span><span><span><span><span><span></span><span>ψ</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>]</span></span></span></span>，所以第一个分量就是 <code>x[1]</code>。</p></section><section><h3>位置式 PID<a href="#位置式-pid"><span>#</span></a></h3><p>这就是 RM 比赛代码里最常见的那一类：积分是矩形累加再限幅，微分是对误差的后向差分，第一拍微分为 0（<code>primed</code> 标志防止第一拍用一个不存在的上一次误差），输出限幅。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>ref</span></span><span>:</span><span> </span><span>float</span><span>,</span><span><span> </span><span>meas</span></span><span>:</span><span> </span><span>float</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> ref </span><span>-</span><span> meas</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span><span>.integral </span><span>+=</span><span> err </span><span>*</span><span> </span></span><span>self</span><span>.ts</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.integral </span><span>=</span><span> </span><span>clamp</span><span>(</span></span><span>self</span><span><span>.integral, </span><span>-</span></span><span>self</span><span>.i_limit, </span><span>self</span><span>.i_limit)</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>d </span><span>=</span><span> (err </span><span>-</span><span> </span><span>self</span><span><span>.prev_err) </span><span>/</span><span> </span></span><span>self</span><span>.ts </span><span>if</span><span> </span><span>self</span><span>.primed </span><span>else</span><span> </span><span>0.0</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.prev_err </span><span>=</span><span> err</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.primed </span><span>=</span><span> </span></span><span>True</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>out </span><span>=</span><span> </span><span>self</span><span><span>.kp </span><span>*</span><span> err </span><span>+</span><span> </span></span><span>self</span><span><span>.ki </span><span>*</span><span> </span></span><span>self</span><span><span>.integral </span><span>+</span><span> </span></span><span>self</span><span><span>.kd </span><span>*</span><span> d</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span><span> </span><span>clamp</span><span>(out, </span><span>-</span></span><span>self</span><span>.out_limit, </span><span>self</span><span>.out_limit)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>积分限幅 <code>i_limit</code> 作用在积分量 <span><span>∫e dt\int e\,dt</span><span><span><span></span><span>∫</span><span></span><span>e</span><span></span><span>d</span><span>t</span></span></span></span> 上（不是 <span><span>Ki∫e dtK_i\int e\,dt</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∫</span><span></span><span>e</span><span></span><span>d</span><span>t</span></span></span></span> 上），所以实际的 I 项上限是 <code>ki * i_limit</code>。我在 <code>speed</code> 实验里取 <code>i_limit = 2</code>，对应 <span><span>10×2=2010\times 2 = 20</span><span><span><span></span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span></span><span>=</span><span></span></span><span><span></span><span>20</span></span></span></span> A，刚好是电流上限；云台取 0.05，对应 <span><span>20×0.05=120\times 0.05 = 1</span><span><span><span></span><span>20</span><span></span><span>×</span><span></span></span><span><span></span><span>0.05</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> N·m，刚好是力矩上限。这个限幅在这一篇的所有实验里没有触发过，它是第 5 篇的主角。</p></section><section><h3>驱动：数据、回路、实验表<a href="#驱动数据回路实验表"><span>#</span></a></h3><p>驱动 <code>python/ch01_p_i_d.py</code> 有两个子命令：<code>ref</code> 写数据并跑 Python 参考实现，<code>report</code> 对比四种语言、打印指标、断言结论并画图。九个实验用一张表描述：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># name: (plant, kp, ki, kd, r, load, i_limit)</span></div></div><div><div><div>2</div></div><div><span><span>CASES</span><span> </span><span>=</span><span> {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>"speed_p_k0.2"</span><span>: (</span><span>"speed"</span><span>, </span><span>0.2</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>10.0</span><span>, </span><span>1.0</span><span>, </span><span>2.0</span><span>),</span></div></div><div><div><div>4</div></div><div><span>    </span><span># ... speed_p_k0.5, k1, k2, k5, speed_pi</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"pos_p"</span><span>: (</span><span>"pos"</span><span>, </span><span>5.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.1</span><span>, </span><span>0.1</span><span>, </span><span>0.05</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"pos_pd"</span><span>: (</span><span>"pos"</span><span>, </span><span>5.0</span><span>, </span><span>0.0</span><span>, </span><span>0.3</span><span>, </span><span>0.1</span><span>, </span><span>0.1</span><span>, </span><span>0.05</span><span>),</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"pos_pid"</span><span>: (</span><span>"pos"</span><span>, </span><span>5.0</span><span>, </span><span>20.0</span><span>, </span><span>0.3</span><span>, </span><span>0.1</span><span>, </span><span>0.1</span><span>, </span><span>0.05</span><span>),</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>转速环的 P（五个 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）、一个 PI，云台的 P、PD、PID。每个实验 2 秒，设定值在 0.05 s 跳变，负载在 1.0 s 加上。数据文件里有 <code>t,r,d,n</code> 四列，<code>n</code> 是测量噪声，这一篇全是 0。</p><p>每个采样点 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 按下面的顺序执行，四种语言一模一样：</p><ol>
<li><span><span>tk=k tst_k = k\,t_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>k</span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
<li>测量 <span><span>yk=h(xk)+nky_k = h(x_k) + n_k</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>nkn_k</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 来自数据文件。</li>
<li>控制器输出 <span><span>uku_k</span><span><span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
<li>执行器限幅。</li>
<li>把指令推进延迟线（整数个采样周期，0 也允许，这一篇用 0）。</li>
<li>延迟后的指令和扰动 <span><span>dkd_k</span><span><span><span></span><span><span>d</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在 10 个 RK4 小步里保持不变。</li>
<li>记录这一行，用的是积分之前的 <span><span>xkx_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
</ol><p><code>run_case</code> 的循环体和这七步一一对应：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span><span>(</span><span>STEPS</span><span>):</span></span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>r, load </span><span>=</span><span> data[</span><span>"r"</span><span>][k], data[</span><span>"d"</span><span>][k]</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> plant.</span><span>output</span><span>(x) </span><span>+</span><span> data[</span><span>"n"</span><span>][k]</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>err </span><span>=</span><span> r </span><span>-</span><span> y</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>d_term </span><span>=</span><span> kd </span><span>*</span><span> (err </span><span>-</span><span> pid.prev_err) </span><span>/</span><span> </span><span>TS</span><span> </span><span>if</span><span> pid.primed </span><span>else</span><span> </span><span>0.0</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> pid.</span><span>update</span><span>(r, y)</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>u </span><span>=</span><span> </span><span>min</span><span>(</span><span>max</span><span><span>(u, </span><span>-</span><span>plant.u_max), plant.u_max)</span></span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>rows.</span><span>append</span><span>(</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>[data[</span><span>"t"</span><span><span>][k], r, y, u, kp </span><span>*</span><span> err, ki </span><span>*</span><span> pid.integral, d_term]</span></span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>u_in </span><span>=</span><span> delay.</span><span>push</span><span>(u)</span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span><span> kind </span><span>==</span><span> </span></span><span>"speed"</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>inputs </span><span>=</span><span> [u_in, load]</span></div></div><div><div><div>14</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>inputs </span><span>=</span><span> [u_in, load, </span><span>0.0</span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> </span><span>advance</span><span>(plant.deriv, x, inputs, </span><span>TS</span><span>, </span><span>SUBSTEPS</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意 <code>d_term</code> 和 <code>kp * err</code>、<code>ki * pid.integral</code> 是在驱动里另算一遍的，目的是把三项分别记进 CSV 画图，PID 类本身不暴露它们。<code>ref()</code> 把所有实验循环跑一遍，写 <code>out/ch01/&lt;case&gt;_py.csv</code>；<code>report</code> 里是对比、指标、断言和画图，在 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch01_p_i_d.py" target="_blank"><code>python/ch01_p_i_d.py</code></a> 里。</p></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>三个版本的结构和 Python 一样：<code>sim</code>、<code>plants</code>、<code>pid_basic</code> 三个模块加一个驱动。差别在这几处：</p><ul>
<li><strong>标量类型。</strong> C 用 <code>real.h</code> 里的 <code>pl_real</code>（默认 <code>double</code>，<code>-DPL_USE_FLOAT</code> 切 <code>float</code>），C++ 用模板参数 <code>T</code>（默认 <code>double</code>），Rust 用 <code>real.rs</code> 里的 <code>Real</code> 别名（<code>f32</code> 特性开关）。第 10 篇上 float32 时不用改控制器一行代码。</li>
<li><strong>内存。</strong> 没有任何 <code>malloc</code>、<code>new</code>、<code>Vec</code>（读写 CSV 的主机端代码除外）。状态是固定大小的数组：C 里 <code>PL_MAX_STATES = 8</code>，C++ 和 Rust 是 <code>std::array&lt;T, N&gt;</code> 和 <code>[Real; N]</code>，维数编译期确定。延迟线是结构体里一个 <code>PL_MAX_DELAY = 256</code> 个元素的数组，调用者拥有它。</li>
<li><strong>“把对象传给积分器”。</strong> 这是三种语言差别最大的地方：C 用函数指针加 <code>const void *ctx</code>，C++ 用模板参数 <code>F&amp;&amp;</code>（lambda 内联），Rust 用 <code>impl Fn</code> 闭包。C 的函数指针我没有去看汇编，大概率不会被内联，C++ 和 Rust 的多半会，但这里不是热路径，我没有测。</li>
<li><strong>校验。</strong> 构造可能失败的东西（延迟线容量越界）：C 返回 <code>-EINVAL</code>，C++ 返回 <code>std::optional</code>，Rust 返回 <code>Option</code>。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p>C 是 Linux 内核风格，Tab 缩进，<code>struct pl_pid_basic</code> 不 typedef。PID 更新就是 Python 版逐行翻译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pl_real </span><span>pl_pid_basic_update</span><span>(</span><span>struct</span><span> pl_pid_basic </span><span>*</span><span><span>pid</span><span>,</span><span> pl_real </span><span>ref</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span><span>          </span></span><span>pl_real </span><span>meas</span><span>)</span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>pl_real err </span><span>=</span><span> ref </span><span>-</span><span> meas;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>pl_real d </span><span>=</span><span> </span><span>0.0</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>pl_real out;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>  </span><span>pid</span><span>-&gt;</span><span>integral</span><span> </span><span>+=</span><span> err </span><span>*</span><span> </span><span>pid</span><span>-&gt;</span><span>ts</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>pid</span><span>-&gt;</span><span>integral</span><span> </span><span>=</span><span> </span><span>pl_clamp</span><span>(</span><span>pid</span><span>-&gt;</span><span>integral</span><span>, </span><span>-</span><span>pid</span><span>-&gt;</span><span>i_limit</span><span>, </span><span>pid</span><span>-&gt;</span><span>i_limit</span><span>);</span></div></div><div><div><div>10</div></div><div><span>  </span><span>if</span><span> (</span><span>pid</span><span>-&gt;</span><span>primed</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> (err </span><span>-</span><span> </span><span>pid</span><span>-&gt;</span><span>prev_err</span><span>) </span><span>/</span><span> </span><span>pid</span><span>-&gt;</span><span>ts</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>pid</span><span>-&gt;</span><span>prev_err</span><span> </span><span>=</span><span> err;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>pid</span><span>-&gt;</span><span>primed</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>out </span><span>=</span><span> </span><span>pid</span><span>-&gt;</span><span>kp</span><span> </span><span>*</span><span> err </span><span>+</span><span> </span><span>pid</span><span>-&gt;</span><span>ki</span><span> </span><span>*</span><span> </span><span>pid</span><span>-&gt;</span><span>integral</span><span> </span><span>+</span><span> </span><span>pid</span><span>-&gt;</span><span>kd</span><span> </span><span>*</span><span> d;</span></div></div><div><div><div>15</div></div><div><span>  </span><span>return</span><span> </span><span>pl_clamp</span><span>(out, </span><span>-</span><span>pid</span><span>-&gt;</span><span>out_limit</span><span>, </span><span>pid</span><span>-&gt;</span><span>out_limit</span><span>);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对象的 <code>deriv</code> 函数签名统一成 <code>(ctx, x, u, dx)</code>，<code>ctx</code> 指向参数结构体，结果写进调用者给的 <code>dx</code>，所以同一个 <code>pl_rk4_step</code> 和 <code>pl_advance</code> 能积分任何对象：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>pl_motor_speed_deriv</span><span>(</span><span>const</span><span> </span><span>void</span><span> </span><span>*</span><span><span>ctx</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>x</span><span>,</span><span> </span></span><span>const</span><span> pl_real </span><span>*</span><span><span>u</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>pl_real </span><span>*</span><span><span>dx</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span>  </span><span>const</span><span> </span><span>struct</span><span> pl_motor_speed </span><span>*</span><span>p </span><span>=</span><span> ctx;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>dx</span><span>[</span><span>0</span><span>] </span><span>=</span><span> (</span><span>p</span><span>-&gt;</span><span>kt</span><span> </span><span>*</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>p</span><span>-&gt;</span><span>b</span><span> </span><span>*</span><span> </span><span>x</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>u</span><span>[</span><span>1</span><span>]) </span><span>/</span><span> </span><span>p</span><span>-&gt;</span><span>j</span><span>;</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>RK4 一步，<code>k1</code> 到 <code>k4</code> 放在栈上的固定数组里，<code>n</code> 不能超过 <code>PL_MAX_STATES</code>（调用者的责任）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>pl_real </span><span>k1</span><span>[PL_MAX_STATES], </span><span>k2</span><span>[PL_MAX_STATES];</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>pl_real </span><span>k3</span><span>[PL_MAX_STATES], </span><span>k4</span><span>[PL_MAX_STATES];</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>pl_real </span><span>tmp</span><span>[PL_MAX_STATES];</span></div></div><div><div><div>4</div></div><div><span>  </span><span>int</span><span> i;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>f</span><span><span>(</span><span>ctx</span><span>,</span><span> x</span><span>,</span><span> u</span><span>,</span><span> k1</span><span>);</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>tmp</span><span>[i] </span><span>=</span><span> </span><span>x</span><span>[i] </span><span>+</span><span> </span><span>0.5</span><span> </span><span>*</span><span> h </span><span>*</span><span> </span><span>k1</span><span>[i];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>f</span><span><span>(</span><span>ctx</span><span>,</span><span> tmp</span><span>,</span><span> u</span><span>,</span><span> k2</span><span>);</span></span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>/* ... k3, k4 the same way, then x[i] += h / 6.0 * (...) */</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>回路本体和七步顺序一一对应，在 <a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch01.c" target="_blank"><code>c/apps/ch01.c</code></a> 里：转速环和云台是两个分支，各自调 <code>pl_advance</code>，维数用 <code>PL_MOTOR_SPEED_N</code> 和 <code>PL_GIMBAL_AXIS_N</code> 传进去。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p>C++ 是 Google 风格：2 空格缩进，<code>PascalCase</code> 函数，<code>snake_case_</code> 成员，纯头文件，全部在 <code>namespace pidlab</code> 里。<code>PidBasic</code> 是一个类，成员私有，状态用访问器读（<code>integral()</code>、<code>prev_err()</code>、<code>primed()</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>// u = kp e + ki * integral(e) + kd (e - e_prev) / ts, then clamped.</span></div></div><div><div><div>2</div></div><div><span>  </span><span>T</span><span> </span><span>Update</span><span>(</span><span>T</span><span><span> </span><span>ref</span><span>, </span></span><span>T</span><span><span> </span><span>meas</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>err</span><span> </span><span>=</span><span> </span><span>ref</span><span> </span><span>-</span><span> </span><span>meas</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>integral_</span><span> </span><span>+=</span><span> </span><span>err</span><span> </span><span>*</span><span> </span><span>ts_</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>integral_</span><span> </span><span>=</span><span> std::</span><span>clamp</span><span>(</span><span>integral_</span><span>, </span><span>-</span><span>i_limit_</span><span>, </span><span>i_limit_</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>d</span><span> </span><span>=</span><span> </span><span>primed_</span><span> </span><span>?</span><span> (</span><span>err</span><span> </span><span>-</span><span> </span><span>prev_err_</span><span>) </span><span>/</span><span> </span><span>ts_</span><span> </span><span>:</span><span> </span><span>T</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>prev_err_</span><span> </span><span>=</span><span> </span><span>err</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>primed_</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>out</span><span> </span><span>=</span><span> </span><span>kp_</span><span> </span><span>*</span><span> </span><span>err</span><span> </span><span>+</span><span> </span><span>ki_</span><span> </span><span>*</span><span> </span><span>integral_</span><span> </span><span>+</span><span> </span><span>kd_</span><span> </span><span>*</span><span> </span><span>d</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> std::</span><span>clamp</span><span>(</span><span>out</span><span>, </span><span>-</span><span>out_limit_</span><span>, </span><span>out_limit_</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>RK4 是函数模板，<code>N</code>、<code>M</code> 是编译期维数，<code>f</code> 是任何能被调用的东西，返回 <code>std::array&lt;T, N&gt;</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>, std::</span><span>size_t</span><span> </span><span>N</span><span>, std::</span><span>size_t</span><span> </span><span>M</span><span>, </span><span>class</span><span> </span><span>F</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>void</span><span> </span><span>Rk4Step</span><span>(</span><span>F</span><span>&amp;&amp;</span><span><span> </span><span>f</span><span>, std::</span></span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>x</span><span>, </span></span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>M</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>u</span><span>, </span></span><span>T</span><span><span> </span><span>h</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt; </span><span>tmp</span><span>;</span></div></div><div><div><div>4</div></div><div><span>  </span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt; </span><span>k1</span><span> </span><span>=</span><span> </span><span>f</span><span>(</span><span>x</span><span>, </span><span>u</span><span>);</span></div></div><div><div><div>5</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>++</span><span>i</span><span>) </span><span>tmp</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>x</span><span>[</span><span>i</span><span>] </span><span>+</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> </span><span>h</span><span> </span><span>*</span><span> </span><span>k1</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt; </span><span>k2</span><span> </span><span>=</span><span> </span><span>f</span><span>(</span><span>tmp</span><span>, </span><span>u</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>// ... k3, k4 the same way</span></div></div><div><div><div>8</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>x</span><span>[</span><span>i</span><span>] </span><span>+=</span><span> </span><span>h</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>6</span><span>) </span><span>*</span><span> (</span><span>k1</span><span>[</span><span>i</span><span>] </span><span>+</span><span> </span><span>T</span><span>(</span><span>2</span><span>) </span><span>*</span><span> </span><span>k2</span><span>[</span><span>i</span><span>] </span><span>+</span><span> </span><span>T</span><span>(</span><span>2</span><span>) </span><span>*</span><span> </span><span>k3</span><span>[</span><span>i</span><span>] </span><span>+</span><span> </span><span>k4</span><span>[</span><span>i</span><span>]);</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>和 C 的区别：对象是带 <code>Deriv</code> 和 <code>Output</code> 方法的小 struct，维数写成 <code>static constexpr std::size_t kN</code>、<code>kM</code>；驱动写成 <code>RunLoop&lt;Plant&gt;</code> 模板，用 <code>Plant::kN</code>、<code>Plant::kM</code> 当数组尺寸，转速环和云台共用同一份回路代码，C 版里这里是两个分支。<code>DelayLine::Create</code> 返回 <code>std::optional</code>，因为本库不用异常。驱动：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch01.cc" target="_blank"><code>cpp/apps/ch01.cc</code></a>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><p>Rust 是 edition 2024，<code>rustfmt</code> 默认格式，库是 <code>no_std</code> 兼容的（<code>io.rs</code> 在 <code>std</code> 特性之后）。PID 更新：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// `u = kp e + ki * integral(e) + kd (e - e_prev) / ts`, then clamped.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>r</span><span>: </span><span>Real</span><span>, </span><span>y</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>err</span><span><span> </span><span>=</span><span> </span></span><span>r</span><span> - </span><span>y</span><span>;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.integral </span><span>+=</span><span> </span></span><span>err</span><span> * </span><span>self</span><span>.ts;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.integral </span><span>=</span><span> </span></span><span>self</span><span>.integral.</span><span>clamp</span><span>(-</span><span>self</span><span>.i_limit, </span><span>self</span><span>.i_limit);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>d</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>self</span><span>.primed {</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>(</span><span>err</span><span> - </span><span>self</span><span>.prev_err) / </span><span>self</span><span>.ts</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>0.0</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span><span>.prev_err </span><span>=</span><span> </span></span><span>err</span><span>;</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span><span>.primed </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>out</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.kp * </span><span>err</span><span> + </span><span>self</span><span>.ki * </span><span>self</span><span>.integral + </span><span>self</span><span>.kd * </span><span>d</span><span>;</span></div></div><div><div><div>14</div></div><div><span>        </span><span>out</span><span>.</span><span>clamp</span><span>(-</span><span>self</span><span>.out_limit, </span><span>self</span><span>.out_limit)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>RK4 用 const generics 把维数放进类型里，积分器签名里的 <code>impl Fn</code> 接闭包：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>rk4_step</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>2</div></div><div><span>    </span><span>f</span><span>: </span><span>impl</span><span> </span><span>Fn</span><span>(&amp;[</span><span>Real</span><span>; </span><span>N</span><span>], &amp;[</span><span>Real</span><span>; </span><span>M</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>3</div></div><div><span>    </span><span>x</span><span>: &amp;</span><span>mut</span><span> [</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>4</div></div><div><span>    </span><span>u</span><span>: &amp;[</span><span>Real</span><span>; </span><span>M</span><span>],</span></div></div><div><div><div>5</div></div><div><span>    </span><span>h</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>6</div></div><div><span>) {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>k1</span><span><span> </span><span>=</span><span> </span></span><span>f</span><span>(</span><span>x</span><span>, </span><span>u</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>tmp</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>tmp</span><span>[</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>x</span><span>[</span><span>i</span><span>] + </span><span>0.5</span><span> * </span><span>h</span><span> * </span><span>k1</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>k2</span><span><span> </span><span>=</span><span> </span></span><span>f</span><span>(&amp;</span><span>tmp</span><span>, </span><span>u</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>// ... k3, k4 the same way</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>和 C、C++ 的区别：数组越界在 Rust 里是 panic 而不是未定义行为，<code>clamp</code> 是 <code>f64</code> 自带的方法，不用自己写。驱动里的 <code>run_loop&lt;const N, const M&gt;</code> 和 C++ 的 <code>RunLoop&lt;Plant&gt;</code> 是同一个想法，对象的 <code>deriv</code> 和 <code>output</code> 以闭包的形式传进去（<code>|x, u| p.deriv(x, u)</code>）。驱动：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch01.rs" target="_blank"><code>rust/src/bin/ch01.rs</code></a>。</p><p>编译参数：C 是 <code>gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Werror</code>，C++ 是 <code>g++ -std=c++20</code> 加同样的警告，Rust 是 <code>cargo build --release</code> 加 <code>cargo clippy -- -D warnings</code>。代码风格我用工具检查，不靠自觉：C 用内核的 <code>checkpatch.pl --no-tree</code>（0 errors，0 warnings），C++ 用 <code>clang-format --style=Google</code>（21.1.8），Python 用 <code>ruff format --line-length 79</code> 和 <code>ruff check</code>（0.15.14），Rust 用 <code>cargo fmt --check</code>。<code>checkpatch.pl</code> 我加了 <code>--ignore SPDX_LICENSE_TAG,NEW_TYPEDEFS</code>：前者是它对许可证标识的检查，这个库的许可证是 MIT OR Apache-2.0，我没有去迁就内核的写法，后者因为整个库里我只保留了 <code>pl_real</code> 这一个标量 typedef（做 float/double 开关），结构体一概不 typedef。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make ch01</code> 一次跑完。先看对比：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch01</span></div></div><div><div><div>2</div></div><div><span>ch01: C wrote 9 cases to out/ch01</span></div></div><div><div><div>3</div></div><div><span>ch01: C++ wrote 9 cases to out/ch01</span></div></div><div><div><div>4</div></div><div><span>ch01: Rust wrote 9 cases to out/ch01</span></div></div><div><div><div>5</div></div><div><span>ok   pos_p_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   pos_p_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   pos_p_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>ok   pos_pd_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>9</div></div><div><span>ok   pos_pd_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   pos_pd_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>... (trimmed: 18 more ok lines)</span></div></div><div><div><div>12</div></div><div><span>ok   speed_pi_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>13</div></div><div><span>ok   speed_pi_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>14</div></div><div><span>ok   speed_pi_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>27 个文件（9 个实验乘以 C、C++、Rust），最大差值全是 <code>0.000e+00</code>，逐位一致。我没有指望这一点：这里只有加减乘除，IEEE 754 规定它们必须正确舍入，我又把 RK4 每一步的运算顺序在四种语言里写成了一样，而且 x86-64 上 gcc 默认不生成 FMA，所以才一个比特都不差。我猜换成 <code>-march=native</code> 或者换一种 CPU 就未必还能做到，这是 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 容差存在的理由。</p><section><h3>P：Kp 越大误差越小，但永远不为零<a href="#pkp-越大误差越小但永远不为零"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch01-speed-p-sweep.svg" alt="转速环 P 控制：五个 Kp 的转速与电流指令，Kp 越大静差越小，加负载后所有曲线都被拉下来一截" /><figcaption>转速环 P 控制：五个 Kp 的转速与电流指令，Kp 越大静差越小，加负载后所有曲线都被拉下来一截</figcaption></figure><p></p><p>五条曲线是 <span><span>Kp=0.2,0.5,1,2,5K_p = 0.2, 0.5, 1, 2, 5</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span><span>,</span><span></span><span>0.5</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>5</span></span></span></span>。上图是转速，下图是电流指令，竖虚线是 1.0 s 加负载的时刻。先看没有负载的阶段：<span><span>Kp=0.2K_p = 0.2</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> 的轮子在 0.99 s 才爬到 7.64 rad/s，还没到；<span><span>Kp=5K_p = 5</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 在 0.99 s 是 9.934 rad/s，很快但还是差一点。再看负载：1 N·m 的负载加上去，每条曲线都掉一截然后停在一个新的位置，<span><span>Kp=5K_p = 5</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 停在 9.27，<span><span>Kp=0.2K_p = 0.2</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> 甚至被拉到了 −4.42 rad/s（负值：负载比它能抵抗的更大，轮子被倒着拖）。下图里 <span><span>Kp=5K_p = 5</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>5</span></span></span></span> 起跳时电流在 20 A 的上限上停了 31 拍（31 ms），是一段平顶；<span><span>Kp=2K_p = 2</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 刚好碰到上限，只有一个尖峰。</p><p>误差公式对不对，我让 <code>report</code> 去验证：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch01</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>P-only speed loop: steady error, formula vs simulation</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>Kp   phase  e_ss formula   e @ end  tau_cl (s)  prediction  |pred-sim|</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>0.2 no load        1.4286    2.3627      0.4286      2.3626    1.08e-04</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>0.2    load       15.7143   14.4193      0.4286     14.4193    1.35e-06</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>0.5 no load        0.6250    0.6837      0.1875      0.6841    4.34e-04</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>0.5    load        6.8750    6.8453      0.1875      6.8451    2.39e-04</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>1 no load        0.3226    0.3231      0.0968      0.3231    2.02e-05</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>1    load        3.5484    3.5483      0.0968      3.5483    4.28e-06</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>2 no load        0.1639    0.1639      0.0492      0.1639    3.37e-08  (sat.)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>2    load        1.8033    1.8033      0.0492      1.8033    4.10e-10</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>5 no load        0.0662    0.0662      0.0199      0.0662    1.76e-13  (sat.)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>5    load        0.7285    0.7285      0.0199      0.7285    1.63e-13</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>表里 <code>e_ss formula</code> 是 <span><span>(br+τd)/(b+KtKp)(br + \tau_d)/(b + K_tK_p)</span><span><span><span></span><span>(</span><span>b</span><span>r</span><span></span><span>+</span><span></span></span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/</span><span>(</span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，<code>e @ end</code> 是仿真在该阶段末尾的 <span><span>r−yr - y</span><span><span><span></span><span>r</span><span></span><span>−</span><span></span></span><span><span></span><span>y</span></span></span></span>。<span><span>Kp≥1K_p \ge 1</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>1</span></span></span></span> 的行两者已经对上；<span><span>Kp=0.2K_p = 0.2</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> 和 <span><span>0.50.5</span><span><span><span></span><span>0.5</span></span></span></span> 的无负载阶段对不上，原因不是公式错，是 1 秒内还没到稳态（<span><span>Kp=0.2K_p = 0.2</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> 的时间常数是 0.4286 s，到 0.99 s 才过了 2.2 个时间常数）。所以我让断言检查的是“带瞬态的预测”：<span><span>e(t)=ess+(e0−ess) e−t/τcle(t) = e_{ss} + (e_0 - e_{ss})\,e^{-t/\tau_{cl}}</span><span><span><span></span><span>e</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>e</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>−</span><span>t</span><span>/</span><span><span>τ</span><span><span><span><span><span><span></span><span><span><span>c</span><span>l</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>，对每个起步段没有饱和的阶段，要求它和仿真的差小于 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rad/s；起步段饱和的阶段（<span><span>Kp=2,5K_p = 2, 5</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>,</span><span></span><span>5</span></span></span></span> 的无负载阶段，瞬态不再是一阶的）改成直接比稳态公式，它们已经收敛，差都小于 <span><span>10−710^{-7}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>（表里标 <code>(sat.)</code> 的就是这两行，表示该阶段起步时电流触顶）。</p><p><strong>我踩的第一个坑。</strong> 我第一次写断言时直接用了 <span><span>t=tend−tstartt = t_{end} - t_{start}</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>e</span><span>n</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span><span>s</span><span>t</span><span>a</span><span>r</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>Kp=0.2K_p = 0.2</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.2</span></span></span></span> 无负载那一行的偏差是 <span><span>2.07×10−32.07\times 10^{-3}</span><span><span><span></span><span>2.07</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rad/s，超过了我给自己定的 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，断言挂了。查下来是上一节说的保持器：控制器的输出相对于它看到的测量晚了大约一个采样周期，所以预测的指数衰减要多走一个 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。把 <span><span>tt</span><span><span><span></span><span>t</span></span></span></span> 加上 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 之后，全表最大偏差降到 <span><span>4.34×10−44.34\times 10^{-4}</span><span><span><span></span><span>4.34</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>（<span><span>Kp=0.5K_p = 0.5</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span></span></span></span> 无负载阶段）。理论上这个延迟是一个半周期（<span><span>ts/2t_s/2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/2</span></span></span></span> 加一拍），我只试了加一个 <span><span>tst_s</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，够过断言就停了，没有再细调。这是我第一次在这个系列里直接看到采样的代价，量很小，但不是零。</p></section><section><h3>I：积分把静差拉回来，代价是超调<a href="#i积分把静差拉回来代价是超调"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch01-speed-pi.svg" alt="转速环 P 与 PI 对比：PI 超调但最终回到设定值，P 留下 3.5 rad/s 静差" /><figcaption>转速环 P 与 PI 对比：PI 超调但最终回到设定值，P 留下 3.5 rad/s 静差</figcaption></figure><p></p><p><span><span>Kp=1K_p = 1</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的 P 控制在负载之后留下 3.548 rad/s 的稳态误差（公式预测 3.5484，仿真 3.5483）。同样的 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 加上 <span><span>Ki=10K_i = 10</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span>，到 2.0 s 时转速是 9.9835 rad/s，误差 0.0165 rad/s，缩小到原来的 0.46%（<span><span>0.0165/3.5480.0165/3.548</span><span><span><span></span><span>0.0165/3.548</span></span></span></span>）。代价写在表里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch01</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>Speed loop (r = 10 rad/s at 0.05 s, 1 N*m load at 1.0 s)</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>case  y @0.99s  overshoot %  peak u (A)  dip after load   y @2.0s</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>speed_p_k0.2    7.6351         0.00        2.00         14.4193   -4.4193</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>speed_p_k0.5    9.3160         0.00        5.00          6.8453    3.1547</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>speed_p_k1    9.6769         0.00       10.00          3.5483    6.4517</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>speed_p_k2    9.8361         0.00       20.00          1.8033    8.1967</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>speed_p_k5    9.9338         0.00       20.00          0.7285    9.2715</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>speed_pi   10.0599        28.02       10.10          1.7523    9.9835</span></div></div><div><div><div>11</div></div><div><span>speed_pi: settles (2%) 0.742 s after the step, final error 1.65e-02 rad/s</span></div></div><div><div><div>12</div></div><div><span>speed_pi after load: min y 8.248 rad/s at 1.122 s</span></div></div><div><div><div>13</div></div><div><span>speed_pi final I term 3.670 A (needed (tau_d + b w)/Kt = 3.667 A)</span></div></div><div><div><div>14</div></div><div><span>PI (Kp=1, Ki=10): wn = 10.00 rad/s, zeta = 0.517, 2nd-order 15.0 %</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>PI 的超调是 28.02%，不是我前面用二阶公式预测的 15.0%。多出来的 13 个百分点大概率是 PI 控制器本身的零点（闭环传递函数分子里的 <span><span>Kps+KiK_p s + K_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，在 <span><span>−Ki/Kp=−10-K_i/K_p = -10</span><span><span><span></span><span>−</span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>10</span></span></span></span> rad/s 处，刚好在 <span><span>ωn\omega_n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 附近），它把一部分输入的“速度”也送给了输出，让响应更激进。我说“大概率”是因为这一篇我还没有传递函数这个工具，没法在这里证明，第 2 篇会专门拿 I-P 结构做对照实验。</p><p>负载加上去之后 PI 的转速最低掉到 8.248 rad/s（1.122 s），然后爬回来；电流的 I 项最后停在 3.670 A，正好是平衡所需的 <span><span>(τd+bω)/Kt=(1+0.1)/0.3=3.667(\tau_d + b\omega)/K_t = (1 + 0.1)/0.3 = 3.667</span><span><span><span></span><span>(</span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>bω</span><span>)</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>+</span><span></span></span><span><span></span><span>0.1</span><span>)</span><span>/0.3</span><span></span><span>=</span><span></span></span><span><span></span><span>3.667</span></span></span></span> A（上面输出的倒数第二行）。这就是积分器的“水位”：它自己记住了负载。</p></section><section><h3>云台：P 在晃，D 来阻尼，I 去静差<a href="#云台p-在晃d-来阻尼i-去静差"><span>#</span></a></h3><p></p><figure><img src="/assets/images/pid-control/ch01-position-p-pd-pid.svg" alt="云台 P、PD、PID 对比：P 持续振荡，PD 快速收敛但留静差，PID 收敛且无静差" /><figcaption>云台 P、PD、PID 对比：P 持续振荡，PD 快速收敛但留静差，PID 收敛且无静差</figcaption></figure><p></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch01</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>Gimbal loop (r = 0.1 rad at 0.05 s, 0.1 N*m load at 1.0 s)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>case  overshoot %  settle 2% (s)    e @1.0s  mean e 1.6-2s   max|u|</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>pos_p        90.76          0.949    -0.0465         0.0211    0.500</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>pos_pd        15.61          0.499     0.0001         0.0200    1.000</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>pos_pid        45.22          0.797     0.0003         0.0007    1.000</span></div></div><div><div><div>8</div></div><div><span>pos_pid: y first reaches r at 0.176 s, I term peaks at 0.147 N*m at 0.175 s</span></div></div><div><div><div>9</div></div><div><span>pos_pid: I term ends at 0.0993 N*m, max D term 30.0 N*m</span></div></div><div><div><div>10</div></div><div><span>Kd r / ts = 30.0 N*m, u at that sample 1.0 N*m</span></div></div><div><div><div>11</div></div><div><span>damping ratio: P only 0.035, PD 0.509; wn = 15.81 rad/s</span></div></div><div><div><div>12</div></div><div><span>plain 2nd-order overshoot: P only 89.6 %, PD 15.6 %</span></div></div><div><div><div>13</div></div><div><span>static error under load: tau_d/Kp = 0.0200 rad</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>P：超调 90.76%，到 1.0 s 加负载的时候还没停下（误差 −0.0465 rad，仍在晃），2 秒末也在晃。阻尼比 0.035 的二阶系统，公式预测超调 89.6%，仿真 90.76%，差的 1.2 个百分点我归在采样的头上，没有深究。因为 P 没有稳定下来，我把“负载后的静差”取成最后 0.4 s 的平均误差（正好是一个振荡周期 <span><span>2π/ωn≈0.3972\pi/\omega_n \approx 0.397</span><span><span><span></span><span>2</span><span>π</span><span>/</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>0.397</span></span></span></span> s）：0.0211 rad，和 <span><span>τd/Kp=0.02\tau_d/K_p = 0.02</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.02</span></span></span></span> 一致。</p><p>PD：超调降到 15.61%，公式预测 15.6%；从阶跃起 0.499 s 之后误差一直在 2% 以内，到 1.0 s 误差 0.0001 rad。负载加上之后静差落在 0.0200 rad，和 <span><span>τd/Kp\tau_d/K_p</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 精确对上。D 没有消除静差，和推导一致。</p><p>PID：负载之后的静差是 0.0007 rad，I 把它清掉了。但它的超调是 45.22%，比 PD 的 15.61% 还大很多。这是我没预料到的：我原先只想到“I 会加一点超调”。输出里有一条线索：云台在 0.176 s 第一次碰到设定值，而 I 项恰好在 0.175 s 到达最高点 0.147 N·m。整个上升过程里误差一直为正，积分器一直在往上攒，过了设定值之后误差才变负，积分器才开始往回放，这份攒多了的 I 项推着云台冲得更远。这是积分饱和的近亲（没有触到限幅，但机制一样），第 5 篇会处理。</p><p><strong>三项各自出了多少力。</strong></p><p></p><figure><img src="/assets/images/pid-control/ch01-pid-terms.svg" alt="云台 PID 里 P、I、D 三项和总输出随时间的变化，D 项在设定值跳变那一拍有 30 N·m 的尖峰" /><figcaption>云台 PID 里 P、I、D 三项和总输出随时间的变化，D 项在设定值跳变那一拍有 30 N·m 的尖峰</figcaption></figure><p></p><p>最上面是 P，设定值跳变那一拍是 <span><span>5×0.1=0.55\times 0.1 = 0.5</span><span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>0.1</span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span></span></span></span> N·m，之后随误差衰减，负载期间只剩一点点。第二张是 I，起步时被推到 0.147 N·m，之后降下来，负载加上去后爬到 0.0993 N·m（2.0 s 末），接近 <span><span>τd=0.1\tau_d = 0.1</span><span><span><span></span><span><span>τ</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span> N·m，积分器学到了负载的大小。</p><p>第三张是 D，我最想让你看一眼的一张：D 项在 0.05 s 有一个 <strong>30 N·m</strong> 的尖峰，对应输出里 <code>max D term 30.0</code>，<span><span>Kd⋅r/ts=0.3×0.1/0.001=30K_d\cdot r/t_s = 0.3\times 0.1/0.001 = 30</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>r</span><span>/</span><span><span>t</span><span><span><span><span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.3</span><span></span><span>×</span><span></span></span><span><span></span><span>0.1/0.001</span><span></span><span>=</span><span></span></span><span><span></span><span>30</span></span></span></span>。因为设定值是阶跃，误差在一个采样周期内跳了 0.1 rad，后向差分把它除以 1 ms。控制器的输出限幅是 1 N·m，所以实际送给电机的只有 1 N·m 的一拍（总输出图上最前面那一根针），尖峰被限幅削掉了，但它还是白白占用了一拍的力矩。这是“微分冲击”（derivative kick）。这一篇我故意不处理它，让它留在数据里；第 5 篇会用 <span><span>c=0c = 0</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>（微分只作用在测量上）来消掉它。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在仓库根目录、<code>nix develop</code> 里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-s</span><span> </span><span>ch01</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的输出见上面的“结果”一节：27 行 <code>ok</code>，最大差值全是 <code>0.000e+00</code>，然后是三张指标表，<code>report</code> 里的断言（<span><span>esse_{ss}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>ss</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 公式与仿真相差小于 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rad/s）全部通过，最后画出 4 张图。三种原生语言各写 9 个 CSV，加上 Python 的 9 个，共 36 个文件在 <code>out/ch01/</code>。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇新增的文件，Python 侧（命令都在仓库根目录运行）：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/sim.py" target="_blank"><code>python/pidlab/sim.py</code></a></td><td>随 <code>make -s ch01</code> 运行</td><td>RK4 和 <code>DelayLine</code>，是所有语言的参考</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plants.py" target="_blank"><code>python/pidlab/plants.py</code></a></td><td>同上</td><td><code>MotorSpeed</code>、<code>GimbalAxis</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/pid_basic.py" target="_blank"><code>python/pidlab/pid_basic.py</code></a></td><td>同上</td><td>位置式 PID</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/ch01_p_i_d.py" target="_blank"><code>python/ch01_p_i_d.py</code></a></td><td><code>python3 python/ch01_p_i_d.py ref</code> 然后 <code>report</code></td><td><code>ref</code> 生成 9 个数据文件和 9 个 <code>_py.csv</code>；<code>report</code> 对比、打印三张表、画 4 张图</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/compare.py" target="_blank"><code>python/compare.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/io.py" target="_blank"><code>python/pidlab/io.py</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/python/pidlab/plotting.py" target="_blank"><code>python/pidlab/plotting.py</code></a></td><td>同上</td><td>逐列对比（容差 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>）、CSV 读写、画图辅助，后面每一篇都用</td></tr></tbody></table><p>C、C++、Rust 侧，<code>make -s ch01</code> 会依次编译并运行三个驱动，各写 9 个 CSV 到 <code>out/ch01/</code>：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/real.h" target="_blank"><code>c/include/pl/real.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/csv.h" target="_blank"><code>c/include/pl/csv.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/csv.c" target="_blank"><code>c/src/csv.c</code></a></td><td>随 <code>make -s ch01</code> 编译</td><td><code>pl_real</code> 标量开关；C 里唯一用堆的 CSV 读写</td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/sim.h" target="_blank"><code>c/include/pl/sim.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/sim.c" target="_blank"><code>c/src/sim.c</code></a></td><td>同上</td><td><code>pl_rk4_step</code>、<code>pl_advance</code>、<code>struct pl_delay_line</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/plants.h" target="_blank"><code>c/include/pl/plants.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/plants.c" target="_blank"><code>c/src/plants.c</code></a></td><td>同上</td><td><code>struct pl_motor_speed</code>、<code>struct pl_gimbal_axis</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/include/pl/pid_basic.h" target="_blank"><code>c/include/pl/pid_basic.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/src/pid_basic.c" target="_blank"><code>c/src/pid_basic.c</code></a></td><td>同上</td><td><code>struct pl_pid_basic</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/apps/ch01.c" target="_blank"><code>c/apps/ch01.c</code></a></td><td>同上</td><td><code>ch01: C wrote 9 cases to out/ch01</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/sim.h" target="_blank"><code>cpp/include/pidlab/sim.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/plants.h" target="_blank"><code>plants.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/pid_basic.h" target="_blank"><code>pid_basic.h</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/include/pidlab/csv.h" target="_blank"><code>csv.h</code></a></td><td>同上</td><td><code>Rk4Step</code>、<code>DelayLine&lt;T&gt;</code>、<code>MotorSpeed&lt;T&gt;</code>、<code>GimbalAxis&lt;T&gt;</code>、<code>PidBasic&lt;T&gt;</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/apps/ch01.cc" target="_blank"><code>cpp/apps/ch01.cc</code></a></td><td>同上</td><td><code>ch01: C++ wrote 9 cases to out/ch01</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/real.rs" target="_blank"><code>rust/src/real.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/sim.rs" target="_blank"><code>sim.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/plants.rs" target="_blank"><code>plants.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/pid_basic.rs" target="_blank"><code>pid_basic.rs</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/lib.rs" target="_blank"><code>lib.rs</code></a></td><td>同上</td><td><code>Real</code>、<code>rk4_step</code>、<code>DelayLine</code>、<code>MotorSpeed</code>、<code>GimbalAxis</code>、<code>PidBasic</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/rust/src/bin/ch01.rs" target="_blank"><code>rust/src/bin/ch01.rs</code></a></td><td>同上</td><td><code>ch01: Rust wrote 9 cases to out/ch01</code></td></tr></tbody></table><p>构建和辅助文件：<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/Makefile" target="_blank"><code>Makefile</code></a>（<code>make ch01</code> 的规则）、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/c/Makefile" target="_blank"><code>c/Makefile</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/cpp/Makefile" target="_blank"><code>cpp/Makefile</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/flake.nix" target="_blank"><code>flake.nix</code></a>、<a href="https://github.com/bfmhno3/pid-lab/blob/tutorial-v1/scripts/sync-figures.sh" target="_blank"><code>scripts/sync-figures.sh</code></a>（把 <code>figures/ch01/</code> 里的 4 张 SVG 复制到本站）。</p></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>这一篇我把 P、I、D 分别拆开看了，每一项都对应一个具体的物理效应：P 是弹簧，D 是阻尼，I 是记忆。但我是靠“看曲线加猜”得到这些结论的，有两处没有说清楚：PI 的超调为什么是 28% 而不是 15%；PID 的 45% 超调怎么压下来。第 2 篇 <a href="/posts/pid-control-02-transfer-functions/">传递函数、极点与根轨迹</a> 引入拉普拉斯变换，把闭环的极点和零点直接写出来，用极点配置把 <span><span>KpK_p</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>KiK_i</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从“调”变成“算”，再用 Routh 判据和根轨迹看增益加到多大系统会不稳定。那一篇还会给 <code>MotorSpeed</code> 加上电流环的滞后，对象的阶数就比这一篇多一阶了。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-08-porting/</id>
      <title type="text">移植之前：float32、定长内存与每步耗时</title>
      <published>2026-10-14T07:00:00.000Z</published>
      <updated>2026-10-14T07:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-08-porting/"/>
      <summary type="text">卡尔曼滤波系列第 8 篇，也是最后一篇：在上硬件之前，纯软件地量清楚 float32 的精度代价、顺序标量更新和 UD 分解、每个滤波器的内存占用、没有堆分配的证据、Cortex-M4F 上的代码体积和每步耗时，附一份移植清单和一个硬铁偏置的演示。</summary>
      <content type="html"><![CDATA[<p>七个滤波器（1D、KF、Mahony、EKF、ESKF、UKF，再加这一篇的 UD 版 ESKF），每个四种语言，跑过了同一份数据。这一篇不再写新的滤波器，只回答一个问题：如果我现在把它们搬到一块 Cortex-M4F 的板子上，会碰到什么。答案里最出乎我意料的是这个：一个我以为”换成 float32 就完事”的例子，在 float32 下只用了两步就把协方差算成了负数，而第 6 篇 C 版里一个不起眼的 <code>fmod</code> 调用，在 Cortex-M4F 上会悄悄拖进软件实现的 double 运算。</p>
<p>这一篇所有的数字都来自这台机器和这次 <code>make ch08</code>，没有一个来自真实的 MCU。凡是对 MCU 的推测我都会说明是推测。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch08</code> 通过（它先构建 double 和 float 两套，再顺序跑完所有检查和计时，最好在没有别的负载的机器上跑，大概 3 分钟），<code>out/ch08/</code> 下的 float32 输出、<code>sizes_*.txt</code>、<code>timing_*.csv</code>，四张图；新模块 <code>eskf_seq</code>（顺序标量更新）、<code>ud</code>（UD 分解）、<code>eskf_ud</code>（UD 形式的 ESKF），各语言各一份；<code>scripts/ch08_checks.sh</code>（静态检查）。
前置：<a href="/posts/kalman-filter-07-ukf/">第 7 篇</a>，尤其是第 3 篇关于 Joseph 形式和对称性的讨论。</p></div></div>
<section><h2>要量什么<a href="#要量什么"><span>#</span></a></h2><ul>
<li><strong>float32 的精度代价</strong>：KF、ESKF、UKF 在 float32 下和 double 的 Python 参考差多少。</li>
<li><strong>协方差的数值健康</strong>：顺序标量更新、UD 分解，和第 3 篇留下的那个病态例子。</li>
<li><strong>内存</strong>：每个滤波器结构体多少字节。</li>
<li><strong>有没有堆</strong>：C、C++、Rust 各自怎么证明。</li>
<li><strong>代码体积</strong>：C 滤波器编译到 Cortex-M4F 上多大。</li>
<li><strong>每步耗时</strong>：这台机器上七个滤波器的中位数，以及 numpy 慢多少。</li>
<li><strong>移植清单</strong>，和一个硬铁偏置的演示。</li>
</ul></section>
<section><h2>float32<a href="#float32"><span>#</span></a></h2><p>在第 1 篇我就把标量类型做成可切换的，所以 float32 构建不改任何滤波器代码：C 用 <code>-DKL_USE_FLOAT</code>（<code>kl_real</code> 变成 <code>float</code>，<code>KL_SQRT</code> 变成 <code>sqrtf</code>），C++ 的模板参数 <code>T</code> 取 <code>float</code>，Rust 打开 <code>f32</code> 特性（<code>Real = f32</code>，数学函数走 <code>libm</code> 的 <code>sqrtf</code> 等）。C 和 Rust 要分别构建，所以 <code>make ch08</code> 里有 <code>build/c32</code> 和 <code>rust/target32</code> 两个目录；C++ 一个二进制里两种精度都有。</p><p><code>ch08 run</code> 在两种精度下处理种子 1 的数据，输出 <code>out/ch08/&lt;case&gt;_&lt;f64|f32&gt;_&lt;lang&gt;.csv</code>。double 构建先和 Python 参考对一遍（所有 9 个 case，最大差异小于 <span><span>10−1310^{-13}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span>，病态例子除外，和第 3 篇一样是 <span><span>7.5×10−97.5\times10^{-9}</span><span><span><span></span><span>7.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>）。float32 的输出和 float64 的 Python 参考对比：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>float32 vs the float64 Python reference (seed 1)</span></div></div><div><div><div>2</div></div><div><span>case   lang   max attitude diff (deg)  RMS (deg)  max |db| (rad/s)   min pmin</span></div></div><div><div><div>3</div></div><div><span>kf     C                    2.283e-05  5.346e-06         2.312e-07        nan</span></div></div><div><div><div>4</div></div><div><span>kf     C++                  2.283e-05  5.346e-06         2.312e-07        nan</span></div></div><div><div><div>5</div></div><div><span>kf     Rust                 2.283e-05  5.365e-06         1.683e-07        nan</span></div></div><div><div><div>6</div></div><div><span>eskf   C                    2.102e-04  7.632e-05         8.476e-07   6.68e-08</span></div></div><div><div><div>7</div></div><div><span>eskf   C++                  1.707e-04  7.718e-05         7.436e-07   6.68e-08</span></div></div><div><div><div>8</div></div><div><span>eskf   Rust                 1.788e-04  7.687e-05         6.021e-07   6.68e-08</span></div></div><div><div><div>9</div></div><div><span>ukf    C                    7.688e-03  9.627e-04         2.776e-05   6.68e-08</span></div></div><div><div><div>10</div></div><div><span>ukf    C++                  5.747e-03  7.581e-04         1.828e-05   6.68e-08</span></div></div><div><div><div>11</div></div><div><span>ukf    Rust                 6.319e-03  1.048e-03         1.400e-05   6.68e-08</span></div></div><div><div><div>12</div></div><div><span>float32 ESKF, largest attitude difference between two native languages: 7.55e-05 deg</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>KF（第 2 篇的角度加零偏）</strong>：角度最大偏差 <span><span>2.3×10−52.3\times10^{-5}</span><span><span><span></span><span>2.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span> 度。</li>
<li><strong>ESKF</strong>：姿态最大偏差 <span><span>1.7×10−41.7\times10^{-4}</span><span><span><span></span><span>1.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>2.1×10−42.1\times10^{-4}</span><span><span><span></span><span>2.1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> 度，三个语言各不相同（语言之间两两最大偏差 <span><span>7.6×10−57.6\times10^{-5}</span><span><span><span></span><span>7.6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span> 度，这是编译器对浮点表达式的不同合并方式，float32 没有逐位复现的保证）。</li>
<li><strong>UKF</strong>：最大偏差 <span><span>5.7×10−35.7\times10^{-3}</span><span><span><span></span><span>5.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>7.7×10−37.7\times10^{-3}</span><span><span><span></span><span>7.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> 度，是 ESKF 的 30 倍左右。我猜差别来自 sigma 点：13 个点加权求和以及一个额外的 Cholesky 分解，每一步多积累的舍入多得多，但我没有做实验去验证这个猜测。</li>
<li>三个滤波器的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的最小主元在整个运行中都是正的（整个运行里的最小值是 <span><span>6.68×10−86.68\times10^{-8}</span><span><span><span></span><span>6.68</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>，各版本一样）。</li>
</ul><p></p><figure><img src="/assets/images/kalman-filter/ch08-float32-error.svg" alt="ESKF 和 UKF 的 float32 输出（C）与 float64 参考的姿态差，随时间" /><figcaption>ESKF 和 UKF 的 float32 输出（C）与 float64 参考的姿态差，随时间</figcaption></figure><p></p><p>所以在这份数据上，float32 的代价是 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>10−210^{-2}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>2</span></span></span></span></span></span></span></span></span></span></span></span> 度，比传感器的精度（加速度计噪声对应约 0.3 度的瞬时角度误差）小两到四个数量级。它不是问题。</p></section>
<section><h2>顺序标量更新<a href="#顺序标量更新"><span>#</span></a></h2><p>加速度计的测量噪声是对角的，<span><span>R=rI3R = r I_3</span><span><span><span></span><span>R</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。三个分量相互独立，所以一次三维的更新可以换成三次一维的更新：每次一个分量，<span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 是个标量，求 Cholesky 变成除一个数，没有矩阵求逆，没有 <span><span>3×33\times3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span> 的分解。</p><p><strong>为什么等价。</strong> 联合更新是”先验乘似然”，似然 <span><span>p(z∣x)=∏jp(zj∣x)p(z\mid x) = \prod_jp(z_j\mid x)</span><span><span><span></span><span>p</span><span>(</span><span>z</span><span></span><span>∣</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>∏</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>p</span><span>(</span><span><span>z</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∣</span><span></span></span><span><span></span><span>x</span><span>)</span></span></span></span> 因为噪声独立而分解成三个因子，贝叶斯公式可以一次乘一个：每处理一个分量，后验成为下一个分量的先验。对线性测量，这和联合更新给出同样的后验，是代数恒等式，不是近似。</p><p><strong>对误差状态滤波器的一个细节。</strong> 三个分量用的 <span><span>HjH_j</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（<span><span>H=[h]×H = [h]_\times</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>h</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 行）都在同一个名义姿态处线性化。处理第 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 个分量的时候，误差状态已经被前面的分量修正了 <span><span>δxacc\delta x_{acc}</span><span><span><span></span><span>δ</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>a</span><span>cc</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以它的新息要减掉这部分：<span><span>yj′=yj−Hj δxaccy_j' = y_j - H_j\,\delta x_{acc}</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>δ</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>a</span><span>cc</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。三个分量处理完，才把累积的 <span><span>δxacc\delta x_{acc}</span><span><span><span></span><span>δ</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>a</span><span>cc</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 一次性注入名义状态。如果每个分量处理完都注入，后一个分量的线性化点就变了，和联合更新不再等价。</p><p>实现：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Sequential scalar updates and a UD-factored ESKF (chapter 8). Built on kl.eskf.ESKF."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>from</span><span> .eskf </span><span>import</span><span><span> </span><span>ESKF</span></span></div></div><div><div><div>6</div></div><div><span>from</span><span> .imu_sim </span><span>import</span><span> G</span></div></div><div><div><div>7</div></div><div><span>from</span><span> .quat </span><span>import</span><span> qexp, qmul, qnormalize, qrot, skew, wrap_pi</span></div></div><div><div><div>8</div></div><div><span>from</span><span> .ud </span><span>import</span><span> bierman_update, thornton_predict, ud_to_P</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>scalar_update</span><span>(</span><span>P</span><span>,</span><span><span> </span><span>h</span></span><span>,</span><span><span> </span><span>r</span></span><span>,</span><span><span> </span><span>y</span></span><span>,</span><span><span> </span><span>joseph</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"""One scalar measurement: returns (dx, P_new). joseph=False is the short form (I - K h) P."""</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> h </span><span>@</span><span> P </span><span>@</span><span> h </span><span>+</span><span> r</span></div></div><div><div><div>14</div></div><div><span>    </span><span>if</span><span> </span><span>not</span><span><span> s </span><span>&gt;</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>15</div></div><div><span>        </span><span>raise</span><span><span> np.linalg.</span><span>LinAlgError</span><span>(</span></span><span>"S is not positive"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>K </span><span>=</span><span> P </span><span>@</span><span> h </span><span>/</span><span> s</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>len</span><span><span>(h)) </span><span>-</span><span> np.</span><span>outer</span><span>(K, h)</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>if</span><span> joseph:</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>Pn </span><span>=</span><span> A </span><span>@</span><span> P </span><span>@</span><span> A.T </span><span>+</span><span> r </span><span>*</span><span> np.</span><span>outer</span><span>(K, K)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>Pn </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> (Pn </span><span>+</span><span> Pn.T)</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>else</span><span>:</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>Pn </span><span>=</span><span> A </span><span>@</span><span> P</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span><span> K </span><span>*</span><span> y, Pn</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>update_accel_seq</span><span>(</span><span>f</span><span>,</span><span><span> </span><span>accel</span></span><span>,</span><span><span> </span><span>joseph</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>27</div></div><div><span>    </span><span>"""Accelerometer update processing the three components one at a time (diagonal R).</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>Same linearization point for all three rows; the nominal state is injected once at the end.</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(accel </span><span>@</span><span> accel)</span></div></div><div><div><div>32</div></div><div><span>    </span><span>if</span><span> f.accel_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> G) </span><span>&gt;</span><span> f.accel_gate:</span></span></div></div><div><div><div>33</div></div><div><span>        </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> </span><span>qrot</span><span>(f.q).T </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>Hm </span><span>=</span><span> </span><span>skew</span><span>(h)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> accel </span><span>/</span><span> norm </span><span>-</span><span> h</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> (f.sa </span><span>/</span><span> G) </span><span>**</span><span> </span><span>2</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>dx </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>):</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>hj </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>hj[:</span><span>3</span><span><span>] </span><span>=</span><span> Hm[j]</span></span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>dxj, f.P </span><span>=</span><span> </span><span>scalar_update</span><span>(f.P, hj, r, y[j] </span><span>-</span><span> hj </span><span>@</span><span> dx, joseph)</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>dx </span><span>=</span><span> dx </span><span>+</span><span> dxj</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>f.</span><span>_inject</span><span>(dx)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>return</span><span> </span><span>True</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>def</span><span> </span><span>update_mag_heading_seq</span><span>(</span><span>f</span><span>,</span><span><span> </span><span>mag</span></span><span>,</span><span><span> </span><span>joseph</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(mag </span><span>@</span><span> mag)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>if</span><span> f.mag_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> </span></span><span>1.0</span><span><span>) </span><span>&gt;</span><span> f.mag_gate:</span></span></div></div><div><div><div>51</div></div><div><span>        </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>mw </span><span>=</span><span> </span><span>qrot</span><span>(f.q) </span><span>@</span><span> mag</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>y </span><span>=</span><span> </span><span>wrap_pi</span><span>(np.</span><span>arctan2</span><span>(mw[</span><span>1</span><span>], mw[</span><span>0</span><span><span>]) </span><span>-</span><span> np.</span><span>arctan2</span><span>(f.m_w[</span></span><span>1</span><span>], f.m_w[</span><span>0</span><span>]))</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>h[:</span><span>3</span><span><span>] </span><span>=</span><span> f.</span><span>heading_jacobian</span><span>()</span></span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>r </span><span>=</span><span> (f.sm </span><span>/</span><span> np.</span><span>hypot</span><span>(f.m_w[</span><span>0</span><span>], f.m_w[</span><span>1</span><span><span>])) </span><span>**</span><span> </span></span><span>2</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>dx, f.P </span><span>=</span><span> </span><span>scalar_update</span><span>(f.P, h, r, y, joseph)</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>f.</span><span>_inject</span><span>(dx)</span></div></div><div><div><div>59</div></div><div><span>    </span><span>return</span><span> </span><span>True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C、C++、Rust 的版本是各自一个新文件（有两处我碰了第 6 篇的东西：Rust 的 <code>Eskf</code> 的参数字段原来是私有的，我改成了 <code>pub</code>，别的没动；C 的 <code>kl_eskf.c</code> 里的 <code>fmod</code>，见后面的代码体积一节）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* One scalar measurement h^T x + v, var(v) = r, innovation y: P in place, dx = K y.</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* w is scratch for 126 kl_real. Returns -1 (P unchanged) if s &lt;= 0. */</span></div></div><div><div><div>3</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>scalar_update</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>h</span><span>,</span><span> kl_real </span><span>r</span><span>,</span><span> kl_real </span><span>y</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>joseph</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>dx</span><span>,</span></span></div></div><div><div><div>4</div></div><div><span><span>       </span></span><span>kl_real </span><span>*</span><span><span>w</span><span>)</span></span></div></div><div><div><div>5</div></div><div><span>{</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Ph </span><span>=</span><span> w, </span><span>*</span><span>hP </span><span>=</span><span> Ph </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>K </span><span>=</span><span> hP </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>A </span><span>=</span><span> K </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>T </span><span>=</span><span> A </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>kl_mat_mul</span><span>(P, h, Ph, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>kl_mat_mul</span><span>(h, P, hP, </span><span>1</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>kl_real s </span><span>=</span><span> r;</span></div></div><div><div><div>10</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>s </span><span>+=</span><span> </span><span>hP</span><span>[i] </span><span>*</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>12</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>(s </span><span>&gt;</span><span> </span><span>0</span><span>))</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>K</span><span>[i] </span><span>=</span><span> </span><span>Ph</span><span>[i] </span><span>/</span><span> s;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>dx</span><span>[i] </span><span>=</span><span> </span><span>K</span><span>[i] </span><span>*</span><span> y;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>/* A = I - K h^T */</span></div></div><div><div><div>19</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>21</div></div><div><span>      </span><span>A</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>-</span><span> </span><span>K</span><span>[i] </span><span>*</span><span> </span><span>h</span><span>[j];</span></div></div><div><div><div>22</div></div><div><span>  </span><span>kl_mat_mul</span><span>(A, P, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>if</span><span> (joseph) {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, A, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>26</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>27</div></div><div><span>        </span><span>P</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>U</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>+</span><span> r </span><span>*</span><span> (</span><span>K</span><span>[i] </span><span>*</span><span> </span><span>K</span><span>[j]);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>kl_mat_symmetrize</span><span>(P, </span><span>6</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>30</div></div><div><span>    </span><span>kl_mat_copy</span><span>(T, P, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>inject</code> 在这里又复制了一份（第 6 篇的那份是 <code>static</code> 的，我没有为了去重去改已经定稿的文件）。<code>kl_eskf_update_accel_seq</code> 在一份 <code>P</code> 的拷贝上工作，处理失败时不改动 <code>f-&gt;P</code>。</p><p><strong>验证。</strong> 200 个随机的名义姿态和协方差，分别做向量更新和顺序更新，再做航向更新：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sequential scalar updates vs the vector update, 200 random states: max difference 6.73e-16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在整段数据上，三种协方差更新形式（向量 Joseph、顺序 Joseph、顺序短形式）和 UD 形式在 float64 下的姿态差都小于 <span><span>10−1310^{-13}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> 度。</p></section>
<section><h2>UD 分解<a href="#ud-分解"><span>#</span></a></h2><p>第 3 篇我没能让短形式崩溃，Joseph 形式的优势是个”保证”而不是实测差距。UD 分解是更强的保证：不存 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span>，存它的分解 <span><span>P=UDUTP = UDU^T</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>D</span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，<span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 是对角线为 1 的上三角矩阵，<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 是对角矩阵。<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 半正定等价于 <span><span>D≥0D\ge0</span><span><span><span></span><span>D</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>；如果更新算法只做加法、乘法和除以正数，<span><span>DD</span><span><span><span></span><span>D</span></span></span></span> 就不会变成负数。数值上这还有个额外的好处：<span><span>UDU\sqrt D</span><span><span><span></span><span>U</span><span><span><span><span><span><span></span><span>D</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 的条件数是 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的条件数的平方根，<span><span>101410^{14}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span></span></span></span></span></span></span></span> 的动态范围在 UD 里只是 <span><span>10710^7</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span></span></span></span>，float32（相对精度 <span><span>10−710^{-7}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>）勉强装得下。</p><p>两个算法（Bierman 1977；Grewal 和 Andrews，<em>Kalman Filtering: Theory and Practice</em>）：</p><p><strong>测量更新（Bierman）。</strong> 标量测量 <span><span>z=hTx+vz = h^Tx+v</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span><span>h</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span>，<span><span>var⁡(v)=r\operatorname{var}(v) = r</span><span><span><span></span><span><span>var</span></span><span>(</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>r</span></span></span></span>。令 <span><span>f=UThf = U^Th</span><span><span><span></span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>h</span></span></span></span>，<span><span>g=Dfg = Df</span><span><span><span></span><span>g</span><span></span><span>=</span><span></span></span><span><span></span><span>D</span><span>f</span></span></span></span>。逐列 <span><span>j=1..nj = 1..n</span><span><span><span></span><span>j</span><span></span><span>=</span><span></span></span><span><span></span><span>1..</span><span>n</span></span></span></span> 递推 <span><span>αj=αj−1+fjgj\alpha_j = \alpha_{j-1}+f_jg_j</span><span><span><span></span><span><span>α</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>α</span><span><span><span><span><span><span></span><span><span><span>j</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（<span><span>α0=r\alpha_0 = r</span><span><span><span></span><span><span>α</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>r</span></span></span></span>），</p><span><span><span>Dj←Djαj−1αj,Uij←Uij+biλj,  bi←bi+Uijoldgj,  λj=−fjαj−1D_j\leftarrow D_j\frac{\alpha_{j-1}}{\alpha_j},\qquad U_{ij}\leftarrow U_{ij}+b_i\lambda_j,\ \ b_i\leftarrow b_i+U_{ij}^{old}g_j,\ \ \lambda_j = -\frac{f_j}{\alpha_{j-1}}</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>α</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>α</span><span><span><span><span><span><span></span><span><span><span>j</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span> </span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>U</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span><span><span></span><span><span><span>o</span><span>l</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span> </span><span></span><span><span>λ</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>α</span><span><span><span><span><span><span></span><span><span><span>j</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>f</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>最后增益 <span><span>K=b/αnK = b/\alpha_n</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span>/</span><span><span>α</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。每一步都是正数的加、乘、除，没有减法抵消。我没有在这里重新推导它，而是按文献实现，再用数值检验：随机的 <span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 正定 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span>，Bierman 更新得到的 <span><span>UDUTUDU^T</span><span><span><span></span><span>U</span><span>D</span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 和 Joseph 形式的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 在 <span><span>2×10−152\times10^{-15}</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 以内，<span><span>dxdx</span><span><span><span></span><span>d</span><span>x</span></span></span></span> 差 <span><span>3×10−173\times10^{-17}</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p><strong>时间更新（Thornton）。</strong> 要从 <span><span>P=UDUTP = UDU^T</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>U</span><span>D</span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 得到 <span><span>FPFT+QF P F^T+Q</span><span><span><span></span><span>F</span><span>P</span><span><span>F</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Q</span></span></span></span> 的分解。把 <span><span>W=[FU  I]W = [FU\ \ I]</span><span><span><span></span><span>W</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>F</span><span>U</span><span> </span><span> </span><span>I</span><span>]</span></span></span></span>（<span><span>n×2nn\times2n</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span>n</span></span></span></span>）的行，用权重 <span><span>diag⁡(D,diag⁡Q)\operatorname{diag}(D,\operatorname{diag}Q)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span>D</span><span>,</span><span></span><span><span>diag</span></span><span></span><span>Q</span><span>)</span></span></span></span> 做修正的加权 Gram-Schmidt 正交化，得到新的 <span><span>UU</span><span><span><span></span><span>U</span></span></span></span> 和 <span><span>DD</span><span><span><span></span><span>D</span></span></span></span>：因为 <span><span>WDwWT=FUDUTFT+QWD_wW^T = FUDU^TF^T+Q</span><span><span><span></span><span>W</span><span><span>D</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span>U</span><span>D</span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Q</span></span></span></span>，其中 <span><span>Dw=diag⁡(D,q)D_w = \operatorname{diag}(D,q)</span><span><span><span></span><span><span>D</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>D</span><span>,</span><span></span><span>q</span><span>)</span></span></span></span>，正交化给出的就是它的 UD 分解。这里 <span><span>QδQ_\delta</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>δ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是对角的，所以 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 的列配 <span><span>diag⁡Q\operatorname{diag}Q</span><span><span><span></span><span><span>diag</span></span><span></span><span>Q</span></span></span></span>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""UD (unit upper triangular x diagonal) covariance factorization, P = U D U^T.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>Bierman's scalar measurement update and Thornton's time update (modified weighted</span></div></div><div><div><div>4</div></div><div><span>Gram-Schmidt). See G. J. Bierman, "Factorization Methods for Discrete Sequential</span></div></div><div><div><div>5</div></div><div><span>Estimation" (1977) and Grewal and Andrews, "Kalman Filtering: Theory and Practice".</span></div></div><div><div><div>6</div></div><div><span>"""</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>ud_factor</span><span>(</span><span>P</span><span>):</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"""P (symmetric positive definite) -&gt; (U, D). Raises ValueError if a pivot is &lt;= 0."""</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> P.shape[</span><span>0</span><span>]</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>U </span><span>=</span><span> np.</span><span>eye</span><span>(n)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>D </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> P.</span><span>copy</span><span>()</span></div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span>):</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>D[j] </span><span>=</span><span> A[j, j]</span></div></div><div><div><div>19</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span><span> D[j] </span><span>&gt;</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>20</div></div><div><span>            </span><span>raise</span><span> </span><span>ValueError</span><span>(</span><span>"matrix is not positive definite"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(j):</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>U[i, j] </span><span>=</span><span> A[i, j] </span><span>/</span><span> D[j]</span></div></div><div><div><div>23</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(j):</span></div></div><div><div><div>24</div></div><div><span>            </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span><span>(i </span><span>+</span><span> </span></span><span>1</span><span>):</span></div></div><div><div><div>25</div></div><div><span><span>                </span></span><span>A[k, i] </span><span>-=</span><span> U[k, j] </span><span>*</span><span> D[j] </span><span>*</span><span> U[i, j]</span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span> U, D</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>def</span><span> </span><span>ud_to_P</span><span>(</span><span>U</span><span>,</span><span><span> </span><span>D</span></span><span>):</span></div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span><span> (U </span><span>*</span><span> D) </span><span>@</span><span> U.T</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>def</span><span> </span><span>bierman_update</span><span>(</span><span>U</span><span>,</span><span><span> </span><span>D</span></span><span>,</span><span><span> </span><span>h</span></span><span>,</span><span><span> </span><span>r</span></span><span>,</span><span><span> </span><span>y</span></span><span>):</span></div></div><div><div><div>34</div></div><div><span>    </span><span>"""Scalar measurement update of (U, D) in place; returns the correction dx = K y.</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>Measurement z = h x + v, var(v) = r, innovation y. Uses the Joseph-equivalent</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>factored form, so P stays positive definite by construction.</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(D)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> U.T </span><span>@</span><span> h</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> D </span><span>*</span><span> f</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>alpha </span><span>=</span><span> r </span><span>+</span><span> f[</span><span>0</span><span><span>] </span><span>*</span><span> g[</span></span><span>0</span><span>]</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>D[</span><span>0</span><span><span>] </span><span>=</span><span> D[</span></span><span>0</span><span><span>] </span><span>*</span><span> r </span><span>/</span><span> alpha</span></span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>b[</span><span>0</span><span><span>] </span><span>=</span><span> g[</span></span><span>0</span><span>]</span></div></div><div><div><div>46</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, n):</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>gamma </span><span>=</span><span> alpha</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>alpha </span><span>=</span><span> alpha </span><span>+</span><span> f[j] </span><span>*</span><span> g[j]</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>D[j] </span><span>=</span><span> D[j] </span><span>*</span><span> gamma </span><span>/</span><span> alpha</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>lam </span><span>=</span><span> </span><span>-</span><span>f[j] </span><span>/</span><span> gamma</span></div></div><div><div><div>51</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(j):</span></div></div><div><div><div>52</div></div><div><span><span>            </span></span><span>s </span><span>=</span><span> U[i, j]</span></div></div><div><div><div>53</div></div><div><span><span>            </span></span><span>U[i, j] </span><span>=</span><span> s </span><span>+</span><span> b[i] </span><span>*</span><span> lam</span></div></div><div><div><div>54</div></div><div><span><span>            </span></span><span>b[i] </span><span>=</span><span> b[i] </span><span>+</span><span> s </span><span>*</span><span> g[j]</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>b[j] </span><span>=</span><span> g[j]</span></div></div><div><div><div>56</div></div><div><span>    </span><span>return</span><span><span> b </span><span>/</span><span> alpha </span><span>*</span><span> y</span></span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>def</span><span> </span><span>thornton_predict</span><span>(</span><span>U</span><span>,</span><span><span> </span><span>D</span></span><span>,</span><span><span> </span><span>F</span></span><span>,</span><span><span> </span><span>qdiag</span></span><span>):</span></div></div><div><div><div>60</div></div><div><span>    </span><span>"""Time update P &lt;- F P F^T + diag(qdiag) in factored form. Returns new (U, D)."""</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(D)</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>W </span><span>=</span><span> np.</span><span>hstack</span><span>([F </span><span>@</span><span> U, np.</span><span>eye</span><span>(n)])  </span><span># rows are the vectors to orthogonalize</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>wt </span><span>=</span><span> np.</span><span>concatenate</span><span>([D, qdiag])  </span><span># their weights</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>Un, Dn </span><span>=</span><span> np.</span><span>eye</span><span>(n), np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>65</div></div><div><span>    </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span><span>(n </span><span>-</span><span> </span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span>):</span></div></div><div><div><div>66</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> wt </span><span>*</span><span> W[j]</span></div></div><div><div><div>67</div></div><div><span><span>        </span></span><span>Dn[j] </span><span>=</span><span> W[j] </span><span>@</span><span> c</span></div></div><div><div><div>68</div></div><div><span><span>        </span></span><span>d </span><span>=</span><span> c </span><span>/</span><span> Dn[j]</span></div></div><div><div><div>69</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(j):</span></div></div><div><div><div>70</div></div><div><span><span>            </span></span><span>Un[i, j] </span><span>=</span><span> W[i] </span><span>@</span><span> d</span></div></div><div><div><div>71</div></div><div><span><span>            </span></span><span>W[i] </span><span>=</span><span> W[i] </span><span>-</span><span> Un[i, j] </span><span>*</span><span> W[j]</span></div></div><div><div><div>72</div></div><div><span>    </span><span>return</span><span> Un, Dn</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C 版的 Bierman 更新（Python 的逐行翻译）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>kl_ud_bierman</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>U</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>D</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>h</span><span>,</span><span> kl_real </span><span>r</span><span>,</span><span> kl_real </span><span>y</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>2</div></div><div><span><span>       </span></span><span>kl_real </span><span>*</span><span><span>dx</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>f </span><span>=</span><span> work, </span><span>*</span><span>g </span><span>=</span><span> f </span><span>+</span><span> n, </span><span>*</span><span>b </span><span>=</span><span> g </span><span>+</span><span> n;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>f</span><span>[j] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;=</span><span> j; i</span><span>++</span><span>)</span></div></div><div><div><div>8</div></div><div><span>      </span><span>f</span><span>[j] </span><span>+=</span><span> </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>*</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>9</div></div><div><span>    </span><span>g</span><span>[j] </span><span>=</span><span> </span><span>D</span><span>[j] </span><span>*</span><span> </span><span>f</span><span>[j];</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>kl_real alpha </span><span>=</span><span> r </span><span>+</span><span> </span><span>f</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>12</div></div><div><span>  </span><span>D</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>D</span><span>[</span><span>0</span><span>] </span><span>*</span><span> r </span><span>/</span><span> alpha;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>b</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>14</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>1</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>) {</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>kl_real gamma </span><span>=</span><span> alpha;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>alpha </span><span>=</span><span> alpha </span><span>+</span><span> </span><span>f</span><span>[j] </span><span>*</span><span> </span><span>g</span><span>[j];</span></div></div><div><div><div>17</div></div><div><span>    </span><span>D</span><span>[j] </span><span>=</span><span> </span><span>D</span><span>[j] </span><span>*</span><span> gamma </span><span>/</span><span> alpha;</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>kl_real lam </span><span>=</span><span> </span><span>-</span><span>f</span><span>[j] </span><span>/</span><span> gamma;</span></div></div><div><div><div>19</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> j; i</span><span>++</span><span>) {</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j];</span></div></div><div><div><div>21</div></div><div><span>      </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> s </span><span>+</span><span> </span><span>b</span><span>[i] </span><span>*</span><span> lam;</span></div></div><div><div><div>22</div></div><div><span>      </span><span>b</span><span>[i] </span><span>=</span><span> </span><span>b</span><span>[i] </span><span>+</span><span> s </span><span>*</span><span> </span><span>g</span><span>[j];</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>    </span><span>b</span><span>[j] </span><span>=</span><span> </span><span>g</span><span>[j];</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>dx</span><span>[i] </span><span>=</span><span> </span><span>b</span><span>[i] </span><span>/</span><span> alpha </span><span>*</span><span> y;</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C++ 版的 Thornton 时间更新（<code>W</code> 是 <span><span>N×2NN\times2N</span><span><span><span></span><span>N</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span>N</span></span></span></span> 的 <code>Mat</code>，行就是被正交化的向量）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Time update P &lt;- F P F^T + diag(qdiag) in factored form, in place.</span></div></div><div><div><div>2</div></div><div><span>// The rows of W = [F U | I] are orthogonalized with weights [D, qdiag].</span></div></div><div><div><div>3</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span>void</span><span> </span><span>thornton_predict</span><span>(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>U</span><span>, std::</span></span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>D</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>F</span><span>,</span></span></div></div><div><div><div>5</div></div><div><span>          </span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>qdiag</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>FU</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>U</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>W</span><span>{};</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>&gt; </span><span>wt</span><span>{}, </span><span>c</span><span>{};</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>W</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>=</span><span> </span><span>FU</span><span>(</span><span>i</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>W</span><span>(</span><span>i</span><span>, </span><span>N</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>12</div></div><div><span>    </span><span>wt</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>D</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>13</div></div><div><span>    </span><span>wt</span><span>[</span><span>N</span><span> </span><span>+</span><span> </span><span>i</span><span>] </span><span>=</span><span> </span><span>qdiag</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>  </span><span>U</span><span> </span><span>=</span><span> Mat&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>16</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>N</span><span>; </span><span>j</span><span>--</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>;) {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>T</span><span> </span><span>dj</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) {</span></div></div><div><div><div>19</div></div><div><span>      </span><span>c</span><span>[</span><span>k</span><span>] </span><span>=</span><span> </span><span>wt</span><span>[</span><span>k</span><span>] </span><span>*</span><span> </span><span>W</span><span>(</span><span>j</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>20</div></div><div><span>      </span><span>dj</span><span> </span><span>+=</span><span> </span><span>W</span><span>(</span><span>j</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>c</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>    </span><span>D</span><span>[</span><span>j</span><span>] </span><span>=</span><span> </span><span>dj</span><span>;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>24</div></div><div><span>      </span><span>T</span><span> </span><span>u</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>25</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>u</span><span> </span><span>+=</span><span> </span><span>W</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>*</span><span> (</span><span>c</span><span>[</span><span>k</span><span>] </span><span>/</span><span> </span><span>dj</span><span>);</span></div></div><div><div><div>26</div></div><div><span>      </span><span>U</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>u</span><span>;</span></div></div><div><div><div>27</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>W</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>-=</span><span> </span><span>u</span><span> </span><span>*</span><span> </span><span>W</span><span>(</span><span>j</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Rust 版的同一个函数，结构一样，但是写法不同：稳定版 Rust 的 const 泛型里不能写 <code>2 * N</code> 当数组长度，所以 <span><span>W=[FU  I]W = [FU\ \ I]</span><span><span><span></span><span>W</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>F</span><span>U</span><span> </span><span> </span><span>I</span><span>]</span></span></span></span> 被拆成两个 <span><span>N×NN\times N</span><span><span><span></span><span>N</span><span></span><span>×</span><span></span></span><span><span></span><span>N</span></span></span></span> 的块 <code>wa</code> 和 <code>wb</code>，权重也拆成 <code>d</code> 和 <code>qdiag</code>，内积分两段累加：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Time update P &lt;- F P F^T + diag(qdiag) in factored form, in place.</span></div></div><div><div><div>2</div></div><div><span>/// Orthogonalizes the rows of W = [F U | I] with weights [D, qdiag].</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>thornton_predict</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>4</div></div><div><span>    </span><span>u</span><span>: &amp;</span><span>mut</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>d</span><span>: &amp;</span><span>mut</span><span> [</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>6</div></div><div><span>    </span><span>f</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>qdiag</span><span>: &amp;[</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>8</div></div><div><span>) {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>wa</span><span><span> </span><span>=</span><span> *</span></span><span>f</span><span> * *</span><span>u</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>wb</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>un</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>dn</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>13</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> (</span><span>0</span><span>..</span><span>N</span><span>).</span><span>rev</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>ca</span><span>: [</span><span>Real</span><span>; </span><span>N</span><span><span>] </span><span>=</span><span> </span><span>core</span><span>::</span><span>array</span><span>::</span></span><span>from_fn</span><span><span>(</span><span>|</span></span><span>k</span><span><span>|</span><span> </span></span><span>d</span><span>[</span><span>k</span><span>] * </span><span>wa</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>]);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>cb</span><span>: [</span><span>Real</span><span>; </span><span>N</span><span><span>] </span><span>=</span><span> </span><span>core</span><span>::</span><span>array</span><span>::</span></span><span>from_fn</span><span><span>(</span><span>|</span></span><span>k</span><span><span>|</span><span> </span></span><span>qdiag</span><span>[</span><span>k</span><span>] * </span><span>wb</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>]);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>dj</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>17</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>dj</span><span><span> </span><span>+=</span><span> </span></span><span>wa</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>] * </span><span>ca</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>dj</span><span><span> </span><span>+=</span><span> </span></span><span>wb</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>] * </span><span>cb</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>        </span><span>dn</span><span>[</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>dj</span><span>;</span></div></div><div><div><div>24</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>26</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>27</div></div><div><span>                </span><span>s</span><span><span> </span><span>+=</span><span> </span></span><span>wa</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>] * (</span><span>ca</span><span>[</span><span>k</span><span>] / </span><span>dj</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>30</div></div><div><span>                </span><span>s</span><span><span> </span><span>+=</span><span> </span></span><span>wb</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>] * (</span><span>cb</span><span>[</span><span>k</span><span>] / </span><span>dj</span><span>);</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>            </span><span>un</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>33</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>34</div></div><div><span>                </span><span>wa</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span> * </span><span>wa</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>35</div></div><div><span>                </span><span>wb</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span> * </span><span>wb</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>*</span><span>u</span><span><span> </span><span>=</span><span> </span></span><span>un</span><span>;</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>*</span><span>d</span><span><span> </span><span>=</span><span> </span></span><span>dn</span><span>;</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>EskfUD</code> 用它们替换了 ESKF 的协方差：predict 用 Thornton，加速度计三个分量用三次 Bierman（同样的线性化点，同样的 <span><span>δxacc\delta x_{acc}</span><span><span><span></span><span>δ</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>a</span><span>cc</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 修正），航向一次。它没有重置雅可比（第 6 篇测过它的效果可以忽略，并且 <span><span>GUDUTGTGUD U^TG^T</span><span><span><span></span><span>G</span><span>U</span><span>D</span><span><span>U</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>G</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 不再是 UD 形式），所以对比的参照是 <code>reset=False</code> 的 ESKF。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span><span> </span><span>EskfUD</span></span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""ESKF whose covariance lives in UD form: Thornton predict, Bierman scalar updates.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>No reset Jacobian (it would not preserve the factored form); compare with ESKF(reset=False).</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>sigma_g</span></span><span>,</span><span><span> </span><span>sigma_bw</span></span><span>,</span><span><span> </span><span>sigma_a</span></span><span>,</span><span><span> </span><span>sigma_m</span></span><span>,</span><span><span> </span><span>m_w</span></span><span>,</span><span><span> </span><span>q0</span></span><span>,</span><span><span> </span><span>P0</span></span><span>,</span><span><span> </span><span>accel_gate</span></span><span>=</span><span>0.5</span><span>,</span><span><span> </span><span>mag_gate</span></span><span>=</span><span>0.1</span><span>):</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span><span>.ref </span><span>=</span><span> </span><span>ESKF</span><span>(sigma_g, sigma_bw, sigma_a, sigma_m, m_w, q0, P0, accel_gate, mag_gate, </span></span><span>reset</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>d </span><span>=</span><span> np.</span><span>diag</span><span>(P0).</span><span>copy</span><span>()</span></div></div><div><div><div>10</div></div><div><span>        </span><span>assert</span><span><span> np.</span><span>allclose</span><span>(np.</span><span>diag</span><span>(d), P0), </span></span><span>"initial covariance must be diagonal"</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span>.U, </span><span>self</span><span><span>.D </span><span>=</span><span> np.</span><span>eye</span><span>(</span></span><span>6</span><span>), d</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>q </span><span>=</span><span> </span><span>property</span><span>(</span><span>lambda</span><span> </span><span>s</span><span>: s.ref.q)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>b </span><span>=</span><span> </span><span>property</span><span>(</span><span>lambda</span><span> </span><span>s</span><span>: s.ref.b)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> </span><span>property</span><span>(</span><span>lambda</span><span> </span><span>s</span><span><span>: </span><span>ud_to_P</span><span>(s.U, s.D))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>def</span><span> </span><span>predict</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>gyro</span></span><span>,</span><span><span> </span><span>dt</span></span><span>):</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>f </span><span>=</span><span> </span><span>self</span><span>.ref</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>dq </span><span>=</span><span> </span><span>qexp</span><span>((gyro </span><span>-</span><span> f.b) </span><span>*</span><span> dt)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>F </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>F[:</span><span>3</span><span>, :</span><span>3</span><span><span>] </span><span>=</span><span> </span><span>qrot</span><span>(dq).T</span></span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>F[:</span><span>3</span><span>, </span><span>3</span><span><span>:] </span><span>=</span><span> </span><span>-</span><span>dt </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>qd </span><span>=</span><span> np.</span><span>array</span><span>([f.sg</span><span>**</span><span>2</span><span><span> </span><span>*</span><span> dt</span><span>**</span></span><span>2</span><span><span>] </span><span>*</span><span> </span></span><span>3</span><span><span> </span><span>+</span><span> [f.sbw</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> dt] </span><span>*</span><span> </span></span><span>3</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>f.q </span><span>=</span><span> </span><span>qmul</span><span>(f.q, dq)</span></div></div><div><div><div>25</div></div><div><span>        </span><span>self</span><span>.U, </span><span>self</span><span><span>.D </span><span>=</span><span> </span><span>thornton_predict</span><span>(</span></span><span>self</span><span>.U, </span><span>self</span><span>.D, F, qd)</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span>def</span><span> </span><span>_inject</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>dx</span></span><span>):</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>f </span><span>=</span><span> </span><span>self</span><span>.ref</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>f.q </span><span>=</span><span> </span><span>qnormalize</span><span>(</span><span>qmul</span><span>(f.q, </span><span>qexp</span><span>(dx[:</span><span>3</span><span>])))</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>f.b </span><span>=</span><span> f.b </span><span>+</span><span> dx[</span><span>3</span><span>:]</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>def</span><span> </span><span>update_accel</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>accel</span></span><span>):</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>f </span><span>=</span><span> </span><span>self</span><span>.ref</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(accel </span><span>@</span><span> accel)</span></div></div><div><div><div>35</div></div><div><span>        </span><span>if</span><span> f.accel_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> G) </span><span>&gt;</span><span> f.accel_gate:</span></span></div></div><div><div><div>36</div></div><div><span>            </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>h </span><span>=</span><span> </span><span>qrot</span><span>(f.q).T </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>Hm </span><span>=</span><span> </span><span>skew</span><span>(h)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> accel </span><span>/</span><span> norm </span><span>-</span><span> h</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> (f.sa </span><span>/</span><span> G) </span><span>**</span><span> </span><span>2</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>dx </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>42</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>):</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>hj </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>hj[:</span><span>3</span><span><span>] </span><span>=</span><span> Hm[j]</span></span></div></div><div><div><div>45</div></div><div><span><span>            </span></span><span>dx </span><span>=</span><span> dx </span><span>+</span><span> </span><span>bierman_update</span><span>(</span><span>self</span><span>.U, </span><span>self</span><span><span>.D, hj, r, y[j] </span><span>-</span><span> hj </span><span>@</span><span> dx)</span></span></div></div><div><div><div>46</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_inject</span><span>(dx)</span></span></div></div><div><div><div>47</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span>def</span><span> </span><span>update_mag_heading</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>mag</span></span><span>):</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>f </span><span>=</span><span> </span><span>self</span><span>.ref</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(mag </span><span>@</span><span> mag)</span></div></div><div><div><div>52</div></div><div><span>        </span><span>if</span><span> f.mag_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> </span></span><span>1.0</span><span><span>) </span><span>&gt;</span><span> f.mag_gate:</span></span></div></div><div><div><div>53</div></div><div><span>            </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>mw </span><span>=</span><span> </span><span>qrot</span><span>(f.q) </span><span>@</span><span> mag</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> </span><span>wrap_pi</span><span>(np.</span><span>arctan2</span><span>(mw[</span><span>1</span><span>], mw[</span><span>0</span><span><span>]) </span><span>-</span><span> np.</span><span>arctan2</span><span>(f.m_w[</span></span><span>1</span><span>], f.m_w[</span><span>0</span><span>]))</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>h </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>h[:</span><span>3</span><span><span>] </span><span>=</span><span> f.</span><span>heading_jacobian</span><span>()</span></span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> (f.sm </span><span>/</span><span> np.</span><span>hypot</span><span>(f.m_w[</span><span>0</span><span>], f.m_w[</span><span>1</span><span><span>])) </span><span>**</span><span> </span></span><span>2</span></div></div><div><div><div>59</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_inject</span><span>(</span><span>bierman_update</span><span>(</span></span><span>self</span><span>.U, </span><span>self</span><span>.D, h, r, y))</span></div></div><div><div><div>60</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：c/include/kl/kl_ud.h、c/src/kl_ud.c、c/include/kl/kl_eskf_seq.h、c/src/kl_eskf_seq.c、c/include/kl/kl_eskf_ud.h、c/src/kl_eskf_ud.c<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* UD covariance factorization P = U D U^T: U unit upper triangular (row-major n x n, zeros below</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* the diagonal), D diagonal (n). Bierman's scalar measurement update and Thornton's time update</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* (modified weighted Gram-Schmidt). See G. J. Bierman, "Factorization Methods for Discrete</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>* Sequential Estimation" (1977) and Grewal and Andrews, "Kalman Filtering: Theory and Practice". */</span></div></div><div><div><div>5</div></div><div><span>#ifndef</span><span> </span><span>KL_UD_H</span></div></div><div><div><div>6</div></div><div><span>#define</span><span> </span><span>KL_UD_H</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>/* Workspace sizes (kl_real count) */</span></div></div><div><div><div>11</div></div><div><span>#define</span><span> </span><span>KL_UD_FACTOR_WORK_LEN</span><span><span>(</span><span>n</span><span>) ((n) </span></span><span>*</span><span> (n))</span></div></div><div><div><div>12</div></div><div><span>#define</span><span> </span><span>KL_UD_BIERMAN_WORK_LEN</span><span><span>(</span><span>n</span><span>) (</span></span><span>3</span><span> </span><span>*</span><span> (n))</span></div></div><div><div><div>13</div></div><div><span>#define</span><span> </span><span>KL_UD_THORNTON_WORK_LEN</span><span><span>(</span><span>n</span><span>) (</span></span><span>4</span><span> </span><span>*</span><span> (n) </span><span>*</span><span> (n) </span><span>+</span><span> </span><span>6</span><span> </span><span>*</span><span> (n) </span><span>+</span><span> (n))</span></div></div><div><div><div>14</div></div><div><span>#define</span><span> </span><span>KL_UD_PIVOT_WORK_LEN</span><span><span>(</span><span>n</span><span>) ((n) </span></span><span>*</span><span> (n) </span><span>+</span><span> (n))</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>/* P (symmetric positive definite, upper triangle read) -&gt; U, D. Returns -1 if a pivot is &lt;= 0. */</span></div></div><div><div><div>17</div></div><div><span>int</span><span> </span><span>kl_ud_factor</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>P</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>U</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>D</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>);</span></span></div></div><div><div><div>18</div></div><div><span>/* P = U diag(D) U^T */</span></div></div><div><div><div>19</div></div><div><span>void</span><span> </span><span>kl_ud_to_p</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>U</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>D</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>);</span></span></div></div><div><div><div>20</div></div><div><span>/* Scalar update (z = h x + v, var(v) = r, innovation y) of U, D in place; dx = K y.</span></div></div><div><div><div>21</div></div><div><span><span> </span></span><span>* The factored form keeps P positive definite by construction. */</span></div></div><div><div><div>22</div></div><div><span>void</span><span> </span><span>kl_ud_bierman</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>U</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>D</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>h</span><span>,</span><span> kl_real </span><span>r</span><span>,</span><span> kl_real </span><span>y</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>23</div></div><div><span><span>       </span></span><span>kl_real </span><span>*</span><span><span>dx</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>);</span></span></div></div><div><div><div>24</div></div><div><span>/* P &lt;- F P F^T + diag(qdiag) in factored form, in place. */</span></div></div><div><div><div>25</div></div><div><span>void</span><span> </span><span>kl_ud_thornton</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>U</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>D</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>F</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>qdiag</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>kl_real </span><span>*</span><span><span>work</span><span>);</span></span></div></div><div><div><div>27</div></div><div><span>/* Smallest pivot of the UD elimination of P (upper triangle read). Never fails: a value &lt;= 0</span></div></div><div><div><div>28</div></div><div><span><span> </span></span><span>* means P is not positive definite. */</span></div></div><div><div><div>29</div></div><div><span>kl_real </span><span>kl_ud_min_pivot</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>);</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_ud.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>int</span><span> </span><span>kl_ud_factor</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>P</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>U</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>D</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>A </span><span>=</span><span> work;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>kl_mat_copy</span><span>(P, A, n, n);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>kl_mat_identity</span><span>(U, n);</span></div></div><div><div><div>10</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> n </span><span>-</span><span> </span><span>1</span><span>; j </span><span>&gt;=</span><span> </span><span>0</span><span>; j</span><span>--</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>D</span><span>[j] </span><span>=</span><span> </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> j];</span></div></div><div><div><div>12</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>(</span><span>D</span><span>[j] </span><span>&gt;</span><span> </span><span>0</span><span>))</span></div></div><div><div><div>13</div></div><div><span>      </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> j; i</span><span>++</span><span>)</span></div></div><div><div><div>15</div></div><div><span>      </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> </span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>/</span><span> </span><span>D</span><span>[j];</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> j; i</span><span>++</span><span>)</span></div></div><div><div><div>17</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;=</span><span> i; k</span><span>++</span><span>)</span></div></div><div><div><div>18</div></div><div><span>        </span><span>A</span><span>[k </span><span>*</span><span> n </span><span>+</span><span> i] </span><span>-=</span><span> </span><span>U</span><span>[k </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>*</span><span> </span><span>D</span><span>[j] </span><span>*</span><span> </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j];</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>void</span><span> </span><span>kl_ud_to_p</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>U</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>D</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>24</div></div><div><span>{</span></div></div><div><div><div>25</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>) {</span></div></div><div><div><div>27</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>28</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> n; k</span><span>++</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> k] </span><span>*</span><span> </span><span>D</span><span>[k] </span><span>*</span><span> </span><span>U</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> k];</span></div></div><div><div><div>30</div></div><div><span>      </span><span>P</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> s;</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>void</span><span> </span><span>kl_ud_bierman</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>U</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>D</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>h</span><span>,</span><span> kl_real </span><span>r</span><span>,</span><span> kl_real </span><span>y</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>35</div></div><div><span><span>       </span></span><span>kl_real </span><span>*</span><span><span>dx</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>36</div></div><div><span>{</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>f </span><span>=</span><span> work, </span><span>*</span><span>g </span><span>=</span><span> f </span><span>+</span><span> n, </span><span>*</span><span>b </span><span>=</span><span> g </span><span>+</span><span> n;</span></div></div><div><div><div>38</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>) {</span></div></div><div><div><div>39</div></div><div><span>    </span><span>f</span><span>[j] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>40</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;=</span><span> j; i</span><span>++</span><span>)</span></div></div><div><div><div>41</div></div><div><span>      </span><span>f</span><span>[j] </span><span>+=</span><span> </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>*</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>42</div></div><div><span>    </span><span>g</span><span>[j] </span><span>=</span><span> </span><span>D</span><span>[j] </span><span>*</span><span> </span><span>f</span><span>[j];</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>kl_real alpha </span><span>=</span><span> r </span><span>+</span><span> </span><span>f</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>45</div></div><div><span>  </span><span>D</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>D</span><span>[</span><span>0</span><span>] </span><span>*</span><span> r </span><span>/</span><span> alpha;</span></div></div><div><div><div>46</div></div><div><span>  </span><span>b</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>47</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>1</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>) {</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>kl_real gamma </span><span>=</span><span> alpha;</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>alpha </span><span>=</span><span> alpha </span><span>+</span><span> </span><span>f</span><span>[j] </span><span>*</span><span> </span><span>g</span><span>[j];</span></div></div><div><div><div>50</div></div><div><span>    </span><span>D</span><span>[j] </span><span>=</span><span> </span><span>D</span><span>[j] </span><span>*</span><span> gamma </span><span>/</span><span> alpha;</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>kl_real lam </span><span>=</span><span> </span><span>-</span><span>f</span><span>[j] </span><span>/</span><span> gamma;</span></div></div><div><div><div>52</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> j; i</span><span>++</span><span>) {</span></div></div><div><div><div>53</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j];</span></div></div><div><div><div>54</div></div><div><span>      </span><span>U</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> s </span><span>+</span><span> </span><span>b</span><span>[i] </span><span>*</span><span> lam;</span></div></div><div><div><div>55</div></div><div><span>      </span><span>b</span><span>[i] </span><span>=</span><span> </span><span>b</span><span>[i] </span><span>+</span><span> s </span><span>*</span><span> </span><span>g</span><span>[j];</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>57</div></div><div><span>    </span><span>b</span><span>[j] </span><span>=</span><span> </span><span>g</span><span>[j];</span></div></div><div><div><div>58</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>dx</span><span>[i] </span><span>=</span><span> </span><span>b</span><span>[i] </span><span>/</span><span> alpha </span><span>*</span><span> y;</span></div></div><div><div><div>61</div></div><div><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>void</span><span> </span><span>kl_ud_thornton</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>U</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>D</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>F</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>qdiag</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>64</div></div><div><span><span>        </span></span><span>kl_real </span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>65</div></div><div><span>{</span></div></div><div><div><div>66</div></div><div><span>  </span><span>int</span><span> m </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> n;</span></div></div><div><div><div>67</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>W </span><span>=</span><span> work;</span><span>            /* n x 2n: rows are the vectors to orthogonalize, [F U | I] */</span></div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>FU </span><span>=</span><span> W </span><span>+</span><span> n </span><span>*</span><span> m;</span><span>      /* n x n */</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Un </span><span>=</span><span> FU </span><span>+</span><span> n </span><span>*</span><span> n;</span><span>     /* n x n */</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>wt </span><span>=</span><span> Un </span><span>+</span><span> n </span><span>*</span><span> n;</span><span>     /* 2n weights [D, qdiag] */</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>c </span><span>=</span><span> wt </span><span>+</span><span> m;</span><span>          /* 2n */</span></div></div><div><div><div>72</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>d </span><span>=</span><span> c </span><span>+</span><span> m;</span><span>           /* 2n */</span></div></div><div><div><div>73</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Dn </span><span>=</span><span> d </span><span>+</span><span> m;</span><span>          /* n */</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>  </span><span>kl_mat_mul</span><span>(F, U, FU, n, n, n);</span></div></div><div><div><div>76</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>77</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>78</div></div><div><span>      </span><span>W</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>=</span><span> k </span><span>&lt;</span><span> n </span><span>?</span><span> </span><span>FU</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> k] </span><span>:</span><span> (k </span><span>-</span><span> n </span><span>==</span><span> i </span><span>?</span><span> </span><span>1</span><span> </span><span>:</span><span> </span><span>0</span><span>);</span></div></div><div><div><div>79</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> n; k</span><span>++</span><span>) {</span></div></div><div><div><div>80</div></div><div><span>    </span><span>wt</span><span>[k] </span><span>=</span><span> </span><span>D</span><span>[k];</span></div></div><div><div><div>81</div></div><div><span>    </span><span>wt</span><span>[n </span><span>+</span><span> k] </span><span>=</span><span> </span><span>qdiag</span><span>[k];</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>83</div></div><div><span>  </span><span>kl_mat_identity</span><span>(Un, n);</span></div></div><div><div><div>84</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> n </span><span>-</span><span> </span><span>1</span><span>; j </span><span>&gt;=</span><span> </span><span>0</span><span>; j</span><span>--</span><span>) {</span></div></div><div><div><div>85</div></div><div><span><span>    </span></span><span>kl_real dj </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>86</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>) {</span></div></div><div><div><div>87</div></div><div><span>      </span><span>c</span><span>[k] </span><span>=</span><span> </span><span>wt</span><span>[k] </span><span>*</span><span> </span><span>W</span><span>[j </span><span>*</span><span> m </span><span>+</span><span> k];</span></div></div><div><div><div>88</div></div><div><span><span>      </span></span><span>dj </span><span>+=</span><span> </span><span>W</span><span>[j </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>*</span><span> </span><span>c</span><span>[k];</span></div></div><div><div><div>89</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>90</div></div><div><span>    </span><span>Dn</span><span>[j] </span><span>=</span><span> dj;</span></div></div><div><div><div>91</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>92</div></div><div><span>      </span><span>d</span><span>[k] </span><span>=</span><span> </span><span>c</span><span>[k] </span><span>/</span><span> dj;</span></div></div><div><div><div>93</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> j; i</span><span>++</span><span>) {</span></div></div><div><div><div>94</div></div><div><span><span>      </span></span><span>kl_real u </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>95</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>96</div></div><div><span><span>        </span></span><span>u </span><span>+=</span><span> </span><span>W</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>*</span><span> </span><span>d</span><span>[k];</span></div></div><div><div><div>97</div></div><div><span>      </span><span>Un</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> u;</span></div></div><div><div><div>98</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>99</div></div><div><span>        </span><span>W</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>-=</span><span> u </span><span>*</span><span> </span><span>W</span><span>[j </span><span>*</span><span> m </span><span>+</span><span> k];</span></div></div><div><div><div>100</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>101</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>102</div></div><div><span>  </span><span>kl_mat_copy</span><span>(Un, U, n, n);</span></div></div><div><div><div>103</div></div><div><span>  </span><span>kl_mat_copy</span><span>(Dn, D, n, </span><span>1</span><span>);</span></div></div><div><div><div>104</div></div><div><span>}</span></div></div><div><div><div>105</div></div><div>
</div></div><div><div><div>106</div></div><div><span>kl_real </span><span>kl_ud_min_pivot</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>107</div></div><div><span>{</span></div></div><div><div><div>108</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>A </span><span>=</span><span> work, </span><span>*</span><span>u </span><span>=</span><span> work </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>109</div></div><div><span><span>  </span></span><span>kl_real dmin </span><span>=</span><span> (kl_real)INFINITY;</span></div></div><div><div><div>110</div></div><div><span>  </span><span>kl_mat_copy</span><span>(P, A, n, n);</span></div></div><div><div><div>111</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> n </span><span>-</span><span> </span><span>1</span><span>; j </span><span>&gt;=</span><span> </span><span>0</span><span>; j</span><span>--</span><span>) {</span></div></div><div><div><div>112</div></div><div><span><span>    </span></span><span>kl_real d </span><span>=</span><span> </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> j];</span></div></div><div><div><div>113</div></div><div><span>    </span><span>if</span><span> (d </span><span>&lt;</span><span> dmin)</span></div></div><div><div><div>114</div></div><div><span><span>      </span></span><span>dmin </span><span>=</span><span> d;</span></div></div><div><div><div>115</div></div><div><span>    </span><span>if</span><span> (d </span><span>==</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>116</div></div><div><span>      </span><span>continue</span><span>;</span></div></div><div><div><div>117</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> j; k</span><span>++</span><span>)</span></div></div><div><div><div>118</div></div><div><span>      </span><span>u</span><span>[k] </span><span>=</span><span> </span><span>A</span><span>[k </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>/</span><span> d;</span></div></div><div><div><div>119</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> j; i</span><span>++</span><span>)</span></div></div><div><div><div>120</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;=</span><span> i; k</span><span>++</span><span>)</span></div></div><div><div><div>121</div></div><div><span>        </span><span>A</span><span>[k </span><span>*</span><span> n </span><span>+</span><span> i] </span><span>-=</span><span> </span><span>u</span><span>[k] </span><span>*</span><span> d </span><span>*</span><span> </span><span>u</span><span>[i];</span></div></div><div><div><div>122</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>123</div></div><div><span>  </span><span>return</span><span> dmin;</span></div></div><div><div><div>124</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Sequential scalar measurement updates for the ESKF (diagonal R, no matrix inverse or Cholesky).</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* Same linearization point for every row; the nominal state is injected once at the end. */</span></div></div><div><div><div>3</div></div><div><span>#ifndef</span><span> </span><span>KL_ESKF_SEQ_H</span></div></div><div><div><div>4</div></div><div><span>#define</span><span> </span><span>KL_ESKF_SEQ_H</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_eskf.h"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/* Accelerometer update, one component at a time. joseph = 0 uses the short form (I - K h) P.</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>* Return 1 if applied, 0 if gated out, -1 if a scalar innovation variance is &lt;= 0 (state unchanged). */</span></div></div><div><div><div>10</div></div><div><span>int</span><span> </span><span>kl_eskf_update_accel_seq</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>joseph</span><span>);</span></span></div></div><div><div><div>11</div></div><div><span>/* Heading-only magnetometer update as one scalar update; same return convention. */</span></div></div><div><div><div>12</div></div><div><span>int</span><span> </span><span>kl_eskf_update_mag_heading_seq</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>joseph</span><span>);</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_eskf_seq.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/kl_ekf_att.h"</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/* One scalar measurement h^T x + v, var(v) = r, innovation y: P in place, dx = K y.</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>* w is scratch for 126 kl_real. Returns -1 (P unchanged) if s &lt;= 0. */</span></div></div><div><div><div>9</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>scalar_update</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>h</span><span>,</span><span> kl_real </span><span>r</span><span>,</span><span> kl_real </span><span>y</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>joseph</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>dx</span><span>,</span></span></div></div><div><div><div>10</div></div><div><span><span>       </span></span><span>kl_real </span><span>*</span><span><span>w</span><span>)</span></span></div></div><div><div><div>11</div></div><div><span>{</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Ph </span><span>=</span><span> w, </span><span>*</span><span>hP </span><span>=</span><span> Ph </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>K </span><span>=</span><span> hP </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>A </span><span>=</span><span> K </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>T </span><span>=</span><span> A </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>kl_mat_mul</span><span>(P, h, Ph, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>kl_mat_mul</span><span>(h, P, hP, </span><span>1</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>kl_real s </span><span>=</span><span> r;</span></div></div><div><div><div>16</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>s </span><span>+=</span><span> </span><span>hP</span><span>[i] </span><span>*</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>18</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>(s </span><span>&gt;</span><span> </span><span>0</span><span>))</span></div></div><div><div><div>19</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>K</span><span>[i] </span><span>=</span><span> </span><span>Ph</span><span>[i] </span><span>/</span><span> s;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>dx</span><span>[i] </span><span>=</span><span> </span><span>K</span><span>[i] </span><span>*</span><span> y;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>/* A = I - K h^T */</span></div></div><div><div><div>25</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>27</div></div><div><span>      </span><span>A</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>-</span><span> </span><span>K</span><span>[i] </span><span>*</span><span> </span><span>h</span><span>[j];</span></div></div><div><div><div>28</div></div><div><span>  </span><span>kl_mat_mul</span><span>(A, P, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>29</div></div><div><span>  </span><span>if</span><span> (joseph) {</span></div></div><div><div><div>30</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, A, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>32</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>33</div></div><div><span>        </span><span>P</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>U</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>+</span><span> r </span><span>*</span><span> (</span><span>K</span><span>[i] </span><span>*</span><span> </span><span>K</span><span>[j]);</span></div></div><div><div><div>34</div></div><div><span>    </span><span>kl_mat_symmetrize</span><span>(P, </span><span>6</span><span>);</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>36</div></div><div><span>    </span><span>kl_mat_copy</span><span>(T, P, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>39</div></div><div><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>/* q = normalize(q (x) exp(dtheta)), b += db, then the reset Jacobian if f-&gt;reset */</span></div></div><div><div><div>42</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>inject</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>dx</span><span>[</span><span>6</span><span><span>]</span><span>)</span></span></div></div><div><div><div>43</div></div><div><span>{</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>G </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>T </span><span>=</span><span> G </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>kl_real </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>46</div></div><div><span>  </span><span>kl_quat_exp</span><span>(dx, dq);</span></div></div><div><div><div>47</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>48</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>49</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>50</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>51</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+=</span><span> </span><span>dx</span><span>[</span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>52</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>reset</span><span>) {</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>kl_real </span><span>half</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {(kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>0</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>1</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>2</span><span>]};</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>kl_real </span><span>S</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>55</div></div><div><span>    </span><span>kl_skew</span><span>(half, S);</span></div></div><div><div><div>56</div></div><div><span>    </span><span>kl_mat_identity</span><span>(G, </span><span>6</span><span>);</span></div></div><div><div><div>57</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>58</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>59</div></div><div><span>        </span><span>G</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>-=</span><span> </span><span>S</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>60</div></div><div><span>    </span><span>kl_mat_mul</span><span>(G, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>61</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, G, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>62</div></div><div><span>    </span><span>kl_mat_copy</span><span>(U, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>64</div></div><div><span>}</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div><span>int</span><span> </span><span>kl_eskf_update_accel_seq</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>joseph</span><span>)</span></span></div></div><div><div><div>67</div></div><div><span>{</span></div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Pw </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>w </span><span>=</span><span> Pw </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>70</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> (kl_real)KL_GRAVITY) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span>)</span></div></div><div><div><div>71</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>72</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>down</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>73</div></div><div><span><span>  </span></span><span>kl_real </span><span>h</span><span>[</span><span>3</span><span>], </span><span>S</span><span>[</span><span>9</span><span>], </span><span>y</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[</span><span>6</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>}, </span><span>dxj</span><span>[</span><span>6</span><span>];</span></div></div><div><div><div>74</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY);</span></div></div><div><div><div>75</div></div><div><span>  </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, down, h);</span></div></div><div><div><div>76</div></div><div><span>  </span><span>kl_skew</span><span>(h, S);</span></div></div><div><div><div>77</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>78</div></div><div><span>    </span><span>y</span><span>[i] </span><span>=</span><span> </span><span>accel</span><span>[i] </span><span>/</span><span> norm </span><span>-</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>79</div></div><div><span>  </span><span>kl_mat_copy</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, Pw, </span><span>6</span><span>, </span><span>6</span><span>);</span><span> /* work on a copy so a failure leaves P untouched */</span></div></div><div><div><div>80</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>) {</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>kl_real </span><span>hj</span><span>[</span><span>6</span><span>] </span><span>=</span><span> {</span><span>S</span><span>[j </span><span>*</span><span> </span><span>3</span><span>], </span><span>S</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> </span><span>1</span><span>], </span><span>S</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> </span><span>2</span><span>], </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>}, hd </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>82</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>6</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>83</div></div><div><span><span>      </span></span><span>hd </span><span>+=</span><span> </span><span>hj</span><span>[k] </span><span>*</span><span> </span><span>dx</span><span>[k];</span></div></div><div><div><div>84</div></div><div><span>    </span><span>if</span><span> (</span><span>scalar_update</span><span>(Pw, hj, r, </span><span>y</span><span>[j] </span><span>-</span><span> hd, joseph, dxj, w) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>85</div></div><div><span>      </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>86</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>6</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>87</div></div><div><span>      </span><span>dx</span><span>[k] </span><span>+=</span><span> </span><span>dxj</span><span>[k];</span></div></div><div><div><div>88</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>89</div></div><div><span>  </span><span>kl_mat_copy</span><span>(Pw, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>90</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>91</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>92</div></div><div><span>}</span></div></div><div><div><div>93</div></div><div>
</div></div><div><div><div>94</div></div><div><span>int</span><span> </span><span>kl_eskf_update_mag_heading_seq</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>joseph</span><span>)</span></span></div></div><div><div><div>95</div></div><div><span>{</span></div></div><div><div><div>96</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Pw </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>w </span><span>=</span><span> Pw </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>97</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>98</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span>)</span></div></div><div><div><div>99</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>100</div></div><div><span><span>  </span></span><span>kl_real </span><span>C</span><span>[</span><span>9</span><span>], </span><span>mw</span><span>[</span><span>3</span><span>], </span><span>h</span><span>[</span><span>6</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>}, </span><span>dx</span><span>[</span><span>6</span><span>], y;</span></div></div><div><div><div>101</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>102</div></div><div><span>  </span><span>kl_mat_mul</span><span>(C, mag, mw, </span><span>3</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>103</div></div><div><span><span>  </span></span><span>y </span><span>=</span><span> </span><span>kl_wrap_pi</span><span>(</span><span>KL_ATAN2</span><span>(</span><span>mw</span><span>[</span><span>1</span><span>], </span><span>mw</span><span>[</span><span>0</span><span>]) </span><span>-</span><span> </span><span>KL_ATAN2</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>]));</span></div></div><div><div><div>104</div></div><div><span>  </span><span>kl_eskf_heading_jacobian</span><span>(f, h);</span></div></div><div><div><div>105</div></div><div><span><span>  </span></span><span>kl_real mh </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>]);</span></div></div><div><div><div>106</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> mh) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> mh);</span></div></div><div><div><div>107</div></div><div><span>  </span><span>kl_mat_copy</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, Pw, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>108</div></div><div><span>  </span><span>if</span><span> (</span><span>scalar_update</span><span>(Pw, h, r, y, joseph, dx, w) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>109</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>110</div></div><div><span>  </span><span>kl_mat_copy</span><span>(Pw, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>111</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>112</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>113</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* ESKF whose covariance lives in UD form: Thornton predict, Bierman scalar updates.</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* No reset Jacobian (it would not preserve the factored form); compare with kl_eskf (reset = 0).</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* Quaternion conventions: see kl_quat.h. All storage is inline. */</span></div></div><div><div><div>4</div></div><div><span>#ifndef</span><span> </span><span>KL_ESKF_UD_H</span></div></div><div><div><div>5</div></div><div><span>#define</span><span> </span><span>KL_ESKF_UD_H</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>kl_real </span><span>q</span><span>[</span><span>4</span><span>], </span><span>b</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>kl_real </span><span>U</span><span>[</span><span>36</span><span>], </span><span>D</span><span>[</span><span>6</span><span>];</span><span> /* P = U diag(D) U^T */</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>kl_real sg, sbw, sa, sm;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>kl_real </span><span>m_w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>kl_real accel_gate, mag_gate;</span><span> /* negative disables the gate */</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>kl_real </span><span>work</span><span>[</span><span>256</span><span>];</span></div></div><div><div><div>16</div></div><div><span>} kl_eskf_ud;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>/* p0_diag: initial diagonal of P (6); the initial covariance is diagonal so U = I, D = p0_diag. */</span></div></div><div><div><div>19</div></div><div><span>void</span><span> </span><span>kl_eskf_ud_init</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>6</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>20</div></div><div><span><span>         </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>21</div></div><div><span><span>         </span></span><span>kl_real </span><span>accel_gate</span><span>,</span><span> kl_real </span><span>mag_gate</span><span>);</span></div></div><div><div><div>22</div></div><div><span>void</span><span> </span><span>kl_eskf_ud_predict</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>);</span></span></div></div><div><div><div>23</div></div><div><span>/* Three sequential Bierman updates. Return 1 applied, 0 gated out. */</span></div></div><div><div><div>24</div></div><div><span>int</span><span> </span><span>kl_eskf_ud_update_accel</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>25</div></div><div><span>int</span><span> </span><span>kl_eskf_ud_update_mag_heading</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>26</div></div><div><span>/* P = U diag(D) U^T (6x6) */</span></div></div><div><div><div>27</div></div><div><span>void</span><span> </span><span>kl_eskf_ud_cov</span><span>(</span><span>const</span><span> kl_eskf_ud </span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span></span><span>P</span><span>[</span><span>36</span><span><span>]</span><span>);</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_eskf_ud.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/kl_ekf_att.h"</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>"kl/kl_eskf.h"</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>"kl/kl_ud.h"</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>void</span><span> </span><span>kl_eskf_ud_init</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>6</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>10</div></div><div><span><span>         </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>11</div></div><div><span><span>         </span></span><span>kl_real </span><span>accel_gate</span><span>,</span><span> kl_real </span><span>mag_gate</span><span>)</span></div></div><div><div><div>12</div></div><div><span>{</span></div></div><div><div><div>13</div></div><div><span>  </span><span>kl_mat_copy</span><span>(q0, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>kl_mat_copy</span><span>(m_w, </span><span>f</span><span>-&gt;</span><span>m_w</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>kl_mat_copy</span><span>(p0_diag, </span><span>f</span><span>-&gt;</span><span>D</span><span>, </span><span>6</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>16</div></div><div><span>  </span><span>kl_mat_identity</span><span>(</span><span>f</span><span>-&gt;</span><span>U</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>=</span><span> sigma_g;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>=</span><span> sigma_bw;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>=</span><span> sigma_a;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>=</span><span> sigma_m;</span></div></div><div><div><div>23</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>=</span><span> accel_gate;</span></div></div><div><div><div>24</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>=</span><span> mag_gate;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>void</span><span> </span><span>kl_eskf_ud_cov</span><span>(</span><span>const</span><span> kl_eskf_ud </span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span></span><span>P</span><span>[</span><span>36</span><span><span>]</span><span>)</span></span></div></div><div><div><div>28</div></div><div><span>{</span></div></div><div><div><div>29</div></div><div><span>  </span><span>kl_ud_to_p</span><span>(</span><span>f</span><span>-&gt;</span><span>U</span><span>, </span><span>f</span><span>-&gt;</span><span>D</span><span>, P, </span><span>6</span><span>);</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>void</span><span> </span><span>kl_eskf_ud_predict</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>33</div></div><div><span>{</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>kl_real </span><span>F</span><span>[</span><span>36</span><span>], </span><span>qd</span><span>[</span><span>6</span><span>], </span><span>omega</span><span>[</span><span>3</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>C</span><span>[</span><span>9</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>35</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>omega</span><span>[i] </span><span>=</span><span> (</span><span>gyro</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i]) </span><span>*</span><span> dt;</span></div></div><div><div><div>37</div></div><div><span>  </span><span>kl_quat_exp</span><span>(omega, dq);</span></div></div><div><div><div>38</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(dq, C);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>kl_mat_identity</span><span>(F, </span><span>6</span><span>);</span></div></div><div><div><div>40</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>41</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>42</div></div><div><span>      </span><span>F</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>C</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>43</div></div><div><span>    </span><span>F</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> </span><span>3</span><span> </span><span>+</span><span> i] </span><span>=</span><span> </span><span>-</span><span>dt;</span></div></div><div><div><div>44</div></div><div><span>    </span><span>qd</span><span>[i] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> dt </span><span>*</span><span> dt;</span></div></div><div><div><div>45</div></div><div><span>    </span><span>qd</span><span>[</span><span>3</span><span> </span><span>+</span><span> i] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> dt;</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>48</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>49</div></div><div><span>  </span><span>kl_ud_thornton</span><span>(</span><span>f</span><span>-&gt;</span><span>U</span><span>, </span><span>f</span><span>-&gt;</span><span>D</span><span>, F, qd, </span><span>6</span><span>, </span><span>f</span><span>-&gt;</span><span>work</span><span>);</span></div></div><div><div><div>50</div></div><div><span>}</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>inject</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>dx</span><span>[</span><span>6</span><span><span>]</span><span>)</span></span></div></div><div><div><div>53</div></div><div><span>{</span></div></div><div><div><div>54</div></div><div><span><span>  </span></span><span>kl_real </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>55</div></div><div><span>  </span><span>kl_quat_exp</span><span>(dx, dq);</span></div></div><div><div><div>56</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>57</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>58</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>59</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+=</span><span> </span><span>dx</span><span>[</span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>61</div></div><div><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>int</span><span> </span><span>kl_eskf_ud_update_accel</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>64</div></div><div><span>{</span></div></div><div><div><div>65</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>66</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> (kl_real)KL_GRAVITY) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span>)</span></div></div><div><div><div>67</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>68</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>down</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>kl_real </span><span>h</span><span>[</span><span>3</span><span>], </span><span>S</span><span>[</span><span>9</span><span>], </span><span>y</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[</span><span>6</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>}, </span><span>dxj</span><span>[</span><span>6</span><span>];</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY);</span></div></div><div><div><div>71</div></div><div><span>  </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, down, h);</span></div></div><div><div><div>72</div></div><div><span>  </span><span>kl_skew</span><span>(h, S);</span></div></div><div><div><div>73</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>74</div></div><div><span>    </span><span>y</span><span>[i] </span><span>=</span><span> </span><span>accel</span><span>[i] </span><span>/</span><span> norm </span><span>-</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>75</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>) {</span></div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>kl_real </span><span>hj</span><span>[</span><span>6</span><span>] </span><span>=</span><span> {</span><span>S</span><span>[j </span><span>*</span><span> </span><span>3</span><span>], </span><span>S</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> </span><span>1</span><span>], </span><span>S</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> </span><span>2</span><span>], </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>}, hd </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>77</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>6</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>78</div></div><div><span><span>      </span></span><span>hd </span><span>+=</span><span> </span><span>hj</span><span>[k] </span><span>*</span><span> </span><span>dx</span><span>[k];</span></div></div><div><div><div>79</div></div><div><span>    </span><span>kl_ud_bierman</span><span>(</span><span>f</span><span>-&gt;</span><span>U</span><span>, </span><span>f</span><span>-&gt;</span><span>D</span><span>, hj, r, </span><span>y</span><span>[j] </span><span>-</span><span> hd, </span><span>6</span><span>, dxj, </span><span>f</span><span>-&gt;</span><span>work</span><span>);</span></div></div><div><div><div>80</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>6</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>81</div></div><div><span>      </span><span>dx</span><span>[k] </span><span>+=</span><span> </span><span>dxj</span><span>[k];</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>83</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>84</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>85</div></div><div><span>}</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>int</span><span> </span><span>kl_eskf_ud_update_mag_heading</span><span><span>(</span><span>kl_eskf_ud </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>88</div></div><div><span>{</span></div></div><div><div><div>89</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>90</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span>)</span></div></div><div><div><div>91</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>92</div></div><div><span><span>  </span></span><span>kl_real </span><span>C</span><span>[</span><span>9</span><span>], </span><span>mw</span><span>[</span><span>3</span><span>], </span><span>h</span><span>[</span><span>6</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>}, </span><span>dx</span><span>[</span><span>6</span><span>], y;</span></div></div><div><div><div>93</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>94</div></div><div><span>  </span><span>kl_mat_mul</span><span>(C, mag, mw, </span><span>3</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>y </span><span>=</span><span> </span><span>kl_wrap_pi</span><span>(</span><span>KL_ATAN2</span><span>(</span><span>mw</span><span>[</span><span>1</span><span>], </span><span>mw</span><span>[</span><span>0</span><span>]) </span><span>-</span><span> </span><span>KL_ATAN2</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>]));</span></div></div><div><div><div>96</div></div><div><span><span>  </span></span><span>/* d heading / d dtheta = [mx mz, my mz, -(mx^2+my^2)] / (mx^2+my^2) C(q) */</span></div></div><div><div><div>97</div></div><div><span><span>  </span></span><span>kl_real mx </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>], my </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], mz </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>2</span><span>], mh2 </span><span>=</span><span> mx </span><span>*</span><span> mx </span><span>+</span><span> my </span><span>*</span><span> my;</span></div></div><div><div><div>98</div></div><div><span><span>  </span></span><span>kl_real </span><span>v</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {mx </span><span>*</span><span> mz </span><span>/</span><span> mh2, my </span><span>*</span><span> mz </span><span>/</span><span> mh2, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>99</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>100</div></div><div><span>    </span><span>h</span><span>[j] </span><span>=</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>C</span><span>[j] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>C</span><span>[</span><span>3</span><span> </span><span>+</span><span> j] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>C</span><span>[</span><span>6</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>101</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> </span><span>KL_SQRT</span><span>(mh2)) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> </span><span>KL_SQRT</span><span>(mh2));</span></div></div><div><div><div>102</div></div><div><span>  </span><span>kl_ud_bierman</span><span>(</span><span>f</span><span>-&gt;</span><span>U</span><span>, </span><span>f</span><span>-&gt;</span><span>D</span><span>, h, r, y, </span><span>6</span><span>, dx, </span><span>f</span><span>-&gt;</span><span>work</span><span>);</span></div></div><div><div><div>103</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>104</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>105</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：cpp/include/kl/ud.hpp、eskf_seq.hpp、eskf_ud.hpp<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// UD (unit upper triangular x diagonal) covariance factorization, P = U D U^T.</span></div></div><div><div><div>2</div></div><div><span>// Bierman's scalar measurement update and Thornton's time update (modified weighted</span></div></div><div><div><div>3</div></div><div><span>// Gram-Schmidt). See G. J. Bierman, "Factorization Methods for Discrete Sequential</span></div></div><div><div><div>4</div></div><div><span>// Estimation" (1977) and Grewal and Andrews, "Kalman Filtering: Theory and Practice".</span></div></div><div><div><div>5</div></div><div><span>// U is stored as a full N x N matrix with unit diagonal and zeros below it.</span></div></div><div><div><div>6</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>&lt;limits&gt;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#include</span><span> </span><span>"kl/mat.hpp"</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>// P (symmetric positive definite, upper triangle read) -&gt; (U, D).</span></div></div><div><div><div>14</div></div><div><span>// Returns false if a pivot is &lt;= 0.</span></div></div><div><div><div>15</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>16</div></div><div><span>bool</span><span> </span><span>ud_factor</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>U</span><span>, std::</span></span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>D</span><span>) {</span></span></div></div><div><div><div>17</div></div><div><span>  </span><span>U</span><span> </span><span>=</span><span> Mat&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>18</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>A</span><span> </span><span>=</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>N</span><span>; </span><span>j</span><span>--</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>;) {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>D</span><span>[</span><span>j</span><span>] </span><span>=</span><span> </span><span>A</span><span>(</span><span>j</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>(</span><span>D</span><span>[</span><span>j</span><span>] </span><span>&gt;</span><span> </span><span>0</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>i</span><span>++</span><span>) </span><span>U</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>A</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>/</span><span> </span><span>D</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>23</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>24</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;=</span><span> </span><span>i</span><span>; </span><span>k</span><span>++</span><span>) </span><span>A</span><span>(</span><span>k</span><span>, </span><span>i</span><span>) </span><span>-=</span><span> </span><span>U</span><span>(</span><span>k</span><span>, </span><span>j</span><span>) </span><span>*</span><span> </span><span>D</span><span>[</span><span>j</span><span>] </span><span>*</span><span> </span><span>U</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>30</div></div><div><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>ud_to_p</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>U</span><span>, </span></span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>D</span><span>) {</span></span></div></div><div><div><div>31</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>UD</span><span> </span><span>=</span><span> </span><span>U</span><span>;</span></div></div><div><div><div>32</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>j</span><span>++</span><span>) </span><span>UD</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>*=</span><span> </span><span>D</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>34</div></div><div><span>  </span><span>return</span><span> </span><span>UD</span><span> </span><span>*</span><span> </span><span>U</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>// Scalar measurement update of (U, D) in place; returns the correction dx = K y.</span></div></div><div><div><div>38</div></div><div><span>// Measurement z = h x + v, var(v) = r, innovation y. Positive definiteness is kept</span></div></div><div><div><div>39</div></div><div><span>// by construction (no subtraction of covariances).</span></div></div><div><div><div>40</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>41</div></div><div><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>bierman_update</span><span>(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>U</span><span>, std::</span></span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>D</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>h</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>, </span></span><span>T</span><span><span> </span><span>y</span><span>) {</span></span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt; </span><span>f</span><span>{}, </span><span>g</span><span>{}, </span><span>b</span><span>{};</span></div></div><div><div><div>43</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>44</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) </span><span>f</span><span>[</span><span>j</span><span>] </span><span>+=</span><span> </span><span>U</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>*</span><span> </span><span>h</span><span>(</span><span>i</span><span>, </span><span>0</span><span>);</span><span>  // f = U^T h</span></div></div><div><div><div>45</div></div><div><span>    </span><span>g</span><span>[</span><span>j</span><span>] </span><span>=</span><span> </span><span>D</span><span>[</span><span>j</span><span>] </span><span>*</span><span> </span><span>f</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span>  </span><span>T</span><span> </span><span>alpha</span><span> </span><span>=</span><span> </span><span>r</span><span> </span><span>+</span><span> </span><span>f</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>48</div></div><div><span>  </span><span>D</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>D</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>r</span><span> </span><span>/</span><span> </span><span>alpha</span><span>;</span></div></div><div><div><div>49</div></div><div><span>  </span><span>b</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>50</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>1</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>51</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>gamma</span><span> </span><span>=</span><span> </span><span>alpha</span><span>;</span></div></div><div><div><div>52</div></div><div><span>    </span><span>alpha</span><span> </span><span>=</span><span> </span><span>alpha</span><span> </span><span>+</span><span> </span><span>f</span><span>[</span><span>j</span><span>] </span><span>*</span><span> </span><span>g</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>53</div></div><div><span>    </span><span>D</span><span>[</span><span>j</span><span>] </span><span>=</span><span> </span><span>D</span><span>[</span><span>j</span><span>] </span><span>*</span><span> </span><span>gamma</span><span> </span><span>/</span><span> </span><span>alpha</span><span>;</span></div></div><div><div><div>54</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>lam</span><span> </span><span>=</span><span> </span><span>-</span><span>f</span><span>[</span><span>j</span><span>] </span><span>/</span><span> </span><span>gamma</span><span>;</span></div></div><div><div><div>55</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>56</div></div><div><span>      </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>U</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>57</div></div><div><span>      </span><span>U</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>s</span><span> </span><span>+</span><span> </span><span>b</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>lam</span><span>;</span></div></div><div><div><div>58</div></div><div><span>      </span><span>b</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>b</span><span>[</span><span>i</span><span>] </span><span>+</span><span> </span><span>s</span><span> </span><span>*</span><span> </span><span>g</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>60</div></div><div><span>    </span><span>b</span><span>[</span><span>j</span><span>] </span><span>=</span><span> </span><span>g</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span>;</span></div></div><div><div><div>63</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) </span><span>dx</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>b</span><span>[</span><span>i</span><span>] </span><span>/</span><span> </span><span>alpha</span><span> </span><span>*</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>64</div></div><div><span>  </span><span>return</span><span> </span><span>dx</span><span>;</span></div></div><div><div><div>65</div></div><div><span>}</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>// Time update P &lt;- F P F^T + diag(qdiag) in factored form, in place.</span></div></div><div><div><div>68</div></div><div><span>// The rows of W = [F U | I] are orthogonalized with weights [D, qdiag].</span></div></div><div><div><div>69</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>70</div></div><div><span>void</span><span> </span><span>thornton_predict</span><span>(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>U</span><span>, std::</span></span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>D</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>F</span><span>,</span></span></div></div><div><div><div>71</div></div><div><span>          </span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>qdiag</span><span>) {</span></span></div></div><div><div><div>72</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>FU</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>U</span><span>;</span></div></div><div><div><div>73</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>W</span><span>{};</span></div></div><div><div><div>74</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>&gt; </span><span>wt</span><span>{}, </span><span>c</span><span>{};</span></div></div><div><div><div>75</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>76</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>W</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>=</span><span> </span><span>FU</span><span>(</span><span>i</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>77</div></div><div><span>    </span><span>W</span><span>(</span><span>i</span><span>, </span><span>N</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>78</div></div><div><span>    </span><span>wt</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>D</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>79</div></div><div><span>    </span><span>wt</span><span>[</span><span>N</span><span> </span><span>+</span><span> </span><span>i</span><span>] </span><span>=</span><span> </span><span>qdiag</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>80</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span>  </span><span>U</span><span> </span><span>=</span><span> Mat&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>82</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>N</span><span>; </span><span>j</span><span>--</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>;) {</span></div></div><div><div><div>83</div></div><div><span>    </span><span>T</span><span> </span><span>dj</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>84</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) {</span></div></div><div><div><div>85</div></div><div><span>      </span><span>c</span><span>[</span><span>k</span><span>] </span><span>=</span><span> </span><span>wt</span><span>[</span><span>k</span><span>] </span><span>*</span><span> </span><span>W</span><span>(</span><span>j</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>86</div></div><div><span>      </span><span>dj</span><span> </span><span>+=</span><span> </span><span>W</span><span>(</span><span>j</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>c</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>87</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>88</div></div><div><span>    </span><span>D</span><span>[</span><span>j</span><span>] </span><span>=</span><span> </span><span>dj</span><span>;</span></div></div><div><div><div>89</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>90</div></div><div><span>      </span><span>T</span><span> </span><span>u</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>91</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>u</span><span> </span><span>+=</span><span> </span><span>W</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>*</span><span> (</span><span>c</span><span>[</span><span>k</span><span>] </span><span>/</span><span> </span><span>dj</span><span>);</span></div></div><div><div><div>92</div></div><div><span>      </span><span>U</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>u</span><span>;</span></div></div><div><div><div>93</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>W</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>-=</span><span> </span><span>u</span><span> </span><span>*</span><span> </span><span>W</span><span>(</span><span>j</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>94</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>96</div></div><div><span>}</span></div></div><div><div><div>97</div></div><div>
</div></div><div><div><div>98</div></div><div><span>// Smallest pivot of the UD elimination of P (reads the upper triangle only).</span></div></div><div><div><div>99</div></div><div><span>// Unlike ud_factor this never fails: a value &lt;= 0 means P is not positive definite.</span></div></div><div><div><div>100</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>101</div></div><div><span>T</span><span> </span><span>min_pivot</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>) {</span></span></div></div><div><div><div>102</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>A</span><span> </span><span>=</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>103</div></div><div><span>  </span><span>T</span><span> </span><span>dmin</span><span> </span><span>=</span><span><span> std::</span><span>numeric_limits</span><span>&lt;</span></span><span>T</span><span>&gt;::</span><span>infinity</span><span>();</span></div></div><div><div><div>104</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>N</span><span>; </span><span>j</span><span>--</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>;) {</span></div></div><div><div><div>105</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>d</span><span> </span><span>=</span><span> </span><span>A</span><span>(</span><span>j</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>106</div></div><div><span>    </span><span>dmin</span><span> </span><span>=</span><span> </span><span>d</span><span> </span><span>&lt;</span><span> </span><span>dmin</span><span> </span><span>?</span><span> </span><span>d</span><span> </span><span>:</span><span> </span><span>dmin</span><span>;</span></div></div><div><div><div>107</div></div><div><span>    </span><span>if</span><span> (</span><span>d</span><span> </span><span>==</span><span> </span><span>0</span><span>) </span><span>continue</span><span>;</span></div></div><div><div><div>108</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>N</span><span>&gt; </span><span>u</span><span>{};</span></div></div><div><div><div>109</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>i</span><span>++</span><span>) </span><span>u</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>A</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>/</span><span> </span><span>d</span><span>;</span></div></div><div><div><div>110</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>111</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;=</span><span> </span><span>i</span><span>; </span><span>k</span><span>++</span><span>) </span><span>A</span><span>(</span><span>k</span><span>, </span><span>i</span><span>) </span><span>-=</span><span> </span><span>u</span><span>[</span><span>k</span><span>] </span><span>*</span><span> </span><span>d</span><span> </span><span>*</span><span> </span><span>u</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>112</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>113</div></div><div><span>  </span><span>return</span><span> </span><span>dmin</span><span>;</span></div></div><div><div><div>114</div></div><div><span>}</span></div></div><div><div><div>115</div></div><div>
</div></div><div><div><div>116</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Sequential scalar updates for the ESKF: with a diagonal R the measurement</span></div></div><div><div><div>2</div></div><div><span>// components are processed one at a time, so no matrix inverse or Cholesky is needed.</span></div></div><div><div><div>3</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>"kl/eskf.hpp"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// One scalar measurement z = h^T x + v, var(v) = r, innovation y.</span></div></div><div><div><div>9</div></div><div><span>// Updates P (short form (I - K h^T) P unless joseph) and returns dx = K y.</span></div></div><div><div><div>10</div></div><div><span>// Returns false, leaving P unchanged, if the innovation variance is not positive.</span></div></div><div><div><div>11</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span>bool</span><span> </span><span>scalar_update</span><span>(</span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>h</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>, </span></span><span>T</span><span><span> </span><span>y</span><span>, </span></span><span>bool</span><span><span> </span><span>joseph</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>dx</span><span>) {</span></span></div></div><div><div><div>13</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> (</span><span>h</span><span>.</span><span>transpose</span><span>() </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>h</span><span>)(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>+</span><span> </span><span>r</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>(</span><span>s</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>15</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>Ph</span><span> </span><span>=</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>h</span><span>;</span></div></div><div><div><div>16</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>K</span><span>;</span></div></div><div><div><div>17</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>6</span><span>; </span><span>i</span><span>++</span><span>) </span><span>K</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>Ph</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>/</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>A</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>h</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>19</div></div><div><span>  </span><span>P</span><span> </span><span>=</span><span> </span><span>joseph</span><span> </span><span>?</span><span> (</span><span>A</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>A</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> (</span><span>K</span><span> </span><span>*</span><span> </span><span>K</span><span>.</span><span>transpose</span><span>()) </span><span>*</span><span> </span><span>r</span><span>).</span><span>symmetrized</span><span>() </span><span>:</span><span> </span><span>A</span><span> </span><span>*</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>dx</span><span> </span><span>=</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>namespace</span><span><span> </span><span>detail</span><span> {</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>// Injection as in ESKF::inject (duplicated so the ESKF class stays untouched).</span></div></div><div><div><div>27</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>28</div></div><div><span>void</span><span> </span><span>inject</span><span>(</span><span>ESKF</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>f</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>dx</span><span>) {</span></span></div></div><div><div><div>29</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dth</span><span>{{</span><span>dx</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>30</div></div><div><span>  </span><span>f</span><span>.</span><span>q</span><span> </span><span>=</span><span> (</span><span>f</span><span>.</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>dth</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>31</div></div><div><span>  </span><span>f</span><span>.</span><span>b</span><span> </span><span>=</span><span> </span><span>f</span><span>.</span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>3</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>4</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>5</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>32</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>.</span><span>reset</span><span>) {</span></div></div><div><div><div>33</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>G</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>34</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>dth</span><span> </span><span>*</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>));</span></div></div><div><div><div>35</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>36</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>G</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>-=</span><span> </span><span>S</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>37</div></div><div><span>    </span><span>f</span><span>.</span><span>P</span><span> </span><span>=</span><span> </span><span>G</span><span> </span><span>*</span><span> </span><span>f</span><span>.</span><span>P</span><span> </span><span>*</span><span> </span><span>G</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>}</span><span>  // namespace detail</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>// Accelerometer update, one component at a time. All three rows share the</span></div></div><div><div><div>44</div></div><div><span>// linearization point; the nominal state is injected once at the end.</span></div></div><div><div><div>45</div></div><div><span>// Returns false when gated out or on failure (state unchanged).</span></div></div><div><div><div>46</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>47</div></div><div><span>bool</span><span> </span><span>update_accel_seq</span><span>(</span><span>ESKF</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>f</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>, </span></span><span>bool</span><span><span> </span><span>joseph</span><span> </span></span><span>=</span><span> </span><span>true</span><span>) {</span></div></div><div><div><div>48</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>49</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>.</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>&gt;</span><span> </span><span>f</span><span>.</span><span>accel_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>50</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>h</span><span> </span><span>=</span><span> </span><span>f</span><span>.</span><span>q</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>() </span><span>*</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}};</span></div></div><div><div><div>51</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Hm</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>52</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>y</span><span> </span><span>=</span><span> </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> </span><span>norm</span><span>) </span><span>-</span><span> </span><span>h</span><span>;</span></div></div><div><div><div>53</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> (</span><span>f</span><span>.</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>*</span><span> (</span><span>f</span><span>.</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>));</span></div></div><div><div><div>54</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>P</span><span> </span><span>=</span><span> </span><span>f</span><span>.</span><span>P</span><span>;</span></div></div><div><div><div>55</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span>{};</span></div></div><div><div><div>56</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>57</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>hj</span><span>{}, </span><span>dxj</span><span>;</span></div></div><div><div><div>58</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) </span><span>hj</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>Hm</span><span>(</span><span>j</span><span>, </span><span>i</span><span>);</span></div></div><div><div><div>59</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>scalar_update</span><span>(</span><span>P</span><span>, </span><span>hj</span><span>, </span><span>r</span><span>, </span><span>y</span><span>(</span><span>j</span><span>, </span><span>0</span><span>) </span><span>-</span><span> (</span><span>hj</span><span>.</span><span>transpose</span><span>() </span><span>*</span><span> </span><span>dx</span><span>)(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>joseph</span><span>, </span><span>dxj</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>60</div></div><div><span>    </span><span>dx</span><span> </span><span>=</span><span> </span><span>dx</span><span> </span><span>+</span><span> </span><span>dxj</span><span>;</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span>  </span><span>f</span><span>.</span><span>P</span><span> </span><span>=</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>detail::</span><span>inject</span><span>(</span><span>f</span><span>, </span><span>dx</span><span>);</span></div></div><div><div><div>64</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>65</div></div><div><span>}</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>// Heading-only magnetometer update as a single scalar measurement.</span></div></div><div><div><div>68</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>69</div></div><div><span>bool</span><span> </span><span>update_mag_heading_seq</span><span>(</span><span>ESKF</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>f</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>, </span></span><span>bool</span><span><span> </span><span>joseph</span><span> </span></span><span>=</span><span> </span><span>true</span><span>) {</span></div></div><div><div><div>70</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>71</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>.</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>f</span><span>.</span><span>mag_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>72</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>mw</span><span> </span><span>=</span><span> </span><span>f</span><span>.</span><span>q</span><span>.</span><span>dcm</span><span>() </span><span>*</span><span> </span><span>mag</span><span>;</span></div></div><div><div><div>73</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>y</span><span> </span><span>=</span><span> </span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> std::</span><span>atan2</span><span>(</span><span>f</span><span>.</span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>f</span><span>.</span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)));</span></div></div><div><div><div>74</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>h</span><span>{}, </span><span>dx</span><span>;</span></div></div><div><div><div>75</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Hp</span><span> </span><span>=</span><span> </span><span>f</span><span>.</span><span>heading_jacobian</span><span>();</span></div></div><div><div><div>76</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>h</span><span>(</span><span>j</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>Hp</span><span>(</span><span>0</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>77</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>f</span><span>.</span><span>sm</span><span> </span><span>/</span><span> std::</span><span>hypot</span><span>(</span><span>f</span><span>.</span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>f</span><span>.</span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>78</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>P</span><span> </span><span>=</span><span> </span><span>f</span><span>.</span><span>P</span><span>;</span></div></div><div><div><div>79</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>scalar_update</span><span>(</span><span>P</span><span>, </span><span>h</span><span>, </span><span>r</span><span> </span><span>*</span><span> </span><span>r</span><span>, </span><span>y</span><span>, </span><span>joseph</span><span>, </span><span>dx</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>80</div></div><div><span>  </span><span>f</span><span>.</span><span>P</span><span> </span><span>=</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>81</div></div><div><span><span>  </span></span><span>detail::</span><span>inject</span><span>(</span><span>f</span><span>, </span><span>dx</span><span>);</span></div></div><div><div><div>82</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>83</div></div><div><span>}</span></div></div><div><div><div>84</div></div><div>
</div></div><div><div><div>85</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// ESKF whose covariance lives in UD form: Thornton predict, Bierman scalar updates.</span></div></div><div><div><div>2</div></div><div><span>// There is no reset Jacobian (it would not preserve the factored form), so compare</span></div></div><div><div><div>3</div></div><div><span>// with ESKF(reset = false).</span></div></div><div><div><div>4</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/eskf.hpp"</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/ud.hpp"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>11</div></div><div><span>class</span><span> </span><span>EskfUD</span><span> {</span></div></div><div><div><div>12</div></div><div><span>public:</span></div></div><div><div><div>13</div></div><div><span>  </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>q</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>b</span><span>{};</span></div></div><div><div><div>15</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>U</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>6</span><span>&gt; </span><span>D</span><span>;</span></div></div><div><div><div>17</div></div><div><span>  </span><span>T</span><span> </span><span>sg</span><span>, </span><span>sbw</span><span>, </span><span>sa</span><span>, </span><span>sm</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_w</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>T</span><span> </span><span>accel_gate</span><span>, </span><span>mag_gate</span><span>;</span><span>  // negative disables the gate</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>// p0_diag: the initial covariance must be diagonal, so U = I and D = p0_diag.</span></div></div><div><div><div>22</div></div><div><span>  </span><span>EskfUD</span><span>(</span><span>T</span><span><span> </span><span>sigma_g</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_bw</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_a</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_m</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m_w</span><span>, </span></span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q0</span><span>,</span></span></div></div><div><div><div>23</div></div><div><span>         </span><span>const</span><span> std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>6</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>p0_diag</span><span>, </span></span><span>T</span><span><span> </span><span>accel_gate</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>), </span><span>T</span><span><span> </span><span>mag_gate</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0.1</span><span>))</span></div></div><div><div><div>24</div></div><div><span><span>      </span></span><span>: </span><span>q</span><span>(</span><span>q0</span><span>), </span><span>D</span><span>(</span><span>p0_diag</span><span>), </span><span>sg</span><span>(</span><span>sigma_g</span><span>), </span><span>sbw</span><span>(</span><span>sigma_bw</span><span>), </span><span>sa</span><span>(</span><span>sigma_a</span><span>), </span><span>sm</span><span>(</span><span>sigma_m</span><span>), </span><span>m_w</span><span>(</span><span>m_w</span><span>),</span></div></div><div><div><div>25</div></div><div><span>        </span><span>accel_gate</span><span>(</span><span>accel_gate</span><span>), </span><span>mag_gate</span><span>(</span><span>mag_gate</span><span>) {}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>P</span><span>() </span><span>const</span><span> { </span><span>return</span><span> </span><span>ud_to_p</span><span>(</span><span>U</span><span>, </span><span>D</span><span>); }</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dq</span><span> </span><span>=</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>((</span><span>gyro</span><span> </span><span>-</span><span> </span><span>b</span><span>) </span><span>*</span><span> </span><span>dt</span><span>);</span></div></div><div><div><div>31</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Ct</span><span> </span><span>=</span><span> </span><span>dq</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>32</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>F</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>6</span><span>&gt; </span><span>qd</span><span>;</span></div></div><div><div><div>34</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>35</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>F</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Ct</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>36</div></div><div><span>      </span><span>F</span><span>(</span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>=</span><span> </span><span>-</span><span>dt</span><span>;</span></div></div><div><div><div>37</div></div><div><span>      </span><span>qd</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>dt</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>38</div></div><div><span>      </span><span>qd</span><span>[</span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>] </span><span>=</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>q</span><span> </span><span>*</span><span> </span><span>dq</span><span>;</span><span>  // not normalized here</span></div></div><div><div><div>41</div></div><div><span>    </span><span>thornton_predict</span><span>(</span><span>U</span><span>, </span><span>D</span><span>, </span><span>F</span><span>, </span><span>qd</span><span>);</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>// Gravity-direction update as three sequential Bierman updates.</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>// Returns false when gated out.</span></div></div><div><div><div>46</div></div><div><span>  </span><span>bool</span><span> </span><span>update_accel</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>) {</span></span></div></div><div><div><div>47</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>48</div></div><div><span>    </span><span>if</span><span> (</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>&gt;</span><span> </span><span>accel_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>49</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>h</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>() </span><span>*</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}};</span></div></div><div><div><div>50</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Hm</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>51</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>y</span><span> </span><span>=</span><span> </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> </span><span>norm</span><span>) </span><span>-</span><span> </span><span>h</span><span>;</span></div></div><div><div><div>52</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>*</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>));</span></div></div><div><div><div>53</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span>{};</span></div></div><div><div><div>54</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>55</div></div><div><span>      </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>hj</span><span>{};</span></div></div><div><div><div>56</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) </span><span>hj</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>Hm</span><span>(</span><span>j</span><span>, </span><span>i</span><span>);</span></div></div><div><div><div>57</div></div><div><span>      </span><span>dx</span><span> </span><span>=</span><span> </span><span>dx</span><span> </span><span>+</span><span> </span><span>bierman_update</span><span>(</span><span>U</span><span>, </span><span>D</span><span>, </span><span>hj</span><span>, </span><span>r</span><span>, </span><span>y</span><span>(</span><span>j</span><span>, </span><span>0</span><span>) </span><span>-</span><span> (</span><span>hj</span><span>.</span><span>transpose</span><span>() </span><span>*</span><span> </span><span>dx</span><span>)(</span><span>0</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span>    </span><span>inject</span><span>(</span><span>dx</span><span>);</span></div></div><div><div><div>60</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>// Heading-only magnetometer update as one Bierman update. Returns false when gated out.</span></div></div><div><div><div>64</div></div><div><span>  </span><span>bool</span><span> </span><span>update_mag_heading</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>) {</span></span></div></div><div><div><div>65</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>66</div></div><div><span>    </span><span>if</span><span> (</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>mag_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>67</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>C</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>();</span></div></div><div><div><div>68</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>mw</span><span> </span><span>=</span><span> </span><span>C</span><span> </span><span>*</span><span> </span><span>mag</span><span>;</span></div></div><div><div><div>69</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>y</span><span> </span><span>=</span><span> </span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> std::</span><span>atan2</span><span>(</span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)));</span></div></div><div><div><div>70</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>mx</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>my</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mz</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>2</span><span>, </span><span>0</span><span>), </span><span>mh2</span><span> </span><span>=</span><span> </span><span>mx</span><span> </span><span>*</span><span> </span><span>mx</span><span> </span><span>+</span><span> </span><span>my</span><span> </span><span>*</span><span> </span><span>my</span><span>;</span></div></div><div><div><div>71</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Hp</span><span> </span><span>=</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>mx</span><span> </span><span>*</span><span> </span><span>mz</span><span> </span><span>/</span><span> </span><span>mh2</span><span>, </span><span>my</span><span> </span><span>*</span><span> </span><span>mz</span><span> </span><span>/</span><span> </span><span>mh2</span><span>, </span><span>T</span><span>(</span><span>-</span><span>1</span><span>)}} </span><span>*</span><span> </span><span>C</span><span>;</span></div></div><div><div><div>72</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>h</span><span>{};</span></div></div><div><div><div>73</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>h</span><span>(</span><span>j</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>Hp</span><span>(</span><span>0</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>74</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>sm</span><span> </span><span>/</span><span> std::</span><span>hypot</span><span>(</span><span>mx</span><span>, </span><span>my</span><span>);</span></div></div><div><div><div>75</div></div><div><span>    </span><span>inject</span><span>(</span><span>bierman_update</span><span>(</span><span>U</span><span>, </span><span>D</span><span>, </span><span>h</span><span>, </span><span>r</span><span> </span><span>*</span><span> </span><span>r</span><span>, </span><span>y</span><span>));</span></div></div><div><div><div>76</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>77</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>78</div></div><div>
</div></div><div><div><div>79</div></div><div><span>private:</span></div></div><div><div><div>80</div></div><div><span>  </span><span>void</span><span> </span><span>inject</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>dx</span><span>) {</span></span></div></div><div><div><div>81</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> (</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)}})).</span><span>normalized</span><span>();</span></div></div><div><div><div>82</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>3</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>4</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>5</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>83</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>84</div></div><div><span>};</span></div></div><div><div><div>85</div></div><div>
</div></div><div><div><div>86</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：rust/src/ud.rs、eskf_seq.rs、eskf_ud.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! UD covariance factorization, P = U D U^T, U unit upper triangular, D diagonal.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Bierman's scalar measurement update and Thornton's time update (modified</span></div></div><div><div><div>4</div></div><div><span>//! weighted Gram-Schmidt). See G. J. Bierman, "Factorization Methods for Discrete</span></div></div><div><div><div>5</div></div><div><span>//! Sequential Estimation" (1977) and Grewal and Andrews, "Kalman Filtering: Theory</span></div></div><div><div><div>6</div></div><div><span>//! and Practice".</span></div></div><div><div><div>7</div></div><div><span>#![allow(clippy</span><span>::</span><span>needless_range_loop)]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::</span></span><span>Mat</span><span>;</span></div></div><div><div><div>10</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::</span></span><span>Real</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>/// P (symmetric positive definite) -&gt; (U, D). `None` if a pivot is not positive.</span></div></div><div><div><div>13</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>ud_factor</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt;(</span><span>p</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;) -&gt; </span><span>Option</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;, [</span><span>Real</span><span>; </span><span>N</span><span>])&gt; {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>u</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>d</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>a</span><span><span> </span><span>=</span><span> *</span></span><span>p</span><span>;</span></div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> (</span><span>0</span><span>..</span><span>N</span><span>).</span><span>rev</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>d</span><span>[</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>a</span><span>.m[</span><span>j</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>19</div></div><div><span>        </span><span>if</span><span> </span><span>d</span><span>[</span><span>j</span><span>].</span><span>is_nan</span><span><span>() </span><span>||</span><span> </span></span><span>d</span><span>[</span><span>j</span><span><span>] </span><span>&lt;=</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>u</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>a</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>] / </span><span>d</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>26</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..=</span><span>i</span><span> {</span></div></div><div><div><div>27</div></div><div><span>                </span><span>a</span><span>.m[</span><span>k</span><span>][</span><span>i</span><span><span>] </span><span>-=</span><span> </span></span><span>u</span><span>.m[</span><span>k</span><span>][</span><span>j</span><span>] * </span><span>d</span><span>[</span><span>j</span><span>] * </span><span>u</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>    </span><span>Some</span><span>((</span><span>u</span><span>, </span><span>d</span><span>))</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>/// U diag(D) U^T</span></div></div><div><div><div>35</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>ud_to_p</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt;(</span><span>u</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;, </span><span>d</span><span>: &amp;[</span><span>Real</span><span>; </span><span>N</span><span>]) -&gt; </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt; {</span></div></div><div><div><div>36</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>ud</span><span><span> </span><span>=</span><span> *</span></span><span>u</span><span>;</span></div></div><div><div><div>37</div></div><div><span>    </span><span>for</span><span> </span><span>row</span><span> </span><span>in</span><span> </span><span>ud</span><span>.m.</span><span>iter_mut</span><span>() {</span></div></div><div><div><div>38</div></div><div><span>        </span><span>for</span><span> (</span><span>v</span><span>, </span><span>dj</span><span>) </span><span>in</span><span> </span><span>row</span><span>.</span><span>iter_mut</span><span>().</span><span>zip</span><span>(</span><span>d</span><span>) {</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>*</span><span>v</span><span><span> </span><span>*=</span><span> </span></span><span>dj</span><span>;</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span>    </span><span>ud</span><span> * </span><span>u</span><span>.</span><span>transpose</span><span>()</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>/// Scalar measurement update of (U, D) in place; returns the correction dx = K y.</span></div></div><div><div><div>46</div></div><div><span>/// Measurement z = h x + v, var(v) = r, innovation y. Factored Joseph-equivalent</span></div></div><div><div><div>47</div></div><div><span>/// form: P stays positive definite by construction.</span></div></div><div><div><div>48</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>bierman_update</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>49</div></div><div><span>    </span><span>u</span><span>: &amp;</span><span>mut</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>50</div></div><div><span>    </span><span>d</span><span>: &amp;</span><span>mut</span><span> [</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>51</div></div><div><span>    </span><span>h</span><span>: &amp;[</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>52</div></div><div><span>    </span><span>r</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>53</div></div><div><span>    </span><span>y</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>54</div></div><div><span>) -&gt; [</span><span>Real</span><span>; </span><span>N</span><span>] {</span></div></div><div><div><div>55</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>56</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>g</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>57</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>58</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>59</div></div><div><span>            </span><span>f</span><span>[</span><span>j</span><span><span>] </span><span>+=</span><span> </span></span><span>u</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>] * </span><span>h</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>60</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>61</div></div><div><span>        </span><span>g</span><span>[</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>d</span><span>[</span><span>j</span><span>] * </span><span>f</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>b</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>64</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>alpha</span><span><span> </span><span>=</span><span> </span></span><span>r</span><span> + </span><span>f</span><span>[</span><span>0</span><span>] * </span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>65</div></div><div><span>    </span><span>d</span><span>[</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>d</span><span>[</span><span>0</span><span>] * </span><span>r</span><span> / </span><span>alpha</span><span>;</span></div></div><div><div><div>66</div></div><div><span>    </span><span>b</span><span>[</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>g</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>67</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>1</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>68</div></div><div><span>        </span><span>let</span><span> </span><span>gamma</span><span><span> </span><span>=</span><span> </span></span><span>alpha</span><span>;</span></div></div><div><div><div>69</div></div><div><span>        </span><span>alpha</span><span><span> </span><span>+=</span><span> </span></span><span>f</span><span>[</span><span>j</span><span>] * </span><span>g</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>70</div></div><div><span>        </span><span>d</span><span>[</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>d</span><span>[</span><span>j</span><span>] * </span><span>gamma</span><span> / </span><span>alpha</span><span>;</span></div></div><div><div><div>71</div></div><div><span>        </span><span>let</span><span> </span><span>lam</span><span><span> </span><span>=</span><span> -</span></span><span>f</span><span>[</span><span>j</span><span>] / </span><span>gamma</span><span>;</span></div></div><div><div><div>72</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>73</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>u</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>74</div></div><div><span>            </span><span>u</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>s</span><span> + </span><span>b</span><span>[</span><span>i</span><span>] * </span><span>lam</span><span>;</span></div></div><div><div><div>75</div></div><div><span>            </span><span>b</span><span>[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>s</span><span> * </span><span>g</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>76</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>77</div></div><div><span>        </span><span>b</span><span>[</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>g</span><span>[</span><span>j</span><span>];</span></div></div><div><div><div>78</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>79</div></div><div><span>    </span><span>b</span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>bi</span><span><span>|</span><span> </span></span><span>bi</span><span> / </span><span>alpha</span><span> * </span><span>y</span><span>)</span></div></div><div><div><div>80</div></div><div><span>}</span></div></div><div><div><div>81</div></div><div>
</div></div><div><div><div>82</div></div><div><span>/// Time update P &lt;- F P F^T + diag(qdiag) in factored form, in place.</span></div></div><div><div><div>83</div></div><div><span>/// Orthogonalizes the rows of W = [F U | I] with weights [D, qdiag].</span></div></div><div><div><div>84</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>thornton_predict</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>85</div></div><div><span>    </span><span>u</span><span>: &amp;</span><span>mut</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>86</div></div><div><span>    </span><span>d</span><span>: &amp;</span><span>mut</span><span> [</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>87</div></div><div><span>    </span><span>f</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>88</div></div><div><span>    </span><span>qdiag</span><span>: &amp;[</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>89</div></div><div><span>) {</span></div></div><div><div><div>90</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>wa</span><span><span> </span><span>=</span><span> *</span></span><span>f</span><span> * *</span><span>u</span><span>;</span></div></div><div><div><div>91</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>wb</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>92</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>un</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>93</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>dn</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>94</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> (</span><span>0</span><span>..</span><span>N</span><span>).</span><span>rev</span><span>() {</span></div></div><div><div><div>95</div></div><div><span>        </span><span>let</span><span> </span><span>ca</span><span>: [</span><span>Real</span><span>; </span><span>N</span><span><span>] </span><span>=</span><span> </span><span>core</span><span>::</span><span>array</span><span>::</span></span><span>from_fn</span><span><span>(</span><span>|</span></span><span>k</span><span><span>|</span><span> </span></span><span>d</span><span>[</span><span>k</span><span>] * </span><span>wa</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>]);</span></div></div><div><div><div>96</div></div><div><span>        </span><span>let</span><span> </span><span>cb</span><span>: [</span><span>Real</span><span>; </span><span>N</span><span><span>] </span><span>=</span><span> </span><span>core</span><span>::</span><span>array</span><span>::</span></span><span>from_fn</span><span><span>(</span><span>|</span></span><span>k</span><span><span>|</span><span> </span></span><span>qdiag</span><span>[</span><span>k</span><span>] * </span><span>wb</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>]);</span></div></div><div><div><div>97</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>dj</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>98</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>99</div></div><div><span>            </span><span>dj</span><span><span> </span><span>+=</span><span> </span></span><span>wa</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>] * </span><span>ca</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>100</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>101</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>102</div></div><div><span>            </span><span>dj</span><span><span> </span><span>+=</span><span> </span></span><span>wb</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>] * </span><span>cb</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>103</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>104</div></div><div><span>        </span><span>dn</span><span>[</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>dj</span><span>;</span></div></div><div><div><div>105</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>106</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>107</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>108</div></div><div><span>                </span><span>s</span><span><span> </span><span>+=</span><span> </span></span><span>wa</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>] * (</span><span>ca</span><span>[</span><span>k</span><span>] / </span><span>dj</span><span>);</span></div></div><div><div><div>109</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>110</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>111</div></div><div><span>                </span><span>s</span><span><span> </span><span>+=</span><span> </span></span><span>wb</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>] * (</span><span>cb</span><span>[</span><span>k</span><span>] / </span><span>dj</span><span>);</span></div></div><div><div><div>112</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>113</div></div><div><span>            </span><span>un</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>114</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>115</div></div><div><span>                </span><span>wa</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span> * </span><span>wa</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>116</div></div><div><span>                </span><span>wb</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span> * </span><span>wb</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>117</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>118</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>119</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>120</div></div><div><span><span>    </span></span><span>*</span><span>u</span><span><span> </span><span>=</span><span> </span></span><span>un</span><span>;</span></div></div><div><div><div>121</div></div><div><span><span>    </span></span><span>*</span><span>d</span><span><span> </span><span>=</span><span> </span></span><span>dn</span><span>;</span></div></div><div><div><div>122</div></div><div><span>}</span></div></div><div><div><div>123</div></div><div>
</div></div><div><div><div>124</div></div><div><span>/// Smallest pivot of the UD elimination of P (reads the upper triangle only).</span></div></div><div><div><div>125</div></div><div><span>/// Unlike `ud_factor` this never fails: a value &lt;= 0 means P is not positive definite.</span></div></div><div><div><div>126</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>min_pivot</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt;(</span><span>p</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>127</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>a</span><span><span> </span><span>=</span><span> *</span></span><span>p</span><span>;</span></div></div><div><div><div>128</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>dmin</span><span><span> </span><span>=</span><span> </span></span><span>Real</span><span>::</span><span>INFINITY</span><span>;</span></div></div><div><div><div>129</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> (</span><span>0</span><span>..</span><span>N</span><span>).</span><span>rev</span><span>() {</span></div></div><div><div><div>130</div></div><div><span>        </span><span>let</span><span> </span><span>d</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span>.m[</span><span>j</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>131</div></div><div><span>        </span><span>if</span><span> </span><span>d</span><span><span> </span><span>&lt;</span><span> </span></span><span>dmin</span><span> {</span></div></div><div><div><div>132</div></div><div><span>            </span><span>dmin</span><span><span> </span><span>=</span><span> </span></span><span>d</span><span>;</span></div></div><div><div><div>133</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>134</div></div><div><span>        </span><span>if</span><span> </span><span>d</span><span><span> </span><span>==</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>135</div></div><div><span>            </span><span>continue</span><span>;</span></div></div><div><div><div>136</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>137</div></div><div><span>        </span><span>let</span><span> </span><span>u</span><span>: [</span><span>Real</span><span>; </span><span>N</span><span><span>] </span><span>=</span><span> </span><span>core</span><span>::</span><span>array</span><span>::</span></span><span>from_fn</span><span><span>(</span><span>|</span></span><span>k</span><span><span>|</span><span> </span></span><span>if</span><span> </span><span>k</span><span><span> </span><span>&lt;</span><span> </span></span><span>j</span><span> { </span><span>a</span><span>.m[</span><span>k</span><span>][</span><span>j</span><span>] / </span><span>d</span><span> } </span><span>else</span><span> { </span><span>0.0</span><span> });</span></div></div><div><div><div>138</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>139</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..=</span><span>i</span><span> {</span></div></div><div><div><div>140</div></div><div><span>                </span><span>a</span><span>.m[</span><span>k</span><span>][</span><span>i</span><span><span>] </span><span>-=</span><span> </span></span><span>u</span><span>[</span><span>k</span><span>] * </span><span>d</span><span> * </span><span>u</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>141</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>142</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>143</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>144</div></div><div><span>    </span><span>dmin</span></div></div><div><div><div>145</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Sequential scalar updates for the ESKF (diagonal R, no Cholesky, no inverse).</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#![allow(clippy</span><span>::</span><span>needless_range_loop)]</span></div></div><div><div><div>4</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>ekf_att</span><span>::</span></span><span>G</span><span>;</span></div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>eskf</span><span>::{</span></span><span>Eskf</span><span>, wrap_pi};</span></div></div><div><div><div>6</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::</span></span><span>Mat</span><span>;</span></div></div><div><div><div>7</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>quat</span><span>::{</span></span><span>Quat</span><span>, skew};</span></div></div><div><div><div>8</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, atan2, fabs, sqrt};</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>/// One scalar measurement with row `h`: returns dx = K y and replaces `p`.</span></div></div><div><div><div>11</div></div><div><span>/// `joseph = false` is the short form (I - K h) P. `None` if S &lt;= 0.</span></div></div><div><div><div>12</div></div><div><span>fn</span><span> </span><span>scalar_update</span><span>(</span></div></div><div><div><div>13</div></div><div><span>    </span><span>p</span><span>: &amp;</span><span>mut</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>h</span><span>: &amp;[</span><span>Real</span><span>; </span><span>6</span><span>],</span></div></div><div><div><div>15</div></div><div><span>    </span><span>r</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>y</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>joseph</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>18</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;[</span><span>Real</span><span>; </span><span>6</span><span>]&gt; {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>hr</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>1</span><span>, </span><span>6</span><span>&gt; { </span><span>m</span><span>: [*</span><span>h</span><span>] };</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>ph</span><span><span> </span><span>=</span><span> *</span></span><span>p</span><span> * </span><span>hr</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> (</span></span><span>hr</span><span> * </span><span>ph</span><span>).m[</span><span>0</span><span>][</span><span>0</span><span>] + </span><span>r</span><span>;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>if</span><span> </span><span>s</span><span>.</span><span>is_nan</span><span><span>() </span><span>||</span><span> </span></span><span>s</span><span><span> </span><span>&lt;=</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>    </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>ph</span><span> * (</span><span>1.0</span><span> / </span><span>s</span><span>);</span></div></div><div><div><div>26</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>identity</span><span>() - </span><span>k</span><span> * </span><span>hr</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>*</span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>joseph</span><span> {</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>(</span><span>a</span><span> * *</span><span>p</span><span> * </span><span>a</span><span>.</span><span>transpose</span><span>() + (</span><span>k</span><span> * </span><span>k</span><span>.</span><span>transpose</span><span>()) * </span><span>r</span><span>).</span><span>symmetrized</span><span>()</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>a</span><span> * *</span><span>p</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>32</div></div><div><span>    </span><span>Some</span><span>(</span><span>k</span><span>.m.</span><span>map</span><span><span>(</span><span>|</span></span><span>row</span><span><span>|</span><span> </span></span><span>row</span><span>[</span><span>0</span><span>] * </span><span>y</span><span>))</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>/// Inject the error state (no reset Jacobian is applied when `f.reset` is false).</span></div></div><div><div><div>36</div></div><div><span>fn</span><span> </span><span>inject</span><span>(</span><span>f</span><span>: &amp;</span><span>mut</span><span> </span><span>Eskf</span><span>, </span><span>dx</span><span>: &amp;[</span><span>Real</span><span>; </span><span>6</span><span>]) {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>f</span><span><span>.q </span><span>=</span><span> (</span></span><span>f</span><span>.q * </span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>dx</span><span>[</span><span>0</span><span>], </span><span>dx</span><span>[</span><span>1</span><span>], </span><span>dx</span><span>[</span><span>2</span><span>]])).</span><span>normalized</span><span>();</span></div></div><div><div><div>38</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>39</div></div><div><span>        </span><span>f</span><span>.b[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>dx</span><span>[</span><span>3</span><span> + </span><span>i</span><span>];</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span>    </span><span>if</span><span> </span><span>f</span><span>.reset {</span></div></div><div><div><div>42</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>([</span><span>0.5</span><span> * </span><span>dx</span><span>[</span><span>0</span><span>], </span><span>0.5</span><span> * </span><span>dx</span><span>[</span><span>1</span><span>], </span><span>0.5</span><span> * </span><span>dx</span><span>[</span><span>2</span><span>]]);</span></div></div><div><div><div>43</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>g</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>44</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>45</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>46</div></div><div><span>                </span><span>g</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span>        </span><span>f</span><span><span>.p </span><span>=</span><span> </span></span><span>g</span><span> * </span><span>f</span><span>.p * </span><span>g</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>/// Accelerometer update processing the three components one at a time. All rows</span></div></div><div><div><div>54</div></div><div><span>/// share one linearization point; the nominal state is injected once at the end.</span></div></div><div><div><div>55</div></div><div><span>/// False when gated out or on failure (state unchanged).</span></div></div><div><div><div>56</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>update_accel_seq</span><span>(</span><span>f</span><span>: &amp;</span><span>mut</span><span> </span><span>Eskf</span><span>, </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>joseph</span><span>: </span><span>bool</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>57</div></div><div><span>    </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] * </span><span>accel</span><span>[</span><span>0</span><span>] + </span><span>accel</span><span>[</span><span>1</span><span>] * </span><span>accel</span><span>[</span><span>1</span><span>] + </span><span>accel</span><span>[</span><span>2</span><span>] * </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>58</div></div><div><span>    </span><span>if</span><span> </span><span>f</span><span><span>.accel_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>G</span><span>) &gt; </span><span>f</span><span>.accel_gate {</span></div></div><div><div><div>59</div></div><div><span>        </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>61</div></div><div><span>    </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>f</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>62</div></div><div><span>    </span><span>let</span><span> </span><span>h</span><span><span> </span><span>=</span><span> [-</span></span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>1</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>2</span><span>]];</span></div></div><div><div><div>63</div></div><div><span>    </span><span>let</span><span> </span><span>hm</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>64</div></div><div><span>    </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> (</span></span><span>f</span><span>.sa / </span><span>G</span><span>) * (</span><span>f</span><span>.sa / </span><span>G</span><span>);</span></div></div><div><div><div>65</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>f</span><span>.p;</span></div></div><div><div><div>66</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>dx</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>6</span><span>];</span></div></div><div><div><div>67</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>68</div></div><div><span>        </span><span>let</span><span> </span><span>hj</span><span><span> </span><span>=</span><span> [</span></span><span>hm</span><span>.m[</span><span>j</span><span>][</span><span>0</span><span>], </span><span>hm</span><span>.m[</span><span>j</span><span>][</span><span>1</span><span>], </span><span>hm</span><span>.m[</span><span>j</span><span>][</span><span>2</span><span>], </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>];</span></div></div><div><div><div>69</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>hdx</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>70</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>6</span><span> {</span></div></div><div><div><div>71</div></div><div><span>            </span><span>hdx</span><span><span> </span><span>+=</span><span> </span></span><span>hj</span><span>[</span><span>k</span><span>] * </span><span>dx</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>73</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>dxj</span><span><span>) </span><span>=</span><span> </span></span><span>scalar_update</span><span>(&amp;</span><span>mut</span><span> </span><span>p</span><span>, &amp;</span><span>hj</span><span>, </span><span>r</span><span>, </span><span>accel</span><span>[</span><span>j</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>j</span><span>] - </span><span>hdx</span><span>, </span><span>joseph</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>74</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>75</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>76</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>6</span><span> {</span></div></div><div><div><div>77</div></div><div><span>            </span><span>dx</span><span>[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>dxj</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>78</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>79</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>80</div></div><div><span>    </span><span>f</span><span><span>.p </span><span>=</span><span> </span></span><span>p</span><span>;</span></div></div><div><div><div>81</div></div><div><span>    </span><span>inject</span><span>(</span><span>f</span><span>, &amp;</span><span>dx</span><span>);</span></div></div><div><div><div>82</div></div><div><span>    </span><span>true</span></div></div><div><div><div>83</div></div><div><span>}</span></div></div><div><div><div>84</div></div><div>
</div></div><div><div><div>85</div></div><div><span>/// Heading-only magnetometer update with one scalar measurement.</span></div></div><div><div><div>86</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>update_mag_heading_seq</span><span>(</span><span>f</span><span>: &amp;</span><span>mut</span><span> </span><span>Eskf</span><span>, </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>joseph</span><span>: </span><span>bool</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>87</div></div><div><span>    </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>mag</span><span>[</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>mag</span><span>[</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>88</div></div><div><span>    </span><span>if</span><span> </span><span>f</span><span><span>.mag_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>1.0</span><span>) &gt; </span><span>f</span><span>.mag_gate {</span></div></div><div><div><div>89</div></div><div><span>        </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>90</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>91</div></div><div><span>    </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>f</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>92</div></div><div><span>    </span><span>let</span><span> </span><span>mw</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>i</span><span>: </span><span>usize</span><span><span>|</span><span> </span></span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>93</div></div><div><span>    </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>wrap_pi</span><span>(</span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>)) - </span><span>atan2</span><span>(</span><span>f</span><span>.m_w[</span><span>1</span><span>], </span><span>f</span><span>.m_w[</span><span>0</span><span>]));</span></div></div><div><div><div>94</div></div><div><span>    </span><span>let</span><span> </span><span>hj</span><span><span> </span><span>=</span><span> </span></span><span>f</span><span>.</span><span>heading_jacobian</span><span>();</span></div></div><div><div><div>95</div></div><div><span>    </span><span>let</span><span> </span><span>h</span><span><span> </span><span>=</span><span> [</span></span><span>hj</span><span>[</span><span>0</span><span>], </span><span>hj</span><span>[</span><span>1</span><span>], </span><span>hj</span><span>[</span><span>2</span><span>], </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>];</span></div></div><div><div><div>96</div></div><div><span>    </span><span>let</span><span> </span><span>mh</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>f</span><span>.m_w[</span><span>0</span><span>] * </span><span>f</span><span>.m_w[</span><span>0</span><span>] + </span><span>f</span><span>.m_w[</span><span>1</span><span>] * </span><span>f</span><span>.m_w[</span><span>1</span><span>]);</span></div></div><div><div><div>97</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>f</span><span>.p;</span></div></div><div><div><div>98</div></div><div><span>    </span><span>let</span><span> </span><span>Some</span><span>(</span><span>dx</span><span><span>) </span><span>=</span><span> </span></span><span>scalar_update</span><span>(&amp;</span><span>mut</span><span> </span><span>p</span><span>, &amp;</span><span>h</span><span>, (</span><span>f</span><span>.sm / </span><span>mh</span><span>) * (</span><span>f</span><span>.sm / </span><span>mh</span><span>), </span><span>y</span><span>, </span><span>joseph</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>99</div></div><div><span>        </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>100</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>101</div></div><div><span>    </span><span>f</span><span><span>.p </span><span>=</span><span> </span></span><span>p</span><span>;</span></div></div><div><div><div>102</div></div><div><span>    </span><span>inject</span><span>(</span><span>f</span><span>, &amp;</span><span>dx</span><span>);</span></div></div><div><div><div>103</div></div><div><span>    </span><span>true</span></div></div><div><div><div>104</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! ESKF whose covariance lives in UD form: Thornton predict, Bierman scalar updates.</span></div></div><div><div><div>2</div></div><div><span>//! No reset Jacobian (it would not preserve the factored form).</span></div></div><div><div><div>3</div></div><div><span>#![allow(clippy</span><span>::</span><span>needless_range_loop)]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>ekf_att</span><span>::</span></span><span>G</span><span>;</span></div></div><div><div><div>6</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>eskf</span><span>::wrap_pi;</span></span></div></div><div><div><div>7</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::</span></span><span>Mat</span><span>;</span></div></div><div><div><div>8</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>quat</span><span>::{</span></span><span>Quat</span><span>, skew};</span></div></div><div><div><div>9</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, atan2, fabs, sqrt};</span></div></div><div><div><div>10</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>ud</span><span>::{bierman_update, thornton_predict, ud_to_p};</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>EskfUd</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>q</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pub</span><span> </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>u</span><span>: </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>d</span><span>: [</span><span>Real</span><span>; </span><span>6</span><span>],</span></div></div><div><div><div>17</div></div><div><span>    </span><span>sg</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>18</div></div><div><span>    </span><span>sbw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>sa</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>20</div></div><div><span>    </span><span>sm</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>21</div></div><div><span>    </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>22</div></div><div><span>    </span><span>/// Gates on | |f| - g | (m/s^2) and | |m| - 1 |; negative disables gating.</span></div></div><div><div><div>23</div></div><div><span>    </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>24</div></div><div><span>    </span><span>mag_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>impl</span><span> </span><span>EskfUd</span><span> {</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>#[allow(clippy</span><span>::</span><span>too_many_arguments)]</span></div></div><div><div><div>29</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span></div></div><div><div><div>30</div></div><div><span>        </span><span>q0</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>31</div></div><div><span>        </span><span>p0_diag</span><span>: [</span><span>Real</span><span>; </span><span>6</span><span>],</span></div></div><div><div><div>32</div></div><div><span>        </span><span>sigma_g</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>33</div></div><div><span>        </span><span>sigma_bw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>34</div></div><div><span>        </span><span>sigma_a</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>35</div></div><div><span>        </span><span>sigma_m</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>36</div></div><div><span>        </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>37</div></div><div><span>        </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>38</div></div><div><span>        </span><span>mag_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>40</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>41</div></div><div><span>            </span><span>q</span><span>: </span><span>q0</span><span>,</span></div></div><div><div><div>42</div></div><div><span>            </span><span>b</span><span>: [</span><span>0.0</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>43</div></div><div><span>            </span><span>u</span><span>: </span><span>Mat</span><span>::</span><span>identity</span><span>(),</span></div></div><div><div><div>44</div></div><div><span>            </span><span>d</span><span>: </span><span>p0_diag</span><span>,</span></div></div><div><div><div>45</div></div><div><span>            </span><span>sg</span><span>: </span><span>sigma_g</span><span>,</span></div></div><div><div><div>46</div></div><div><span>            </span><span>sbw</span><span>: </span><span>sigma_bw</span><span>,</span></div></div><div><div><div>47</div></div><div><span>            </span><span>sa</span><span>: </span><span>sigma_a</span><span>,</span></div></div><div><div><div>48</div></div><div><span>            </span><span>sm</span><span>: </span><span>sigma_m</span><span>,</span></div></div><div><div><div>49</div></div><div><span>            </span><span>m_w</span><span>,</span></div></div><div><div><div>50</div></div><div><span>            </span><span>accel_gate</span><span>,</span></div></div><div><div><div>51</div></div><div><span>            </span><span>mag_gate</span><span>,</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>    </span><span>/// The covariance P = U D U^T.</span></div></div><div><div><div>56</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>p</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt; {</span></div></div><div><div><div>57</div></div><div><span>        </span><span>ud_to_p</span><span>(&amp;</span><span>self</span><span>.u, &amp;</span><span>self</span><span>.d)</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>61</div></div><div><span>        </span><span>let</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>62</div></div><div><span>            </span><span>gyro</span><span>[</span><span>0</span><span>] - </span><span>self</span><span>.b[</span><span>0</span><span>],</span></div></div><div><div><div>63</div></div><div><span>            </span><span>gyro</span><span>[</span><span>1</span><span>] - </span><span>self</span><span>.b[</span><span>1</span><span>],</span></div></div><div><div><div>64</div></div><div><span>            </span><span>gyro</span><span>[</span><span>2</span><span>] - </span><span>self</span><span>.b[</span><span>2</span><span>],</span></div></div><div><div><div>65</div></div><div><span><span>        </span></span><span>];</span></div></div><div><div><div>66</div></div><div><span>        </span><span>let</span><span> </span><span>dq</span><span><span> </span><span>=</span><span> </span></span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>w</span><span>[</span><span>0</span><span>] * </span><span>dt</span><span>, </span><span>w</span><span>[</span><span>1</span><span>] * </span><span>dt</span><span>, </span><span>w</span><span>[</span><span>2</span><span>] * </span><span>dt</span><span>]);</span></div></div><div><div><div>67</div></div><div><span>        </span><span>let</span><span> </span><span>ct</span><span><span> </span><span>=</span><span> </span></span><span>dq</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>68</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>69</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>70</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>71</div></div><div><span>                </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>72</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>73</div></div><div><span>            </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>3</span><span> + </span><span>i</span><span><span>] </span><span>=</span><span> -</span></span><span>dt</span><span>;</span></div></div><div><div><div>74</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>75</div></div><div><span>        </span><span>let</span><span> </span><span>qd</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>76</div></div><div><span>            </span><span>self</span><span>.sg * </span><span>self</span><span>.sg * (</span><span>dt</span><span> * </span><span>dt</span><span>),</span></div></div><div><div><div>77</div></div><div><span>            </span><span>self</span><span>.sg * </span><span>self</span><span>.sg * (</span><span>dt</span><span> * </span><span>dt</span><span>),</span></div></div><div><div><div>78</div></div><div><span>            </span><span>self</span><span>.sg * </span><span>self</span><span>.sg * (</span><span>dt</span><span> * </span><span>dt</span><span>),</span></div></div><div><div><div>79</div></div><div><span>            </span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>,</span></div></div><div><div><div>80</div></div><div><span>            </span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>,</span></div></div><div><div><div>81</div></div><div><span>            </span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>,</span></div></div><div><div><div>82</div></div><div><span><span>        </span></span><span>];</span></div></div><div><div><div>83</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>self</span><span>.q * </span><span>dq</span><span>;</span></div></div><div><div><div>84</div></div><div><span>        </span><span>thornton_predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.u, &amp;</span><span>mut</span><span> </span><span>self</span><span>.d, &amp;</span><span>f</span><span>, &amp;</span><span>qd</span><span>);</span></div></div><div><div><div>85</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>    </span><span>fn</span><span> </span><span>inject</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>dx</span><span>: &amp;[</span><span>Real</span><span>; </span><span>6</span><span>]) {</span></div></div><div><div><div>88</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> (</span></span><span>self</span><span>.q * </span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>dx</span><span>[</span><span>0</span><span>], </span><span>dx</span><span>[</span><span>1</span><span>], </span><span>dx</span><span>[</span><span>2</span><span>]])).</span><span>normalized</span><span>();</span></div></div><div><div><div>89</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>90</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>dx</span><span>[</span><span>3</span><span> + </span><span>i</span><span>];</span></div></div><div><div><div>91</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>93</div></div><div>
</div></div><div><div><div>94</div></div><div><span>    </span><span>/// Three sequential Bierman updates at one linearization point.</span></div></div><div><div><div>95</div></div><div><span>    </span><span>/// False when gated out.</span></div></div><div><div><div>96</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_accel</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>97</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] * </span><span>accel</span><span>[</span><span>0</span><span>] + </span><span>accel</span><span>[</span><span>1</span><span>] * </span><span>accel</span><span>[</span><span>1</span><span>] + </span><span>accel</span><span>[</span><span>2</span><span>] * </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>98</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.accel_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>G</span><span>) &gt; </span><span>self</span><span>.accel_gate {</span></div></div><div><div><div>99</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>100</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>101</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>102</div></div><div><span>        </span><span>let</span><span> </span><span>h</span><span><span> </span><span>=</span><span> [-</span></span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>1</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>2</span><span>]];</span></div></div><div><div><div>103</div></div><div><span>        </span><span>let</span><span> </span><span>hm</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>104</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> (</span></span><span>self</span><span>.sa / </span><span>G</span><span>) * (</span><span>self</span><span>.sa / </span><span>G</span><span>);</span></div></div><div><div><div>105</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>dx</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>6</span><span>];</span></div></div><div><div><div>106</div></div><div><span>        </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>107</div></div><div><span>            </span><span>let</span><span> </span><span>hj</span><span><span> </span><span>=</span><span> [</span></span><span>hm</span><span>.m[</span><span>j</span><span>][</span><span>0</span><span>], </span><span>hm</span><span>.m[</span><span>j</span><span>][</span><span>1</span><span>], </span><span>hm</span><span>.m[</span><span>j</span><span>][</span><span>2</span><span>], </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>];</span></div></div><div><div><div>108</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>hdx</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>109</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>6</span><span> {</span></div></div><div><div><div>110</div></div><div><span>                </span><span>hdx</span><span><span> </span><span>+=</span><span> </span></span><span>hj</span><span>[</span><span>k</span><span>] * </span><span>dx</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>111</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>112</div></div><div><span>            </span><span>let</span><span> </span><span>dxj</span><span><span> </span><span>=</span><span> </span></span><span>bierman_update</span><span>(</span></div></div><div><div><div>113</div></div><div><span><span>                </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>.u,</span></div></div><div><div><div>114</div></div><div><span><span>                </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>.d,</span></div></div><div><div><div>115</div></div><div><span><span>                </span></span><span>&amp;</span><span>hj</span><span>,</span></div></div><div><div><div>116</div></div><div><span>                </span><span>r</span><span>,</span></div></div><div><div><div>117</div></div><div><span>                </span><span>accel</span><span>[</span><span>j</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>j</span><span>] - </span><span>hdx</span><span>,</span></div></div><div><div><div>118</div></div><div><span><span>            </span></span><span>);</span></div></div><div><div><div>119</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>6</span><span> {</span></div></div><div><div><div>120</div></div><div><span>                </span><span>dx</span><span>[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>dxj</span><span>[</span><span>k</span><span>];</span></div></div><div><div><div>121</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>122</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>123</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>124</div></div><div><span>        </span><span>true</span></div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>126</div></div><div>
</div></div><div><div><div>127</div></div><div><span>    </span><span>/// Heading-only magnetometer update. False when gated out.</span></div></div><div><div><div>128</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_mag_heading</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>129</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>mag</span><span>[</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>mag</span><span>[</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>130</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.mag_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>1.0</span><span>) &gt; </span><span>self</span><span>.mag_gate {</span></div></div><div><div><div>131</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>132</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>133</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>134</div></div><div><span>        </span><span>let</span><span> </span><span>mw</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>i</span><span>: </span><span>usize</span><span><span>|</span><span> </span></span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>135</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>wrap_pi</span><span>(</span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>)) - </span><span>atan2</span><span>(</span><span>self</span><span>.m_w[</span><span>1</span><span>], </span><span>self</span><span>.m_w[</span><span>0</span><span>]));</span></div></div><div><div><div>136</div></div><div><span>        </span><span>// d heading / d dtheta = [mx mz, my mz, -(mx^2 + my^2)] / (mx^2 + my^2) C(q)</span></div></div><div><div><div>137</div></div><div><span>        </span><span>let</span><span> [</span><span>mx</span><span>, </span><span>my</span><span>, </span><span>mz</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.m_w;</span></div></div><div><div><div>138</div></div><div><span>        </span><span>let</span><span> </span><span>mh2</span><span><span> </span><span>=</span><span> </span></span><span>mx</span><span> * </span><span>mx</span><span> + </span><span>my</span><span> * </span><span>my</span><span>;</span></div></div><div><div><div>139</div></div><div><span>        </span><span>let</span><span> </span><span>row</span><span><span> </span><span>=</span><span> [</span></span><span>mx</span><span> * </span><span>mz</span><span> / </span><span>mh2</span><span>, </span><span>my</span><span> * </span><span>mz</span><span> / </span><span>mh2</span><span>, -</span><span>1.0</span><span>];</span></div></div><div><div><div>140</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>h</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>6</span><span>];</span></div></div><div><div><div>141</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>142</div></div><div><span>            </span><span>h</span><span>[</span><span>k</span><span><span>] </span><span>=</span><span> </span></span><span>row</span><span>[</span><span>0</span><span>] * </span><span>c</span><span>.m[</span><span>0</span><span>][</span><span>k</span><span>] + </span><span>row</span><span>[</span><span>1</span><span>] * </span><span>c</span><span>.m[</span><span>1</span><span>][</span><span>k</span><span>] + </span><span>row</span><span>[</span><span>2</span><span>] * </span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>143</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>144</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> (</span></span><span>self</span><span>.sm / </span><span>sqrt</span><span>(</span><span>mh2</span><span>)) * (</span><span>self</span><span>.sm / </span><span>sqrt</span><span>(</span><span>mh2</span><span>));</span></div></div><div><div><div>145</div></div><div><span>        </span><span>let</span><span> </span><span>dx</span><span><span> </span><span>=</span><span> </span></span><span>bierman_update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.u, &amp;</span><span>mut</span><span> </span><span>self</span><span>.d, &amp;</span><span>h</span><span>, </span><span>r</span><span>, </span><span>y</span><span>);</span></div></div><div><div><div>146</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>147</div></div><div><span>        </span><span>true</span></div></div><div><div><div>148</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>149</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>ch08 的三个 app（<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/c/apps/ch08.c" target="_blank"><code>c/apps/ch08.c</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/cpp/apps/ch08.cpp" target="_blank"><code>cpp/apps/ch08.cpp</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/rust/src/bin/ch08.rs" target="_blank"><code>rust/src/bin/ch08.rs</code></a>）有三个子命令：<code>run</code> 写输出，<code>sizes</code> 打印结构体大小，<code>bench</code> 计时。</p><p><strong>float32 下四种写法。</strong> 在姿态数据上，四种写法（向量 Joseph，顺序 Joseph，顺序短形式，UD）的 float32 输出和 float64 参考：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>float32 covariance forms, ESKF without reset Jacobian (vs the float64 vector ESKF)</span></div></div><div><div><div>2</div></div><div><span>variant  lang   max diff (deg)  RMS (deg)   min pmin  steps pmin&lt;=0</span></div></div><div><div><div>3</div></div><div><span>v_vec    C           2.890e-04  7.777e-05   6.68e-08              0</span></div></div><div><div><div>4</div></div><div><span>v_vec    C++         2.761e-04  7.707e-05   6.68e-08              0</span></div></div><div><div><div>5</div></div><div><span>v_vec    Rust        2.906e-04  7.965e-05   6.68e-08              0</span></div></div><div><div><div>6</div></div><div><span>v_seqj   C           2.974e-04  7.794e-05   6.68e-08              0</span></div></div><div><div><div>7</div></div><div><span>v_seqj   C++         2.856e-04  7.869e-05   6.68e-08              0</span></div></div><div><div><div>8</div></div><div><span>v_seqj   Rust        2.762e-04  7.717e-05   6.68e-08              0</span></div></div><div><div><div>9</div></div><div><span>v_seqs   C           2.982e-04  7.763e-05   6.68e-08              0</span></div></div><div><div><div>10</div></div><div><span>v_seqs   C++         2.921e-04  7.866e-05   6.68e-08              0</span></div></div><div><div><div>11</div></div><div><span>v_seqs   Rust        2.771e-04  7.617e-05   6.68e-08              0</span></div></div><div><div><div>12</div></div><div><span>v_ud     C           2.976e-04  7.942e-05   6.68e-08              0</span></div></div><div><div><div>13</div></div><div><span>v_ud     C++         3.042e-04  8.131e-05   6.68e-08              0</span></div></div><div><div><div>14</div></div><div><span>v_ud     Rust        2.890e-04  8.071e-05   6.68e-08              0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四种写法在这份数据上没有任何可见的区别：最大姿态差都是 <span><span>3×10−43\times10^{-4}</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> 度，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的最小主元都是 <span><span>6.68×10−86.68\times10^{-8}</span><span><span><span></span><span>6.68</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>，没有一步是非正的。这份数据太温和了，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的动态范围很小。能把它们分出高下的是第 3 篇的病态例子（<span><span>P0=106IP_0 = 10^6I</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span><span>I</span></span></span></span>，<span><span>r=10−8r = 10^{-8}</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>，动态范围 <span><span>101410^{14}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>）。</p><p><strong>病态例子在 float32 下。</strong> C、C++、Rust 的 float32 构建一致地崩溃：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ill-conditioned case (P0 = 1e6 I, r = 1e-8) in float32</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>C     ill_joseph: min eig -3.526e+00, max |P01-P10| 0.000e+00, NaN rows 0, failed updates 998</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>C     ill_short : min eig -1.989e+00, max |P01-P10| 2.154e-04, NaN rows 0, failed updates 997</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>C++   ill_joseph: min eig -3.526e+00, max |P01-P10| 0.000e+00, NaN rows 0, failed updates 998</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>C++   ill_short : min eig -1.989e+00, max |P01-P10| 2.154e-04, NaN rows 0, failed updates 997</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>Rust  ill_joseph: min eig -3.526e+00, max |P01-P10| 0.000e+00, NaN rows 0, failed updates 998</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>Rust  ill_short : min eig -1.989e+00, max |P01-P10| 2.154e-04, NaN rows 0, failed updates 997</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>1000 步里 998 步（Joseph）和 997 步（短形式）的更新因为 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 不正定而失败，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的最小特征值是 <span><span>−3.5-3.5</span><span><span><span></span><span>−</span><span>3.5</span></span></span></span> 和 <span><span>−2.0-2.0</span><span><span><span></span><span>−</span><span>2.0</span></span></span></span>。原因就在第二步：<span><span>P11P_{11}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>11</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 从 <span><span>10610^6</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 经过更新应该变成 <span><span>2.7×10−32.7\times10^{-3}</span><span><span><span></span><span>2.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，这个相消的量级是 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，float32 的相对精度是 <span><span>10−710^{-7}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，绝对误差大约 0.06，结果成了 <span><span>−0.035-0.035</span><span><span><span></span><span>−</span><span>0.035</span></span></span></span>，之后一切都乱了。</p><p>这和第 3 篇的 Python float32 不一致，那里 Joseph 形式几乎没事。我的解释如下，没有逐位验证：第 3 篇的 Python 是直接做除法求增益 <span><span>K=P−HT/SK = P^-H^T/S</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span>S</span></span></span></span>，那个例子的第二步所有中间量碰巧是整数（<span><span>P01=−104P_{01} = -10^4</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>01</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span>，<span><span>S=100S = 100</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>100</span></span></span></span>，<span><span>K1=−100K_1 = -100</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>100</span></span></span></span>），舍入没有损失，<span><span>106−2×106+10610^6-2\times10^6+10^6</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 精确等于 0。C、C++、Rust 用的是 Cholesky 风格的增益 <span><span>K=(P−HT/S)/SK = (P^-H^T/\sqrt S)/\sqrt S</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span>S</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span><span>/</span><span><span><span><span><span><span></span><span>S</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，<span><span>100=10\sqrt{100} = 10</span><span><span><span></span><span><span><span><span><span><span></span><span><span>100</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>10</span></span></span></span> 之后的两次除法在 float32 里不再精确，相消失败了。我在 numpy float32 里用同样的 Cholesky 风格增益重做了这个实验，并且换了两组不那么”整”的初值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ill-conditioned case, Python numpy, Cholesky-style gain: steps with a non-positive pivot (smallest pivot)</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>P0   sigma_v    dtype                     UD                 Joseph             short form</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>1e+06  1.00e-04  float64         0 ( 9.629e-09)         0 ( 9.630e-09)         0 ( 9.630e-09)</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1e+06  1.00e-04  float32         0 ( 9.629e-09)       999 (-3.516e-02)      1000 (-1.000e+00)</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>1.234e+06  1.17e-04  float64         0 ( 1.301e-08)         0 ( 1.301e-08)         0 ( 1.301e-08)</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>1.234e+06  1.17e-04  float32         0 ( 1.301e-08)         0 ( 1.301e-08)         1 (-1.000e+00)</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>3.3e+05  9.00e-05  float64         0 ( 7.853e-09)         0 ( 7.854e-09)         0 ( 7.854e-09)</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>3.3e+05  9.00e-05  float32         0 ( 7.853e-09)         0 ( 7.854e-09)         1 (-1.000e+00)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>UD 在三组初值、两种精度下，一步非正主元都没有</strong>，float32 下的最小主元和 float64 一致到 4 位有效数字（<span><span>9.629×10−99.629\times10^{-9}</span><span><span><span></span><span>9.629</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 对 <span><span>9.629×10−99.629\times10^{-9}</span><span><span><span></span><span>9.629</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>）。</li>
<li><strong>Joseph 形式</strong>：第一组初值（整数）float32 下 999 步失败，复现了 C、C++、Rust 的崩溃；后两组（不整的初值）没有失败。</li>
<li><strong>短形式</strong>：三组初值里，float32 下第一组 1000 步全失败，后两组各有 1 步（第一次更新）非正。</li>
</ul><p>所以我的结论是：Joseph 形式在 float32 下不是”总是安全”，它依赖于具体问题的动态范围；短形式比它更脆弱；UD 在这三组条件里都是正的。三组不是证明，只是我试过的全部。如果你的 <span><span>P0P_0</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 之间有 <span><span>101210^{12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 以上的动态范围，并且要在 float32 上跑，值得考虑 UD，或者干脆把 <span><span>P0P_0</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 设小一点（初始不确定度设到 <span><span>10610^6</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 是我为了制造病态例子故意写的）。</p><p></p><figure><img src="/assets/images/kalman-filter/ch08-ud-vs-joseph.svg" alt="上：姿态数据上四种协方差写法的 float32 输出与 float64 参考的差；下：病态例子里三种写法最小主元的前 20 步（float32，Python）" /><figcaption>上：姿态数据上四种协方差写法的 float32 输出与 float64 参考的差；下：病态例子里三种写法最小主元的前 20 步（float32，Python）</figcaption></figure><p></p></section>
<section><h2>内存<a href="#内存"><span>#</span></a></h2><p>每个滤波器结构体（状态加协方差加参数，不含数据文件）的字节数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sizeof / size_of of the filter state, bytes</span></div></div><div><div><div>2</div></div><div><span>filter         c f64     c f32   cpp f64   cpp f32    rs f64    rs f32</span></div></div><div><div><div>3</div></div><div><span>kf1d              40        20        40        20        40        20</span></div></div><div><div><div>4</div></div><div><span>kf_2_1_1         416       248       168        84       168        84</span></div></div><div><div><div>5</div></div><div><span>mahony            96        48        96        48        96        48</span></div></div><div><div><div>6</div></div><div><span>ekf             2752      1376       512       256       512       256</span></div></div><div><div><div>7</div></div><div><span>eskf            2472      1236       424       212       424       212</span></div></div><div><div><div>8</div></div><div><span>eskf_ud         2512      1256       464       232       464       232</span></div></div><div><div><div>9</div></div><div><span>ukf             4032      2016       424       212       424       212</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>Rust 和 C++ 的 <code>size_of</code> 是 double 到 float 刚好减半：滤波器的状态全是标量。</li>
<li>C 的数字大一个数量级：<code>kl_ekf_att</code> 2752 字节，<code>kl_ukf_att</code> 4032 字节，原因是每个 C 结构体里有一块给 <code>joseph_update</code>、<code>predict</code> 用的内联工作区（<code>work[...]</code>，几百个 <code>kl_real</code>），C++ 和 Rust 的临时矩阵是栈上的局部变量，不出现在 <code>sizeof</code> 里。这未必说明 C 更浪费：C++ 和 Rust 的栈帧在调用时同样需要这些临时空间，我估计量级相近，区别只是它们在不在滤波器对象里。真正要关心的是最坏情况下总的 RAM 占用（对象加栈峰值），我没有测栈峰值。</li>
<li><code>kl_kf</code> 在 C 里只是一组指针（80 字节），表里的 416 字节是我把调用者持有的数组（<code>x</code>、<code>P</code>、<code>F</code>、<code>B</code>、<code>H</code>、<code>Q</code>、<code>R</code>、<code>work</code>）也加上。</li>
<li>最大的 UKF 在 float32 下 C 版 2 KiB。一块有 64 KiB RAM 的 Cortex-M4 装得下全部。</li>
</ul><p><strong>在写这一节的时候我发现了一个 C 的越界 bug。</strong> 第 5 篇的 <code>kl_ekf_att</code> 的工作区我写成了 256 个元素，更新步骤用到 279 个。越界部分写进了紧挨着结构体的下一个变量。第 5 篇的 app 里碰巧没有踩到有用的东西，输出和 Python 对得上，所以测试通过了；第 8 篇的 app 把同一份库代码链接进另一个程序，邻居变成了一个全局常量，UD 滤波器的输出变成了 NaN。改成 280，并把这个数字的来由写在注释里，第 5 篇的正文也据此更新了。AddressSanitizer 和 UBSan 跑 C 的全部 app（ch01 到 ch08 的 <code>run</code>）都是干净的，但它们抓不到结构体内部成员之间的越界，这个 bug 是靠”输出突然是 NaN”发现的。C++ 和 Rust 的矩阵尺寸是类型的一部分，不会有这类问题。</p></section>
<section><h2>没有堆分配：三个证明<a href="#没有堆分配三个证明"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>== C: heap symbols referenced by the filter objects (everything except csv.o)</span></div></div><div><div><div>2</div></div><div><span>ok: no malloc/calloc/realloc/free in 13 filter objects</span></div></div><div><div><div>3</div></div><div><span>== C++: heap symbols in the compiled filters (-fno-exceptions -fno-rtti)</span></div></div><div><div><div>4</div></div><div><span>ok: no operator new / malloc; undefined symbols are:</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>U atan2</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>U atan2f</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>U fmod</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>U fmodf</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>U hypot</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>U hypotf</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>U sincos</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>U sincosf</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>U sqrt</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>U sqrtf</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>U __stack_chk_fail</span></div></div><div><div><div>16</div></div><div><span>== Rust: no_std build of the library (no alloc)</span></div></div><div><div><div>17</div></div><div><span>ok: cargo build --lib --no-default-features --features f32</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><strong>C：</strong> 对每个滤波器目标文件（<code>build/c/obj/</code> 下除了 <code>csv.o</code> 的 13 个）跑 <code>nm -u</code>，不引用 <code>malloc</code>、<code>calloc</code>、<code>realloc</code>、<code>free</code>。</li>
<li><strong>C++：</strong> 单独一个测试文件 <code>cpp/tests/no_heap.cpp</code> 把所有滤波器各实例化一遍（double 和 float），用 <code>-fno-exceptions -fno-rtti</code> 编译，<code>nm -C -u</code> 列出的未定义符号只有数学函数和栈保护 <code>__stack_chk_fail</code>，没有 <code>operator new</code> 和 <code>malloc</code>。</li>
<li><strong>Rust：</strong> <code>cargo build --lib --no-default-features --features f32</code> 成功。库是 <code>#![no_std]</code>，没有 <code>alloc</code> crate，所以任何堆分配都会在编译时报错。</li>
</ul><p>这三种检查的强度不同：C 和 C++ 是”当前编译出来的目标文件不引用堆函数”，编译器版本换了或者优化选项变了，结论要重测；Rust 的是”编译不过”，保证更强。</p></section>
<section><h2>Cortex-M4F 上的代码体积<a href="#cortex-m4f-上的代码体积"><span>#</span></a></h2><p>把 C 滤波器源码（除了 <code>csv.c</code>）按 float 编译成 Cortex-M4F 的目标文件，只编译不链接：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>== Cortex-M4F code size of the C filters (float, -O2, compile only)</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>text     data      bss      dec      hex  filename</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>516        0        0      516      204  build/arm/kl_baselines.o</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>1840        0        0     1840      730  build/arm/kl_ekf_att.o</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>1620        0        0     1620      654  build/arm/kl_eskf.o</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>1356        0        0     1356      54c  build/arm/kl_eskf_seq.o</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>1160        0        0     1160      488  build/arm/kl_eskf_ud.o</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>116        0        0      116       74  build/arm/kl_kf1d.o</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>1360        0        0     1360      550  build/arm/kl_kf.o</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>532        0        0      532      214  build/arm/kl_mahony.o</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>1140        0        0     1140      474  build/arm/kl_mat.o</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>1924        0        0     1924      784  build/arm/kl_quat.o</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>1424        0        0     1424      590  build/arm/kl_ud.o</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>2596        0        0     2596      a24  build/arm/kl_ukf_att.o</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>364        0        0      364      16c  build/arm/kl_update.o</span></div></div><div><div><div>16</div></div><div><span>double-promotion warnings across all objects: 0</span></div></div><div><div><div>17</div></div><div><span>distinct soft-double helper symbols (__aeabi_d*) referenced: 0</span></div></div><div><div><div>18</div></div><div><span>./build/c/ch08 bench</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>所有模块加起来 15948 字节（text），一个具体滤波器要它自己加上它依赖的几个：ESKF 是 <code>kl_eskf</code>（1620）加 <code>kl_mat</code>（1140）加 <code>kl_quat</code>（1924）加 <code>kl_update</code>（364）加 <code>kl_ekf_att</code> 里的 <code>rot_inv</code>（1840，我偷懒直接依赖了整个模块），约 7 KB；UKF 是 <code>kl_ukf_att</code>（2596）加同一批依赖，再加 <code>kl_wrap_pi</code> 所在的 <code>kl_eskf</code>（1620），在 9 到 10 KB 之间。链接的时候 <code>-ffunction-sections -Wl,--gc-sections</code> 会去掉没用到的函数。这些是目标文件的大小，没有链接 <code>libm</code>（<code>sqrtf</code>、<code>atan2f</code>、<code>sincosf</code> 的实现需要几 KB），也没有启动代码。</p><p>这个检查还抓到一个 bug：我第一次编译的时候，<code>kl_eskf.o</code> 引用了两个软件 double 的辅助函数（<code>__aeabi_f2d</code> 和 <code>__aeabi_d2f</code>，我的脚本一开始只搜 <code>__aeabi_d*</code>，所以当时数到的是 1 个）。原因是第 6 篇的 <code>kl_wrap_pi</code> 里我写的是 <code>fmod</code>，它是 double 的函数，float 构建里每次调用都要把 float 提升成 double 再降回来。在没有双精度硬件的 Cortex-M4F 上，这会拖进软件实现的 double <code>fmod</code>，既慢又大。改成 float 构建里用 <code>fmodf</code> 之后，脚本数到的软件 double 辅助函数引用变成 0，<code>kl_eskf.o</code> 小了 56 字节。<code>-Wdouble-promotion</code> 的警告数一直是 0，它没有抓到这一个，我没有深究为什么。这个教训我觉得值得单独记一笔：<strong>float 构建里一个函数名写错，不会报错，不会警告，只会让你的 MCU 变慢</strong>，检查办法是 <code>nm -u</code> 里搜 <code>__aeabi_d</code>。</p></section>
<section><h2>每步耗时<a href="#每步耗时"><span>#</span></a></h2><div><div><div></div><div>这是这台机器的数字</div></div><div><p>CPU：AMD Ryzen 7 PRO 6850U with Radeon Graphics。C、C++、Rust 各 100 次重复取中位数（每次重复处理整份数据），Python 因为太慢只取 3 到 5 次。不是 Cortex-M 的数字。</p></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make ch08   # timing part of the report</span></div></div><div><div><div>2</div></div><div><span>filter       c f64     c f32   cpp f64   cpp f32    rs f64    rs f32    py f64  py/c f64</span></div></div><div><div><div>3</div></div><div><span>KF1D            10        10         6         5         6         5       586        56</span></div></div><div><div><div>4</div></div><div><span>KF             115       119        42        39        35        31     55807       485</span></div></div><div><div><div>5</div></div><div><span>Mahony          73        70        70        80        54        47     76632      1053</span></div></div><div><div><div>6</div></div><div><span>EKF           2131      2138      1677      1366      1235      1153    399979       188</span></div></div><div><div><div>7</div></div><div><span>ESKF          1862      1787      1420      1029       865       931    237895       128</span></div></div><div><div><div>8</div></div><div><span>ESKF-UD        864       807       738       620       510       473    208850       242</span></div></div><div><div><div>9</div></div><div><span>UKF           3730      3680      3274      2715      2232      2088    706759       189</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>单位是纳秒每步。几个观察：</p><ul>
<li><strong>最贵的 UKF 每步约 3730 ns（C，double）</strong>，在 100 Hz 的循环里占 10 ms 预算的 0.037%。在这台机器上，滤波器的算力完全不是问题。</li>
<li><strong>ESKF 比 EKF 便宜</strong>（C 里 1862 对 2131 ns）：状态少一维，协方差 <span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 对 <span><span>7×77\times7</span><span><span><span></span><span>7</span><span></span><span>×</span><span></span></span><span><span></span><span>7</span></span></span></span>。</li>
<li><strong>ESKF-UD 比 ESKF 便宜</strong>（864 对 1862 ns），比我预期的好得多：它每步只做 4 次标量更新，没有 Cholesky，没有 <span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 的矩阵乘法。我没有拿它去和 Joseph 形式的 ESKF 比”同样的算法”，两者在代码结构上差得太多，这个比较只能说明”UD + 标量更新”这条路在这台机器上没有慢。</li>
<li><strong>C 在多数滤波器上比 Rust 和 C++ 慢</strong>，这可能让人意外。我没有深究：C 版的矩阵库是运行期维度的循环加指针，Rust 和 C++ 的矩阵尺寸是编译期常量，编译器可以完全展开。这说明我的 C 版没有做任何性能上的调优，别拿它当 C 的上限。</li>
<li><strong>float32 在这台 x86 上并不更快</strong>：C 里 float32 和 float64 的耗时之比在 0.93 到 1.03 之间，几乎没变；C++ 在 0.72 到 1.14 之间，EKF、ESKF、UKF 快了 15% 到 30%；Rust 在 0.83 到 1.08 之间，有快有慢（ESKF 的 float32 反而慢了 8%）。x86 的 SSE 标量单精度和双精度的吞吐差不多。float32 的价值在没有双精度硬件的 MCU 上，不在这里。</li>
<li><strong>Python（numpy）比 C（float64）慢 59 到 1050 倍</strong>（按滤波器），最小的是 KF1D（59 倍），最大的是 Mahony（1050 倍），ESKF 是 128 倍。numpy 对 <span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 这样的小矩阵没有任何优势，调度开销压倒了计算。</li>
</ul><p></p><figure><img src="/assets/images/kalman-filter/ch08-timing.svg" alt="七个滤波器的每步耗时，对数坐标，四种语言，float64" /><figcaption>七个滤波器的每步耗时，对数坐标，四种语言，float64</figcaption></figure><p></p><p><strong>推测，没有测量。</strong> 我没有 Cortex-M 的板子在手边，所以下面是外推，不是数据。Cortex-M4F 在 168 MHz，没有乱序执行，没有 SIMD，单精度浮点是单周期的 FMA 量级。我估计同一份代码在上面会比这台机器慢几十倍，大概 30 到 100 倍之间，这样 UKF 每步在 0.1 到 0.4 ms，占 10 ms 预算的 1% 到 4%；ESKF 在几十到一百多微秒。如果真是这个量级，100 Hz 的姿态估计在 M4F 上算力是有富余的，UKF 也是可行的，但请用你自己的板子量一遍再相信我。</p></section>
<section><h2>移植清单<a href="#移植清单"><span>#</span></a></h2><p>纯软件做到现在，剩下的问题都是接口和物理世界的问题：</p><ul>
<li><strong>回放接口。</strong> 在硬件上把原始传感器数据按 <code>data/imu_seed*.csv</code> 的列格式（<code>t,gx,gy,gz,ax,ay,az,mx,my,mz</code>，后面的真值列可以是任意值，滤波器不读）记下来，原样拿回这个仓库里，用同一批 app 回放。这样你可以在笔记本上复现板子上的任何一个问题。</li>
<li><strong>步长。</strong> 我所有的滤波器都用固定的 <span><span>Δt=0.01\Delta t = 0.01</span><span><span><span></span><span>Δ</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span> s。真实 MCU 上的定时器有抖动，要么用硬件定时器保证采样间隔固定，要么用测得的 <span><span>Δt\Delta t</span><span><span><span></span><span>Δ</span><span>t</span></span></span></span>（predict 里已经有 <code>dt</code> 参数，只是 app 里没有用）。把 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 按 <span><span>Δt\Delta t</span><span><span><span></span><span>Δ</span><span>t</span></span></span></span> 缩放的部分（第 2 篇的连续到离散）已经写对了。</li>
<li><strong>时间戳对齐。</strong> 陀螺、加速度计、磁力计往往不是同一时刻采的，磁力计经常慢得多（我的仿真里三者同步 100 Hz）。可以把慢传感器的更新放在有新数据的时候做，其余时候只 predict；我的滤波器结构（predict 之后各自独立的 update）已经支持这样。</li>
<li><strong>轴映射。</strong> 仿真里机体系是 FRD。真实的 IMU 芯片的轴向可能是 FLU、RFU，每个传感器还可能互相不一致（磁力计常常自己一套轴）。在数据进入滤波器之前，用一个常数旋转矩阵统一到 FRD，并用静止的数据检验：水平放置时加速度计读数应该是 <span><span>[0,0,−g][0,0,-g]</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>g</span><span>]</span></span></span></span>。</li>
<li><strong>加速度计、陀螺的偏置和比例因子。</strong> 陀螺零偏滤波器会估计，但初始值最好静止几秒测一下；加速度计的偏置和比例因子需要多姿态标定。</li>
<li><strong>磁力计的硬铁和软铁标定。</strong> 见下。</li>
<li><strong>float 还是 double。</strong> 如果 MCU 有双精度 FPU（Cortex-M7 一部分型号），double 不是问题；只有单精度 FPU（M4F）的话，用 float32，并且按上一节检查有没有软件 double 辅助函数混进去。</li>
</ul><section><h3>硬铁偏置：一个演示<a href="#硬铁偏置一个演示"><span>#</span></a></h3><p>磁力计的硬铁偏置是个固定的偏移（板子上的永磁体和电流产生的磁场），在传感器自己的坐标系里不变。我给种子 1 的磁力计读数在机体系里加上 <span><span>[0.2,0,0][0.2,0,0]</span><span><span><span></span><span>[</span><span>0.2</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span>（场强是 1，所以是 20% 的偏置），跑第 6 篇的 ESKF：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hard-iron offset [0.2, 0, 0] in the body frame, ESKF, yaw error over the calm period [20,60) s</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>no offset             : yaw mean   -0.04 deg, RMS    0.09 deg, total RMS    0.09 deg, gated mag samples 1000 of 12000</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>offset, |m| gate 0.1  : yaw mean   -2.01 deg, RMS    7.95 deg, total RMS    7.92 deg, gated mag samples 4064 of 12000</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>offset, no gate       : yaw mean   -2.00 deg, RMS    8.46 deg, total RMS    8.42 deg, gated mag samples 0 of 12000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/kalman-filter/ch08-hard-iron.svg" alt="硬铁偏置下的偏航误差：无偏置（蓝），有偏置加模长门控（橙），有偏置无门控（绿）" /><figcaption>硬铁偏置下的偏航误差：无偏置（蓝），有偏置加模长门控（橙），有偏置无门控（绿）</figcaption></figure><p></p><p>偏航误差从 0.09 度的均方根变成约 8 度，来回摆动，平均值只有 <span><span>−2-2</span><span><span><span></span><span>−</span><span>2</span></span></span></span> 度，但是峰值超过 15 度：偏置是固定在机体上的，机体一转，它在世界系里的方向就转，偏航估计被拉着来回摆。模长门控这次没能帮上忙：被偏置污染以后，磁场模长在 0.8 到 1.2 之间随姿态变化，门控拒绝了 12000 个样本里的 4064 个（34%），剩下的样本仍然是错的，只是错得没有超出门限。门控是对”突发干扰”的检测，不是对”持续偏置”的检测。需要的是标定：转动传感器一圈，拟合出球心，减掉。</p></section></section>
<section><h2>外推<a href="#外推"><span>#</span></a></h2><p>回到我最初的目的：控制用的姿态估计。这条路的软件部分到这里做完了：公式有推导，代码有四种语言的实现，每一个数字有可以重跑的来源。真正有意思的部分接下来才开始，在硬件上：你会发现数据里有仿真器没有的东西（传感器延迟、非线性、温度漂移、振动），而你手里已经有一套可以复现每一个数字的工具来逐个对付它们。我会拿 ESKF 先上板子，原因很具体：它比 EKF 少一维、没有四元数协方差的幽灵方向，比 UKF 便宜，C 里大约 2.0 倍，而且在这份数据上，它的结果和 UKF 在三位小数内一样。</p><p>要做的第一件事，大概是把上面那个回放接口接上，把静止状态下的 IMU 噪声实测出来，然后把 <code>GYRO_SIGMA</code>、<code>ACC_SIGMA</code> 换成测得的值。第 3 篇的 NEES 检验到时候会用不上（没有真值），但新息自相关一直能用。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-07-ukf/</id>
      <title type="text">无迹卡尔曼滤波，以及四个滤波器同台</title>
      <published>2026-10-14T06:00:00.000Z</published>
      <updated>2026-10-14T06:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-07-ukf/"/>
      <summary type="text">卡尔曼滤波系列第 7 篇：推导无迹变换为什么能抓到均值的二阶项，写一个不需要雅可比的误差状态 UKF，并在同一份 50 个种子的数据上把 Mahony、EKF、ESKF、UKF 摆在一张表里比较，包括大初始误差。Python、C、C++、Rust 实现。</summary>
      <content type="html"><![CDATA[<p>先看一个玩具问题。把极坐标 <span><span>(r,θ)(r,\theta)</span><span><span><span></span><span>(</span><span>r</span><span>,</span><span></span><span>θ</span><span>)</span></span></span></span> 变成直角坐标 <span><span>(rcos⁡θ,rsin⁡θ)(r\cos\theta,r\sin\theta)</span><span><span><span></span><span>(</span><span>r</span><span></span><span>cos</span><span></span><span>θ</span><span>,</span><span></span><span>r</span><span></span><span>sin</span><span></span><span>θ</span><span>)</span></span></span></span>，<span><span>r∼N(1,0.022)r\sim\mathcal N(1,0.02^2)</span><span><span><span></span><span>r</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>1</span><span>,</span><span></span><span>0.0</span><span><span>2</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>θ∼N(π/2,0.52)\theta\sim\mathcal N(\pi/2,0.5^2)</span><span><span><span></span><span>θ</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>π</span><span>/2</span><span>,</span><span></span><span>0.</span><span><span>5</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。用一百万个样本算出来，<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 的均值是 0.8827。线性化给的答案是 1.0000，差了 0.117；无迹变换只用 5 个点，给的答案是 0.8801，差了 0.003。这就是这一篇的主角，它连一个雅可比也没求。</p>
<p>然后是更扫兴的部分：把它放进姿态滤波器之后，在 50 个种子的蒙特卡洛里，UKF 的总姿态误差是 0.929 度，和上一篇的 ESKF 的 0.929 度在三位小数里没有区别。我会把这件事说清楚，也会说它在哪里确实赢了。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch07</code> 通过（它跑得比较久，大概 6 分钟，大部分时间花在 50 个种子的四滤波器蒙特卡洛上），<code>out/ch07/ukf_{nominal,large}_{py,c,cpp,rs}.csv</code>，三张图；C、C++、Rust 各一个误差状态 UKF（<code>kl_ukf_att</code>、<code>ukf.hpp</code>、<code>ukf.rs</code>）。每个语言的 app 先做一个 sigma 点的自检。
前置：<a href="/posts/kalman-filter-06-eskf/">第 6 篇</a>（误差状态、注入与重置、航向更新、门控）。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>为什么传点而不是传函数<a href="#为什么传点而不是传函数"><span>#</span></a></h3><p>EKF 的做法是把非线性函数 <span><span>ff</span><span><span><span></span><span>f</span></span></span></span> 换成它的一阶泰勒展开，再传播高斯分布。无迹变换（unscented transform，UT）反过来：保留真正的非线性函数，用一小撮精心选择的点来代表高斯分布，把点一个个过一遍 <span><span>ff</span><span><span><span></span><span>f</span></span></span></span>，再用输出点的加权统计量近似输出分布的均值和协方差。</p><p>设 <span><span>x∼(μ,Σ)x\sim(\mu,\Sigma)</span><span><span><span></span><span>x</span><span></span><span>∼</span><span></span></span><span><span></span><span>(</span><span>μ</span><span>,</span><span></span><span>Σ</span><span>)</span></span></span></span>，<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 维。选 <span><span>2n+12n+1</span><span><span><span></span><span>2</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个 sigma 点 <span><span>μ+χi\mu+\chi_i</span><span><span><span></span><span>μ</span><span></span><span>+</span><span></span></span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，权重 <span><span>WimW_i^m</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（均值用）和 <span><span>WicW_i^c</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（协方差用）。对称选法：<span><span>χ0=0\chi_0 = 0</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>χi=±\chi_i = \pm</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>±</span></span></span></span> 矩阵 <span><span>(n+λ)Σ\sqrt{(n+\lambda)\Sigma}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>n</span><span></span><span>+</span><span></span><span>λ</span><span>)</span><span>Σ</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 的第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 列，使得</p><span><span><span>∑iWimχi=0,∑iWicχiχiT≈Σ\sum_iW_i^m\chi_i = 0,\qquad \sum_iW_i^c\chi_i\chi_i^T\approx\Sigma</span><span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>Σ</span></span></span></span></span><p>其中 <span><span>⋅\sqrt{\cdot}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>⋅</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 用 Cholesky 因子：<span><span>(n+λ)Σ=L\sqrt{(n+\lambda)\Sigma} = L</span><span><span><span></span><span><span><span><span><span><span></span><span><span>(</span><span>n</span><span></span><span>+</span><span></span><span>λ</span><span>)</span><span>Σ</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>L</span></span></span></span>，<span><span>LLT=(n+λ)ΣLL^T = (n+\lambda)\Sigma</span><span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>λ</span><span>)</span><span>Σ</span></span></span></span>。</p></section><section><h3>为什么它能抓到二阶项<a href="#为什么它能抓到二阶项"><span>#</span></a></h3><p>把 <span><span>ff</span><span><span><span></span><span>f</span></span></span></span> 在 <span><span>μ\mu</span><span><span><span></span><span>μ</span></span></span></span> 处展开到二阶：</p><span><span><span>f(μ+δ)=f(μ)+Jδ+12∑kek δTHkδ+O(∣δ∣3)f(\mu+\delta) = f(\mu) + J\delta + \tfrac12\sum_ke_k\,\delta^TH_k\delta+O(|\delta|^3)</span><span><span><span></span><span>f</span><span>(</span><span>μ</span><span></span><span>+</span><span></span></span><span><span></span><span>δ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span>μ</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>J</span><span>δ</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>δ</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>δ</span><span></span><span>+</span><span></span></span><span><span></span><span>O</span><span>(</span><span>∣</span><span>δ</span><span><span>∣</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>)</span></span></span></span></span><p><span><span>JJ</span><span><span><span></span><span>J</span></span></span></span> 是雅可比，<span><span>HkH_k</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是第 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 个输出分量的海森矩阵，<span><span>eke_k</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是单位向量。真实的输出均值（<span><span>δ∼N(0,Σ)\delta\sim\mathcal N(0,\Sigma)</span><span><span><span></span><span>δ</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span>Σ</span><span>)</span></span></span></span> 对称，奇次项为零）：</p><span><span><span>E[f(x)]=f(μ)+12∑kektr⁡(HkΣ)+O(4 阶)E[f(x)] = f(\mu)+\tfrac12\sum_ke_k\operatorname{tr}(H_k\Sigma)+O(\text{4 阶})</span><span><span><span></span><span>E</span><span>[</span><span>f</span><span>(</span><span>x</span><span>)]</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span>μ</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>tr</span></span><span>(</span><span><span>H</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Σ</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>O</span><span>(</span><span><span>4 </span><span>阶</span></span><span>)</span></span></span></span></span><p>线性化给 <span><span>f(μ)f(\mu)</span><span><span><span></span><span>f</span><span>(</span><span>μ</span><span>)</span></span></span></span>，漏掉了第二项。UT 的均值是</p><span><span><span>∑iWimf(μ+χi)=f(μ)∑iWim+J∑iWimχi+12∑kek∑iWimχiTHkχi\sum_iW_i^mf(\mu+\chi_i) = f(\mu)\sum_iW^m_i + J\sum_iW_i^m\chi_i+\tfrac12\sum_ke_k\sum_iW_i^m\chi_i^TH_k\chi_i</span><span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>f</span><span>(</span><span>μ</span><span></span><span>+</span><span></span></span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span>μ</span><span>)</span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>J</span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>权重和为 1，一次项因对称选点而为零，二次项用 <span><span>∑iWiχiχiT=Σ\sum_iW_i\chi_i\chi_i^T = \Sigma</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>Σ</span></span></span></span> 得到 <span><span>∑iWiχiTHkχi=tr⁡(HkΣ)\sum_iW_i\chi_i^TH_k\chi_i = \operatorname{tr}(H_k\Sigma)</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>tr</span></span><span>(</span><span><span>H</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Σ</span><span>)</span></span></span></span>。于是 UT 的均值和真实均值在二阶项上相同。协方差的一阶项 <span><span>JΣJTJ\Sigma J^T</span><span><span><span></span><span>J</span><span>Σ</span><span><span>J</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 也被精确抓住。四阶以后的项会有误差，参数 <span><span>β\beta</span><span><span><span></span><span>β</span></span></span></span> 就是用来在高斯情形下补偿四阶矩的。</p><p>玩具问题里，<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 的二阶项是 <span><span>12tr⁡(HyΣ)=−12rsin⁡θ σθ2=−0.125\tfrac12\operatorname{tr}(H_y\Sigma) = -\tfrac12r\sin\theta\,\sigma_\theta^2 = -0.125</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span><span>tr</span></span><span>(</span><span><span>H</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Σ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>r</span><span></span><span>sin</span><span></span><span>θ</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>θ</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>0.125</span></span></span></span>，对应线性化和真实均值的差（实测 0.1173，差别来自更高阶）。</p></section><section><h3>缩放参数<a href="#缩放参数"><span>#</span></a></h3><p>标准的缩放 UT 有三个参数 <span><span>α,β,κ\alpha,\beta,\kappa</span><span><span><span></span><span>α</span><span>,</span><span></span><span>β</span><span>,</span><span></span><span>κ</span></span></span></span>，<span><span>λ=α2(n+κ)−n\lambda = \alpha^2(n+\kappa)-n</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>α</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>(</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>κ</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>n</span></span></span></span>：</p><span><span><span>W0m=λn+λ,W0c=W0m+(1−α2+β),Wim=Wic=12(n+λ), i=1..2nW^m_0 = \frac{\lambda}{n+\lambda},\quad W^c_0 = W_0^m+(1-\alpha^2+\beta),\quad W^m_i = W^c_i = \frac1{2(n+\lambda)},\ i=1..2n</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>n</span><span></span><span>+</span><span></span><span>λ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>λ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>α</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>β</span><span>)</span><span>,</span><span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span>(</span><span>n</span><span></span><span>+</span><span></span><span>λ</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span> </span><span></span><span>i</span><span></span><span>=</span><span></span></span><span><span></span><span>1..2</span><span>n</span></span></span></span></span><p>我取 <span><span>α=1\alpha = 1</span><span><span><span></span><span>α</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，<span><span>β=2\beta = 2</span><span><span><span></span><span>β</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，<span><span>κ=0\kappa = 0</span><span><span><span></span><span>κ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，于是 <span><span>λ=0\lambda = 0</span><span><span><span></span><span>λ</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>：<span><span>W0m=0W_0^m = 0</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>W0c=2W_0^c = 2</span><span><span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>，其余 12 个点权重都是 <span><span>1/121/12</span><span><span><span></span><span>1/12</span></span></span></span>，全部是正数。常见的另一种选择是 <span><span>α=10−3\alpha = 10^{-3}</span><span><span><span></span><span>α</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，让 sigma 点贴近均值（局部性好）。但它的代价是 <span><span>λ≈−n\lambda\approx-n</span><span><span><span></span><span>λ</span><span></span><span>≈</span><span></span></span><span><span></span><span>−</span><span>n</span></span></span></span>，<span><span>n+λ≈nα2n+\lambda\approx n\alpha^2</span><span><span><span></span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>λ</span><span></span><span>≈</span><span></span></span><span><span></span><span>n</span><span><span>α</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 极小，中心点权重变成 <span><span>−n/(nα2)≈−106-n/(n\alpha^2)\approx-10^6</span><span><span><span></span><span>−</span><span>n</span><span>/</span><span>(</span><span>n</span><span><span>α</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>−</span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span>，其他点权重是 <span><span>+1/(2nα2)≈8×104+1/(2n\alpha^2)\approx8\times10^4</span><span><span><span></span><span>+</span><span>1/</span><span>(</span><span>2</span><span>n</span><span><span>α</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>8</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span></span></span></span>：几十万量级的正负权重相消，在 float32 里精度会丢得很难看。<span><span>α=1\alpha = 1</span><span><span><span></span><span>α</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的代价是 sigma 点离均值 <span><span>nσ\sqrt n\sigma</span><span><span><span></span><span><span><span><span><span><span></span><span>n</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>σ</span></span></span></span> 远，对于很强的非线性，这个”非局部”采样会有问题。折衷之下，我为了第 8 篇的 float32 选了 <span><span>α=1\alpha = 1</span><span><span><span></span><span>α</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>。</p></section><section><h3>加性噪声 UKF<a href="#加性噪声-ukf"><span>#</span></a></h3><p>状态 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>，过程 <span><span>x′=f(x)+wx' = f(x)+w</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span></span></span></span>，测量 <span><span>z=h(x)+vz = h(x)+v</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span>(</span><span>x</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span>，噪声加性。</p><p><strong>预测：</strong> 从 <span><span>(x^,P)(\hat x,P)</span><span><span><span></span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>,</span><span></span><span>P</span><span>)</span></span></span></span> 生成 sigma 点 <span><span>χi\chi_i</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>；<span><span>yi=f(χi)y_i = f(\chi_i)</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>；</p><span><span><span>x^−=∑iWimyi,P−=∑iWic(yi−x^−)(yi−x^−)T+Q\hat x^- = \sum_iW_i^my_i,\qquad P^- = \sum_iW_i^c(y_i-\hat x^-)(y_i-\hat x^-)^T+Q</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Q</span></span></span></span></span><p><strong>更新：</strong> 从 <span><span>(x^−,P−)(\hat x^-,P^-)</span><span><span><span></span><span>(</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 重新生成 sigma 点；<span><span>zi=h(χi)z_i = h(\chi_i)</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span>(</span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>；</p><span><span><span>z^=∑iWimzi,S=∑iWic(zi−z^)(zi−z^)T+R,Pxz=∑iWic(χi−x^−)(zi−z^)T\hat z = \sum_iW_i^mz_i,\quad S = \sum_iW_i^c(z_i-\hat z)(z_i-\hat z)^T+R,\quad P_{xz} = \sum_iW_i^c(\chi_i-\hat x^-)(z_i-\hat z)^T</span><span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span>(</span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span><span>,</span><span></span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>x</span><span>z</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>∑</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span>(</span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><span><span><span>K=PxzS−1,x^=x^−+K(z−z^),P=P−−KSKTK = P_{xz}S^{-1},\qquad\hat x = \hat x^-+K(z-\hat z),\qquad P = P^--KSK^T</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>x</span><span>z</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>S</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>最后一个等式：<span><span>KSKT=PxzS−1PxzTKSK^T = P_{xz}S^{-1}P_{xz}^T</span><span><span><span></span><span>K</span><span>S</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>x</span><span>z</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>x</span><span>z</span></span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。检验：<span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 是线性的 <span><span>h(x)=Hxh(x) = Hx</span><span><span><span></span><span>h</span><span>(</span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>x</span></span></span></span> 时，<span><span>zi−z^=H(χi−x^−)z_i-\hat z = H(\chi_i-\hat x^-)</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>z</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>(</span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>Pxz=P−HTP_{xz} = P^-H^T</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>x</span><span>z</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，<span><span>S=HP−HT+RS = HP^-H^T+R</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span></span></span></span>，UKF 恰好还原成第 2 篇的线性卡尔曼滤波。</p></section><section><h3>四元数的均值<a href="#四元数的均值"><span>#</span></a></h3><p>怎么对旋转取”均值”？对四元数的四个分量直接取加权平均，不能：结果不是单位四元数，而且 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 与 <span><span>−q-q</span><span><span><span></span><span>−</span><span>q</span></span></span></span> 是同一个旋转，<span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 和 <span><span>−q-q</span><span><span><span></span><span>−</span><span>q</span></span></span></span> 的平均是零。解法就是第 6 篇的做法：在名义姿态的切空间里工作。</p><ul>
<li>生成 sigma 点：用 6 维误差向量 <span><span>[δθi,δbi][\delta\theta_i,\delta b_i]</span><span><span><span></span><span>[</span><span>δ</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>δ</span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span> 的 sigma 点，每个点对应一个姿态 <span><span>qi=q^⊗exp⁡(δθi)q_i = \hat q\otimes\exp(\delta\theta_i)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。</li>
<li>传播：每个点各自过一遍陀螺模型，<span><span>qi←qi⊗exp⁡((ωm−bi)Δt)q_i\leftarrow q_i\otimes\exp((\omega_m-b_i)\Delta t)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>((</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>Δ</span><span>t</span><span>)</span></span></span></span>。</li>
<li>取参考：把传播后的中心点 <span><span>qrefq_{ref}</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 当参考，每个点相对参考的偏差 <span><span>δθi=log⁡(qref−1⊗qi)\delta\theta_i = \log(q_{ref}^{-1}\otimes q_i)</span><span><span><span></span><span>δ</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>lo<span>g</span></span><span>(</span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>（就是第 4 篇的 <code>att_error</code>），这些偏差是普通向量，可以加权平均、求协方差。</li>
<li>回到流形：<span><span>q^−=qref⊗exp⁡(δθˉ)\hat q^- = q_{ref}\otimes\exp(\bar{\delta\theta})</span><span><span><span></span><span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span><span><span><span><span><span></span><span><span>δ</span><span>θ</span></span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span>)</span></span></span></span>。</li>
</ul><p>测量更新同样：用名义姿态周围的 sigma 点，对每个点算第 6 篇的加速度计模型（重力方向向量）和航向模型，其余是标准 UKF。注入、重置、门控、噪声参数与第 6 篇完全一样。</p><p><strong>一个航向的小处理。</strong> 航向是个角度，有折叠问题。在 90 度初始误差下 sigma 点会分散到 <span><span>6⋅π/2=3.85\sqrt6\cdot\pi/2 = 3.85</span><span><span><span></span><span><span><span><span><span><span></span><span>6</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>π</span><span>/2</span><span></span><span>=</span><span></span></span><span><span></span><span>3.85</span></span></span></span> rad（超过 <span><span>π\pi</span><span><span><span></span><span>π</span></span></span></span>），朴素地对折叠后的角度加权平均会出错。我让每个点的航向相对中心点的航向取差（折到 <span><span>(−π,π](-\pi,\pi]</span><span><span><span></span><span>(</span><span>−</span><span>π</span><span>,</span><span></span><span>π</span><span>]</span></span></span></span>），再加权平均，最后加回中心点的航向。</p></section></section>
<section><h2>玩具问题的实现<a href="#玩具问题的实现"><span>#</span></a></h2><p>玩具问题只在 Python 里。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>toy</span><span>():</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Polar to Cartesian: true moments (Monte Carlo) vs linearization vs unscented transform."""</span></div></div><div><div><div>3</div></div><div><span>    </span><span>from</span><span> kl.plotting </span><span>import</span><span> L, save</span></div></div><div><div><div>4</div></div><div><span>    </span><span>import</span><span> matplotlib.pyplot </span><span>as</span><span> plt</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>mu </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>1.0</span><span><span>, np.pi </span><span>/</span><span> </span></span><span>2</span><span>])</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> np.</span><span>diag</span><span>([</span><span>0.02</span><span>**</span><span>2</span><span>, </span><span>0.5</span><span>**</span><span>2</span><span>])</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> </span><span>lambda</span><span> </span><span>rt</span><span><span>: np.</span><span>column_stack</span><span>([rt[:, </span></span><span>0</span><span><span>] </span><span>*</span><span> np.</span><span>cos</span><span>(rt[:, </span></span><span>1</span><span>]), rt[:, </span><span>0</span><span><span>] </span><span>*</span><span> np.</span><span>sin</span><span>(rt[:, </span></span><span>1</span><span>])])  </span><span># noqa: E731</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>smp </span><span>=</span><span> rng.</span><span>multivariate_normal</span><span>(mu, S, </span><span>1_000_000</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>ys </span><span>=</span><span> </span><span>f</span><span>(smp)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>m_mc, c_mc </span><span>=</span><span> ys.</span><span>mean</span><span>(</span><span>axis</span><span>=</span><span>0</span><span><span>), np.</span><span>cov</span><span>(ys.T)</span></span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>J </span><span>=</span><span> np.</span><span>array</span><span>([[np.</span><span>cos</span><span>(mu[</span><span>1</span><span><span>]), </span><span>-</span><span>mu[</span></span><span>0</span><span><span>] </span><span>*</span><span> np.</span><span>sin</span><span>(mu[</span></span><span>1</span><span><span>])], [np.</span><span>sin</span><span>(mu[</span></span><span>1</span><span>]), mu[</span><span>0</span><span><span>] </span><span>*</span><span> np.</span><span>cos</span><span>(mu[</span></span><span>1</span><span>])]])</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>m_lin, c_lin </span><span>=</span><span> </span><span>f</span><span>(mu[</span><span>None</span><span>])[</span><span>0</span><span><span>], J </span><span>@</span><span> S </span><span>@</span><span> J.T</span></span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>X, Wm, Wc </span><span>=</span><span> </span><span>sigma_points</span><span>(S)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>m_ut, c_ut </span><span>=</span><span> </span><span>unscented_transform</span><span>(</span><span>f</span><span>(mu </span><span>+</span><span> X), Wm, Wc)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>"Polar to Cartesian: r ~ N(1, 0.02^2), theta ~ N(pi/2, 0.5^2)"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>''</span><span>:16s</span><span>}</span><span> </span><span>{</span><span>'mean x'</span><span>:&gt;9s</span><span>}</span><span> </span><span>{</span><span>'mean y'</span><span>:&gt;9s</span><span>}</span><span> </span><span>{</span><span>'var x'</span><span>:&gt;10s</span><span>}</span><span> </span><span>{</span><span>'var y'</span><span>:&gt;10s</span><span>}</span><span> </span><span>{</span><span>'cov xy'</span><span>:&gt;10s</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>for</span><span> name, m, c </span><span>in</span><span> ((</span><span>"Monte Carlo 1e6"</span><span>, m_mc, c_mc), (</span><span>"linearization"</span><span>, m_lin, c_lin), (</span><span>"unscented"</span><span>, m_ut, c_ut)):</span></div></div><div><div><div>20</div></div><div><span>        </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>:16s</span><span>}</span><span> </span><span>{</span><span>m[</span><span>0</span><span>]</span><span>:9.4f</span><span>}</span><span> </span><span>{</span><span>m[</span><span>1</span><span>]</span><span>:9.4f</span><span>}</span><span> </span><span>{</span><span>c[</span><span>0</span><span>, </span><span>0</span><span>]</span><span>:10.5f</span><span>}</span><span> </span><span>{</span><span>c[</span><span>1</span><span>, </span><span>1</span><span>]</span><span>:10.5f</span><span>}</span><span> </span><span>{</span><span>c[</span><span>0</span><span>, </span><span>1</span><span>]</span><span>:10.5f</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>sigma_points</code> 和 <code>unscented_transform</code> 是后面姿态 UKF 共用的两个函数。</p></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Unscented transform and an error-state unscented Kalman filter for attitude."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>from</span><span> .imu_sim </span><span>import</span><span> G</span></div></div><div><div><div>6</div></div><div><span>from</span><span> .quat </span><span>import</span><span><span> </span><span>SMALL</span><span>, qrot, skew, wrap_pi</span></span></div></div><div><div><div>7</div></div><div><span>from</span><span> .update </span><span>import</span><span> joseph_update  </span><span># noqa: F401  (kept for symmetry with the other filters)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>sigma_points</span><span>(</span><span>P</span><span>,</span><span><span> </span><span>alpha</span></span><span>=</span><span>1.0</span><span>,</span><span><span> </span><span>beta</span></span><span>=</span><span>2.0</span><span>,</span><span><span> </span><span>kappa</span></span><span>=</span><span>0.0</span><span>):</span></div></div><div><div><div>11</div></div><div><span>    </span><span>"""Scaled sigma points around zero for N(0, P): returns (X, Wm, Wc).</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>X has 2n+1 rows: the centre, then +-columns of chol((n + lambda) P).</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> P.shape[</span><span>0</span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>lam </span><span>=</span><span> alpha</span><span>**</span><span>2</span><span><span> </span><span>*</span><span> (n </span><span>+</span><span> kappa) </span><span>-</span><span> n</span></span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.linalg.</span><span>cholesky</span><span>((n </span><span>+</span><span> lam) </span><span>*</span><span> P)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>X </span><span>=</span><span> np.</span><span>vstack</span><span>([np.</span><span>zeros</span><span>(n), L.T, </span><span>-</span><span>L.T])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>Wm </span><span>=</span><span> np.</span><span>full</span><span>(</span><span>2</span><span><span> </span><span>*</span><span> n </span><span>+</span><span> </span></span><span>1</span><span>, </span><span>0.5</span><span><span> </span><span>/</span><span> (n </span><span>+</span><span> lam))</span></span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>Wc </span><span>=</span><span> Wm.</span><span>copy</span><span>()</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>Wm[</span><span>0</span><span><span>] </span><span>=</span><span> lam </span><span>/</span><span> (n </span><span>+</span><span> lam)</span></span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>Wc[</span><span>0</span><span><span>] </span><span>=</span><span> Wm[</span></span><span>0</span><span><span>] </span><span>+</span><span> (</span></span><span>1.0</span><span><span> </span><span>-</span><span> alpha</span><span>**</span></span><span>2</span><span><span> </span><span>+</span><span> beta)</span></span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span> X, Wm, Wc</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>unscented_transform</span><span>(</span><span>Y</span><span>,</span><span><span> </span><span>Wm</span></span><span>,</span><span><span> </span><span>Wc</span></span><span>,</span><span><span> </span><span>noise</span></span><span>=</span><span>None</span><span>):</span></div></div><div><div><div>27</div></div><div><span>    </span><span>"""Weighted mean and covariance of transformed sigma points Y (rows)."""</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>mean </span><span>=</span><span> Wm </span><span>@</span><span> Y</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>dev </span><span>=</span><span> Y </span><span>-</span><span> mean</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>cov </span><span>=</span><span> (Wc[:, </span><span>None</span><span><span>] </span><span>*</span><span> dev).T </span><span>@</span><span> dev</span></span></div></div><div><div><div>31</div></div><div><span>    </span><span>return</span><span> mean, cov </span><span>if</span><span> noise </span><span>is</span><span> </span><span>None</span><span> </span><span>else</span><span><span> cov </span><span>+</span><span> noise</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span># ---- row-wise quaternion helpers (same formulas as kl.quat) ----</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>def</span><span> </span><span>qexp_rows</span><span>(</span><span>V</span><span>):</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>th </span><span>=</span><span> np.</span><span>sqrt</span><span>(np.</span><span>sum</span><span>(V </span><span>*</span><span> V, </span><span>axis</span><span>=</span><span>1</span><span>))</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>small </span><span>=</span><span> th </span><span>&lt;</span><span> </span><span>SMALL</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>ths </span><span>=</span><span> np.</span><span>where</span><span>(small, </span><span>1.0</span><span>, th)</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> np.</span><span>where</span><span>(small, </span><span>1.0</span><span><span> </span><span>-</span><span> th </span><span>*</span><span> th </span><span>/</span><span> </span></span><span>8.0</span><span><span>, np.</span><span>cos</span><span>(</span></span><span>0.5</span><span><span> </span><span>*</span><span> th))</span></span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> np.</span><span>where</span><span>(small, </span><span>0.5</span><span><span>, np.</span><span>sin</span><span>(</span></span><span>0.5</span><span><span> </span><span>*</span><span> th) </span><span>/</span><span> ths)</span></span></div></div><div><div><div>42</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>column_stack</span><span>([w, s[:, </span></span><span>None</span><span><span>] </span><span>*</span><span> V])</span></span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>def</span><span> </span><span>qmul_rows</span><span>(</span><span>a</span><span>,</span><span><span> </span><span>b</span></span><span>):</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>a, b </span><span>=</span><span> np.</span><span>broadcast_arrays</span><span>(np.</span><span>atleast_2d</span><span>(a), np.</span><span>atleast_2d</span><span>(b))</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>aw, ax, ay, az </span><span>=</span><span> a.T</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>bw, bx, by, bz </span><span>=</span><span> b.T</span></div></div><div><div><div>49</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>column_stack</span><span>([</span></span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> bw </span><span>-</span><span> ax </span><span>*</span><span> bx </span><span>-</span><span> ay </span><span>*</span><span> by </span><span>-</span><span> az </span><span>*</span><span> bz,</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> bx </span><span>+</span><span> ax </span><span>*</span><span> bw </span><span>+</span><span> ay </span><span>*</span><span> bz </span><span>-</span><span> az </span><span>*</span><span> by,</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> by </span><span>-</span><span> ax </span><span>*</span><span> bz </span><span>+</span><span> ay </span><span>*</span><span> bw </span><span>+</span><span> az </span><span>*</span><span> bx,</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> bz </span><span>+</span><span> ax </span><span>*</span><span> by </span><span>-</span><span> ay </span><span>*</span><span> bx </span><span>+</span><span> az </span><span>*</span><span> bw,</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>])</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>def</span><span> </span><span>qlog_rows</span><span>(</span><span>Q</span><span>):</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>where</span><span>((Q[:, </span><span>0</span><span><span>] </span><span>&lt;</span><span> </span></span><span>0</span><span>)[:, </span><span>None</span><span><span>], </span><span>-</span><span>Q, Q)</span></span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> Q[:, </span><span>1</span><span>:]</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> np.</span><span>sqrt</span><span>(np.</span><span>sum</span><span>(u </span><span>*</span><span> u, </span><span>axis</span><span>=</span><span>1</span><span>))</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>small </span><span>=</span><span> n </span><span>&lt;</span><span> </span><span>SMALL</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>ns </span><span>=</span><span> np.</span><span>where</span><span>(small, </span><span>1.0</span><span>, n)</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>k </span><span>=</span><span> np.</span><span>where</span><span>(small, </span><span>2.0</span><span><span> </span><span>/</span><span> Q[:, </span></span><span>0</span><span>], </span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>arctan2</span><span>(n, Q[:, </span></span><span>0</span><span><span>]) </span><span>/</span><span> ns)</span></span></div></div><div><div><div>64</div></div><div><span>    </span><span>return</span><span> k[:, </span><span>None</span><span><span>] </span><span>*</span><span> u</span></span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>def</span><span> </span><span>rot_inv_rows</span><span>(</span><span>Q</span><span>,</span><span><span> </span><span>v</span></span><span>):</span></div></div><div><div><div>68</div></div><div><span>    </span><span>"""C(q_i)^T v for every row (unit q_i): (w^2 - u.u) v + 2 (u.v) u - 2 w (u x v)."""</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>w, u </span><span>=</span><span> Q[:, :</span><span>1</span><span>], Q[:, </span><span>1</span><span>:]</span></div></div><div><div><div>70</div></div><div><span>    </span><span>return</span><span><span> (w </span><span>*</span><span> w </span><span>-</span><span> np.</span><span>sum</span><span>(u </span><span>*</span><span> u, </span></span><span>axis</span><span>=</span><span>1</span><span>, </span><span>keepdims</span><span>=</span><span>True</span><span><span>)) </span><span>*</span><span> v </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> (u </span><span>@</span><span> v)[:, </span></span><span>None</span><span><span>] </span><span>*</span><span> u </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> w </span><span>*</span><span> np.</span><span>cross</span><span>(u, v)</span></span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>class</span><span><span> </span><span>AttitudeUKF</span></span><span>:</span></div></div><div><div><div>74</div></div><div><span>    </span><span>"""Error-state UKF: same 6-dim error as the ESKF, additive noise, scaled unscented transform."""</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>sigma_g</span></span><span>,</span><span><span> </span><span>sigma_bw</span></span><span>,</span><span><span> </span><span>sigma_a</span></span><span>,</span><span><span> </span><span>sigma_m</span></span><span>,</span><span><span> </span><span>m_w</span></span><span>,</span><span><span> </span><span>q0</span></span><span>,</span><span><span> </span><span>P0</span></span><span>,</span></div></div><div><div><div>77</div></div><div><span><span>                 </span></span><span>accel_gate</span><span>=</span><span>0.5</span><span>,</span><span><span> </span><span>mag_gate</span></span><span>=</span><span>0.1</span><span>,</span><span><span> </span><span>reset</span></span><span>=</span><span>True</span><span>,</span><span><span> </span><span>alpha</span></span><span>=</span><span>1.0</span><span>,</span><span><span> </span><span>beta</span></span><span>=</span><span>2.0</span><span>,</span><span><span> </span><span>kappa</span></span><span>=</span><span>0.0</span><span>):</span></div></div><div><div><div>78</div></div><div><span>        </span><span>self</span><span>.sg, </span><span>self</span><span>.sbw, </span><span>self</span><span>.sa, </span><span>self</span><span><span>.sm </span><span>=</span><span> sigma_g, sigma_bw, sigma_a, sigma_m</span></span></div></div><div><div><div>79</div></div><div><span>        </span><span>self</span><span><span>.m_w </span><span>=</span><span> np.</span><span>asarray</span><span>(m_w, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>80</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> np.</span><span>asarray</span><span>(q0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>81</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> np.</span><span>zeros</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>82</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> np.</span><span>asarray</span><span>(P0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>83</div></div><div><span>        </span><span>self</span><span>.accel_gate, </span><span>self</span><span>.mag_gate, </span><span>self</span><span><span>.reset </span><span>=</span><span> accel_gate, mag_gate, reset</span></span></div></div><div><div><div>84</div></div><div><span>        </span><span>self</span><span><span>.ut </span><span>=</span><span> (alpha, beta, kappa)</span></span></div></div><div><div><div>85</div></div><div><span>        </span><span>self</span><span><span>.last_nis </span><span>=</span><span> np.nan</span></span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>    </span><span>def</span><span> </span><span>predict</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>gyro</span></span><span>,</span><span><span> </span><span>dt</span></span><span>):</span></div></div><div><div><div>88</div></div><div><span><span>        </span></span><span>X, Wm, Wc </span><span>=</span><span> </span><span>sigma_points</span><span>(</span><span>self</span><span>.P, *</span><span>self</span><span>.ut)</span></div></div><div><div><div>89</div></div><div><span><span>        </span></span><span>qi </span><span>=</span><span> </span><span>qmul_rows</span><span>(</span><span>self</span><span><span>.q, </span><span>qexp_rows</span><span>(X[:, :</span></span><span>3</span><span>]))</span></div></div><div><div><div>90</div></div><div><span><span>        </span></span><span>bi </span><span>=</span><span> </span><span>self</span><span><span>.b </span><span>+</span><span> X[:, </span></span><span>3</span><span>:]</span></div></div><div><div><div>91</div></div><div><span><span>        </span></span><span>qi </span><span>=</span><span> </span><span>qmul_rows</span><span>(qi, </span><span>qexp_rows</span><span>((gyro </span><span>-</span><span> bi) </span><span>*</span><span> dt))  </span><span># each point through the gyro model</span></div></div><div><div><div>92</div></div><div><span><span>        </span></span><span>q_ref </span><span>=</span><span> qi[</span><span>0</span><span>]  </span><span># the propagated centre point</span></div></div><div><div><div>93</div></div><div><span><span>        </span></span><span>D </span><span>=</span><span> np.</span><span>column_stack</span><span>([</span><span>qlog_rows</span><span>(</span><span>qmul_rows</span><span>(q_ref </span><span>*</span><span> np.</span><span>array</span><span>([</span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span><span>]), qi)), bi </span><span>-</span><span> </span></span><span>self</span><span>.b])</span></div></div><div><div><div>94</div></div><div><span><span>        </span></span><span>mean, cov </span><span>=</span><span> </span><span>unscented_transform</span><span>(D, Wm, Wc)</span></div></div><div><div><div>95</div></div><div><span><span>        </span></span><span>Q </span><span>=</span><span> np.</span><span>diag</span><span>([</span><span>self</span><span><span>.sg</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> dt</span><span>**</span></span><span>2</span><span><span>] </span><span>*</span><span> </span></span><span>3</span><span><span> </span><span>+</span><span> [</span></span><span>self</span><span><span>.sbw</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> dt] </span><span>*</span><span> </span></span><span>3</span><span>)</span></div></div><div><div><div>96</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span><span>qmul_rows</span><span>(q_ref, </span><span>qexp_rows</span><span>(mean[</span></span><span>None</span><span>, :</span><span>3</span><span>]))[</span><span>0</span><span>]</span></div></div><div><div><div>97</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> </span></span><span>self</span><span><span>.b </span><span>+</span><span> mean[</span></span><span>3</span><span>:]</span></div></div><div><div><div>98</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> cov </span><span>+</span><span> Q</span></span></div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div><span>    </span><span>def</span><span> </span><span>_inject</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>dx</span></span><span>):</span></div></div><div><div><div>101</div></div><div><span>        </span><span>from</span><span> .quat </span><span>import</span><span> qexp, qmul, qnormalize</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div><span><span>        </span></span><span>dth </span><span>=</span><span> dx[:</span><span>3</span><span>]</span></div></div><div><div><div>104</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span><span>qnormalize</span><span>(</span><span>qmul</span><span>(</span></span><span>self</span><span><span>.q, </span><span>qexp</span><span>(dth)))</span></span></div></div><div><div><div>105</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> </span></span><span>self</span><span><span>.b </span><span>+</span><span> dx[</span></span><span>3</span><span>:]</span></div></div><div><div><div>106</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.reset:</span></div></div><div><div><div>107</div></div><div><span><span>            </span></span><span>Gm </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>108</div></div><div><span><span>            </span></span><span>Gm[:</span><span>3</span><span>, :</span><span>3</span><span><span>] </span><span>-=</span><span> </span><span>skew</span><span>(</span></span><span>0.5</span><span><span> </span><span>*</span><span> dth)</span></span></div></div><div><div><div>109</div></div><div><span>            </span><span>self</span><span><span>.P </span><span>=</span><span> Gm </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> Gm.T</span></span></div></div><div><div><div>110</div></div><div>
</div></div><div><div><div>111</div></div><div><span>    </span><span>def</span><span> </span><span>_correct</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>X</span></span><span>,</span><span><span> </span><span>Wm</span></span><span>,</span><span><span> </span><span>Wc</span></span><span>,</span><span><span> </span><span>Z</span></span><span>,</span><span><span> </span><span>y</span></span><span>,</span><span><span> </span><span>R</span></span><span>):</span></div></div><div><div><div>112</div></div><div><span>        </span><span>"""Standard UKF measurement update given model outputs Z at the sigma points."""</span></div></div><div><div><div>113</div></div><div><span><span>        </span></span><span>zbar </span><span>=</span><span> Wm </span><span>@</span><span> Z</span></div></div><div><div><div>114</div></div><div><span><span>        </span></span><span>dz </span><span>=</span><span> Z </span><span>-</span><span> zbar</span></div></div><div><div><div>115</div></div><div><span><span>        </span></span><span>S </span><span>=</span><span> (Wc[:, </span><span>None</span><span><span>] </span><span>*</span><span> dz).T </span><span>@</span><span> dz </span><span>+</span><span> R</span></span></div></div><div><div><div>116</div></div><div><span><span>        </span></span><span>Pxz </span><span>=</span><span> (Wc[:, </span><span>None</span><span><span>] </span><span>*</span><span> X).T </span><span>@</span><span> dz</span></span></div></div><div><div><div>117</div></div><div><span><span>        </span></span><span>L </span><span>=</span><span> np.linalg.</span><span>cholesky</span><span>(S)</span></div></div><div><div><div>118</div></div><div><span><span>        </span></span><span>K </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(L.T, np.linalg.</span><span>solve</span><span>(L, Pxz.T)).T</span></div></div><div><div><div>119</div></div><div><span><span>        </span></span><span>dx </span><span>=</span><span> K </span><span>@</span><span> y</span></div></div><div><div><div>120</div></div><div><span><span>        </span></span><span>P_new </span><span>=</span><span> </span><span>self</span><span><span>.P </span><span>-</span><span> K </span><span>@</span><span> S </span><span>@</span><span> K.T</span></span></div></div><div><div><div>121</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> </span></span><span>0.5</span><span><span> </span><span>*</span><span> (P_new </span><span>+</span><span> P_new.T)</span></span></div></div><div><div><div>122</div></div><div><span>        </span><span>self</span><span><span>.last_nis </span><span>=</span><span> </span></span><span>float</span><span><span>(y </span><span>@</span><span> np.linalg.</span><span>solve</span><span>(S, y))</span></span></div></div><div><div><div>123</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_inject</span><span>(dx)</span></span></div></div><div><div><div>124</div></div><div>
</div></div><div><div><div>125</div></div><div><span>    </span><span>def</span><span> </span><span>update_accel</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>accel</span></span><span>):</span></div></div><div><div><div>126</div></div><div><span><span>        </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(accel </span><span>@</span><span> accel)</span></div></div><div><div><div>127</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.accel_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> G) </span><span>&gt;</span><span> </span></span><span>self</span><span>.accel_gate:</span></div></div><div><div><div>128</div></div><div><span>            </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>129</div></div><div><span><span>        </span></span><span>X, Wm, Wc </span><span>=</span><span> </span><span>sigma_points</span><span>(</span><span>self</span><span>.P, *</span><span>self</span><span>.ut)</span></div></div><div><div><div>130</div></div><div><span><span>        </span></span><span>qi </span><span>=</span><span> </span><span>qmul_rows</span><span>(</span><span>self</span><span><span>.q, </span><span>qexp_rows</span><span>(X[:, :</span></span><span>3</span><span>]))</span></div></div><div><div><div>131</div></div><div><span><span>        </span></span><span>Z </span><span>=</span><span> </span><span>rot_inv_rows</span><span>(qi, np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]))</span></div></div><div><div><div>132</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> accel </span><span>/</span><span> norm </span><span>-</span><span> Wm </span><span>@</span><span> Z</span></div></div><div><div><div>133</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_correct</span><span>(X, Wm, Wc, Z, y, (</span></span><span>self</span><span><span>.sa </span><span>/</span><span> G) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>))</span></div></div><div><div><div>134</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div><div><div><div>135</div></div><div>
</div></div><div><div><div>136</div></div><div><span>    </span><span>def</span><span> </span><span>update_mag_heading</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>mag</span></span><span>):</span></div></div><div><div><div>137</div></div><div><span><span>        </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(mag </span><span>@</span><span> mag)</span></div></div><div><div><div>138</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.mag_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> </span></span><span>1.0</span><span><span>) </span><span>&gt;</span><span> </span></span><span>self</span><span>.mag_gate:</span></div></div><div><div><div>139</div></div><div><span>            </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>140</div></div><div><span><span>        </span></span><span>X, Wm, Wc </span><span>=</span><span> </span><span>sigma_points</span><span>(</span><span>self</span><span>.P, *</span><span>self</span><span>.ut)</span></div></div><div><div><div>141</div></div><div><span><span>        </span></span><span>qi </span><span>=</span><span> </span><span>qmul_rows</span><span>(</span><span>self</span><span><span>.q, </span><span>qexp_rows</span><span>(X[:, :</span></span><span>3</span><span>]))</span></div></div><div><div><div>142</div></div><div><span><span>        </span></span><span>C </span><span>=</span><span> </span><span>qrot</span><span>(</span><span>self</span><span>.q)</span></div></div><div><div><div>143</div></div><div><span><span>        </span></span><span>mw </span><span>=</span><span> </span><span>rot_inv_rows</span><span>(qi, </span><span>self</span><span><span>.m_w) </span><span>@</span><span> C.T  </span></span><span># C(q) C(q_i)^T m_w for every point</span></div></div><div><div><div>144</div></div><div><span><span>        </span></span><span>psi_h </span><span>=</span><span> np.</span><span>arctan2</span><span>(</span><span>self</span><span>.m_w[</span><span>1</span><span>], </span><span>self</span><span>.m_w[</span><span>0</span><span>])</span></div></div><div><div><div>145</div></div><div><span><span>        </span></span><span>Z0 </span><span>=</span><span> </span><span>wrap_pi</span><span>(np.</span><span>arctan2</span><span>(mw[:, </span><span>1</span><span>], mw[:, </span><span>0</span><span><span>]) </span><span>-</span><span> psi_h)</span></span></div></div><div><div><div>146</div></div><div><span><span>        </span></span><span>Zrel </span><span>=</span><span> </span><span>wrap_pi</span><span>(Z0 </span><span>-</span><span> Z0[</span><span>0</span><span>])[:, </span><span>None</span><span>]  </span><span># heading relative to the centre point: no wrap trouble</span></div></div><div><div><div>147</div></div><div><span><span>        </span></span><span>zbar </span><span>=</span><span> Z0[</span><span>0</span><span><span>] </span><span>+</span><span> Wm </span><span>@</span><span> Zrel[:, </span></span><span>0</span><span>]</span></div></div><div><div><div>148</div></div><div><span><span>        </span></span><span>m_meas </span><span>=</span><span> C </span><span>@</span><span> mag</span></div></div><div><div><div>149</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>wrap_pi</span><span>(</span><span>wrap_pi</span><span>(np.</span><span>arctan2</span><span>(m_meas[</span><span>1</span><span>], m_meas[</span><span>0</span><span><span>]) </span><span>-</span><span> psi_h) </span><span>-</span><span> zbar)])</span></span></div></div><div><div><div>150</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> (</span><span>self</span><span><span>.sm </span><span>/</span><span> np.</span><span>hypot</span><span>(</span></span><span>self</span><span>.m_w[</span><span>0</span><span>], </span><span>self</span><span>.m_w[</span><span>1</span><span><span>])) </span><span>**</span><span> </span></span><span>2</span></div></div><div><div><div>151</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_correct</span><span>(X, Wm, Wc, Zrel, y, np.</span><span>array</span><span>([[r]]))</span></span></div></div><div><div><div>152</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>sigma_points</code> 返回 <span><span>13×613\times6</span><span><span><span></span><span>13</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 的点（每行一个）和两组权重。后面四个 <code>*_rows</code> 函数是第 4 篇四元数函数的逐行批量版本，公式一模一样，只是对 13 个点一次算完，因为 Python 里循环 13 次太慢。<code>AttitudeUKF.predict</code> 是上一节”四元数的均值”的四个步骤。<code>_correct</code> 是标准 UKF 更新，<code>update_accel</code> 和 <code>update_mag_heading</code> 只负责给出模型输出 <code>Z</code> 和新息 <code>y</code>。</p></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>三种语言的结构和 Python 一致。区别：</p><ul>
<li><strong>C：</strong> sigma 点（<span><span>13×613\times6</span><span><span><span></span><span>13</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span>）、四元数数组、权重都是 <code>kl_ukf_att</code> 结构体里的定长数组，没有 <code>malloc</code>。<code>kl_ukf_sigma_points</code> 公开出来，因为 app 的自检要直接调它。</li>
<li><strong>C++：</strong> <code>SigmaPoints&lt;N, T&gt;</code> 是个小模板结构体，权重在 <code>compute</code> 里算出来；参数 <span><span>α,β,κ\alpha,\beta,\kappa</span><span><span><span></span><span>α</span><span>,</span><span></span><span>β</span><span>,</span><span></span><span>κ</span></span></span></span> 是 <code>constexpr</code> 成员，编译期常量。</li>
<li><strong>Rust：</strong> <code>sigma_points(&amp;P)</code> 返回 <code>Option&lt;[[Real; 6]; 13]&gt;</code>，Cholesky 失败是 <code>None</code>，权重函数 <code>unscented_weights()</code> 单独暴露。</li>
</ul><section><h3>C++<a href="#c"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Sigma points of N(0, P): the centre, then +-columns of chol((n + lambda) P).</span></div></div><div><div><div>2</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>SigmaPoints</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> std::</span><span>size_t</span><span> </span><span>COUNT</span><span> </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>T</span><span> </span><span>ALPHA</span><span> </span><span>=</span><span> </span><span>1</span><span>, </span><span>BETA</span><span> </span><span>=</span><span> </span><span>2</span><span>, </span><span>KAPPA</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>T</span><span> </span><span>LAMBDA</span><span> </span><span>=</span><span> </span><span>ALPHA</span><span> </span><span>*</span><span> </span><span>ALPHA</span><span> </span><span>*</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>KAPPA</span><span>) </span><span>-</span><span> </span><span>N</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>COUNT</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>X</span><span>;</span><span>  // one point per row</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>COUNT</span><span>&gt; </span><span>Wm</span><span>, </span><span>Wc</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>// Returns false if P is not positive definite.</span></div></div><div><div><div>12</div></div><div><span>  </span><span>bool</span><span> </span><span>compute</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>) {</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>P</span><span> </span><span>*</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>LAMBDA</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>15</div></div><div><span>    </span><span>X</span><span> </span><span>=</span><span> {};</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>j</span><span>++</span><span>)</span></div></div><div><div><div>17</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>j</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) {</span><span>  // L is lower triangular</span></div></div><div><div><div>18</div></div><div><span>        </span><span>X</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>j</span><span>, </span><span>k</span><span>) </span><span>=</span><span> </span><span>L</span><span>(</span><span>k</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>X</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>N</span><span> </span><span>+</span><span> </span><span>j</span><span>, </span><span>k</span><span>) </span><span>=</span><span> </span><span>-</span><span>L</span><span>(</span><span>k</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>    </span><span>Wm</span><span>.</span><span>fill</span><span>(</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>/</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>LAMBDA</span><span>));</span></div></div><div><div><div>22</div></div><div><span>    </span><span>Wc</span><span> </span><span>=</span><span> </span><span>Wm</span><span>;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>Wm</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>LAMBDA</span><span> </span><span>/</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>LAMBDA</span><span>);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Wc</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>Wm</span><span>[</span><span>0</span><span>] </span><span>+</span><span> (</span><span>1</span><span> </span><span>-</span><span> </span><span>ALPHA</span><span> </span><span>*</span><span> </span><span>ALPHA</span><span> </span><span>+</span><span> </span><span>BETA</span><span>);</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>// Returns false, leaving the state unchanged, if P is not positive definite.</span></div></div><div><div><div>2</div></div><div><span>  </span><span>bool</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>SigmaPoints</span><span>&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>sp</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>sp</span><span>.</span><span>compute</span><span>(</span><span>P</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;, </span><span>K</span><span>&gt; </span><span>qi</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;, </span><span>K</span><span>&gt; </span><span>bi</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>      </span><span>qi</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>rows3</span><span>(</span><span>sp</span><span>.</span><span>X</span><span>, </span><span>i</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>9</div></div><div><span>      </span><span>bi</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>rows3</span><span>(</span><span>sp</span><span>.</span><span>X</span><span>, </span><span>i</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>10</div></div><div><span>      </span><span>qi</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>qi</span><span>[</span><span>i</span><span>] </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>((</span><span>gyro</span><span> </span><span>-</span><span> </span><span>bi</span><span>[</span><span>i</span><span>]) </span><span>*</span><span> </span><span>dt</span><span>);</span><span>  // each point through the gyro model</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>q_ref</span><span> </span><span>=</span><span> </span><span>qi</span><span>[</span><span>0</span><span>];</span><span>  // the propagated centre point</span></div></div><div><div><div>13</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>K</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>D</span><span>;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>      </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>th</span><span> </span><span>=</span><span> (</span><span>q_ref</span><span>.</span><span>conj</span><span>() </span><span>*</span><span> </span><span>qi</span><span>[</span><span>i</span><span>]).</span><span>log</span><span>(), </span><span>db</span><span> </span><span>=</span><span> </span><span>bi</span><span>[</span><span>i</span><span>] </span><span>-</span><span> </span><span>b</span><span>;</span></div></div><div><div><div>16</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>D</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>th</span><span>(</span><span>j</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>18</div></div><div><span>        </span><span>D</span><span>(</span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>j</span><span>) </span><span>=</span><span> </span><span>db</span><span>(</span><span>j</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>mean</span><span>{};</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>23</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>6</span><span>; </span><span>j</span><span>++</span><span>) </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>j</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wm</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>D</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>cov</span><span>{};</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>26</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>r</span><span> </span><span>&lt;</span><span> </span><span>6</span><span>; </span><span>r</span><span>++</span><span>)</span></div></div><div><div><div>27</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>c</span><span> </span><span>&lt;</span><span> </span><span>6</span><span>; </span><span>c</span><span>++</span><span>)</span></div></div><div><div><div>28</div></div><div><span>          </span><span>cov</span><span>(</span><span>r</span><span>, </span><span>c</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wc</span><span>[</span><span>i</span><span>] </span><span>*</span><span> (</span><span>D</span><span>(</span><span>i</span><span>, </span><span>r</span><span>) </span><span>-</span><span> </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>r</span><span>)) </span><span>*</span><span> (</span><span>D</span><span>(</span><span>i</span><span>, </span><span>c</span><span>) </span><span>-</span><span> </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>c</span><span>));</span></div></div><div><div><div>29</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>30</div></div><div><span>      </span><span>cov</span><span>(</span><span>i</span><span>, </span><span>i</span><span>) </span><span>+=</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>dt</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>31</div></div><div><span>      </span><span>cov</span><span>(</span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>+=</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>q_ref</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>mean</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>1</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>2</span><span>)}});</span><span>  // not normalized here</span></div></div><div><div><div>34</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>mean</span><span>(</span><span>0</span><span>, </span><span>3</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>4</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>5</span><span>)}};</span></div></div><div><div><div>35</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>cov</span><span>;</span></div></div><div><div><div>36</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>// Heading-only magnetometer update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>2</div></div><div><span>  </span><span>bool</span><span> </span><span>update_mag_heading</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> (</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>mag_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>SigmaPoints</span><span>&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>sp</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>sp</span><span>.</span><span>compute</span><span>(</span><span>P</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>C</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>psi_h</span><span> </span><span>=</span><span> std::</span><span>atan2</span><span>(</span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>K</span><span>&gt; </span><span>z0</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>      </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>mw</span><span> </span><span>=</span><span> </span><span>C</span><span> </span><span>*</span><span> </span><span>rot_inv</span><span>(</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>rows3</span><span>(</span><span>sp</span><span>.</span><span>X</span><span>, </span><span>i</span><span>, </span><span>0</span><span>)), </span><span>m_w</span><span>);</span></div></div><div><div><div>12</div></div><div><span>      </span><span>z0</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> </span><span>psi_h</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>K</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>Zrel</span><span>;</span><span>  // heading relative to the centre point: no wrap trouble</span></div></div><div><div><div>15</div></div><div><span>    </span><span>T</span><span> </span><span>zbar</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>      </span><span>Zrel</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>wrap_pi</span><span>(</span><span>z0</span><span>[</span><span>i</span><span>] </span><span>-</span><span> </span><span>z0</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>18</div></div><div><span>      </span><span>zbar</span><span> </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wm</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>Zrel</span><span>(</span><span>i</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>    </span><span>zbar</span><span> </span><span>+=</span><span> </span><span>z0</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>21</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_meas</span><span> </span><span>=</span><span> </span><span>C</span><span> </span><span>*</span><span> </span><span>mag</span><span>;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>y</span><span>{{</span><span>wrap_pi</span><span>(</span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>m_meas</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>m_meas</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> </span><span>psi_h</span><span>) </span><span>-</span><span> </span><span>zbar</span><span>)}};</span></div></div><div><div><div>23</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>sm</span><span> </span><span>/</span><span> std::</span><span>hypot</span><span>(</span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span> </span><span>correct</span><span>(</span><span>sp</span><span>, </span><span>Zrel</span><span>, </span><span>y</span><span>, </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>r</span><span> </span><span>*</span><span> </span><span>r</span><span>}});</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>private:</span></div></div><div><div><div>28</div></div><div><span>  </span><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>R</span><span>, std::</span><span>size_t</span><span> </span><span>C</span><span>&gt;</span></div></div><div><div><div>29</div></div><div><span>  </span><span>static</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>rows3</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>C</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>X</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>i</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>c0</span><span>) {</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>X</span><span>(</span><span>i</span><span>, </span><span>c0</span><span>), </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c0</span><span> </span><span>+</span><span> </span><span>1</span><span>), </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c0</span><span> </span><span>+</span><span> </span><span>2</span><span>)}};</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>// Standard UKF measurement update given the model outputs Z at the sigma points.</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>// y is the innovation, already formed by the caller.</span></div></div><div><div><div>35</div></div><div><span>  </span><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>M</span><span>&gt;</span></div></div><div><div><div>36</div></div><div><span>  </span><span>bool</span><span> </span><span>correct</span><span>(</span><span>const</span><span> </span><span>SigmaPoints</span><span>&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>sp</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>K</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Z</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>y</span><span>,</span></span></div></div><div><div><div>37</div></div><div><span>         </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>R</span><span>) {</span></span></div></div><div><div><div>38</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>zbar</span><span>{};</span></div></div><div><div><div>39</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>40</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>M</span><span>; </span><span>j</span><span>++</span><span>) </span><span>zbar</span><span>(</span><span>0</span><span>, </span><span>j</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wm</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>Z</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>41</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span> </span><span>=</span><span> </span><span>R</span><span>;</span></div></div><div><div><div>42</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>Pxz</span><span>{};</span></div></div><div><div><div>43</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>44</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>r</span><span> </span><span>&lt;</span><span> </span><span>M</span><span>; </span><span>r</span><span>++</span><span>) {</span></div></div><div><div><div>45</div></div><div><span>        </span><span>const</span><span> </span><span>T</span><span> </span><span>dr</span><span> </span><span>=</span><span> </span><span>Z</span><span>(</span><span>i</span><span>, </span><span>r</span><span>) </span><span>-</span><span> </span><span>zbar</span><span>(</span><span>0</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>46</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>c</span><span> </span><span>&lt;</span><span> </span><span>M</span><span>; </span><span>c</span><span>++</span><span>) </span><span>S</span><span>(</span><span>r</span><span>, </span><span>c</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wc</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>dr</span><span> </span><span>*</span><span> (</span><span>Z</span><span>(</span><span>i</span><span>, </span><span>c</span><span>) </span><span>-</span><span> </span><span>zbar</span><span>(</span><span>0</span><span>, </span><span>c</span><span>));</span></div></div><div><div><div>47</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>Pxz</span><span>(</span><span>k</span><span>, </span><span>r</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wc</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>sp</span><span>.</span><span>X</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>dr</span><span>;</span></div></div><div><div><div>48</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>S</span><span>;</span></div></div><div><div><div>50</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>51</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>Kg</span><span> </span><span>=</span><span> </span><span>chol_solve</span><span>(</span><span>L</span><span>, </span><span>Pxz</span><span>.</span><span>transpose</span><span>()).</span><span>transpose</span><span>();</span></div></div><div><div><div>52</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span> </span><span>=</span><span> </span><span>Kg</span><span> </span><span>*</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>53</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> (</span><span>P</span><span> </span><span>-</span><span> </span><span>Kg</span><span> </span><span>*</span><span> </span><span>S</span><span> </span><span>*</span><span> </span><span>Kg</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>54</div></div><div><span>    </span><span>inject</span><span>(</span><span>dx</span><span>);</span></div></div><div><div><div>55</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>56</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>predict</code> 里 <code>q_ref.conj() * qi[i]</code> 再 <code>.log()</code> 就是 <span><span>δθi=log⁡(qref−1⊗qi)\delta\theta_i = \log(q_{ref}^{-1}\otimes q_i)</span><span><span><span></span><span>δ</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>lo<span>g</span></span><span>(</span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>r</span><span>e</span><span>f</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，是第 4 篇的四元数运算直接复用。<code>correct</code> 是个模板函数，<code>M</code> 由 <code>Z</code> 的列数推断：加速度计 <span><span>M=3M=3</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>，航向 <span><span>M=1M=1</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，同一份代码。</p></section><section><h3>C<a href="#c-1"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>kl_ukf_att_predict</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>bi </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>D </span><span>=</span><span> bi </span><span>+</span><span> </span><span>3</span><span> </span><span>*</span><span> NP, </span><span>*</span><span>cov </span><span>=</span><span> D </span><span>+</span><span> N </span><span>*</span><span> NP;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>mean</span><span>[N], </span><span>dev</span><span>[N], </span><span>q_ref</span><span>[</span><span>4</span><span>], </span><span>qc</span><span>[</span><span>4</span><span>], </span><span>t</span><span>[</span><span>4</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>5</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_ukf_sigma_points</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>f</span><span>-&gt;</span><span>X</span><span>, </span><span>f</span><span>-&gt;</span><span>Wm</span><span>, </span><span>f</span><span>-&gt;</span><span>Wc</span><span>) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>point_attitudes</span><span>(f);</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>/* each point through the gyro model */</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>kl_real </span><span>w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>11</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>      </span><span>bi</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[k] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>X</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> </span><span>3</span><span> </span><span>+</span><span> k];</span></div></div><div><div><div>13</div></div><div><span>      </span><span>w</span><span>[k] </span><span>=</span><span> (</span><span>gyro</span><span>[k] </span><span>-</span><span> </span><span>bi</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k]) </span><span>*</span><span> dt;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>kl_quat_exp</span><span>(w, dq);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>kl_quat_mul</span><span>(</span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], dq, t);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>kl_mat_copy</span><span>(t, </span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>/* deviations from the propagated centre point */</span></div></div><div><div><div>20</div></div><div><span>  </span><span>kl_mat_copy</span><span>(</span><span>f</span><span>-&gt;</span><span>qi</span><span>, q_ref, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>kl_quat_conj</span><span>(q_ref, qc);</span></div></div><div><div><div>22</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>kl_quat_mul</span><span>(qc, </span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], t);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>kl_quat_log</span><span>(t, </span><span>&amp;</span><span>D</span><span>[i </span><span>*</span><span> N]);</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>26</div></div><div><span>      </span><span>D</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> </span><span>3</span><span> </span><span>+</span><span> k] </span><span>=</span><span> </span><span>bi</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[k];</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> N; k</span><span>++</span><span>)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>mean</span><span>[k] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>30</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> N; k</span><span>++</span><span>)</span></div></div><div><div><div>32</div></div><div><span>      </span><span>mean</span><span>[k] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wm</span><span>[i] </span><span>*</span><span> </span><span>D</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> k];</span></div></div><div><div><div>33</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> N </span><span>*</span><span> N; i</span><span>++</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>cov</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>35</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> N; k</span><span>++</span><span>)</span></div></div><div><div><div>37</div></div><div><span>      </span><span>dev</span><span>[k] </span><span>=</span><span> </span><span>D</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> k] </span><span>-</span><span> </span><span>mean</span><span>[k];</span></div></div><div><div><div>38</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> a </span><span>=</span><span> </span><span>0</span><span>; a </span><span>&lt;</span><span> N; a</span><span>++</span><span>)</span></div></div><div><div><div>39</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> c </span><span>=</span><span> </span><span>0</span><span>; c </span><span>&lt;</span><span> N; c</span><span>++</span><span>)</span></div></div><div><div><div>40</div></div><div><span>        </span><span>cov</span><span>[a </span><span>*</span><span> N </span><span>+</span><span> c] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wc</span><span>[i] </span><span>*</span><span> </span><span>dev</span><span>[a] </span><span>*</span><span> </span><span>dev</span><span>[c];</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span>  </span><span>kl_quat_exp</span><span>(mean, dq);</span></div></div><div><div><div>43</div></div><div><span>  </span><span>kl_quat_mul</span><span>(q_ref, dq, qn);</span></div></div><div><div><div>44</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>45</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>) {</span></div></div><div><div><div>46</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[k] </span><span>+=</span><span> </span><span>mean</span><span>[</span><span>3</span><span> </span><span>+</span><span> k];</span></div></div><div><div><div>47</div></div><div><span>    </span><span>cov</span><span>[k </span><span>*</span><span> </span><span>7</span><span>] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> dt </span><span>*</span><span> dt;</span></div></div><div><div><div>48</div></div><div><span>    </span><span>cov</span><span>[(k </span><span>+</span><span> </span><span>3</span><span>) </span><span>*</span><span> </span><span>7</span><span>] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> dt;</span></div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span>  </span><span>kl_mat_copy</span><span>(cov, </span><span>f</span><span>-&gt;</span><span>P</span><span>, N, N);</span></div></div><div><div><div>51</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>52</div></div><div><span>}</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>/* Move the error estimate into the nominal state, then reset the error to zero. */</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>correct</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>y</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>dz </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>S </span><span>=</span><span> dz </span><span>+</span><span> NP </span><span>*</span><span> m, </span><span>*</span><span>Pxz </span><span>=</span><span> S </span><span>+</span><span> m </span><span>*</span><span> m, </span><span>*</span><span>Kt </span><span>=</span><span> Pxz </span><span>+</span><span> N </span><span>*</span><span> m;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> Kt </span><span>+</span><span> m </span><span>*</span><span> N, </span><span>*</span><span>KS </span><span>=</span><span> K </span><span>+</span><span> N </span><span>*</span><span> m, </span><span>*</span><span>KSKt </span><span>=</span><span> KS </span><span>+</span><span> N </span><span>*</span><span> m;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>kl_real </span><span>zbar</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[N];</span></div></div><div><div><div>6</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>zbar</span><span>[k] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>)</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>10</div></div><div><span>      </span><span>zbar</span><span>[k] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wm</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>Z</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k];</span></div></div><div><div><div>11</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> m </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>S</span><span>[i] </span><span>=</span><span> </span><span>R</span><span>[i];</span></div></div><div><div><div>13</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> N </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Pxz</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>15</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>17</div></div><div><span>      </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>Z</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>-</span><span> </span><span>zbar</span><span>[k];</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> a </span><span>=</span><span> </span><span>0</span><span>; a </span><span>&lt;</span><span> m; a</span><span>++</span><span>)</span></div></div><div><div><div>19</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> c </span><span>=</span><span> </span><span>0</span><span>; c </span><span>&lt;</span><span> m; c</span><span>++</span><span>)</span></div></div><div><div><div>20</div></div><div><span>        </span><span>S</span><span>[a </span><span>*</span><span> m </span><span>+</span><span> c] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wc</span><span>[i] </span><span>*</span><span> </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> a] </span><span>*</span><span> </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> c];</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> a </span><span>=</span><span> </span><span>0</span><span>; a </span><span>&lt;</span><span> N; a</span><span>++</span><span>)</span></div></div><div><div><div>22</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> c </span><span>=</span><span> </span><span>0</span><span>; c </span><span>&lt;</span><span> m; c</span><span>++</span><span>)</span></div></div><div><div><div>23</div></div><div><span>        </span><span>Pxz</span><span>[a </span><span>*</span><span> m </span><span>+</span><span> c] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wc</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>X</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> a] </span><span>*</span><span> </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> c];</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>/* K^T = S^-1 Pxz^T by Cholesky; the factor goes in KS, which is free until below */</span></div></div><div><div><div>26</div></div><div><span>  </span><span>kl_mat_copy</span><span>(S, KS, m, m);</span></div></div><div><div><div>27</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(KS, m) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>29</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Pxz, Kt, N, m);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>kl_chol_solve</span><span>(KS, Kt, m, N);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Kt, K, m, N);</span></div></div><div><div><div>32</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, y, dx, N, m, </span><span>1</span><span>);</span></div></div><div><div><div>33</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, S, KS, N, m, m);</span></div></div><div><div><div>34</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(KS, K, KSKt, N, m, N);</span></div></div><div><div><div>35</div></div><div><span>  </span><span>kl_mat_sub</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, KSKt, </span><span>f</span><span>-&gt;</span><span>P</span><span>, N, N);</span></div></div><div><div><div>36</div></div><div><span>  </span><span>kl_mat_symmetrize</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, N);</span></div></div><div><div><div>37</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>38</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>39</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C 版里 <code>inject</code> 是文件内静态函数的又一份拷贝，没有和 ESKF 的共用：ESKF 的 <code>inject</code> 是 <code>static</code>，我没有为了去重去改前一章已经定稿的文件。三个语言的 UKF 都各自有一份几行的 <code>inject</code>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>x</span><span><span>) </span><span>=</span><span> </span></span><span>sigma_points</span><span>(&amp;</span><span>self</span><span>.p) </span><span>else</span><span> {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> (</span><span>wm</span><span>, </span><span>wc</span><span><span>) </span><span>=</span><span> </span></span><span>weights</span><span>();</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>qi</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>point_attitudes</span><span>(&amp;</span><span>x</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>bi</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>3</span><span>]; </span><span>NP</span><span>];</span></div></div><div><div><div>8</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>k</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.b[</span><span>k</span><span>] + </span><span>x</span><span>[</span><span>i</span><span>][</span><span>3</span><span> + </span><span>k</span><span>];</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>            </span><span>let</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>(</span><span>gyro</span><span>[</span><span>0</span><span>] - </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>0</span><span>]) * </span><span>dt</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>(</span><span>gyro</span><span>[</span><span>1</span><span>] - </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>1</span><span>]) * </span><span>dt</span><span>,</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>(</span><span>gyro</span><span>[</span><span>2</span><span>] - </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>2</span><span>]) * </span><span>dt</span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>];</span></div></div><div><div><div>17</div></div><div><span>            </span><span>qi</span><span>[</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>qi</span><span>[</span><span>i</span><span>] * </span><span>Quat</span><span>::</span><span>exp</span><span>(</span><span>w</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>        </span><span>// Deviations from the propagated centre point, in its tangent space.</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>q_ref</span><span><span> </span><span>=</span><span> </span></span><span>qi</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>d</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>N</span><span>]; </span><span>NP</span><span>];</span></div></div><div><div><div>22</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>let</span><span> </span><span>th</span><span><span> </span><span>=</span><span> (</span></span><span>q_ref</span><span>.</span><span>conj</span><span>() * </span><span>qi</span><span>[</span><span>i</span><span>]).</span><span>log</span><span>();</span></div></div><div><div><div>24</div></div><div><span>            </span><span>d</span><span>[</span><span>i</span><span>][..</span><span>3</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>th</span><span>);</span></div></div><div><div><div>25</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>26</div></div><div><span>                </span><span>d</span><span>[</span><span>i</span><span>][</span><span>3</span><span> + </span><span>k</span><span><span>] </span><span>=</span><span> </span></span><span>bi</span><span>[</span><span>i</span><span>][</span><span>k</span><span>] - </span><span>self</span><span>.b[</span><span>k</span><span>];</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>mean</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>30</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>31</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>32</div></div><div><span>                </span><span>mean</span><span>[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>wm</span><span>[</span><span>i</span><span>] * </span><span>d</span><span>[</span><span>i</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>cov</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>36</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>37</div></div><div><span>            </span><span>for</span><span> </span><span>r</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>38</div></div><div><span>                </span><span>for</span><span> </span><span>c</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>39</div></div><div><span>                    </span><span>cov</span><span>.m[</span><span>r</span><span>][</span><span>c</span><span><span>] </span><span>+=</span><span> </span></span><span>wc</span><span>[</span><span>i</span><span>] * (</span><span>d</span><span>[</span><span>i</span><span>][</span><span>r</span><span>] - </span><span>mean</span><span>[</span><span>r</span><span>]) * (</span><span>d</span><span>[</span><span>i</span><span>][</span><span>c</span><span>] - </span><span>mean</span><span>[</span><span>c</span><span>]);</span></div></div><div><div><div>40</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>44</div></div><div><span>            </span><span>cov</span><span>.m[</span><span>k</span><span>][</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>self</span><span>.sg * </span><span>self</span><span>.sg * (</span><span>dt</span><span> * </span><span>dt</span><span>);</span></div></div><div><div><div>45</div></div><div><span>            </span><span>cov</span><span>.m[</span><span>3</span><span> + </span><span>k</span><span>][</span><span>3</span><span> + </span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>;</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>q_ref</span><span> * </span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>mean</span><span>[</span><span>0</span><span>], </span><span>mean</span><span>[</span><span>1</span><span>], </span><span>mean</span><span>[</span><span>2</span><span>]]);</span></div></div><div><div><div>48</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>49</div></div><div><span>            </span><span>self</span><span>.b[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>mean</span><span>[</span><span>3</span><span> + </span><span>k</span><span>];</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>cov</span><span>;</span></div></div><div><div><div>52</div></div><div><span>        </span><span>true</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>correct</span><span>&lt;</span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>3</div></div><div><span>        </span><span>x</span><span>: &amp;</span><span>Points</span><span>,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>z</span><span>: &amp;[[</span><span>Real</span><span>; </span><span>M</span><span>]; </span><span>NP</span><span>],</span></div></div><div><div><div>5</div></div><div><span>        </span><span>y</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>r</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> (</span><span>wm</span><span>, </span><span>wc</span><span><span>) </span><span>=</span><span> </span></span><span>weights</span><span>();</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>zbar</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>M</span><span>];</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>M</span><span> {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>zbar</span><span>[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>wm</span><span>[</span><span>i</span><span>] * </span><span>z</span><span>[</span><span>i</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> *</span></span><span>r</span><span>;</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>pxz</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>17</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>for</span><span> </span><span>a</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>M</span><span> {</span></div></div><div><div><div>19</div></div><div><span>                </span><span>for</span><span> </span><span>b</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>M</span><span> {</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>s</span><span>.m[</span><span>a</span><span>][</span><span>b</span><span><span>] </span><span>+=</span><span> </span></span><span>wc</span><span>[</span><span>i</span><span>] * (</span><span>z</span><span>[</span><span>i</span><span>][</span><span>a</span><span>] - </span><span>zbar</span><span>[</span><span>a</span><span>]) * (</span><span>z</span><span>[</span><span>i</span><span>][</span><span>b</span><span>] - </span><span>zbar</span><span>[</span><span>b</span><span>]);</span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>                </span><span>for</span><span> </span><span>a2</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>23</div></div><div><span>                    </span><span>pxz</span><span>.m[</span><span>a2</span><span>][</span><span>a</span><span><span>] </span><span>+=</span><span> </span></span><span>wc</span><span>[</span><span>i</span><span>] * </span><span>x</span><span>[</span><span>i</span><span>][</span><span>a2</span><span>] * (</span><span>z</span><span>[</span><span>i</span><span>][</span><span>a</span><span>] - </span><span>zbar</span><span>[</span><span>a</span><span>]);</span></div></div><div><div><div>24</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>l</span><span><span>) </span><span>=</span><span> </span></span><span>s</span><span>.</span><span>cholesky</span><span>() </span><span>else</span><span> {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>chol_solve</span><span>(&amp;</span><span>l</span><span>, &amp;</span><span>pxz</span><span>.</span><span>transpose</span><span>()).</span><span>transpose</span><span>();</span></div></div><div><div><div>31</div></div><div><span>        </span><span>let</span><span> </span><span>dx</span><span><span> </span><span>=</span><span> </span></span><span>k</span><span> * *</span><span>y</span><span>;</span></div></div><div><div><div>32</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> (</span></span><span>self</span><span>.p - </span><span>k</span><span> * </span><span>s</span><span> * </span><span>k</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>33</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>34</div></div><div><span>        </span><span>true</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>/// Gravity-direction update. False when gated out or on Cholesky failure.</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：c/include/kl/kl_ukf_att.h、c/src/kl_ukf_att.c<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Error-state unscented Kalman filter for attitude: same 6-dim error [dtheta, db] as the ESKF,</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* additive noise, scaled unscented transform with alpha = 1, beta = 2, kappa = 0</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* (lambda = 0, so Wm[0] = 0, Wc[0] = 2 and every other weight is 1/12).</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>* Quaternion conventions: see kl_quat.h. All storage is inline; nothing is allocated. */</span></div></div><div><div><div>5</div></div><div><span>#ifndef</span><span> </span><span>KL_UKF_ATT_H</span></div></div><div><div><div>6</div></div><div><span>#define</span><span> </span><span>KL_UKF_ATT_H</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#define</span><span> </span><span>KL_UKF_N</span><span> </span><span>6</span></div></div><div><div><div>11</div></div><div><span>#define</span><span> </span><span>KL_UKF_NPTS</span><span> (</span><span>2</span><span> </span><span>*</span><span> KL_UKF_N </span><span>+</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>/* Sigma points of N(0, P): X (13 x 6, row-major) is the centre (zero) followed by</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>* +-columns of chol((n + lambda) P); Wm and Wc are the mean and covariance weights.</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>* Returns -1 if P is not positive definite. */</span></div></div><div><div><div>16</div></div><div><span>int</span><span> </span><span>kl_ukf_sigma_points</span><span>(</span><span>const</span><span> kl_real </span><span>P</span><span>[</span><span>36</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>X</span><span>[</span><span>78</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>Wm</span><span>[</span><span>13</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>Wc</span><span>[</span><span>13</span><span><span>]</span><span>);</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>kl_real </span><span>q</span><span>[</span><span>4</span><span>], </span><span>b</span><span>[</span><span>3</span><span>], </span><span>P</span><span>[</span><span>36</span><span>];</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>kl_real sg, sbw, sa, sm;</span><span> /* gyro, bias random walk, accel, mag noise */</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>kl_real </span><span>m_w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>kl_real accel_gate, mag_gate;</span><span> /* negative disables the gate */</span></div></div><div><div><div>23</div></div><div><span>  </span><span>int</span><span> reset;</span><span>                    /* apply the reset Jacobian after injection */</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>kl_real </span><span>X</span><span>[</span><span>78</span><span>], </span><span>Wm</span><span>[</span><span>13</span><span>], </span><span>Wc</span><span>[</span><span>13</span><span>], </span><span>qi</span><span>[</span><span>52</span><span>], </span><span>Z</span><span>[</span><span>39</span><span>];</span><span> /* sigma points, weights, attitudes, outputs */</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>kl_real </span><span>work</span><span>[</span><span>256</span><span>];</span></div></div><div><div><div>26</div></div><div><span>} kl_ukf_att;</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>/* p0_diag: initial diagonal of P (6). The bias starts at zero. */</span></div></div><div><div><div>29</div></div><div><span>void</span><span> </span><span>kl_ukf_att_init</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>6</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>30</div></div><div><span><span>         </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>31</div></div><div><span><span>         </span></span><span>kl_real </span><span>accel_gate</span><span>,</span><span> kl_real </span><span>mag_gate</span><span>,</span><span> </span><span>int</span><span><span> </span><span>reset</span><span>);</span></span></div></div><div><div><div>32</div></div><div><span>/* Returns 0 on success, -1 if P is not positive definite (state unchanged). */</span></div></div><div><div><div>33</div></div><div><span>int</span><span> </span><span>kl_ukf_att_predict</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>);</span></span></div></div><div><div><div>34</div></div><div><span>/* Return 1 if applied, 0 if gated out, -1 on Cholesky failure (state unchanged). */</span></div></div><div><div><div>35</div></div><div><span>int</span><span> </span><span>kl_ukf_att_update_accel</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>36</div></div><div><span>int</span><span> </span><span>kl_ukf_att_update_mag_heading</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_ukf_att.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;stddef.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/kl_ekf_att.h"</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_eskf.h"</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#define</span><span> </span><span>N</span><span> KL_UKF_N</span></div></div><div><div><div>11</div></div><div><span>#define</span><span> </span><span>NP</span><span> KL_UKF_NPTS</span></div></div><div><div><div>12</div></div><div><span>#define</span><span> </span><span>ALPHA</span><span> ((kl_real)</span><span>1</span><span>)</span></div></div><div><div><div>13</div></div><div><span>#define</span><span> </span><span>BETA</span><span> ((kl_real)</span><span>2</span><span>)</span></div></div><div><div><div>14</div></div><div><span>#define</span><span> </span><span>KAPPA</span><span> ((kl_real)</span><span>0</span><span>)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>int</span><span> </span><span>kl_ukf_sigma_points</span><span>(</span><span>const</span><span> kl_real </span><span>P</span><span>[</span><span>36</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>X</span><span>[</span><span>78</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>Wm</span><span>[</span><span>13</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>Wc</span><span>[</span><span>13</span><span><span>]</span><span>)</span></span></div></div><div><div><div>17</div></div><div><span>{</span></div></div><div><div><div>18</div></div><div><span>  </span><span>const</span><span> kl_real lam </span><span>=</span><span> ALPHA </span><span>*</span><span> ALPHA </span><span>*</span><span> (N </span><span>+</span><span> KAPPA) </span><span>-</span><span> N;</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>kl_real </span><span>L</span><span>[N </span><span>*</span><span> N];</span></div></div><div><div><div>20</div></div><div><span>  </span><span>kl_mat_scale</span><span>(P, N </span><span>+</span><span> lam, L, N, N);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(L, N) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>23</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> N; j</span><span>++</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>X</span><span>[j] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>25</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> N; j</span><span>++</span><span>)</span></div></div><div><div><div>26</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> N; i</span><span>++</span><span>) {</span></div></div><div><div><div>27</div></div><div><span>      </span><span>X</span><span>[(</span><span>1</span><span> </span><span>+</span><span> j) </span><span>*</span><span> N </span><span>+</span><span> i] </span><span>=</span><span> </span><span>L</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> j];</span></div></div><div><div><div>28</div></div><div><span>      </span><span>X</span><span>[(</span><span>1</span><span> </span><span>+</span><span> N </span><span>+</span><span> j) </span><span>*</span><span> N </span><span>+</span><span> i] </span><span>=</span><span> </span><span>-</span><span>L</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> j];</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>Wm</span><span>[i] </span><span>=</span><span> </span><span>Wc</span><span>[i] </span><span>=</span><span> (kl_real)</span><span>0.5</span><span> </span><span>/</span><span> (N </span><span>+</span><span> lam);</span></div></div><div><div><div>32</div></div><div><span>  </span><span>Wm</span><span>[</span><span>0</span><span>] </span><span>=</span><span> lam </span><span>/</span><span> (N </span><span>+</span><span> lam);</span></div></div><div><div><div>33</div></div><div><span>  </span><span>Wc</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>Wm</span><span>[</span><span>0</span><span>] </span><span>+</span><span> (</span><span>1</span><span> </span><span>-</span><span> ALPHA </span><span>*</span><span> ALPHA </span><span>+</span><span> BETA);</span></div></div><div><div><div>34</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>void</span><span> </span><span>kl_ukf_att_init</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>6</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>38</div></div><div><span><span>         </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>39</div></div><div><span><span>         </span></span><span>kl_real </span><span>accel_gate</span><span>,</span><span> kl_real </span><span>mag_gate</span><span>,</span><span> </span><span>int</span><span><span> </span><span>reset</span><span>)</span></span></div></div><div><div><div>40</div></div><div><span>{</span></div></div><div><div><div>41</div></div><div><span>  </span><span>kl_mat_copy</span><span>(q0, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>42</div></div><div><span>  </span><span>kl_mat_copy</span><span>(m_w, </span><span>f</span><span>-&gt;</span><span>m_w</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>43</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>44</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>45</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>36</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>P</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>47</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> N; i</span><span>++</span><span>)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>P</span><span>[i </span><span>*</span><span> </span><span>7</span><span>] </span><span>=</span><span> </span><span>p0_diag</span><span>[i];</span></div></div><div><div><div>49</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>=</span><span> sigma_g;</span></div></div><div><div><div>50</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>=</span><span> sigma_bw;</span></div></div><div><div><div>51</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>=</span><span> sigma_a;</span></div></div><div><div><div>52</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>=</span><span> sigma_m;</span></div></div><div><div><div>53</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>=</span><span> accel_gate;</span></div></div><div><div><div>54</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>=</span><span> mag_gate;</span></div></div><div><div><div>55</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>reset</span><span> </span><span>=</span><span> reset;</span></div></div><div><div><div>56</div></div><div><span>}</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>/* qi[i] = q (x) exp(X[i, :3]) for every sigma point */</span></div></div><div><div><div>59</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>point_attitudes</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>)</span></span></div></div><div><div><div>60</div></div><div><span>{</span></div></div><div><div><div>61</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>kl_real </span><span>dq</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>63</div></div><div><span>    </span><span>kl_quat_exp</span><span>(</span><span>&amp;</span><span>f</span><span>-&gt;</span><span>X</span><span>[i </span><span>*</span><span> N], dq);</span></div></div><div><div><div>64</div></div><div><span>    </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, </span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>]);</span></div></div><div><div><div>65</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>66</div></div><div><span>}</span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>int</span><span> </span><span>kl_ukf_att_predict</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>69</div></div><div><span>{</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>bi </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>D </span><span>=</span><span> bi </span><span>+</span><span> </span><span>3</span><span> </span><span>*</span><span> NP, </span><span>*</span><span>cov </span><span>=</span><span> D </span><span>+</span><span> N </span><span>*</span><span> NP;</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>kl_real </span><span>mean</span><span>[N], </span><span>dev</span><span>[N], </span><span>q_ref</span><span>[</span><span>4</span><span>], </span><span>qc</span><span>[</span><span>4</span><span>], </span><span>t</span><span>[</span><span>4</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>72</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_ukf_sigma_points</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>f</span><span>-&gt;</span><span>X</span><span>, </span><span>f</span><span>-&gt;</span><span>Wm</span><span>, </span><span>f</span><span>-&gt;</span><span>Wc</span><span>) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>73</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>74</div></div><div><span>  </span><span>point_attitudes</span><span>(f);</span></div></div><div><div><div>75</div></div><div><span><span>  </span></span><span>/* each point through the gyro model */</span></div></div><div><div><div>76</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>kl_real </span><span>w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>78</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>) {</span></div></div><div><div><div>79</div></div><div><span>      </span><span>bi</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[k] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>X</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> </span><span>3</span><span> </span><span>+</span><span> k];</span></div></div><div><div><div>80</div></div><div><span>      </span><span>w</span><span>[k] </span><span>=</span><span> (</span><span>gyro</span><span>[k] </span><span>-</span><span> </span><span>bi</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k]) </span><span>*</span><span> dt;</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>82</div></div><div><span>    </span><span>kl_quat_exp</span><span>(w, dq);</span></div></div><div><div><div>83</div></div><div><span>    </span><span>kl_quat_mul</span><span>(</span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], dq, t);</span></div></div><div><div><div>84</div></div><div><span>    </span><span>kl_mat_copy</span><span>(t, </span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>85</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>86</div></div><div><span><span>  </span></span><span>/* deviations from the propagated centre point */</span></div></div><div><div><div>87</div></div><div><span>  </span><span>kl_mat_copy</span><span>(</span><span>f</span><span>-&gt;</span><span>qi</span><span>, q_ref, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>88</div></div><div><span>  </span><span>kl_quat_conj</span><span>(q_ref, qc);</span></div></div><div><div><div>89</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>90</div></div><div><span>    </span><span>kl_quat_mul</span><span>(qc, </span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], t);</span></div></div><div><div><div>91</div></div><div><span>    </span><span>kl_quat_log</span><span>(t, </span><span>&amp;</span><span>D</span><span>[i </span><span>*</span><span> N]);</span></div></div><div><div><div>92</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>93</div></div><div><span>      </span><span>D</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> </span><span>3</span><span> </span><span>+</span><span> k] </span><span>=</span><span> </span><span>bi</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[k];</span></div></div><div><div><div>94</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>95</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> N; k</span><span>++</span><span>)</span></div></div><div><div><div>96</div></div><div><span>    </span><span>mean</span><span>[k] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>97</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>)</span></div></div><div><div><div>98</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> N; k</span><span>++</span><span>)</span></div></div><div><div><div>99</div></div><div><span>      </span><span>mean</span><span>[k] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wm</span><span>[i] </span><span>*</span><span> </span><span>D</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> k];</span></div></div><div><div><div>100</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> N </span><span>*</span><span> N; i</span><span>++</span><span>)</span></div></div><div><div><div>101</div></div><div><span>    </span><span>cov</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>102</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>103</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> N; k</span><span>++</span><span>)</span></div></div><div><div><div>104</div></div><div><span>      </span><span>dev</span><span>[k] </span><span>=</span><span> </span><span>D</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> k] </span><span>-</span><span> </span><span>mean</span><span>[k];</span></div></div><div><div><div>105</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> a </span><span>=</span><span> </span><span>0</span><span>; a </span><span>&lt;</span><span> N; a</span><span>++</span><span>)</span></div></div><div><div><div>106</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> c </span><span>=</span><span> </span><span>0</span><span>; c </span><span>&lt;</span><span> N; c</span><span>++</span><span>)</span></div></div><div><div><div>107</div></div><div><span>        </span><span>cov</span><span>[a </span><span>*</span><span> N </span><span>+</span><span> c] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wc</span><span>[i] </span><span>*</span><span> </span><span>dev</span><span>[a] </span><span>*</span><span> </span><span>dev</span><span>[c];</span></div></div><div><div><div>108</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>109</div></div><div><span>  </span><span>kl_quat_exp</span><span>(mean, dq);</span></div></div><div><div><div>110</div></div><div><span>  </span><span>kl_quat_mul</span><span>(q_ref, dq, qn);</span></div></div><div><div><div>111</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>112</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>) {</span></div></div><div><div><div>113</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[k] </span><span>+=</span><span> </span><span>mean</span><span>[</span><span>3</span><span> </span><span>+</span><span> k];</span></div></div><div><div><div>114</div></div><div><span>    </span><span>cov</span><span>[k </span><span>*</span><span> </span><span>7</span><span>] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> dt </span><span>*</span><span> dt;</span></div></div><div><div><div>115</div></div><div><span>    </span><span>cov</span><span>[(k </span><span>+</span><span> </span><span>3</span><span>) </span><span>*</span><span> </span><span>7</span><span>] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> dt;</span></div></div><div><div><div>116</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>117</div></div><div><span>  </span><span>kl_mat_copy</span><span>(cov, </span><span>f</span><span>-&gt;</span><span>P</span><span>, N, N);</span></div></div><div><div><div>118</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>119</div></div><div><span>}</span></div></div><div><div><div>120</div></div><div>
</div></div><div><div><div>121</div></div><div><span>/* Move the error estimate into the nominal state, then reset the error to zero. */</span></div></div><div><div><div>122</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>inject</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>dx</span><span>[</span><span>6</span><span><span>]</span><span>)</span></span></div></div><div><div><div>123</div></div><div><span>{</span></div></div><div><div><div>124</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>G </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>T </span><span>=</span><span> G </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>125</div></div><div><span><span>  </span></span><span>kl_real </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>126</div></div><div><span>  </span><span>kl_quat_exp</span><span>(dx, dq);</span></div></div><div><div><div>127</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>128</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>129</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>130</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>131</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+=</span><span> </span><span>dx</span><span>[</span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>132</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>reset</span><span>) {</span></div></div><div><div><div>133</div></div><div><span><span>    </span></span><span>kl_real </span><span>half</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {(kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>0</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>1</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>2</span><span>]};</span></div></div><div><div><div>134</div></div><div><span><span>    </span></span><span>kl_real </span><span>S</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>135</div></div><div><span>    </span><span>kl_skew</span><span>(half, S);</span></div></div><div><div><div>136</div></div><div><span>    </span><span>kl_mat_identity</span><span>(G, </span><span>6</span><span>);</span></div></div><div><div><div>137</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>138</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>139</div></div><div><span>        </span><span>G</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>-=</span><span> </span><span>S</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>140</div></div><div><span>    </span><span>kl_mat_mul</span><span>(G, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>141</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, G, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>142</div></div><div><span>    </span><span>kl_mat_copy</span><span>(U, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>143</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>144</div></div><div><span>}</span></div></div><div><div><div>145</div></div><div>
</div></div><div><div><div>146</div></div><div><span>/* Standard UKF measurement update given the model outputs Z (13 x m) at the sigma points X. */</span></div></div><div><div><div>147</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>correct</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>y</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>)</span></span></div></div><div><div><div>148</div></div><div><span>{</span></div></div><div><div><div>149</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>dz </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>S </span><span>=</span><span> dz </span><span>+</span><span> NP </span><span>*</span><span> m, </span><span>*</span><span>Pxz </span><span>=</span><span> S </span><span>+</span><span> m </span><span>*</span><span> m, </span><span>*</span><span>Kt </span><span>=</span><span> Pxz </span><span>+</span><span> N </span><span>*</span><span> m;</span></div></div><div><div><div>150</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> Kt </span><span>+</span><span> m </span><span>*</span><span> N, </span><span>*</span><span>KS </span><span>=</span><span> K </span><span>+</span><span> N </span><span>*</span><span> m, </span><span>*</span><span>KSKt </span><span>=</span><span> KS </span><span>+</span><span> N </span><span>*</span><span> m;</span></div></div><div><div><div>151</div></div><div><span><span>  </span></span><span>kl_real </span><span>zbar</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[N];</span></div></div><div><div><div>152</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>153</div></div><div><span>    </span><span>zbar</span><span>[k] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>154</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>)</span></div></div><div><div><div>155</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>156</div></div><div><span>      </span><span>zbar</span><span>[k] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wm</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>Z</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k];</span></div></div><div><div><div>157</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> m </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>158</div></div><div><span>    </span><span>S</span><span>[i] </span><span>=</span><span> </span><span>R</span><span>[i];</span></div></div><div><div><div>159</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> N </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>160</div></div><div><span>    </span><span>Pxz</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>161</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>162</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>163</div></div><div><span>      </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>Z</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>-</span><span> </span><span>zbar</span><span>[k];</span></div></div><div><div><div>164</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> a </span><span>=</span><span> </span><span>0</span><span>; a </span><span>&lt;</span><span> m; a</span><span>++</span><span>)</span></div></div><div><div><div>165</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> c </span><span>=</span><span> </span><span>0</span><span>; c </span><span>&lt;</span><span> m; c</span><span>++</span><span>)</span></div></div><div><div><div>166</div></div><div><span>        </span><span>S</span><span>[a </span><span>*</span><span> m </span><span>+</span><span> c] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wc</span><span>[i] </span><span>*</span><span> </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> a] </span><span>*</span><span> </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> c];</span></div></div><div><div><div>167</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> a </span><span>=</span><span> </span><span>0</span><span>; a </span><span>&lt;</span><span> N; a</span><span>++</span><span>)</span></div></div><div><div><div>168</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> c </span><span>=</span><span> </span><span>0</span><span>; c </span><span>&lt;</span><span> m; c</span><span>++</span><span>)</span></div></div><div><div><div>169</div></div><div><span>        </span><span>Pxz</span><span>[a </span><span>*</span><span> m </span><span>+</span><span> c] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wc</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>X</span><span>[i </span><span>*</span><span> N </span><span>+</span><span> a] </span><span>*</span><span> </span><span>dz</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> c];</span></div></div><div><div><div>170</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>171</div></div><div><span><span>  </span></span><span>/* K^T = S^-1 Pxz^T by Cholesky; the factor goes in KS, which is free until below */</span></div></div><div><div><div>172</div></div><div><span>  </span><span>kl_mat_copy</span><span>(S, KS, m, m);</span></div></div><div><div><div>173</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(KS, m) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>174</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>175</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Pxz, Kt, N, m);</span></div></div><div><div><div>176</div></div><div><span>  </span><span>kl_chol_solve</span><span>(KS, Kt, m, N);</span></div></div><div><div><div>177</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Kt, K, m, N);</span></div></div><div><div><div>178</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, y, dx, N, m, </span><span>1</span><span>);</span></div></div><div><div><div>179</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, S, KS, N, m, m);</span></div></div><div><div><div>180</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(KS, K, KSKt, N, m, N);</span></div></div><div><div><div>181</div></div><div><span>  </span><span>kl_mat_sub</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, KSKt, </span><span>f</span><span>-&gt;</span><span>P</span><span>, N, N);</span></div></div><div><div><div>182</div></div><div><span>  </span><span>kl_mat_symmetrize</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, N);</span></div></div><div><div><div>183</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>184</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>185</div></div><div><span>}</span></div></div><div><div><div>186</div></div><div>
</div></div><div><div><div>187</div></div><div><span>int</span><span> </span><span>kl_ukf_att_update_accel</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>188</div></div><div><span>{</span></div></div><div><div><div>189</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>190</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> (kl_real)KL_GRAVITY) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span>)</span></div></div><div><div><div>191</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>192</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>down</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>193</div></div><div><span><span>  </span></span><span>kl_real </span><span>y</span><span>[</span><span>3</span><span>], </span><span>R</span><span>[</span><span>9</span><span>], </span><span>zbar</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>};</span></div></div><div><div><div>194</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_ukf_sigma_points</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>f</span><span>-&gt;</span><span>X</span><span>, </span><span>f</span><span>-&gt;</span><span>Wm</span><span>, </span><span>f</span><span>-&gt;</span><span>Wc</span><span>) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>195</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>196</div></div><div><span>  </span><span>point_attitudes</span><span>(f);</span></div></div><div><div><div>197</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>198</div></div><div><span>    </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], down, </span><span>&amp;</span><span>f</span><span>-&gt;</span><span>Z</span><span>[i </span><span>*</span><span> </span><span>3</span><span>]);</span></div></div><div><div><div>199</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>200</div></div><div><span>      </span><span>zbar</span><span>[k] </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wm</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>Z</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k];</span></div></div><div><div><div>201</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>202</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY);</span></div></div><div><div><div>203</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>204</div></div><div><span>    </span><span>y</span><span>[i] </span><span>=</span><span> </span><span>accel</span><span>[i] </span><span>/</span><span> norm </span><span>-</span><span> </span><span>zbar</span><span>[i];</span></div></div><div><div><div>205</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>206</div></div><div><span>      </span><span>R</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>?</span><span> r </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>207</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>208</div></div><div><span>  </span><span>return</span><span> </span><span>correct</span><span>(f, </span><span>3</span><span>, y, R);</span></div></div><div><div><div>209</div></div><div><span>}</span></div></div><div><div><div>210</div></div><div>
</div></div><div><div><div>211</div></div><div><span>int</span><span> </span><span>kl_ukf_att_update_mag_heading</span><span><span>(</span><span>kl_ukf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>212</div></div><div><span>{</span></div></div><div><div><div>213</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>214</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span>)</span></div></div><div><div><div>215</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>216</div></div><div><span><span>  </span></span><span>kl_real </span><span>C</span><span>[</span><span>9</span><span>], </span><span>h</span><span>[</span><span>3</span><span>], </span><span>mw</span><span>[</span><span>3</span><span>], </span><span>z0</span><span>[NP], zbar, y, R;</span></div></div><div><div><div>217</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_ukf_sigma_points</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>f</span><span>-&gt;</span><span>X</span><span>, </span><span>f</span><span>-&gt;</span><span>Wm</span><span>, </span><span>f</span><span>-&gt;</span><span>Wc</span><span>) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>218</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>219</div></div><div><span>  </span><span>point_attitudes</span><span>(f);</span></div></div><div><div><div>220</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>221</div></div><div><span>  </span><span>const</span><span> kl_real psi_h </span><span>=</span><span> </span><span>KL_ATAN2</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>222</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>223</div></div><div><span>    </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>&amp;</span><span>f</span><span>-&gt;</span><span>qi</span><span>[i </span><span>*</span><span> </span><span>4</span><span>], </span><span>f</span><span>-&gt;</span><span>m_w</span><span>, h);</span><span> /* C(q) C(q_i)^T m_w */</span></div></div><div><div><div>224</div></div><div><span>    </span><span>kl_mat_mul</span><span>(C, h, mw, </span><span>3</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>225</div></div><div><span>    </span><span>z0</span><span>[i] </span><span>=</span><span> </span><span>kl_wrap_pi</span><span>(</span><span>KL_ATAN2</span><span>(</span><span>mw</span><span>[</span><span>1</span><span>], </span><span>mw</span><span>[</span><span>0</span><span>]) </span><span>-</span><span> psi_h);</span></div></div><div><div><div>226</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>227</div></div><div><span><span>  </span></span><span>/* heading relative to the centre point: no wrap trouble */</span></div></div><div><div><div>228</div></div><div><span><span>  </span></span><span>zbar </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>229</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NP; i</span><span>++</span><span>) {</span></div></div><div><div><div>230</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>Z</span><span>[i] </span><span>=</span><span> </span><span>kl_wrap_pi</span><span>(</span><span>z0</span><span>[i] </span><span>-</span><span> </span><span>z0</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>231</div></div><div><span><span>    </span></span><span>zbar </span><span>+=</span><span> </span><span>f</span><span>-&gt;</span><span>Wm</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>Z</span><span>[i];</span></div></div><div><div><div>232</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>233</div></div><div><span><span>  </span></span><span>zbar </span><span>+=</span><span> </span><span>z0</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>234</div></div><div><span>  </span><span>kl_mat_mul</span><span>(C, mag, mw, </span><span>3</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>235</div></div><div><span><span>  </span></span><span>y </span><span>=</span><span> </span><span>kl_wrap_pi</span><span>(</span><span>kl_wrap_pi</span><span>(</span><span>KL_ATAN2</span><span>(</span><span>mw</span><span>[</span><span>1</span><span>], </span><span>mw</span><span>[</span><span>0</span><span>]) </span><span>-</span><span> psi_h) </span><span>-</span><span> zbar);</span></div></div><div><div><div>236</div></div><div><span><span>  </span></span><span>R </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> </span><span>KL_SQRT</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>]));</span></div></div><div><div><div>237</div></div><div><span><span>  </span></span><span>R </span><span>=</span><span> R </span><span>*</span><span> R;</span></div></div><div><div><div>238</div></div><div><span>  </span><span>return</span><span> </span><span>correct</span><span>(f, </span><span>1</span><span>, </span><span>&amp;</span><span>y, </span><span>&amp;</span><span>R);</span></div></div><div><div><div>239</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：cpp/include/kl/ukf.hpp<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Error-state unscented Kalman filter for attitude: the same 6-dim error</span></div></div><div><div><div>2</div></div><div><span>// [dtheta, db] as the ESKF, additive noise, scaled unscented transform with</span></div></div><div><div><div>3</div></div><div><span>// alpha = 1, beta = 2, kappa = 0 (so lambda = 0, W0 = 0 and all weights positive).</span></div></div><div><div><div>4</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/eskf.hpp"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// Sigma points of N(0, P): the centre, then +-columns of chol((n + lambda) P).</span></div></div><div><div><div>10</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>SigmaPoints</span><span> {</span></div></div><div><div><div>12</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> std::</span><span>size_t</span><span> </span><span>COUNT</span><span> </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>N</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>T</span><span> </span><span>ALPHA</span><span> </span><span>=</span><span> </span><span>1</span><span>, </span><span>BETA</span><span> </span><span>=</span><span> </span><span>2</span><span>, </span><span>KAPPA</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>T</span><span> </span><span>LAMBDA</span><span> </span><span>=</span><span> </span><span>ALPHA</span><span> </span><span>*</span><span> </span><span>ALPHA</span><span> </span><span>*</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>KAPPA</span><span>) </span><span>-</span><span> </span><span>N</span><span>;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>COUNT</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>X</span><span>;</span><span>  // one point per row</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>COUNT</span><span>&gt; </span><span>Wm</span><span>, </span><span>Wc</span><span>;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>// Returns false if P is not positive definite.</span></div></div><div><div><div>20</div></div><div><span>  </span><span>bool</span><span> </span><span>compute</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>) {</span></span></div></div><div><div><div>21</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>P</span><span> </span><span>*</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>LAMBDA</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>X</span><span> </span><span>=</span><span> {};</span></div></div><div><div><div>24</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>j</span><span>++</span><span>)</span></div></div><div><div><div>25</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>j</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) {</span><span>  // L is lower triangular</span></div></div><div><div><div>26</div></div><div><span>        </span><span>X</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>j</span><span>, </span><span>k</span><span>) </span><span>=</span><span> </span><span>L</span><span>(</span><span>k</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>27</div></div><div><span>        </span><span>X</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>N</span><span> </span><span>+</span><span> </span><span>j</span><span>, </span><span>k</span><span>) </span><span>=</span><span> </span><span>-</span><span>L</span><span>(</span><span>k</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>    </span><span>Wm</span><span>.</span><span>fill</span><span>(</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>/</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>LAMBDA</span><span>));</span></div></div><div><div><div>30</div></div><div><span>    </span><span>Wc</span><span> </span><span>=</span><span> </span><span>Wm</span><span>;</span></div></div><div><div><div>31</div></div><div><span>    </span><span>Wm</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>LAMBDA</span><span> </span><span>/</span><span> (</span><span>N</span><span> </span><span>+</span><span> </span><span>LAMBDA</span><span>);</span></div></div><div><div><div>32</div></div><div><span>    </span><span>Wc</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>Wm</span><span>[</span><span>0</span><span>] </span><span>+</span><span> (</span><span>1</span><span> </span><span>-</span><span> </span><span>ALPHA</span><span> </span><span>*</span><span> </span><span>ALPHA</span><span> </span><span>+</span><span> </span><span>BETA</span><span>);</span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>};</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>38</div></div><div><span>class</span><span> </span><span>AttitudeUKF</span><span> {</span></div></div><div><div><div>39</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> std::</span><span>size_t</span><span> </span><span>N</span><span> </span><span>=</span><span> </span><span>6</span><span>, </span><span>K</span><span> </span><span>=</span><span> SigmaPoints&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>COUNT</span><span>;</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>public:</span></div></div><div><div><div>42</div></div><div><span>  </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>q</span><span>;</span></div></div><div><div><div>43</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>b</span><span>{};</span></div></div><div><div><div>44</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>P</span><span>{};</span></div></div><div><div><div>45</div></div><div><span>  </span><span>T</span><span> </span><span>sg</span><span>, </span><span>sbw</span><span>, </span><span>sa</span><span>, </span><span>sm</span><span>;</span></div></div><div><div><div>46</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_w</span><span>;</span></div></div><div><div><div>47</div></div><div><span>  </span><span>T</span><span> </span><span>accel_gate</span><span>, </span><span>mag_gate</span><span>;</span><span>  // negative disables the gate</span></div></div><div><div><div>48</div></div><div><span>  </span><span>bool</span><span> </span><span>reset</span><span>;</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>  </span><span>AttitudeUKF</span><span>(</span><span>T</span><span><span> </span><span>sigma_g</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_bw</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_a</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_m</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m_w</span><span>, </span></span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q0</span><span>,</span></span></div></div><div><div><div>51</div></div><div><span>        </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P0</span><span>, </span></span><span>T</span><span><span> </span><span>accel_gate</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>), </span><span>T</span><span><span> </span><span>mag_gate</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0.1</span><span>), </span><span>bool</span><span><span> </span><span>reset</span><span> </span></span><span>=</span><span> </span><span>true</span><span>)</span></div></div><div><div><div>52</div></div><div><span><span>      </span></span><span>: </span><span>q</span><span>(</span><span>q0</span><span>), </span><span>P</span><span>(</span><span>P0</span><span>), </span><span>sg</span><span>(</span><span>sigma_g</span><span>), </span><span>sbw</span><span>(</span><span>sigma_bw</span><span>), </span><span>sa</span><span>(</span><span>sigma_a</span><span>), </span><span>sm</span><span>(</span><span>sigma_m</span><span>), </span><span>m_w</span><span>(</span><span>m_w</span><span>),</span></div></div><div><div><div>53</div></div><div><span>        </span><span>accel_gate</span><span>(</span><span>accel_gate</span><span>), </span><span>mag_gate</span><span>(</span><span>mag_gate</span><span>), </span><span>reset</span><span>(</span><span>reset</span><span>) {}</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span><span>  </span></span><span>// Returns false, leaving the state unchanged, if P is not positive definite.</span></div></div><div><div><div>56</div></div><div><span>  </span><span>bool</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>57</div></div><div><span>    </span><span>SigmaPoints</span><span>&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>sp</span><span>;</span></div></div><div><div><div>58</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>sp</span><span>.</span><span>compute</span><span>(</span><span>P</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;, </span><span>K</span><span>&gt; </span><span>qi</span><span>;</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;, </span><span>K</span><span>&gt; </span><span>bi</span><span>;</span></div></div><div><div><div>61</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>62</div></div><div><span>      </span><span>qi</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>rows3</span><span>(</span><span>sp</span><span>.</span><span>X</span><span>, </span><span>i</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>63</div></div><div><span>      </span><span>bi</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>rows3</span><span>(</span><span>sp</span><span>.</span><span>X</span><span>, </span><span>i</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>64</div></div><div><span>      </span><span>qi</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>qi</span><span>[</span><span>i</span><span>] </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>((</span><span>gyro</span><span> </span><span>-</span><span> </span><span>bi</span><span>[</span><span>i</span><span>]) </span><span>*</span><span> </span><span>dt</span><span>);</span><span>  // each point through the gyro model</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>66</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>q_ref</span><span> </span><span>=</span><span> </span><span>qi</span><span>[</span><span>0</span><span>];</span><span>  // the propagated centre point</span></div></div><div><div><div>67</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>K</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>D</span><span>;</span></div></div><div><div><div>68</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>69</div></div><div><span>      </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>th</span><span> </span><span>=</span><span> (</span><span>q_ref</span><span>.</span><span>conj</span><span>() </span><span>*</span><span> </span><span>qi</span><span>[</span><span>i</span><span>]).</span><span>log</span><span>(), </span><span>db</span><span> </span><span>=</span><span> </span><span>bi</span><span>[</span><span>i</span><span>] </span><span>-</span><span> </span><span>b</span><span>;</span></div></div><div><div><div>70</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>71</div></div><div><span>        </span><span>D</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>th</span><span>(</span><span>j</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>72</div></div><div><span>        </span><span>D</span><span>(</span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>j</span><span>) </span><span>=</span><span> </span><span>db</span><span>(</span><span>j</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>73</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>75</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>mean</span><span>{};</span></div></div><div><div><div>76</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>77</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>6</span><span>; </span><span>j</span><span>++</span><span>) </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>j</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wm</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>D</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>78</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>cov</span><span>{};</span></div></div><div><div><div>79</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>80</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>r</span><span> </span><span>&lt;</span><span> </span><span>6</span><span>; </span><span>r</span><span>++</span><span>)</span></div></div><div><div><div>81</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>c</span><span> </span><span>&lt;</span><span> </span><span>6</span><span>; </span><span>c</span><span>++</span><span>)</span></div></div><div><div><div>82</div></div><div><span>          </span><span>cov</span><span>(</span><span>r</span><span>, </span><span>c</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wc</span><span>[</span><span>i</span><span>] </span><span>*</span><span> (</span><span>D</span><span>(</span><span>i</span><span>, </span><span>r</span><span>) </span><span>-</span><span> </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>r</span><span>)) </span><span>*</span><span> (</span><span>D</span><span>(</span><span>i</span><span>, </span><span>c</span><span>) </span><span>-</span><span> </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>c</span><span>));</span></div></div><div><div><div>83</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>84</div></div><div><span>      </span><span>cov</span><span>(</span><span>i</span><span>, </span><span>i</span><span>) </span><span>+=</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>dt</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>85</div></div><div><span>      </span><span>cov</span><span>(</span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>+=</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>86</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>87</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>q_ref</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>mean</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>1</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>2</span><span>)}});</span><span>  // not normalized here</span></div></div><div><div><div>88</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>mean</span><span>(</span><span>0</span><span>, </span><span>3</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>4</span><span>), </span><span>mean</span><span>(</span><span>0</span><span>, </span><span>5</span><span>)}};</span></div></div><div><div><div>89</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>cov</span><span>;</span></div></div><div><div><div>90</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>91</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span><span>  </span></span><span>// Gravity-direction update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>94</div></div><div><span>  </span><span>bool</span><span> </span><span>update_accel</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>) {</span></span></div></div><div><div><div>95</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>96</div></div><div><span>    </span><span>if</span><span> (</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>&gt;</span><span> </span><span>accel_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>97</div></div><div><span>    </span><span>SigmaPoints</span><span>&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>sp</span><span>;</span></div></div><div><div><div>98</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>sp</span><span>.</span><span>compute</span><span>(</span><span>P</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>99</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>K</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Z</span><span>;</span></div></div><div><div><div>100</div></div><div><span>    </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>zbar</span><span>{};</span></div></div><div><div><div>101</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>102</div></div><div><span>      </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>z</span><span> </span><span>=</span><span> </span><span>rot_inv</span><span>(</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>rows3</span><span>(</span><span>sp</span><span>.</span><span>X</span><span>, </span><span>i</span><span>, </span><span>0</span><span>)), </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}});</span></div></div><div><div><div>103</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>104</div></div><div><span>        </span><span>Z</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>z</span><span>(</span><span>j</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>105</div></div><div><span>        </span><span>zbar</span><span>(</span><span>j</span><span>, </span><span>0</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wm</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>z</span><span>(</span><span>j</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>106</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>107</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>108</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>*</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>));</span></div></div><div><div><div>109</div></div><div><span>    </span><span>return</span><span> </span><span>correct</span><span>(</span><span>sp</span><span>, </span><span>Z</span><span>, </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> </span><span>norm</span><span>) </span><span>-</span><span> </span><span>zbar</span><span>, Mat&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>*</span><span> </span><span>r</span><span>);</span></div></div><div><div><div>110</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>111</div></div><div>
</div></div><div><div><div>112</div></div><div><span><span>  </span></span><span>// Heading-only magnetometer update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>113</div></div><div><span>  </span><span>bool</span><span> </span><span>update_mag_heading</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>) {</span></span></div></div><div><div><div>114</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>115</div></div><div><span>    </span><span>if</span><span> (</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>mag_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>116</div></div><div><span>    </span><span>SigmaPoints</span><span>&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>sp</span><span>;</span></div></div><div><div><div>117</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>sp</span><span>.</span><span>compute</span><span>(</span><span>P</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>118</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>C</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>();</span></div></div><div><div><div>119</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>psi_h</span><span> </span><span>=</span><span> std::</span><span>atan2</span><span>(</span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>120</div></div><div><span><span>    </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>K</span><span>&gt; </span><span>z0</span><span>;</span></div></div><div><div><div>121</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>122</div></div><div><span>      </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>mw</span><span> </span><span>=</span><span> </span><span>C</span><span> </span><span>*</span><span> </span><span>rot_inv</span><span>(</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>rows3</span><span>(</span><span>sp</span><span>.</span><span>X</span><span>, </span><span>i</span><span>, </span><span>0</span><span>)), </span><span>m_w</span><span>);</span></div></div><div><div><div>123</div></div><div><span>      </span><span>z0</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> </span><span>psi_h</span><span>);</span></div></div><div><div><div>124</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>125</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>K</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>Zrel</span><span>;</span><span>  // heading relative to the centre point: no wrap trouble</span></div></div><div><div><div>126</div></div><div><span>    </span><span>T</span><span> </span><span>zbar</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>127</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>128</div></div><div><span>      </span><span>Zrel</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>wrap_pi</span><span>(</span><span>z0</span><span>[</span><span>i</span><span>] </span><span>-</span><span> </span><span>z0</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>129</div></div><div><span>      </span><span>zbar</span><span> </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wm</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>Zrel</span><span>(</span><span>i</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>130</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>131</div></div><div><span>    </span><span>zbar</span><span> </span><span>+=</span><span> </span><span>z0</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>132</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_meas</span><span> </span><span>=</span><span> </span><span>C</span><span> </span><span>*</span><span> </span><span>mag</span><span>;</span></div></div><div><div><div>133</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>y</span><span>{{</span><span>wrap_pi</span><span>(</span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>m_meas</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>m_meas</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> </span><span>psi_h</span><span>) </span><span>-</span><span> </span><span>zbar</span><span>)}};</span></div></div><div><div><div>134</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>sm</span><span> </span><span>/</span><span> std::</span><span>hypot</span><span>(</span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>135</div></div><div><span>    </span><span>return</span><span> </span><span>correct</span><span>(</span><span>sp</span><span>, </span><span>Zrel</span><span>, </span><span>y</span><span>, </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>r</span><span> </span><span>*</span><span> </span><span>r</span><span>}});</span></div></div><div><div><div>136</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>137</div></div><div>
</div></div><div><div><div>138</div></div><div><span>private:</span></div></div><div><div><div>139</div></div><div><span>  </span><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>R</span><span>, std::</span><span>size_t</span><span> </span><span>C</span><span>&gt;</span></div></div><div><div><div>140</div></div><div><span>  </span><span>static</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>rows3</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>C</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>X</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>i</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>c0</span><span>) {</span></span></div></div><div><div><div>141</div></div><div><span>    </span><span>return</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>X</span><span>(</span><span>i</span><span>, </span><span>c0</span><span>), </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c0</span><span> </span><span>+</span><span> </span><span>1</span><span>), </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c0</span><span> </span><span>+</span><span> </span><span>2</span><span>)}};</span></div></div><div><div><div>142</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>143</div></div><div>
</div></div><div><div><div>144</div></div><div><span><span>  </span></span><span>// Standard UKF measurement update given the model outputs Z at the sigma points.</span></div></div><div><div><div>145</div></div><div><span><span>  </span></span><span>// y is the innovation, already formed by the caller.</span></div></div><div><div><div>146</div></div><div><span>  </span><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>M</span><span>&gt;</span></div></div><div><div><div>147</div></div><div><span>  </span><span>bool</span><span> </span><span>correct</span><span>(</span><span>const</span><span> </span><span>SigmaPoints</span><span>&lt;</span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>sp</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>K</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Z</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>y</span><span>,</span></span></div></div><div><div><div>148</div></div><div><span>         </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>R</span><span>) {</span></span></div></div><div><div><div>149</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>zbar</span><span>{};</span></div></div><div><div><div>150</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>151</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>M</span><span>; </span><span>j</span><span>++</span><span>) </span><span>zbar</span><span>(</span><span>0</span><span>, </span><span>j</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wm</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>Z</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>152</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span> </span><span>=</span><span> </span><span>R</span><span>;</span></div></div><div><div><div>153</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>Pxz</span><span>{};</span></div></div><div><div><div>154</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>155</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>r</span><span> </span><span>&lt;</span><span> </span><span>M</span><span>; </span><span>r</span><span>++</span><span>) {</span></div></div><div><div><div>156</div></div><div><span>        </span><span>const</span><span> </span><span>T</span><span> </span><span>dr</span><span> </span><span>=</span><span> </span><span>Z</span><span>(</span><span>i</span><span>, </span><span>r</span><span>) </span><span>-</span><span> </span><span>zbar</span><span>(</span><span>0</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>157</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>c</span><span> </span><span>&lt;</span><span> </span><span>M</span><span>; </span><span>c</span><span>++</span><span>) </span><span>S</span><span>(</span><span>r</span><span>, </span><span>c</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wc</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>dr</span><span> </span><span>*</span><span> (</span><span>Z</span><span>(</span><span>i</span><span>, </span><span>c</span><span>) </span><span>-</span><span> </span><span>zbar</span><span>(</span><span>0</span><span>, </span><span>c</span><span>));</span></div></div><div><div><div>158</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>Pxz</span><span>(</span><span>k</span><span>, </span><span>r</span><span>) </span><span>+=</span><span> </span><span>sp</span><span>.</span><span>Wc</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>sp</span><span>.</span><span>X</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>dr</span><span>;</span></div></div><div><div><div>159</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>160</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>S</span><span>;</span></div></div><div><div><div>161</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>162</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>Kg</span><span> </span><span>=</span><span> </span><span>chol_solve</span><span>(</span><span>L</span><span>, </span><span>Pxz</span><span>.</span><span>transpose</span><span>()).</span><span>transpose</span><span>();</span></div></div><div><div><div>163</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span> </span><span>=</span><span> </span><span>Kg</span><span> </span><span>*</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>164</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> (</span><span>P</span><span> </span><span>-</span><span> </span><span>Kg</span><span> </span><span>*</span><span> </span><span>S</span><span> </span><span>*</span><span> </span><span>Kg</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>165</div></div><div><span>    </span><span>inject</span><span>(</span><span>dx</span><span>);</span></div></div><div><div><div>166</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>167</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>168</div></div><div>
</div></div><div><div><div>169</div></div><div><span><span>  </span></span><span>// q &lt;- q (x) exp(dtheta), b &lt;- b + db, then reset P &lt;- G P G^T.</span></div></div><div><div><div>170</div></div><div><span>  </span><span>void</span><span> </span><span>inject</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>dx</span><span>) {</span></span></div></div><div><div><div>171</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dth</span><span>{{</span><span>dx</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>172</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> (</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>dth</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>173</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>3</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>4</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>5</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>174</div></div><div><span>    </span><span>if</span><span> (</span><span>reset</span><span>) {</span></div></div><div><div><div>175</div></div><div><span>      </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>G</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>176</div></div><div><span>      </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>dth</span><span> </span><span>*</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>));</span></div></div><div><div><div>177</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>178</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>G</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>-=</span><span> </span><span>S</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>179</div></div><div><span>      </span><span>P</span><span> </span><span>=</span><span> </span><span>G</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>G</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>180</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>181</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>182</div></div><div><span>};</span></div></div><div><div><div>183</div></div><div>
</div></div><div><div><div>184</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：rust/src/ukf.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Error-state unscented Kalman filter for attitude: the same 6-dim error as the</span></div></div><div><div><div>2</div></div><div><span>//! ESKF (dtheta, db), additive noise, scaled unscented transform.</span></div></div><div><div><div>3</div></div><div><span>#![allow(clippy</span><span>::</span><span>needless_range_loop)]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>ekf_att</span><span>::{</span></span><span>G</span><span>, rot_inv};</span></div></div><div><div><div>6</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>eskf</span><span>::wrap_pi;</span></span></div></div><div><div><div>7</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::{</span></span><span>Mat</span><span>, chol_solve};</span></div></div><div><div><div>8</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>quat</span><span>::{</span></span><span>Quat</span><span>, skew};</span></div></div><div><div><div>9</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, atan2, fabs, sqrt};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>6</span><span>;</span></div></div><div><div><div>12</div></div><div><span>const</span><span> </span><span>NP</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>2</span><span> * </span><span>N</span><span> + </span><span>1</span><span>;</span></div></div><div><div><div>13</div></div><div><span>/// Scaled UT with alpha = 1, beta = 2, kappa = 0: lambda = 0, so the centre</span></div></div><div><div><div>14</div></div><div><span>/// point has mean weight 0 and the other 2n points share the weight 1/(2n).</span></div></div><div><div><div>15</div></div><div><span>const</span><span> </span><span>LAMBDA</span><span>: </span><span>Real</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>16</div></div><div><span>const</span><span> </span><span>BETA</span><span>: </span><span>Real</span><span><span> </span><span>=</span><span> </span></span><span>2.0</span><span>;</span></div></div><div><div><div>17</div></div><div><span>const</span><span> </span><span>ALPHA2</span><span>: </span><span>Real</span><span><span> </span><span>=</span><span> </span></span><span>1.0</span><span>;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>pub</span><span> </span><span>type</span><span> </span><span>Points</span><span><span> </span><span>=</span><span> [[</span></span><span>Real</span><span>; </span><span>N</span><span>]; </span><span>NP</span><span>];</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>fn</span><span> </span><span>weights</span><span>() -&gt; ([</span><span>Real</span><span>; </span><span>NP</span><span>], [</span><span>Real</span><span>; </span><span>NP</span><span>]) {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>N</span><span> </span><span>as</span><span> </span><span>Real</span><span>;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>wm</span><span><span> </span><span>=</span><span> [</span></span><span>0.5</span><span> / (</span><span>n</span><span> + </span><span>LAMBDA</span><span>); </span><span>NP</span><span>];</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>wc</span><span><span> </span><span>=</span><span> </span></span><span>wm</span><span>;</span></div></div><div><div><div>25</div></div><div><span>    </span><span>wm</span><span>[</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>LAMBDA</span><span> / (</span><span>n</span><span> + </span><span>LAMBDA</span><span>);</span></div></div><div><div><div>26</div></div><div><span>    </span><span>wc</span><span>[</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>wm</span><span>[</span><span>0</span><span>] + (</span><span>1.0</span><span> - </span><span>ALPHA2</span><span> + </span><span>BETA</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>(</span><span>wm</span><span>, </span><span>wc</span><span>)</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>/// Sigma points around zero for N(0, P): the centre, then +-columns of</span></div></div><div><div><div>31</div></div><div><span>/// chol((n + lambda) P). Returns `None` if P is not positive definite.</span></div></div><div><div><div>32</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>sigma_points</span><span>(</span><span>p</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;) -&gt; </span><span>Option</span><span>&lt;</span><span>Points</span><span>&gt; {</span></div></div><div><div><div>33</div></div><div><span>    </span><span>let</span><span> </span><span>l</span><span><span> </span><span>=</span><span> (*</span></span><span>p</span><span> * (</span><span>N</span><span> </span><span>as</span><span> </span><span>Real</span><span> + </span><span>LAMBDA</span><span>)).</span><span>cholesky</span><span>()?;</span></div></div><div><div><div>34</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>x</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>N</span><span>]; </span><span>NP</span><span>];</span></div></div><div><div><div>35</div></div><div><span>    </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>37</div></div><div><span>            </span><span>x</span><span>[</span><span>1</span><span> + </span><span>j</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>l</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>38</div></div><div><span>            </span><span>x</span><span>[</span><span>1</span><span> + </span><span>N</span><span> + </span><span>j</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> -</span></span><span>l</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span>    </span><span>Some</span><span>(</span><span>x</span><span>)</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>fn</span><span> </span><span>dot</span><span>(</span><span>a</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>45</div></div><div><span>    </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>] + </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>] + </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>]</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>AttitudeUkf</span><span> {</span></div></div><div><div><div>49</div></div><div><span>    </span><span>pub</span><span> </span><span>q</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>50</div></div><div><span>    </span><span>pub</span><span> </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>51</div></div><div><span>    </span><span>pub</span><span> </span><span>p</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>52</div></div><div><span>    </span><span>sg</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>53</div></div><div><span>    </span><span>sbw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>54</div></div><div><span>    </span><span>sa</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>55</div></div><div><span>    </span><span>sm</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>56</div></div><div><span>    </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>57</div></div><div><span>    </span><span>/// Gates on | |f| - g | (m/s^2) and | |m| - 1 |; negative disables gating.</span></div></div><div><div><div>58</div></div><div><span>    </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>59</div></div><div><span>    </span><span>mag_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>60</div></div><div><span>    </span><span>reset</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>61</div></div><div><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>impl</span><span> </span><span>AttitudeUkf</span><span> {</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>#[allow(clippy</span><span>::</span><span>too_many_arguments)]</span></div></div><div><div><div>65</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span></div></div><div><div><div>66</div></div><div><span>        </span><span>q0</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>67</div></div><div><span>        </span><span>p0_diag</span><span>: [</span><span>Real</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>68</div></div><div><span>        </span><span>sigma_g</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>69</div></div><div><span>        </span><span>sigma_bw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>70</div></div><div><span>        </span><span>sigma_a</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>71</div></div><div><span>        </span><span>sigma_m</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>72</div></div><div><span>        </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>73</div></div><div><span>        </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>74</div></div><div><span>        </span><span>mag_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>75</div></div><div><span>        </span><span>reset</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>77</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>78</div></div><div><span>        </span><span>for</span><span> (</span><span>i</span><span>, </span><span>d</span><span>) </span><span>in</span><span> </span><span>p0_diag</span><span>.</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>79</div></div><div><span>            </span><span>p</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> *</span></span><span>d</span><span>;</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>82</div></div><div><span>            </span><span>q</span><span>: </span><span>q0</span><span>,</span></div></div><div><div><div>83</div></div><div><span>            </span><span>b</span><span>: [</span><span>0.0</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>84</div></div><div><span>            </span><span>p</span><span>,</span></div></div><div><div><div>85</div></div><div><span>            </span><span>sg</span><span>: </span><span>sigma_g</span><span>,</span></div></div><div><div><div>86</div></div><div><span>            </span><span>sbw</span><span>: </span><span>sigma_bw</span><span>,</span></div></div><div><div><div>87</div></div><div><span>            </span><span>sa</span><span>: </span><span>sigma_a</span><span>,</span></div></div><div><div><div>88</div></div><div><span>            </span><span>sm</span><span>: </span><span>sigma_m</span><span>,</span></div></div><div><div><div>89</div></div><div><span>            </span><span>m_w</span><span>,</span></div></div><div><div><div>90</div></div><div><span>            </span><span>accel_gate</span><span>,</span></div></div><div><div><div>91</div></div><div><span>            </span><span>mag_gate</span><span>,</span></div></div><div><div><div>92</div></div><div><span>            </span><span>reset</span><span>,</span></div></div><div><div><div>93</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>94</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>95</div></div><div>
</div></div><div><div><div>96</div></div><div><span>    </span><span>/// Sigma-point attitudes q (x) exp(dtheta_i).</span></div></div><div><div><div>97</div></div><div><span>    </span><span>fn</span><span> </span><span>point_attitudes</span><span>(&amp;</span><span>self</span><span>, </span><span>x</span><span>: &amp;</span><span>Points</span><span>) -&gt; [</span><span>Quat</span><span>; </span><span>NP</span><span>] {</span></div></div><div><div><div>98</div></div><div><span>        </span><span>x</span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>r</span><span><span>|</span><span> </span></span><span>self</span><span><span>.q * </span><span>Quat</span><span>::</span></span><span>exp</span><span>([</span><span>r</span><span>[</span><span>0</span><span>], </span><span>r</span><span>[</span><span>1</span><span>], </span><span>r</span><span>[</span><span>2</span><span>]]))</span></div></div><div><div><div>99</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>100</div></div><div>
</div></div><div><div><div>101</div></div><div><span>    </span><span>/// Propagate every sigma point through the gyro model. False (state</span></div></div><div><div><div>102</div></div><div><span>    </span><span>/// unchanged) if P is not positive definite.</span></div></div><div><div><div>103</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>104</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>x</span><span><span>) </span><span>=</span><span> </span></span><span>sigma_points</span><span>(&amp;</span><span>self</span><span>.p) </span><span>else</span><span> {</span></div></div><div><div><div>105</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>106</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>107</div></div><div><span>        </span><span>let</span><span> (</span><span>wm</span><span>, </span><span>wc</span><span><span>) </span><span>=</span><span> </span></span><span>weights</span><span>();</span></div></div><div><div><div>108</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>qi</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>point_attitudes</span><span>(&amp;</span><span>x</span><span>);</span></div></div><div><div><div>109</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>bi</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>3</span><span>]; </span><span>NP</span><span>];</span></div></div><div><div><div>110</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>111</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>112</div></div><div><span>                </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>k</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.b[</span><span>k</span><span>] + </span><span>x</span><span>[</span><span>i</span><span>][</span><span>3</span><span> + </span><span>k</span><span>];</span></div></div><div><div><div>113</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>114</div></div><div><span>            </span><span>let</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>115</div></div><div><span><span>                </span></span><span>(</span><span>gyro</span><span>[</span><span>0</span><span>] - </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>0</span><span>]) * </span><span>dt</span><span>,</span></div></div><div><div><div>116</div></div><div><span><span>                </span></span><span>(</span><span>gyro</span><span>[</span><span>1</span><span>] - </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>1</span><span>]) * </span><span>dt</span><span>,</span></div></div><div><div><div>117</div></div><div><span><span>                </span></span><span>(</span><span>gyro</span><span>[</span><span>2</span><span>] - </span><span>bi</span><span>[</span><span>i</span><span>][</span><span>2</span><span>]) * </span><span>dt</span><span>,</span></div></div><div><div><div>118</div></div><div><span><span>            </span></span><span>];</span></div></div><div><div><div>119</div></div><div><span>            </span><span>qi</span><span>[</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>qi</span><span>[</span><span>i</span><span>] * </span><span>Quat</span><span>::</span><span>exp</span><span>(</span><span>w</span><span>);</span></div></div><div><div><div>120</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>121</div></div><div><span>        </span><span>// Deviations from the propagated centre point, in its tangent space.</span></div></div><div><div><div>122</div></div><div><span>        </span><span>let</span><span> </span><span>q_ref</span><span><span> </span><span>=</span><span> </span></span><span>qi</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>123</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>d</span><span><span> </span><span>=</span><span> [[</span></span><span>0.0</span><span>; </span><span>N</span><span>]; </span><span>NP</span><span>];</span></div></div><div><div><div>124</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>125</div></div><div><span>            </span><span>let</span><span> </span><span>th</span><span><span> </span><span>=</span><span> (</span></span><span>q_ref</span><span>.</span><span>conj</span><span>() * </span><span>qi</span><span>[</span><span>i</span><span>]).</span><span>log</span><span>();</span></div></div><div><div><div>126</div></div><div><span>            </span><span>d</span><span>[</span><span>i</span><span>][..</span><span>3</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>th</span><span>);</span></div></div><div><div><div>127</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>128</div></div><div><span>                </span><span>d</span><span>[</span><span>i</span><span>][</span><span>3</span><span> + </span><span>k</span><span><span>] </span><span>=</span><span> </span></span><span>bi</span><span>[</span><span>i</span><span>][</span><span>k</span><span>] - </span><span>self</span><span>.b[</span><span>k</span><span>];</span></div></div><div><div><div>129</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>130</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>131</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>mean</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>N</span><span>];</span></div></div><div><div><div>132</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>133</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>134</div></div><div><span>                </span><span>mean</span><span>[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>wm</span><span>[</span><span>i</span><span>] * </span><span>d</span><span>[</span><span>i</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>135</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>136</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>137</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>cov</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>138</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>139</div></div><div><span>            </span><span>for</span><span> </span><span>r</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>140</div></div><div><span>                </span><span>for</span><span> </span><span>c</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>141</div></div><div><span>                    </span><span>cov</span><span>.m[</span><span>r</span><span>][</span><span>c</span><span><span>] </span><span>+=</span><span> </span></span><span>wc</span><span>[</span><span>i</span><span>] * (</span><span>d</span><span>[</span><span>i</span><span>][</span><span>r</span><span>] - </span><span>mean</span><span>[</span><span>r</span><span>]) * (</span><span>d</span><span>[</span><span>i</span><span>][</span><span>c</span><span>] - </span><span>mean</span><span>[</span><span>c</span><span>]);</span></div></div><div><div><div>142</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>143</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>144</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>145</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>146</div></div><div><span>            </span><span>cov</span><span>.m[</span><span>k</span><span>][</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>self</span><span>.sg * </span><span>self</span><span>.sg * (</span><span>dt</span><span> * </span><span>dt</span><span>);</span></div></div><div><div><div>147</div></div><div><span>            </span><span>cov</span><span>.m[</span><span>3</span><span> + </span><span>k</span><span>][</span><span>3</span><span> + </span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>;</span></div></div><div><div><div>148</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>149</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>q_ref</span><span> * </span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>mean</span><span>[</span><span>0</span><span>], </span><span>mean</span><span>[</span><span>1</span><span>], </span><span>mean</span><span>[</span><span>2</span><span>]]);</span></div></div><div><div><div>150</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>151</div></div><div><span>            </span><span>self</span><span>.b[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>mean</span><span>[</span><span>3</span><span> + </span><span>k</span><span>];</span></div></div><div><div><div>152</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>153</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>cov</span><span>;</span></div></div><div><div><div>154</div></div><div><span>        </span><span>true</span></div></div><div><div><div>155</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>156</div></div><div>
</div></div><div><div><div>157</div></div><div><span>    </span><span>fn</span><span> </span><span>inject</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>dx</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>&gt;) {</span></div></div><div><div><div>158</div></div><div><span>        </span><span>let</span><span> </span><span>dth</span><span><span> </span><span>=</span><span> [</span></span><span>dx</span><span>.m[</span><span>0</span><span>][</span><span>0</span><span>], </span><span>dx</span><span>.m[</span><span>1</span><span>][</span><span>0</span><span>], </span><span>dx</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>]];</span></div></div><div><div><div>159</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> (</span></span><span>self</span><span>.q * </span><span>Quat</span><span>::</span><span>exp</span><span>(</span><span>dth</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>160</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>161</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>dx</span><span>.m[</span><span>3</span><span> + </span><span>i</span><span>][</span><span>0</span><span>];</span></div></div><div><div><div>162</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>163</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.reset {</span></div></div><div><div><div>164</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>([</span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>0</span><span>], </span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>1</span><span>], </span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>2</span><span>]]);</span></div></div><div><div><div>165</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>g</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>166</div></div><div><span>            </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>167</div></div><div><span>                </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>168</div></div><div><span>                    </span><span>g</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>169</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>170</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>171</div></div><div><span>            </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>g</span><span> * </span><span>self</span><span>.p * </span><span>g</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>172</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>173</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>174</div></div><div>
</div></div><div><div><div>175</div></div><div><span>    </span><span>/// Standard UKF correction given model outputs `z` at the sigma points `x`.</span></div></div><div><div><div>176</div></div><div><span>    </span><span>/// False (state unchanged) if S is not positive definite.</span></div></div><div><div><div>177</div></div><div><span>    </span><span>fn</span><span> </span><span>correct</span><span>&lt;</span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>178</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>179</div></div><div><span>        </span><span>x</span><span>: &amp;</span><span>Points</span><span>,</span></div></div><div><div><div>180</div></div><div><span>        </span><span>z</span><span>: &amp;[[</span><span>Real</span><span>; </span><span>M</span><span>]; </span><span>NP</span><span>],</span></div></div><div><div><div>181</div></div><div><span>        </span><span>y</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>182</div></div><div><span>        </span><span>r</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>183</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>184</div></div><div><span>        </span><span>let</span><span> (</span><span>wm</span><span>, </span><span>wc</span><span><span>) </span><span>=</span><span> </span></span><span>weights</span><span>();</span></div></div><div><div><div>185</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>zbar</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>M</span><span>];</span></div></div><div><div><div>186</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>187</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>M</span><span> {</span></div></div><div><div><div>188</div></div><div><span>                </span><span>zbar</span><span>[</span><span>k</span><span><span>] </span><span>+=</span><span> </span></span><span>wm</span><span>[</span><span>i</span><span>] * </span><span>z</span><span>[</span><span>i</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>189</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>190</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>191</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> *</span></span><span>r</span><span>;</span></div></div><div><div><div>192</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>pxz</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>193</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>194</div></div><div><span>            </span><span>for</span><span> </span><span>a</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>M</span><span> {</span></div></div><div><div><div>195</div></div><div><span>                </span><span>for</span><span> </span><span>b</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>M</span><span> {</span></div></div><div><div><div>196</div></div><div><span>                    </span><span>s</span><span>.m[</span><span>a</span><span>][</span><span>b</span><span><span>] </span><span>+=</span><span> </span></span><span>wc</span><span>[</span><span>i</span><span>] * (</span><span>z</span><span>[</span><span>i</span><span>][</span><span>a</span><span>] - </span><span>zbar</span><span>[</span><span>a</span><span>]) * (</span><span>z</span><span>[</span><span>i</span><span>][</span><span>b</span><span>] - </span><span>zbar</span><span>[</span><span>b</span><span>]);</span></div></div><div><div><div>197</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>198</div></div><div><span>                </span><span>for</span><span> </span><span>a2</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>199</div></div><div><span>                    </span><span>pxz</span><span>.m[</span><span>a2</span><span>][</span><span>a</span><span><span>] </span><span>+=</span><span> </span></span><span>wc</span><span>[</span><span>i</span><span>] * </span><span>x</span><span>[</span><span>i</span><span>][</span><span>a2</span><span>] * (</span><span>z</span><span>[</span><span>i</span><span>][</span><span>a</span><span>] - </span><span>zbar</span><span>[</span><span>a</span><span>]);</span></div></div><div><div><div>200</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>201</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>202</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>203</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>l</span><span><span>) </span><span>=</span><span> </span></span><span>s</span><span>.</span><span>cholesky</span><span>() </span><span>else</span><span> {</span></div></div><div><div><div>204</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>205</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>206</div></div><div><span>        </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>chol_solve</span><span>(&amp;</span><span>l</span><span>, &amp;</span><span>pxz</span><span>.</span><span>transpose</span><span>()).</span><span>transpose</span><span>();</span></div></div><div><div><div>207</div></div><div><span>        </span><span>let</span><span> </span><span>dx</span><span><span> </span><span>=</span><span> </span></span><span>k</span><span> * *</span><span>y</span><span>;</span></div></div><div><div><div>208</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> (</span></span><span>self</span><span>.p - </span><span>k</span><span> * </span><span>s</span><span> * </span><span>k</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>209</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>210</div></div><div><span>        </span><span>true</span></div></div><div><div><div>211</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>212</div></div><div>
</div></div><div><div><div>213</div></div><div><span>    </span><span>/// Gravity-direction update. False when gated out or on Cholesky failure.</span></div></div><div><div><div>214</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_accel</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>215</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>216</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.accel_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>G</span><span>) &gt; </span><span>self</span><span>.accel_gate {</span></div></div><div><div><div>217</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>218</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>219</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>x</span><span><span>) </span><span>=</span><span> </span></span><span>sigma_points</span><span>(&amp;</span><span>self</span><span>.p) </span><span>else</span><span> {</span></div></div><div><div><div>220</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>221</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>222</div></div><div><span>        </span><span>let</span><span> (</span><span>wm</span><span>, </span><span>_</span><span><span>) </span><span>=</span><span> </span></span><span>weights</span><span>();</span></div></div><div><div><div>223</div></div><div><span>        </span><span>let</span><span> </span><span>qi</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>point_attitudes</span><span>(&amp;</span><span>x</span><span>);</span></div></div><div><div><div>224</div></div><div><span>        </span><span>let</span><span> </span><span>z</span><span><span> </span><span>=</span><span> </span></span><span>qi</span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>q</span><span><span>|</span><span> </span></span><span>rot_inv</span><span>(</span><span>q</span><span>, [</span><span>0.0</span><span>, </span><span>0.0</span><span>, -</span><span>1.0</span><span>]));</span></div></div><div><div><div>225</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>1</span><span>&gt; {</span></div></div><div><div><div>226</div></div><div><span>            </span><span>m</span><span>: [[</span><span>accel</span><span>[</span><span>0</span><span>] / </span><span>norm</span><span>], [</span><span>accel</span><span>[</span><span>1</span><span>] / </span><span>norm</span><span>], [</span><span>accel</span><span>[</span><span>2</span><span>] / </span><span>norm</span><span>]],</span></div></div><div><div><div>227</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>228</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>229</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>230</div></div><div><span>                </span><span>y</span><span>.m[</span><span>k</span><span>][</span><span>0</span><span><span>] </span><span>-=</span><span> </span></span><span>wm</span><span>[</span><span>i</span><span>] * </span><span>z</span><span>[</span><span>i</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>231</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>232</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>233</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>3</span><span>&gt;::</span><span>identity</span><span>() * ((</span><span>self</span><span>.sa / </span><span>G</span><span>) * (</span><span>self</span><span>.sa / </span><span>G</span><span>));</span></div></div><div><div><div>234</div></div><div><span>        </span><span>self</span><span>.</span><span>correct</span><span>(&amp;</span><span>x</span><span>, &amp;</span><span>z</span><span>, &amp;</span><span>y</span><span>, &amp;</span><span>r</span><span>)</span></div></div><div><div><div>235</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>236</div></div><div>
</div></div><div><div><div>237</div></div><div><span>    </span><span>/// Heading-only magnetometer update. False when gated out or on Cholesky failure.</span></div></div><div><div><div>238</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_mag_heading</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>239</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>240</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.mag_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>1.0</span><span>) &gt; </span><span>self</span><span>.mag_gate {</span></div></div><div><div><div>241</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>242</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>243</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>x</span><span><span>) </span><span>=</span><span> </span></span><span>sigma_points</span><span>(&amp;</span><span>self</span><span>.p) </span><span>else</span><span> {</span></div></div><div><div><div>244</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>245</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>246</div></div><div><span>        </span><span>let</span><span> (</span><span>wm</span><span>, </span><span>_</span><span><span>) </span><span>=</span><span> </span></span><span>weights</span><span>();</span></div></div><div><div><div>247</div></div><div><span>        </span><span>let</span><span> </span><span>qi</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>point_attitudes</span><span>(&amp;</span><span>x</span><span>);</span></div></div><div><div><div>248</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>249</div></div><div><span>        </span><span>let</span><span> </span><span>rot</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span><span>]</span><span>|</span><span> -&gt; [</span></span><span>Real</span><span>; </span><span>3</span><span>] { [</span><span>dot</span><span>(</span><span>c</span><span>.m[</span><span>0</span><span>], </span><span>v</span><span>), </span><span>dot</span><span>(</span><span>c</span><span>.m[</span><span>1</span><span>], </span><span>v</span><span>), </span><span>dot</span><span>(</span><span>c</span><span>.m[</span><span>2</span><span>], </span><span>v</span><span>)] };</span></div></div><div><div><div>250</div></div><div><span>        </span><span>let</span><span> </span><span>psi_h</span><span><span> </span><span>=</span><span> </span></span><span>atan2</span><span>(</span><span>self</span><span>.m_w[</span><span>1</span><span>], </span><span>self</span><span>.m_w[</span><span>0</span><span>]);</span></div></div><div><div><div>251</div></div><div><span>        </span><span>let</span><span> </span><span>z0</span><span><span> </span><span>=</span><span> </span></span><span>qi</span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>q</span><span><span>|</span><span> {</span></span></div></div><div><div><div>252</div></div><div><span>            </span><span>let</span><span> </span><span>mw</span><span><span> </span><span>=</span><span> </span></span><span>rot</span><span>(</span><span>rot_inv</span><span>(</span><span>q</span><span>, </span><span>self</span><span>.m_w));</span></div></div><div><div><div>253</div></div><div><span>            </span><span>wrap_pi</span><span>(</span><span>atan2</span><span>(</span><span>mw</span><span>[</span><span>1</span><span>], </span><span>mw</span><span>[</span><span>0</span><span>]) - </span><span>psi_h</span><span>)</span></div></div><div><div><div>254</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>255</div></div><div><span>        </span><span>// Headings relative to the centre point: no wrap trouble near +-pi.</span></div></div><div><div><div>256</div></div><div><span>        </span><span>let</span><span> </span><span>zrel</span><span><span> </span><span>=</span><span> </span></span><span>z0</span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>v</span><span><span>|</span><span> [</span></span><span>wrap_pi</span><span>(</span><span>v</span><span> - </span><span>z0</span><span>[</span><span>0</span><span>])]);</span></div></div><div><div><div>257</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>zbar</span><span><span> </span><span>=</span><span> </span></span><span>z0</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>258</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>NP</span><span> {</span></div></div><div><div><div>259</div></div><div><span>            </span><span>zbar</span><span><span> </span><span>+=</span><span> </span></span><span>wm</span><span>[</span><span>i</span><span>] * </span><span>zrel</span><span>[</span><span>i</span><span>][</span><span>0</span><span>];</span></div></div><div><div><div>260</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>261</div></div><div><span>        </span><span>let</span><span> </span><span>mm</span><span><span> </span><span>=</span><span> </span></span><span>rot</span><span>(</span><span>mag</span><span>);</span></div></div><div><div><div>262</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>wrap_pi</span><span>(</span><span>wrap_pi</span><span>(</span><span>atan2</span><span>(</span><span>mm</span><span>[</span><span>1</span><span>], </span><span>mm</span><span>[</span><span>0</span><span>]) - </span><span>psi_h</span><span>) - </span><span>zbar</span><span>);</span></div></div><div><div><div>263</div></div><div><span>        </span><span>let</span><span> </span><span>mh</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>self</span><span>.m_w[</span><span>0</span><span>] * </span><span>self</span><span>.m_w[</span><span>0</span><span>] + </span><span>self</span><span>.m_w[</span><span>1</span><span>] * </span><span>self</span><span>.m_w[</span><span>1</span><span>]);</span></div></div><div><div><div>264</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span> {</span></div></div><div><div><div>265</div></div><div><span>            </span><span>m</span><span>: [[(</span><span>self</span><span>.sm / </span><span>mh</span><span>) * (</span><span>self</span><span>.sm / </span><span>mh</span><span>)]],</span></div></div><div><div><div>266</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>267</div></div><div><span>        </span><span>self</span><span>.</span><span>correct</span><span>(&amp;</span><span>x</span><span>, &amp;</span><span>zrel</span><span>, &amp;</span><span>Mat</span><span> { </span><span>m</span><span>: [[</span><span>y</span><span>]] }, &amp;</span><span>r</span><span>)</span></div></div><div><div><div>268</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>269</div></div><div><span>}</span></div></div><div><div><div>270</div></div><div>
</div></div><div><div><div>271</div></div><div><span>/// Weights, exposed for the self-check.</span></div></div><div><div><div>272</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>unscented_weights</span><span>() -&gt; ([</span><span>Real</span><span>; </span><span>NP</span><span>], [</span><span>Real</span><span>; </span><span>NP</span><span>]) {</span></div></div><div><div><div>273</div></div><div><span>    </span><span>weights</span><span>()</span></div></div><div><div><div>274</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个 app（<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/c/apps/ch07.c" target="_blank"><code>c/apps/ch07.c</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/cpp/apps/ch07.cpp" target="_blank"><code>cpp/apps/ch07.cpp</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/rust/src/bin/ch07.rs" target="_blank"><code>rust/src/bin/ch07.rs</code></a>）先做 sigma 点自检：对 <span><span>P=diag⁡(1,2,3,4,5,6)P = \operatorname{diag}(1,2,3,4,5,6)</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>5</span><span>,</span><span></span><span>6</span><span>)</span></span></span></span>，<span><span>∑iWimχi=0\sum_iW^m_i\chi_i = 0</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>、<span><span>∑iWicχiχiT=P\sum_iW^c_i\chi_i\chi_i^T = P</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>W</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>i</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span></span></span></span> 都要在 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 内成立（float 构建是 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>）。然后处理第 6 篇的两个初始条件，写 <code>out/ch07/ukf_{nominal,large}_&lt;lang&gt;.csv</code>。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch07</span></div></div><div><div><div>2</div></div><div><span>sigma point check: worst error 8.882e-16</span></div></div><div><div><div>3</div></div><div><span>ch07 self-check passed (worst 8.88e-16)</span></div></div><div><div><div>4</div></div><div><span>ok   ukf_large_c.csv: max diff 1.037e-13 &lt;= 1.0e-09</span></div></div><div><div><div>5</div></div><div><span>ok   ukf_large_cpp.csv: max diff 7.694e-14 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   ukf_large_rs.csv: max diff 7.622e-14 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   ukf_nominal_c.csv: max diff 8.227e-14 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>ok   ukf_nominal_cpp.csv: max diff 6.395e-14 &lt;= 1.0e-09</span></div></div><div><div><div>9</div></div><div><span>ok   ukf_nominal_rs.csv: max diff 6.761e-14 &lt;= 1.0e-09</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Python 参考的 UKF 和三种语言的输出最大差异在 <span><span>10−1310^{-13}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>13</span></span></span></span></span></span></span></span></span></span></span></span> 量级（比第 6 篇 ESKF 的 <span><span>10−1510^{-15}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 大了两个数量级）：13 个点的加权求和和 Cholesky 里累积的舍入多了。</p><section><h3>玩具问题<a href="#玩具问题"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Polar to Cartesian: r ~ N(1, 0.02^2), theta ~ N(pi/2, 0.5^2)</span></div></div><div><div><div>2</div></div><div><span><span>                    </span></span><span>mean x    mean y      var x      var y     cov xy</span></div></div><div><div><div>3</div></div><div><span>Monte Carlo 1e6    -0.0006    0.8827    0.19641    0.02486    0.00017</span></div></div><div><div><div>4</div></div><div><span>linearization       0.0000    1.0000    0.25000    0.00040   -0.00000</span></div></div><div><div><div>5</div></div><div><span>unscented           0.0000    0.8801    0.21101    0.04351   -0.00000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/kalman-filter/ch07-ut-toy.svg" alt="极坐标到直角坐标：蒙特卡洛样本（灰点）、真实一倍标准差椭圆（黑）、线性化（红）、无迹变换（蓝）" /><figcaption>极坐标到直角坐标：蒙特卡洛样本（灰点）、真实一倍标准差椭圆（黑）、线性化（红）、无迹变换（蓝）</figcaption></figure><p></p><p>均值：蒙特卡洛 <span><span>(−0.0006, 0.8827)(−0.0006,\ 0.8827)</span><span><span><span></span><span>(</span><span>−</span><span>0.0006</span><span>,</span><span> </span><span></span><span>0.8827</span><span>)</span></span></span></span>，线性化 <span><span>(0, 1.0000)(0,\ 1.0000)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span> </span><span></span><span>1.0000</span><span>)</span></span></span></span>，UT <span><span>(0, 0.8801)(0,\ 0.8801)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span> </span><span></span><span>0.8801</span><span>)</span></span></span></span>。UT 抓到了二阶项，这是上面推导的实证。方差：<span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 方向真实是 0.02486，线性化给了 0.00040，小了 62 倍；UT 给了 0.04351，偏大 1.75 倍。所以 UT 不是”更准”，它是”没有系统性漏掉一个量级”：它的方差偏大，在滤波里偏保守，这个方向相对安全。图里蓝色椭圆比黑色大，红色椭圆扁成了一条线。</p></section><section><h3>四个滤波器同台<a href="#四个滤波器同台"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Four filters, seeds 1..50, 120 s each (deg RMS pooled over seeds and time)</span></div></div><div><div><div>2</div></div><div><span>method       yaw   pitch    roll   total  calm tot  mean NEES  calm NEES  in bounds</span></div></div><div><div><div>3</div></div><div><span>mahony     4.391   1.488   2.243   4.880     0.733          -          -          -</span></div></div><div><div><div>4</div></div><div><span>ekf        4.504   0.685   1.120   4.721     0.074    9557.93      6.048      96.5%</span></div></div><div><div><div>5</div></div><div><span>eskf       0.599   0.592   0.866   0.929     0.075     895.58      6.048      96.0%</span></div></div><div><div><div>6</div></div><div><span>ukf        0.599   0.592   0.866   0.929     0.075     895.47      6.048      96.0%</span></div></div><div><div><div>7</div></div><div><span>(NEES 6 dof, mean of 50 runs; 95% bounds [5.078, 6.997]; 'in bounds' = fraction of calm [20,60) steps)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读这张表的几个地方：</p><ul>
<li><strong>UKF 与 ESKF 在三位小数内没有区别</strong>：总误差 0.929 对 0.929，平静段 0.075 对 0.075，NEES 895.47 对 895.58，落界比例都是 96.0%。这个问题里，误差是度级甚至零点几度的小量，线性化几乎是精确的，UT 没有可以补的东西。它花了 13 倍的传播和 26 次 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 求值（下面的清单），换来的是和 ESKF 一样的结果。</li>
<li><strong>EKF 的整段 RMSE 和平均 NEES 是 4.721 度和 9558，被磁干扰拖累</strong>；平静段（<span><span>[20,60)[20,60)</span><span><span><span></span><span>[</span><span>20</span><span>,</span><span></span><span>60</span><span>)</span></span></span></span>）的总误差是 0.074 度，和 ESKF、UKF 的 0.075 没有区别，NEES 都是 6.048。这三者在没有干扰时，等价于同一个滤波器的三种写法。</li>
<li><strong>Mahony 的平静段误差 0.733 度，是卡尔曼三兄弟的 10 倍</strong>：卡尔曼滤波器的好处主要体现在这里，增益自己算。</li>
<li><strong>没有哪个滤波器的整段数字好看</strong>，因为线加速度窗口（60 到 70 秒）对所有人都是未建模的扰动。</li>
</ul><p>模型评估次数（每个滤波步）：</p><ul>
<li>Mahony：2 个预测方向，没有协方差。</li>
<li>EKF：1 次传播，2 次 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 求值加 2 个解析的 <span><span>3×43\times4</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 雅可比，<span><span>7×77\times7</span><span><span><span></span><span>7</span><span></span><span>×</span><span></span></span><span><span></span><span>7</span></span></span></span> 协方差。</li>
<li>ESKF：1 次传播，2 次 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 求值加 2 个解析雅可比，<span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 协方差。</li>
<li>UKF：13 次传播（<span><span>2n+12n+1</span><span><span><span></span><span>2</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个 sigma 点），<span><span>13+13=2613+13 = 26</span><span><span><span></span><span>13</span><span></span><span>+</span><span></span></span><span><span></span><span>13</span><span></span><span>=</span><span></span></span><span><span></span><span>26</span></span></span></span> 次 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 求值，没有雅可比，<span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 协方差。</li>
</ul><p>每步的耗时放到第 8 篇一起测。</p><p></p><figure><img src="/assets/images/kalman-filter/ch07-four-way-error.svg" alt="50 个种子的总姿态误差均方根随时间的变化。卡尔曼三兄弟在平静段几乎重合；磁干扰段 EKF 被拉开" /><figcaption>50 个种子的总姿态误差均方根随时间的变化。卡尔曼三兄弟在平静段几乎重合；磁干扰段 EKF 被拉开</figcaption></figure><p></p></section><section><h3>大初始误差：UKF 赢了<a href="#大初始误差ukf-赢了"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Large initial error, seed 1: time until the total error stays below 2 deg for 5 s (error at 10 s)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>90 deg yaw                   | mahony  43.28 s (3.96) | ekf   1.18 s (0.05) | eskf   0.02 s (0.04) | ukf   0.01 s (0.04)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>120 deg about world [1,1,0]  | mahony  15.21 s (7.00) | ekf   0.01 s (0.04) | eskf   0.86 s (0.18) | ukf   0.04 s (0.04)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>括号里是 10 秒时的误差（度）。第 6 篇我用的两个大误差初始条件：</p><ul>
<li><strong>90 度偏航误差</strong>：UKF 0.01 秒，ESKF 0.02 秒，EKF 1.18 秒，Mahony 43.28 秒。</li>
<li><strong>120 度、绕世界 <span><span>[1,1,0][1,1,0]</span><span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span></strong>：UKF 0.04 秒，EKF 0.01 秒，ESKF 0.86 秒，Mahony 15.21 秒。</li>
</ul><p>只有 UKF 在两种情形里都在几十毫秒内收敛：EKF 和 ESKF 各有一个情形慢了一个数量级。它不是赢了精度，是赢了鲁棒性。我猜道理是 sigma 点把大范围的不确定度（<span><span>6⋅π/2\sqrt6\cdot\pi/2</span><span><span><span></span><span><span><span><span><span><span></span><span>6</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>π</span><span>/2</span></span></span></span> 弧度的散布）真的当成了非线性函数的输入，而 EKF 和 ESKF 只在一个点上线性化；但我没有做更多的实验来证实这个解释，所以这只是一个猜测。Mahony 在两种情形里都需要十几到几十秒，我猜是因为它的增益 <span><span>kp=1.0k_p = 1.0</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.0</span></span></span></span> 是固定的，对大误差来说是个慢滤波。</p><p></p><figure><img src="/assets/images/kalman-filter/ch07-large-init.svg" alt="两种大初始误差下四个滤波器的总姿态误差（对数坐标）。虚线是 2 度。" /><figcaption>两种大初始误差下四个滤波器的总姿态误差（对数坐标）。虚线是 2 度。</figcaption></figure><p></p></section><section><h3>粒子滤波，只给个路标<a href="#粒子滤波只给个路标"><span>#</span></a></h3><p>UKF 用 13 个点表示一个高斯。如果后验分布明显不是高斯（比如多峰），点数再多也没用，需要粒子滤波：用成百上千个带权样本直接表示任意分布，靠重采样避免权重退化。它的代价是每步要传播几百到几千个粒子，不适合我们这个 100 Hz 的姿态估计。我没有实现它。要读的话，Arulampalam 等人的 “A tutorial on particle filters for online nonlinear/non-Gaussian Bayesian tracking”（IEEE TSP 2002）是一份很清晰的入门，Doucet、de Freitas、Gordon 编的 <em>Sequential Monte Carlo Methods in Practice</em>（2001）是更完整的参考。UKF 的原始论文是 Julier 和 Uhlmann 的 “A new extension of the Kalman filter to nonlinear systems”（1997），缩放参数的版本来自 Wan 和 van der Merwe 的 “The unscented Kalman filter for nonlinear estimation”（2000）。</p></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>三个卡尔曼滤波器在这个问题上，精度几乎没有区别，真正的区别是：每步花多少算力，数字格式（float32）能不能撑住，内存是不是固定。下一篇是整个系列的最后一篇，我们把这些全部量一量：在 float32 下把 KF、ESKF、UKF 跑一遍，看和 double 参考差多少；实现 UD 分解，看协方差的数值健康；打印每个滤波器结构体的字节数；检验库里没有堆分配；把 C 的滤波器编译到 Cortex-M4 上看代码大小；测每步的耗时，包括 Python 慢多少。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-06-eskf/</id>
      <title type="text">误差状态卡尔曼滤波：四元数的正确打开方式</title>
      <published>2026-10-14T05:00:00.000Z</published>
      <updated>2026-10-14T05:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-06-eskf/"/>
      <summary type="text">卡尔曼滤波系列第 6 篇：推导误差状态卡尔曼滤波（ESKF），把姿态误差放在 3 维切空间里，协方差满秩；仅用航向更新磁力计；诚实地对比 EKF，包括一个我没想到的结论：大部分收益来自一行门控。Python、C、C++、Rust 实现。</summary>
      <content type="html"><![CDATA[<p>先给一个会让人高兴的数：同一份 120 秒的数据，上一篇的 EKF 总姿态误差 RMSE 是 4.719 度，这一篇的误差状态卡尔曼滤波（ESKF）是 0.932 度，磁干扰窗口里的横滚俯仰误差从 3.214 度降到 0.070 度。再给一个让这个数不那么高兴的数：把上一篇的 EKF 加上和 ESKF 一样的一行磁力计模长门控，它自己是 0.804 度，横滚俯仰是 0.068 度。也就是说，这 4.7 降到 0.9 的绝大部分功劳属于那一行 <code>if</code>，不属于误差状态。</p>
<p>我想把这件事说清楚，因为误差状态滤波器确实有它的价值，只是不在这张表上。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch06</code> 通过，<code>out/ch06/eskf_{nominal,large}_{py,c,cpp,rs}.csv</code>，四张图；C、C++、Rust 各一个 ESKF（<code>kl_eskf</code>、<code>eskf.hpp</code>、<code>eskf.rs</code>）。两个测量雅可比的有限差分检查在每个语言的 app 里先于数据处理运行。蒙特卡洛统计（种子 1 到 50）只在 Python 里算，同第 3 篇的理由。
前置：<a href="/posts/kalman-filter-05-ekf/">第 5 篇</a>。理论部分参考 Joan Solà，“Quaternion kinematics for the error-state Kalman filter”（arXiv&lt;1711&gt;.02508，2017），符号基本沿用它。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>名义状态与误差状态<a href="#名义状态与误差状态"><span>#</span></a></h3><p>EKF 把四元数的 4 个分量直接当成状态，带来了一个多余的自由度（第 5 篇最后看到的 <span><span>PqqP_{qq}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>q</span><span>q</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的第四个特征值）。ESKF 换了个做法：</p><ul>
<li><strong>名义状态</strong> <span><span>(q,b)(q, b)</span><span><span><span></span><span>(</span><span>q</span><span>,</span><span></span><span>b</span><span>)</span></span></span></span>：我们认为的姿态和零偏，非线性地传播，不带协方差。</li>
<li><strong>误差状态</strong> <span><span>δx=[δθ, δb]∈R6\delta x = [\delta\theta,\ \delta b]\in\mathbb R^6</span><span><span><span></span><span>δ</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>δ</span><span>θ</span><span>,</span><span> </span><span></span><span>δ</span><span>b</span><span>]</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span>：真值和名义状态的差。姿态误差定义成第 4 篇的右乘形式：<span><span>qtrue=q⊗exp⁡(δθ)q_{\mathrm{true}} = q\otimes\exp(\delta\theta)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>θ</span><span>)</span></span></span></span>，零偏误差 <span><span>btrue=b+δbb_{\mathrm{true}} = b+\delta b</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>δ</span><span>b</span></span></span></span>。滤波器的协方差是这个 6 维向量的协方差，<span><span>P∈R6×6P\in\mathbb R^{6\times6}</span><span><span><span></span><span>P</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>6</span><span>×</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span>。</li>
</ul><p>好处有三：</p><ol>
<li><span><span>δθ\delta\theta</span><span><span><span></span><span>δ</span><span>θ</span></span></span></span> 是 3 维旋转向量，没有约束，在原点附近是平坦的，协方差 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 满秩，没有多余自由度。</li>
<li>误差永远很小（滤波器的目标就是让它小），所以线性化在原点附近一阶展开是好的近似；大的运动由名义状态的非线性传播承担。</li>
<li>每次更新后，误差被注入名义状态，然后重置为 0：滤波器总是在原点附近线性化。</li>
</ol></section><section><h3>误差的动力学<a href="#误差的动力学"><span>#</span></a></h3><p>设真实角速度 <span><span>ω=ωm−btrue−ng\omega = \omega_m-b_{\mathrm{true}}-n_g</span><span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，名义角速度 <span><span>ω^=ωm−b\hat\omega = \omega_m-b</span><span><span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span></span></span></span>。要求 <span><span>δθ\delta\theta</span><span><span><span></span><span>δ</span><span>θ</span></span></span></span> 怎么演化。令 <span><span>δq=q∗⊗qtrue=exp⁡(δθ)\delta q = q^*\otimes q_{\mathrm{true}} = \exp(\delta\theta)</span><span><span><span></span><span>δ</span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>θ</span><span>)</span></span></span></span>，对时间求导，用 <span><span>q˙=12q⊗ω\dot q = \tfrac12q\otimes\omega</span><span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>ω</span></span></span></span>（<span><span>ω\omega</span><span><span><span></span><span>ω</span></span></span></span> 在这里指 <span><span>[0,ω][0,\omega]</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>ω</span><span>]</span></span></span></span>）：</p><span><span><span>δq˙=q˙∗⊗qtrue+q∗⊗q˙true=12(q⊗ω^)∗⊗qtrue+12q∗⊗qtrue⊗ω=12(δq⊗ω−ω^⊗δq)\begin{aligned}
\dot{\delta q} &amp;= \dot q^*\otimes q_{\mathrm{true}}+q^*\otimes\dot q_{\mathrm{true}}\\
&amp;= \tfrac12(q\otimes\hat\omega)^*\otimes q_{\mathrm{true}}+\tfrac12q^*\otimes q_{\mathrm{true}}\otimes\omega\\
&amp;= \tfrac12\big(\delta q\otimes\omega-\hat\omega\otimes\delta q\big)
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span>δ</span><span>q</span></span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span><span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>q</span><span></span><span>⊗</span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span><span>ω</span></span></span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>(</span></span><span>δ</span><span>q</span><span></span><span>⊗</span><span></span><span>ω</span><span></span><span>−</span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>⊗</span><span></span><span>δ</span><span>q</span><span><span>)</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>（用了 <span><span>(q⊗ω^)∗=ω^∗⊗q∗=−ω^⊗q∗(q\otimes\hat\omega)^* = \hat\omega^*\otimes q^* = -\hat\omega\otimes q^*</span><span><span><span></span><span>(</span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>，纯虚四元数的共轭是它的负。）把 <span><span>δq≈[1,12δθ]\delta q\approx[1,\tfrac12\delta\theta]</span><span><span><span></span><span>δ</span><span>q</span><span></span><span>≈</span><span></span></span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>δ</span><span>θ</span><span>]</span></span></span></span> 代入，向量部分：</p><span><span><span>12δθ˙=12(ω−ω^+12δθ×ω−12ω^×δθ)≈12(ω−ω^)+12δθ×ω^\begin{aligned}
\tfrac12\dot{\delta\theta} &amp;= \tfrac12\big(\omega-\hat\omega+\tfrac12\delta\theta\times\omega-\tfrac12\hat\omega\times\delta\theta\big)\\
&amp;\approx\tfrac12(\omega-\hat\omega)+\tfrac12\delta\theta\times\hat\omega
\end{aligned}</span><span><span><span></span><span><span><span><span><span><span><span><span></span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span><span><span><span></span><span><span>δ</span><span>θ</span></span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span></span><span></span><span>=</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>(</span></span><span>ω</span><span></span><span>−</span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>δ</span><span>θ</span><span></span><span>×</span><span></span><span>ω</span><span></span><span>−</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>×</span><span></span><span>δ</span><span>θ</span><span><span>)</span></span></span></span><span><span></span><span><span></span><span></span><span>≈</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>ω</span><span></span><span>−</span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>δ</span><span>θ</span><span></span><span>×</span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>（保留一阶小量，<span><span>ω≈ω^\omega\approx\hat\omega</span><span><span><span></span><span>ω</span><span></span><span>≈</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 用在二阶项里。）所以</p><span><span><span>δθ˙=−[ω^]×δθ−δb−ng,δb˙=nbw\dot{\delta\theta} = -[\hat\omega]_\times\delta\theta - \delta b - n_g,\qquad \dot{\delta b} = n_{bw}</span><span><span><span></span><span><span><span><span><span><span></span><span><span>δ</span><span>θ</span></span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>[</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>δ</span><span>θ</span><span></span><span>−</span><span></span></span><span><span></span><span>δ</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span><span>δ</span><span>b</span></span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>这里 <span><span>ω−ω^=−(btrue−b)−ng=−δb−ng\omega-\hat\omega = -(b_{\mathrm{true}}-b)-n_g = -\delta b-n_g</span><span><span><span></span><span>ω</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>(</span><span><span>b</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>δ</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。这是 Solà 的结果。</p><p><strong>离散化。</strong> 角速度在一步里取常数 <span><span>ω^\hat\omega</span><span><span><span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>，<span><span>−[ω^]×-[\hat\omega]_\times</span><span><span><span></span><span>−</span><span>[</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的矩阵指数是旋转 <span><span>exp⁡(−ω^Δt)\exp(-\hat\omega\Delta t)</span><span><span><span></span><span>exp</span><span>(</span><span>−</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>Δ</span><span>t</span><span>)</span></span></span></span> 对应的矩阵，也就是 <span><span>C(exp⁡(ω^Δt))TC\big(\exp(\hat\omega\Delta t)\big)^T</span><span><span><span></span><span>C</span><span><span>(</span></span><span></span><span>exp</span><span>(</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>Δ</span><span>t</span><span>)</span><span><span><span>)</span></span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>；对 <span><span>δb\delta b</span><span><span><span></span><span>δ</span><span>b</span></span></span></span> 的耦合到一阶是 <span><span>−IΔt-I\Delta t</span><span><span><span></span><span>−</span><span>I</span><span>Δ</span><span>t</span></span></span></span>：</p><span><span><span>Fδ=[C(exp⁡(ω^Δt))T−IΔt0I],Qδ=[σg2Δt2I00σbw2ΔtI]F_\delta = \begin{bmatrix}C(\exp(\hat\omega\Delta t))^T&amp;-I\Delta t\\0&amp;I\end{bmatrix},\qquad Q_\delta = \begin{bmatrix}\sigma_g^2\Delta t^2I&amp;0\\0&amp;\sigma_{bw}^2\Delta tI\end{bmatrix}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>δ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>C</span><span>(</span><span>exp</span><span>(</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>Δ</span><span>t</span><span>)</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>I</span><span>Δ</span><span>t</span></span></span><span><span></span><span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>δ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>I</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span>I</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>名义状态的预测还是 <span><span>q←q⊗exp⁡(ω^Δt)q\leftarrow q\otimes\exp(\hat\omega\Delta t)</span><span><span><span></span><span>q</span><span></span><span>←</span><span></span></span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>Δ</span><span>t</span><span>)</span></span></span></span>，<span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 不变。因为每一步更新之后误差都被重置为 0，预测步里误差状态均值始终是 0，只有协方差在动。</p></section><section><h3>测量模型<a href="#测量模型"><span>#</span></a></h3><p><strong>加速度计。</strong> 重力方向的真实机体系读数是 <span><span>C(qtrue)TvC(q_{\mathrm{true}})^Tv</span><span><span><span></span><span>C</span><span>(</span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>v</span></span></span></span>，<span><span>v=[0,0,−1]Tv = [0,0,-1]^T</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。<span><span>C(q⊗exp⁡(δθ))T=exp⁡(δθ)TC(q\otimes\exp(\delta\theta))^T = \exp(\delta\theta)^T</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>θ</span><span>)</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>θ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 对应的矩阵作用在 <span><span>C(q)TC(q)^T</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 上，一阶是 <span><span>(I−[δθ]×)(I-[\delta\theta]_\times)</span><span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>[</span><span>δ</span><span>θ</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，所以</p><span><span><span>za≈(I−[δθ]×) h=h+[h]×δθ,h=C(q)Tvz_a\approx(I-[\delta\theta]_\times)\,h = h+[h]_\times\delta\theta,\qquad h = C(q)^Tv</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>[</span><span>δ</span><span>θ</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span></span><span>+</span><span></span></span><span><span></span><span>[</span><span>h</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>δ</span><span>θ</span><span>,</span><span></span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>v</span></span></span></span></span><p>（最后一步是 <span><span>−[δθ]×h=−δθ×h=h×δθ=[h]×δθ-[\delta\theta]_\times h = -\delta\theta\times h = h\times\delta\theta = [h]_\times\delta\theta</span><span><span><span></span><span>−</span><span>[</span><span>δ</span><span>θ</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>δ</span><span>θ</span><span></span><span>×</span><span></span></span><span><span></span><span>h</span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span></span><span>×</span><span></span></span><span><span></span><span>δ</span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>h</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>δ</span><span>θ</span></span></span></span>。）于是 <span><span>Hδθ=[h]×H_{\delta\theta} = [h]_\times</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>δ</span><span>θ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>h</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>Hδb=0H_{\delta b} = 0</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>δ</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，新息 <span><span>y=za−hy = z_a-h</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>h</span></span></span></span>，<span><span>Ra=(σa/g)2I3R_a = (\sigma_a/g)^2I_3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>g</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，和 EKF 一样，门控也一样（<span><span>∣∣f∣−g∣&gt;0.5\big||f|-g\big|&gt;0.5</span><span><span><span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span>f</span><span>∣</span><span></span><span>−</span><span></span></span><span><span></span><span>g</span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0.5</span></span></span></span>）。</p><p><strong>磁力计：只更新航向。</strong> 第 5 篇的 EKF 把磁力计的整个 3 维向量塞进更新，磁干扰会污染横滚俯仰。这一篇换一个模型：把磁力计测量按名义姿态旋转到世界系，只比较水平面内的航向角。设 <span><span>m′=C(q) zmm' = C(q)\,z_m</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，测得的航向 <span><span>ψz=atan2⁡(my′,mx′)\psi_z = \operatorname{atan2}(m'_y,m'_x)</span><span><span><span></span><span><span>ψ</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>atan2</span></span><span>(</span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，参考场的航向 <span><span>ψh=atan2⁡(mw,y,mw,x)=0\psi_h = \operatorname{atan2}(m_{w,y},m_{w,x}) = 0</span><span><span><span></span><span><span>ψ</span><span><span><span><span><span><span></span><span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>atan2</span></span><span>(</span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>w</span><span>,</span><span>y</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>w</span><span>,</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，新息 <span><span>y=wrap⁡(ψz−ψh)y = \operatorname{wrap}(\psi_z-\psi_h)</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span><span>wrap</span></span><span>(</span><span><span>ψ</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>ψ</span><span><span><span><span><span><span></span><span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，折到一个周期内。</p><p>求 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span>。真实姿态是 <span><span>q⊗exp⁡(δθ)q\otimes\exp(\delta\theta)</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>θ</span><span>)</span></span></span></span>，磁力计读数 <span><span>zm=CtrueTmwz_m = C_{\mathrm{true}}^Tm_w</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>m′=C(q)CtrueTmwm' = C(q)C_{\mathrm{true}}^Tm_w</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span><span><span>C</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。令世界系下的误差旋转 <span><span>δϕ=C(q)δθ\delta\phi = C(q)\delta\theta</span><span><span><span></span><span>δ</span><span>ϕ</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span><span>δ</span><span>θ</span></span></span></span>（局部误差转到世界系），<span><span>Ctrue=(I+[δϕ]×)C(q)C_{\mathrm{true}} = (I+[\delta\phi]_\times)C(q)</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>[</span><span>δ</span><span>ϕ</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>C</span><span>(</span><span>q</span><span>)</span></span></span></span>，所以 <span><span>m′≈(I−[δϕ]×)mw=mw−δϕ×mwm'\approx(I-[\delta\phi]_\times)m_w = m_w-\delta\phi\times m_w</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>[</span><span>δ</span><span>ϕ</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>δ</span><span>ϕ</span><span></span><span>×</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。对航向角求导：<span><span>ψ=atan2⁡(my′,mx′)\psi = \operatorname{atan2}(m'_y,m'_x)</span><span><span><span></span><span>ψ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>atan2</span></span><span>(</span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>∂ψ/∂m′=[−my′, mx′]/(mx′2+my′2)\partial\psi/\partial m' = [-m'_y,\ m'_x]/(m'^2_x+m'^2_y)</span><span><span><span></span><span>∂</span><span>ψ</span><span>/</span><span>∂</span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>−</span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span><span>′</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span>/</span><span>(</span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span><span>′2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span><span><span></span><span><span><span>′2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，在标称点 <span><span>m′=mw=[mx,my,mz]m' = m_w = [m_x,m_y,m_z]</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>，<span><span>∂m′/∂δϕ=[mw]×\partial m'/\partial\delta\phi = [m_w]_\times</span><span><span><span></span><span>∂</span><span><span>m</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>/</span><span>∂</span><span>δ</span><span>ϕ</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（因为 <span><span>−δϕ×mw=mw×δϕ-\delta\phi\times m_w = m_w\times\delta\phi</span><span><span><span></span><span>−</span><span>δ</span><span>ϕ</span><span></span><span>×</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>δ</span><span>ϕ</span></span></span></span>），链式法则得到</p><span><span><span>∂ψ∂δϕ=[ mxmz,  mymz,  −(mx2+my2) ]mx2+my2,Hδθ=∂ψ∂δϕ C(q),Hδb=0\frac{\partial\psi}{\partial\delta\phi} = \frac{[\,m_xm_z,\ \ m_ym_z,\ \ -(m_x^2+m_y^2)\,]}{m_x^2+m_y^2},\qquad H_{\delta\theta} = \frac{\partial\psi}{\partial\delta\phi}\,C(q),\quad H_{\delta b} = 0</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∂</span><span>δ</span><span>ϕ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>∂</span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>[</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span> </span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span> </span><span></span><span>−</span><span>(</span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>]</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>δ</span><span>θ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∂</span><span>δ</span><span>ϕ</span></span></span><span><span></span><span></span></span><span><span></span><span><span>∂</span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>C</span><span>(</span><span>q</span><span>)</span><span>,</span><span></span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>δ</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>对我们的磁场 <span><span>mw=[cos⁡50∘,0,sin⁡50∘]m_w = [\cos50^\circ,0,\sin50^\circ]</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>cos</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>sin</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>]</span></span></span></span>，<span><span>∂ψ/∂δϕ=[tan⁡50∘, 0, −1]\partial\psi/\partial\delta\phi = [\tan50^\circ,\ 0,\ -1]</span><span><span><span></span><span>∂</span><span>ψ</span><span>/</span><span>∂</span><span>δ</span><span>ϕ</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>tan</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span>0</span><span>,</span><span> </span><span></span><span>−</span><span>1</span><span>]</span></span></span></span>。</p><p>注意第一项：只有”世界系 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 分量 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span>“不对。我一开始预想的就是只有这一项，有限差分检查给了它一个明确的否定：只取世界 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 分量，和有限差分的最大误差是 1.19，恰好是 <span><span>tan⁡50∘=1.1918\tan50^\circ = 1.1918</span><span><span><span></span><span>tan</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.1918</span></span></span></span>。物理原因是磁场有 50 度的倾角：横滚误差（绕世界 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 的误差旋转 <span><span>δϕx\delta\phi_x</span><span><span><span></span><span>δ</span><span><span>ϕ</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>）会让磁场向量的水平投影转动，在航向读数里造成 <span><span>tan⁡50∘δϕx\tan50^\circ\delta\phi_x</span><span><span><span></span><span>tan</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>δ</span><span><span>ϕ</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的耦合。把这一项放进 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 之后，两个雅可比的有限差分检查通过：<span><span>max⁡∣Hacc−Hfd∣=3.04×10−10\max|H_{acc}-H_{fd}| = 3.04\times10^{-10}</span><span><span><span></span><span>max</span><span></span><span>∣</span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>a</span><span>cc</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>f</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>3.04</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>max⁡∣Hhdg−Hfd∣=4.73×10−10\max|H_{hdg}-H_{fd}| = 4.73\times10^{-10}</span><span><span><span></span><span>max</span><span></span><span>∣</span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>h</span><span>d</span><span>g</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>f</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>4.73</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，都在 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span> 的门槛之内。有限差分的做法是”通过注入来扰动”：对 100 个确定性的名义姿态，给姿态右乘 <span><span>exp⁡(±ϵej)\exp(\pm\epsilon e_j)</span><span><span><span></span><span>exp</span><span>(</span><span>±</span><span>ϵ</span><span><span>e</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 当作”真实姿态”，按真实姿态生成无噪声测量，对新息做中心差分。</p><p>这个耦合项在滤波器里的实际作用是：横滚、俯仰的协方差已经被加速度计压得很小，所以卡尔曼增益里这部分权重很小，航向修正主要落在偏航上，这才是”只更新航向”想要的效果。</p><p><strong>航向噪声。</strong> 磁力计水平分量噪声约 <span><span>σm\sigma_m</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，水平分量的长度是 <span><span>∥mh∥=mx2+my2=cos⁡50∘\|m_h\| = \sqrt{m_x^2+m_y^2} = \cos50^\circ</span><span><span><span></span><span>∥</span><span><span>m</span><span><span><span><span><span><span></span><span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∥</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>m</span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>y</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>cos</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>，所以航向噪声 <span><span>Rψ=(σm/∥mh∥)2R_\psi = (\sigma_m/\|m_h\|)^2</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/∥</span><span><span>m</span><span><span><span><span><span><span></span><span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∥</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。</p><p><strong>磁力计门控。</strong> 跳过 <span><span>∣∣mm∣−1∣&gt;0.1\big||m_m|-1\big|&gt;0.1</span><span><span><span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span><span>m</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0.1</span></span></span></span> 的样本。我们的磁干扰是加上 <span><span>[0.3,0.3,0][0.3,0.3,0]</span><span><span><span></span><span>[</span><span>0.3</span><span>,</span><span></span><span>0.3</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span>，磁场模长变成 <span><span>∣(0.943,0.3,0.766)∣=1.25|(0.943,0.3,0.766)| = 1.25</span><span><span><span></span><span>∣</span><span>(</span><span>0.943</span><span>,</span><span></span><span>0.3</span><span>,</span><span></span><span>0.766</span><span>)</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>1.25</span></span></span></span>，被检测出来是必然的：这个检测器对这种干扰没有盲区。</p></section><section><h3>注入与重置<a href="#注入与重置"><span>#</span></a></h3><p>更新给出 <span><span>δx=[δθ,δb]\delta x = [\delta\theta,\delta b]</span><span><span><span></span><span>δ</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>δ</span><span>θ</span><span>,</span><span></span><span>δ</span><span>b</span><span>]</span></span></span></span>，注入名义状态：</p><span><span><span>q←q⊗exp⁡(δθ),b←b+δbq\leftarrow q\otimes\exp(\delta\theta),\qquad b\leftarrow b+\delta b</span><span><span><span></span><span>q</span><span></span><span>←</span><span></span></span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>θ</span><span>)</span><span>,</span><span></span><span></span><span>b</span><span></span><span>←</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>δ</span><span>b</span></span></span></span></span><p>注入之后误差状态应该清零。但是协方差是在旧的名义姿态的切空间里定义的，名义姿态变了，切空间也跟着转了一个 <span><span>δθ\delta\theta</span><span><span><span></span><span>δ</span><span>θ</span></span></span></span>。Solà 给出重置雅可比 <span><span>G=blkdiag⁡(I−[12δθ]×, I)G = \operatorname{blkdiag}(I-[\tfrac12\delta\theta]_\times,\ I)</span><span><span><span></span><span>G</span><span></span><span>=</span><span></span></span><span><span></span><span><span>blkdiag</span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>[</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>δ</span><span>θ</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span>I</span><span>)</span></span></span></span>，<span><span>P←GPGTP\leftarrow GPG^T</span><span><span><span></span><span>P</span><span></span><span>←</span><span></span></span><span><span></span><span>GP</span><span><span>G</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。<span><span>δθ\delta\theta</span><span><span><span></span><span>δ</span><span>θ</span></span></span></span> 很小的时候 <span><span>G≈IG\approx I</span><span><span><span></span><span>G</span><span></span><span>≈</span><span></span></span><span><span></span><span>I</span></span></span></span>。我把它实现了，并且测了它的效果：见结果。</p></section><section><h3>初值<a href="#初值"><span>#</span></a></h3><p>名义初值：TRIAD 的 <span><span>q0q_0</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>b0=0b_0 = 0</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>；<span><span>P0=diag⁡(0.05I3, 10−3I3)P_0 = \operatorname{diag}(0.05I_3,\ 10^{-3}I_3)</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>0.05</span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。</p></section></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Error-state (multiplicative) Kalman filter for attitude, after Sola (2017).</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>Nominal state: q, b. Error state: [dtheta (3), db (3)] with q_true = q (x) exp(dtheta),</span></div></div><div><div><div>4</div></div><div><span>b_true = b + db. The covariance P is 6x6 and always full rank.</span></div></div><div><div><div>5</div></div><div><span>"""</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>from</span><span> .imu_sim </span><span>import</span><span> G</span></div></div><div><div><div>10</div></div><div><span>from</span><span> .quat </span><span>import</span><span> qexp, qmul, qnormalize, qrot, skew, wrap_pi</span></div></div><div><div><div>11</div></div><div><span>from</span><span> .update </span><span>import</span><span> joseph_update</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>class</span><span><span> </span><span>ESKF</span></span><span>:</span></div></div><div><div><div>15</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>sigma_g</span></span><span>,</span><span><span> </span><span>sigma_bw</span></span><span>,</span><span><span> </span><span>sigma_a</span></span><span>,</span><span><span> </span><span>sigma_m</span></span><span>,</span><span><span> </span><span>m_w</span></span><span>,</span><span><span> </span><span>q0</span></span><span>,</span><span><span> </span><span>P0</span></span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>                 </span></span><span>accel_gate</span><span>=</span><span>0.5</span><span>,</span><span><span> </span><span>mag_gate</span></span><span>=</span><span>0.1</span><span>,</span><span><span> </span><span>reset</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span>.sg, </span><span>self</span><span>.sbw, </span><span>self</span><span>.sa, </span><span>self</span><span><span>.sm </span><span>=</span><span> sigma_g, sigma_bw, sigma_a, sigma_m</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>self</span><span><span>.m_w </span><span>=</span><span> np.</span><span>asarray</span><span>(m_w, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> np.</span><span>asarray</span><span>(q0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> np.</span><span>zeros</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> np.</span><span>asarray</span><span>(P0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>22</div></div><div><span>        </span><span>self</span><span>.accel_gate, </span><span>self</span><span>.mag_gate, </span><span>self</span><span><span>.reset </span><span>=</span><span> accel_gate, mag_gate, reset</span></span></div></div><div><div><div>23</div></div><div><span>        </span><span>self</span><span><span>.last_nis </span><span>=</span><span> np.nan</span></span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span>def</span><span> </span><span>predict</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>gyro</span></span><span>,</span><span><span> </span><span>dt</span></span><span>):</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>omega </span><span>=</span><span> gyro </span><span>-</span><span> </span><span>self</span><span>.b</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>dq </span><span>=</span><span> </span><span>qexp</span><span>(omega </span><span>*</span><span> dt)</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>F </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>F[:</span><span>3</span><span>, :</span><span>3</span><span><span>] </span><span>=</span><span> </span><span>qrot</span><span>(dq).T</span></span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>F[:</span><span>3</span><span>, </span><span>3</span><span><span>:] </span><span>=</span><span> </span><span>-</span><span>dt </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>Q </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>6</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>Q[:</span><span>3</span><span>, :</span><span>3</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span><span>.sg</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> dt</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>Q[</span><span>3</span><span>:, </span><span>3</span><span><span>:] </span><span>=</span><span> </span></span><span>self</span><span><span>.sbw</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> dt </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>34</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span><span>qmul</span><span>(</span></span><span>self</span><span>.q, dq)</span></div></div><div><div><div>35</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> F </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> F.T </span><span>+</span><span> Q</span></span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>def</span><span> </span><span>_inject</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>dx</span></span><span>):</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>dth </span><span>=</span><span> dx[:</span><span>3</span><span>]</span></div></div><div><div><div>39</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span><span>qnormalize</span><span>(</span><span>qmul</span><span>(</span></span><span>self</span><span><span>.q, </span><span>qexp</span><span>(dth)))</span></span></div></div><div><div><div>40</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> </span></span><span>self</span><span><span>.b </span><span>+</span><span> dx[</span></span><span>3</span><span>:]</span></div></div><div><div><div>41</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.reset:</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>Gm </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>6</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>Gm[:</span><span>3</span><span>, :</span><span>3</span><span><span>] </span><span>-=</span><span> </span><span>skew</span><span>(</span></span><span>0.5</span><span><span> </span><span>*</span><span> dth)</span></span></div></div><div><div><div>44</div></div><div><span>            </span><span>self</span><span><span>.P </span><span>=</span><span> Gm </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> Gm.T</span></span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>    </span><span>def</span><span> </span><span>update_accel</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>accel</span></span><span>):</span></div></div><div><div><div>47</div></div><div><span>        </span><span>"""Gravity-direction update. Returns False when gated out."""</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(accel </span><span>@</span><span> accel)</span></div></div><div><div><div>49</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.accel_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> G) </span><span>&gt;</span><span> </span></span><span>self</span><span>.accel_gate:</span></div></div><div><div><div>50</div></div><div><span>            </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>h </span><span>=</span><span> </span><span>qrot</span><span>(</span><span>self</span><span><span>.q).T </span><span>@</span><span> np.</span><span>array</span><span>([</span></span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>H </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>H[:, :</span><span>3</span><span><span>] </span><span>=</span><span> </span><span>skew</span><span>(h)</span></span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> accel </span><span>/</span><span> norm </span><span>-</span><span> h</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>dx, </span><span>self</span><span><span>.P, S </span><span>=</span><span> </span><span>joseph_update</span><span>(</span></span><span>self</span><span>.P, H, (</span><span>self</span><span><span>.sa </span><span>/</span><span> G) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>), y)</span></div></div><div><div><div>56</div></div><div><span>        </span><span>self</span><span><span>.last_nis </span><span>=</span><span> </span></span><span>float</span><span><span>(y </span><span>@</span><span> np.linalg.</span><span>solve</span><span>(S, y))</span></span></div></div><div><div><div>57</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_inject</span><span>(dx)</span></span></div></div><div><div><div>58</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>    </span><span>def</span><span> </span><span>heading_jacobian</span><span>(</span><span>self</span><span>):</span></div></div><div><div><div>61</div></div><div><span>        </span><span>"""d heading / d (dtheta) = [mx mz, my mz, -(mx^2+my^2)] / (mx^2+my^2) C(q)."""</span></div></div><div><div><div>62</div></div><div><span><span>        </span></span><span>mx, my, mz </span><span>=</span><span> </span><span>self</span><span>.m_w</span></div></div><div><div><div>63</div></div><div><span><span>        </span></span><span>mh2 </span><span>=</span><span> mx </span><span>*</span><span> mx </span><span>+</span><span> my </span><span>*</span><span> my</span></div></div><div><div><div>64</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([mx </span><span>*</span><span> mz </span><span>/</span><span> mh2, my </span><span>*</span><span> mz </span><span>/</span><span> mh2, </span><span>-</span></span><span>1.0</span><span><span>]) </span><span>@</span><span> </span><span>qrot</span><span>(</span></span><span>self</span><span>.q)</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div><span>    </span><span>def</span><span> </span><span>update_mag_heading</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>mag</span></span><span>):</span></div></div><div><div><div>67</div></div><div><span>        </span><span>"""Heading-only magnetometer update. Returns False when gated out."""</span></div></div><div><div><div>68</div></div><div><span><span>        </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(mag </span><span>@</span><span> mag)</span></div></div><div><div><div>69</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.mag_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> </span></span><span>1.0</span><span><span>) </span><span>&gt;</span><span> </span></span><span>self</span><span>.mag_gate:</span></div></div><div><div><div>70</div></div><div><span>            </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>71</div></div><div><span><span>        </span></span><span>mw </span><span>=</span><span> </span><span>qrot</span><span>(</span><span>self</span><span><span>.q) </span><span>@</span><span> mag</span></span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> </span><span>wrap_pi</span><span>(np.</span><span>arctan2</span><span>(mw[</span><span>1</span><span>], mw[</span><span>0</span><span><span>]) </span><span>-</span><span> np.</span><span>arctan2</span><span>(</span></span><span>self</span><span>.m_w[</span><span>1</span><span>], </span><span>self</span><span>.m_w[</span><span>0</span><span>]))</span></div></div><div><div><div>73</div></div><div><span><span>        </span></span><span>H </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>1</span><span>, </span><span>6</span><span>))</span></div></div><div><div><div>74</div></div><div><span><span>        </span></span><span>H[</span><span>0</span><span>, :</span><span>3</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span><span>.</span><span>heading_jacobian</span><span>()</span></span></div></div><div><div><div>75</div></div><div><span><span>        </span></span><span>r </span><span>=</span><span> (</span><span>self</span><span><span>.sm </span><span>/</span><span> np.</span><span>hypot</span><span>(</span></span><span>self</span><span>.m_w[</span><span>0</span><span>], </span><span>self</span><span>.m_w[</span><span>1</span><span><span>])) </span><span>**</span><span> </span></span><span>2</span></div></div><div><div><div>76</div></div><div><span><span>        </span></span><span>dx, </span><span>self</span><span><span>.P, S </span><span>=</span><span> </span><span>joseph_update</span><span>(</span></span><span>self</span><span><span>.P, H, np.</span><span>array</span><span>([[r]]), np.</span><span>array</span><span>([y]))</span></span></div></div><div><div><div>77</div></div><div><span>        </span><span>self</span><span><span>.last_nis </span><span>=</span><span> </span></span><span>float</span><span><span>(y </span><span>*</span><span> y </span><span>/</span><span> S[</span></span><span>0</span><span>, </span><span>0</span><span>])</span></div></div><div><div><div>78</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_inject</span><span>(dx)</span></span></div></div><div><div><div>79</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>和第 5 篇的 EKF 比，几处区别：协方差是 <code>6×6</code>；<code>predict</code> 里 <code>F[:3,:3] = qrot(dq).T</code> 就是 <span><span>C(exp⁡(ω^Δt))TC(\exp(\hat\omega\Delta t))^T</span><span><span><span></span><span>C</span><span>(</span><span>exp</span><span>(</span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>Δ</span><span>t</span><span>)</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>；<code>update_accel</code> 里 <code>skew(h)</code> 是 <span><span>[h]×[h]_\times</span><span><span><span></span><span>[</span><span>h</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>；<code>update_mag_heading</code> 里 <code>y</code> 用 <code>wrap_pi</code> 折叠，<code>H</code> 只有一行；两种更新最后都调 <code>_inject</code>。<code>joseph_update</code> 是第 5 篇的共用函数，这里 <span><span>m=3m=3</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>3</span></span></span></span>（加速度计）和 <span><span>m=1m=1</span><span><span><span></span><span>m</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（航向）都能用。</p><p>有限差分检查：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>jacobian_checks</span><span>():</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Finite-difference checks of both measurement Jacobians, perturbing through injection."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>eps </span><span>=</span><span> </span><span>1e-6</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>worst_a </span><span>=</span><span> worst_m </span><span>=</span><span> </span><span>0.0</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> </span><span>make_eskf</span><span>(np.</span><span>array</span><span>([</span><span>1.0</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0</span><span><span>]), </span><span>P0_NOMINAL</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(</span><span>100</span><span>):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> np.</span><span>array</span><span>([np.</span><span>cos</span><span>(</span><span>0.3</span><span><span> </span><span>*</span><span> i </span><span>+</span><span> </span></span><span>0.1</span><span><span>), np.</span><span>sin</span><span>(</span></span><span>0.7</span><span><span> </span><span>*</span><span> i), np.</span><span>cos</span><span>(</span></span><span>1.1</span><span><span> </span><span>*</span><span> i), np.</span><span>sin</span><span>(</span></span><span>1.9</span><span><span> </span><span>*</span><span> i </span><span>+</span><span> </span></span><span>0.5</span><span>)])</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>f.q </span><span>=</span><span> q </span><span>/</span><span> np.linalg.</span><span>norm</span><span>(q)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>C </span><span>=</span><span> </span><span>qrot</span><span>(f.q)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>h </span><span>=</span><span> C.T </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>Ha, Hm </span><span>=</span><span> </span><span>skew</span><span>(h), f.</span><span>heading_jacobian</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>Fa, Fm </span><span>=</span><span> np.</span><span>empty</span><span>((</span><span>3</span><span>, </span><span>3</span><span><span>)), np.</span><span>empty</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>):</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>e </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>e[j] </span><span>=</span><span> eps</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>ys </span><span>=</span><span> []</span></div></div><div><div><div>17</div></div><div><span>            </span><span>for</span><span> s </span><span>in</span><span><span> (</span><span>+</span></span><span>1</span><span><span>, </span><span>-</span></span><span>1</span><span>):</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>qt </span><span>=</span><span> </span><span>qmul</span><span>(f.q, </span><span>qexp</span><span>(s </span><span>*</span><span> e))  </span><span># "true" attitude after injecting +-eps</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>za </span><span>=</span><span> </span><span>qrot</span><span>(qt).T </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>zm </span><span>=</span><span> </span><span>qrot</span><span>(qt).T </span><span>@</span><span> </span><span>MAG_W</span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>mw </span><span>=</span><span> C </span><span>@</span><span> zm</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>ys.</span><span>append</span><span>((za </span><span>-</span><span> h, </span><span>wrap_pi</span><span>(np.</span><span>arctan2</span><span>(mw[</span><span>1</span><span>], mw[</span><span>0</span><span><span>]) </span><span>-</span><span> np.</span><span>arctan2</span><span>(</span><span>MAG_W</span><span>[</span></span><span>1</span><span><span>], </span><span>MAG_W</span><span>[</span></span><span>0</span><span>]))))</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>Fa[:, j] </span><span>=</span><span> (ys[</span><span>0</span><span>][</span><span>0</span><span><span>] </span><span>-</span><span> ys[</span></span><span>1</span><span>][</span><span>0</span><span><span>]) </span><span>/</span><span> (</span></span><span>2</span><span><span> </span><span>*</span><span> eps)</span></span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>Fm[j] </span><span>=</span><span> (ys[</span><span>0</span><span>][</span><span>1</span><span><span>] </span><span>-</span><span> ys[</span></span><span>1</span><span>][</span><span>1</span><span><span>]) </span><span>/</span><span> (</span></span><span>2</span><span><span> </span><span>*</span><span> eps)</span></span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>worst_a </span><span>=</span><span> </span><span>max</span><span>(worst_a, </span><span>float</span><span><span>(np.</span><span>max</span><span>(np.</span><span>abs</span><span>(Ha </span><span>-</span><span> Fa))))</span></span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>worst_m </span><span>=</span><span> </span><span>max</span><span>(worst_m, </span><span>float</span><span><span>(np.</span><span>max</span><span>(np.</span><span>abs</span><span>(Hm </span><span>-</span><span> Fm))))</span></span></div></div><div><div><div>27</div></div><div><span>    </span><span>assert</span><span><span> worst_a </span><span>&lt;</span><span> </span></span><span>1e-6</span><span> </span><span>and</span><span><span> worst_m </span><span>&lt;</span><span> </span></span><span>1e-6</span><span>, (worst_a, worst_m)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>return</span><span> worst_a, worst_m</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>三种语言的结构完全对应 Python。差别和 EKF 一章类似：C 是定长数组加内联工作区（<code>work[256]</code>），C++ 是模板加 <code>Mat&lt;6,6,T&gt;</code>，Rust 是 <code>Mat&lt;6,6&gt;</code>、<code>Option</code>/<code>let-else</code>。</p><section><h3>C<a href="#c"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>kl_eskf_predict</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>F </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>Q </span><span>=</span><span> F </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>T </span><span>=</span><span> Q </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>omega</span><span>[</span><span>3</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>C</span><span>[</span><span>9</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>5</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>omega</span><span>[i] </span><span>=</span><span> (</span><span>gyro</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i]) </span><span>*</span><span> dt;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>kl_quat_exp</span><span>(omega, dq);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(dq, C);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>/* F = [[C(dq)^T, -dt I], [0, I]], Q = diag(sg^2 dt^2 I, sbw^2 dt I) */</span></div></div><div><div><div>11</div></div><div><span>  </span><span>kl_mat_identity</span><span>(F, </span><span>6</span><span>);</span></div></div><div><div><div>12</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>36</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>Q</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>16</div></div><div><span>      </span><span>F</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>C</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>17</div></div><div><span>    </span><span>F</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> </span><span>3</span><span> </span><span>+</span><span> i] </span><span>=</span><span> </span><span>-</span><span>dt;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>Q</span><span>[i </span><span>*</span><span> </span><span>7</span><span>] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> dt </span><span>*</span><span> dt;</span></div></div><div><div><div>19</div></div><div><span>    </span><span>Q</span><span>[(i </span><span>+</span><span> </span><span>3</span><span>) </span><span>*</span><span> </span><span>7</span><span>] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> dt;</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>22</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>kl_mat_mul</span><span>(F, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(T, F, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>25</div></div><div><span>  </span><span>kl_mat_add</span><span>(U, Q, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>/* Move the error estimate into the nominal state, then reset the error to zero. */</span></div></div><div><div><div>29</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>inject</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>dx</span><span>[</span><span>6</span><span><span>]</span><span>)</span></span></div></div><div><div><div>30</div></div><div><span>{</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>G </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>T </span><span>=</span><span> G </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>kl_real </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>33</div></div><div><span>  </span><span>kl_quat_exp</span><span>(dx, dq);</span></div></div><div><div><div>34</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>35</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>36</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>37</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>38</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+=</span><span> </span><span>dx</span><span>[</span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>39</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>reset</span><span>) {</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>/* G = blkdiag(I - [dtheta/2]x, I), P = G P G^T */</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>kl_real </span><span>half</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {(kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>0</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>1</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>2</span><span>]};</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>kl_real </span><span>S</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>43</div></div><div><span>    </span><span>kl_skew</span><span>(half, S);</span></div></div><div><div><div>44</div></div><div><span>    </span><span>kl_mat_identity</span><span>(G, </span><span>6</span><span>);</span></div></div><div><div><div>45</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>46</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>47</div></div><div><span>        </span><span>G</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>-=</span><span> </span><span>S</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>48</div></div><div><span>    </span><span>kl_mat_mul</span><span>(G, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>49</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, G, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>50</div></div><div><span>    </span><span>kl_mat_copy</span><span>(U, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>52</div></div><div><span>}</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>int</span><span> </span><span>kl_eskf_update_accel</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>55</div></div><div><span>{</span></div></div><div><div><div>56</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>H </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>R </span><span>=</span><span> H </span><span>+</span><span> </span><span>18</span><span>, </span><span>*</span><span>jw </span><span>=</span><span> R </span><span>+</span><span> </span><span>9</span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>58</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> (kl_real)KL_GRAVITY) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span>)</span></div></div><div><div><div>59</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>60</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>down</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>kl_real </span><span>h</span><span>[</span><span>3</span><span>], </span><span>S</span><span>[</span><span>9</span><span>], </span><span>y</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[</span><span>6</span><span>];</span></div></div><div><div><div>62</div></div><div><span>  </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, down, h);</span><span> /* C(q)^T [0,0,-1] for unit q */</span></div></div><div><div><div>63</div></div><div><span>  </span><span>kl_skew</span><span>(h, S);</span></div></div><div><div><div>64</div></div><div><span><span>  </span></span><span>/* H = [[h]x | 0] */</span></div></div><div><div><div>65</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>66</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>67</div></div><div><span>      </span><span>H</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> j </span><span>&lt;</span><span> </span><span>3</span><span> </span><span>?</span><span> </span><span>S</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY);</span></div></div><div><div><div>69</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>70</div></div><div><span>    </span><span>y</span><span>[i] </span><span>=</span><span> </span><span>accel</span><span>[i] </span><span>/</span><span> norm </span><span>-</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>71</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>72</div></div><div><span>      </span><span>R</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>?</span><span> r </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>73</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>74</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_joseph_update</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, H, R, y, </span><span>6</span><span>, </span><span>3</span><span>, dx, </span><span>NULL</span><span>, jw) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>75</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>76</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>77</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>78</div></div><div><span>}</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>void</span><span> </span><span>kl_eskf_heading_jacobian</span><span>(</span><span>const</span><span> kl_eskf </span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span></span><span>H</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>81</div></div><div><span>{</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>kl_real mx </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>], my </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], mz </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>2</span><span>], </span><span>C</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>83</div></div><div><span><span>  </span></span><span>kl_real mh2 </span><span>=</span><span> mx </span><span>*</span><span> mx </span><span>+</span><span> my </span><span>*</span><span> my;</span></div></div><div><div><div>84</div></div><div><span><span>  </span></span><span>kl_real </span><span>v</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {mx </span><span>*</span><span> mz </span><span>/</span><span> mh2, my </span><span>*</span><span> mz </span><span>/</span><span> mh2, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>85</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>86</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>87</div></div><div><span>    </span><span>H</span><span>[j] </span><span>=</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>C</span><span>[j] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>C</span><span>[</span><span>3</span><span> </span><span>+</span><span> j] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>C</span><span>[</span><span>6</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>88</div></div><div><span>}</span></div></div><div><div><div>89</div></div><div>
</div></div><div><div><div>90</div></div><div><span>int</span><span> </span><span>kl_eskf_update_mag_heading</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>91</div></div><div><span>{</span></div></div><div><div><div>92</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>H </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>R </span><span>=</span><span> H </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>jw </span><span>=</span><span> R </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>93</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>94</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span>)</span></div></div><div><div><div>95</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>96</div></div><div><span><span>  </span></span><span>kl_real </span><span>C</span><span>[</span><span>9</span><span>], </span><span>mw</span><span>[</span><span>3</span><span>], y, </span><span>dx</span><span>[</span><span>6</span><span>];</span></div></div><div><div><div>97</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>98</div></div><div><span>  </span><span>kl_mat_mul</span><span>(C, mag, mw, </span><span>3</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>99</div></div><div><span><span>  </span></span><span>y </span><span>=</span><span> </span><span>kl_wrap_pi</span><span>(</span><span>KL_ATAN2</span><span>(</span><span>mw</span><span>[</span><span>1</span><span>], </span><span>mw</span><span>[</span><span>0</span><span>]) </span><span>-</span><span> </span><span>KL_ATAN2</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>]));</span></div></div><div><div><div>100</div></div><div><span>  </span><span>kl_eskf_heading_jacobian</span><span>(f, H);</span></div></div><div><div><div>101</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>3</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>102</div></div><div><span>    </span><span>H</span><span>[j] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>103</div></div><div><span><span>  </span></span><span>kl_real mh </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>]);</span></div></div><div><div><div>104</div></div><div><span>  </span><span>R</span><span>[</span><span>0</span><span>] </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> mh) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> mh);</span></div></div><div><div><div>105</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_joseph_update</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, H, R, </span><span>&amp;</span><span>y, </span><span>6</span><span>, </span><span>1</span><span>, dx, </span><span>NULL</span><span>, jw) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>106</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>107</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>108</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>109</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>kl_eskf_update_accel</code> 里 <code>H</code> 是 <span><span>3×63\times6</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span>、<code>R</code> 是 <span><span>3×33\times3</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>3</span></span></span></span>，<code>kl_eskf_update_mag_heading</code> 里 <code>H</code> 是 <span><span>1×61\times6</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span>、<code>R</code> 是 <span><span>1×11\times1</span><span><span><span></span><span>1</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span>：都调同一个 <code>kl_joseph_update</code>，维度作为参数传。注入函数 <code>inject</code> 里 <span><span>GG</span><span><span><span></span><span>G</span></span></span></span> 和临时矩阵共用工作区 <code>f-&gt;work</code>（用完 <code>update_*</code> 里的 <span><span>HH</span><span><span><span></span><span>H</span></span></span></span>、<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 之后才用到，所以不冲突）。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dq</span><span> </span><span>=</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>((</span><span>gyro</span><span> </span><span>-</span><span> </span><span>b</span><span>) </span><span>*</span><span> </span><span>dt</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Ct</span><span> </span><span>=</span><span> </span><span>dq</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>F</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>Q</span><span>{};</span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>F</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Ct</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>8</div></div><div><span>      </span><span>F</span><span>(</span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>=</span><span> </span><span>-</span><span>dt</span><span>;</span></div></div><div><div><div>9</div></div><div><span>      </span><span>Q</span><span>(</span><span>i</span><span>, </span><span>i</span><span>) </span><span>=</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>dt</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>10</div></div><div><span>      </span><span>Q</span><span>(</span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>=</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>q</span><span> </span><span>*</span><span> </span><span>dq</span><span>;</span><span>  // not normalized here</span></div></div><div><div><div>13</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>// Gravity-direction update. Returns false when gated out or when S is not</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>// positive definite (state unchanged in both cases).</span></div></div><div><div><div>18</div></div><div><span>  </span><span>bool</span><span> </span><span>update_accel</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>) {</span></span></div></div><div><div><div>19</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>20</div></div><div><span>    </span><span>if</span><span> (</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>&gt;</span><span> </span><span>accel_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>21</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>h</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>() </span><span>*</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}};</span></div></div><div><div><div>22</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>H</span><span>{};</span></div></div><div><div><div>23</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Sh</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>25</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>H</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Sh</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>26</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>*</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>));</span></div></div><div><div><div>27</div></div><div><span>    </span><span>return</span><span> </span><span>correct</span><span>(</span><span>H</span><span>, Mat&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>*</span><span> </span><span>r</span><span>, </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> </span><span>norm</span><span>) </span><span>-</span><span> </span><span>h</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>// d heading / d dtheta = [mx mz, my mz, -(mx^2+my^2)] / (mx^2+my^2) C(q).</span></div></div><div><div><div>31</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>heading_jacobian</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>mx</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>my</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mz</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>2</span><span>, </span><span>0</span><span>), </span><span>mh2</span><span> </span><span>=</span><span> </span><span>mx</span><span> </span><span>*</span><span> </span><span>mx</span><span> </span><span>+</span><span> </span><span>my</span><span> </span><span>*</span><span> </span><span>my</span><span>;</span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>mx</span><span> </span><span>*</span><span> </span><span>mz</span><span> </span><span>/</span><span> </span><span>mh2</span><span>, </span><span>my</span><span> </span><span>*</span><span> </span><span>mz</span><span> </span><span>/</span><span> </span><span>mh2</span><span>, </span><span>T</span><span>(</span><span>-</span><span>1</span><span>)}} </span><span>*</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>();</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>// Heading-only magnetometer update. Returns false when gated out or when S is</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>// not positive definite (state unchanged in both cases).</span></div></div><div><div><div>38</div></div><div><span>  </span><span>bool</span><span> </span><span>update_mag_heading</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>) {</span></span></div></div><div><div><div>39</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>40</div></div><div><span>    </span><span>if</span><span> (</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>mag_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>41</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>mw</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>() </span><span>*</span><span> </span><span>mag</span><span>;</span></div></div><div><div><div>42</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>y</span><span>{{</span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> std::</span><span>atan2</span><span>(</span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)))}};</span></div></div><div><div><div>43</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>H</span><span>{};</span></div></div><div><div><div>44</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Hp</span><span> </span><span>=</span><span> </span><span>heading_jacobian</span><span>();</span></div></div><div><div><div>45</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>H</span><span>(</span><span>0</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Hp</span><span>(</span><span>0</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>46</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>sm</span><span> </span><span>/</span><span> std::</span><span>hypot</span><span>(</span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>47</div></div><div><span>    </span><span>return</span><span> </span><span>correct</span><span>(</span><span>H</span><span>, </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>r</span><span> </span><span>*</span><span> </span><span>r</span><span>}}, </span><span>y</span><span>);</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>private:</span></div></div><div><div><div>51</div></div><div><span>  </span><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>M</span><span>&gt;</span></div></div><div><div><div>52</div></div><div><span>  </span><span>bool</span><span> </span><span>correct</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>H</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>R</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>y</span><span>) {</span></span></div></div><div><div><div>53</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span>;</span></div></div><div><div><div>54</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>joseph_update</span><span>(</span><span>P</span><span>, </span><span>H</span><span>, </span><span>R</span><span>, </span><span>y</span><span>, </span><span>dx</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>55</div></div><div><span>    </span><span>inject</span><span>(</span><span>dx</span><span>);</span></div></div><div><div><div>56</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span><span>  </span></span><span>// q &lt;- q (x) exp(dtheta), b &lt;- b + db, then reset P &lt;- G P G^T.</span></div></div><div><div><div>60</div></div><div><span>  </span><span>void</span><span> </span><span>inject</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>dx</span><span>) {</span></span></div></div><div><div><div>61</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dth</span><span>{{</span><span>dx</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>62</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> (</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>dth</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>63</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>3</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>4</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>5</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>64</div></div><div><span>    </span><span>if</span><span> (</span><span>reset</span><span>) {</span></div></div><div><div><div>65</div></div><div><span>      </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>G</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>66</div></div><div><span>      </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>dth</span><span> </span><span>*</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>));</span></div></div><div><div><div>67</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>68</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>G</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>-=</span><span> </span><span>S</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>69</div></div><div><span>      </span><span>P</span><span> </span><span>=</span><span> </span><span>G</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>G</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>correct</code> 是个模板函数，<code>M</code> 由 <code>H</code> 的行数推断：加速度计 <code>M = 3</code>，航向 <code>M = 1</code>，同一份代码，不同维度，编译器各生成一份。这是 C++ 版比 C 版舒服的地方：不需要在参数里传维度，也不可能传错。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>3</div></div><div><span>            </span><span>gyro</span><span>[</span><span>0</span><span>] - </span><span>self</span><span>.b[</span><span>0</span><span>],</span></div></div><div><div><div>4</div></div><div><span>            </span><span>gyro</span><span>[</span><span>1</span><span>] - </span><span>self</span><span>.b[</span><span>1</span><span>],</span></div></div><div><div><div>5</div></div><div><span>            </span><span>gyro</span><span>[</span><span>2</span><span>] - </span><span>self</span><span>.b[</span><span>2</span><span>],</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>];</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>dq</span><span><span> </span><span>=</span><span> </span></span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>w</span><span>[</span><span>0</span><span>] * </span><span>dt</span><span>, </span><span>w</span><span>[</span><span>1</span><span>] * </span><span>dt</span><span>, </span><span>w</span><span>[</span><span>2</span><span>] * </span><span>dt</span><span>]);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>ct</span><span><span> </span><span>=</span><span> </span></span><span>dq</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>q</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>            </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>3</span><span> + </span><span>i</span><span><span>] </span><span>=</span><span> -</span></span><span>dt</span><span>;</span></div></div><div><div><div>16</div></div><div><span>            </span><span>q</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.sg * </span><span>self</span><span>.sg * (</span><span>dt</span><span> * </span><span>dt</span><span>);</span></div></div><div><div><div>17</div></div><div><span>            </span><span>q</span><span>.m[</span><span>3</span><span> + </span><span>i</span><span>][</span><span>3</span><span> + </span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>self</span><span>.q * </span><span>dq</span><span>;</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>f</span><span> * </span><span>self</span><span>.p * </span><span>f</span><span>.</span><span>transpose</span><span>() + </span><span>q</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>/// Fold the error state into the nominal state, then reset the error to zero.</span></div></div><div><div><div>24</div></div><div><span>    </span><span>fn</span><span> </span><span>inject</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>dx</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>&gt;) {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>dth</span><span><span> </span><span>=</span><span> [</span></span><span>dx</span><span>.m[</span><span>0</span><span>][</span><span>0</span><span>], </span><span>dx</span><span>.m[</span><span>1</span><span>][</span><span>0</span><span>], </span><span>dx</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>]];</span></div></div><div><div><div>26</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> (</span></span><span>self</span><span>.q * </span><span>Quat</span><span>::</span><span>exp</span><span>(</span><span>dth</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>27</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>dx</span><span>.m[</span><span>3</span><span> + </span><span>i</span><span>][</span><span>0</span><span>];</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.reset {</span></div></div><div><div><div>31</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>([</span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>0</span><span>], </span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>1</span><span>], </span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>2</span><span>]]);</span></div></div><div><div><div>32</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>g</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>33</div></div><div><span>            </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>34</div></div><div><span>                </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>35</div></div><div><span>                    </span><span>g</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>36</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>            </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>g</span><span> * </span><span>self</span><span>.p * </span><span>g</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span>/// Gravity-direction update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>43</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_accel</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>44</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] * </span><span>accel</span><span>[</span><span>0</span><span>] + </span><span>accel</span><span>[</span><span>1</span><span>] * </span><span>accel</span><span>[</span><span>1</span><span>] + </span><span>accel</span><span>[</span><span>2</span><span>] * </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>45</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.accel_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>G</span><span>) &gt; </span><span>self</span><span>.accel_gate {</span></div></div><div><div><div>46</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>        </span><span>// h = C(q)^T [0, 0, -1]</span></div></div><div><div><div>49</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>50</div></div><div><span>        </span><span>let</span><span> </span><span>h</span><span><span> </span><span>=</span><span> [-</span></span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>1</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>2</span><span>]];</span></div></div><div><div><div>51</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>hm</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>6</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>52</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>53</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>54</div></div><div><span>            </span><span>hm</span><span>.m[</span><span>i</span><span>][..</span><span>3</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>s</span><span>.m[</span><span>i</span><span>]);</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>56</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span> {</span></div></div><div><div><div>57</div></div><div><span>            </span><span>m</span><span>: [</span></div></div><div><div><div>58</div></div><div><span><span>                </span></span><span>[</span><span>accel</span><span>[</span><span>0</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>0</span><span>]],</span></div></div><div><div><div>59</div></div><div><span><span>                </span></span><span>[</span><span>accel</span><span>[</span><span>1</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>1</span><span>]],</span></div></div><div><div><div>60</div></div><div><span><span>                </span></span><span>[</span><span>accel</span><span>[</span><span>2</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>2</span><span>]],</span></div></div><div><div><div>61</div></div><div><span><span>            </span></span><span>],</span></div></div><div><div><div>62</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>63</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>3</span><span>&gt;::</span><span>identity</span><span>() * ((</span><span>self</span><span>.sa / </span><span>G</span><span>) * (</span><span>self</span><span>.sa / </span><span>G</span><span>));</span></div></div><div><div><div>64</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>((</span><span>dx</span><span>, </span><span>_</span><span><span>)) </span><span>=</span><span> </span></span><span>joseph_update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.p, &amp;</span><span>hm</span><span>, &amp;</span><span>r</span><span>, &amp;</span><span>y</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>65</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>66</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>67</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>68</div></div><div><span>        </span><span>true</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>70</div></div><div>
</div></div><div><div><div>71</div></div><div><span>    </span><span>/// d heading / d dtheta = [mx mz, my mz, -(mx^2 + my^2)] / (mx^2 + my^2) C(q).</span></div></div><div><div><div>72</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>heading_jacobian</span><span>(&amp;</span><span>self</span><span>) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>73</div></div><div><span>        </span><span>let</span><span> [</span><span>mx</span><span>, </span><span>my</span><span>, </span><span>mz</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.m_w;</span></div></div><div><div><div>74</div></div><div><span>        </span><span>let</span><span> </span><span>mh2</span><span><span> </span><span>=</span><span> </span></span><span>mx</span><span> * </span><span>mx</span><span> + </span><span>my</span><span> * </span><span>my</span><span>;</span></div></div><div><div><div>75</div></div><div><span>        </span><span>let</span><span> </span><span>row</span><span><span> </span><span>=</span><span> [</span></span><span>mx</span><span> * </span><span>mz</span><span> / </span><span>mh2</span><span>, </span><span>my</span><span> * </span><span>mz</span><span> / </span><span>mh2</span><span>, -</span><span>1.0</span><span>];</span></div></div><div><div><div>76</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>77</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>j</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>3</span><span>];</span></div></div><div><div><div>78</div></div><div><span>        </span><span>for</span><span> (</span><span>k</span><span>, </span><span>jk</span><span>) </span><span>in</span><span> </span><span>j</span><span>.</span><span>iter_mut</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>79</div></div><div><span><span>            </span></span><span>*</span><span>jk</span><span><span> </span><span>=</span><span> </span></span><span>row</span><span>[</span><span>0</span><span>] * </span><span>c</span><span>.m[</span><span>0</span><span>][</span><span>k</span><span>] + </span><span>row</span><span>[</span><span>1</span><span>] * </span><span>c</span><span>.m[</span><span>1</span><span>][</span><span>k</span><span>] + </span><span>row</span><span>[</span><span>2</span><span>] * </span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span>        </span><span>j</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>    </span><span>/// Heading-only magnetometer update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>85</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_mag_heading</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>86</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>mag</span><span>[</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>mag</span><span>[</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>87</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.mag_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>1.0</span><span>) &gt; </span><span>self</span><span>.mag_gate {</span></div></div><div><div><div>88</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>89</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>90</div></div><div><span>        </span><span>// Rotate the measurement to the world frame and compare headings.</span></div></div><div><div><div>91</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>92</div></div><div><span>        </span><span>let</span><span> </span><span>mw</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>i</span><span>: </span><span>usize</span><span><span>|</span><span> </span></span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>93</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>wrap_pi</span><span>(</span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>)) - </span><span>atan2</span><span>(</span><span>self</span><span>.m_w[</span><span>1</span><span>], </span><span>self</span><span>.m_w[</span><span>0</span><span>]));</span></div></div><div><div><div>94</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>h</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>1</span><span>, </span><span>6</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>95</div></div><div><span>        </span><span>h</span><span>.m[</span><span>0</span><span>][..</span><span>3</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>self</span><span>.</span><span>heading_jacobian</span><span>());</span></div></div><div><div><div>96</div></div><div><span>        </span><span>let</span><span> </span><span>mh</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>self</span><span>.m_w[</span><span>0</span><span>] * </span><span>self</span><span>.m_w[</span><span>0</span><span>] + </span><span>self</span><span>.m_w[</span><span>1</span><span>] * </span><span>self</span><span>.m_w[</span><span>1</span><span>]);</span></div></div><div><div><div>97</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span> {</span></div></div><div><div><div>98</div></div><div><span>            </span><span>m</span><span>: [[(</span><span>self</span><span>.sm / </span><span>mh</span><span>) * (</span><span>self</span><span>.sm / </span><span>mh</span><span>)]],</span></div></div><div><div><div>99</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>100</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>((</span><span>dx</span><span>, </span><span>_</span><span><span>)) </span><span>=</span><span> </span></span><span>joseph_update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.p, &amp;</span><span>h</span><span>, &amp;</span><span>r</span><span>, &amp;</span><span>Mat</span><span> { </span><span>m</span><span>: [[</span><span>y</span><span>]] }) </span><span>else</span><span> {</span></div></div><div><div><div>101</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>102</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>103</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>104</div></div><div><span>        </span><span>true</span></div></div><div><div><div>105</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>update_mag_heading</code> 里 <code>mw</code> 是个闭包，只在需要的时候计算世界系航向的两个分量，没有给一个完整向量起名字。</p>
完整源码：c/include/kl/kl_eskf.h、c/src/kl_eskf.c<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Error-state (multiplicative) Kalman filter for attitude, after Sola (2017).</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* Nominal state: q, b. Error state: [dtheta (3), db (3)] with q_true = q (x) exp(dtheta),</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* b_true = b + db. P is 6x6 and always full rank. Quaternion conventions: see kl_quat.h.</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>* All storage is inline; nothing is allocated. */</span></div></div><div><div><div>5</div></div><div><span>#ifndef</span><span> </span><span>KL_ESKF_H</span></div></div><div><div><div>6</div></div><div><span>#define</span><span> </span><span>KL_ESKF_H</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>kl_real </span><span>q</span><span>[</span><span>4</span><span>], </span><span>b</span><span>[</span><span>3</span><span>], </span><span>P</span><span>[</span><span>36</span><span>];</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>kl_real sg, sbw, sa, sm;</span><span> /* gyro, bias random walk, accel, mag noise */</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>kl_real </span><span>m_w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>kl_real accel_gate, mag_gate;</span><span> /* negative disables the gate */</span></div></div><div><div><div>15</div></div><div><span>  </span><span>int</span><span> reset;</span><span>                    /* apply the reset Jacobian after injection */</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>kl_real </span><span>work</span><span>[</span><span>256</span><span>];</span></div></div><div><div><div>17</div></div><div><span>} kl_eskf;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>/* p0_diag: initial diagonal of P (6). The bias starts at zero. */</span></div></div><div><div><div>20</div></div><div><span>void</span><span> </span><span>kl_eskf_init</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>6</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>21</div></div><div><span><span>      </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>22</div></div><div><span><span>      </span></span><span>kl_real </span><span>accel_gate</span><span>,</span><span> kl_real </span><span>mag_gate</span><span>,</span><span> </span><span>int</span><span><span> </span><span>reset</span><span>);</span></span></div></div><div><div><div>23</div></div><div><span>void</span><span> </span><span>kl_eskf_predict</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>);</span></span></div></div><div><div><div>24</div></div><div><span>/* Return 1 if applied, 0 if gated out, -1 on Cholesky failure (state unchanged). */</span></div></div><div><div><div>25</div></div><div><span>int</span><span> </span><span>kl_eskf_update_accel</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>26</div></div><div><span>/* d heading / d dtheta = [mx mz, my mz, -(mx^2+my^2)] / (mx^2+my^2) C(q) */</span></div></div><div><div><div>27</div></div><div><span>void</span><span> </span><span>kl_eskf_heading_jacobian</span><span>(</span><span>const</span><span> kl_eskf </span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span></span><span>H</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>28</div></div><div><span>/* Heading-only magnetometer update; same return convention. */</span></div></div><div><div><div>29</div></div><div><span>int</span><span> </span><span>kl_eskf_update_mag_heading</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>30</div></div><div><span>/* Wrap an angle to [-pi, pi). */</span></div></div><div><div><div>31</div></div><div><span>kl_real </span><span>kl_wrap_pi</span><span><span>(</span><span>kl_real </span><span>a</span><span>);</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_eskf.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;stddef.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/kl_ekf_att.h"</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>"kl/kl_update.h"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#define</span><span> </span><span>PI</span><span> ((kl_real)</span><span>3.14159265358979323846</span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#ifdef</span><span> </span><span>KL_USE_FLOAT</span></div></div><div><div><div>13</div></div><div><span>#define</span><span> </span><span>KL_FMOD</span><span> fmodf</span><span> /* fmod would drag soft-double code into a Cortex-M4F build */</span></div></div><div><div><div>14</div></div><div><span>#else</span></div></div><div><div><div>15</div></div><div><span>#define</span><span> </span><span>KL_FMOD</span><span> fmod</span></div></div><div><div><div>16</div></div><div><span>#endif</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>kl_real </span><span>kl_wrap_pi</span><span><span>(</span><span>kl_real </span><span>a</span><span>)</span></span></div></div><div><div><div>19</div></div><div><span>{</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> </span><span>KL_FMOD</span><span>(a </span><span>+</span><span> PI, </span><span>2</span><span> </span><span>*</span><span> PI);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>if</span><span> (r </span><span>&lt;</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>r </span><span>+=</span><span> </span><span>2</span><span> </span><span>*</span><span> PI;</span></div></div><div><div><div>23</div></div><div><span>  </span><span>return</span><span> r </span><span>-</span><span> PI;</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>void</span><span> </span><span>kl_eskf_init</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>6</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>27</div></div><div><span><span>      </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>28</div></div><div><span><span>      </span></span><span>kl_real </span><span>accel_gate</span><span>,</span><span> kl_real </span><span>mag_gate</span><span>,</span><span> </span><span>int</span><span><span> </span><span>reset</span><span>)</span></span></div></div><div><div><div>29</div></div><div><span>{</span></div></div><div><div><div>30</div></div><div><span>  </span><span>kl_mat_copy</span><span>(q0, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>kl_mat_copy</span><span>(m_w, </span><span>f</span><span>-&gt;</span><span>m_w</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>32</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>34</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>36</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>P</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>36</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>6</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>37</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>P</span><span>[i </span><span>*</span><span> </span><span>7</span><span>] </span><span>=</span><span> </span><span>p0_diag</span><span>[i];</span></div></div><div><div><div>38</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>=</span><span> sigma_g;</span></div></div><div><div><div>39</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>=</span><span> sigma_bw;</span></div></div><div><div><div>40</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>=</span><span> sigma_a;</span></div></div><div><div><div>41</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>=</span><span> sigma_m;</span></div></div><div><div><div>42</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>=</span><span> accel_gate;</span></div></div><div><div><div>43</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>=</span><span> mag_gate;</span></div></div><div><div><div>44</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>reset</span><span> </span><span>=</span><span> reset;</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>void</span><span> </span><span>kl_eskf_predict</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>48</div></div><div><span>{</span></div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>F </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>Q </span><span>=</span><span> F </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>T </span><span>=</span><span> Q </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>50</div></div><div><span><span>  </span></span><span>kl_real </span><span>omega</span><span>[</span><span>3</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>C</span><span>[</span><span>9</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>51</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>omega</span><span>[i] </span><span>=</span><span> (</span><span>gyro</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i]) </span><span>*</span><span> dt;</span></div></div><div><div><div>53</div></div><div><span>  </span><span>kl_quat_exp</span><span>(omega, dq);</span></div></div><div><div><div>54</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(dq, C);</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span><span>  </span></span><span>/* F = [[C(dq)^T, -dt I], [0, I]], Q = diag(sg^2 dt^2 I, sbw^2 dt I) */</span></div></div><div><div><div>57</div></div><div><span>  </span><span>kl_mat_identity</span><span>(F, </span><span>6</span><span>);</span></div></div><div><div><div>58</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>36</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>59</div></div><div><span>    </span><span>Q</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>60</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>61</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>62</div></div><div><span>      </span><span>F</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>C</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>63</div></div><div><span>    </span><span>F</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> </span><span>3</span><span> </span><span>+</span><span> i] </span><span>=</span><span> </span><span>-</span><span>dt;</span></div></div><div><div><div>64</div></div><div><span>    </span><span>Q</span><span>[i </span><span>*</span><span> </span><span>7</span><span>] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> dt </span><span>*</span><span> dt;</span></div></div><div><div><div>65</div></div><div><span>    </span><span>Q</span><span>[(i </span><span>+</span><span> </span><span>3</span><span>) </span><span>*</span><span> </span><span>7</span><span>] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> dt;</span></div></div><div><div><div>66</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>67</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>68</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>69</div></div><div><span>  </span><span>kl_mat_mul</span><span>(F, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>70</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(T, F, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>71</div></div><div><span>  </span><span>kl_mat_add</span><span>(U, Q, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>72</div></div><div><span>}</span></div></div><div><div><div>73</div></div><div>
</div></div><div><div><div>74</div></div><div><span>/* Move the error estimate into the nominal state, then reset the error to zero. */</span></div></div><div><div><div>75</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>inject</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>dx</span><span>[</span><span>6</span><span><span>]</span><span>)</span></span></div></div><div><div><div>76</div></div><div><span>{</span></div></div><div><div><div>77</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>G </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>T </span><span>=</span><span> G </span><span>+</span><span> </span><span>36</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>36</span><span>;</span></div></div><div><div><div>78</div></div><div><span><span>  </span></span><span>kl_real </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>79</div></div><div><span>  </span><span>kl_quat_exp</span><span>(dx, dq);</span></div></div><div><div><div>80</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>81</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>82</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>83</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>84</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+=</span><span> </span><span>dx</span><span>[</span><span>3</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>85</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>reset</span><span>) {</span></div></div><div><div><div>86</div></div><div><span><span>    </span></span><span>/* G = blkdiag(I - [dtheta/2]x, I), P = G P G^T */</span></div></div><div><div><div>87</div></div><div><span><span>    </span></span><span>kl_real </span><span>half</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {(kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>0</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>1</span><span>], (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>dx</span><span>[</span><span>2</span><span>]};</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>kl_real </span><span>S</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>89</div></div><div><span>    </span><span>kl_skew</span><span>(half, S);</span></div></div><div><div><div>90</div></div><div><span>    </span><span>kl_mat_identity</span><span>(G, </span><span>6</span><span>);</span></div></div><div><div><div>91</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>92</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>93</div></div><div><span>        </span><span>G</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>-=</span><span> </span><span>S</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>94</div></div><div><span>    </span><span>kl_mat_mul</span><span>(G, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>95</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, G, U, </span><span>6</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>96</div></div><div><span>    </span><span>kl_mat_copy</span><span>(U, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>6</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>97</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>98</div></div><div><span>}</span></div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div><span>int</span><span> </span><span>kl_eskf_update_accel</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>101</div></div><div><span>{</span></div></div><div><div><div>102</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>H </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>R </span><span>=</span><span> H </span><span>+</span><span> </span><span>18</span><span>, </span><span>*</span><span>jw </span><span>=</span><span> R </span><span>+</span><span> </span><span>9</span><span>;</span></div></div><div><div><div>103</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>accel</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>104</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> (kl_real)KL_GRAVITY) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span>)</span></div></div><div><div><div>105</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>106</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>down</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>107</div></div><div><span><span>  </span></span><span>kl_real </span><span>h</span><span>[</span><span>3</span><span>], </span><span>S</span><span>[</span><span>9</span><span>], </span><span>y</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[</span><span>6</span><span>];</span></div></div><div><div><div>108</div></div><div><span>  </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, down, h);</span><span> /* C(q)^T [0,0,-1] for unit q */</span></div></div><div><div><div>109</div></div><div><span>  </span><span>kl_skew</span><span>(h, S);</span></div></div><div><div><div>110</div></div><div><span><span>  </span></span><span>/* H = [[h]x | 0] */</span></div></div><div><div><div>111</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>112</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>113</div></div><div><span>      </span><span>H</span><span>[i </span><span>*</span><span> </span><span>6</span><span> </span><span>+</span><span> j] </span><span>=</span><span> j </span><span>&lt;</span><span> </span><span>3</span><span> </span><span>?</span><span> </span><span>S</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>114</div></div><div><span><span>  </span></span><span>kl_real r </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY);</span></div></div><div><div><div>115</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>116</div></div><div><span>    </span><span>y</span><span>[i] </span><span>=</span><span> </span><span>accel</span><span>[i] </span><span>/</span><span> norm </span><span>-</span><span> </span><span>h</span><span>[i];</span></div></div><div><div><div>117</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>118</div></div><div><span>      </span><span>R</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>?</span><span> r </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>119</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>120</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_joseph_update</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, H, R, y, </span><span>6</span><span>, </span><span>3</span><span>, dx, </span><span>NULL</span><span>, jw) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>121</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>122</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>123</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>124</div></div><div><span>}</span></div></div><div><div><div>125</div></div><div>
</div></div><div><div><div>126</div></div><div><span>void</span><span> </span><span>kl_eskf_heading_jacobian</span><span>(</span><span>const</span><span> kl_eskf </span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span></span><span>H</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>127</div></div><div><span>{</span></div></div><div><div><div>128</div></div><div><span><span>  </span></span><span>kl_real mx </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>], my </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], mz </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>2</span><span>], </span><span>C</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>129</div></div><div><span><span>  </span></span><span>kl_real mh2 </span><span>=</span><span> mx </span><span>*</span><span> mx </span><span>+</span><span> my </span><span>*</span><span> my;</span></div></div><div><div><div>130</div></div><div><span><span>  </span></span><span>kl_real </span><span>v</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {mx </span><span>*</span><span> mz </span><span>/</span><span> mh2, my </span><span>*</span><span> mz </span><span>/</span><span> mh2, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>131</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>132</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>133</div></div><div><span>    </span><span>H</span><span>[j] </span><span>=</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>C</span><span>[j] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>C</span><span>[</span><span>3</span><span> </span><span>+</span><span> j] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>C</span><span>[</span><span>6</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>134</div></div><div><span>}</span></div></div><div><div><div>135</div></div><div>
</div></div><div><div><div>136</div></div><div><span>int</span><span> </span><span>kl_eskf_update_mag_heading</span><span><span>(</span><span>kl_eskf </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>137</div></div><div><span>{</span></div></div><div><div><div>138</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>H </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>R </span><span>=</span><span> H </span><span>+</span><span> </span><span>6</span><span>, </span><span>*</span><span>jw </span><span>=</span><span> R </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>139</div></div><div><span><span>  </span></span><span>kl_real norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>mag</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>140</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>mag_gate</span><span>)</span></div></div><div><div><div>141</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>142</div></div><div><span><span>  </span></span><span>kl_real </span><span>C</span><span>[</span><span>9</span><span>], </span><span>mw</span><span>[</span><span>3</span><span>], y, </span><span>dx</span><span>[</span><span>6</span><span>];</span></div></div><div><div><div>143</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>144</div></div><div><span>  </span><span>kl_mat_mul</span><span>(C, mag, mw, </span><span>3</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>145</div></div><div><span><span>  </span></span><span>y </span><span>=</span><span> </span><span>kl_wrap_pi</span><span>(</span><span>KL_ATAN2</span><span>(</span><span>mw</span><span>[</span><span>1</span><span>], </span><span>mw</span><span>[</span><span>0</span><span>]) </span><span>-</span><span> </span><span>KL_ATAN2</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>], </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>]));</span></div></div><div><div><div>146</div></div><div><span>  </span><span>kl_eskf_heading_jacobian</span><span>(f, H);</span></div></div><div><div><div>147</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>3</span><span>; j </span><span>&lt;</span><span> </span><span>6</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>148</div></div><div><span>    </span><span>H</span><span>[j] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>149</div></div><div><span><span>  </span></span><span>kl_real mh </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>]);</span></div></div><div><div><div>150</div></div><div><span>  </span><span>R</span><span>[</span><span>0</span><span>] </span><span>=</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> mh) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>/</span><span> mh);</span></div></div><div><div><div>151</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_joseph_update</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, H, R, </span><span>&amp;</span><span>y, </span><span>6</span><span>, </span><span>1</span><span>, dx, </span><span>NULL</span><span>, jw) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>152</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>153</div></div><div><span>  </span><span>inject</span><span>(f, dx);</span></div></div><div><div><div>154</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>155</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：cpp/include/kl/eskf.hpp<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Error-state (multiplicative) Kalman filter for attitude, after Sola (2017).</span></div></div><div><div><div>2</div></div><div><span>// Nominal state: q, b. Error state: [dtheta (3), db (3)] with</span></div></div><div><div><div>3</div></div><div><span>// q_true = q (x) exp(dtheta), b_true = b + db. P is 6x6 and always full rank.</span></div></div><div><div><div>4</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/ekf_attitude.hpp"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// Wraps an angle to [-pi, pi).</span></div></div><div><div><div>10</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span>T</span><span> </span><span>wrap_pi</span><span>(</span><span>T</span><span><span> </span><span>a</span><span>) {</span></span></div></div><div><div><div>12</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>two_pi</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>2</span><span> </span><span>*</span><span> </span><span>3.14159265358979323846</span><span>);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>T</span><span> </span><span>m</span><span> </span><span>=</span><span> std::</span><span>fmod</span><span>(</span><span>a</span><span> </span><span>+</span><span> </span><span>T</span><span>(</span><span>3.14159265358979323846</span><span>), </span><span>two_pi</span><span>);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> (</span><span>m</span><span> </span><span>&lt;</span><span> </span><span>0</span><span>) </span><span>m</span><span> </span><span>+=</span><span> </span><span>two_pi</span><span>;</span></div></div><div><div><div>15</div></div><div><span>  </span><span>return</span><span> </span><span>m</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>3.14159265358979323846</span><span>);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>19</div></div><div><span>class</span><span> </span><span>ESKF</span><span> {</span></div></div><div><div><div>20</div></div><div><span>public:</span></div></div><div><div><div>21</div></div><div><span>  </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>q</span><span>;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>b</span><span>{};</span></div></div><div><div><div>23</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>P</span><span>{};</span></div></div><div><div><div>24</div></div><div><span>  </span><span>T</span><span> </span><span>sg</span><span>, </span><span>sbw</span><span>, </span><span>sa</span><span>, </span><span>sm</span><span>;</span></div></div><div><div><div>25</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_w</span><span>;</span></div></div><div><div><div>26</div></div><div><span>  </span><span>T</span><span> </span><span>accel_gate</span><span>, </span><span>mag_gate</span><span>;</span><span>  // negative disables the gate</span></div></div><div><div><div>27</div></div><div><span>  </span><span>bool</span><span> </span><span>reset</span><span>;</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>  </span><span>ESKF</span><span>(</span><span>T</span><span><span> </span><span>sigma_g</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_bw</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_a</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_m</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m_w</span><span>, </span></span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q0</span><span>,</span></span></div></div><div><div><div>30</div></div><div><span>       </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P0</span><span>, </span></span><span>T</span><span><span> </span><span>accel_gate</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>), </span><span>T</span><span><span> </span><span>mag_gate</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0.1</span><span>), </span><span>bool</span><span><span> </span><span>reset</span><span> </span></span><span>=</span><span> </span><span>true</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>      </span></span><span>: </span><span>q</span><span>(</span><span>q0</span><span>), </span><span>P</span><span>(</span><span>P0</span><span>), </span><span>sg</span><span>(</span><span>sigma_g</span><span>), </span><span>sbw</span><span>(</span><span>sigma_bw</span><span>), </span><span>sa</span><span>(</span><span>sigma_a</span><span>), </span><span>sm</span><span>(</span><span>sigma_m</span><span>), </span><span>m_w</span><span>(</span><span>m_w</span><span>),</span></div></div><div><div><div>32</div></div><div><span>        </span><span>accel_gate</span><span>(</span><span>accel_gate</span><span>), </span><span>mag_gate</span><span>(</span><span>mag_gate</span><span>), </span><span>reset</span><span>(</span><span>reset</span><span>) {}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dq</span><span> </span><span>=</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>((</span><span>gyro</span><span> </span><span>-</span><span> </span><span>b</span><span>) </span><span>*</span><span> </span><span>dt</span><span>);</span></div></div><div><div><div>36</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Ct</span><span> </span><span>=</span><span> </span><span>dq</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>37</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>F</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>38</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>Q</span><span>{};</span></div></div><div><div><div>39</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>40</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>F</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Ct</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>41</div></div><div><span>      </span><span>F</span><span>(</span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>=</span><span> </span><span>-</span><span>dt</span><span>;</span></div></div><div><div><div>42</div></div><div><span>      </span><span>Q</span><span>(</span><span>i</span><span>, </span><span>i</span><span>) </span><span>=</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>dt</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>43</div></div><div><span>      </span><span>Q</span><span>(</span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>, </span><span>3</span><span> </span><span>+</span><span> </span><span>i</span><span>) </span><span>=</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>q</span><span> </span><span>*</span><span> </span><span>dq</span><span>;</span><span>  // not normalized here</span></div></div><div><div><div>46</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>// Gravity-direction update. Returns false when gated out or when S is not</span></div></div><div><div><div>50</div></div><div><span><span>  </span></span><span>// positive definite (state unchanged in both cases).</span></div></div><div><div><div>51</div></div><div><span>  </span><span>bool</span><span> </span><span>update_accel</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>) {</span></span></div></div><div><div><div>52</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>53</div></div><div><span>    </span><span>if</span><span> (</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>&gt;</span><span> </span><span>accel_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>54</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>h</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>() </span><span>*</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}};</span></div></div><div><div><div>55</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>H</span><span>{};</span></div></div><div><div><div>56</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Sh</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>57</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>58</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>H</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Sh</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>59</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>*</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>));</span></div></div><div><div><div>60</div></div><div><span>    </span><span>return</span><span> </span><span>correct</span><span>(</span><span>H</span><span>, Mat&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>*</span><span> </span><span>r</span><span>, </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> </span><span>norm</span><span>) </span><span>-</span><span> </span><span>h</span><span>);</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>// d heading / d dtheta = [mx mz, my mz, -(mx^2+my^2)] / (mx^2+my^2) C(q).</span></div></div><div><div><div>64</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>heading_jacobian</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>65</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>mx</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>my</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mz</span><span> </span><span>=</span><span> </span><span>m_w</span><span>(</span><span>2</span><span>, </span><span>0</span><span>), </span><span>mh2</span><span> </span><span>=</span><span> </span><span>mx</span><span> </span><span>*</span><span> </span><span>mx</span><span> </span><span>+</span><span> </span><span>my</span><span> </span><span>*</span><span> </span><span>my</span><span>;</span></div></div><div><div><div>66</div></div><div><span>    </span><span>return</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>mx</span><span> </span><span>*</span><span> </span><span>mz</span><span> </span><span>/</span><span> </span><span>mh2</span><span>, </span><span>my</span><span> </span><span>*</span><span> </span><span>mz</span><span> </span><span>/</span><span> </span><span>mh2</span><span>, </span><span>T</span><span>(</span><span>-</span><span>1</span><span>)}} </span><span>*</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>();</span></div></div><div><div><div>67</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>// Heading-only magnetometer update. Returns false when gated out or when S is</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>// not positive definite (state unchanged in both cases).</span></div></div><div><div><div>71</div></div><div><span>  </span><span>bool</span><span> </span><span>update_mag_heading</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>) {</span></span></div></div><div><div><div>72</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>73</div></div><div><span>    </span><span>if</span><span> (</span><span>mag_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>1</span><span>) </span><span>&gt;</span><span> </span><span>mag_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>74</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>mw</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>() </span><span>*</span><span> </span><span>mag</span><span>;</span></div></div><div><div><div>75</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>y</span><span>{{</span><span>wrap_pi</span><span><span>(</span><span>std</span><span>::</span></span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)) </span><span>-</span><span> std::</span><span>atan2</span><span>(</span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)))}};</span></div></div><div><div><div>76</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>H</span><span>{};</span></div></div><div><div><div>77</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Hp</span><span> </span><span>=</span><span> </span><span>heading_jacobian</span><span>();</span></div></div><div><div><div>78</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>H</span><span>(</span><span>0</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Hp</span><span>(</span><span>0</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>79</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>sm</span><span> </span><span>/</span><span> std::</span><span>hypot</span><span>(</span><span>m_w</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>m_w</span><span>(</span><span>1</span><span>, </span><span>0</span><span>));</span></div></div><div><div><div>80</div></div><div><span>    </span><span>return</span><span> </span><span>correct</span><span>(</span><span>H</span><span>, </span><span>Mat</span><span>&lt;</span><span>1</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>r</span><span> </span><span>*</span><span> </span><span>r</span><span>}}, </span><span>y</span><span>);</span></div></div><div><div><div>81</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span>private:</span></div></div><div><div><div>84</div></div><div><span>  </span><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>M</span><span>&gt;</span></div></div><div><div><div>85</div></div><div><span>  </span><span>bool</span><span> </span><span>correct</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>H</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>R</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>y</span><span>) {</span></span></div></div><div><div><div>86</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span>;</span></div></div><div><div><div>87</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>joseph_update</span><span>(</span><span>P</span><span>, </span><span>H</span><span>, </span><span>R</span><span>, </span><span>y</span><span>, </span><span>dx</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>88</div></div><div><span>    </span><span>inject</span><span>(</span><span>dx</span><span>);</span></div></div><div><div><div>89</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>90</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>91</div></div><div>
</div></div><div><div><div>92</div></div><div><span><span>  </span></span><span>// q &lt;- q (x) exp(dtheta), b &lt;- b + db, then reset P &lt;- G P G^T.</span></div></div><div><div><div>93</div></div><div><span>  </span><span>void</span><span> </span><span>inject</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>dx</span><span>) {</span></span></div></div><div><div><div>94</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dth</span><span>{{</span><span>dx</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>95</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> (</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>dth</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>96</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>3</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>4</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>5</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>97</div></div><div><span>    </span><span>if</span><span> (</span><span>reset</span><span>) {</span></div></div><div><div><div>98</div></div><div><span>      </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt; </span><span>G</span><span> </span><span>=</span><span> Mat&lt;</span><span>6</span><span>, </span><span>6</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>99</div></div><div><span>      </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span> </span><span>=</span><span> </span><span>skew</span><span>(</span><span>dth</span><span> </span><span>*</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>));</span></div></div><div><div><div>100</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>101</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>G</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>-=</span><span> </span><span>S</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>102</div></div><div><span>      </span><span>P</span><span> </span><span>=</span><span> </span><span>G</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>G</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>103</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>104</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>105</div></div><div><span>};</span></div></div><div><div><div>106</div></div><div>
</div></div><div><div><div>107</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：rust/src/eskf.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Error-state (multiplicative) Kalman filter for attitude, after Sola (2017).</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Nominal state q, b. Error state [dtheta (3), db (3)] with</span></div></div><div><div><div>4</div></div><div><span>//! q_true = q (x) exp(dtheta), b_true = b + db. P is 6x6 and stays full rank.</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>ekf_att</span><span>::</span></span><span>G</span><span>;</span></div></div><div><div><div>7</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::</span></span><span>Mat</span><span>;</span></div></div><div><div><div>8</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>quat</span><span>::{</span></span><span>Quat</span><span>, skew};</span></div></div><div><div><div>9</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, atan2, fabs, sqrt};</span></div></div><div><div><div>10</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>update</span><span>::joseph_update;</span></span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>const</span><span> </span><span>PI</span><span>: </span><span>Real</span><span><span> </span><span>=</span><span> </span><span>core</span><span>::</span></span><span>f64</span><span><span>::</span><span>consts</span><span>::</span></span><span>PI</span><span> </span><span>as</span><span> </span><span>Real</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>/// Wrap an angle to [-pi, pi).</span></div></div><div><div><div>15</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>wrap_pi</span><span>(</span><span>a</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>r</span><span><span> </span><span>=</span><span> (</span></span><span>a</span><span> + </span><span>PI</span><span>) % (</span><span>2.0</span><span> * </span><span>PI</span><span>);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>if</span><span> </span><span>r</span><span><span> </span><span>&lt;</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>r</span><span><span> </span><span>+=</span><span> </span></span><span>2.0</span><span> * </span><span>PI</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>    </span><span>r</span><span> - </span><span>PI</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Eskf</span><span> {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>pub</span><span> </span><span>q</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>25</div></div><div><span>    </span><span>pub</span><span> </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>26</div></div><div><span>    </span><span>pub</span><span> </span><span>p</span><span>: </span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;,</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pub</span><span> </span><span>sg</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>28</div></div><div><span>    </span><span>pub</span><span> </span><span>sbw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>29</div></div><div><span>    </span><span>pub</span><span> </span><span>sa</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>30</div></div><div><span>    </span><span>pub</span><span> </span><span>sm</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>31</div></div><div><span>    </span><span>pub</span><span> </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>32</div></div><div><span>    </span><span>/// Gates on | |f| - g | (m/s^2) and | |m| - 1 |; negative disables gating.</span></div></div><div><div><div>33</div></div><div><span>    </span><span>pub</span><span> </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>34</div></div><div><span>    </span><span>pub</span><span> </span><span>mag_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>35</div></div><div><span>    </span><span>/// Apply the reset Jacobian P &lt;- G P G^T after each injection.</span></div></div><div><div><div>36</div></div><div><span>    </span><span>pub</span><span> </span><span>reset</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>impl</span><span> </span><span>Eskf</span><span> {</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>#[allow(clippy</span><span>::</span><span>too_many_arguments)]</span></div></div><div><div><div>41</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span></div></div><div><div><div>42</div></div><div><span>        </span><span>q0</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>43</div></div><div><span>        </span><span>p0_diag</span><span>: [</span><span>Real</span><span>; </span><span>6</span><span>],</span></div></div><div><div><div>44</div></div><div><span>        </span><span>sigma_g</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>45</div></div><div><span>        </span><span>sigma_bw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>46</div></div><div><span>        </span><span>sigma_a</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>47</div></div><div><span>        </span><span>sigma_m</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>48</div></div><div><span>        </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>49</div></div><div><span>        </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>50</div></div><div><span>        </span><span>mag_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>51</div></div><div><span>        </span><span>reset</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>53</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>54</div></div><div><span>        </span><span>for</span><span> (</span><span>i</span><span>, </span><span>d</span><span>) </span><span>in</span><span> </span><span>p0_diag</span><span>.</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>55</div></div><div><span>            </span><span>p</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> *</span></span><span>d</span><span>;</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>57</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>58</div></div><div><span>            </span><span>q</span><span>: </span><span>q0</span><span>,</span></div></div><div><div><div>59</div></div><div><span>            </span><span>b</span><span>: [</span><span>0.0</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>60</div></div><div><span>            </span><span>p</span><span>,</span></div></div><div><div><div>61</div></div><div><span>            </span><span>sg</span><span>: </span><span>sigma_g</span><span>,</span></div></div><div><div><div>62</div></div><div><span>            </span><span>sbw</span><span>: </span><span>sigma_bw</span><span>,</span></div></div><div><div><div>63</div></div><div><span>            </span><span>sa</span><span>: </span><span>sigma_a</span><span>,</span></div></div><div><div><div>64</div></div><div><span>            </span><span>sm</span><span>: </span><span>sigma_m</span><span>,</span></div></div><div><div><div>65</div></div><div><span>            </span><span>m_w</span><span>,</span></div></div><div><div><div>66</div></div><div><span>            </span><span>accel_gate</span><span>,</span></div></div><div><div><div>67</div></div><div><span>            </span><span>mag_gate</span><span>,</span></div></div><div><div><div>68</div></div><div><span>            </span><span>reset</span><span>,</span></div></div><div><div><div>69</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>73</div></div><div><span>        </span><span>let</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>74</div></div><div><span>            </span><span>gyro</span><span>[</span><span>0</span><span>] - </span><span>self</span><span>.b[</span><span>0</span><span>],</span></div></div><div><div><div>75</div></div><div><span>            </span><span>gyro</span><span>[</span><span>1</span><span>] - </span><span>self</span><span>.b[</span><span>1</span><span>],</span></div></div><div><div><div>76</div></div><div><span>            </span><span>gyro</span><span>[</span><span>2</span><span>] - </span><span>self</span><span>.b[</span><span>2</span><span>],</span></div></div><div><div><div>77</div></div><div><span><span>        </span></span><span>];</span></div></div><div><div><div>78</div></div><div><span>        </span><span>let</span><span> </span><span>dq</span><span><span> </span><span>=</span><span> </span></span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>w</span><span>[</span><span>0</span><span>] * </span><span>dt</span><span>, </span><span>w</span><span>[</span><span>1</span><span>] * </span><span>dt</span><span>, </span><span>w</span><span>[</span><span>2</span><span>] * </span><span>dt</span><span>]);</span></div></div><div><div><div>79</div></div><div><span>        </span><span>let</span><span> </span><span>ct</span><span><span> </span><span>=</span><span> </span></span><span>dq</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>80</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>81</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>q</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>82</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>83</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>84</div></div><div><span>                </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>85</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>86</div></div><div><span>            </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>3</span><span> + </span><span>i</span><span><span>] </span><span>=</span><span> -</span></span><span>dt</span><span>;</span></div></div><div><div><div>87</div></div><div><span>            </span><span>q</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.sg * </span><span>self</span><span>.sg * (</span><span>dt</span><span> * </span><span>dt</span><span>);</span></div></div><div><div><div>88</div></div><div><span>            </span><span>q</span><span>.m[</span><span>3</span><span> + </span><span>i</span><span>][</span><span>3</span><span> + </span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>;</span></div></div><div><div><div>89</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>90</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>self</span><span>.q * </span><span>dq</span><span>;</span></div></div><div><div><div>91</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>f</span><span> * </span><span>self</span><span>.p * </span><span>f</span><span>.</span><span>transpose</span><span>() + </span><span>q</span><span>;</span></div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>93</div></div><div>
</div></div><div><div><div>94</div></div><div><span>    </span><span>/// Fold the error state into the nominal state, then reset the error to zero.</span></div></div><div><div><div>95</div></div><div><span>    </span><span>fn</span><span> </span><span>inject</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>dx</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>6</span><span>, </span><span>1</span><span>&gt;) {</span></div></div><div><div><div>96</div></div><div><span>        </span><span>let</span><span> </span><span>dth</span><span><span> </span><span>=</span><span> [</span></span><span>dx</span><span>.m[</span><span>0</span><span>][</span><span>0</span><span>], </span><span>dx</span><span>.m[</span><span>1</span><span>][</span><span>0</span><span>], </span><span>dx</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>]];</span></div></div><div><div><div>97</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> (</span></span><span>self</span><span>.q * </span><span>Quat</span><span>::</span><span>exp</span><span>(</span><span>dth</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>98</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>99</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>dx</span><span>.m[</span><span>3</span><span> + </span><span>i</span><span>][</span><span>0</span><span>];</span></div></div><div><div><div>100</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>101</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.reset {</span></div></div><div><div><div>102</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>([</span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>0</span><span>], </span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>1</span><span>], </span><span>0.5</span><span> * </span><span>dth</span><span>[</span><span>2</span><span>]]);</span></div></div><div><div><div>103</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>g</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>6</span><span>, </span><span>6</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>104</div></div><div><span>            </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>105</div></div><div><span>                </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>106</div></div><div><span>                    </span><span>g</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>-=</span><span> </span></span><span>s</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>107</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>108</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>109</div></div><div><span>            </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>g</span><span> * </span><span>self</span><span>.p * </span><span>g</span><span>.</span><span>transpose</span><span>();</span></div></div><div><div><div>110</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>111</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>112</div></div><div>
</div></div><div><div><div>113</div></div><div><span>    </span><span>/// Gravity-direction update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>114</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_accel</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>115</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>accel</span><span>[</span><span>0</span><span>] * </span><span>accel</span><span>[</span><span>0</span><span>] + </span><span>accel</span><span>[</span><span>1</span><span>] * </span><span>accel</span><span>[</span><span>1</span><span>] + </span><span>accel</span><span>[</span><span>2</span><span>] * </span><span>accel</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>116</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.accel_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>G</span><span>) &gt; </span><span>self</span><span>.accel_gate {</span></div></div><div><div><div>117</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>118</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>119</div></div><div><span>        </span><span>// h = C(q)^T [0, 0, -1]</span></div></div><div><div><div>120</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>121</div></div><div><span>        </span><span>let</span><span> </span><span>h</span><span><span> </span><span>=</span><span> [-</span></span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>1</span><span>], -</span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>2</span><span>]];</span></div></div><div><div><div>122</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>hm</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>6</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>123</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>(</span><span>h</span><span>);</span></div></div><div><div><div>124</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>125</div></div><div><span>            </span><span>hm</span><span>.m[</span><span>i</span><span>][..</span><span>3</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>s</span><span>.m[</span><span>i</span><span>]);</span></div></div><div><div><div>126</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>127</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span> {</span></div></div><div><div><div>128</div></div><div><span>            </span><span>m</span><span>: [</span></div></div><div><div><div>129</div></div><div><span><span>                </span></span><span>[</span><span>accel</span><span>[</span><span>0</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>0</span><span>]],</span></div></div><div><div><div>130</div></div><div><span><span>                </span></span><span>[</span><span>accel</span><span>[</span><span>1</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>1</span><span>]],</span></div></div><div><div><div>131</div></div><div><span><span>                </span></span><span>[</span><span>accel</span><span>[</span><span>2</span><span>] / </span><span>norm</span><span> - </span><span>h</span><span>[</span><span>2</span><span>]],</span></div></div><div><div><div>132</div></div><div><span><span>            </span></span><span>],</span></div></div><div><div><div>133</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>134</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>3</span><span>&gt;::</span><span>identity</span><span>() * ((</span><span>self</span><span>.sa / </span><span>G</span><span>) * (</span><span>self</span><span>.sa / </span><span>G</span><span>));</span></div></div><div><div><div>135</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>((</span><span>dx</span><span>, </span><span>_</span><span><span>)) </span><span>=</span><span> </span></span><span>joseph_update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.p, &amp;</span><span>hm</span><span>, &amp;</span><span>r</span><span>, &amp;</span><span>y</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>136</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>137</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>138</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>139</div></div><div><span>        </span><span>true</span></div></div><div><div><div>140</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>141</div></div><div>
</div></div><div><div><div>142</div></div><div><span>    </span><span>/// d heading / d dtheta = [mx mz, my mz, -(mx^2 + my^2)] / (mx^2 + my^2) C(q).</span></div></div><div><div><div>143</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>heading_jacobian</span><span>(&amp;</span><span>self</span><span>) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>144</div></div><div><span>        </span><span>let</span><span> [</span><span>mx</span><span>, </span><span>my</span><span>, </span><span>mz</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.m_w;</span></div></div><div><div><div>145</div></div><div><span>        </span><span>let</span><span> </span><span>mh2</span><span><span> </span><span>=</span><span> </span></span><span>mx</span><span> * </span><span>mx</span><span> + </span><span>my</span><span> * </span><span>my</span><span>;</span></div></div><div><div><div>146</div></div><div><span>        </span><span>let</span><span> </span><span>row</span><span><span> </span><span>=</span><span> [</span></span><span>mx</span><span> * </span><span>mz</span><span> / </span><span>mh2</span><span>, </span><span>my</span><span> * </span><span>mz</span><span> / </span><span>mh2</span><span>, -</span><span>1.0</span><span>];</span></div></div><div><div><div>147</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>148</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>j</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>3</span><span>];</span></div></div><div><div><div>149</div></div><div><span>        </span><span>for</span><span> (</span><span>k</span><span>, </span><span>jk</span><span>) </span><span>in</span><span> </span><span>j</span><span>.</span><span>iter_mut</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>150</div></div><div><span><span>            </span></span><span>*</span><span>jk</span><span><span> </span><span>=</span><span> </span></span><span>row</span><span>[</span><span>0</span><span>] * </span><span>c</span><span>.m[</span><span>0</span><span>][</span><span>k</span><span>] + </span><span>row</span><span>[</span><span>1</span><span>] * </span><span>c</span><span>.m[</span><span>1</span><span>][</span><span>k</span><span>] + </span><span>row</span><span>[</span><span>2</span><span>] * </span><span>c</span><span>.m[</span><span>2</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>151</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>152</div></div><div><span>        </span><span>j</span></div></div><div><div><div>153</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>154</div></div><div>
</div></div><div><div><div>155</div></div><div><span>    </span><span>/// Heading-only magnetometer update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>156</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_mag_heading</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>157</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>mag</span><span>[</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>mag</span><span>[</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>mag</span><span>[</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>158</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.mag_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>1.0</span><span>) &gt; </span><span>self</span><span>.mag_gate {</span></div></div><div><div><div>159</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>160</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>161</div></div><div><span>        </span><span>// Rotate the measurement to the world frame and compare headings.</span></div></div><div><div><div>162</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>();</span></div></div><div><div><div>163</div></div><div><span>        </span><span>let</span><span> </span><span>mw</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>i</span><span>: </span><span>usize</span><span><span>|</span><span> </span></span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>0</span><span>] * </span><span>mag</span><span>[</span><span>0</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>1</span><span>] * </span><span>mag</span><span>[</span><span>1</span><span>] + </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>2</span><span>] * </span><span>mag</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>164</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>wrap_pi</span><span>(</span><span>atan2</span><span>(</span><span>mw</span><span>(</span><span>1</span><span>), </span><span>mw</span><span>(</span><span>0</span><span>)) - </span><span>atan2</span><span>(</span><span>self</span><span>.m_w[</span><span>1</span><span>], </span><span>self</span><span>.m_w[</span><span>0</span><span>]));</span></div></div><div><div><div>165</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>h</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>1</span><span>, </span><span>6</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>166</div></div><div><span>        </span><span>h</span><span>.m[</span><span>0</span><span>][..</span><span>3</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>self</span><span>.</span><span>heading_jacobian</span><span>());</span></div></div><div><div><div>167</div></div><div><span>        </span><span>let</span><span> </span><span>mh</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>self</span><span>.m_w[</span><span>0</span><span>] * </span><span>self</span><span>.m_w[</span><span>0</span><span>] + </span><span>self</span><span>.m_w[</span><span>1</span><span>] * </span><span>self</span><span>.m_w[</span><span>1</span><span>]);</span></div></div><div><div><div>168</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span> {</span></div></div><div><div><div>169</div></div><div><span>            </span><span>m</span><span>: [[(</span><span>self</span><span>.sm / </span><span>mh</span><span>) * (</span><span>self</span><span>.sm / </span><span>mh</span><span>)]],</span></div></div><div><div><div>170</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>171</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>((</span><span>dx</span><span>, </span><span>_</span><span><span>)) </span><span>=</span><span> </span></span><span>joseph_update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.p, &amp;</span><span>h</span><span>, &amp;</span><span>r</span><span>, &amp;</span><span>Mat</span><span> { </span><span>m</span><span>: [[</span><span>y</span><span>]] }) </span><span>else</span><span> {</span></div></div><div><div><div>172</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>173</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>174</div></div><div><span>        </span><span>self</span><span>.</span><span>inject</span><span>(&amp;</span><span>dx</span><span>);</span></div></div><div><div><div>175</div></div><div><span>        </span><span>true</span></div></div><div><div><div>176</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>177</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个 app（<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/c/apps/ch06.c" target="_blank"><code>c/apps/ch06.c</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/cpp/apps/ch06.cpp" target="_blank"><code>cpp/apps/ch06.cpp</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/rust/src/bin/ch06.rs" target="_blank"><code>rust/src/bin/ch06.rs</code></a>）先做上面的有限差分检查，再处理两个初始条件：正常（TRIAD）和大误差（TRIAD 绕世界 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 轴转 90 度，<span><span>P0=diag⁡((π/2)2I3,10−3I3)P_0 = \operatorname{diag}((\pi/2)^2I_3,10^{-3}I_3)</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>((</span><span>π</span><span>/2</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>）。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch06</span></div></div><div><div><div>2</div></div><div><span>H checks: max|H_accel - fd| = 3.04e-10, max|H_heading - fd| = 4.73e-10</span></div></div><div><div><div>3</div></div><div><span>H checks: max|H_accel - fd| = 3.04e-10, max|H_heading - fd| = 4.73e-10</span></div></div><div><div><div>4</div></div><div><span>jacobian check: max|H_acc - fd| = 3.041e-10, max|H_mag - fd| = 4.729e-10</span></div></div><div><div><div>5</div></div><div><span>Jacobian checks: max|H_acc - fd| = 3.04e-10, max|H_mag - fd| = 4.73e-10</span></div></div><div><div><div>6</div></div><div><span>ok   eskf_large_c.csv: max diff 7.216e-16 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   eskf_large_cpp.csv: max diff 9.992e-16 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>ok   eskf_large_rs.csv: max diff 1.187e-15 &lt;= 1.0e-09</span></div></div><div><div><div>9</div></div><div><span>ok   eskf_nominal_c.csv: max diff 7.494e-16 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   eskf_nominal_cpp.csv: max diff 8.882e-16 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   eskf_nominal_rs.csv: max diff 7.442e-16 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>(其余输出在下面的各小节里引用)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三种语言的有限差分检查打印同样的最大误差，滤波输出和 Python 的最大差异都是 <span><span>10−1510^{-15}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 量级。</p><section><h3>对 EKF：功劳该记在谁头上<a href="#对-ekf功劳该记在谁头上"><span>#</span></a></h3><p>先是表格里的前三行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>method        yaw    pitch     roll    total</span></div></div><div><div><div>2</div></div><div><span>ekf         4.504    0.684    1.127    4.719</span></div></div><div><div><div>3</div></div><div><span>ekf+gate    0.524    0.506    0.746    0.804</span></div></div><div><div><div>4</div></div><div><span>eskf        0.608    0.591    0.870    0.932</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>ekf+gate</code> 是第 5 篇的 EKF，只加了一行：磁力计样本模长偏离 1 超过 0.1 就不更新。它比 ESKF 还略好（0.804 对 0.932 度）。磁干扰窗口内的横滚俯仰也一样：EKF 不设门是 3.214 度，加门是 0.068 度，ESKF 是 0.070 度。这一整段 1000 个样本全部被门挡住（磁干扰期间模长约 1.25），对 EKF 和 ESKF 一视同仁。</p><p>结论：<strong>磁干扰段的巨大改善来自”不用坏数据”，不来自误差状态也不来自航向更新。</strong> 反过来，这个对照也说明了航向更新没有害处：它在干净数据上不比三维向量更新差（平静段 EKF 0.086、ESKF 0.088 度）。ESKF 在线加速度段比 EKF 差一点（3.054 对 2.640 度），我没有深究原因。</p><p></p><figure><img src="/assets/images/kalman-filter/ch06-eskf-vs-ekf.svg" alt="EKF 与 ESKF 的总姿态误差（红色阴影：线加速度扰动；紫色阴影：磁干扰）。ESKF 的优势主要在磁干扰段" /><figcaption>EKF 与 ESKF 的总姿态误差（红色阴影：线加速度扰动；紫色阴影：磁干扰）。ESKF 的优势主要在磁干扰段</figcaption></figure><p></p><p></p><figure><img src="/assets/images/kalman-filter/ch06-mag-disturbance.svg" alt="磁干扰段的俯仰与横滚误差：不设门的 EKF 被拉偏，ESKF 不受影响" /><figcaption>磁干扰段的俯仰与横滚误差：不设门的 EKF 被拉偏，ESKF 不受影响</figcaption></figure><p></p></section><section><h3>ESKF 的好处在哪<a href="#eskf-的好处在哪"><span>#</span></a></h3><p>在这份数据上我能找到的好处有这些：</p><ul>
<li><strong><span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 满秩协方差。</strong> 没有多余自由度，没有”沿 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 方向”的幽灵特征值。状态少一维，协方差矩阵 <span><span>49→3649\to36</span><span><span><span></span><span>49</span><span></span><span>→</span><span></span></span><span><span></span><span>36</span></span></span></span> 个元素，每步的矩阵运算也就少一些，第 8 篇会量化。</li>
<li><strong>每一项的物理意义清楚。</strong> 误差是 3 维旋转向量；<span><span>Hδθ=[h]×H_{\delta\theta} = [h]_\times</span><span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>δ</span><span>θ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>h</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的形式简单，比 EKF 里 <span><span>3×43\times4</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的对四元数的雅可比好检查好调试。</li>
<li><strong>一致性。</strong> 平静段的 NEES 落在界内（见下）。</li>
</ul></section><section><h3>大初始误差：和我预期的不一样<a href="#大初始误差和我预期的不一样"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>large initial error, ESKF: initial error 90.4 deg; first time the error stays below 2 deg for 5 s: 0.02 s (nominal init: 0.00 s)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>90 deg yaw                  : initial   90.4 deg | ESKF converged at   0.02 s, EKF at   1.18 s (error at 10 s: ESKF 0.04, EKF 0.05 deg)</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>120 deg about world [1,1,0] : initial  120.3 deg | ESKF converged at   0.86 s, EKF at   0.01 s (error at 10 s: ESKF 0.18, EKF 0.04 deg)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我原本预期”EKF 在大误差下线性化会失效，ESKF 更稳”。数据说不是。90 度的偏航误差，ESKF 在 0.02 秒内就收敛了（第一个样本之后误差降到 2 度以内并保持），EKF 要 1.18 秒。道理是这个误差恰好是绕世界竖直轴的纯旋转，加速度计更新先把横滚俯仰锁死（<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 在这两个方向塌缩），剩下的不确定方向就是纯偏航，航向测量对偏航误差是精确线性的，一次更新就修正了。换成一个 120 度、绕世界 <span><span>[1,1,0][1,1,0]</span><span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span>1</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span> 的误差（横滚俯仰偏航都错），结果反转了：ESKF 要 0.86 秒，EKF 0.01 秒，10 秒时误差也是 EKF 略好（0.04 对 0.18 度）。EKF 的优势我猜是它的磁力计更新用的是整个 3 维向量，一次就同时给出了横滚俯仰偏航的信息；ESKF 的航向更新只给一个标量，得靠加速度计更新和航向更新交替几轮才把三个方向都拉回来。</p><p>所以：<strong>这份数据上没有证据表明 ESKF 在大初始误差下比 EKF 更好。</strong> 我不知道其他类型的初始误差（或更苛刻的噪声）下会怎样，这个结论只对上面两种初始误差成立。</p><p></p><figure><img src="/assets/images/kalman-filter/ch06-large-init.svg" alt="两种初始化下 ESKF 的总姿态误差（对数坐标）：90 度偏航误差在几个采样之内收敛" /><figcaption>两种初始化下 ESKF 的总姿态误差（对数坐标）：90 度偏航误差在几个采样之内收敛</figcaption></figure><p></p></section><section><h3>重置雅可比<a href="#重置雅可比"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>reset Jacobian: total RMSE with 0.93200 deg, without 0.93160 deg</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>relative change of the P diagonal: max over the run 5.20e-02, max for t &gt;= 5 s 5.20e-02</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>重置雅可比 <span><span>GG</span><span><span><span></span><span>G</span></span></span></span> 对精度的影响是 0.0004 度，可以忽略。对协方差的影响就不能说是微乎其微了，两次运行（带重置和不带重置）的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 对角元之间，最大的相对差异是 5.2%，出现在 61.75 秒，也就是线加速度扰动里，加速度计的错误数据让估计出现了较大的跳变，<span><span>δθ\delta\theta</span><span><span><span></span><span>δ</span><span>θ</span></span></span></span> 大，<span><span>GG</span><span><span><span></span><span>G</span></span></span></span> 离 <span><span>II</span><span><span><span></span><span>I</span></span></span></span> 就远。其余大部分时刻这个相对差异在 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> 量级（1 秒时 0.13%，50 秒时 0.015%，100 秒时 0.027%）。我保留 <code>reset=True</code>，因为它在大注入的时候有影响，小注入时没有副作用。</p></section><section><h3>一致性：蒙特卡洛 NEES<a href="#一致性蒙特卡洛-nees"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Monte Carlo, seeds 1..50, 6-dof NEES bounds [5.078, 6.997]</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>mean NEES over the whole run: 895.576; fraction of steps inside the bounds: 67.2%</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>calm period [20,60): mean NEES 6.048, inside 96.0%</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>excluding [60,80) (accel disturbance and its aftermath): mean NEES 9.616, inside 80.6%</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>accel disturbance [60,70): mean NEES 9399.926; mag disturbance [90,100): 7.179</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>total error RMS over 50 runs: 0.929 deg</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/kalman-filter/ch06-nees.svg" alt="50 次平均 6 自由度 NEES 的对数曲线，虚线是 95% 界。平静段落在界内，60 到 70 秒的线加速度扰动把它推到 10^4" /><figcaption>50 次平均 6 自由度 NEES 的对数曲线，虚线是 95% 界。平静段落在界内，60 到 70 秒的线加速度扰动把它推到 10^4</figcaption></figure><p></p><p>全程只有 67.2% 的步落在界内，低于我给自己设的 80%。但是平静段 <span><span>[20,60)[20,60)</span><span><span><span></span><span>[</span><span>20</span><span>,</span><span></span><span>60</span><span>)</span></span></span></span> 是 96.0%，平均 NEES 6.048，几乎正好是 6（自由度）。滤波器的噪声参数在没有干扰时是准的。失败的部分是线加速度扰动：它没有被门控挡住（第 5 篇讲过原因），也不在滤波器的模型里，加速度计把水平加速度当成了重力方向的偏转，NEES 在那 10 秒里是 9400。</p><p>常规的补救办法是重新调滤波器的 <span><span>σg\sigma_g</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>σbw\sigma_{bw}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。我试了，但这不是对症的药（<code>python3 python/ch06_eskf.py tune</code>，大约 5 分钟，不在 <code>make ch06</code> 里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ python3 python/ch06_eskf.py tune</span></div></div><div><div><div>2</div></div><div><span>6-dof NEES bounds [5.078, 6.997]; fraction of steps inside the bounds, 50 runs</span></div></div><div><div><div>3</div></div><div><span>filter noise      mean NEES  whole run  excl [60,80)  calm [20,60)</span></div></div><div><div><div>4</div></div><div><span>as simulated         895.58      67.2%         80.6%         96.0%</span></div></div><div><div><div>5</div></div><div><span>sigma_g x2          1099.03       6.9%          8.3%          0.0%</span></div></div><div><div><div>6</div></div><div><span>sigma_bw x5         1334.00       2.7%          2.3%          0.0%</span></div></div><div><div><div>7</div></div><div><span>sigma_a x3            60.80      10.7%         12.7%          6.2%</span></div></div><div><div><div>8</div></div><div><span>sigma_a x10            6.86       8.3%          8.6%          2.9%</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把陀螺噪声放大 2 倍或者把零偏随机游走放大 5 倍，平静段的落界比例从 96.0% 掉到 0.0%，滤波器变得保守；把加速度计噪声放大 10 倍，整体平均 NEES 回到 6.86，看上去”正常”了，但落界比例只有 8.3%：它在平静段变得过于保守，在扰动段还是不够。<strong>这个不一致是模型缺失造成的（没有线加速度这个状态），不是参数错了；调参只能在平静段和扰动段之间做交换。</strong> 我保持噪声参数等于仿真的真实值，不调。这也解释了为什么没有一个 <span><span>σa\sigma_a</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 能同时满足两种工况，真正的办法是给滤波器一个线加速度的模型，或者一个对它不敏感的门控，这超出了这个系列的范围。</p></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>EKF 和 ESKF 都靠线性化。下一篇是无迹卡尔曼滤波（UKF）：不求雅可比，用一小撮”sigma 点”过一遍非线性函数再统计，它能不能在这里比线性化更好？答案要用一个 6 维误差状态、12 个 sigma 点的 UKF 去验证。然后把 Mahony、EKF、ESKF、UKF 四个滤波器放在同一张表里，50 个种子，同台比较。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-05-ekf/</id>
      <title type="text">扩展卡尔曼滤波：把四元数塞进 EKF</title>
      <published>2026-10-14T04:00:00.000Z</published>
      <updated>2026-10-14T04:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-05-ekf/"/>
      <summary type="text">卡尔曼滤波系列第 5 篇：推导扩展卡尔曼滤波，用四元数加陀螺零偏的 7 维状态做三维姿态估计，手推测量雅可比并用有限差分验证，看它在平静段、线加速度扰动和磁干扰下的表现，并指出四元数协方差的结构性问题。Python、C、C++、Rust 实现。</summary>
      <content type="html"><![CDATA[<p>平静段 20 到 60 秒，上一篇的 Mahony 互补滤波器误差是 0.758 度，这一篇的扩展卡尔曼滤波器是 0.086 度，差了 8.8 倍。同一份数据，同样的传感器。但整段 120 秒算下来，两者的总均方根误差是 4.881 度对 4.719 度，几乎没分出胜负：它们共同被 90 到 100 秒的磁干扰拖到了同一个水平。</p>
<p>这一篇做的事情：把第 1、2 篇推导的滤波器推广到非线性系统，用它估计三维姿态，然后诚实地看它哪里好、哪里不够。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch05</code> 通过，<code>out/ch05/ekf_{py,c,cpp,rs}.csv</code>，四张图；C、C++、Rust 各一个四元数 EKF（<code>kl_ekf_att</code>、<code>ekf_attitude.hpp</code>、<code>ekf_att.rs</code>）和一个共用的测量更新模块（<code>kl_update</code>、<code>update.hpp</code>、<code>update.rs</code>）。雅可比的有限差分检查在每个语言的 app 里先于数据处理运行。
前置：<a href="/posts/kalman-filter-04-attitude-imu/">第 4 篇</a>（约定、四元数、IMU 仿真器、Mahony 基线）和<a href="/posts/kalman-filter-02-multivariate/">第 2 篇</a>（线性卡尔曼滤波、Joseph 形式、Cholesky）。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>非线性系统与线性化<a href="#非线性系统与线性化"><span>#</span></a></h3><p>前面的滤波器都是线性的：<span><span>xk=Fxk−1+…x_k = Fx_{k-1}+\dots</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>…</span></span></span></span>，<span><span>z=Hx+vz = Hx+v</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span>。姿态问题不是：状态方程里有 <span><span>q⊗exp⁡(⋅)q\otimes\exp(\cdot)</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>⋅</span><span>)</span></span></span></span>，测量方程里有 <span><span>C(q)TvC(q)^Tv</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>v</span></span></span></span>。设</p><span><span><span>xk+1=f(xk,uk)+wk,zk=h(xk)+vkx_{k+1} = f(x_k,u_k)+w_k,\qquad z_k = h(x_k)+v_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>h</span><span>(</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>扩展卡尔曼滤波（EKF）的想法是：在当前估计附近，把 <span><span>ff</span><span><span><span></span><span>f</span></span></span></span> 和 <span><span>hh</span><span><span><span></span><span>h</span></span></span></span> 做一阶泰勒展开，然后把展开式当成线性模型套用线性滤波器。</p><span><span><span>f(x)≈f(x^)+F(x−x^),F=∂f∂x∣x^;h(x)≈h(x^−)+H(x−x^−),H=∂h∂x∣x^−f(x)\approx f(\hat x)+F(x-\hat x),\quad F = \left.\frac{\partial f}{\partial x}\right|_{\hat x};\qquad h(x)\approx h(\hat x^-)+H(x-\hat x^-),\quad H = \left.\frac{\partial h}{\partial x}\right|_{\hat x^-}</span><span><span><span></span><span>f</span><span>(</span><span>x</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>f</span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>F</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∂</span><span>x</span></span></span><span><span></span><span></span></span><span><span></span><span><span>∂</span><span>f</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>;</span><span></span><span></span><span>h</span><span>(</span><span>x</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>h</span><span>(</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>H</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∂</span><span>x</span></span></span><span><span></span><span></span></span><span><span></span><span><span>∂</span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>结果是：</p><span><span><span>x^−=f(x^,u),P−=FPFT+Q;y=z−h(x^−),S=HP−HT+R,K=P−HTS−1\hat x^- = f(\hat x,u),\quad P^- = FPF^T+Q;\qquad y = z-h(\hat x^-),\quad S = HP^-H^T+R,\quad K = P^-H^TS^{-1}</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>f</span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>,</span><span></span><span>u</span><span>)</span><span>,</span><span></span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span>P</span><span><span>F</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Q</span><span>;</span><span></span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>h</span><span>(</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span><span>,</span><span></span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><span><span><span>x^=x^−+Ky,P=(I−KH)P−(I−KH)T+KRKT\hat x = \hat x^- + Ky,\qquad P = (I-KH)P^-(I-KH)^T+KRK^T</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>y</span><span>,</span><span></span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>R</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>注意状态和测量的”均值”用的是非线性函数本身（<span><span>f(x^)f(\hat x)</span><span><span><span></span><span>f</span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span></span></span></span>、<span><span>h(x^−)h(\hat x^-)</span><span><span><span></span><span>h</span><span>(</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span>），线性化只用在协方差和增益上。</p></section><section><h3>EKF 丢掉了什么<a href="#ekf-丢掉了什么"><span>#</span></a></h3><p>线性滤波器的最优性来自高斯分布经过线性变换仍是高斯。非线性函数不保持这个性质，所以 EKF 没有任何最优性保证。具体的损失有三项：</p><ul>
<li><strong>线性化误差</strong>：<span><span>FF</span><span><span><span></span><span>F</span></span></span></span>、<span><span>HH</span><span><span><span></span><span>H</span></span></span></span> 是在估计值处求的，不是在真值处。估计差得越远，线性模型越不准。第 6 篇的大初始误差实验会看到这一点。</li>
<li><strong>均值偏差</strong>：非线性函数把高斯变成非高斯，<span><span>f(x^)f(\hat x)</span><span><span><span></span><span>f</span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span></span></span></span> 不是均值的无偏估计（二阶项）。</li>
<li><strong>不一致</strong>：线性化的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 常常偏小，滤波器过于自信，NEES 超出界限（第 3 篇）。</li>
</ul><p>还有一个工程问题，我觉得是最危险的：<strong>雅可比写错，滤波器通常不会崩溃</strong>，它会给出一个”看着合理但略差”的结果，你可能永远不会发现。所以我强制在每个 app 里先做有限差分检查，用中心差分 <span><span>(g(x+ϵej)−g(x−ϵej))/2ϵ(g(x+\epsilon e_j)-g(x-\epsilon e_j))/2\epsilon</span><span><span><span></span><span>(</span><span>g</span><span>(</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>ϵ</span><span><span>e</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>−</span><span></span></span><span><span></span><span>g</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>ϵ</span><span><span>e</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>))</span><span>/2</span><span>ϵ</span></span></span></span> 对比解析雅可比。检查不通过，程序直接退出。</p></section><section><h3>过程模型<a href="#过程模型"><span>#</span></a></h3><p>状态 <span><span>x=[q (4), b (3)]x = [q\ (4),\ b\ (3)]</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>q</span><span> </span><span>(</span><span>4</span><span>)</span><span>,</span><span> </span><span></span><span>b</span><span> </span><span>(</span><span>3</span><span>)]</span></span></span></span>，7 维，输入是陀螺读数 <span><span>ωm\omega_m</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。预测：</p><span><span><span>q−=q⊗exp⁡((ωm−b)Δt),b−=bq^- = q\otimes\exp\big((\omega_m-b)\Delta t\big),\qquad b^- = b</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span></span><span><span>(</span></span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span><span>Δ</span><span>t</span><span><span>)</span></span><span>,</span><span></span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>b</span></span></span></span></span><p>（第 4 篇的记号：<span><span>exp⁡(v)\exp(v)</span><span><span><span></span><span>exp</span><span>(</span><span>v</span><span>)</span></span></span></span> 是旋转向量 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 对应的四元数。）</p><p><strong><span><span>FqqF_{qq}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>q</span><span>q</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</strong> 四元数乘法对 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 是线性的：<span><span>q⊗p=R(p)qq\otimes p = R(p)q</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>R</span><span>(</span><span>p</span><span>)</span><span>q</span></span></span></span>，<span><span>R(p)R(p)</span><span><span><span></span><span>R</span><span>(</span><span>p</span><span>)</span></span></span></span> 是 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 的”右乘矩阵”。展开乘法公式就是（<span><span>p=[pw,px,py,pz]p = [p_w,p_x,p_y,p_z]</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>p</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>）</p><span><span><span>R(p)=[pw−px−py−pzpxpwpz−pypy−pzpwpxpzpy−pxpw]R(p) = \begin{bmatrix}p_w&amp;-p_x&amp;-p_y&amp;-p_z\\p_x&amp;p_w&amp;p_z&amp;-p_y\\p_y&amp;-p_z&amp;p_w&amp;p_x\\p_z&amp;p_y&amp;-p_x&amp;p_w\end{bmatrix}</span><span><span><span></span><span>R</span><span>(</span><span>p</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span>p</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>p</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span>p</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>p</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span>p</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>p</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>p</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>所以 <span><span>Fqq=R(exp⁡((ωm−b)Δt))F_{qq} = R(\exp((\omega_m-b)\Delta t))</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>q</span><span>q</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>R</span><span>(</span><span>exp</span><span>((</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span><span>Δ</span><span>t</span><span>))</span></span></span></span>，是精确的，不是近似。</p><p><strong><span><span>FqbF_{qb}</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>q</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</strong> 类似地，<span><span>q⊗[0,ω]=Ξ(q)ωq\otimes[0,\omega] = \Xi(q)\omega</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>ω</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>Ξ</span><span>(</span><span>q</span><span>)</span><span>ω</span></span></span></span>，</p><span><span><span>Ξ(q)=[−x−y−zw−zyzw−x−yxw]\Xi(q) = \begin{bmatrix}-x&amp;-y&amp;-z\\w&amp;-z&amp;y\\z&amp;w&amp;-x\\-y&amp;x&amp;w\end{bmatrix}</span><span><span><span></span><span>Ξ</span><span>(</span><span>q</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>x</span></span></span><span><span></span><span><span>w</span></span></span><span><span></span><span><span>z</span></span></span><span><span></span><span><span>−</span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>y</span></span></span><span><span></span><span><span>−</span><span>z</span></span></span><span><span></span><span><span>w</span></span></span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>z</span></span></span><span><span></span><span><span>y</span></span></span><span><span></span><span><span>−</span><span>x</span></span></span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>exp⁡(ϕ)≈[1,12ϕ]\exp(\phi)\approx[1,\tfrac12\phi]</span><span><span><span></span><span>exp</span><span>(</span><span>ϕ</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>ϕ</span><span>]</span></span></span></span>（一阶），<span><span>ϕ=(ωm−b)Δt\phi = (\omega_m-b)\Delta t</span><span><span><span></span><span>ϕ</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span><span>Δ</span><span>t</span></span></span></span>，所以 <span><span>q−≈q+12Δt Ξ(q)(ωm−b)q^-\approx q + \tfrac12\Delta t\,\Xi(q)(\omega_m-b)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>q</span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>Δ</span><span>t</span><span></span><span>Ξ</span><span>(</span><span>q</span><span>)</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span></span></span></span>，对 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 求导：<span><span>Fqb=−Δt2Ξ(q)F_{qb} = -\tfrac{\Delta t}{2}\Xi(q)</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>q</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Δ</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>Ξ</span><span>(</span><span>q</span><span>)</span></span></span></span>。这个是一阶近似，不是精确的：精确的导数含 <span><span>exp⁡\exp</span><span><span><span></span><span>exp</span></span></span></span> 的精确雅可比，差一个 <span><span>O(Δt∣ϕ∣)O(\Delta t|\phi|)</span><span><span><span></span><span>O</span><span>(</span><span>Δ</span><span>t</span><span>∣</span><span>ϕ</span><span>∣</span><span>)</span></span></span></span> 的量。有限差分检查会量化它：差距是 <span><span>2.2×10−52.2\times10^{-5}</span><span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span>，我给这一项的容差是 <span><span>10−310^{-3}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p><span><span>Fbb=IF_{bb} = I</span><span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span>。</p><p><strong>过程噪声。</strong> 陀螺噪声 <span><span>ng∼N(0,σg2I)n_g\sim\mathcal N(0,\sigma_g^2I)</span><span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>I</span><span>)</span></span></span></span> 通过 <span><span>ω=ωm−b−ng\omega = \omega_m-b-n_g</span><span><span><span></span><span>ω</span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 进入。<span><span>q−q^-</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 对 <span><span>ω\omega</span><span><span><span></span><span>ω</span></span></span></span> 的灵敏度和对 <span><span>−b-b</span><span><span><span></span><span>−</span><span>b</span></span></span></span> 的一样，<span><span>Δt2Ξ(q)\tfrac{\Delta t}{2}\Xi(q)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>Δ</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>Ξ</span><span>(</span><span>q</span><span>)</span></span></span></span>，所以</p><span><span><span>Qqq=(Δt2)2Ξ(q) σg2I Ξ(q)T,Qbb=σbw2Δt IQ_{qq} = \Big(\frac{\Delta t}2\Big)^2\Xi(q)\,\sigma_g^2I\,\Xi(q)^T,\qquad Q_{bb} = \sigma_{bw}^2\Delta t\,I</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>q</span><span>q</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>(</span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>Δ</span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span><span>)</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>Ξ</span><span>(</span><span>q</span><span>)</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>I</span><span></span><span>Ξ</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span></span><span>I</span></span></span></span></span><p><span><span>QbbQ_{bb}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 就是第 2 篇的零偏随机游走。<span><span>σg=0.005\sigma_g = 0.005</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.005</span></span></span></span> rad/s 是每样本噪声，积分一步的角度噪声是 <span><span>σgΔt\sigma_g\Delta t</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span></span></span></span>，和第 1 篇的 <span><span>q=(σnΔt)2q = (\sigma_n\Delta t)^2</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 同一个意思。</p></section><section><h3>测量模型<a href="#测量模型"><span>#</span></a></h3><p>加速度计（静止时）测到的是 <span><span>−CTgw-C^T\mathbf g_w</span><span><span><span></span><span>−</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，方向是机体系下的 <span><span>C(q)T[0,0,−1]TC(q)^T[0,0,-1]^T</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。我们只用它的方向：<span><span>za=fm/∣fm∣z_a = f_m/|f_m|</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/∣</span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span>，预测 <span><span>ha(q)=C(q)T[0,0,−1]Th_a(q) = C(q)^T[0,0,-1]^T</span><span><span><span></span><span><span>h</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>q</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。单位向量的每个分量的噪声约为 <span><span>σa/g\sigma_a/g</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>g</span></span></span></span>，所以 <span><span>Ra=(σa/g)2I3R_a = (\sigma_a/g)^2I_3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>g</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p><p>磁力计同理：<span><span>zm=mm/∣mm∣z_m = m_m/|m_m|</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/∣</span><span><span>m</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span>，<span><span>hm(q)=C(q)Tmwh_m(q) = C(q)^Tm_w</span><span><span><span></span><span><span>h</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>q</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>Rm=σm2I3R_m = \sigma_m^2I_3</span><span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>m</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（磁场本身就是单位长度的）。</p><p><strong>加速度计门控。</strong> 线性加速度会让 <span><span>fmf_m</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 偏离重力。简单的检测：比力的大小应该接近 <span><span>gg</span><span><span><span></span><span>g</span></span></span></span>，当 <span><span>∣∣fm∣−g∣&gt;0.5\big||f_m|-g\big|&gt;0.5</span><span><span><span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>−</span><span></span></span><span><span></span><span>g</span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0.5</span></span></span></span> m/s² 时跳过这次加速度计更新。这个门限的效果我在结果里给出，会有意外。</p><p><strong>磁力计在本篇不设门。</strong> 磁干扰窗口故意让磁力计的数据是错的，EKF 照单全收，这个后果留给第 6 篇修。</p></section><section><h3>测量雅可比<a href="#测量雅可比"><span>#</span></a></h3><p>需要 <span><span>∂h/∂q\partial h/\partial q</span><span><span><span></span><span>∂</span><span>h</span><span>/</span><span>∂</span><span>q</span></span></span></span>，<span><span>h(q)=C(q)Tvh(q) = C(q)^Tv</span><span><span><span></span><span>h</span><span>(</span><span>q</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>v</span></span></span></span>，<span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 是常向量。第 4 篇推出过 <span><span>C(q)v=(w2−u⋅u)v+2(u⋅v)u+2w(u×v)C(q)v = (w^2-u\cdot u)v+2(u\cdot v)u+2w(u\times v)</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>u</span><span>)</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span>u</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>w</span><span>(</span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>)</span></span></span></span>。转置对应共轭 <span><span>q∗q^*</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>，也就是 <span><span>u→−uu\to-u</span><span><span><span></span><span>u</span><span></span><span>→</span><span></span></span><span><span></span><span>−</span><span>u</span></span></span></span>：</p><span><span><span>C(q)Tv=(w2−u⋅u) v+2(u⋅v) u−2w (u×v)C(q)^Tv = (w^2-u\cdot u)\,v + 2(u\cdot v)\,u - 2w\,(u\times v)</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>u</span><span>)</span><span></span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span></span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>w</span><span></span><span>(</span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>)</span></span></span></span></span><p>这个多项式形式对任意 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span>（不限单位长度）都有定义，是 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 的二次齐次函数，我下面求导和做有限差分都用它（实现里的 <code>rot_inv</code>）。对 <span><span>ww</span><span><span><span></span><span>w</span></span></span></span>：</p><span><span><span>∂h∂w=2wv−2(u×v)\frac{\partial h}{\partial w} = 2wv-2(u\times v)</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∂</span><span>w</span></span></span><span><span></span><span></span></span><span><span></span><span><span>∂</span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>w</span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>(</span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>)</span></span></span></span></span><p>对 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>（逐项）：<span><span>∂[(w2−u⋅u)v]/∂u=−2vuT\partial[(w^2-u\cdot u)v]/\partial u = -2vu^T</span><span><span><span></span><span>∂</span><span>[(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>u</span><span>)</span><span>v</span><span>]</span><span>/</span><span>∂</span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span><span>v</span><span><span>u</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>；<span><span>∂[2(u⋅v)u]/∂u=2uvT+2(u⋅v)I\partial[2(u\cdot v)u]/\partial u = 2uv^T+2(u\cdot v)I</span><span><span><span></span><span>∂</span><span>[</span><span>2</span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span>u</span><span>]</span><span>/</span><span>∂</span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>u</span><span><span>v</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span>I</span></span></span></span>；<span><span>∂[−2w(u×v)]/∂u\partial[-2w(u\times v)]/\partial u</span><span><span><span></span><span>∂</span><span>[</span><span>−</span><span>2</span><span>w</span><span>(</span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>)]</span><span>/</span><span>∂</span><span>u</span></span></span></span>，因为 <span><span>u×v=−[v]×uu\times v = -[v]_\times u</span><span><span><span></span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>[</span><span>v</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>u</span></span></span></span>，是 <span><span>+2w[v]×+2w[v]_\times</span><span><span><span></span><span>+</span><span>2</span><span>w</span><span>[</span><span>v</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。合起来：</p><span><span><span>∂h∂u=2(u⋅v)I+2uvT−2vuT+2w[v]×\frac{\partial h}{\partial u} = 2(u\cdot v)I+2uv^T-2vu^T+2w[v]_\times</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∂</span><span>u</span></span></span><span><span></span><span></span></span><span><span></span><span><span>∂</span><span>h</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>u</span><span><span>v</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>v</span><span><span>u</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>w</span><span>[</span><span>v</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>H=[∂h/∂q  03×3]H = [\partial h/\partial q\ \ 0_{3\times3}]</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>∂</span><span>h</span><span>/</span><span>∂</span><span>q</span><span> </span><span> </span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>3</span><span>×</span><span>3</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>，<span><span>3×73\times7</span><span><span><span></span><span>3</span><span></span><span>×</span><span></span></span><span><span></span><span>7</span></span></span></span>。有限差分检查：100 个确定性的状态，两个参考向量（重力方向和磁场），<span><span>max⁡∣H−Hfd∣=2.25×10−10\max|H-H_{fd}| = 2.25\times10^{-10}</span><span><span><span></span><span>max</span><span></span><span>∣</span><span>H</span><span></span><span>−</span><span></span></span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>f</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>2.25</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，远小于 <span><span>10−610^{-6}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>6</span></span></span></span></span></span></span></span></span></span></span></span> 的门槛。</p></section><section><h3>归一化和协方差<a href="#归一化和协方差"><span>#</span></a></h3><p>状态里的 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 要保持单位长度，每次更新之后做 <span><span>q←q/∣q∣q\leftarrow q/|q|</span><span><span><span></span><span>q</span><span></span><span>←</span><span></span></span><span><span></span><span>q</span><span>/∣</span><span>q</span><span>∣</span></span></span></span>。但是 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 里 <span><span>4×44\times4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的块没有被相应处理，这是个结构性问题：真实的姿态不确定度只有 3 个自由度，而 <span><span>PqqP_{qq}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>q</span><span>q</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>4×44\times4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的，它的第四个方向（沿着 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 本身）在物理上没有意义。后面的数据里可以看到它的特征值。这是第 6 篇换成误差状态表示的主要动机。</p></section><section><h3>初值<a href="#初值"><span>#</span></a></h3><p><span><span>q0q_0</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 取第一个样本的 TRIAD，<span><span>b0=0b_0 = 0</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>P0=diag⁡(0.01I4, 10−3I3)P_0 = \operatorname{diag}(0.01I_4,\ 10^{-3}I_3)</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>0.01</span><span><span>I</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span><span><span>I</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。噪声参数用仿真里的真实值：<span><span>σg=0.005\sigma_g = 0.005</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.005</span></span></span></span>，<span><span>σbw=10−4\sigma_{bw} = 10^{-4}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>σa=0.05\sigma_a = 0.05</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.05</span></span></span></span>，<span><span>σm=0.01\sigma_m = 0.01</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span>。</p></section></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><section><h3>共用的测量更新<a href="#共用的测量更新"><span>#</span></a></h3><p>三个非线性滤波器（EKF、ESKF、UKF）都需要同一个动作：给定新息 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span>、<span><span>HH</span><span><span><span></span><span>H</span></span></span></span>、<span><span>RR</span><span><span><span></span><span>R</span></span></span></span>，算增益、算 Joseph 形式的新 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span>、返回修正量 <span><span>dx=Kydx = Ky</span><span><span><span></span><span>d</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>K</span><span>y</span></span></span></span>。我把它单独拿出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Measurement update shared by the nonlinear filters (chapters 5 to 7)."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div><span>from</span><span> scipy.linalg </span><span>import</span><span> cho_factor, cho_solve</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>def</span><span> </span><span>joseph_update</span><span>(</span><span>P</span><span>,</span><span><span> </span><span>H</span></span><span>,</span><span><span> </span><span>R</span></span><span>,</span><span><span> </span><span>y</span></span><span>):</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"""Joseph-form covariance update for a given innovation y.</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>Returns (dx, P_new, S) with dx = K y, K^T = S^-1 H P, S = H P H^T + R.</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>The caller decides what dx means: an additive correction (EKF) or an error</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>state to inject (ESKF). Raises LinAlgError if S is not positive definite.</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> P.shape[</span><span>0</span><span>]</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> H </span><span>@</span><span> P </span><span>@</span><span> H.T </span><span>+</span><span> R</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> </span><span>cho_factor</span><span>(S, </span><span>lower</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>K </span><span>=</span><span> </span><span>cho_solve</span><span>(c, H </span><span>@</span><span> P).T</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>A </span><span>=</span><span> np.</span><span>eye</span><span>(n) </span><span>-</span><span> K </span><span>@</span><span> H</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>P_new </span><span>=</span><span> A </span><span>@</span><span> P </span><span>@</span><span> A.T </span><span>+</span><span> K </span><span>@</span><span> R </span><span>@</span><span> K.T</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span><span> K </span><span>@</span><span> y, </span></span><span>0.5</span><span><span> </span><span>*</span><span> (P_new </span><span>+</span><span> P_new.T), S</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>返回的是 <span><span>dxdx</span><span><span><span></span><span>d</span><span>x</span></span></span></span> 而不是更新后的状态，因为 EKF 把它加到状态上，误差状态滤波器（第 6 篇）要把它”注入”名义状态。<code>P_new</code> 是对称化过的。</p></section><section><h3>EKF<a href="#ekf"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Multiplicative-quaternion EKF: state x = [q (4), b (3)], input = gyro."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>from</span><span> .imu_sim </span><span>import</span><span> G</span></div></div><div><div><div>6</div></div><div><span>from</span><span> .quat </span><span>import</span><span> qexp, qmul, qnormalize</span></div></div><div><div><div>7</div></div><div><span>from</span><span> .update </span><span>import</span><span> joseph_update</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>def</span><span> </span><span>rot_inv</span><span>(</span><span>q</span><span>,</span><span><span> </span><span>v</span></span><span>):</span></div></div><div><div><div>11</div></div><div><span>    </span><span>"""C(q)^T v written as a polynomial in q: (w^2 - u.u) v + 2 (u.v) u - 2 w (u x v).</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>Equal to qrot(q).T @ v for unit q, and defined (and differentiable) for any q.</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>w, u </span><span>=</span><span> q[</span><span>0</span><span>], q[</span><span>1</span><span>:]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span><span> (w </span><span>*</span><span> w </span><span>-</span><span> u </span><span>@</span><span> u) </span><span>*</span><span> v </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> (u </span><span>@</span><span> v) </span><span>*</span><span> u </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> w </span><span>*</span><span> np.</span><span>cross</span><span>(u, v)</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>def</span><span> </span><span>h_jac</span><span>(</span><span>q</span><span>,</span><span><span> </span><span>v</span></span><span>):</span></div></div><div><div><div>20</div></div><div><span>    </span><span>"""d rot_inv(q, v) / d q, a 3x4 matrix [d/dw | d/du]."""</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>w, u </span><span>=</span><span> q[</span><span>0</span><span>], q[</span><span>1</span><span>:]</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>dw </span><span>=</span><span> </span><span>2</span><span><span> </span><span>*</span><span> w </span><span>*</span><span> v </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>cross</span><span>(u, v)</span></span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>skew_v </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>0.0</span><span><span>, </span><span>-</span><span>v[</span></span><span>2</span><span>], v[</span><span>1</span><span>]], [v[</span><span>2</span><span>], </span><span>0.0</span><span><span>, </span><span>-</span><span>v[</span></span><span>0</span><span><span>]], [</span><span>-</span><span>v[</span></span><span>1</span><span>], v[</span><span>0</span><span>], </span><span>0.0</span><span>]])</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>du </span><span>=</span><span> </span><span>2</span><span><span> </span><span>*</span><span> (u </span><span>@</span><span> v) </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span><span>) </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>outer</span><span>(u, v) </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> np.</span><span>outer</span><span>(v, u) </span><span>+</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> w </span><span>*</span><span> skew_v</span></span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>column_stack</span><span>([dw, du])</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>def</span><span> </span><span>xi</span><span>(</span><span>q</span><span>):</span></div></div><div><div><div>29</div></div><div><span>    </span><span>"""4x3 matrix with q (x) [0, omega] = xi(q) omega."""</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>w, x, y, z </span><span>=</span><span> q</span></div></div><div><div><div>31</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([[</span><span>-</span><span>x, </span><span>-</span><span>y, </span><span>-</span><span>z], [w, </span><span>-</span><span>z, y], [z, w, </span><span>-</span><span>x], [</span><span>-</span><span>y, x, w]])</span></span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>def</span><span> </span><span>right_mul</span><span>(</span><span>p</span><span>):</span></div></div><div><div><div>35</div></div><div><span>    </span><span>"""4x4 matrix with q (x) p = right_mul(p) q."""</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>pw, px, py, pz </span><span>=</span><span> p</span></div></div><div><div><div>37</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([</span></span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>[pw, </span><span>-</span><span>px, </span><span>-</span><span>py, </span><span>-</span><span>pz],</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>[px, pw, pz, </span><span>-</span><span>py],</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>[py, </span><span>-</span><span>pz, pw, px],</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>[pz, py, </span><span>-</span><span>px, pw],</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>])</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>class</span><span><span> </span><span>QuatEKF</span></span><span>:</span></div></div><div><div><div>46</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>sigma_g</span></span><span>,</span><span><span> </span><span>sigma_bw</span></span><span>,</span><span><span> </span><span>sigma_a</span></span><span>,</span><span><span> </span><span>sigma_m</span></span><span>,</span><span><span> </span><span>m_w</span></span><span>,</span><span><span> </span><span>q0</span></span><span>,</span><span><span> </span><span>P0</span></span><span>,</span><span><span> </span><span>accel_gate</span></span><span>=</span><span>0.5</span><span>):</span></div></div><div><div><div>47</div></div><div><span>        </span><span>self</span><span>.sg, </span><span>self</span><span>.sbw, </span><span>self</span><span>.sa, </span><span>self</span><span><span>.sm </span><span>=</span><span> sigma_g, sigma_bw, sigma_a, sigma_m</span></span></div></div><div><div><div>48</div></div><div><span>        </span><span>self</span><span><span>.m_w </span><span>=</span><span> np.</span><span>asarray</span><span>(m_w, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>49</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> np.</span><span>asarray</span><span>(q0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>50</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> np.</span><span>zeros</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>51</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> np.</span><span>asarray</span><span>(P0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>52</div></div><div><span>        </span><span>self</span><span><span>.accel_gate </span><span>=</span><span> accel_gate  </span></span><span># m/s^2 on | |f| - g |; None disables gating</span></div></div><div><div><div>53</div></div><div><span>        </span><span>self</span><span><span>.last_nis </span><span>=</span><span> np.nan</span></span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>    </span><span>def</span><span> </span><span>predict</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>gyro</span></span><span>,</span><span><span> </span><span>dt</span></span><span>):</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>omega </span><span>=</span><span> gyro </span><span>-</span><span> </span><span>self</span><span>.b</span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>dq </span><span>=</span><span> </span><span>qexp</span><span>(omega </span><span>*</span><span> dt)</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>X </span><span>=</span><span> </span><span>xi</span><span>(</span><span>self</span><span>.q)</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>F </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>7</span><span>)</span></div></div><div><div><div>60</div></div><div><span><span>        </span></span><span>F[:</span><span>4</span><span>, :</span><span>4</span><span><span>] </span><span>=</span><span> </span><span>right_mul</span><span>(dq)</span></span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>F[:</span><span>4</span><span>, </span><span>4</span><span><span>:] </span><span>=</span><span> </span><span>-</span></span><span>0.5</span><span><span> </span><span>*</span><span> dt </span><span>*</span><span> X</span></span></div></div><div><div><div>62</div></div><div><span><span>        </span></span><span>Q </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>7</span><span>, </span><span>7</span><span>))</span></div></div><div><div><div>63</div></div><div><span><span>        </span></span><span>Q[:</span><span>4</span><span>, :</span><span>4</span><span><span>] </span><span>=</span><span> (</span></span><span>0.5</span><span><span> </span><span>*</span><span> dt) </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> </span></span><span>self</span><span><span>.sg</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> X </span><span>@</span><span> X.T</span></span></div></div><div><div><div>64</div></div><div><span><span>        </span></span><span>Q[</span><span>4</span><span>:, </span><span>4</span><span><span>:] </span><span>=</span><span> </span></span><span>self</span><span><span>.sbw</span><span>**</span></span><span>2</span><span><span> </span><span>*</span><span> dt </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>65</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span><span>qmul</span><span>(</span></span><span>self</span><span>.q, dq)</span></div></div><div><div><div>66</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> F </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> F.T </span><span>+</span><span> Q</span></span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>    </span><span>def</span><span> </span><span>_update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>z</span></span><span>,</span><span><span> </span><span>v</span></span><span>,</span><span><span> </span><span>r</span></span><span>):</span></div></div><div><div><div>69</div></div><div><span><span>        </span></span><span>H </span><span>=</span><span> np.</span><span>zeros</span><span>((</span><span>3</span><span>, </span><span>7</span><span>))</span></div></div><div><div><div>70</div></div><div><span><span>        </span></span><span>H[:, :</span><span>4</span><span><span>] </span><span>=</span><span> </span><span>h_jac</span><span>(</span></span><span>self</span><span>.q, v)</span></div></div><div><div><div>71</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> z </span><span>-</span><span> </span><span>rot_inv</span><span>(</span><span>self</span><span>.q, v)</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>dx, </span><span>self</span><span><span>.P, S </span><span>=</span><span> </span><span>joseph_update</span><span>(</span></span><span>self</span><span><span>.P, H, r </span><span>*</span><span> np.</span><span>eye</span><span>(</span></span><span>3</span><span>), y)</span></div></div><div><div><div>73</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span><span>qnormalize</span><span>(</span></span><span>self</span><span><span>.q </span><span>+</span><span> dx[:</span></span><span>4</span><span>])</span></div></div><div><div><div>74</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> </span></span><span>self</span><span><span>.b </span><span>+</span><span> dx[</span></span><span>4</span><span>:]</span></div></div><div><div><div>75</div></div><div><span>        </span><span>self</span><span><span>.last_nis </span><span>=</span><span> </span></span><span>float</span><span><span>(y </span><span>@</span><span> np.linalg.</span><span>solve</span><span>(S, y))</span></span></div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span>    </span><span>def</span><span> </span><span>update_accel</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>accel</span></span><span>):</span></div></div><div><div><div>78</div></div><div><span>        </span><span>"""Gravity-direction update. Returns False when gated out."""</span></div></div><div><div><div>79</div></div><div><span><span>        </span></span><span>norm </span><span>=</span><span> np.</span><span>sqrt</span><span>(accel </span><span>@</span><span> accel)</span></div></div><div><div><div>80</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.accel_gate </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>abs</span><span><span>(norm </span><span>-</span><span> G) </span><span>&gt;</span><span> </span></span><span>self</span><span>.accel_gate:</span></div></div><div><div><div>81</div></div><div><span>            </span><span>return</span><span> </span><span>False</span></div></div><div><div><div>82</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_update</span><span>(accel </span><span>/</span><span> norm, np.</span><span>array</span><span>([</span></span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>]), (</span><span>self</span><span><span>.sa </span><span>/</span><span> G) </span><span>**</span><span> </span></span><span>2</span><span>)</span></div></div><div><div><div>83</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div><div><div><div>84</div></div><div>
</div></div><div><div><div>85</div></div><div><span>    </span><span>def</span><span> </span><span>update_mag</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>mag</span></span><span>):</span></div></div><div><div><div>86</div></div><div><span>        </span><span>self</span><span><span>.</span><span>_update</span><span>(mag </span><span>/</span><span> np.</span><span>sqrt</span><span>(mag </span><span>@</span><span> mag), </span></span><span>self</span><span>.m_w, </span><span>self</span><span><span>.sm</span><span>**</span></span><span>2</span><span>)</span></div></div><div><div><div>87</div></div><div><span>        </span><span>return</span><span> </span><span>True</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>rot_inv</code>、<code>h_jac</code>、<code>xi</code>、<code>right_mul</code> 是上面四个矩阵的逐字翻译。<code>predict</code> 里 <code>self.q = qmul(self.q, dq)</code> 没有归一化，<code>_update</code> 里才归一化：单位四元数乘单位四元数，舍入误差每步 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级，用 <code>qnormalize</code> 管一管。<code>update_accel</code> 的返回值告诉你这次有没有被门控掉。</p><p>驱动脚本里每一步依次做三件事：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>run_ekf</span><span>(</span><span>gyro</span><span>,</span><span><span> </span><span>acc</span></span><span>,</span><span><span> </span><span>mag</span></span><span>,</span><span><span> </span><span>accel_gate</span></span><span>=</span><span>0.5</span><span>,</span><span><span> </span><span>keep_final</span></span><span>=</span><span>False</span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(gyro)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>f </span><span>=</span><span> </span><span>make_ekf</span><span>(</span><span>triad</span><span>(acc[</span><span>0</span><span>], mag[</span><span>0</span><span><span>], </span><span>MAG_W</span><span>), accel_gate)</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>out </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>14</span><span>))</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>out[</span><span>0</span><span><span>] </span><span>=</span><span> [</span><span>*</span><span>f.q, </span><span>*</span><span>f.b, </span><span>*</span><span>np.</span><span>diag</span><span>(f.P)]</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, n):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>f.</span><span>predict</span><span>(gyro[k], </span><span>DT</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>f.</span><span>update_accel</span><span>(acc[k])</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>f.</span><span>update_mag</span><span>(mag[k])</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>out[k] </span><span>=</span><span> [</span><span>*</span><span>f.q, </span><span>*</span><span>f.b, </span><span>*</span><span>np.</span><span>diag</span><span>(f.P)]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> (out, f) </span><span>if</span><span> keep_final </span><span>else</span><span> out</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><section><h3>共用的测量更新<a href="#共用的测量更新-1"><span>#</span></a></h3><p>三个语言的共用更新，各自约 25 行。C 版：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_update.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>int</span><span> </span><span>kl_joseph_update</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>y</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span></span></div></div><div><div><div>6</div></div><div><span><span>         </span></span><span>kl_real </span><span>*</span><span><span>dx</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>7</div></div><div><span>{</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>HP </span><span>=</span><span> work;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>S </span><span>=</span><span> HP </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>L </span><span>=</span><span> S </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Kt </span><span>=</span><span> L </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> Kt </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>A </span><span>=</span><span> K </span><span>+</span><span> n </span><span>*</span><span> m;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> A </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>KR </span><span>=</span><span> U </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>  </span><span>kl_mat_mul</span><span>(H, P, HP, m, n, n);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(HP, H, T, m, n, m);</span></div></div><div><div><div>20</div></div><div><span>  </span><span>kl_mat_add</span><span>(T, R, S, m, m);</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>  </span><span>kl_mat_copy</span><span>(S, L, m, m);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(L, m) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>25</div></div><div><span>  </span><span>kl_mat_copy</span><span>(HP, Kt, m, n);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>kl_chol_solve</span><span>(L, Kt, m, n);</span></div></div><div><div><div>27</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Kt, K, m, n);</span></div></div><div><div><div>28</div></div><div><span>  </span><span>if</span><span> (S_out)</span></div></div><div><div><div>29</div></div><div><span>    </span><span>kl_mat_copy</span><span>(S, S_out, m, m);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, y, dx, n, m, </span><span>1</span><span>);</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>/* A = I - K H; P = A P A^T + K R K^T */</span></div></div><div><div><div>33</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, H, T, n, m, n);</span></div></div><div><div><div>34</div></div><div><span>  </span><span>kl_mat_identity</span><span>(U, n);</span></div></div><div><div><div>35</div></div><div><span>  </span><span>kl_mat_sub</span><span>(U, T, A, n, n);</span></div></div><div><div><div>36</div></div><div><span>  </span><span>kl_mat_mul</span><span>(A, P, T, n, n, n);</span></div></div><div><div><div>37</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(T, A, U, n, n, n);</span></div></div><div><div><div>38</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, R, KR, n, m, m);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(KR, K, T, n, m, n);</span></div></div><div><div><div>40</div></div><div><span>  </span><span>kl_mat_add</span><span>(U, T, P, n, n);</span></div></div><div><div><div>41</div></div><div><span>  </span><span>kl_mat_symmetrize</span><span>(P, n);</span></div></div><div><div><div>42</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C++ 和 Rust 版本的区别：C++ 通过输出引用 <code>dx</code> 返回修正量，返回 <code>bool</code>；Rust 返回 <code>Option&lt;(dx, S)&gt;</code>，<code>None</code> 表示 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 不正定，此时 <code>P</code> 保持不变。</p>
完整源码：c/include/kl/kl_update.h、cpp/include/kl/update.hpp、rust/src/update.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Measurement update shared by the nonlinear filters. */</span></div></div><div><div><div>2</div></div><div><span>#ifndef</span><span> </span><span>KL_UPDATE_H</span></div></div><div><div><div>3</div></div><div><span>#define</span><span> </span><span>KL_UPDATE_H</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/* Workspace (kl_real count) of kl_joseph_update, laid out as</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>*   HP m*n | S m*m | L m*m | Kt m*n | K n*m | A n*n | T n*n | U n*n | KR n*m */</span></div></div><div><div><div>9</div></div><div><span>#define</span><span> </span><span>KL_JOSEPH_WORK_LEN</span><span><span>(</span><span>n</span><span>, </span><span>m</span><span>) (</span></span><span>4</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (n) </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (m) </span><span>+</span><span> </span><span>3</span><span> </span><span>*</span><span> (n) </span><span>*</span><span> (n))</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>/* Joseph-form covariance update for a given innovation y (m):</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>* S = H P H^T + R, K^T = S^-1 H P (Cholesky), dx = K y,</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>* P &lt;- A P A^T + K R K^T with A = I - K H, then symmetrized.</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>* The caller decides what dx means: an additive correction (EKF) or an error state to inject.</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>* P is n x n (in place), H m x n, R m x m. S_out (m x m) may be NULL.</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>* Returns -1 if S is not positive definite (P is then unchanged), else 0. */</span></div></div><div><div><div>17</div></div><div><span>int</span><span> </span><span>kl_joseph_update</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>y</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span></span></div></div><div><div><div>18</div></div><div><span><span>         </span></span><span>kl_real </span><span>*</span><span><span>dx</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>work</span><span>);</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Measurement update shared by the nonlinear filters (chapters 5 to 7).</span></div></div><div><div><div>2</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/mat.hpp"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// Joseph-form covariance update for a given innovation y.</span></div></div><div><div><div>8</div></div><div><span>// Computes dx = K y with K^T = S^-1 H P and S = H P H^T + R, and updates P in place.</span></div></div><div><div><div>9</div></div><div><span>// The caller decides what dx means: an additive correction (EKF) or an error state</span></div></div><div><div><div>10</div></div><div><span>// to inject (ESKF). Returns false, leaving P and dx unchanged, if S is not positive</span></div></div><div><div><div>11</div></div><div><span>// definite.</span></div></div><div><div><div>12</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, std::</span><span>size_t</span><span> </span><span>M</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span>bool</span><span> </span><span>joseph_update</span><span>(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>H</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>R</span><span>,</span></span></div></div><div><div><div>14</div></div><div><span>       </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>y</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>dx</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>*</span><span><span> </span><span>S_out</span><span> </span></span><span>=</span><span> </span><span>nullptr</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>HP</span><span> </span><span>=</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>16</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span> </span><span>=</span><span> </span><span>HP</span><span> </span><span>*</span><span> </span><span>H</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>R</span><span>;</span></div></div><div><div><div>17</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>S</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>K</span><span> </span><span>=</span><span> </span><span>chol_solve</span><span>(</span><span>L</span><span>, </span><span>HP</span><span>).</span><span>transpose</span><span>();</span></div></div><div><div><div>20</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>A</span><span> </span><span>=</span><span> Mat&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>H</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>P</span><span> </span><span>=</span><span> (</span><span>A</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>A</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>R</span><span> </span><span>*</span><span> </span><span>K</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>22</div></div><div><span>  </span><span>dx</span><span> </span><span>=</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>23</div></div><div><span>  </span><span>if</span><span> (</span><span>S_out</span><span>) </span><span>*</span><span>S_out</span><span> </span><span>=</span><span> </span><span>S</span><span>;</span></div></div><div><div><div>24</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Measurement update shared by the nonlinear filters.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::{</span></span><span>Mat</span><span>, chol_solve};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>/// Joseph-form covariance update for a given innovation `y`.</span></div></div><div><div><div>6</div></div><div><span>/// S = H P H^T + R, K^T = S^-1 (H P) by Cholesky solve. Returns `dx = K y`</span></div></div><div><div><div>7</div></div><div><span>/// and replaces `p` with A P A^T + K R K^T, symmetrized, A = I - K H.</span></div></div><div><div><div>8</div></div><div><span>/// The caller decides what `dx` means: an additive correction (EKF) or an</span></div></div><div><div><div>9</div></div><div><span>/// error state to inject (ESKF). Returns `None` (P unchanged) if S is not</span></div></div><div><div><div>10</div></div><div><span>/// positive definite.</span></div></div><div><div><div>11</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>joseph_update</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>12</div></div><div><span>    </span><span>p</span><span>: &amp;</span><span>mut</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>h</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>r</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>y</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>16</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;)&gt; {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>hp</span><span><span> </span><span>=</span><span> *</span></span><span>h</span><span> * *</span><span>p</span><span>;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>hp</span><span> * </span><span>h</span><span>.</span><span>transpose</span><span>() + *</span><span>r</span><span>;</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>l</span><span><span> </span><span>=</span><span> </span></span><span>s</span><span>.</span><span>cholesky</span><span>()?;</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>chol_solve</span><span>(&amp;</span><span>l</span><span>, &amp;</span><span>hp</span><span>).</span><span>transpose</span><span>();</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>() - </span><span>k</span><span> * *</span><span>h</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>*</span><span>p</span><span><span> </span><span>=</span><span> (</span></span><span>a</span><span> * *</span><span>p</span><span> * </span><span>a</span><span>.</span><span>transpose</span><span>() + </span><span>k</span><span> * *</span><span>r</span><span> * </span><span>k</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>23</div></div><div><span>    </span><span>Some</span><span>((</span><span>k</span><span> * *</span><span>y</span><span>, </span><span>s</span><span>))</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>C<a href="#c"><span>#</span></a></h3><p>C 版的滤波器状态是一个结构体，所有数组都是定长的，包括一块 280 个元素的内联工作区：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>kl_real </span><span>q</span><span>[</span><span>4</span><span>], </span><span>b</span><span>[</span><span>3</span><span>], </span><span>P</span><span>[</span><span>49</span><span>];</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>kl_real sg, sbw, sa, sm;</span><span> /* gyro, bias random walk, accel, mag noise */</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>m_w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>kl_real accel_gate;</span><span> /* m/s^2 on | |f| - g |; negative disables gating */</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>kl_real </span><span>work</span><span>[</span><span>280</span><span>];</span><span>  /* scratch: predict needs 196, accel/mag updates 30 + KL_JOSEPH_WORK_LEN(7, 3) = 279 */</span></div></div><div><div><div>7</div></div><div><span>} kl_ekf_att;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有 <code>malloc</code>，没有”调用者要提供缓冲区”的负担，代价是每个 <code>kl_ekf_att</code> 对象带着一块不一定用满的工作区：<code>predict</code> 用到 196 个元素，两个测量更新用到 30 加上 <code>KL_JOSEPH_WORK_LEN(7, 3)</code> 共 279 个，所以是 280。（这个数字我第一次写的是 256，更新步骤会越界写 23 个元素，覆盖紧挨着结构体的下一个变量。第 5 篇的 app 里它恰好没有踩到任何有用的东西，输出还是和 Python 对得上；到了第 8 篇，同一份代码链接进另一个程序，邻居变成了一个全局常量，结果变成了 NaN，我才发现。这是 C 里没有边界检查的代价，也是为什么 C++ 和 Rust 版本没有这个问题：矩阵尺寸是类型的一部分。）预测：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>kl_ekf_att_predict</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>F </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>Q </span><span>=</span><span> F </span><span>+</span><span> </span><span>49</span><span>, </span><span>*</span><span>T </span><span>=</span><span> Q </span><span>+</span><span> </span><span>49</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>49</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>omega</span><span>[</span><span>3</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>X</span><span>[</span><span>12</span><span>], </span><span>Rm</span><span>[</span><span>16</span><span>], </span><span>Xs</span><span>[</span><span>12</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>5</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>omega</span><span>[i] </span><span>=</span><span> (</span><span>gyro</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i]) </span><span>*</span><span> dt;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>kl_quat_exp</span><span>(omega, dq);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>kl_ekf_att_xi</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, X);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>kl_ekf_att_right_mul</span><span>(dq, Rm);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>/* F = [[right_mul(dq), -dt/2 Xi], [0, I]] */</span></div></div><div><div><div>12</div></div><div><span>  </span><span>kl_mat_identity</span><span>(F, </span><span>7</span><span>);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>4</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>15</div></div><div><span>      </span><span>F</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>Rm</span><span>[i </span><span>*</span><span> </span><span>4</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>17</div></div><div><span>      </span><span>F</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> </span><span>4</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>-</span><span>(kl_real)</span><span>0.5</span><span> </span><span>*</span><span> dt </span><span>*</span><span> </span><span>X</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>/* Q = blkdiag((dt/2)^2 sg^2 Xi Xi^T, sbw^2 dt I) */</span></div></div><div><div><div>20</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>49</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>Q</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>kl_mat_scale</span><span>(X, ((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> dt) </span><span>*</span><span> ((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> dt) </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span>, Xs, </span><span>4</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(Xs, X, Rm, </span><span>4</span><span>, </span><span>3</span><span>, </span><span>4</span><span>);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>4</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>26</div></div><div><span>      </span><span>Q</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>Rm</span><span>[i </span><span>*</span><span> </span><span>4</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>27</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>4</span><span>; i </span><span>&lt;</span><span> </span><span>7</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>Q</span><span>[i </span><span>*</span><span> </span><span>8</span><span>] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> dt;</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>32</div></div><div><span>  </span><span>kl_mat_mul</span><span>(F, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>7</span><span>, </span><span>7</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>33</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(T, F, U, </span><span>7</span><span>, </span><span>7</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>34</div></div><div><span>  </span><span>kl_mat_add</span><span>(U, Q, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>7</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>更新（加速度计和磁力计共用一个内部函数，差别只在 <code>z</code>、<code>v</code>、<code>r</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* H = [dh/dq | 0], y = z - h(q); the correction dx is added to the state. */</span></div></div><div><div><div>2</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>update</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>z</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>r</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>H </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>R </span><span>=</span><span> H </span><span>+</span><span> </span><span>21</span><span>, </span><span>*</span><span>jw </span><span>=</span><span> R </span><span>+</span><span> </span><span>9</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>kl_real </span><span>Hq</span><span>[</span><span>12</span><span>], </span><span>hq</span><span>[</span><span>3</span><span>], </span><span>y</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[</span><span>7</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>6</div></div><div><span>  </span><span>kl_ekf_att_h_jac</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, v, Hq);</span></div></div><div><div><div>7</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>7</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>9</div></div><div><span>      </span><span>H</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> j] </span><span>=</span><span> j </span><span>&lt;</span><span> </span><span>4</span><span> </span><span>?</span><span> </span><span>Hq</span><span>[i </span><span>*</span><span> </span><span>4</span><span> </span><span>+</span><span> j] </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>10</div></div><div><span>  </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, v, hq);</span></div></div><div><div><div>11</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>y</span><span>[i] </span><span>=</span><span> </span><span>z</span><span>[i] </span><span>-</span><span> </span><span>hq</span><span>[i];</span></div></div><div><div><div>13</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>14</div></div><div><span>      </span><span>R</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>?</span><span> r </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_joseph_update</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, H, R, y, </span><span>7</span><span>, </span><span>3</span><span>, dx, </span><span>NULL</span><span>, jw) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>qn</span><span>[i] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>q</span><span>[i] </span><span>+</span><span> </span><span>dx</span><span>[i];</span></div></div><div><div><div>20</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>22</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+=</span><span> </span><span>dx</span><span>[</span><span>4</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>24</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>unit</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>v</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>u</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>norm</span><span>)</span></span></div></div><div><div><div>28</div></div><div><span>{</span></div></div><div><div><div>29</div></div><div><span>  </span><span>*</span><span>norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>31</div></div><div><span>    </span><span>u</span><span>[i] </span><span>=</span><span> </span><span>v</span><span>[i] </span><span>/</span><span> </span><span>*</span><span>norm;</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>int</span><span> </span><span>kl_ekf_att_update_accel</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>35</div></div><div><span>{</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>kl_real </span><span>z</span><span>[</span><span>3</span><span>], norm;</span></div></div><div><div><div>37</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>down</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>38</div></div><div><span>  </span><span>unit</span><span>(accel, z, </span><span>&amp;</span><span>norm);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> (kl_real)KL_GRAVITY) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span>)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>41</div></div><div><span>  </span><span>return</span><span> </span><span>update</span><span>(f, z, down, (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY));</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>int</span><span> </span><span>kl_ekf_att_update_mag</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>45</div></div><div><span>{</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>kl_real </span><span>z</span><span>[</span><span>3</span><span>], norm;</span></div></div><div><div><div>47</div></div><div><span>  </span><span>unit</span><span>(mag, z, </span><span>&amp;</span><span>norm);</span></div></div><div><div><div>48</div></div><div><span>  </span><span>return</span><span> </span><span>update</span><span>(f, z, </span><span>f</span><span>-&gt;</span><span>m_w</span><span>, </span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sm</span><span>);</span></div></div><div><div><div>49</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dq</span><span> </span><span>=</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>((</span><span>gyro</span><span> </span><span>-</span><span> </span><span>b</span><span>) </span><span>*</span><span> </span><span>dt</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>X</span><span> </span><span>=</span><span> </span><span>xi</span><span>(</span><span>q</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt; </span><span>F</span><span> </span><span>=</span><span> Mat&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt; </span><span>Q</span><span>{};</span></div></div><div><div><div>6</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>Qq</span><span> </span><span>=</span><span> </span><span>X</span><span> </span><span>*</span><span> </span><span>X</span><span>.</span><span>transpose</span><span>() </span><span>*</span><span> ((</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> </span><span>dt</span><span>) </span><span>*</span><span> (</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> </span><span>dt</span><span>) </span><span>*</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>sg</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>Fq</span><span> </span><span>=</span><span> </span><span>right_mul</span><span>(</span><span>dq</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>4</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>4</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>F</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Fq</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Q</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Qq</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>F</span><span>(</span><span>i</span><span>, </span><span>4</span><span> </span><span>+</span><span> </span><span>j</span><span>) </span><span>=</span><span> </span><span>-</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> </span><span>dt</span><span> </span><span>*</span><span> </span><span>X</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>4</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>7</span><span>; </span><span>i</span><span>++</span><span>) </span><span>Q</span><span>(</span><span>i</span><span>, </span><span>i</span><span>) </span><span>=</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>q</span><span> </span><span>*</span><span> </span><span>dq</span><span>;</span><span>  // not normalized here</span></div></div><div><div><div>17</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>// Gravity-direction update. Returns false when gated out or when S is not</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>// positive definite (state unchanged in both cases).</span></div></div><div><div><div>22</div></div><div><span>  </span><span>bool</span><span> </span><span>update_accel</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>) {</span></span></div></div><div><div><div>23</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>24</div></div><div><span>    </span><span>if</span><span> (</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>&gt;</span><span> </span><span>accel_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span> </span><span>update</span><span>(</span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> </span><span>norm</span><span>), </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}}, (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>*</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)));</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>// Magnetic-field update; false if S is not positive definite (state unchanged).</span></div></div><div><div><div>29</div></div><div><span>  </span><span>bool</span><span> </span><span>update_mag</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>) {</span></span></div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span> </span><span>update</span><span>(</span><span>mag</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>))), </span><span>m_w</span><span>, </span><span>sm</span><span> </span><span>*</span><span> </span><span>sm</span><span>);</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>private:</span></div></div><div><div><div>34</div></div><div><span>  </span><span>bool</span><span> </span><span>update</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>) {</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt; </span><span>H</span><span>{};</span></div></div><div><div><div>36</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>J</span><span> </span><span>=</span><span> </span><span>h_jac</span><span>(</span><span>q</span><span>, </span><span>v</span><span>);</span></div></div><div><div><div>37</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>38</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>4</span><span>; </span><span>j</span><span>++</span><span>) </span><span>H</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>J</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>39</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>y</span><span> </span><span>=</span><span> </span><span>z</span><span> </span><span>-</span><span> </span><span>rot_inv</span><span>(</span><span>q</span><span>, </span><span>v</span><span>);</span></div></div><div><div><div>40</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span>;</span></div></div><div><div><div>41</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>joseph_update</span><span>(</span><span>P</span><span>, </span><span>H</span><span>, Mat&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>*</span><span> </span><span>r</span><span>, </span><span>y</span><span>, </span><span>dx</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>42</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{</span><span>q</span><span>.</span><span>w</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>q</span><span>.</span><span>x</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>q</span><span>.</span><span>y</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>2</span><span>, </span><span>0</span><span>), </span><span>q</span><span>.</span><span>z</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>3</span><span>, </span><span>0</span><span>)}.</span><span>normalized</span><span>();</span></div></div><div><div><div>43</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>4</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>5</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>6</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>44</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C++ 版几乎是对着公式写的：<code>F</code>、<code>Q</code> 是 <span><span>7×77\times7</span><span><span><span></span><span>7</span><span></span><span>×</span><span></span></span><span><span></span><span>7</span></span></span></span> 的 <code>Mat</code>，<code>F * P * F.transpose() + Q</code> 一行完成协方差预测。<code>update_accel</code> 和 <code>update_mag</code> 共用私有的 <code>update</code>，<code>dx</code> 用 <code>Mat&lt;7,1,T&gt;</code>，修正 <code>q</code> 的四个分量，再 <code>normalized()</code>。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>omega</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>3</div></div><div><span>            </span><span>gyro</span><span>[</span><span>0</span><span>] - </span><span>self</span><span>.b[</span><span>0</span><span>],</span></div></div><div><div><div>4</div></div><div><span>            </span><span>gyro</span><span>[</span><span>1</span><span>] - </span><span>self</span><span>.b[</span><span>1</span><span>],</span></div></div><div><div><div>5</div></div><div><span>            </span><span>gyro</span><span>[</span><span>2</span><span>] - </span><span>self</span><span>.b[</span><span>2</span><span>],</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>];</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>dq</span><span><span> </span><span>=</span><span> </span></span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>omega</span><span>[</span><span>0</span><span>] * </span><span>dt</span><span>, </span><span>omega</span><span>[</span><span>1</span><span>] * </span><span>dt</span><span>, </span><span>omega</span><span>[</span><span>2</span><span>] * </span><span>dt</span><span>]);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>xq</span><span><span> </span><span>=</span><span> </span></span><span>xi</span><span>(</span><span>self</span><span>.q);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>7</span><span>, </span><span>7</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>q</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>7</span><span>, </span><span>7</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>rm</span><span><span> </span><span>=</span><span> </span></span><span>right_mul</span><span>(</span><span>dq</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>qq</span><span><span> </span><span>=</span><span> (</span></span><span>xq</span><span> * </span><span>xq</span><span>.</span><span>transpose</span><span>()) * ((</span><span>0.5</span><span> * </span><span>dt</span><span>) * (</span><span>0.5</span><span> * </span><span>dt</span><span>) * </span><span>self</span><span>.sg * </span><span>self</span><span>.sg);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>4</span><span> {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>4</span><span> {</span></div></div><div><div><div>15</div></div><div><span>                </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>rm</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>16</div></div><div><span>                </span><span>q</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>qq</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>19</div></div><div><span>                </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>4</span><span> + </span><span>j</span><span><span>] </span><span>=</span><span> -</span></span><span>0.5</span><span> * </span><span>dt</span><span> * </span><span>xq</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>4</span><span>..</span><span>7</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>q</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>self</span><span>.q * </span><span>dq</span><span>;</span></div></div><div><div><div>26</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>f</span><span> * </span><span>self</span><span>.p * </span><span>f</span><span>.</span><span>transpose</span><span>() + </span><span>q</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>/// Returns false (state unchanged) if S is not positive definite.</span></div></div><div><div><div>30</div></div><div><span>    </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>r</span><span>: </span><span>Real</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>31</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>h</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>7</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>32</div></div><div><span>        </span><span>let</span><span> </span><span>hq</span><span><span> </span><span>=</span><span> </span></span><span>h_jac</span><span>(</span><span>self</span><span>.q, </span><span>v</span><span>);</span></div></div><div><div><div>33</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>34</div></div><div><span>            </span><span>h</span><span>.m[</span><span>i</span><span>][..</span><span>4</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>hq</span><span>.m[</span><span>i</span><span>]);</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>        </span><span>let</span><span> </span><span>pred</span><span><span> </span><span>=</span><span> </span></span><span>rot_inv</span><span>(</span><span>self</span><span>.q, </span><span>v</span><span>);</span></div></div><div><div><div>37</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span> {</span></div></div><div><div><div>38</div></div><div><span>            </span><span>m</span><span>: [[</span><span>z</span><span>[</span><span>0</span><span>] - </span><span>pred</span><span>[</span><span>0</span><span>]], [</span><span>z</span><span>[</span><span>1</span><span>] - </span><span>pred</span><span>[</span><span>1</span><span>]], [</span><span>z</span><span>[</span><span>2</span><span>] - </span><span>pred</span><span>[</span><span>2</span><span>]]],</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>40</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>((</span><span>dx</span><span>, </span><span>_</span><span><span>)) </span><span>=</span><span> </span></span><span>joseph_update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.p, &amp;</span><span>h</span><span><span>, &amp;(</span><span>Mat</span><span>::&lt;</span></span><span>3</span><span>, </span><span>3</span><span>&gt;::</span><span>identity</span><span>() * </span><span>r</span><span>), &amp;</span><span>y</span><span>)</span></div></div><div><div><div>41</div></div><div><span>        </span><span>else</span><span> {</span></div></div><div><div><div>42</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>44</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>Quat</span><span> {</span></div></div><div><div><div>45</div></div><div><span>            </span><span>w</span><span>: </span><span>self</span><span>.q.w + </span><span>dx</span><span>.m[</span><span>0</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>46</div></div><div><span>            </span><span>x</span><span>: </span><span>self</span><span>.q.x + </span><span>dx</span><span>.m[</span><span>1</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>47</div></div><div><span>            </span><span>y</span><span>: </span><span>self</span><span>.q.y + </span><span>dx</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>48</div></div><div><span>            </span><span>z</span><span>: </span><span>self</span><span>.q.z + </span><span>dx</span><span>.m[</span><span>3</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>.</span><span>normalized</span><span>();</span></div></div><div><div><div>51</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>52</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>dx</span><span>.m[</span><span>4</span><span> + </span><span>i</span><span>][</span><span>0</span><span>];</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>54</div></div><div><span>        </span><span>true</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>    </span><span>/// Gravity-direction update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>58</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_accel</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>59</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>60</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.accel_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>G</span><span>) &gt; </span><span>self</span><span>.accel_gate {</span></div></div><div><div><div>61</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>62</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span>        </span><span>let</span><span> </span><span>z</span><span><span> </span><span>=</span><span> [</span></span><span>accel</span><span>[</span><span>0</span><span>] / </span><span>norm</span><span>, </span><span>accel</span><span>[</span><span>1</span><span>] / </span><span>norm</span><span>, </span><span>accel</span><span>[</span><span>2</span><span>] / </span><span>norm</span><span>];</span></div></div><div><div><div>64</div></div><div><span>        </span><span>self</span><span>.</span><span>update</span><span>(</span><span>z</span><span>, [</span><span>0.0</span><span>, </span><span>0.0</span><span>, -</span><span>1.0</span><span>], (</span><span>self</span><span>.sa / </span><span>G</span><span>) * (</span><span>self</span><span>.sa / </span><span>G</span><span>))</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>    </span><span>/// Returns false on Cholesky failure.</span></div></div><div><div><div>68</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_mag</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>69</div></div><div><span>        </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>70</div></div><div><span>        </span><span>self</span><span>.</span><span>update</span><span>(</span></div></div><div><div><div>71</div></div><div><span><span>            </span></span><span>[</span><span>mag</span><span>[</span><span>0</span><span>] / </span><span>n</span><span>, </span><span>mag</span><span>[</span><span>1</span><span>] / </span><span>n</span><span>, </span><span>mag</span><span>[</span><span>2</span><span>] / </span><span>n</span><span>],</span></div></div><div><div><div>72</div></div><div><span>            </span><span>self</span><span>.m_w,</span></div></div><div><div><div>73</div></div><div><span>            </span><span>self</span><span>.sm * </span><span>self</span><span>.sm,</span></div></div><div><div><div>74</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Rust 版里 <code>q</code> 是 <code>Mat&lt;7,7&gt;</code> 的局部变量名和 <code>self.q</code>（<code>Quat</code>）并存，读起来有点绕，这是我给自己挖的坑。<code>let Some((dx, _)) = joseph_update(...) else { return false; };</code> 是 <code>let-else</code>：失败分支必须发散（这里是提前返回），所以 <code>update</code> 不会在 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 不正定时悄悄修改 <code>self.p</code>，因为 <code>joseph_update</code> 在 Cholesky 失败时不动 <code>p</code>。</p>
完整源码：c/include/kl/kl_ekf_att.h、c/src/kl_ekf_att.c<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Multiplicative-quaternion EKF: state x = [q (4), b (3)], input = gyro.</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* Quaternion conventions: see kl_quat.h. All storage is inline; nothing is allocated. */</span></div></div><div><div><div>3</div></div><div><span>#ifndef</span><span> </span><span>KL_EKF_ATT_H</span></div></div><div><div><div>4</div></div><div><span>#define</span><span> </span><span>KL_EKF_ATT_H</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#define</span><span> </span><span>KL_GRAVITY</span><span> </span><span>9.80665</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>/* C(q)^T v as a polynomial in q: (w^2 - u.u) v + 2 (u.v) u - 2 w (u x v).</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>* Equals the DCM transpose for unit q and is defined (and differentiable) for any q. */</span></div></div><div><div><div>12</div></div><div><span>void</span><span> </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>out</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>13</div></div><div><span>/* d rot_inv(q, v) / d q, 3x4 row-major [d/dw | d/du] */</span></div></div><div><div><div>14</div></div><div><span>void</span><span> </span><span>kl_ekf_att_h_jac</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>J</span><span>[</span><span>12</span><span><span>]</span><span>);</span></span></div></div><div><div><div>15</div></div><div><span>/* 4x3 row-major matrix with q (x) [0, omega] = xi(q) omega */</span></div></div><div><div><div>16</div></div><div><span>void</span><span> </span><span>kl_ekf_att_xi</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>Xi</span><span>[</span><span>12</span><span><span>]</span><span>);</span></span></div></div><div><div><div>17</div></div><div><span>/* 4x4 row-major matrix with q (x) p = right_mul(p) q */</span></div></div><div><div><div>18</div></div><div><span>void</span><span> </span><span>kl_ekf_att_right_mul</span><span>(</span><span>const</span><span> kl_real </span><span>p</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>M</span><span>[</span><span>16</span><span><span>]</span><span>);</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>kl_real </span><span>q</span><span>[</span><span>4</span><span>], </span><span>b</span><span>[</span><span>3</span><span>], </span><span>P</span><span>[</span><span>49</span><span>];</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>kl_real sg, sbw, sa, sm;</span><span> /* gyro, bias random walk, accel, mag noise */</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>kl_real </span><span>m_w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>kl_real accel_gate;</span><span> /* m/s^2 on | |f| - g |; negative disables gating */</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>kl_real </span><span>work</span><span>[</span><span>280</span><span>];</span><span>  /* scratch: predict needs 196, accel/mag updates 30 + KL_JOSEPH_WORK_LEN(7, 3) = 279 */</span></div></div><div><div><div>26</div></div><div><span>} kl_ekf_att;</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>/* p0_diag: initial diagonal of P (7). The bias starts at zero. */</span></div></div><div><div><div>29</div></div><div><span>void</span><span> </span><span>kl_ekf_att_init</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>7</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>30</div></div><div><span><span>         </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>31</div></div><div><span><span>         </span></span><span>kl_real </span><span>accel_gate</span><span>);</span></div></div><div><div><div>32</div></div><div><span>void</span><span> </span><span>kl_ekf_att_predict</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>);</span></span></div></div><div><div><div>33</div></div><div><span>/* Return 1 if applied, 0 if gated out, -1 on Cholesky failure (state unchanged). */</span></div></div><div><div><div>34</div></div><div><span>int</span><span> </span><span>kl_ekf_att_update_accel</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>35</div></div><div><span>/* Return 1 if applied, -1 on Cholesky failure (state unchanged). */</span></div></div><div><div><div>36</div></div><div><span>int</span><span> </span><span>kl_ekf_att_update_mag</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_ekf_att.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;stddef.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>"kl/kl_update.h"</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>void</span><span> </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>out</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>10</div></div><div><span>{</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>kl_real w </span><span>=</span><span> </span><span>q</span><span>[</span><span>0</span><span>], </span><span>u</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>q</span><span>[</span><span>1</span><span>], </span><span>q</span><span>[</span><span>2</span><span>], </span><span>q</span><span>[</span><span>3</span><span>]};</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>kl_real uu </span><span>=</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>kl_real uv </span><span>=</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>kl_real </span><span>c</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>], </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>], </span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>-</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>]};</span></div></div><div><div><div>15</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>out</span><span>[i] </span><span>=</span><span> (w </span><span>*</span><span> w </span><span>-</span><span> uu) </span><span>*</span><span> </span><span>v</span><span>[i] </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> uv </span><span>*</span><span> </span><span>u</span><span>[i] </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> w </span><span>*</span><span> </span><span>c</span><span>[i];</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>void</span><span> </span><span>kl_ekf_att_h_jac</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>J</span><span>[</span><span>12</span><span><span>]</span><span>)</span></span></div></div><div><div><div>20</div></div><div><span>{</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>kl_real w </span><span>=</span><span> </span><span>q</span><span>[</span><span>0</span><span>], </span><span>u</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>q</span><span>[</span><span>1</span><span>], </span><span>q</span><span>[</span><span>2</span><span>], </span><span>q</span><span>[</span><span>3</span><span>]};</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>kl_real uv </span><span>=</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>kl_real </span><span>c</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>], </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>], </span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>-</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>]};</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>kl_real </span><span>S</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>25</div></div><div><span>  </span><span>kl_skew</span><span>(v, S);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>J</span><span>[i </span><span>*</span><span> </span><span>4</span><span>] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> w </span><span>*</span><span> </span><span>v</span><span>[i] </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>c</span><span>[i];</span></div></div><div><div><div>28</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>29</div></div><div><span>      </span><span>J</span><span>[i </span><span>*</span><span> </span><span>4</span><span> </span><span>+</span><span> </span><span>1</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> uv </span><span>*</span><span> (i </span><span>==</span><span> j) </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>u</span><span>[i] </span><span>*</span><span> </span><span>v</span><span>[j] </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>v</span><span>[i] </span><span>*</span><span> </span><span>u</span><span>[j] </span><span>+</span></div></div><div><div><div>30</div></div><div><span>             </span><span>2</span><span> </span><span>*</span><span> w </span><span>*</span><span> </span><span>S</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>void</span><span> </span><span>kl_ekf_att_xi</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>Xi</span><span>[</span><span>12</span><span><span>]</span><span>)</span></span></div></div><div><div><div>35</div></div><div><span>{</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>kl_real w </span><span>=</span><span> </span><span>q</span><span>[</span><span>0</span><span>], x </span><span>=</span><span> </span><span>q</span><span>[</span><span>1</span><span>], y </span><span>=</span><span> </span><span>q</span><span>[</span><span>2</span><span>], z </span><span>=</span><span> </span><span>q</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>37</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>X</span><span>[</span><span>12</span><span>] </span><span>=</span><span> {</span><span>-</span><span>x, </span><span>-</span><span>y, </span><span>-</span><span>z, w, </span><span>-</span><span>z, y, z, w, </span><span>-</span><span>x, </span><span>-</span><span>y, x, w};</span></div></div><div><div><div>38</div></div><div><span>  </span><span>kl_mat_copy</span><span>(X, Xi, </span><span>4</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>39</div></div><div><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>void</span><span> </span><span>kl_ekf_att_right_mul</span><span>(</span><span>const</span><span> kl_real </span><span>p</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>M</span><span>[</span><span>16</span><span><span>]</span><span>)</span></span></div></div><div><div><div>42</div></div><div><span>{</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>kl_real pw </span><span>=</span><span> </span><span>p</span><span>[</span><span>0</span><span>], px </span><span>=</span><span> </span><span>p</span><span>[</span><span>1</span><span>], py </span><span>=</span><span> </span><span>p</span><span>[</span><span>2</span><span>], pz </span><span>=</span><span> </span><span>p</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>44</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>R</span><span>[</span><span>16</span><span>] </span><span>=</span><span> {pw, </span><span>-</span><span>px, </span><span>-</span><span>py, </span><span>-</span><span>pz, px, pw, pz, </span><span>-</span><span>py, py, </span><span>-</span><span>pz, pw, px, pz, py, </span><span>-</span><span>px, pw};</span></div></div><div><div><div>45</div></div><div><span>  </span><span>kl_mat_copy</span><span>(R, M, </span><span>4</span><span>, </span><span>4</span><span>);</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>void</span><span> </span><span>kl_ekf_att_init</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>p0_diag</span><span>[</span><span>7</span><span><span>]</span><span>,</span><span> kl_real </span><span>sigma_g</span><span>,</span></span></div></div><div><div><div>49</div></div><div><span><span>         </span></span><span>kl_real </span><span>sigma_bw</span><span>,</span><span> kl_real </span><span>sigma_a</span><span>,</span><span> kl_real </span><span>sigma_m</span><span>,</span><span> </span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>50</div></div><div><span><span>         </span></span><span>kl_real </span><span>accel_gate</span><span>)</span></div></div><div><div><div>51</div></div><div><span>{</span></div></div><div><div><div>52</div></div><div><span>  </span><span>kl_mat_copy</span><span>(q0, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>53</div></div><div><span>  </span><span>kl_mat_copy</span><span>(m_w, </span><span>f</span><span>-&gt;</span><span>m_w</span><span>, </span><span>3</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>54</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>55</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>56</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>49</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>57</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>P</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>58</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>7</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>59</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>P</span><span>[i </span><span>*</span><span> </span><span>8</span><span>] </span><span>=</span><span> </span><span>p0_diag</span><span>[i];</span></div></div><div><div><div>60</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>=</span><span> sigma_g;</span></div></div><div><div><div>61</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>=</span><span> sigma_bw;</span></div></div><div><div><div>62</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>=</span><span> sigma_a;</span></div></div><div><div><div>63</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>=</span><span> sigma_m;</span></div></div><div><div><div>64</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>=</span><span> accel_gate;</span></div></div><div><div><div>65</div></div><div><span>}</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>void</span><span> </span><span>kl_ekf_att_predict</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>68</div></div><div><span>{</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>F </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>Q </span><span>=</span><span> F </span><span>+</span><span> </span><span>49</span><span>, </span><span>*</span><span>T </span><span>=</span><span> Q </span><span>+</span><span> </span><span>49</span><span>, </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> </span><span>49</span><span>;</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>kl_real </span><span>omega</span><span>[</span><span>3</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>X</span><span>[</span><span>12</span><span>], </span><span>Rm</span><span>[</span><span>16</span><span>], </span><span>Xs</span><span>[</span><span>12</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>71</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>72</div></div><div><span>    </span><span>omega</span><span>[i] </span><span>=</span><span> (</span><span>gyro</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i]) </span><span>*</span><span> dt;</span></div></div><div><div><div>73</div></div><div><span>  </span><span>kl_quat_exp</span><span>(omega, dq);</span></div></div><div><div><div>74</div></div><div><span>  </span><span>kl_ekf_att_xi</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, X);</span></div></div><div><div><div>75</div></div><div><span>  </span><span>kl_ekf_att_right_mul</span><span>(dq, Rm);</span></div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span><span>  </span></span><span>/* F = [[right_mul(dq), -dt/2 Xi], [0, I]] */</span></div></div><div><div><div>78</div></div><div><span>  </span><span>kl_mat_identity</span><span>(F, </span><span>7</span><span>);</span></div></div><div><div><div>79</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>80</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>4</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>81</div></div><div><span>      </span><span>F</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>Rm</span><span>[i </span><span>*</span><span> </span><span>4</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>82</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>83</div></div><div><span>      </span><span>F</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> </span><span>4</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>-</span><span>(kl_real)</span><span>0.5</span><span> </span><span>*</span><span> dt </span><span>*</span><span> </span><span>X</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>84</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>85</div></div><div><span><span>  </span></span><span>/* Q = blkdiag((dt/2)^2 sg^2 Xi Xi^T, sbw^2 dt I) */</span></div></div><div><div><div>86</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>49</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>87</div></div><div><span>    </span><span>Q</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>88</div></div><div><span>  </span><span>kl_mat_scale</span><span>(X, ((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> dt) </span><span>*</span><span> ((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> dt) </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sg</span><span>, Xs, </span><span>4</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>89</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(Xs, X, Rm, </span><span>4</span><span>, </span><span>3</span><span>, </span><span>4</span><span>);</span></div></div><div><div><div>90</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>91</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>4</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>92</div></div><div><span>      </span><span>Q</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> j] </span><span>=</span><span> </span><span>Rm</span><span>[i </span><span>*</span><span> </span><span>4</span><span> </span><span>+</span><span> j];</span></div></div><div><div><div>93</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>4</span><span>; i </span><span>&lt;</span><span> </span><span>7</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>94</div></div><div><span>    </span><span>Q</span><span>[i </span><span>*</span><span> </span><span>8</span><span>] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sbw</span><span> </span><span>*</span><span> dt;</span></div></div><div><div><div>95</div></div><div>
</div></div><div><div><div>96</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>97</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>98</div></div><div><span>  </span><span>kl_mat_mul</span><span>(F, </span><span>f</span><span>-&gt;</span><span>P</span><span>, T, </span><span>7</span><span>, </span><span>7</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>99</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(T, F, U, </span><span>7</span><span>, </span><span>7</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>100</div></div><div><span>  </span><span>kl_mat_add</span><span>(U, Q, </span><span>f</span><span>-&gt;</span><span>P</span><span>, </span><span>7</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>101</div></div><div><span>}</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div><span>/* H = [dh/dq | 0], y = z - h(q); the correction dx is added to the state. */</span></div></div><div><div><div>104</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>update</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>z</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>r</span><span>)</span></span></div></div><div><div><div>105</div></div><div><span>{</span></div></div><div><div><div>106</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>H </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>work</span><span>, </span><span>*</span><span>R </span><span>=</span><span> H </span><span>+</span><span> </span><span>21</span><span>, </span><span>*</span><span>jw </span><span>=</span><span> R </span><span>+</span><span> </span><span>9</span><span>;</span></div></div><div><div><div>107</div></div><div><span><span>  </span></span><span>kl_real </span><span>Hq</span><span>[</span><span>12</span><span>], </span><span>hq</span><span>[</span><span>3</span><span>], </span><span>y</span><span>[</span><span>3</span><span>], </span><span>dx</span><span>[</span><span>7</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>108</div></div><div><span>  </span><span>kl_ekf_att_h_jac</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, v, Hq);</span></div></div><div><div><div>109</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>110</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>7</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>111</div></div><div><span>      </span><span>H</span><span>[i </span><span>*</span><span> </span><span>7</span><span> </span><span>+</span><span> j] </span><span>=</span><span> j </span><span>&lt;</span><span> </span><span>4</span><span> </span><span>?</span><span> </span><span>Hq</span><span>[i </span><span>*</span><span> </span><span>4</span><span> </span><span>+</span><span> j] </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>112</div></div><div><span>  </span><span>kl_ekf_att_rot_inv</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, v, hq);</span></div></div><div><div><div>113</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>114</div></div><div><span>    </span><span>y</span><span>[i] </span><span>=</span><span> </span><span>z</span><span>[i] </span><span>-</span><span> </span><span>hq</span><span>[i];</span></div></div><div><div><div>115</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>)</span></div></div><div><div><div>116</div></div><div><span>      </span><span>R</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>?</span><span> r </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>117</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>118</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_joseph_update</span><span>(</span><span>f</span><span>-&gt;</span><span>P</span><span>, H, R, y, </span><span>7</span><span>, </span><span>3</span><span>, dx, </span><span>NULL</span><span>, jw) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>119</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>120</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>121</div></div><div><span>    </span><span>qn</span><span>[i] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>q</span><span>[i] </span><span>+</span><span> </span><span>dx</span><span>[i];</span></div></div><div><div><div>122</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>123</div></div><div><span>  </span><span>kl_mat_copy</span><span>(qn, </span><span>f</span><span>-&gt;</span><span>q</span><span>, </span><span>4</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>124</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>125</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+=</span><span> </span><span>dx</span><span>[</span><span>4</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>126</div></div><div><span>  </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>127</div></div><div><span>}</span></div></div><div><div><div>128</div></div><div>
</div></div><div><div><div>129</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>unit</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>v</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>u</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>norm</span><span>)</span></span></div></div><div><div><div>130</div></div><div><span>{</span></div></div><div><div><div>131</div></div><div><span>  </span><span>*</span><span>norm </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>132</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>133</div></div><div><span>    </span><span>u</span><span>[i] </span><span>=</span><span> </span><span>v</span><span>[i] </span><span>/</span><span> </span><span>*</span><span>norm;</span></div></div><div><div><div>134</div></div><div><span>}</span></div></div><div><div><div>135</div></div><div>
</div></div><div><div><div>136</div></div><div><span>int</span><span> </span><span>kl_ekf_att_update_accel</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>137</div></div><div><span>{</span></div></div><div><div><div>138</div></div><div><span><span>  </span></span><span>kl_real </span><span>z</span><span>[</span><span>3</span><span>], norm;</span></div></div><div><div><div>139</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>down</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>140</div></div><div><span>  </span><span>unit</span><span>(accel, z, </span><span>&amp;</span><span>norm);</span></div></div><div><div><div>141</div></div><div><span>  </span><span>if</span><span> (</span><span>f</span><span>-&gt;</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> </span><span>KL_FABS</span><span>(norm </span><span>-</span><span> (kl_real)KL_GRAVITY) </span><span>&gt;</span><span> </span><span>f</span><span>-&gt;</span><span>accel_gate</span><span>)</span></div></div><div><div><div>142</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>143</div></div><div><span>  </span><span>return</span><span> </span><span>update</span><span>(f, z, down, (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY) </span><span>*</span><span> (</span><span>f</span><span>-&gt;</span><span>sa</span><span> </span><span>/</span><span> (kl_real)KL_GRAVITY));</span></div></div><div><div><div>144</div></div><div><span>}</span></div></div><div><div><div>145</div></div><div>
</div></div><div><div><div>146</div></div><div><span>int</span><span> </span><span>kl_ekf_att_update_mag</span><span><span>(</span><span>kl_ekf_att </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>147</div></div><div><span>{</span></div></div><div><div><div>148</div></div><div><span><span>  </span></span><span>kl_real </span><span>z</span><span>[</span><span>3</span><span>], norm;</span></div></div><div><div><div>149</div></div><div><span>  </span><span>unit</span><span>(mag, z, </span><span>&amp;</span><span>norm);</span></div></div><div><div><div>150</div></div><div><span>  </span><span>return</span><span> </span><span>update</span><span>(f, z, </span><span>f</span><span>-&gt;</span><span>m_w</span><span>, </span><span>f</span><span>-&gt;</span><span>sm</span><span> </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>sm</span><span>);</span></div></div><div><div><div>151</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：cpp/include/kl/ekf_attitude.hpp<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Multiplicative-quaternion EKF: state x = [q (4), b (3)], input = gyro.</span></div></div><div><div><div>2</div></div><div><span>// Quaternion conventions as in quat.hpp. P is 7x7 over the raw quaternion</span></div></div><div><div><div>3</div></div><div><span>// components and the bias; q is renormalized after every update.</span></div></div><div><div><div>4</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/quat.hpp"</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/update.hpp"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>constexpr</span><span> </span><span>double</span><span> </span><span>GRAVITY</span><span> </span><span>=</span><span> </span><span>9.80665</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// C(q)^T v written as a polynomial in q: (w^2 - u.u) v + 2 (u.v) u - 2 w (u x v).</span></div></div><div><div><div>13</div></div><div><span>// Equals q.dcm().transpose() * v for unit q, and is defined for any q.</span></div></div><div><div><div>14</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>15</div></div><div><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>rot_inv</span><span>(</span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>16</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>u</span><span>{{</span><span>q</span><span>.</span><span>x</span><span>, </span><span>q</span><span>.</span><span>y</span><span>, </span><span>q</span><span>.</span><span>z</span><span>}};</span></div></div><div><div><div>17</div></div><div><span>  </span><span>return</span><span> </span><span>v</span><span> </span><span>*</span><span> (</span><span>q</span><span>.</span><span>w</span><span> </span><span>*</span><span> </span><span>q</span><span>.</span><span>w</span><span> </span><span>-</span><span> </span><span>dot</span><span>(</span><span>u</span><span>, </span><span>u</span><span>)) </span><span>+</span><span> </span><span>u</span><span> </span><span>*</span><span> (</span><span>2</span><span> </span><span>*</span><span> </span><span>dot</span><span>(</span><span>u</span><span>, </span><span>v</span><span>)) </span><span>-</span><span> </span><span>cross</span><span>(</span><span>u</span><span>, </span><span>v</span><span>) </span><span>*</span><span> (</span><span>2</span><span> </span><span>*</span><span> </span><span>q</span><span>.</span><span>w</span><span>);</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>// d rot_inv(q, v) / d q, a 3x4 matrix [d/dw | d/du].</span></div></div><div><div><div>21</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>22</div></div><div><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>h_jac</span><span>(</span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>23</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>u</span><span>{{</span><span>q</span><span>.</span><span>x</span><span>, </span><span>q</span><span>.</span><span>y</span><span>, </span><span>q</span><span>.</span><span>z</span><span>}};</span></div></div><div><div><div>24</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dw</span><span> </span><span>=</span><span> </span><span>v</span><span> </span><span>*</span><span> (</span><span>2</span><span> </span><span>*</span><span> </span><span>q</span><span>.</span><span>w</span><span>) </span><span>-</span><span> </span><span>cross</span><span>(</span><span>u</span><span>, </span><span>v</span><span>) </span><span>*</span><span> </span><span>2</span><span>;</span></div></div><div><div><div>25</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>du</span><span> </span><span>=</span><span> Mat&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>*</span><span> (</span><span>2</span><span> </span><span>*</span><span> </span><span>dot</span><span>(</span><span>u</span><span>, </span><span>v</span><span>)) </span><span>+</span><span> </span><span>skew</span><span>(</span><span>v</span><span>) </span><span>*</span><span> (</span><span>2</span><span> </span><span>*</span><span> </span><span>q</span><span>.</span><span>w</span><span>);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>du</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>+=</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>u</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>v</span><span>(</span><span>j</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>v</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>u</span><span>(</span><span>j</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>28</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>J</span><span>{};</span></div></div><div><div><div>29</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>30</div></div><div><span>    </span><span>J</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>dw</span><span>(</span><span>i</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>J</span><span>(</span><span>i</span><span>, </span><span>j</span><span> </span><span>+</span><span> </span><span>1</span><span>) </span><span>=</span><span> </span><span>du</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>  </span><span>return</span><span> </span><span>J</span><span>;</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>// 4x3 matrix with q (x) [0, omega] = xi(q) omega.</span></div></div><div><div><div>37</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>38</div></div><div><span>constexpr</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>xi</span><span>(</span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q</span><span>) {</span></span></div></div><div><div><div>39</div></div><div><span>  </span><span>return</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>-</span><span>q</span><span>.</span><span>x</span><span>, </span><span>-</span><span>q</span><span>.</span><span>y</span><span>, </span><span>-</span><span>q</span><span>.</span><span>z</span><span>, </span><span>q</span><span>.</span><span>w</span><span>, </span><span>-</span><span>q</span><span>.</span><span>z</span><span>, </span><span>q</span><span>.</span><span>y</span><span>, </span><span>q</span><span>.</span><span>z</span><span>, </span><span>q</span><span>.</span><span>w</span><span>, </span><span>-</span><span>q</span><span>.</span><span>x</span><span>, </span><span>-</span><span>q</span><span>.</span><span>y</span><span>, </span><span>q</span><span>.</span><span>x</span><span>, </span><span>q</span><span>.</span><span>w</span><span>}};</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>// 4x4 matrix with q (x) p = right_mul(p) q.</span></div></div><div><div><div>43</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>44</div></div><div><span>constexpr</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>right_mul</span><span>(</span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>p</span><span>) {</span></span></div></div><div><div><div>45</div></div><div><span>  </span><span>return</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>p</span><span>.</span><span>w</span><span>, </span><span>-</span><span>p</span><span>.</span><span>x</span><span>, </span><span>-</span><span>p</span><span>.</span><span>y</span><span>, </span><span>-</span><span>p</span><span>.</span><span>z</span><span>, </span><span>p</span><span>.</span><span>x</span><span>, </span><span>p</span><span>.</span><span>w</span><span>, </span><span>p</span><span>.</span><span>z</span><span>, </span><span>-</span><span>p</span><span>.</span><span>y</span><span>,</span></div></div><div><div><div>46</div></div><div><span>           </span><span>p</span><span>.</span><span>y</span><span>, </span><span>-</span><span>p</span><span>.</span><span>z</span><span>, </span><span>p</span><span>.</span><span>w</span><span>, </span><span>p</span><span>.</span><span>x</span><span>, </span><span>p</span><span>.</span><span>z</span><span>, </span><span>p</span><span>.</span><span>y</span><span>, </span><span>-</span><span>p</span><span>.</span><span>x</span><span>, </span><span>p</span><span>.</span><span>w</span><span>}};</span></div></div><div><div><div>47</div></div><div><span>}</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>50</div></div><div><span>class</span><span> </span><span>QuatEKF</span><span> {</span></div></div><div><div><div>51</div></div><div><span>public:</span></div></div><div><div><div>52</div></div><div><span>  </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>q</span><span>;</span></div></div><div><div><div>53</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>b</span><span>{};</span></div></div><div><div><div>54</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt; </span><span>P</span><span>{};</span></div></div><div><div><div>55</div></div><div><span>  </span><span>T</span><span> </span><span>sg</span><span>, </span><span>sbw</span><span>, </span><span>sa</span><span>, </span><span>sm</span><span>;</span></div></div><div><div><div>56</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_w</span><span>;</span></div></div><div><div><div>57</div></div><div><span>  </span><span>T</span><span> </span><span>accel_gate</span><span>;</span><span>  // m/s^2 on | |f| - g |; negative disables gating</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>  </span><span>QuatEKF</span><span>(</span><span>T</span><span><span> </span><span>sigma_g</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_bw</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_a</span><span>, </span></span><span>T</span><span><span> </span><span>sigma_m</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m_w</span><span>, </span></span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q0</span><span>,</span></span></div></div><div><div><div>60</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P0</span><span>, </span></span><span>T</span><span><span> </span><span>accel_gate</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>))</span></div></div><div><div><div>61</div></div><div><span><span>      </span></span><span>: </span><span>q</span><span>(</span><span>q0</span><span>), </span><span>P</span><span>(</span><span>P0</span><span>), </span><span>sg</span><span>(</span><span>sigma_g</span><span>), </span><span>sbw</span><span>(</span><span>sigma_bw</span><span>), </span><span>sa</span><span>(</span><span>sigma_a</span><span>), </span><span>sm</span><span>(</span><span>sigma_m</span><span>), </span><span>m_w</span><span>(</span><span>m_w</span><span>),</span></div></div><div><div><div>62</div></div><div><span>        </span><span>accel_gate</span><span>(</span><span>accel_gate</span><span>) {}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>65</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>dq</span><span> </span><span>=</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>((</span><span>gyro</span><span> </span><span>-</span><span> </span><span>b</span><span>) </span><span>*</span><span> </span><span>dt</span><span>);</span></div></div><div><div><div>66</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>X</span><span> </span><span>=</span><span> </span><span>xi</span><span>(</span><span>q</span><span>);</span></div></div><div><div><div>67</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt; </span><span>F</span><span> </span><span>=</span><span> Mat&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>68</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt; </span><span>Q</span><span>{};</span></div></div><div><div><div>69</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>Qq</span><span> </span><span>=</span><span> </span><span>X</span><span> </span><span>*</span><span> </span><span>X</span><span>.</span><span>transpose</span><span>() </span><span>*</span><span> ((</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> </span><span>dt</span><span>) </span><span>*</span><span> (</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> </span><span>dt</span><span>) </span><span>*</span><span> </span><span>sg</span><span> </span><span>*</span><span> </span><span>sg</span><span>);</span></div></div><div><div><div>70</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>Fq</span><span> </span><span>=</span><span> </span><span>right_mul</span><span>(</span><span>dq</span><span>);</span></div></div><div><div><div>71</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>4</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>72</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>4</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>73</div></div><div><span>        </span><span>F</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Fq</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>74</div></div><div><span>        </span><span>Q</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>Qq</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>75</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>76</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>j</span><span>++</span><span>) </span><span>F</span><span>(</span><span>i</span><span>, </span><span>4</span><span> </span><span>+</span><span> </span><span>j</span><span>) </span><span>=</span><span> </span><span>-</span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> </span><span>dt</span><span> </span><span>*</span><span> </span><span>X</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>78</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>4</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>7</span><span>; </span><span>i</span><span>++</span><span>) </span><span>Q</span><span>(</span><span>i</span><span>, </span><span>i</span><span>) </span><span>=</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>sbw</span><span> </span><span>*</span><span> </span><span>dt</span><span>;</span></div></div><div><div><div>79</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>q</span><span> </span><span>*</span><span> </span><span>dq</span><span>;</span><span>  // not normalized here</span></div></div><div><div><div>80</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>81</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span><span>  </span></span><span>// Gravity-direction update. Returns false when gated out or when S is not</span></div></div><div><div><div>84</div></div><div><span><span>  </span></span><span>// positive definite (state unchanged in both cases).</span></div></div><div><div><div>85</div></div><div><span>  </span><span>bool</span><span> </span><span>update_accel</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>) {</span></span></div></div><div><div><div>86</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>norm</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>87</div></div><div><span>    </span><span>if</span><span> (</span><span>accel_gate</span><span> </span><span>&gt;=</span><span> </span><span>0</span><span> </span><span>&amp;&amp;</span><span> std::</span><span>fabs</span><span>(</span><span>norm</span><span> </span><span>-</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>&gt;</span><span> </span><span>accel_gate</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>88</div></div><div><span>    </span><span>return</span><span> </span><span>update</span><span>(</span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> </span><span>norm</span><span>), </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}}, (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)) </span><span>*</span><span> (</span><span>sa</span><span> </span><span>/</span><span> </span><span>T</span><span>(</span><span>GRAVITY</span><span>)));</span></div></div><div><div><div>89</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>90</div></div><div>
</div></div><div><div><div>91</div></div><div><span><span>  </span></span><span>// Magnetic-field update; false if S is not positive definite (state unchanged).</span></div></div><div><div><div>92</div></div><div><span>  </span><span>bool</span><span> </span><span>update_mag</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>) {</span></span></div></div><div><div><div>93</div></div><div><span>    </span><span>return</span><span> </span><span>update</span><span>(</span><span>mag</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>))), </span><span>m_w</span><span>, </span><span>sm</span><span> </span><span>*</span><span> </span><span>sm</span><span>);</span></div></div><div><div><div>94</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>95</div></div><div>
</div></div><div><div><div>96</div></div><div><span>private:</span></div></div><div><div><div>97</div></div><div><span>  </span><span>bool</span><span> </span><span>update</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>) {</span></span></div></div><div><div><div>98</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>7</span><span>, </span><span>T</span><span>&gt; </span><span>H</span><span>{};</span></div></div><div><div><div>99</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>4</span><span>, </span><span>T</span><span>&gt; </span><span>J</span><span> </span><span>=</span><span> </span><span>h_jac</span><span>(</span><span>q</span><span>, </span><span>v</span><span>);</span></div></div><div><div><div>100</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>101</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>4</span><span>; </span><span>j</span><span>++</span><span>) </span><span>H</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>J</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>102</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>y</span><span> </span><span>=</span><span> </span><span>z</span><span> </span><span>-</span><span> </span><span>rot_inv</span><span>(</span><span>q</span><span>, </span><span>v</span><span>);</span></div></div><div><div><div>103</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>dx</span><span>;</span></div></div><div><div><div>104</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>joseph_update</span><span>(</span><span>P</span><span>, </span><span>H</span><span>, Mat&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>*</span><span> </span><span>r</span><span>, </span><span>y</span><span>, </span><span>dx</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>105</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{</span><span>q</span><span>.</span><span>w</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>q</span><span>.</span><span>x</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>q</span><span>.</span><span>y</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>2</span><span>, </span><span>0</span><span>), </span><span>q</span><span>.</span><span>z</span><span> </span><span>+</span><span> </span><span>dx</span><span>(</span><span>3</span><span>, </span><span>0</span><span>)}.</span><span>normalized</span><span>();</span></div></div><div><div><div>106</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>dx</span><span>(</span><span>4</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>5</span><span>, </span><span>0</span><span>), </span><span>dx</span><span>(</span><span>6</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>107</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>108</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>109</div></div><div><span>};</span></div></div><div><div><div>110</div></div><div>
</div></div><div><div><div>111</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：rust/src/ekf_att.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Multiplicative-quaternion EKF: state x = [q (4), b (3)], input = gyro.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::</span></span><span>Mat</span><span>;</span></div></div><div><div><div>4</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>quat</span><span>::{</span></span><span>Quat</span><span>, skew};</span></div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, fabs, sqrt};</span></div></div><div><div><div>6</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>update</span><span>::joseph_update;</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/// Standard gravity (m/s^2).</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>const</span><span> </span><span>G</span><span>: </span><span>Real</span><span><span> </span><span>=</span><span> </span></span><span>9.80665</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>cross</span><span>(</span><span>a</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>13</div></div><div><span>        </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>] - </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>14</div></div><div><span>        </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>] - </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>15</div></div><div><span>        </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>] - </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>fn</span><span> </span><span>dot</span><span>(</span><span>a</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>] + </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>] + </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>]</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>/// C(q)^T v as a polynomial in q: (w^2 - u.u) v + 2 (u.v) u - 2 w (u x v).</span></div></div><div><div><div>24</div></div><div><span>/// Equal to dcm(q)^T v for unit q, and defined (and differentiable) for any q.</span></div></div><div><div><div>25</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>rot_inv</span><span>(</span><span>q</span><span>: </span><span>Quat</span><span>, </span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>let</span><span> </span><span>u</span><span><span> </span><span>=</span><span> [</span></span><span>q</span><span>.x, </span><span>q</span><span>.y, </span><span>q</span><span>.z];</span></div></div><div><div><div>27</div></div><div><span>    </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>cross</span><span>(</span><span>u</span><span>, </span><span>v</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>let</span><span> (</span><span>a</span><span>, </span><span>b</span><span><span>) </span><span>=</span><span> (</span></span><span>q</span><span>.w * </span><span>q</span><span>.w - </span><span>dot</span><span>(</span><span>u</span><span>, </span><span>u</span><span>), </span><span>2.0</span><span> * </span><span>dot</span><span>(</span><span>u</span><span>, </span><span>v</span><span>));</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>30</div></div><div><span>        </span><span>a</span><span> * </span><span>v</span><span>[</span><span>0</span><span>] + </span><span>b</span><span> * </span><span>u</span><span>[</span><span>0</span><span>] - </span><span>2.0</span><span> * </span><span>q</span><span>.w * </span><span>c</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>31</div></div><div><span>        </span><span>a</span><span> * </span><span>v</span><span>[</span><span>1</span><span>] + </span><span>b</span><span> * </span><span>u</span><span>[</span><span>1</span><span>] - </span><span>2.0</span><span> * </span><span>q</span><span>.w * </span><span>c</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>32</div></div><div><span>        </span><span>a</span><span> * </span><span>v</span><span>[</span><span>2</span><span>] + </span><span>b</span><span> * </span><span>u</span><span>[</span><span>2</span><span>] - </span><span>2.0</span><span> * </span><span>q</span><span>.w * </span><span>c</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>/// d rot_inv(q, v) / d q, a 3x4 matrix [d/dw | d/du].</span></div></div><div><div><div>37</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>h_jac</span><span>(</span><span>q</span><span>: </span><span>Quat</span><span>, </span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>4</span><span>&gt; {</span></div></div><div><div><div>38</div></div><div><span>    </span><span>let</span><span> </span><span>u</span><span><span> </span><span>=</span><span> [</span></span><span>q</span><span>.x, </span><span>q</span><span>.y, </span><span>q</span><span>.z];</span></div></div><div><div><div>39</div></div><div><span>    </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>cross</span><span>(</span><span>u</span><span>, </span><span>v</span><span>);</span></div></div><div><div><div>40</div></div><div><span>    </span><span>let</span><span> </span><span>uv</span><span><span> </span><span>=</span><span> </span></span><span>dot</span><span>(</span><span>u</span><span>, </span><span>v</span><span>);</span></div></div><div><div><div>41</div></div><div><span>    </span><span>let</span><span> </span><span>sv</span><span><span> </span><span>=</span><span> </span></span><span>skew</span><span>(</span><span>v</span><span>);</span></div></div><div><div><div>42</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>j</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>43</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>44</div></div><div><span>        </span><span>j</span><span>.m[</span><span>i</span><span>][</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>2.0</span><span> * </span><span>q</span><span>.w * </span><span>v</span><span>[</span><span>i</span><span>] - </span><span>2.0</span><span> * </span><span>c</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>45</div></div><div><span>        </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>46</div></div><div><span>            </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>i</span><span><span> </span><span>==</span><span> </span></span><span>k</span><span> { </span><span>2.0</span><span> * </span><span>uv</span><span> } </span><span>else</span><span> { </span><span>0.0</span><span> };</span></div></div><div><div><div>47</div></div><div><span>            </span><span>j</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span> + </span><span>1</span><span><span>] </span><span>=</span><span> </span></span><span>id</span><span> + </span><span>2.0</span><span> * </span><span>u</span><span>[</span><span>i</span><span>] * </span><span>v</span><span>[</span><span>k</span><span>] - </span><span>2.0</span><span> * </span><span>v</span><span>[</span><span>i</span><span>] * </span><span>u</span><span>[</span><span>k</span><span>] + </span><span>2.0</span><span> * </span><span>q</span><span>.w * </span><span>sv</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span>    </span><span>j</span></div></div><div><div><div>51</div></div><div><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>/// 4x3 matrix with q (x) [0, omega] = xi(q) omega.</span></div></div><div><div><div>54</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>xi</span><span>(</span><span>q</span><span>: </span><span>Quat</span><span>) -&gt; </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>3</span><span>&gt; {</span></div></div><div><div><div>55</div></div><div><span>    </span><span>let</span><span> </span><span>Quat</span><span> { </span><span>w</span><span>, </span><span>x</span><span>, </span><span>y</span><span>, </span><span>z</span><span><span> } </span><span>=</span><span> </span></span><span>q</span><span>;</span></div></div><div><div><div>56</div></div><div><span>    </span><span>Mat</span><span> {</span></div></div><div><div><div>57</div></div><div><span>        </span><span>m</span><span>: [[-</span><span>x</span><span>, -</span><span>y</span><span>, -</span><span>z</span><span>], [</span><span>w</span><span>, -</span><span>z</span><span>, </span><span>y</span><span>], [</span><span>z</span><span>, </span><span>w</span><span>, -</span><span>x</span><span>], [-</span><span>y</span><span>, </span><span>x</span><span>, </span><span>w</span><span>]],</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span>}</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span>/// 4x4 matrix with q (x) p = right_mul(p) q.</span></div></div><div><div><div>62</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>right_mul</span><span>(</span><span>p</span><span>: </span><span>Quat</span><span>) -&gt; </span><span>Mat</span><span>&lt;</span><span>4</span><span>, </span><span>4</span><span>&gt; {</span></div></div><div><div><div>63</div></div><div><span>    </span><span>let</span><span> </span><span>Quat</span><span> { </span><span>w</span><span>, </span><span>x</span><span>, </span><span>y</span><span>, </span><span>z</span><span><span> } </span><span>=</span><span> </span></span><span>p</span><span>;</span></div></div><div><div><div>64</div></div><div><span>    </span><span>Mat</span><span> {</span></div></div><div><div><div>65</div></div><div><span>        </span><span>m</span><span>: [[</span><span>w</span><span>, -</span><span>x</span><span>, -</span><span>y</span><span>, -</span><span>z</span><span>], [</span><span>x</span><span>, </span><span>w</span><span>, </span><span>z</span><span>, -</span><span>y</span><span>], [</span><span>y</span><span>, -</span><span>z</span><span>, </span><span>w</span><span>, </span><span>x</span><span>], [</span><span>z</span><span>, </span><span>y</span><span>, -</span><span>x</span><span>, </span><span>w</span><span>]],</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>67</div></div><div><span>}</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>QuatEkf</span><span> {</span></div></div><div><div><div>70</div></div><div><span>    </span><span>pub</span><span> </span><span>q</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>71</div></div><div><span>    </span><span>pub</span><span> </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>72</div></div><div><span>    </span><span>pub</span><span> </span><span>p</span><span>: </span><span>Mat</span><span>&lt;</span><span>7</span><span>, </span><span>7</span><span>&gt;,</span></div></div><div><div><div>73</div></div><div><span>    </span><span>sg</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>74</div></div><div><span>    </span><span>sbw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>75</div></div><div><span>    </span><span>sa</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>76</div></div><div><span>    </span><span>sm</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>77</div></div><div><span>    </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>78</div></div><div><span>    </span><span>/// Accel gate on | |f| - g | in m/s^2; negative disables gating.</span></div></div><div><div><div>79</div></div><div><span>    </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>80</div></div><div><span>}</span></div></div><div><div><div>81</div></div><div>
</div></div><div><div><div>82</div></div><div><span>impl</span><span> </span><span>QuatEkf</span><span> {</span></div></div><div><div><div>83</div></div><div><span><span>    </span></span><span>#[allow(clippy</span><span>::</span><span>too_many_arguments)]</span></div></div><div><div><div>84</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span></div></div><div><div><div>85</div></div><div><span>        </span><span>q0</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>86</div></div><div><span>        </span><span>p0_diag</span><span>: [</span><span>Real</span><span>; </span><span>7</span><span>],</span></div></div><div><div><div>87</div></div><div><span>        </span><span>sigma_g</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>88</div></div><div><span>        </span><span>sigma_bw</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>89</div></div><div><span>        </span><span>sigma_a</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>90</div></div><div><span>        </span><span>sigma_m</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>91</div></div><div><span>        </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>92</div></div><div><span>        </span><span>accel_gate</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>93</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>94</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>95</div></div><div><span>        </span><span>for</span><span> (</span><span>i</span><span>, </span><span>d</span><span>) </span><span>in</span><span> </span><span>p0_diag</span><span>.</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>96</div></div><div><span>            </span><span>p</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> *</span></span><span>d</span><span>;</span></div></div><div><div><div>97</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>98</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>99</div></div><div><span>            </span><span>q</span><span>: </span><span>q0</span><span>,</span></div></div><div><div><div>100</div></div><div><span>            </span><span>b</span><span>: [</span><span>0.0</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>101</div></div><div><span>            </span><span>p</span><span>,</span></div></div><div><div><div>102</div></div><div><span>            </span><span>sg</span><span>: </span><span>sigma_g</span><span>,</span></div></div><div><div><div>103</div></div><div><span>            </span><span>sbw</span><span>: </span><span>sigma_bw</span><span>,</span></div></div><div><div><div>104</div></div><div><span>            </span><span>sa</span><span>: </span><span>sigma_a</span><span>,</span></div></div><div><div><div>105</div></div><div><span>            </span><span>sm</span><span>: </span><span>sigma_m</span><span>,</span></div></div><div><div><div>106</div></div><div><span>            </span><span>m_w</span><span>,</span></div></div><div><div><div>107</div></div><div><span>            </span><span>accel_gate</span><span>,</span></div></div><div><div><div>108</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>109</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>110</div></div><div>
</div></div><div><div><div>111</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>112</div></div><div><span>        </span><span>let</span><span> </span><span>omega</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>113</div></div><div><span>            </span><span>gyro</span><span>[</span><span>0</span><span>] - </span><span>self</span><span>.b[</span><span>0</span><span>],</span></div></div><div><div><div>114</div></div><div><span>            </span><span>gyro</span><span>[</span><span>1</span><span>] - </span><span>self</span><span>.b[</span><span>1</span><span>],</span></div></div><div><div><div>115</div></div><div><span>            </span><span>gyro</span><span>[</span><span>2</span><span>] - </span><span>self</span><span>.b[</span><span>2</span><span>],</span></div></div><div><div><div>116</div></div><div><span><span>        </span></span><span>];</span></div></div><div><div><div>117</div></div><div><span>        </span><span>let</span><span> </span><span>dq</span><span><span> </span><span>=</span><span> </span></span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>omega</span><span>[</span><span>0</span><span>] * </span><span>dt</span><span>, </span><span>omega</span><span>[</span><span>1</span><span>] * </span><span>dt</span><span>, </span><span>omega</span><span>[</span><span>2</span><span>] * </span><span>dt</span><span>]);</span></div></div><div><div><div>118</div></div><div><span>        </span><span>let</span><span> </span><span>xq</span><span><span> </span><span>=</span><span> </span></span><span>xi</span><span>(</span><span>self</span><span>.q);</span></div></div><div><div><div>119</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>7</span><span>, </span><span>7</span><span>&gt;::</span><span>identity</span><span>();</span></div></div><div><div><div>120</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>q</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>7</span><span>, </span><span>7</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>121</div></div><div><span>        </span><span>let</span><span> </span><span>rm</span><span><span> </span><span>=</span><span> </span></span><span>right_mul</span><span>(</span><span>dq</span><span>);</span></div></div><div><div><div>122</div></div><div><span>        </span><span>let</span><span> </span><span>qq</span><span><span> </span><span>=</span><span> (</span></span><span>xq</span><span> * </span><span>xq</span><span>.</span><span>transpose</span><span>()) * ((</span><span>0.5</span><span> * </span><span>dt</span><span>) * (</span><span>0.5</span><span> * </span><span>dt</span><span>) * </span><span>self</span><span>.sg * </span><span>self</span><span>.sg);</span></div></div><div><div><div>123</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>4</span><span> {</span></div></div><div><div><div>124</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>4</span><span> {</span></div></div><div><div><div>125</div></div><div><span>                </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>rm</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>126</div></div><div><span>                </span><span>q</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>qq</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>127</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>128</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>129</div></div><div><span>                </span><span>f</span><span>.m[</span><span>i</span><span>][</span><span>4</span><span> + </span><span>j</span><span><span>] </span><span>=</span><span> -</span></span><span>0.5</span><span> * </span><span>dt</span><span> * </span><span>xq</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>130</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>131</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>132</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>4</span><span>..</span><span>7</span><span> {</span></div></div><div><div><div>133</div></div><div><span>            </span><span>q</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.sbw * </span><span>self</span><span>.sbw * </span><span>dt</span><span>;</span></div></div><div><div><div>134</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>135</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>self</span><span>.q * </span><span>dq</span><span>;</span></div></div><div><div><div>136</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>f</span><span> * </span><span>self</span><span>.p * </span><span>f</span><span>.</span><span>transpose</span><span>() + </span><span>q</span><span>;</span></div></div><div><div><div>137</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>138</div></div><div>
</div></div><div><div><div>139</div></div><div><span>    </span><span>/// Returns false (state unchanged) if S is not positive definite.</span></div></div><div><div><div>140</div></div><div><span>    </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>r</span><span>: </span><span>Real</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>141</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>h</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>3</span><span>, </span><span>7</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>142</div></div><div><span>        </span><span>let</span><span> </span><span>hq</span><span><span> </span><span>=</span><span> </span></span><span>h_jac</span><span>(</span><span>self</span><span>.q, </span><span>v</span><span>);</span></div></div><div><div><div>143</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>144</div></div><div><span>            </span><span>h</span><span>.m[</span><span>i</span><span>][..</span><span>4</span><span>].</span><span>copy_from_slice</span><span>(&amp;</span><span>hq</span><span>.m[</span><span>i</span><span>]);</span></div></div><div><div><div>145</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>146</div></div><div><span>        </span><span>let</span><span> </span><span>pred</span><span><span> </span><span>=</span><span> </span></span><span>rot_inv</span><span>(</span><span>self</span><span>.q, </span><span>v</span><span>);</span></div></div><div><div><div>147</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span> {</span></div></div><div><div><div>148</div></div><div><span>            </span><span>m</span><span>: [[</span><span>z</span><span>[</span><span>0</span><span>] - </span><span>pred</span><span>[</span><span>0</span><span>]], [</span><span>z</span><span>[</span><span>1</span><span>] - </span><span>pred</span><span>[</span><span>1</span><span>]], [</span><span>z</span><span>[</span><span>2</span><span>] - </span><span>pred</span><span>[</span><span>2</span><span>]]],</span></div></div><div><div><div>149</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>150</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>((</span><span>dx</span><span>, </span><span>_</span><span><span>)) </span><span>=</span><span> </span></span><span>joseph_update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.p, &amp;</span><span>h</span><span><span>, &amp;(</span><span>Mat</span><span>::&lt;</span></span><span>3</span><span>, </span><span>3</span><span>&gt;::</span><span>identity</span><span>() * </span><span>r</span><span>), &amp;</span><span>y</span><span>)</span></div></div><div><div><div>151</div></div><div><span>        </span><span>else</span><span> {</span></div></div><div><div><div>152</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>153</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>154</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span></span><span>Quat</span><span> {</span></div></div><div><div><div>155</div></div><div><span>            </span><span>w</span><span>: </span><span>self</span><span>.q.w + </span><span>dx</span><span>.m[</span><span>0</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>156</div></div><div><span>            </span><span>x</span><span>: </span><span>self</span><span>.q.x + </span><span>dx</span><span>.m[</span><span>1</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>157</div></div><div><span>            </span><span>y</span><span>: </span><span>self</span><span>.q.y + </span><span>dx</span><span>.m[</span><span>2</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>158</div></div><div><span>            </span><span>z</span><span>: </span><span>self</span><span>.q.z + </span><span>dx</span><span>.m[</span><span>3</span><span>][</span><span>0</span><span>],</span></div></div><div><div><div>159</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>160</div></div><div><span><span>        </span></span><span>.</span><span>normalized</span><span>();</span></div></div><div><div><div>161</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>162</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>+=</span><span> </span></span><span>dx</span><span>.m[</span><span>4</span><span> + </span><span>i</span><span>][</span><span>0</span><span>];</span></div></div><div><div><div>163</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>164</div></div><div><span>        </span><span>true</span></div></div><div><div><div>165</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>166</div></div><div>
</div></div><div><div><div>167</div></div><div><span>    </span><span>/// Gravity-direction update. Returns false when gated out or on Cholesky failure.</span></div></div><div><div><div>168</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_accel</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>169</div></div><div><span>        </span><span>let</span><span> </span><span>norm</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>));</span></div></div><div><div><div>170</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.accel_gate </span><span>&gt;=</span><span> </span></span><span>0.0</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>fabs</span><span>(</span><span>norm</span><span> - </span><span>G</span><span>) &gt; </span><span>self</span><span>.accel_gate {</span></div></div><div><div><div>171</div></div><div><span>            </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>172</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>173</div></div><div><span>        </span><span>let</span><span> </span><span>z</span><span><span> </span><span>=</span><span> [</span></span><span>accel</span><span>[</span><span>0</span><span>] / </span><span>norm</span><span>, </span><span>accel</span><span>[</span><span>1</span><span>] / </span><span>norm</span><span>, </span><span>accel</span><span>[</span><span>2</span><span>] / </span><span>norm</span><span>];</span></div></div><div><div><div>174</div></div><div><span>        </span><span>self</span><span>.</span><span>update</span><span>(</span><span>z</span><span>, [</span><span>0.0</span><span>, </span><span>0.0</span><span>, -</span><span>1.0</span><span>], (</span><span>self</span><span>.sa / </span><span>G</span><span>) * (</span><span>self</span><span>.sa / </span><span>G</span><span>))</span></div></div><div><div><div>175</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>176</div></div><div>
</div></div><div><div><div>177</div></div><div><span>    </span><span>/// Returns false on Cholesky failure.</span></div></div><div><div><div>178</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_mag</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>179</div></div><div><span>        </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>));</span></div></div><div><div><div>180</div></div><div><span>        </span><span>self</span><span>.</span><span>update</span><span>(</span></div></div><div><div><div>181</div></div><div><span><span>            </span></span><span>[</span><span>mag</span><span>[</span><span>0</span><span>] / </span><span>n</span><span>, </span><span>mag</span><span>[</span><span>1</span><span>] / </span><span>n</span><span>, </span><span>mag</span><span>[</span><span>2</span><span>] / </span><span>n</span><span>],</span></div></div><div><div><div>182</div></div><div><span>            </span><span>self</span><span>.m_w,</span></div></div><div><div><div>183</div></div><div><span>            </span><span>self</span><span>.sm * </span><span>self</span><span>.sm,</span></div></div><div><div><div>184</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>185</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>186</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch05</span></div></div><div><div><div>2</div></div><div><span>Jacobian checks: max|H - H_fd| = 2.25e-10, max|F_qb - F_qb_fd| = 2.22e-05</span></div></div><div><div><div>3</div></div><div><span>Jacobian checks: max|H - H_fd| = 2.25e-10, max|F_qb - F_qb_fd| = 2.22e-05</span></div></div><div><div><div>4</div></div><div><span>jacobian check: max|H - H_fd| = 2.249e-10, max|F_qb - F_qb_fd| = 2.216e-05</span></div></div><div><div><div>5</div></div><div><span>Jacobian checks: max|H - H_fd| = 2.25e-10, max|F_qb - F_qb_fd| = 2.22e-5</span></div></div><div><div><div>6</div></div><div><span>ok   ekf_c.csv: max diff 9.992e-16 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   ekf_cpp.csv: max diff 1.221e-15 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>ok   ekf_rs.csv: max diff 1.665e-15 &lt;= 1.0e-09</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>RMSE over 120 s (deg), seed 1</span></div></div><div><div><div>11</div></div><div><span>method        yaw    pitch     roll    total</span></div></div><div><div><div>12</div></div><div><span>mahony      4.390    1.489    2.250    4.881</span></div></div><div><div><div>13</div></div><div><span>ekf         4.504    0.684    1.127    4.719</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>Total error by period (deg RMS)</span></div></div><div><div><div>16</div></div><div><span><span>                     </span></span><span>mahony      ekf</span></div></div><div><div><div>17</div></div><div><span>calm  [20,60) s       0.758    0.086</span></div></div><div><div><div>18</div></div><div><span>accel [60,70) s       2.331    2.640</span></div></div><div><div><div>19</div></div><div><span>mag   [90,100) s     14.851   14.820</span></div></div><div><div><div>20</div></div><div><span>roll/pitch RMS in [90,100): mahony 8.437, ekf 3.214</span></div></div><div><div><div>21</div></div><div><span>peak roll/pitch error in [90,100): mahony 22.52, ekf 8.78 deg</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个语言的雅可比检查打出同样的数：<span><span>max⁡∣H−Hfd∣=2.25×10−10\max|H-H_{fd}| = 2.25\times10^{-10}</span><span><span><span></span><span>max</span><span></span><span>∣</span><span>H</span><span></span><span>−</span><span></span></span><span><span></span><span><span>H</span><span><span><span><span><span><span></span><span><span><span>f</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>2.25</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>max⁡∣Fqb−Fqb,fd∣=2.2×10−5\max|F_{qb}-F_{qb,fd}| = 2.2\times10^{-5}</span><span><span><span></span><span>max</span><span></span><span>∣</span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>q</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>q</span><span>b</span><span>,</span><span>f</span><span>d</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>2.2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span>。滤波输出和 Python 的最大差异都在 <span><span>2×10−152\times10^{-15}</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span> 以下。</p><section><h3>对比 Mahony<a href="#对比-mahony"><span>#</span></a></h3><p>平静段 [20, 60) 秒，EKF 的总误差是 0.086 度，Mahony 是 0.758 度。这是卡尔曼滤波器的价值：它根据陀螺和加速度计各自的噪声水平自己算出增益，不需要我选 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>、<span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。整段 RMSE 4.719 对 4.881 度的小差距，来自磁干扰窗口：两个滤波器在那 10 秒里的总误差都是 14.8 度，因为磁力计的数据是错的，它们都相信了。</p><p>但是<strong>细看磁干扰窗口里的横滚和俯仰</strong>，EKF 的 RMS 是 3.214 度，Mahony 是 8.437 度；峰值 8.78 度对 22.52 度。EKF 的磁力计更新对横滚俯仰的污染比 Mahony 轻得多，原因我猜是它知道加速度计的置信度很高（<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 在横滚俯仰方向上很小），磁力计的修正主要落在偏航上；Mahony 对两个传感器的权重固定。偏航方向它俩都没有办法，那是 14.8 度总误差的主要来源。</p><p></p><figure><img src="/assets/images/kalman-filter/ch05-ekf-vs-mahony.svg" alt="EKF 与 Mahony 的总姿态误差；红色阴影为线加速度扰动，紫色阴影为磁干扰" /><figcaption>EKF 与 Mahony 的总姿态误差；红色阴影为线加速度扰动，紫色阴影为磁干扰</figcaption></figure><p></p><p></p><figure><img src="/assets/images/kalman-filter/ch05-mag-disturbance.svg" alt="磁干扰期间的俯仰和横滚误差：EKF（橙）比 Mahony（蓝）小得多，但也不是零" /><figcaption>磁干扰期间的俯仰和横滚误差：EKF（橙）比 Mahony（蓝）小得多，但也不是零</figcaption></figure><p></p></section><section><h3>加速度计门控：我设的门限从来没触发<a href="#加速度计门控我设的门限从来没触发"><span>#</span></a></h3><p>线性加速度段 [60, 70) 秒，EKF 的总误差 RMS 是 2.640 度，比平静段的 0.086 度差 30 倍，比 Mahony 的 2.331 度还差一点。我的加速度计门控把 <span><span>∣∣fm∣−g∣&gt;0.5\big||f_m|-g\big|&gt;0.5</span><span><span><span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span><span></span><span>−</span><span></span></span><span><span></span><span>g</span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>&gt;</span><span></span></span><span><span></span><span>0.5</span></span></span></span> m/s² 的样本排除。结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>accel gating (|f| - g test)</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>gate (m/s^2)  gated samples  [60,70) RMS  whole run RMS</span></div></div><div><div><div>3</div></div><div><span><span>          </span></span><span>0.5              0        2.640          4.719</span></div></div><div><div><div>4</div></div><div><span><span>          </span></span><span>0.1           1018        0.961          4.668</span></div></div><div><div><div>5</div></div><div><span><span>          </span></span><span>off              0        2.640          4.719</span></div></div><div><div><div>6</div></div><div><span>max | |f| - g | over the run: 0.332 m/s^2 (horizontal acceleration a adds only about a^2/(2 g): 2^2/(2 g) = 0.204)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>门限 0.5 一个样本都没有拦住</strong>，12000 个样本里 0 个。原因是几何：扰动是水平方向的，<span><span>∣f∣=g2+a2≈g+a2/(2g)|f| = \sqrt{g^2+a^2}\approx g+a^2/(2g)</span><span><span><span></span><span>∣</span><span>f</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>g</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>g</span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/</span><span>(</span><span>2</span><span>g</span><span>)</span></span></span></span>，<span><span>a=2a=2</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> m/s² 时只多出 0.204 m/s²，加上噪声也只有 0.332 m/s²，远没到 0.5。比力的模长对水平加速度几乎是二阶不敏感的。门限收紧到 0.1 之后，1018 个样本被拦住，该段 RMS 从 2.640 降到 0.961 度，整段 4.719 降到 4.668 度。这 1018 个里，507 个落在扰动窗口内（窗口共 1000 个样本，拦住一半），511 个是窗口外 11000 个正常样本的误拦，误拦率 4.6%。</p><p>这件事我是跑完才发现的：门控”开着”和”没有门控”的曲线完全重合（下图的蓝线和橙色虚线）。<strong>一个检测器没有触发，不代表它在工作。</strong> 门限 0.1 能拦住，是因为它已经紧到接近噪声水平（单个样本的噪声就有 0.05 m/s²），所以才有上面 4.6% 的误拦，这是个典型的权衡，不是白拿的收益。用模长检测水平线加速度本来就不是个好办法；第 6 篇我仍然沿用 0.5 的门限（保持可比），用的是同一份数据，所以它在那里同样不会触发。</p><p></p><figure><img src="/assets/images/kalman-filter/ch05-accel-gating.svg" alt="60 到 70 秒的总误差，三条曲线：不设门、门限 0.5、门限 0.1。前两条完全重合" /><figcaption>60 到 70 秒的总误差，三条曲线：不设门、门限 0.5、门限 0.1。前两条完全重合</figcaption></figure><p></p></section><section><h3>零偏估计<a href="#零偏估计"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>bias error after 30 s (rad/s): max 0.02121, RMS per axis [0.00411 0.00597 0.00573]</span></div></div><div><div><div>2</div></div><div><span>final bias: true [ 0.01953 -0.01146  0.01275], EKF [ 0.02066 -0.01307  0.01142]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>平静段里零偏估计非常好，在真值附近几乎重合。两次扰动之后它飘了：线加速度窗口里零偏估计跟着加速度计的错误摆动，磁干扰窗口里更厉害，偏离真值最大 0.021 rad/s（和零偏本身一个量级）。道理一样：偏航被错误的磁力计数据拉偏了，滤波器把”偏航在漂”解释成”零偏变了”。扰动结束之后，估计慢慢回到真值附近（最后 0.002 以内），但那需要几十秒。</p><p></p><figure><img src="/assets/images/kalman-filter/ch05-bias.svg" alt="零偏估计与真值；两段阴影时间内估计被扰动带走，之后慢慢回来" /><figcaption>零偏估计与真值；两段阴影时间内估计被扰动带走，之后慢慢回来</figcaption></figure><p></p></section><section><h3><span><span>4×44\times4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 四元数协方差<a href="#444times444-四元数协方差"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>final 4x4 quaternion covariance block</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>eigenvalues: [1.412e-09 6.411e-08 7.028e-08 2.728e-07]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>variance along q: 1.420e-09   (the manifold only has 3 directions)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一步 <span><span>PqqP_{qq}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>q</span><span>q</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的四个特征值里有一个小得多：<span><span>1.4×10−91.4\times10^{-9}</span><span><span><span></span><span>1.4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，另外三个是 <span><span>6.4×10−86.4\times10^{-8}</span><span><span><span></span><span>6.4</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>2.7×10−72.7\times10^{-7}</span><span><span><span></span><span>2.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，差了 45 到 190 倍，而且这个最小特征值对应的方向就是 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 自己（沿着 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 的方差是 <span><span>1.420×10−91.420\times10^{-9}</span><span><span><span></span><span>1.420</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>）。物理上，四元数的长度根本不是一个待估计的量，<span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 在这个方向上的数值没有意义；它小，是因为测量函数是 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 的二次齐次函数，滤波器以为自己”测到了”长度。这个多出来的方向是个自由度的”幽灵”，它会吃掉信息，也让 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 变得接近奇异（条件数大 190 倍），这是数值上的风险。</p><p>我没有证明这个幽灵会让滤波器变差，我只是不喜欢它。下一篇的方案是干脆不要在四元数上做加减：把状态拆成”名义的四元数”和”3 维的误差向量”，协方差只对 3 维误差定义。</p></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>EKF 的问题已经够明确了：4 维四元数的协方差不对，磁力计整个 3 维向量都参与更新所以会污染横滚俯仰，门控检测的方式有盲区。下一篇的误差状态卡尔曼滤波（ESKF）一次处理前两个：误差用 3 维旋转向量表示，协方差是 <span><span>6×66\times6</span><span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>6</span></span></span></span> 满秩；磁力计只用来更新偏航。它还多了一个 EKF 做不到的好处，我们会用一个 90 度的初始偏航误差来测试它。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-04-attitude-imu/</id>
      <title type="text">姿态的数学：旋转、四元数与 IMU 仿真器</title>
      <published>2026-10-14T03:00:00.000Z</published>
      <updated>2026-10-14T03:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-04-attitude-imu/"/>
      <summary type="text">卡尔曼滤波系列第 4 篇：旋转矩阵、欧拉角与万向锁、轴角与指数映射、四元数及其运动学的完整推导；一个带零偏、噪声和两段扰动的 IMU 仿真器；以及纯陀螺积分、TRIAD 和 Mahony 互补滤波三个基线，Python、C、C++、Rust 同步实现。</summary>
      <content type="html"><![CDATA[<p>先看三个数：同一份 120 秒的 IMU 数据，只用陀螺仪积分，姿态误差最后涨到 41.9 度；只用加速度计加磁力计逐点求解（TRIAD），整段的均方根误差是 5.702 度；Mahony 互补滤波是 4.881 度。这三个数里最有意思的是最后那个：它在 20 到 60 秒的平静段只有 0.758 度，在 90 到 100 秒的磁干扰段是 14.851 度。一个滤波器的好坏，要看它怎么对付”传感器在说谎”的时刻。</p>
<p>这一篇不做卡尔曼滤波。它把后面四篇需要的全部材料备齐：姿态的数学、一个能造出这种谎言的仿真器，和三个不用协方差的基线，卡尔曼滤波器之后要和它们比。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch04</code> 通过，<code>data/imu_seed1.csv</code>（12000 个样本的仿真 IMU 日志），<code>out/ch04/</code> 下 <code>gyro</code>、<code>triad</code>、<code>mahony</code> 三种方法在四种语言下的输出，三张图；C、C++、Rust 里的四元数模块（<code>kl_quat</code>、<code>quat.hpp</code>、<code>quat.rs</code>）、Mahony 滤波器和 TRIAD。四元数恒等式的自检在每个语言的 app 里先于数据处理运行。
前置：<a href="/posts/kalman-filter-03-tuning-diagnostics/">第 3 篇</a>。需要的数学：向量叉乘、矩阵指数的概念。</p></div></div>
<section><h2>约定<a href="#约定"><span>#</span></a></h2><p>这些约定在这一篇陈述一次，之后原样沿用。</p><ul>
<li>世界系 NED（北东地），机体系 FRD（前右下）。重力加速度 <span><span>g=9.80665g = 9.80665</span><span><span><span></span><span>g</span><span></span><span>=</span><span></span></span><span><span></span><span>9.80665</span></span></span></span> m/s²，世界系下 <span><span>gw=[0,0,g]T\mathbf g_w = [0,0,g]^T</span><span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>g</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。</li>
<li>四元数用 Hamilton 约定，标量在前：<span><span>q=[w,x,y,z]q = [w,x,y,z]</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>w</span><span>,</span><span></span><span>x</span><span>,</span><span></span><span>y</span><span>,</span><span></span><span>z</span><span>]</span></span></span></span>。<span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 把机体系向量旋转到世界系：<span><span>vw=q⊗vb⊗q∗v_w = q\otimes v_b\otimes q^*</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>，对应的旋转矩阵记作 <span><span>C(q)=CwbC(q) = C_{wb}</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>w</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（机体到世界）。</li>
<li>运动学 <span><span>q˙=12q⊗[0,ωb]\dot q = \tfrac12 q\otimes[0,\omega_b]</span><span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>，离散形式 <span><span>qk+1=qk⊗exp⁡(12ωΔt)q_{k+1} = q_k\otimes\exp(\tfrac12\omega\Delta t)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>ω</span><span>Δ</span><span>t</span><span>)</span></span></span></span>（这里的 <span><span>exp⁡\exp</span><span><span><span></span><span>exp</span></span></span></span> 在下面定义）。</li>
<li>误差定义（右乘，局部/机体系误差）：<span><span>qtrue=q^⊗exp⁡(12δθ)q_{\mathrm{true}} = \hat q\otimes\exp(\tfrac12\delta\theta)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>δ</span><span>θ</span><span>)</span></span></span></span>，姿态误差向量 <span><span>δθ=2log⁡(q^−1⊗qtrue)\delta\theta = 2\log(\hat q^{-1}\otimes q_{\mathrm{true}})</span><span><span><span></span><span>δ</span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span></span><span>lo<span>g</span></span><span>(</span><span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，规范化为 <span><span>w≥0w\ge0</span><span><span><span></span><span>w</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>。</li>
<li>欧拉角用 ZYX 顺序（偏航、俯仰、横滚），只用于显示和报告误差，误差折到 <span><span>[−180∘,180∘)[-180^\circ,180^\circ)</span><span><span><span></span><span>[</span><span>−</span><span>18</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>18</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。我会报告三个轴各自的欧拉角 RMSE（度）和总的旋转角误差 <span><span>∣δθ∣|\delta\theta|</span><span><span><span></span><span>∣</span><span>δ</span><span>θ</span><span>∣</span></span></span></span> 的 RMSE（度）。</li>
<li>传感器模型：
<ul>
<li>陀螺：<span><span>ωm=ω+b+ng\omega_m = \omega + b + n_g</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>ω</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 随机游走。</li>
<li>加速度计测比力：<span><span>fm=C(q)T(aw−gw)+naf_m = C(q)^T(a_w-\mathbf g_w) + n_a</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
<li>磁力计：<span><span>mm=C(q)Tmw+nmm_m = C(q)^Tm_w + n_m</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span>q</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>mw=[cos⁡50∘,0,sin⁡50∘]Tm_w = [\cos50^\circ,0,\sin50^\circ]^T</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>cos</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>sin</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（单位长度，磁倾角 50 度向下，磁偏角 0）。</li>
</ul>
</li>
</ul></section>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>旋转矩阵与 SO(3)<a href="#旋转矩阵与-so3"><span>#</span></a></h3><p>一个把机体系坐标变成世界系坐标的旋转，写成矩阵 <span><span>C∈R3×3C\in\mathbb R^{3\times3}</span><span><span><span></span><span>C</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span><span>3</span><span>×</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span>，满足 <span><span>CTC=IC^TC = I</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span></span></span></span> 和 <span><span>det⁡C=+1\det C = +1</span><span><span><span></span><span>det</span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>+</span><span>1</span></span></span></span>。所有这样的矩阵组成群 SO(3)：两个旋转相乘还是旋转，逆就是转置。它有 9 个数，但只有 3 个自由度，6 个约束把它压到一个三维曲面上。这个”3 个自由度、不能随便加减”的性质，是整个姿态估计比标量滤波难的原因。</p></section><section><h3>欧拉角与万向锁<a href="#欧拉角与万向锁"><span>#</span></a></h3><p>最直观的 3 参数表示是欧拉角。ZYX 顺序：先绕 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 转横滚角 <span><span>ϕ\phi</span><span><span><span></span><span>ϕ</span></span></span></span>，再绕 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span> 转俯仰角 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>，再绕 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 转偏航角 <span><span>ψ\psi</span><span><span><span></span><span>ψ</span></span></span></span>，</p><span><span><span>C=Rz(ψ)Ry(θ)Rx(ϕ)=[cψcθcψsθsϕ−sψcϕcψsθcϕ+sψsϕsψcθsψsθsϕ+cψcϕsψsθcϕ−cψsϕ−sθcθsϕcθcϕ]C = R_z(\psi)R_y(\theta)R_x(\phi) = \begin{bmatrix}c_\psi c_\theta &amp; c_\psi s_\theta s_\phi - s_\psi c_\phi &amp; c_\psi s_\theta c_\phi + s_\psi s_\phi\\ s_\psi c_\theta &amp; s_\psi s_\theta s_\phi + c_\psi c_\phi &amp; s_\psi s_\theta c_\phi - c_\psi s_\phi\\ -s_\theta &amp; c_\theta s_\phi &amp; c_\theta c_\phi\end{bmatrix}</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>ψ</span><span>)</span><span><span>R</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>θ</span><span>)</span><span><span>R</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>ϕ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span><span>c</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>s</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>c</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>c</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>c</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>ψ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>c</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>ϕ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>（<span><span>cc</span><span><span><span></span><span>c</span></span></span></span>、<span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 是余弦和正弦的缩写。）它的毛病在 <span><span>θ=90∘\theta = 90^\circ</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>9</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>：<span><span>cθ=0c_\theta = 0</span><span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>sθ=1s_\theta = 1</span><span><span><span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>θ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>，矩阵化简成</p><span><span><span>C=[0sin⁡(ϕ−ψ)cos⁡(ϕ−ψ)0cos⁡(ϕ−ψ)−sin⁡(ϕ−ψ)−100]C = \begin{bmatrix}0 &amp; \sin(\phi-\psi) &amp; \cos(\phi-\psi)\\ 0 &amp; \cos(\phi-\psi) &amp; -\sin(\phi-\psi)\\ -1 &amp; 0 &amp; 0\end{bmatrix}</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>−</span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>sin</span><span>(</span><span>ϕ</span><span></span><span>−</span><span></span><span>ψ</span><span>)</span></span></span><span><span></span><span><span>cos</span><span>(</span><span>ϕ</span><span></span><span>−</span><span></span><span>ψ</span><span>)</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>cos</span><span>(</span><span>ϕ</span><span></span><span>−</span><span></span><span>ψ</span><span>)</span></span></span><span><span></span><span><span>−</span><span></span><span>sin</span><span>(</span><span>ϕ</span><span></span><span>−</span><span></span><span>ψ</span><span>)</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p>只依赖 <span><span>ϕ−ψ\phi-\psi</span><span><span><span></span><span>ϕ</span><span></span><span>−</span><span></span></span><span><span></span><span>ψ</span></span></span></span>，而不是它们各自的值：偏航和横滚退化成同一个自由度，丢了一个。这就是万向锁。数值验证：<span><span>(ψ,ϕ)=(20∘,10∘)(\psi,\phi) = (20^\circ,10^\circ)</span><span><span><span></span><span>(</span><span>ψ</span><span>,</span><span></span><span>ϕ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>2</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 和 <span><span>(57∘,47∘)(57^\circ,47^\circ)</span><span><span><span></span><span>(</span><span>5</span><span><span>7</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>4</span><span><span>7</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>)</span></span></span></span>（差值都是 <span><span>−10∘-10^\circ</span><span><span><span></span><span>−</span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>）在 <span><span>θ=90∘\theta=90^\circ</span><span><span><span></span><span>θ</span><span></span><span>=</span><span></span></span><span><span></span><span>9</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span> 给出的矩阵，最大元素差是 <span><span>8.33×10−178.33\times10^{-17}</span><span><span><span></span><span>8.33</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>17</span></span></span></span></span></span></span></span></span></span></span></span>，是同一个旋转。</p><p></p><figure><img src="/assets/images/kalman-filter/ch04-gimbal-lock.svg" alt="绕一个接近 y 轴的轴匀速旋转时的欧拉角：俯仰上升到 90 度之后，偏航和横滚在大约 20 度的旋转范围里同时跳变 170 度，而旋转本身是光滑的" /><figcaption>绕一个接近 y 轴的轴匀速旋转时的欧拉角：俯仰上升到 90 度之后，偏航和横滚在大约 20 度的旋转范围里同时跳变 170 度，而旋转本身是光滑的</figcaption></figure><p></p><p>在奇异点附近，一个很小的旋转会让偏航和横滚剧烈变化。我这份仿真数据里，真值的俯仰角在 <span><span>[−77.3∘,80.9∘][-77.3^\circ,80.9^\circ]</span><span><span><span></span><span>[</span><span>−</span><span>77.</span><span><span>3</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>80.</span><span><span>9</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>]</span></span></span></span> 之间摆动，离奇异点不到 10 度，这意味着后面报告的偏航、横滚的欧拉角 RMSE 会被放大。总旋转角误差 <span><span>∣δθ∣|\delta\theta|</span><span><span><span></span><span>∣</span><span>δ</span><span>θ</span><span>∣</span></span></span></span> 没有这个毛病，所以我把它当作主要指标。<strong>结论：欧拉角只用来显示，滤波器内部不用。</strong></p></section><section><h3>轴角、Rodrigues 与指数映射<a href="#轴角rodrigues-与指数映射"><span>#</span></a></h3><p>另一个 3 参数表示是旋转向量 <span><span>v=θnv = \theta n</span><span><span><span></span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>θ</span><span>n</span></span></span></span>：绕单位轴 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 转角度 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>。Rodrigues 公式把它变成矩阵：</p><span><span><span>C=I+sin⁡θ [n]×+(1−cos⁡θ) [n]×2,[n]×=[0−nznynz0−nx−nynx0]C = I + \sin\theta\,[n]_\times + (1-\cos\theta)\,[n]_\times^2,\qquad [n]_\times = \begin{bmatrix}0&amp;-n_z&amp;n_y\\n_z&amp;0&amp;-n_x\\-n_y&amp;n_x&amp;0\end{bmatrix}</span><span><span><span></span><span>C</span><span></span><span>=</span><span></span></span><span><span></span><span>I</span><span></span><span>+</span><span></span></span><span><span></span><span>sin</span><span></span><span>θ</span><span></span><span>[</span><span>n</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>cos</span><span></span><span>θ</span><span>)</span><span></span><span>[</span><span>n</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span>[</span><span>n</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>n</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>n</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span>n</span><span><span><span><span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>n</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>n</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>n</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>[a]×b=a×b[a]_\times b = a\times b</span><span><span><span></span><span>[</span><span>a</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span>×</span><span></span></span><span><span></span><span>b</span></span></span></span>。推导：把向量 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 分解成沿轴的分量 <span><span>(n⋅x)n(n\cdot x)n</span><span><span><span></span><span>(</span><span>n</span><span></span><span>⋅</span><span></span></span><span><span></span><span>x</span><span>)</span><span>n</span></span></span></span> 和垂直分量 <span><span>x⊥x_\perp</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>⊥</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>；沿轴的不变，垂直分量在垂直平面内转 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>：<span><span>x⊥cos⁡θ+(n×x)sin⁡θx_\perp\cos\theta + (n\times x)\sin\theta</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>⊥</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>sin</span><span></span><span>θ</span></span></span></span>。合起来 <span><span>Cx=(n⋅x)n+x⊥cos⁡θ+(n×x)sin⁡θCx = (n\cdot x)n + x_\perp\cos\theta + (n\times x)\sin\theta</span><span><span><span></span><span>C</span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>⋅</span><span></span></span><span><span></span><span>x</span><span>)</span><span>n</span><span></span><span>+</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>⊥</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>cos</span><span></span><span>θ</span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>sin</span><span></span><span>θ</span></span></span></span>，用 <span><span>x⊥=x−(n⋅x)nx_\perp = x - (n\cdot x)n</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>⊥</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>⋅</span><span></span></span><span><span></span><span>x</span><span>)</span><span>n</span></span></span></span> 和 <span><span>[n]×2x=n×(n×x)=(n⋅x)n−x[n]_\times^2x = n\times(n\times x) = (n\cdot x)n - x</span><span><span><span></span><span>[</span><span>n</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>n</span><span></span><span>⋅</span><span></span></span><span><span></span><span>x</span><span>)</span><span>n</span><span></span><span>−</span><span></span></span><span><span></span><span>x</span></span></span></span> 整理即得。</p><p>这个 <span><span>v↦Cv\mapsto C</span><span><span><span></span><span>v</span><span></span><span>↦</span><span></span></span><span><span></span><span>C</span></span></span></span> 就是 SO(3) 上的指数映射 <span><span>exp⁡([v]×)\exp([v]_\times)</span><span><span><span></span><span>exp</span><span>([</span><span>v</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，反方向是对数映射。它是后面所有章节的基本工具：小的旋转，用 3 维向量表示，可以随便加、随便做线性运算；大的旋转，用指数映射接到群上。</p></section><section><h3>四元数<a href="#四元数"><span>#</span></a></h3><p>四元数是 4 维的 <span><span>q=[w,u]q = [w,u]</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>w</span><span>,</span><span></span><span>u</span><span>]</span></span></span></span>，<span><span>u∈R3u\in\mathbb R^3</span><span><span><span></span><span>u</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span></span></span></span>。乘法（Hamilton 积）：</p><span><span><span>a⊗b=[awbw−au⋅bu,  awbu+bwau+au×bu]a\otimes b = \big[a_wb_w - a_u\cdot b_u,\ \ a_wb_u + b_wa_u + a_u\times b_u\big]</span><span><span><span></span><span>a</span><span></span><span>⊗</span><span></span></span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span><span>[</span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span> </span><span> </span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>u</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span><p>共轭 <span><span>q∗=[w,−u]q^* = [w,-u]</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>w</span><span>,</span><span></span><span>−</span><span>u</span><span>]</span></span></span></span>，范数 <span><span>∣q∣2=w2+u⋅u|q|^2 = w^2+u\cdot u</span><span><span><span></span><span>∣</span><span>q</span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>u</span></span></span></span>，单位四元数 <span><span>∣q∣=1|q|=1</span><span><span><span></span><span>∣</span><span>q</span><span>∣</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的逆等于共轭。单位四元数 <span><span>q=[cos⁡θ2, nsin⁡θ2]q = [\cos\tfrac\theta2,\ n\sin\tfrac\theta2]</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>cos</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>θ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span> </span><span></span><span>n</span><span></span><span>sin</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>θ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>]</span></span></span></span> 表示绕 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 转 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>。这里有一个双重覆盖：<span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 与 <span><span>−q-q</span><span><span><span></span><span>−</span><span>q</span></span></span></span> 是同一个旋转（<span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span> 与 <span><span>θ+2π\theta+2\pi</span><span><span><span></span><span>θ</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>π</span></span></span></span> 给出相反符号）。我的约定是误差四元数规范化到 <span><span>w≥0w\ge0</span><span><span><span></span><span>w</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>。</p><p><strong>四元数旋转向量。</strong> 把 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 嵌成 <span><span>[0,v][0,v]</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>v</span><span>]</span></span></span></span>，计算 <span><span>q⊗[0,v]⊗q∗q\otimes[0,v]\otimes q^*</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>v</span><span>]</span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>。先算 <span><span>p=q⊗[0,v]=[−u⋅v, wv+u×v]p = q\otimes[0,v] = [-u\cdot v,\ wv+u\times v]</span><span><span><span></span><span>p</span><span></span><span>=</span><span></span></span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>v</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>−</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>,</span><span> </span><span></span><span>w</span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>]</span></span></span></span>，再乘 <span><span>q∗=[w,−u]q^* = [w,-u]</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>w</span><span>,</span><span></span><span>−</span><span>u</span><span>]</span></span></span></span>，向量部分是</p><span><span><span>p0(−u)+w pv+pv×(−u)=(u⋅v)u+w2v+w u×v−w v×u−(u×v)×up_0(-u) + w\,p_v + p_v\times(-u) = (u\cdot v)u + w^2v + w\,u\times v - w\,v\times u - (u\times v)\times u</span><span><span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>−</span><span>u</span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>−</span><span>u</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span>u</span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span><span></span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>w</span><span></span><span>v</span><span></span><span>×</span><span></span></span><span><span></span><span>u</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>u</span></span></span></span></span><p><span><span>−w v×u=w u×v-w\,v\times u = w\,u\times v</span><span><span><span></span><span>−</span><span>w</span><span></span><span>v</span><span></span><span>×</span><span></span></span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>w</span><span></span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span></span></span></span>；<span><span>(u×v)×u=(u⋅u)v−(u⋅v)u(u\times v)\times u = (u\cdot u)v - (u\cdot v)u</span><span><span><span></span><span>(</span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>)</span><span></span><span>×</span><span></span></span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>u</span><span>)</span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span>u</span></span></span></span>（三重积展开）。代入得</p><span><span><span>C(q)v=(w2−u⋅u) v+2(u⋅v) u+2w (u×v)C(q)v = (w^2-u\cdot u)\,v + 2(u\cdot v)\,u + 2w\,(u\times v)</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span><span>v</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>w</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>u</span><span>)</span><span></span><span>v</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>(</span><span>u</span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span></span><span>u</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>w</span><span></span><span>(</span><span>u</span><span></span><span>×</span><span></span></span><span><span></span><span>v</span><span>)</span></span></span></span></span><p>这个式子很重要：它的第一种写法是 <span><span>q⊗v⊗q∗q\otimes v\otimes q^*</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>v</span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span></span></span></span>（便于推导），第二种写法是矩阵 <span><span>C(q)C(q)</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span></span></span></span>（便于实现），对应代码里的 <code>qrot</code>。第 5 篇求雅可比时会用到它。</p><p><strong>指数映射与对数映射。</strong> 旋转向量 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 对应的四元数是</p><span><span><span>exp⁡(v)=[cos⁡∣v∣2,  v∣v∣sin⁡∣v∣2]\exp(v) = \big[\cos\tfrac{|v|}2,\ \ \tfrac{v}{|v|}\sin\tfrac{|v|}2\big]</span><span><span><span></span><span>exp</span><span>(</span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>[</span></span><span></span><span>cos</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>∣</span><span>v</span><span>∣</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span> </span><span> </span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>∣</span><span>v</span><span>∣</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>v</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>sin</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>∣</span><span>v</span><span>∣</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>]</span></span></span></span></span></span><p>（我写 <span><span>exp⁡(v)\exp(v)</span><span><span><span></span><span>exp</span><span>(</span><span>v</span><span>)</span></span></span></span> 指的是旋转向量 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 对应的四元数，在级数记号里它是 <span><span>exp⁡(12[0,v])\exp(\tfrac12[0,v])</span><span><span><span></span><span>exp</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>[</span><span>0</span><span>,</span><span></span><span>v</span><span>])</span></span></span></span>。）<span><span>∣v∣→0|v|\to0</span><span><span><span></span><span>∣</span><span>v</span><span>∣</span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span> 时 <span><span>sin⁡(∣v∣/2)/∣v∣→12\sin(|v|/2)/|v|\to\tfrac12</span><span><span><span></span><span>sin</span><span>(</span><span>∣</span><span>v</span><span>∣/2</span><span>)</span><span>/∣</span><span>v</span><span>∣</span><span></span><span>→</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span>，<span><span>cos⁡→1−∣v∣2/8\cos\to1-|v|^2/8</span><span><span><span></span><span>cos</span><span></span><span>→</span><span></span></span><span><span></span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>∣</span><span>v</span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/8</span></span></span></span>，代码里 <span><span>∣v∣&lt;10−8|v|&lt;10^{-8}</span><span><span><span></span><span>∣</span><span>v</span><span>∣</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span> 就用这个一阶展开，免得除以零。反方向：<span><span>log⁡q=2atan2⁡(∣u∣,w) u/∣u∣\log q = 2\operatorname{atan2}(|u|,w)\,u/|u|</span><span><span><span></span><span>lo<span>g</span></span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span></span><span><span>atan2</span></span><span>(</span><span>∣</span><span>u</span><span>∣</span><span>,</span><span></span><span>w</span><span>)</span><span></span><span>u</span><span>/∣</span><span>u</span><span>∣</span></span></span></span>（先把 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 规范化到 <span><span>w≥0w\ge0</span><span><span><span></span><span>w</span><span></span><span>≥</span><span></span></span><span><span></span><span>0</span></span></span></span>）。用 <span><span>atan2⁡\operatorname{atan2}</span><span><span><span></span><span><span>atan2</span></span></span></span></span> 而不是 <span><span>arccos⁡\arccos</span><span><span><span></span><span>arccos</span></span></span></span>，在小角度时数值上稳得多。</p><p><strong>运动学。</strong> 设机体以角速度 <span><span>ωb\omega_b</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（机体系表示）转动。经过一小段 <span><span>dtdt</span><span><span><span></span><span>d</span><span>t</span></span></span></span>，机体系相对于上一时刻的机体系，转了一个小旋转 <span><span>ωbdt\omega_bdt</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>d</span><span>t</span></span></span></span>，所以</p><span><span><span>q(t+dt)=q(t)⊗exp⁡(ωbdt)≈q(t)⊗[1,12ωbdt]q(t+dt) = q(t)\otimes\exp(\omega_bdt)\approx q(t)\otimes\big[1,\tfrac12\omega_bdt\big]</span><span><span><span></span><span>q</span><span>(</span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>d</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>q</span><span>(</span><span>t</span><span>)</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>d</span><span>t</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>q</span><span>(</span><span>t</span><span>)</span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>[</span></span><span>1</span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>d</span><span>t</span><span><span>]</span></span></span></span></span></span><p>（右乘，因为 <span><span>ωb\omega_b</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是在机体系里表示的。）展开一阶项，<span><span>q˙=12q⊗[0,ωb]\dot q = \tfrac12q\otimes[0,\omega_b]</span><span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>。如果 <span><span>ωb\omega_b</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在整个 <span><span>Δt\Delta t</span><span><span><span></span><span>Δ</span><span>t</span></span></span></span> 内恒定，那么 <span><span>qk+1=qk⊗exp⁡(ωΔt)q_{k+1} = q_k\otimes\exp(\omega\Delta t)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>ω</span><span>Δ</span><span>t</span><span>)</span></span></span></span> 是精确的，不是近似：这正是离散模型。这条精确式的好处是不需要小步长假设，也永远保持单位范数（至多舍入误差，代码里再归一化一次）。</p><p><strong>小角度近似。</strong> <span><span>exp⁡(δθ)≈[1,12δθ]\exp(\delta\theta)\approx[1,\tfrac12\delta\theta]</span><span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>θ</span><span>)</span><span></span><span>≈</span><span></span></span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>δ</span><span>θ</span><span>]</span></span></span></span>，误差是 <span><span>O(∣δθ∣2)O(|\delta\theta|^2)</span><span><span><span></span><span>O</span><span>(</span><span>∣</span><span>δ</span><span>θ</span><span><span>∣</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。第 6 篇的误差状态滤波器整个建立在它上面。</p><p><strong>斜对称矩阵。</strong> <span><span>[v]×[v]_\times</span><span><span><span></span><span>[</span><span>v</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 把叉乘变成矩阵乘法，上面已经定义。后面经常用到性质：<span><span>[a]×b=−[b]×a[a]_\times b = -[b]_\times a</span><span><span><span></span><span>[</span><span>a</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>[</span><span>b</span><span><span>]</span><span><span><span><span><span><span></span><span><span>×</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>a</span></span></span></span>。</p></section><section><h3>三个传感器测的是什么<a href="#三个传感器测的是什么"><span>#</span></a></h3><ul>
<li><strong>陀螺仪</strong>测机体系下的角速度，加上零偏和噪声。零偏是缓慢变化的，不能被忽略：我的仿真里初始零偏是 <span><span>[0.02,−0.01,0.015][0.02,-0.01,0.015]</span><span><span><span></span><span>[</span><span>0.02</span><span>,</span><span></span><span>−</span><span>0.01</span><span>,</span><span></span><span>0.015</span><span>]</span></span></span></span> rad/s，不处理它，120 秒会积出 <span><span>0.02×120=2.40.02\times120 = 2.4</span><span><span><span></span><span>0.02</span><span></span><span>×</span><span></span></span><span><span></span><span>120</span><span></span><span>=</span><span></span></span><span><span></span><span>2.4</span></span></span></span> rad 的误差。</li>
<li><strong>加速度计</strong>测的不是重力加速度，而是比力（specific force）：<span><span>fm=CT(aw−gw)+naf_m = C^T(a_w-\mathbf g_w)+n_a</span><span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span><span>a</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>g</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。静止时 <span><span>aw=0a_w=0</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>f=−CTgwf = -C^T\mathbf g_w</span><span><span><span></span><span>f</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>g</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，机体水平时是 <span><span>[0,0,−g][0,0,-g]</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>g</span><span>]</span></span></span></span>：加速度计”感觉到的”是地面向上的支持力，在 FRD 系下 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 轴向下，所以读数是负的。线性加速度会直接污染它，这是它的致命弱点。</li>
<li><strong>磁力计</strong>测地磁场在机体系下的方向。我们已知世界系下的磁场 <span><span>mwm_w</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
</ul></section><section><h3>为什么加速度计只给横滚和俯仰，磁力计给偏航<a href="#为什么加速度计只给横滚和俯仰磁力计给偏航"><span>#</span></a></h3><p>加速度计提供一个世界系已知方向（竖直方向）在机体系下的观测。一个方向观测只约束两个自由度：把竖直轴对齐之后，绕竖直轴转任意角度，观测都不变，那就是偏航。所以加速度计能得到横滚和俯仰，得不到偏航。磁力计提供第二个方向（磁场方向），它和重力方向不平行（有 50 度磁倾角），两个不平行的方向合起来约束全部 3 个自由度。</p><p>TRIAD 就是用这一点的最小方案：用两个方向观测直接构造出姿态。机体系观测 <span><span>b1b_1</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（比力的方向）、<span><span>b2b_2</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（磁场方向），世界系参考 <span><span>r1=[0,0,−1]r_1 = [0,0,-1]</span><span><span><span></span><span><span>r</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span>]</span></span></span></span>、<span><span>r2=mwr_2 = m_w</span><span><span><span></span><span><span>r</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。分别构造正交标架：</p><span><span><span>t1=v1,t2=v1×v2∣v1×v2∣,t3=t1×t2t_1 = v_1,\quad t_2 = \frac{v_1\times v_2}{|v_1\times v_2|},\quad t_3 = t_1\times t_2</span><span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∣</span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>v</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>Cwb=[t1r t2r t3r] [t1b t2b t3b]TC_{wb} = [t_1^r\ t_2^r\ t_3^r]\,[t_1^b\ t_2^b\ t_3^b]^T</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>w</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>t</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span> </span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span> </span><span><span>t</span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span></span><span>[</span><span><span>t</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span> </span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span> </span><span><span>t</span><span><span><span><span><span><span></span><span><span>3</span></span></span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，把机体系标架旋转到世界系标架。第一个向量（这里是比力方向）被精确对齐，第二个向量只有垂直于第一个向量的部分被使用，这意味着 TRIAD 把全部可信度给了加速度计，磁力计只用来定偏航。它没有滤波，每个样本独立计算，噪声和扰动直接传进来。</p></section></section>
<section><h2>仿真器<a href="#仿真器"><span>#</span></a></h2><p>仿真器只在 Python 里（<code>python/kl/imu_sim.py</code>）：C、C++、Rust 的任务是处理已经记录好的数据，这也正是硬件上的工作流。</p><p><strong>真值。</strong> 机体系角速度</p><span><span><span>ωb(t)=[0.8sin⁡(2π⋅0.20 t), 0.6sin⁡(2π⋅0.13 t+1.0), 1.0sin⁡(2π⋅0.07 t+2.0)] rad/s\omega_b(t) = \big[0.8\sin(2\pi\cdot0.20\,t),\ 0.6\sin(2\pi\cdot0.13\,t+1.0),\ 1.0\sin(2\pi\cdot0.07\,t+2.0)\big]\ \text{rad/s}</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>[</span></span><span>0.8</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.20</span><span></span><span>t</span><span>)</span><span>,</span><span> </span><span></span><span>0.6</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.13</span><span></span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>1.0</span><span>)</span><span>,</span><span> </span><span></span><span>1.0</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.07</span><span></span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>2.0</span><span>)</span><span><span>]</span></span><span> </span><span><span>rad/s</span></span></span></span></span></span><p>以 1 kHz 用精确四元数指数积分（每步用区间中点的角速度），初始姿态是单位四元数，每 10 步取一个样本，得到 100 Hz 的 IMU 数据，120 秒共 12000 个样本。三个轴用互不通约的频率，是为了让姿态在 120 秒内不重复。</p><p><strong>陀螺。</strong> 陀螺读数是区间平均角速度 <span><span>rotvec⁡(qk−1−1⊗qk)/Δt\operatorname{rotvec}(q_{k-1}^{-1}\otimes q_k)/\Delta t</span><span><span><span></span><span><span>rotvec</span></span><span>(</span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/Δ</span><span>t</span></span></span></span> 加零偏加噪声。用区间平均而不是采样时刻的瞬时值，是因为第 3 篇那个教训：离散滤波模型 <span><span>qk=qk−1⊗exp⁡(ωmΔt)q_k = q_{k-1}\otimes\exp(\omega_m\Delta t)</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span>)</span></span></span></span> 隐含的就是平均角速度，瞬时值会在滤波器和仿真器之间留下一个系统性的模型差。</p><p><strong>噪声与零偏。</strong> 陀螺每样本噪声 <span><span>σg=0.005\sigma_g = 0.005</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.005</span></span></span></span> rad/s，零偏初值 <span><span>[0.02,−0.01,0.015][0.02,-0.01,0.015]</span><span><span><span></span><span>[</span><span>0.02</span><span>,</span><span></span><span>−</span><span>0.01</span><span>,</span><span></span><span>0.015</span><span>]</span></span></span></span> rad/s，随机游走 <span><span>σbw=10−4\sigma_{bw} = 10^{-4}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rad/s/<span><span>s\sqrt{\text{s}}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>s</span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>（每样本标准差 <span><span>σbwΔt\sigma_{bw}\sqrt{\Delta t}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>Δ</span><span>t</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>）。加速度计 <span><span>σa=0.05\sigma_a = 0.05</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.05</span></span></span></span> m/s²，磁力计 <span><span>σm=0.01\sigma_m = 0.01</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span>（单位场）。这些是典型消费级 MEMS 的量级，不是某个具体型号的数据手册。</p><p><strong>两段扰动。</strong></p><ul>
<li>线性加速度：<span><span>t∈[60,70)t\in[60,70)</span><span><span><span></span><span>t</span><span></span><span>∈</span><span></span></span><span><span></span><span>[</span><span>60</span><span>,</span><span></span><span>70</span><span>)</span></span></span></span> 秒内加上世界系下 <span><span>aw=[2sin⁡(2π⋅0.5 t),0,0]a_w = [2\sin(2\pi\cdot0.5\,t),0,0]</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>2</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.5</span><span></span><span>t</span><span>)</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span> m/s²，CSV 里 <code>acc_dist = 1</code>。</li>
<li>磁干扰：<span><span>t∈[90,100)t\in[90,100)</span><span><span><span></span><span>t</span><span></span><span>∈</span><span></span></span><span><span></span><span>[</span><span>90</span><span>,</span><span></span><span>100</span><span>)</span></span></span></span> 秒内世界系磁场被加上 <span><span>[0.3,0.3,0][0.3,0.3,0]</span><span><span><span></span><span>[</span><span>0.3</span><span>,</span><span></span><span>0.3</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span>，CSV 里 <code>mag_dist = 1</code>。</li>
</ul><p></p><figure><img src="/assets/images/kalman-filter/ch04-imu-signals.svg" alt="仿真的陀螺、加速度计、磁力计信号；红色阴影是线加速度扰动，紫色阴影是磁干扰" /><figcaption>仿真的陀螺、加速度计、磁力计信号；红色阴影是线加速度扰动，紫色阴影是磁干扰</figcaption></figure><p></p><p>CSV 的列是 <code>t,gx,gy,gz,ax,ay,az,mx,my,mz,qw,qx,qy,qz,bx,by,bz,acc_dist,mag_dist</code>。滤波器不允许读后面的 9 列（真值四元数、真零偏和扰动标志），它们只在评估时用。种子 1 用于单次运行，之后的蒙特卡洛用种子 1 到 50。</p></section>
<section><h2>Mahony 互补滤波<a href="#mahony-互补滤波"><span>#</span></a></h2><p>第 1 篇留了一个观察：常增益的一维卡尔曼滤波，就是对”陀螺推进后的预测”做一阶低通，加速度计提供低频、陀螺提供高频。Mahony 滤波器是它在 SO(3) 上的非线性版本。</p><p>设当前估计 <span><span>q^\hat q</span><span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，它预测的世界系方向在机体系下是 <span><span>v^a=C(q^)T[0,0,−1]T\hat v_a = C(\hat q)^T[0,0,-1]^T</span><span><span><span></span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>（竖直）和 <span><span>v^m=C(q^)Tmw\hat v_m = C(\hat q)^Tm_w</span><span><span><span></span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>C</span><span>(</span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（磁场）。实际测到的单位向量 <span><span>an=fm/∣fm∣a_n = f_m/|f_m|</span><span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/∣</span><span><span>f</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span>，<span><span>mn=mm/∣mm∣m_n = m_m/|m_m|</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/∣</span><span><span>m</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span>。我们要一个小的机体系旋转 <span><span>δ\delta</span><span><span><span></span><span>δ</span></span></span></span>，使预测向量对准测量向量。给 <span><span>q^\hat q</span><span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 右乘 <span><span>exp⁡(δ)\exp(\delta)</span><span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>)</span></span></span></span> 之后，新预测是 <span><span>C(q^⊗exp⁡(δ))Tvw=exp⁡(δ)Tv^≈v^−δ×v^=v^+v^×δC(\hat q\otimes\exp(\delta))^T v_w = \exp(\delta)^T\hat v\approx\hat v - \delta\times\hat v = \hat v+\hat v\times\delta</span><span><span><span></span><span>C</span><span>(</span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span>)</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>w</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>exp</span><span>(</span><span>δ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>δ</span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>δ</span></span></span></span>。要它等于测量 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span>，解 <span><span>v^×δ=v−v^\hat v\times\delta = v-\hat v</span><span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>δ</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>；对单位向量，垂直于 <span><span>v^\hat v</span><span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 的那部分有解 <span><span>δ=v×v^\delta = v\times\hat v</span><span><span><span></span><span>δ</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>（验证：<span><span>v^×(v×v^)=v−(v^⋅v)v^\hat v\times(v\times\hat v) = v - (\hat v\cdot v)\hat v</span><span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>(</span><span>v</span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>(</span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>v</span><span>)</span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span>，正好是 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 垂直于 <span><span>v^\hat v</span><span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 的分量）。两个观测的修正取和：</p><span><span><span>e=an×v^a+mn×v^me = a_n\times\hat v_a + m_n\times\hat v_m</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span><span>a</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>ee</span><span><span><span></span><span>e</span></span></span></span> 就是”当前估计与测量之间的失配”，是个机体系下的小旋转向量。Mahony 把它当作角速度的修正项，并且对它积分来估计零偏：</p><span><span><span>b←b−kie Δt,q^←q^⊗exp⁡((ωm−b+kpe)Δt)b\leftarrow b - k_ie\,\Delta t,\qquad \hat q\leftarrow\hat q\otimes\exp\big((\omega_m - b + k_pe)\Delta t\big)</span><span><span><span></span><span>b</span><span></span><span>←</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>Δ</span><span>t</span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span></span><span><span>(</span></span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span>)</span><span>Δ</span><span>t</span><span><span>)</span></span></span></span></span></span><p><span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 的符号是这么确定的：稳态下 <span><span>kpe=−(btrue−b^)k_pe = -(b_{\mathrm{true}}-\hat b)</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>(</span><span><span>b</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>b</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>)</span></span></span></span>（失配平衡掉零偏误差），所以 <span><span>ee</span><span><span><span></span><span>e</span></span></span></span> 是负的零偏误差的 <span><span>1/kp1/k_p</span><span><span><span></span><span>1/</span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 倍；要让 <span><span>b^\hat b</span><span><span><span></span><span><span><span><span><span><span></span><span>b</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 趋向 <span><span>btrueb_{\mathrm{true}}</span><span><span><span></span><span><span>b</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，需要 <span><span>b^\hat b</span><span><span><span></span><span><span><span><span><span><span></span><span>b</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span></span></span></span> 沿 <span><span>−e-e</span><span><span><span></span><span>−</span><span>e</span></span></span></span> 方向增长，即 <span><span>b^←b^−kieΔt\hat b\leftarrow\hat b - k_ie\Delta t</span><span><span><span></span><span><span><span><span><span><span></span><span>b</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>←</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>b</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>e</span><span>Δ</span><span>t</span></span></span></span>。</p><p><span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 控制”多快相信测量”，对应第 1 篇 <span><span>K∞/ΔtK_\infty/\Delta t</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/Δ</span><span>t</span></span></span></span>（时间常数的倒数）；<span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 控制零偏估计多快。我取 <span><span>kp=1.0k_p = 1.0</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1.0</span></span></span></span>，<span><span>ki=0.05k_i = 0.05</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.05</span></span></span></span> 作为基线，没有调。</p><p><strong>它的局限也很明显</strong>：<span><span>ee</span><span><span><span></span><span>e</span></span></span></span> 只是方向失配的几何修正，没有协方差，不知道哪个传感器此刻更可信，加速度计和磁力计的权重固定相等；磁干扰会通过 <span><span>mn×v^mm_n\times\hat v_m</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 直接改变横滚和俯仰，而不仅仅是偏航（下面的数据会看到）。第 5 篇之后的卡尔曼滤波器要解决的就是这几件事。</p><p>另外两个基线：<strong>纯陀螺积分</strong>（初始姿态用第一个样本的 TRIAD，之后只积分陀螺），<strong>TRIAD</strong>（每个样本独立求解）。</p></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><section><h3>四元数<a href="#四元数-1"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Quaternion helpers. Hamilton convention, scalar first q = [w, x, y, z].</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>q rotates body to world: v_w = q (x) [0, v_b] (x) q*, and C(q) = C_wb.</span></div></div><div><div><div>4</div></div><div><span>`qexp(v)` is the unit quaternion of the rotation by rotation vector v, i.e. the</span></div></div><div><div><div>5</div></div><div><span>exp(v/2) of the series notation: [cos(|v|/2), sin(|v|/2) v/|v|].</span></div></div><div><div><div>6</div></div><div><span>"""</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>SMALL</span><span> </span><span>=</span><span> </span></span><span>1e-8</span><span>  </span><span># below this angle (rad) use the Taylor expansion</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>qmul</span><span>(</span><span>a</span><span>,</span><span><span> </span><span>b</span></span><span>):</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>aw, ax, ay, az </span><span>=</span><span> a</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>bw, bx, by, bz </span><span>=</span><span> b</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([</span></span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> bw </span><span>-</span><span> ax </span><span>*</span><span> bx </span><span>-</span><span> ay </span><span>*</span><span> by </span><span>-</span><span> az </span><span>*</span><span> bz,</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> bx </span><span>+</span><span> ax </span><span>*</span><span> bw </span><span>+</span><span> ay </span><span>*</span><span> bz </span><span>-</span><span> az </span><span>*</span><span> by,</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> by </span><span>-</span><span> ax </span><span>*</span><span> bz </span><span>+</span><span> ay </span><span>*</span><span> bw </span><span>+</span><span> az </span><span>*</span><span> bx,</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>aw </span><span>*</span><span> bz </span><span>+</span><span> ax </span><span>*</span><span> by </span><span>-</span><span> ay </span><span>*</span><span> bx </span><span>+</span><span> az </span><span>*</span><span> bw,</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>])</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>def</span><span> </span><span>qconj</span><span>(</span><span>q</span><span>):</span></div></div><div><div><div>25</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([q[</span></span><span>0</span><span><span>], </span><span>-</span><span>q[</span></span><span>1</span><span><span>], </span><span>-</span><span>q[</span></span><span>2</span><span><span>], </span><span>-</span><span>q[</span></span><span>3</span><span>]])</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>def</span><span> </span><span>qnormalize</span><span>(</span><span>q</span><span>):</span></div></div><div><div><div>29</div></div><div><span>    </span><span>return</span><span><span> q </span><span>/</span><span> np.</span><span>sqrt</span><span>(q </span><span>@</span><span> q)</span></span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>def</span><span> </span><span>qexp</span><span>(</span><span>v</span><span>):</span></div></div><div><div><div>33</div></div><div><span>    </span><span>"""Rotation vector -&gt; unit quaternion."""</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>th </span><span>=</span><span> np.</span><span>sqrt</span><span>(v </span><span>@</span><span> v)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>if</span><span><span> th </span><span>&lt;</span><span> </span><span>SMALL</span><span>:</span></span></div></div><div><div><div>36</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([</span></span><span>1.0</span><span><span> </span><span>-</span><span> th </span><span>*</span><span> th </span><span>/</span><span> </span></span><span>8.0</span><span><span>, </span><span>*</span><span>(</span></span><span>0.5</span><span><span> </span><span>*</span><span> v)])</span></span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> np.</span><span>sin</span><span>(</span><span>0.5</span><span><span> </span><span>*</span><span> th) </span><span>/</span><span> th</span></span></div></div><div><div><div>38</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([np.</span><span>cos</span><span>(</span></span><span>0.5</span><span><span> </span><span>*</span><span> th), </span><span>*</span><span>(s </span><span>*</span><span> v)])</span></span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>def</span><span> </span><span>qlog</span><span>(</span><span>q</span><span>):</span></div></div><div><div><div>42</div></div><div><span>    </span><span>"""Unit quaternion -&gt; rotation vector, using the sign with w &gt;= 0 (angle in [0, pi])."""</span></div></div><div><div><div>43</div></div><div><span>    </span><span>if</span><span> q[</span><span>0</span><span><span>] </span><span>&lt;</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> </span><span>-</span><span>q</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>u </span><span>=</span><span> q[</span><span>1</span><span>:]</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> np.</span><span>sqrt</span><span>(u </span><span>@</span><span> u)</span></div></div><div><div><div>47</div></div><div><span>    </span><span>if</span><span><span> n </span><span>&lt;</span><span> </span><span>SMALL</span><span>:</span></span></div></div><div><div><div>48</div></div><div><span>        </span><span>return</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> u </span><span>/</span><span> q[</span></span><span>0</span><span>]</span></div></div><div><div><div>49</div></div><div><span>    </span><span>return</span><span> (</span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>arctan2</span><span>(n, q[</span></span><span>0</span><span><span>]) </span><span>/</span><span> n) </span><span>*</span><span> u</span></span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>def</span><span> </span><span>qrot</span><span>(</span><span>q</span><span>):</span></div></div><div><div><div>53</div></div><div><span>    </span><span>"""Direction cosine matrix C(q) = C_wb (body -&gt; world)."""</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>w, x, y, z </span><span>=</span><span> q</span></div></div><div><div><div>55</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([</span></span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>[</span><span>1</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> (y </span><span>*</span><span> y </span><span>+</span><span> z </span><span>*</span><span> z), </span></span><span>2</span><span><span> </span><span>*</span><span> (x </span><span>*</span><span> y </span><span>-</span><span> w </span><span>*</span><span> z), </span></span><span>2</span><span><span> </span><span>*</span><span> (x </span><span>*</span><span> z </span><span>+</span><span> w </span><span>*</span><span> y)],</span></span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>[</span><span>2</span><span><span> </span><span>*</span><span> (x </span><span>*</span><span> y </span><span>+</span><span> w </span><span>*</span><span> z), </span></span><span>1</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> (x </span><span>*</span><span> x </span><span>+</span><span> z </span><span>*</span><span> z), </span></span><span>2</span><span><span> </span><span>*</span><span> (y </span><span>*</span><span> z </span><span>-</span><span> w </span><span>*</span><span> x)],</span></span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>[</span><span>2</span><span><span> </span><span>*</span><span> (x </span><span>*</span><span> z </span><span>-</span><span> w </span><span>*</span><span> y), </span></span><span>2</span><span><span> </span><span>*</span><span> (y </span><span>*</span><span> z </span><span>+</span><span> w </span><span>*</span><span> x), </span></span><span>1</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> (x </span><span>*</span><span> x </span><span>+</span><span> y </span><span>*</span><span> y)],</span></span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>])</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span>def</span><span> </span><span>dcm_to_quat</span><span>(</span><span>C</span><span>):</span></div></div><div><div><div>63</div></div><div><span>    </span><span>"""C_wb -&gt; unit quaternion with w &gt;= 0 (Shepperd's method)."""</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> C[</span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>+</span><span> C[</span></span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>+</span><span> C[</span></span><span>2</span><span>, </span><span>2</span><span>]</span></div></div><div><div><div>65</div></div><div><span>    </span><span>if</span><span><span> t </span><span>&gt;</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>66</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>sqrt</span><span>(</span></span><span>1.0</span><span><span> </span><span>+</span><span> t)</span></span></div></div><div><div><div>67</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.25</span><span><span> </span><span>*</span><span> s, (C[</span></span><span>2</span><span>, </span><span>1</span><span><span>] </span><span>-</span><span> C[</span></span><span>1</span><span>, </span><span>2</span><span><span>]) </span><span>/</span><span> s, (C[</span></span><span>0</span><span>, </span><span>2</span><span><span>] </span><span>-</span><span> C[</span></span><span>2</span><span>, </span><span>0</span><span><span>]) </span><span>/</span><span> s, (C[</span></span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>-</span><span> C[</span></span><span>0</span><span>, </span><span>1</span><span><span>]) </span><span>/</span><span> s])</span></span></div></div><div><div><div>68</div></div><div><span>    </span><span>elif</span><span> C[</span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>&gt;=</span><span> C[</span></span><span>1</span><span>, </span><span>1</span><span>] </span><span>and</span><span> C[</span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>&gt;=</span><span> C[</span></span><span>2</span><span>, </span><span>2</span><span>]:</span></div></div><div><div><div>69</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>sqrt</span><span>(</span></span><span>1.0</span><span><span> </span><span>+</span><span> C[</span></span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>-</span><span> C[</span></span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>-</span><span> C[</span></span><span>2</span><span>, </span><span>2</span><span>])</span></div></div><div><div><div>70</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> np.</span><span>array</span><span>([(C[</span><span>2</span><span>, </span><span>1</span><span><span>] </span><span>-</span><span> C[</span></span><span>1</span><span>, </span><span>2</span><span><span>]) </span><span>/</span><span> s, </span></span><span>0.25</span><span><span> </span><span>*</span><span> s, (C[</span></span><span>0</span><span>, </span><span>1</span><span><span>] </span><span>+</span><span> C[</span></span><span>1</span><span>, </span><span>0</span><span><span>]) </span><span>/</span><span> s, (C[</span></span><span>0</span><span>, </span><span>2</span><span><span>] </span><span>+</span><span> C[</span></span><span>2</span><span>, </span><span>0</span><span><span>]) </span><span>/</span><span> s])</span></span></div></div><div><div><div>71</div></div><div><span>    </span><span>elif</span><span> C[</span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>&gt;=</span><span> C[</span></span><span>2</span><span>, </span><span>2</span><span>]:</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>sqrt</span><span>(</span></span><span>1.0</span><span><span> </span><span>+</span><span> C[</span></span><span>1</span><span>, </span><span>1</span><span><span>] </span><span>-</span><span> C[</span></span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>-</span><span> C[</span></span><span>2</span><span>, </span><span>2</span><span>])</span></div></div><div><div><div>73</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> np.</span><span>array</span><span>([(C[</span><span>0</span><span>, </span><span>2</span><span><span>] </span><span>-</span><span> C[</span></span><span>2</span><span>, </span><span>0</span><span><span>]) </span><span>/</span><span> s, (C[</span></span><span>0</span><span>, </span><span>1</span><span><span>] </span><span>+</span><span> C[</span></span><span>1</span><span>, </span><span>0</span><span><span>]) </span><span>/</span><span> s, </span></span><span>0.25</span><span><span> </span><span>*</span><span> s, (C[</span></span><span>1</span><span>, </span><span>2</span><span><span>] </span><span>+</span><span> C[</span></span><span>2</span><span>, </span><span>1</span><span><span>]) </span><span>/</span><span> s])</span></span></div></div><div><div><div>74</div></div><div><span>    </span><span>else</span><span>:</span></div></div><div><div><div>75</div></div><div><span><span>        </span></span><span>s </span><span>=</span><span> </span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>sqrt</span><span>(</span></span><span>1.0</span><span><span> </span><span>+</span><span> C[</span></span><span>2</span><span>, </span><span>2</span><span><span>] </span><span>-</span><span> C[</span></span><span>0</span><span>, </span><span>0</span><span><span>] </span><span>-</span><span> C[</span></span><span>1</span><span>, </span><span>1</span><span>])</span></div></div><div><div><div>76</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> np.</span><span>array</span><span>([(C[</span><span>1</span><span>, </span><span>0</span><span><span>] </span><span>-</span><span> C[</span></span><span>0</span><span>, </span><span>1</span><span><span>]) </span><span>/</span><span> s, (C[</span></span><span>0</span><span>, </span><span>2</span><span><span>] </span><span>+</span><span> C[</span></span><span>2</span><span>, </span><span>0</span><span><span>]) </span><span>/</span><span> s, (C[</span></span><span>1</span><span>, </span><span>2</span><span><span>] </span><span>+</span><span> C[</span></span><span>2</span><span>, </span><span>1</span><span><span>]) </span><span>/</span><span> s, </span></span><span>0.25</span><span><span> </span><span>*</span><span> s])</span></span></div></div><div><div><div>77</div></div><div><span>    </span><span>return</span><span><span> </span><span>-</span><span>q </span></span><span>if</span><span> q[</span><span>0</span><span><span>] </span><span>&lt;</span><span> </span></span><span>0</span><span> </span><span>else</span><span> q</span></div></div><div><div><div>78</div></div><div>
</div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>def</span><span> </span><span>skew</span><span>(</span><span>v</span><span>):</span></div></div><div><div><div>81</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([[</span></span><span>0.0</span><span><span>, </span><span>-</span><span>v[</span></span><span>2</span><span>], v[</span><span>1</span><span>]], [v[</span><span>2</span><span>], </span><span>0.0</span><span><span>, </span><span>-</span><span>v[</span></span><span>0</span><span><span>]], [</span><span>-</span><span>v[</span></span><span>1</span><span>], v[</span><span>0</span><span>], </span><span>0.0</span><span>]])</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>def</span><span> </span><span>euler_zyx</span><span>(</span><span>q</span><span>):</span></div></div><div><div><div>85</div></div><div><span>    </span><span>"""(yaw, pitch, roll) in radians for C_wb = Rz(yaw) Ry(pitch) Rx(roll)."""</span></div></div><div><div><div>86</div></div><div><span><span>    </span></span><span>w, x, y, z </span><span>=</span><span> q</span></div></div><div><div><div>87</div></div><div><span><span>    </span></span><span>yaw </span><span>=</span><span> np.</span><span>arctan2</span><span>(</span><span>2</span><span><span> </span><span>*</span><span> (w </span><span>*</span><span> z </span><span>+</span><span> x </span><span>*</span><span> y), </span></span><span>1</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> (y </span><span>*</span><span> y </span><span>+</span><span> z </span><span>*</span><span> z))</span></span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>pitch </span><span>=</span><span> np.</span><span>arcsin</span><span>(np.</span><span>clip</span><span>(</span><span>2</span><span><span> </span><span>*</span><span> (w </span><span>*</span><span> y </span><span>-</span><span> z </span><span>*</span><span> x), </span><span>-</span></span><span>1.0</span><span>, </span><span>1.0</span><span>))</span></div></div><div><div><div>89</div></div><div><span><span>    </span></span><span>roll </span><span>=</span><span> np.</span><span>arctan2</span><span>(</span><span>2</span><span><span> </span><span>*</span><span> (w </span><span>*</span><span> x </span><span>+</span><span> y </span><span>*</span><span> z), </span></span><span>1</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> (x </span><span>*</span><span> x </span><span>+</span><span> y </span><span>*</span><span> y))</span></span></div></div><div><div><div>90</div></div><div><span>    </span><span>return</span><span> yaw, pitch, roll</span></div></div><div><div><div>91</div></div><div>
</div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span>def</span><span> </span><span>att_error</span><span>(</span><span>q_est</span><span>,</span><span><span> </span><span>q_true</span></span><span>):</span></div></div><div><div><div>94</div></div><div><span>    </span><span>"""Attitude error vector dtheta = 2 log(q_est^-1 (x) q_true), in the estimate's body frame."""</span></div></div><div><div><div>95</div></div><div><span>    </span><span>return</span><span><span> </span><span>qlog</span><span>(</span><span>qmul</span><span>(</span><span>qconj</span><span>(q_est), q_true))</span></span></div></div><div><div><div>96</div></div><div>
</div></div><div><div><div>97</div></div><div>
</div></div><div><div><div>98</div></div><div><span>def</span><span> </span><span>wrap_pi</span><span>(</span><span>a</span><span>):</span></div></div><div><div><div>99</div></div><div><span>    </span><span>"""Wrap an angle to [-pi, pi)."""</span></div></div><div><div><div>100</div></div><div><span>    </span><span>return</span><span><span> (a </span><span>+</span><span> np.pi) </span><span>%</span><span> (</span></span><span>2</span><span><span> </span><span>*</span><span> np.pi) </span><span>-</span><span> np.pi</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>每个函数都对应上面的一条公式。几点实现细节：</p><ul>
<li><code>qexp</code> 和 <code>qlog</code> 在 <code>SMALL = 1e-8</code> 以下走泰勒展开，这个阈值在四种语言里完全一致，否则小角度那一支的舍入会出现跨语言差异。</li>
<li><code>qlog</code> 先把 <span><span>w&lt;0w&lt;0</span><span><span><span></span><span>w</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>0</span></span></span></span> 的四元数取反，保证返回的是 <span><span>[0,π][0,\pi]</span><span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>π</span><span>]</span></span></span></span> 内最短的旋转。</li>
<li><code>dcm_to_quat</code> 是 Shepperd 方法：对矩阵的迹和三个对角元里最大的那个分情况，避免对接近零的数开方后再相除。</li>
<li><code>euler_zyx</code> 里 <code>np.clip</code> 防止舍入误差把 <code>arcsin</code> 的参数推到 <span><span>±1\pm1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> 之外。</li>
<li><code>att_error</code> 返回 <span><span>log⁡(q^−1⊗qtrue)\log(\hat q^{-1}\otimes q_{\mathrm{true}})</span><span><span><span></span><span>lo<span>g</span></span><span>(</span><span><span><span><span><span><span><span></span><span>q</span></span><span><span></span><span><span>^</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span><span><span>true</span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，没有乘 2：<code>qlog</code> 本身已经返回旋转向量（含了那个 2），它就是约定里的 <span><span>δθ\delta\theta</span><span><span><span></span><span>δ</span><span>θ</span></span></span></span>。</li>
</ul></section><section><h3>仿真器<a href="#仿真器-1"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""IMU simulator: truth attitude, gyro, accelerometer, magnetometer (NED world, FRD body)."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>from</span><span> pathlib </span><span>import</span><span> Path</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>from</span><span> .io </span><span>import</span><span> write_csv</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>ROOT</span><span> </span><span>=</span><span> </span><span>Path</span><span>(</span></span><span>__file__</span><span><span>).</span><span>resolve</span><span>().parents[</span></span><span>2</span><span>]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>G </span><span>=</span><span> </span></span><span>9.80665</span></div></div><div><div><div>12</div></div><div><span><span>GRAVITY_W</span><span> </span><span>=</span><span> np.</span><span>array</span><span>([</span></span><span>0.0</span><span>, </span><span>0.0</span><span>, G])</span></div></div><div><div><div>13</div></div><div><span><span>DIP</span><span> </span><span>=</span><span> np.</span><span>radians</span><span>(</span></span><span>50.0</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>MAG_W</span><span> </span><span>=</span><span> np.</span><span>array</span><span>([np.</span><span>cos</span><span>(</span><span>DIP</span><span>), </span></span><span>0.0</span><span><span>, np.</span><span>sin</span><span>(</span><span>DIP</span><span>)])</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>GYRO_SIGMA</span><span> </span><span>=</span><span> </span></span><span>0.005</span><span>  </span><span># rad/s per sample</span></div></div><div><div><div>17</div></div><div><span><span>BIAS0</span><span> </span><span>=</span><span> np.</span><span>array</span><span>([</span></span><span>0.02</span><span><span>, </span><span>-</span></span><span>0.01</span><span>, </span><span>0.015</span><span>])  </span><span># rad/s</span></div></div><div><div><div>18</div></div><div><span><span>BIAS_RW</span><span> </span><span>=</span><span> </span></span><span>1e-4</span><span>  </span><span># rad/s/sqrt(s)</span></div></div><div><div><div>19</div></div><div><span><span>ACC_SIGMA</span><span> </span><span>=</span><span> </span></span><span>0.05</span><span>  </span><span># m/s^2</span></div></div><div><div><div>20</div></div><div><span><span>MAG_SIGMA</span><span> </span><span>=</span><span> </span></span><span>0.01</span><span>  </span><span># unit-norm field</span></div></div><div><div><div>21</div></div><div><span><span>ACC_WINDOW</span><span> </span><span>=</span><span> (</span></span><span>60.0</span><span>, </span><span>70.0</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>MAG_WINDOW</span><span> </span><span>=</span><span> (</span></span><span>90.0</span><span>, </span><span>100.0</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>MAG_DISTURB</span><span> </span><span>=</span><span> np.</span><span>array</span><span>([</span></span><span>0.3</span><span>, </span><span>0.3</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>def</span><span> </span><span>rate_b</span><span>(</span><span>t</span><span>):</span></div></div><div><div><div>27</div></div><div><span>    </span><span>"""True body angular rate (rad/s)."""</span></div></div><div><div><div>28</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([</span></span></div></div><div><div><div>29</div></div><div><span>        </span><span>0.8</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> np.pi </span><span>*</span><span> </span></span><span>0.20</span><span><span> </span><span>*</span><span> t),</span></span></div></div><div><div><div>30</div></div><div><span>        </span><span>0.6</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> np.pi </span><span>*</span><span> </span></span><span>0.13</span><span><span> </span><span>*</span><span> t </span><span>+</span><span> </span></span><span>1.0</span><span>),</span></div></div><div><div><div>31</div></div><div><span>        </span><span>1.0</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> np.pi </span><span>*</span><span> </span></span><span>0.07</span><span><span> </span><span>*</span><span> t </span><span>+</span><span> </span></span><span>2.0</span><span>),</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>])</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>def</span><span> </span><span>_qmul</span><span>(</span><span>a</span><span>,</span><span><span> </span><span>b</span></span><span>):  </span><span># plain-float quaternion product, hot loop</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>aw, ax, ay, az </span><span>=</span><span> a</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>bw, bx, by, bz </span><span>=</span><span> b</span></div></div><div><div><div>38</div></div><div><span>    </span><span>return</span><span><span> (aw </span><span>*</span><span> bw </span><span>-</span><span> ax </span><span>*</span><span> bx </span><span>-</span><span> ay </span><span>*</span><span> by </span><span>-</span><span> az </span><span>*</span><span> bz,</span></span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>aw </span><span>*</span><span> bx </span><span>+</span><span> ax </span><span>*</span><span> bw </span><span>+</span><span> ay </span><span>*</span><span> bz </span><span>-</span><span> az </span><span>*</span><span> by,</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>aw </span><span>*</span><span> by </span><span>-</span><span> ax </span><span>*</span><span> bz </span><span>+</span><span> ay </span><span>*</span><span> bw </span><span>+</span><span> az </span><span>*</span><span> bx,</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>aw </span><span>*</span><span> bz </span><span>+</span><span> ax </span><span>*</span><span> by </span><span>-</span><span> ay </span><span>*</span><span> bx </span><span>+</span><span> az </span><span>*</span><span> bw)</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>def</span><span> </span><span>_qexp</span><span>(</span><span>v</span><span>):</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>th </span><span>=</span><span> (v[</span><span>0</span><span><span>] </span><span>*</span><span> v[</span></span><span>0</span><span><span>] </span><span>+</span><span> v[</span></span><span>1</span><span><span>] </span><span>*</span><span> v[</span></span><span>1</span><span><span>] </span><span>+</span><span> v[</span></span><span>2</span><span><span>] </span><span>*</span><span> v[</span></span><span>2</span><span><span>]) </span><span>**</span><span> </span></span><span>0.5</span></div></div><div><div><div>46</div></div><div><span>    </span><span>if</span><span><span> th </span><span>&lt;</span><span> </span></span><span>1e-8</span><span>:</span></div></div><div><div><div>47</div></div><div><span>        </span><span>return</span><span> (</span><span>1.0</span><span><span> </span><span>-</span><span> th </span><span>*</span><span> th </span><span>/</span><span> </span></span><span>8.0</span><span>, </span><span>0.5</span><span><span> </span><span>*</span><span> v[</span></span><span>0</span><span>], </span><span>0.5</span><span><span> </span><span>*</span><span> v[</span></span><span>1</span><span>], </span><span>0.5</span><span><span> </span><span>*</span><span> v[</span></span><span>2</span><span>])</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> np.</span><span>sin</span><span>(</span><span>0.5</span><span><span> </span><span>*</span><span> th) </span><span>/</span><span> th</span></span></div></div><div><div><div>49</div></div><div><span>    </span><span>return</span><span><span> (np.</span><span>cos</span><span>(</span></span><span>0.5</span><span><span> </span><span>*</span><span> th), s </span><span>*</span><span> v[</span></span><span>0</span><span><span>], s </span><span>*</span><span> v[</span></span><span>1</span><span><span>], s </span><span>*</span><span> v[</span></span><span>2</span><span>])</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>def</span><span> </span><span>a_world</span><span>(</span><span>t</span><span>):</span></div></div><div><div><div>53</div></div><div><span>    </span><span>"""Linear acceleration disturbance (world frame, m/s^2)."""</span></div></div><div><div><div>54</div></div><div><span>    </span><span>if</span><span><span> </span><span>ACC_WINDOW</span><span>[</span></span><span>0</span><span><span>] </span><span>&lt;=</span><span> t </span><span>&lt;</span><span> </span><span>ACC_WINDOW</span><span>[</span></span><span>1</span><span>]:</span></div></div><div><div><div>55</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>array</span><span>([</span></span><span>2.0</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(</span></span><span>2</span><span><span> </span><span>*</span><span> np.pi </span><span>*</span><span> </span></span><span>0.5</span><span><span> </span><span>*</span><span> t), </span></span><span>0.0</span><span>, </span><span>0.0</span><span>])</span></div></div><div><div><div>56</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>zeros</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>def</span><span> </span><span>simulate</span><span>(</span><span>seed</span><span>,</span><span><span> </span><span>duration</span></span><span>=</span><span>120.0</span><span>,</span><span><span> </span><span>imu_rate</span></span><span>=</span><span>100.0</span><span>,</span><span><span> </span><span>truth_rate</span></span><span>=</span><span>1000.0</span><span>,</span><span><span> </span><span>write</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>60</div></div><div><span>    </span><span>from</span><span> .quat </span><span>import</span><span> qconj, qlog, qrot</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(seed)</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>dt </span><span>=</span><span> </span><span>1.0</span><span><span> </span><span>/</span><span> imu_rate</span></span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>sub </span><span>=</span><span> </span><span>int</span><span>(</span><span>round</span><span><span>(truth_rate </span><span>/</span><span> imu_rate))</span></span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>h </span><span>=</span><span> </span><span>1.0</span><span><span> </span><span>/</span><span> truth_rate</span></span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>int</span><span>(</span><span>round</span><span><span>(duration </span><span>*</span><span> imu_rate))</span></span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>    </span><span># truth attitude at the IMU sample instants, integrated at truth_rate</span></div></div><div><div><div>69</div></div><div><span>    </span><span># (exact quaternion exponential of the mid-interval rate); one extra step</span></div></div><div><div><div>70</div></div><div><span>    </span><span># before t = 0 gives the first sample its average rate as well.</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>q </span><span>=</span><span> (</span><span>1.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>0.0</span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>    </span></span><span>qs </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>4</span><span>))</span></div></div><div><div><div>73</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>74</div></div><div><span><span>        </span></span><span>qs[k] </span><span>=</span><span> q</span></div></div><div><div><div>75</div></div><div><span>        </span><span>for</span><span> j </span><span>in</span><span> </span><span>range</span><span>(sub):</span></div></div><div><div><div>76</div></div><div><span><span>            </span></span><span>t_mid </span><span>=</span><span> k </span><span>*</span><span> dt </span><span>+</span><span> (j </span><span>+</span><span> </span><span>0.5</span><span><span>) </span><span>*</span><span> h</span></span></div></div><div><div><div>77</div></div><div><span><span>            </span></span><span>q </span><span>=</span><span> </span><span>_qmul</span><span>(q, </span><span>_qexp</span><span>(</span><span>rate_b</span><span>(t_mid) </span><span>*</span><span> h))</span></div></div><div><div><div>78</div></div><div><span><span>        </span></span><span>nq </span><span>=</span><span> (q[</span><span>0</span><span><span>] </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>+</span><span> q[</span></span><span>1</span><span><span>] </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>+</span><span> q[</span></span><span>2</span><span><span>] </span><span>**</span><span> </span></span><span>2</span><span><span> </span><span>+</span><span> q[</span></span><span>3</span><span><span>] </span><span>**</span><span> </span></span><span>2</span><span><span>) </span><span>**</span><span> </span></span><span>0.5</span></div></div><div><div><div>79</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> </span><span>tuple</span><span><span>(c </span><span>/</span><span> nq </span></span><span>for</span><span> c </span><span>in</span><span> q)</span></div></div><div><div><div>80</div></div><div>
</div></div><div><div><div>81</div></div><div><span>    </span><span># gyro reports the rate averaged over the sample interval: rotvec(q_{k-1}^-1 q_k) / dt.</span></div></div><div><div><div>82</div></div><div><span>    </span><span># For k = 0 there is no previous sample, so use the instantaneous rate.</span></div></div><div><div><div>83</div></div><div><span><span>    </span></span><span>avg </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>3</span><span>))</span></div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>avg[</span><span>0</span><span><span>] </span><span>=</span><span> </span><span>rate_b</span><span>(</span></span><span>0.0</span><span>)</span></div></div><div><div><div>85</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, n):</span></div></div><div><div><div>86</div></div><div><span><span>        </span></span><span>avg[k] </span><span>=</span><span> </span><span>qlog</span><span>(</span><span>_qmul_np</span><span>(</span><span>qconj</span><span>(qs[k </span><span>-</span><span> </span><span>1</span><span><span>]), qs[k])) </span><span>/</span><span> dt</span></span></div></div><div><div><div>87</div></div><div>
</div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>bias </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>3</span><span>))</span></div></div><div><div><div>89</div></div><div><span><span>    </span></span><span>bias[</span><span>0</span><span><span>] </span><span>=</span><span> </span><span>BIAS0</span></span></div></div><div><div><div>90</div></div><div><span><span>    </span></span><span>bias[</span><span>1</span><span><span>:] </span><span>=</span><span> </span><span>BIAS0</span><span> </span><span>+</span><span> np.</span><span>cumsum</span><span>(</span><span>BIAS_RW</span><span> </span><span>*</span><span> np.</span><span>sqrt</span><span>(dt) </span><span>*</span><span> rng.</span><span>standard_normal</span><span>((n </span><span>-</span><span> </span></span><span>1</span><span>, </span><span>3</span><span>)), </span><span>axis</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>91</div></div><div><span><span>    </span></span><span>gyro </span><span>=</span><span> avg </span><span>+</span><span> bias </span><span>+</span><span> </span><span>GYRO_SIGMA</span><span> </span><span>*</span><span> rng.</span><span>standard_normal</span><span>((n, </span><span>3</span><span>))</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>arange</span><span>(n) </span><span>*</span><span> dt</span></div></div><div><div><div>94</div></div><div><span><span>    </span></span><span>acc </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>3</span><span>))</span></div></div><div><div><div>95</div></div><div><span><span>    </span></span><span>mag </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>3</span><span>))</span></div></div><div><div><div>96</div></div><div><span><span>    </span></span><span>acc_dist </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>97</div></div><div><span><span>    </span></span><span>mag_dist </span><span>=</span><span> np.</span><span>zeros</span><span>(n)</span></div></div><div><div><div>98</div></div><div><span><span>    </span></span><span>na </span><span>=</span><span> </span><span>ACC_SIGMA</span><span> </span><span>*</span><span> rng.</span><span>standard_normal</span><span>((n, </span><span>3</span><span>))</span></div></div><div><div><div>99</div></div><div><span><span>    </span></span><span>nm </span><span>=</span><span> </span><span>MAG_SIGMA</span><span> </span><span>*</span><span> rng.</span><span>standard_normal</span><span>((n, </span><span>3</span><span>))</span></div></div><div><div><div>100</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>101</div></div><div><span><span>        </span></span><span>C </span><span>=</span><span> </span><span>qrot</span><span>(qs[k])</span></div></div><div><div><div>102</div></div><div><span><span>        </span></span><span>aw </span><span>=</span><span> </span><span>a_world</span><span>(t[k])</span></div></div><div><div><div>103</div></div><div><span><span>        </span></span><span>mw </span><span>=</span><span> </span><span>MAG_W</span><span>.</span><span>copy</span><span>()</span></div></div><div><div><div>104</div></div><div><span>        </span><span>if</span><span><span> </span><span>MAG_WINDOW</span><span>[</span></span><span>0</span><span><span>] </span><span>&lt;=</span><span> t[k] </span><span>&lt;</span><span> </span><span>MAG_WINDOW</span><span>[</span></span><span>1</span><span>]:</span></div></div><div><div><div>105</div></div><div><span><span>            </span></span><span>mw </span><span>=</span><span> mw </span><span>+</span><span> </span><span>MAG_DISTURB</span></div></div><div><div><div>106</div></div><div><span><span>            </span></span><span>mag_dist[k] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>107</div></div><div><span>        </span><span>if</span><span><span> </span><span>ACC_WINDOW</span><span>[</span></span><span>0</span><span><span>] </span><span>&lt;=</span><span> t[k] </span><span>&lt;</span><span> </span><span>ACC_WINDOW</span><span>[</span></span><span>1</span><span>]:</span></div></div><div><div><div>108</div></div><div><span><span>            </span></span><span>acc_dist[k] </span><span>=</span><span> </span><span>1.0</span></div></div><div><div><div>109</div></div><div><span><span>        </span></span><span>acc[k] </span><span>=</span><span> C.T </span><span>@</span><span> (aw </span><span>-</span><span> </span><span>GRAVITY_W</span><span>) </span><span>+</span><span> na[k]</span></div></div><div><div><div>110</div></div><div><span><span>        </span></span><span>mag[k] </span><span>=</span><span> C.T </span><span>@</span><span> mw </span><span>+</span><span> nm[k]</span></div></div><div><div><div>111</div></div><div>
</div></div><div><div><div>112</div></div><div><span><span>    </span></span><span>cols </span><span>=</span><span> {</span></div></div><div><div><div>113</div></div><div><span>        </span><span>"t"</span><span>: t,</span></div></div><div><div><div>114</div></div><div><span>        </span><span>"gx"</span><span>: gyro[:, </span><span>0</span><span>], </span><span>"gy"</span><span>: gyro[:, </span><span>1</span><span>], </span><span>"gz"</span><span>: gyro[:, </span><span>2</span><span>],</span></div></div><div><div><div>115</div></div><div><span>        </span><span>"ax"</span><span>: acc[:, </span><span>0</span><span>], </span><span>"ay"</span><span>: acc[:, </span><span>1</span><span>], </span><span>"az"</span><span>: acc[:, </span><span>2</span><span>],</span></div></div><div><div><div>116</div></div><div><span>        </span><span>"mx"</span><span>: mag[:, </span><span>0</span><span>], </span><span>"my"</span><span>: mag[:, </span><span>1</span><span>], </span><span>"mz"</span><span>: mag[:, </span><span>2</span><span>],</span></div></div><div><div><div>117</div></div><div><span>        </span><span>"qw"</span><span>: qs[:, </span><span>0</span><span>], </span><span>"qx"</span><span>: qs[:, </span><span>1</span><span>], </span><span>"qy"</span><span>: qs[:, </span><span>2</span><span>], </span><span>"qz"</span><span>: qs[:, </span><span>3</span><span>],</span></div></div><div><div><div>118</div></div><div><span>        </span><span>"bx"</span><span>: bias[:, </span><span>0</span><span>], </span><span>"by"</span><span>: bias[:, </span><span>1</span><span>], </span><span>"bz"</span><span>: bias[:, </span><span>2</span><span>],</span></div></div><div><div><div>119</div></div><div><span>        </span><span>"acc_dist"</span><span>: acc_dist, </span><span>"mag_dist"</span><span>: mag_dist,</span></div></div><div><div><div>120</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>121</div></div><div><span>    </span><span>if</span><span> write:</span></div></div><div><div><div>122</div></div><div><span><span>        </span></span><span>write_csv</span><span>(</span><span>ROOT</span><span> </span><span>/</span><span> </span><span>"data"</span><span><span> </span><span>/</span><span> </span></span><span>f</span><span>"imu_seed</span><span>{</span><span>seed</span><span>}</span><span>.csv"</span><span>, cols)</span></div></div><div><div><div>123</div></div><div><span>    </span><span>return</span><span> cols</span></div></div><div><div><div>124</div></div><div>
</div></div><div><div><div>125</div></div><div>
</div></div><div><div><div>126</div></div><div><span>def</span><span> </span><span>_qmul_np</span><span>(</span><span>a</span><span>,</span><span><span> </span><span>b</span></span><span>):</span></div></div><div><div><div>127</div></div><div><span>    </span><span>from</span><span> .quat </span><span>import</span><span> qmul</span></div></div><div><div><div>128</div></div><div>
</div></div><div><div><div>129</div></div><div><span>    </span><span>return</span><span><span> </span><span>qmul</span><span>(a, b)</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>热点循环（每个样本里 10 步指数积分）用普通元组实现 <code>_qmul</code>、<code>_qexp</code>，不走 numpy 小数组，120 秒的数据 4 秒内生成完。<code>simulate</code> 的结构：先积分真值，再由真值算出区间平均角速度，再叠加零偏和噪声，最后由真值姿态算加速度计和磁力计读数。</p></section><section><h3>Mahony 与基线<a href="#mahony-与基线"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Mahony explicit complementary filter with an integral bias term."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>from</span><span> .imu_sim </span><span>import</span><span><span> </span><span>MAG_W</span></span></div></div><div><div><div>6</div></div><div><span>from</span><span> .quat </span><span>import</span><span> qexp, qmul, qnormalize, qrot</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>class</span><span><span> </span><span>Mahony</span></span><span>:</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"""q &lt;- q (x) exp((w_m - b + kp e) dt), b &lt;- b - ki e dt,</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>e = a_meas x a_pred + m_meas x m_pred (unit vectors in the body frame)."""</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>kp</span></span><span>,</span><span><span> </span><span>ki</span></span><span>,</span><span><span> </span><span>q0</span></span><span>,</span><span><span> </span><span>m_w</span></span><span><span>=</span><span>MAG_W</span><span>):</span></span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span>.kp, </span><span>self</span><span><span>.ki </span><span>=</span><span> kp, ki</span></span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> np.</span><span>asarray</span><span>(q0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> np.</span><span>zeros</span><span>(</span></span><span>3</span><span>)</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span><span>.m_w </span><span>=</span><span> m_w</span></span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>def</span><span> </span><span>step</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>gyro</span></span><span>,</span><span><span> </span><span>accel</span></span><span>,</span><span><span> </span><span>mag</span></span><span>,</span><span><span> </span><span>dt</span></span><span>):</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>C </span><span>=</span><span> </span><span>qrot</span><span>(</span><span>self</span><span>.q)</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>a_pred </span><span>=</span><span> C.T </span><span>@</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>m_pred </span><span>=</span><span> C.T </span><span>@</span><span> </span><span>self</span><span>.m_w</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>a_n </span><span>=</span><span> accel </span><span>/</span><span> np.</span><span>sqrt</span><span>(accel </span><span>@</span><span> accel)</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>m_n </span><span>=</span><span> mag </span><span>/</span><span> np.</span><span>sqrt</span><span>(mag </span><span>@</span><span> mag)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>e </span><span>=</span><span> np.</span><span>cross</span><span>(a_n, a_pred) </span><span>+</span><span> np.</span><span>cross</span><span>(m_n, m_pred)</span></div></div><div><div><div>26</div></div><div><span>        </span><span>self</span><span><span>.b </span><span>=</span><span> </span></span><span>self</span><span><span>.b </span><span>-</span><span> </span></span><span>self</span><span><span>.ki </span><span>*</span><span> e </span><span>*</span><span> dt</span></span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>w </span><span>=</span><span> gyro </span><span>-</span><span> </span><span>self</span><span><span>.b </span><span>+</span><span> </span></span><span>self</span><span><span>.kp </span><span>*</span><span> e</span></span></div></div><div><div><div>28</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> </span><span>qnormalize</span><span>(</span><span>qmul</span><span>(</span></span><span>self</span><span><span>.q, </span><span>qexp</span><span>(w </span><span>*</span><span> dt)))</span></span></div></div><div><div><div>29</div></div><div><span>        </span><span>return</span><span> </span><span>self</span><span>.q</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Attitude baselines: TRIAD snapshot and gyro-only integration."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>from</span><span> .quat </span><span>import</span><span> dcm_to_quat, qexp, qmul, qnormalize</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>def</span><span> </span><span>triad</span><span>(</span><span>accel</span><span>,</span><span><span> </span><span>mag</span></span><span>,</span><span><span> </span><span>m_w</span></span><span>):</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"""Attitude q (body -&gt; world) from one accelerometer + magnetometer snapshot.</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>Specific force at rest points opposite to gravity, [0, 0, -1] in the NED world frame.</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>r1 </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span><span>, </span><span>-</span></span><span>1.0</span><span>])</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>r2 </span><span>=</span><span> m_w</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>b1 </span><span>=</span><span> accel </span><span>/</span><span> np.</span><span>sqrt</span><span>(accel </span><span>@</span><span> accel)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>b2 </span><span>=</span><span> mag </span><span>/</span><span> np.</span><span>sqrt</span><span>(mag </span><span>@</span><span> mag)</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>def</span><span> </span><span>frame</span><span>(</span><span>v1</span><span>,</span><span><span> </span><span>v2</span></span><span>):</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>t2 </span><span>=</span><span> np.</span><span>cross</span><span>(v1, v2)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>t2 </span><span>=</span><span> t2 </span><span>/</span><span> np.</span><span>sqrt</span><span>(t2 </span><span>@</span><span> t2)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>return</span><span><span> np.</span><span>column_stack</span><span>([v1, t2, np.</span><span>cross</span><span>(v1, t2)])</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>C </span><span>=</span><span> </span><span>frame</span><span>(r1, r2) </span><span>@</span><span> </span><span>frame</span><span>(b1, b2).T  </span><span># world &lt;- body</span></div></div><div><div><div>24</div></div><div><span>    </span><span>return</span><span><span> </span><span>dcm_to_quat</span><span>(C)</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>def</span><span> </span><span>gyro_only</span><span>(</span><span>gyro</span><span>,</span><span><span> </span><span>dt</span></span><span>,</span><span><span> </span><span>q0</span></span><span>):</span></div></div><div><div><div>28</div></div><div><span>    </span><span>"""Integrate raw gyro samples 1..n-1 starting from q0 at sample 0."""</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(gyro)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>q </span><span>=</span><span> np.</span><span>asarray</span><span>(q0, </span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>out </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>4</span><span>))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>out[</span><span>0</span><span><span>] </span><span>=</span><span> q</span></span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(</span><span>1</span><span>, n):</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>q </span><span>=</span><span> </span><span>qnormalize</span><span>(</span><span>qmul</span><span>(q, </span><span>qexp</span><span>(gyro[k] </span><span>*</span><span> dt)))</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>out[k] </span><span>=</span><span> q</span></div></div><div><div><div>36</div></div><div><span>    </span><span>return</span><span> out</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Mahony.step</code> 是上面三个公式的逐行翻译。<code>triad</code> 里的 <code>frame</code> 构造标架（三个列向量），<code>C = frame(r1, r2) @ frame(b1, b2).T</code> 是 <span><span>CwbC_{wb}</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span><span>w</span><span>b</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</p></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>四元数是这一章最典型的”同一个东西，三种语言三种惯用法”。</p><ul>
<li><strong>C：</strong> 四元数是 <code>kl_real[4]</code>，函数把结果写入调用者给的输出数组，输出不与输入别名。没有类型的区别，<code>kl_real a[4]</code> 和 <code>kl_real v[3]</code> 在函数签名里只是文档。</li>
<li><strong>C++：</strong> <code>Quat&lt;T&gt;</code> 是带四个成员的结构体，重载 <code>operator*</code>，<code>Quat::exp(Vec3)</code> 是静态方法，<code>q.log()</code> 返回 <code>Vec3</code>。<code>Vec3&lt;T&gt;</code> 是第 2 篇的 <code>Mat&lt;3,1,T&gt;</code>，所以向量运算（<code>+</code>、<code>-</code>、<code>*</code> 标量）直接可用。</li>
<li><strong>Rust：</strong> <code>Quat</code> 是 <code>Copy</code> 的结构体，<code>impl Mul</code>；向量用 <code>[Real; 3]</code>，旋转矩阵用 <code>Mat&lt;3, 3&gt;</code>。</li>
</ul><p>四元数乘法和指数/对数，三个版本放在一起看：</p><section><h3>C<a href="#c"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#define</span><span> </span><span>SMALL</span><span> ((kl_real)</span><span><span>1</span><span>e</span></span><span>-</span><span>8</span><span>)</span><span> /* below this angle (rad) use the Taylor expansion */</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>kl_quat_mul</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>b</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>out</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>out</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>8</div></div><div><span>  </span><span>out</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>out</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>10</div></div><div><span>  </span><span>out</span><span>[</span><span>3</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>void</span><span> </span><span>kl_quat_conj</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>out</span><span>)</span></span></div></div><div><div><div>14</div></div><div><span>{</span></div></div><div><div><div>15</div></div><div><span>  </span><span>out</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>16</div></div><div><span>  </span><span>out</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>-</span><span>a</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>17</div></div><div><span>  </span><span>out</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>-</span><span>a</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>18</div></div><div><span>  </span><span>out</span><span>[</span><span>3</span><span>] </span><span>=</span><span> </span><span>-</span><span>a</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>void</span><span> </span><span>kl_quat_normalize</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>q</span><span>)</span></span></div></div><div><div><div>22</div></div><div><span>{</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>kl_real n </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>q</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>q</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>q</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>2</span><span>] </span><span>+</span><span> </span><span>q</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>3</span><span>]);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>q</span><span>[i] </span><span>/=</span><span> n;</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>void</span><span> </span><span>kl_quat_exp</span><span>(</span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>)</span></span></div></div><div><div><div>29</div></div><div><span>{</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>kl_real th </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>if</span><span> (th </span><span>&lt;</span><span> SMALL) {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>1</span><span> </span><span>-</span><span> th </span><span>*</span><span> th </span><span>/</span><span> </span><span>8</span><span>;</span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>34</div></div><div><span>      </span><span>q</span><span>[</span><span>1</span><span> </span><span>+</span><span> i] </span><span>=</span><span> (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>v</span><span>[i];</span></div></div><div><div><div>35</div></div><div><span>    </span><span>return</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>kl_real s </span><span>=</span><span> </span><span>KL_SIN</span><span>((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> th) </span><span>/</span><span> th;</span></div></div><div><div><div>38</div></div><div><span>  </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>KL_COS</span><span>((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> th);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>q</span><span>[</span><span>1</span><span> </span><span>+</span><span> i] </span><span>=</span><span> s </span><span>*</span><span> </span><span>v</span><span>[i];</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>void</span><span> </span><span>kl_quat_log</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>44</div></div><div><span>{</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>kl_real sg </span><span>=</span><span> </span><span>q</span><span>[</span><span>0</span><span>] </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>?</span><span> </span><span>-</span><span>1</span><span> </span><span>:</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>kl_real w </span><span>=</span><span> sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>0</span><span>], </span><span>u</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>1</span><span>], sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>2</span><span>], sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>3</span><span>]};</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>kl_real n </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>kl_real k </span><span>=</span><span> n </span><span>&lt;</span><span> SMALL </span><span>?</span><span> </span><span>2</span><span> </span><span>/</span><span> w </span><span>:</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>KL_ATAN2</span><span>(n, w) </span><span>/</span><span> n;</span></div></div><div><div><div>49</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>v</span><span>[i] </span><span>=</span><span> k </span><span>*</span><span> </span><span>u</span><span>[i];</span></div></div><div><div><div>51</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>kl_quat_mul(a, b, out)</code> 的 <code>out</code> 不能与 <code>a</code>、<code>b</code> 是同一块内存：每个分量用到所有输入。Mahony 一步：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>f</span><span>-&gt;</span><span>m_w</span><span>[i] </span><span>=</span><span> </span><span>m_w</span><span>[i];</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>kl_mahony_step</span><span><span>(</span><span>kl_mahony </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>7</div></div><div><span>{</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>kl_real </span><span>C</span><span>[</span><span>9</span><span>], </span><span>a_pred</span><span>[</span><span>3</span><span>], </span><span>m_pred</span><span>[</span><span>3</span><span>], </span><span>a_n</span><span>[</span><span>3</span><span>], </span><span>m_n</span><span>[</span><span>3</span><span>], </span><span>e1</span><span>[</span><span>3</span><span>], </span><span>e2</span><span>[</span><span>3</span><span>], </span><span>e</span><span>[</span><span>3</span><span>], </span><span>w</span><span>[</span><span>3</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>/* a_pred = C^T [0,0,-1], m_pred = C^T m_w */</span></div></div><div><div><div>11</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>a_pred</span><span>[i] </span><span>=</span><span> </span><span>-</span><span>C</span><span>[</span><span>6</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>13</div></div><div><span>    </span><span>m_pred</span><span>[i] </span><span>=</span><span> </span><span>C</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>3</span><span> </span><span>+</span><span> i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>6</span><span> </span><span>+</span><span> i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>  </span><span>unit</span><span>(accel, a_n);</span></div></div><div><div><div>16</div></div><div><span>  </span><span>unit</span><span>(mag, m_n);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>cross</span><span>(a_n, a_pred, e1);</span></div></div><div><div><div>18</div></div><div><span>  </span><span>cross</span><span>(m_n, m_pred, e2);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>e</span><span>[i] </span><span>=</span><span> </span><span>e1</span><span>[i] </span><span>+</span><span> </span><span>e2</span><span>[i];</span></div></div><div><div><div>21</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>ki</span><span> </span><span>*</span><span> </span><span>e</span><span>[i] </span><span>*</span><span> dt;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>w</span><span>[i] </span><span>=</span><span> (</span><span>gyro</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>kp</span><span> </span><span>*</span><span> </span><span>e</span><span>[i]) </span><span>*</span><span> dt;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>  </span><span>kl_quat_exp</span><span>(w, dq);</span></div></div><div><div><div>25</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>27</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>q</span><span>[i] </span><span>=</span><span> </span><span>qn</span><span>[i];</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>a_pred = -C[6 + i]</code>：<span><span>CT[0,0,−1]TC^T[0,0,-1]^T</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>[</span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>−</span><span>1</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 就是 <span><span>−1-1</span><span><span><span></span><span>−</span><span>1</span></span></span></span> 乘以 <span><span>CC</span><span><span><span></span><span>C</span></span></span></span> 的第三行，不需要完整的矩阵向量乘法。</p>
完整源码：c/include/kl/kl_quat.h、c/src/kl_quat.c、c/include/kl/kl_mahony.h、c/src/kl_mahony.c、c/src/kl_baselines.c<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Quaternions, Hamilton convention, scalar first q = [w, x, y, z].</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* q rotates body to world: v_w = q (x) [0, v_b] (x) q*, and C(q) = C_wb (row-major 3x3).</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* Output buffers never alias inputs unless stated. */</span></div></div><div><div><div>4</div></div><div><span>#ifndef</span><span> </span><span>KL_QUAT_H</span></div></div><div><div><div>5</div></div><div><span>#define</span><span> </span><span>KL_QUAT_H</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>/* out = a (x) b */</span></div></div><div><div><div>10</div></div><div><span>void</span><span> </span><span>kl_quat_mul</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>b</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>out</span><span>);</span></span></div></div><div><div><div>11</div></div><div><span>void</span><span> </span><span>kl_quat_conj</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>out</span><span>);</span></span></div></div><div><div><div>12</div></div><div><span>/* q = q / |q|, in place */</span></div></div><div><div><div>13</div></div><div><span>void</span><span> </span><span>kl_quat_normalize</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>q</span><span>);</span></span></div></div><div><div><div>14</div></div><div><span>/* Rotation vector -&gt; unit quaternion [cos(|v|/2), sin(|v|/2) v/|v|]. */</span></div></div><div><div><div>15</div></div><div><span>void</span><span> </span><span>kl_quat_exp</span><span>(</span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>);</span></span></div></div><div><div><div>16</div></div><div><span>/* Unit quaternion -&gt; rotation vector with angle in [0, pi] (q and -q give the same v). */</span></div></div><div><div><div>17</div></div><div><span>void</span><span> </span><span>kl_quat_log</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>18</div></div><div><span>/* C(q) = C_wb */</span></div></div><div><div><div>19</div></div><div><span>void</span><span> </span><span>kl_quat_to_dcm</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>C</span><span>[</span><span>9</span><span><span>]</span><span>);</span></span></div></div><div><div><div>20</div></div><div><span>/* C_wb -&gt; unit quaternion with w &gt;= 0 (Shepperd's method) */</span></div></div><div><div><div>21</div></div><div><span>void</span><span> </span><span>kl_dcm_to_quat</span><span>(</span><span>const</span><span> kl_real </span><span>C</span><span>[</span><span>9</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>);</span></span></div></div><div><div><div>22</div></div><div><span>/* S v = [v]x w = v x w */</span></div></div><div><div><div>23</div></div><div><span>void</span><span> </span><span>kl_skew</span><span>(</span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>S</span><span>[</span><span>9</span><span><span>]</span><span>);</span></span></div></div><div><div><div>24</div></div><div><span>/* C_wb = Rz(yaw) Ry(pitch) Rx(roll) */</span></div></div><div><div><div>25</div></div><div><span>void</span><span> </span><span>kl_quat_to_euler_zyx</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>yaw</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>pitch</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>roll</span><span>);</span></span></div></div><div><div><div>26</div></div><div><span>/* v = log(est^* (x) tru): attitude error in the estimate's body frame */</span></div></div><div><div><div>27</div></div><div><span>void</span><span> </span><span>kl_quat_error</span><span>(</span><span>const</span><span> kl_real </span><span>est</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>tru</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#define</span><span> </span><span>SMALL</span><span> ((kl_real)</span><span><span>1</span><span>e</span></span><span>-</span><span>8</span><span>)</span><span> /* below this angle (rad) use the Taylor expansion */</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>kl_quat_mul</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>b</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>out</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>out</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>8</div></div><div><span>  </span><span>out</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>out</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>10</div></div><div><span>  </span><span>out</span><span>[</span><span>3</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>3</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>a</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>void</span><span> </span><span>kl_quat_conj</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>out</span><span>)</span></span></div></div><div><div><div>14</div></div><div><span>{</span></div></div><div><div><div>15</div></div><div><span>  </span><span>out</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>16</div></div><div><span>  </span><span>out</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>-</span><span>a</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>17</div></div><div><span>  </span><span>out</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>-</span><span>a</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>18</div></div><div><span>  </span><span>out</span><span>[</span><span>3</span><span>] </span><span>=</span><span> </span><span>-</span><span>a</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>void</span><span> </span><span>kl_quat_normalize</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>q</span><span>)</span></span></div></div><div><div><div>22</div></div><div><span>{</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>kl_real n </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>q</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>q</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>q</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>2</span><span>] </span><span>+</span><span> </span><span>q</span><span>[</span><span>3</span><span>] </span><span>*</span><span> </span><span>q</span><span>[</span><span>3</span><span>]);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>25</div></div><div><span>    </span><span>q</span><span>[i] </span><span>/=</span><span> n;</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>void</span><span> </span><span>kl_quat_exp</span><span>(</span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>)</span></span></div></div><div><div><div>29</div></div><div><span>{</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>kl_real th </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>if</span><span> (th </span><span>&lt;</span><span> SMALL) {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>1</span><span> </span><span>-</span><span> th </span><span>*</span><span> th </span><span>/</span><span> </span><span>8</span><span>;</span></div></div><div><div><div>33</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>34</div></div><div><span>      </span><span>q</span><span>[</span><span>1</span><span> </span><span>+</span><span> i] </span><span>=</span><span> (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> </span><span>v</span><span>[i];</span></div></div><div><div><div>35</div></div><div><span>    </span><span>return</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>kl_real s </span><span>=</span><span> </span><span>KL_SIN</span><span>((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> th) </span><span>/</span><span> th;</span></div></div><div><div><div>38</div></div><div><span>  </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>KL_COS</span><span>((kl_real)</span><span>0.5</span><span> </span><span>*</span><span> th);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>q</span><span>[</span><span>1</span><span> </span><span>+</span><span> i] </span><span>=</span><span> s </span><span>*</span><span> </span><span>v</span><span>[i];</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>void</span><span> </span><span>kl_quat_log</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>44</div></div><div><span>{</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>kl_real sg </span><span>=</span><span> </span><span>q</span><span>[</span><span>0</span><span>] </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>?</span><span> </span><span>-</span><span>1</span><span> </span><span>:</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>kl_real w </span><span>=</span><span> sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>0</span><span>], </span><span>u</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>1</span><span>], sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>2</span><span>], sg </span><span>*</span><span> </span><span>q</span><span>[</span><span>3</span><span>]};</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>kl_real n </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>u</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>u</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>u</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>kl_real k </span><span>=</span><span> n </span><span>&lt;</span><span> SMALL </span><span>?</span><span> </span><span>2</span><span> </span><span>/</span><span> w </span><span>:</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>KL_ATAN2</span><span>(n, w) </span><span>/</span><span> n;</span></div></div><div><div><div>49</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>50</div></div><div><span>    </span><span>v</span><span>[i] </span><span>=</span><span> k </span><span>*</span><span> </span><span>u</span><span>[i];</span></div></div><div><div><div>51</div></div><div><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>void</span><span> </span><span>kl_quat_to_dcm</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>C</span><span>[</span><span>9</span><span><span>]</span><span>)</span></span></div></div><div><div><div>54</div></div><div><span>{</span></div></div><div><div><div>55</div></div><div><span><span>  </span></span><span>kl_real w </span><span>=</span><span> </span><span>q</span><span>[</span><span>0</span><span>], x </span><span>=</span><span> </span><span>q</span><span>[</span><span>1</span><span>], y </span><span>=</span><span> </span><span>q</span><span>[</span><span>2</span><span>], z </span><span>=</span><span> </span><span>q</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>56</div></div><div><span>  </span><span>C</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (y </span><span>*</span><span> y </span><span>+</span><span> z </span><span>*</span><span> z);</span></div></div><div><div><div>57</div></div><div><span>  </span><span>C</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> (x </span><span>*</span><span> y </span><span>-</span><span> w </span><span>*</span><span> z);</span></div></div><div><div><div>58</div></div><div><span>  </span><span>C</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> (x </span><span>*</span><span> z </span><span>+</span><span> w </span><span>*</span><span> y);</span></div></div><div><div><div>59</div></div><div><span>  </span><span>C</span><span>[</span><span>3</span><span>] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> (x </span><span>*</span><span> y </span><span>+</span><span> w </span><span>*</span><span> z);</span></div></div><div><div><div>60</div></div><div><span>  </span><span>C</span><span>[</span><span>4</span><span>] </span><span>=</span><span> </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (x </span><span>*</span><span> x </span><span>+</span><span> z </span><span>*</span><span> z);</span></div></div><div><div><div>61</div></div><div><span>  </span><span>C</span><span>[</span><span>5</span><span>] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> (y </span><span>*</span><span> z </span><span>-</span><span> w </span><span>*</span><span> x);</span></div></div><div><div><div>62</div></div><div><span>  </span><span>C</span><span>[</span><span>6</span><span>] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> (x </span><span>*</span><span> z </span><span>-</span><span> w </span><span>*</span><span> y);</span></div></div><div><div><div>63</div></div><div><span>  </span><span>C</span><span>[</span><span>7</span><span>] </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> (y </span><span>*</span><span> z </span><span>+</span><span> w </span><span>*</span><span> x);</span></div></div><div><div><div>64</div></div><div><span>  </span><span>C</span><span>[</span><span>8</span><span>] </span><span>=</span><span> </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (x </span><span>*</span><span> x </span><span>+</span><span> y </span><span>*</span><span> y);</span></div></div><div><div><div>65</div></div><div><span>}</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>void</span><span> </span><span>kl_dcm_to_quat</span><span>(</span><span>const</span><span> kl_real </span><span>C</span><span>[</span><span>9</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>)</span></span></div></div><div><div><div>68</div></div><div><span>{</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>kl_real t </span><span>=</span><span> </span><span>C</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>4</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>8</span><span>], s;</span></div></div><div><div><div>70</div></div><div><span>  </span><span>if</span><span> (t </span><span>&gt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>KL_SQRT</span><span>(</span><span>1</span><span> </span><span>+</span><span> t);</span></div></div><div><div><div>72</div></div><div><span>    </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> (kl_real)</span><span>0.25</span><span> </span><span>*</span><span> s;</span></div></div><div><div><div>73</div></div><div><span>    </span><span>q</span><span>[</span><span>1</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>7</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>5</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>74</div></div><div><span>    </span><span>q</span><span>[</span><span>2</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>6</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>75</div></div><div><span>    </span><span>q</span><span>[</span><span>3</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>3</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>1</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>76</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>C</span><span>[</span><span>0</span><span>] </span><span>&gt;=</span><span> </span><span>C</span><span>[</span><span>4</span><span>] </span><span>&amp;&amp;</span><span> </span><span>C</span><span>[</span><span>0</span><span>] </span><span>&gt;=</span><span> </span><span>C</span><span>[</span><span>8</span><span>]) {</span></div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>KL_SQRT</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>C</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>4</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>8</span><span>]);</span></div></div><div><div><div>78</div></div><div><span>    </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>7</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>5</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>79</div></div><div><span>    </span><span>q</span><span>[</span><span>1</span><span>] </span><span>=</span><span> (kl_real)</span><span>0.25</span><span> </span><span>*</span><span> s;</span></div></div><div><div><div>80</div></div><div><span>    </span><span>q</span><span>[</span><span>2</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>3</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>81</div></div><div><span>    </span><span>q</span><span>[</span><span>3</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>2</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>6</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>C</span><span>[</span><span>4</span><span>] </span><span>&gt;=</span><span> </span><span>C</span><span>[</span><span>8</span><span>]) {</span></div></div><div><div><div>83</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>KL_SQRT</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>C</span><span>[</span><span>4</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>8</span><span>]);</span></div></div><div><div><div>84</div></div><div><span>    </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>6</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>85</div></div><div><span>    </span><span>q</span><span>[</span><span>1</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>3</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>86</div></div><div><span>    </span><span>q</span><span>[</span><span>2</span><span>] </span><span>=</span><span> (kl_real)</span><span>0.25</span><span> </span><span>*</span><span> s;</span></div></div><div><div><div>87</div></div><div><span>    </span><span>q</span><span>[</span><span>3</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>5</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>7</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>88</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>89</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> </span><span>KL_SQRT</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>C</span><span>[</span><span>8</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>4</span><span>]);</span></div></div><div><div><div>90</div></div><div><span>    </span><span>q</span><span>[</span><span>0</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>3</span><span>] </span><span>-</span><span> </span><span>C</span><span>[</span><span>1</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>91</div></div><div><span>    </span><span>q</span><span>[</span><span>1</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>2</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>6</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>92</div></div><div><span>    </span><span>q</span><span>[</span><span>2</span><span>] </span><span>=</span><span> (</span><span>C</span><span>[</span><span>5</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>7</span><span>]) </span><span>/</span><span> s;</span></div></div><div><div><div>93</div></div><div><span>    </span><span>q</span><span>[</span><span>3</span><span>] </span><span>=</span><span> (kl_real)</span><span>0.25</span><span> </span><span>*</span><span> s;</span></div></div><div><div><div>94</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>95</div></div><div><span>  </span><span>if</span><span> (</span><span>q</span><span>[</span><span>0</span><span>] </span><span>&lt;</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>96</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>97</div></div><div><span>      </span><span>q</span><span>[i] </span><span>=</span><span> </span><span>-</span><span>q</span><span>[i];</span></div></div><div><div><div>98</div></div><div><span>}</span></div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div><span>void</span><span> </span><span>kl_skew</span><span>(</span><span>const</span><span> kl_real </span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>S</span><span>[</span><span>9</span><span><span>]</span><span>)</span></span></div></div><div><div><div>101</div></div><div><span>{</span></div></div><div><div><div>102</div></div><div><span>  </span><span>S</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>103</div></div><div><span>  </span><span>S</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>-</span><span>v</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>104</div></div><div><span>  </span><span>S</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>v</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>105</div></div><div><span>  </span><span>S</span><span>[</span><span>3</span><span>] </span><span>=</span><span> </span><span>v</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>106</div></div><div><span>  </span><span>S</span><span>[</span><span>4</span><span>] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>107</div></div><div><span>  </span><span>S</span><span>[</span><span>5</span><span>] </span><span>=</span><span> </span><span>-</span><span>v</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>108</div></div><div><span>  </span><span>S</span><span>[</span><span>6</span><span>] </span><span>=</span><span> </span><span>-</span><span>v</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>109</div></div><div><span>  </span><span>S</span><span>[</span><span>7</span><span>] </span><span>=</span><span> </span><span>v</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>110</div></div><div><span>  </span><span>S</span><span>[</span><span>8</span><span>] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>111</div></div><div><span>}</span></div></div><div><div><div>112</div></div><div>
</div></div><div><div><div>113</div></div><div><span>void</span><span> </span><span>kl_quat_to_euler_zyx</span><span>(</span><span>const</span><span> kl_real </span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>yaw</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>pitch</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>roll</span><span>)</span></span></div></div><div><div><div>114</div></div><div><span>{</span></div></div><div><div><div>115</div></div><div><span><span>  </span></span><span>kl_real w </span><span>=</span><span> </span><span>q</span><span>[</span><span>0</span><span>], x </span><span>=</span><span> </span><span>q</span><span>[</span><span>1</span><span>], y </span><span>=</span><span> </span><span>q</span><span>[</span><span>2</span><span>], z </span><span>=</span><span> </span><span>q</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>116</div></div><div><span><span>  </span></span><span>kl_real sp </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> (w </span><span>*</span><span> y </span><span>-</span><span> z </span><span>*</span><span> x);</span></div></div><div><div><div>117</div></div><div><span><span>  </span></span><span>sp </span><span>=</span><span> sp </span><span>&gt;</span><span> </span><span>1</span><span> </span><span>?</span><span> </span><span>1</span><span> </span><span>:</span><span> (sp </span><span>&lt;</span><span> </span><span>-</span><span>1</span><span> </span><span>?</span><span> </span><span>-</span><span>1</span><span> </span><span>:</span><span> sp);</span></div></div><div><div><div>118</div></div><div><span>  </span><span>*</span><span>yaw </span><span>=</span><span> </span><span>KL_ATAN2</span><span>(</span><span>2</span><span> </span><span>*</span><span> (w </span><span>*</span><span> z </span><span>+</span><span> x </span><span>*</span><span> y), </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (y </span><span>*</span><span> y </span><span>+</span><span> z </span><span>*</span><span> z));</span></div></div><div><div><div>119</div></div><div><span>  </span><span>*</span><span>pitch </span><span>=</span><span> </span><span>KL_ASIN</span><span>(sp);</span></div></div><div><div><div>120</div></div><div><span>  </span><span>*</span><span>roll </span><span>=</span><span> </span><span>KL_ATAN2</span><span>(</span><span>2</span><span> </span><span>*</span><span> (w </span><span>*</span><span> x </span><span>+</span><span> y </span><span>*</span><span> z), </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (x </span><span>*</span><span> x </span><span>+</span><span> y </span><span>*</span><span> y));</span></div></div><div><div><div>121</div></div><div><span>}</span></div></div><div><div><div>122</div></div><div>
</div></div><div><div><div>123</div></div><div><span>void</span><span> </span><span>kl_quat_error</span><span>(</span><span>const</span><span> kl_real </span><span>est</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>tru</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>v</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>124</div></div><div><span>{</span></div></div><div><div><div>125</div></div><div><span><span>  </span></span><span>kl_real </span><span>c</span><span>[</span><span>4</span><span>], </span><span>d</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>126</div></div><div><span>  </span><span>kl_quat_conj</span><span>(est, c);</span></div></div><div><div><div>127</div></div><div><span>  </span><span>kl_quat_mul</span><span>(c, tru, d);</span></div></div><div><div><div>128</div></div><div><span>  </span><span>kl_quat_log</span><span>(d, v);</span></div></div><div><div><div>129</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Mahony explicit complementary filter with an integral bias term.</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* q &lt;- q (x) exp((w_m - b + kp e) dt), b &lt;- b - ki e dt (b is updated first),</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* e = a_meas x a_pred + m_meas x m_pred, unit vectors in the body frame.</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>* Quaternion conventions: see kl_quat.h. */</span></div></div><div><div><div>5</div></div><div><span>#ifndef</span><span> </span><span>KL_MAHONY_H</span></div></div><div><div><div>6</div></div><div><span>#define</span><span> </span><span>KL_MAHONY_H</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>kl_real kp, ki;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>kl_real </span><span>q</span><span>[</span><span>4</span><span>], </span><span>b</span><span>[</span><span>3</span><span>], </span><span>m_w</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>13</div></div><div><span>} kl_mahony;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>/* m_w: reference magnetic field in the world frame (NED), e.g. [cos 50deg, 0, sin 50deg]. */</span></div></div><div><div><div>16</div></div><div><span>void</span><span> </span><span>kl_mahony_init</span><span><span>(</span><span>kl_mahony </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>kp</span><span>,</span><span> kl_real </span><span>ki</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>);</span></span></div></div><div><div><div>17</div></div><div><span>void</span><span> </span><span>kl_mahony_step</span><span><span>(</span><span>kl_mahony </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>);</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_mahony.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>cross</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>b</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>c</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>c</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>8</div></div><div><span>  </span><span>c</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>c</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>unit</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>v</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>u</span><span>)</span></span></div></div><div><div><div>13</div></div><div><span>{</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>kl_real n </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>u</span><span>[i] </span><span>=</span><span> </span><span>v</span><span>[i] </span><span>/</span><span> n;</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>void</span><span> </span><span>kl_mahony_init</span><span><span>(</span><span>kl_mahony </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>kp</span><span>,</span><span> kl_real </span><span>ki</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>q0</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>)</span></span></div></div><div><div><div>20</div></div><div><span>{</span></div></div><div><div><div>21</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>kp</span><span> </span><span>=</span><span> kp;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>ki</span><span> </span><span>=</span><span> ki;</span></div></div><div><div><div>23</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>24</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>q</span><span>[i] </span><span>=</span><span> </span><span>q0</span><span>[i];</span></div></div><div><div><div>25</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>27</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[i] </span><span>=</span><span> </span><span>m_w</span><span>[i];</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>void</span><span> </span><span>kl_mahony_step</span><span><span>(</span><span>kl_mahony </span></span><span>*</span><span><span>f</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>,</span></span></div></div><div><div><div>32</div></div><div><span>        </span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>33</div></div><div><span>{</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>kl_real </span><span>C</span><span>[</span><span>9</span><span>], </span><span>a_pred</span><span>[</span><span>3</span><span>], </span><span>m_pred</span><span>[</span><span>3</span><span>], </span><span>a_n</span><span>[</span><span>3</span><span>], </span><span>m_n</span><span>[</span><span>3</span><span>], </span><span>e1</span><span>[</span><span>3</span><span>], </span><span>e2</span><span>[</span><span>3</span><span>], </span><span>e</span><span>[</span><span>3</span><span>], </span><span>w</span><span>[</span><span>3</span><span>], </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>35</div></div><div><span>  </span><span>kl_quat_to_dcm</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, C);</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>/* a_pred = C^T [0,0,-1], m_pred = C^T m_w */</span></div></div><div><div><div>37</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>38</div></div><div><span>    </span><span>a_pred</span><span>[i] </span><span>=</span><span> </span><span>-</span><span>C</span><span>[</span><span>6</span><span> </span><span>+</span><span> i];</span></div></div><div><div><div>39</div></div><div><span>    </span><span>m_pred</span><span>[i] </span><span>=</span><span> </span><span>C</span><span>[i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>3</span><span> </span><span>+</span><span> i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>C</span><span>[</span><span>6</span><span> </span><span>+</span><span> i] </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>m_w</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span>  </span><span>unit</span><span>(accel, a_n);</span></div></div><div><div><div>42</div></div><div><span>  </span><span>unit</span><span>(mag, m_n);</span></div></div><div><div><div>43</div></div><div><span>  </span><span>cross</span><span>(a_n, a_pred, e1);</span></div></div><div><div><div>44</div></div><div><span>  </span><span>cross</span><span>(m_n, m_pred, e2);</span></div></div><div><div><div>45</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>46</div></div><div><span>    </span><span>e</span><span>[i] </span><span>=</span><span> </span><span>e1</span><span>[i] </span><span>+</span><span> </span><span>e2</span><span>[i];</span></div></div><div><div><div>47</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>ki</span><span> </span><span>*</span><span> </span><span>e</span><span>[i] </span><span>*</span><span> dt;</span></div></div><div><div><div>48</div></div><div><span>    </span><span>w</span><span>[i] </span><span>=</span><span> (</span><span>gyro</span><span>[i] </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>b</span><span>[i] </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>kp</span><span> </span><span>*</span><span> </span><span>e</span><span>[i]) </span><span>*</span><span> dt;</span></div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span>  </span><span>kl_quat_exp</span><span>(w, dq);</span></div></div><div><div><div>51</div></div><div><span>  </span><span>kl_quat_mul</span><span>(</span><span>f</span><span>-&gt;</span><span>q</span><span>, dq, qn);</span></div></div><div><div><div>52</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>53</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>f</span><span>-&gt;</span><span>q</span><span>[i] </span><span>=</span><span> </span><span>qn</span><span>[i];</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Attitude baselines: TRIAD snapshot and gyro-only integration. Conventions: see kl_quat.h. */</span></div></div><div><div><div>2</div></div><div><span>#ifndef</span><span> </span><span>KL_BASELINES_H</span></div></div><div><div><div>3</div></div><div><span>#define</span><span> </span><span>KL_BASELINES_H</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/* Attitude q (body -&gt; world) from one accelerometer + magnetometer snapshot.</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>* Specific force at rest points opposite to gravity, [0, 0, -1] in the NED world frame. */</span></div></div><div><div><div>9</div></div><div><span>void</span><span> </span><span>kl_triad</span><span>(</span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>);</span></span></div></div><div><div><div>10</div></div><div><span>/* q = normalize(q (x) exp(gyro dt)), in place */</span></div></div><div><div><div>11</div></div><div><span>void</span><span> </span><span>kl_gyro_step</span><span><span>(</span><span>kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>);</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_baselines.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/kl_quat.h"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>cross</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>a</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>b</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>c</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>c</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>8</div></div><div><span>  </span><span>c</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>9</div></div><div><span>  </span><span>c</span><span>[</span><span>2</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>1</span><span>] </span><span>-</span><span> </span><span>a</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>b</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>unit</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>v</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>u</span><span>)</span></span></div></div><div><div><div>13</div></div><div><span>{</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>kl_real n </span><span>=</span><span> </span><span>KL_SQRT</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>0</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>1</span><span>] </span><span>+</span><span> </span><span>v</span><span>[</span><span>2</span><span>] </span><span>*</span><span> </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>16</div></div><div><span>    </span><span>u</span><span>[i] </span><span>=</span><span> </span><span>v</span><span>[i] </span><span>/</span><span> n;</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>/* Columns [v1, t2, v1 x t2] with t2 = unit(v1 x v2), stored row-major in M. */</span></div></div><div><div><div>20</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>frame</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>v1</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>v2</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>M</span><span>)</span></span></div></div><div><div><div>21</div></div><div><span>{</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>kl_real </span><span>t2</span><span>[</span><span>3</span><span>], </span><span>t3</span><span>[</span><span>3</span><span>];</span></div></div><div><div><div>23</div></div><div><span>  </span><span>cross</span><span>(v1, v2, t2);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>unit</span><span>(t2, t2);</span></div></div><div><div><div>25</div></div><div><span>  </span><span>cross</span><span>(v1, t2, t3);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>M</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> </span><span>0</span><span>] </span><span>=</span><span> </span><span>v1</span><span>[i];</span></div></div><div><div><div>28</div></div><div><span>    </span><span>M</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> </span><span>1</span><span>] </span><span>=</span><span> </span><span>t2</span><span>[i];</span></div></div><div><div><div>29</div></div><div><span>    </span><span>M</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> </span><span>2</span><span>] </span><span>=</span><span> </span><span>t3</span><span>[i];</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>void</span><span> </span><span>kl_triad</span><span>(</span><span>const</span><span> kl_real </span><span>accel</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>mag</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>m_w</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>)</span></span></div></div><div><div><div>34</div></div><div><span>{</span></div></div><div><div><div>35</div></div><div><span>  </span><span>const</span><span> kl_real </span><span>r1</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>};</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>kl_real </span><span>b1</span><span>[</span><span>3</span><span>], </span><span>b2</span><span>[</span><span>3</span><span>], </span><span>Fr</span><span>[</span><span>9</span><span>], </span><span>Fb</span><span>[</span><span>9</span><span>], </span><span>C</span><span>[</span><span>9</span><span>];</span></div></div><div><div><div>37</div></div><div><span>  </span><span>unit</span><span>(accel, b1);</span></div></div><div><div><div>38</div></div><div><span>  </span><span>unit</span><span>(mag, b2);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>frame</span><span>(r1, m_w, Fr);</span></div></div><div><div><div>40</div></div><div><span>  </span><span>frame</span><span>(b1, b2, Fb);</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>/* C = Fr Fb^T: world &lt;- body */</span></div></div><div><div><div>42</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>3</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>43</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> </span><span>3</span><span>; j</span><span>++</span><span>) {</span></div></div><div><div><div>44</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>45</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> </span><span>3</span><span>; k</span><span>++</span><span>)</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> </span><span>Fr</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k] </span><span>*</span><span> </span><span>Fb</span><span>[j </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> k];</span></div></div><div><div><div>47</div></div><div><span>      </span><span>C</span><span>[i </span><span>*</span><span> </span><span>3</span><span> </span><span>+</span><span> j] </span><span>=</span><span> s;</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span>  </span><span>kl_dcm_to_quat</span><span>(C, q);</span></div></div><div><div><div>50</div></div><div><span>}</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>void</span><span> </span><span>kl_gyro_step</span><span><span>(</span><span>kl_real </span></span><span>q</span><span>[</span><span>4</span><span><span>]</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>gyro</span><span>[</span><span>3</span><span><span>]</span><span>,</span><span> kl_real </span><span>dt</span><span>)</span></span></div></div><div><div><div>53</div></div><div><span>{</span></div></div><div><div><div>54</div></div><div><span><span>  </span></span><span>kl_real </span><span>w</span><span>[</span><span>3</span><span>] </span><span>=</span><span> {</span><span>gyro</span><span>[</span><span>0</span><span>] </span><span>*</span><span> dt, </span><span>gyro</span><span>[</span><span>1</span><span>] </span><span>*</span><span> dt, </span><span>gyro</span><span>[</span><span>2</span><span>] </span><span>*</span><span> dt}, </span><span>dq</span><span>[</span><span>4</span><span>], </span><span>qn</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>55</div></div><div><span>  </span><span>kl_quat_exp</span><span>(w, dq);</span></div></div><div><div><div>56</div></div><div><span>  </span><span>kl_quat_mul</span><span>(q, dq, qn);</span></div></div><div><div><div>57</div></div><div><span>  </span><span>kl_quat_normalize</span><span>(qn);</span></div></div><div><div><div>58</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>59</div></div><div><span>    </span><span>q</span><span>[i] </span><span>=</span><span> </span><span>qn</span><span>[i];</span></div></div><div><div><div>60</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>T</span><span> </span><span>SMALL</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span><span>1</span><span>e</span></span><span>-</span><span>8</span><span>);</span><span>  // below this angle (rad) use the Taylor expansion</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>  </span><span>T</span><span> </span><span>w</span><span> </span><span>=</span><span> </span><span>1</span><span>, </span><span>x</span><span> </span><span>=</span><span> </span><span>0</span><span>, </span><span>y</span><span> </span><span>=</span><span> </span><span>0</span><span>, </span><span>z</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>Quat</span><span> </span><span>identity</span><span>() { </span><span>return</span><span> </span><span>Quat</span><span>{}; }</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Quat</span><span> </span><span>operator</span><span>*(</span><span>const</span><span> </span><span>Quat</span><span>&amp;</span><span><span> </span><span>b</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> {</span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span> </span><span>-</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span> </span><span>-</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span> </span><span>-</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span>, </span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span> </span><span>+</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span> </span><span>-</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span>,</span></div></div><div><div><div>11</div></div><div><span>      </span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span> </span><span>-</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span>, </span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span> </span><span>+</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span> </span><span>-</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span>};</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Quat</span><span> </span><span>conj</span><span>() </span><span>const</span><span> { </span><span>return</span><span> {</span><span>w</span><span>, </span><span>-</span><span>x</span><span>, </span><span>-</span><span>y</span><span>, </span><span>-</span><span>z</span><span>}; }</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>  </span><span>Quat</span><span> </span><span>normalized</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>n</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>w</span><span> </span><span>*</span><span> </span><span>w</span><span> </span><span>+</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>y</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>z</span><span>);</span></div></div><div><div><div>18</div></div><div><span>    </span><span>return</span><span> {</span><span>w</span><span> </span><span>/</span><span> </span><span>n</span><span>, </span><span>x</span><span> </span><span>/</span><span> </span><span>n</span><span>, </span><span>y</span><span> </span><span>/</span><span> </span><span>n</span><span>, </span><span>z</span><span> </span><span>/</span><span> </span><span>n</span><span>};</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>// Rotation vector -&gt; unit quaternion.</span></div></div><div><div><div>22</div></div><div><span>  </span><span>static</span><span> </span><span>Quat</span><span> </span><span>exp</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>23</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>th</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>v</span><span>, </span><span>v</span><span>));</span></div></div><div><div><div>24</div></div><div><span>    </span><span>if</span><span> (</span><span>th</span><span> </span><span>&lt;</span><span> </span><span>SMALL</span><span>) </span><span>return</span><span> {</span><span>1</span><span> </span><span>-</span><span> </span><span>th</span><span> </span><span>*</span><span> </span><span>th</span><span> </span><span>/</span><span> </span><span>8</span><span>, </span><span>v</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>/</span><span> </span><span>2</span><span>, </span><span>v</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>/</span><span> </span><span>2</span><span>, </span><span>v</span><span>(</span><span>2</span><span>, </span><span>0</span><span>) </span><span>/</span><span> </span><span>2</span><span>};</span></div></div><div><div><div>25</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> std::</span><span>sin</span><span>(</span><span>th</span><span> </span><span>/</span><span> </span><span>2</span><span>) </span><span>/</span><span> </span><span>th</span><span>;</span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span><span> {</span><span>std</span><span>::</span></span><span>cos</span><span>(</span><span>th</span><span> </span><span>/</span><span> </span><span>2</span><span>), </span><span>s</span><span> </span><span>*</span><span> </span><span>v</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>s</span><span> </span><span>*</span><span> </span><span>v</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>s</span><span> </span><span>*</span><span> </span><span>v</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)};</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>// Unit quaternion -&gt; rotation vector, using the sign with w &gt;= 0 (angle in [0, pi]).</span></div></div><div><div><div>30</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>log</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>31</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span> </span><span>q</span><span> </span><span>=</span><span> </span><span>w</span><span> </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>?</span><span> </span><span>Quat</span><span>{</span><span>-</span><span>w</span><span>, </span><span>-</span><span>x</span><span>, </span><span>-</span><span>y</span><span>, </span><span>-</span><span>z</span><span>} </span><span>:</span><span> </span><span>*</span><span>this</span><span>;</span></div></div><div><div><div>32</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>u</span><span>{{</span><span>q</span><span>.</span><span>x</span><span>, </span><span>q</span><span>.</span><span>y</span><span>, </span><span>q</span><span>.</span><span>z</span><span>}};</span></div></div><div><div><div>33</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>n</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>u</span><span>, </span><span>u</span><span>));</span></div></div><div><div><div>34</div></div><div><span>    </span><span>if</span><span> (</span><span>n</span><span> </span><span>&lt;</span><span> </span><span>SMALL</span><span>) </span><span>return</span><span> </span><span>u</span><span> </span><span>*</span><span> (</span><span>2</span><span> </span><span>/</span><span> </span><span>q</span><span>.</span><span>w</span><span>);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>return</span><span> </span><span>u</span><span> </span><span>*</span><span> (</span><span>2</span><span> </span><span>*</span><span> std::</span><span>atan2</span><span>(</span><span>n</span><span>, </span><span>q</span><span>.</span><span>w</span><span>) </span><span>/</span><span> </span><span>n</span><span>);</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>Mahony</span><span>(</span><span>T</span><span> </span><span>kp</span><span>, </span><span>T</span><span> </span><span>ki</span><span>, </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span><span>&gt;</span><span>&amp;</span></span><span> </span><span>q0</span><span>, </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span><span>&gt;</span><span>&amp;</span></span><span> </span><span>m_w</span><span> </span><span>=</span><span> </span><span>default_mag_w</span><span>&lt;</span><span>T</span><span>&gt;())</span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>: </span><span>kp</span><span>(</span><span>kp</span><span>), </span><span>ki</span><span>(</span><span>ki</span><span>), </span><span>q</span><span>(</span><span>q0</span><span>), </span><span>m_w</span><span>(</span><span>m_w</span><span>) {}</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>  </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span> </span><span>step</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Ct</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>a_pred</span><span> </span><span>=</span><span> </span><span>Ct</span><span> </span><span>*</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}};</span></div></div><div><div><div>7</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_pred</span><span> </span><span>=</span><span> </span><span>Ct</span><span> </span><span>*</span><span> </span><span>m_w</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>a_n</span><span> </span><span>=</span><span> </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>)));</span></div></div><div><div><div>9</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_n</span><span> </span><span>=</span><span> </span><span>mag</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>)));</span></div></div><div><div><div>10</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>e</span><span> </span><span>=</span><span> </span><span>cross</span><span>(</span><span>a_n</span><span>, </span><span>a_pred</span><span>) </span><span>+</span><span> </span><span>cross</span><span>(</span><span>m_n</span><span>, </span><span>m_pred</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>-</span><span> </span><span>e</span><span> </span><span>*</span><span> (</span><span>ki</span><span> </span><span>*</span><span> </span><span>dt</span><span>);</span><span>  // update the bias before forming the rate</span></div></div><div><div><div>12</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>w</span><span> </span><span>=</span><span> </span><span>gyro</span><span> </span><span>-</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>e</span><span> </span><span>*</span><span> </span><span>kp</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> (</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>w</span><span> </span><span>*</span><span> </span><span>dt</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>step</code> 读起来几乎就是公式：<code>cross(a_n, a_pred) + cross(m_n, m_pred)</code> 是 <span><span>ee</span><span><span><span></span><span>e</span></span></span></span>，<code>b - e * (ki * dt)</code> 是零偏更新，<code>(q * Quat&lt;T&gt;::exp(w * dt)).normalized()</code> 是姿态更新。算术都是第 2 篇的 <code>Mat&lt;3,1,T&gt;</code> 运算符，没有手写循环。</p>
完整源码：cpp/include/kl/quat.hpp、mahony.hpp、baselines.hpp<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Hamilton quaternions, scalar first q = [w, x, y, z].</span></div></div><div><div><div>2</div></div><div><span>// q rotates body to world: v_w = q (x) [0, v_b] (x) q*, and dcm() is C(q) = C_wb.</span></div></div><div><div><div>3</div></div><div><span>// exp(v) maps a rotation vector to the unit quaternion [cos(|v|/2), sin(|v|/2) v/|v|].</span></div></div><div><div><div>4</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;cmath&gt;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>"kl/mat.hpp"</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>12</div></div><div><span>using</span><span> </span><span>Vec3</span><span> </span><span>=</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>15</div></div><div><span>constexpr</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>cross</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>a</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>b</span><span>) {</span></span></div></div><div><div><div>16</div></div><div><span>  </span><span>return</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>a</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>2</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>a</span><span>(</span><span>2</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>a</span><span>(</span><span>2</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>a</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>2</span><span>, </span><span>0</span><span>),</span></div></div><div><div><div>17</div></div><div><span>      </span><span>a</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>a</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>0</span><span>, </span><span>0</span><span>)}};</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>21</div></div><div><span>constexpr</span><span> </span><span>T</span><span> </span><span>dot</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>a</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>b</span><span>) {</span></span></div></div><div><div><div>22</div></div><div><span>  </span><span>return</span><span> </span><span>a</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>+</span><span> </span><span>a</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>+</span><span> </span><span>a</span><span>(</span><span>2</span><span>, </span><span>0</span><span>) </span><span>*</span><span> </span><span>b</span><span>(</span><span>2</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>// [v]_x, so that skew(a) * b = a x b</span></div></div><div><div><div>26</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>27</div></div><div><span>constexpr</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>skew</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>28</div></div><div><span>  </span><span>return</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>-</span><span>v</span><span>(</span><span>2</span><span>, </span><span>0</span><span>), </span><span>v</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>v</span><span>(</span><span>2</span><span>, </span><span>0</span><span>), </span><span>0</span><span>, </span><span>-</span><span>v</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>-</span><span>v</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>v</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>0</span><span>}};</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>32</div></div><div><span>struct</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>33</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>T</span><span> </span><span>SMALL</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span><span>1</span><span>e</span></span><span>-</span><span>8</span><span>);</span><span>  // below this angle (rad) use the Taylor expansion</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>  </span><span>T</span><span> </span><span>w</span><span> </span><span>=</span><span> </span><span>1</span><span>, </span><span>x</span><span> </span><span>=</span><span> </span><span>0</span><span>, </span><span>y</span><span> </span><span>=</span><span> </span><span>0</span><span>, </span><span>z</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>Quat</span><span> </span><span>identity</span><span>() { </span><span>return</span><span> </span><span>Quat</span><span>{}; }</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Quat</span><span> </span><span>operator</span><span>*(</span><span>const</span><span> </span><span>Quat</span><span>&amp;</span><span><span> </span><span>b</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>40</div></div><div><span>    </span><span>return</span><span> {</span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span> </span><span>-</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span> </span><span>-</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span> </span><span>-</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span>, </span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span> </span><span>+</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span> </span><span>-</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span>,</span></div></div><div><div><div>41</div></div><div><span>      </span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span> </span><span>-</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span>, </span><span>w</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>z</span><span> </span><span>+</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>y</span><span> </span><span>-</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>x</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>b</span><span>.</span><span>w</span><span>};</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Quat</span><span> </span><span>conj</span><span>() </span><span>const</span><span> { </span><span>return</span><span> {</span><span>w</span><span>, </span><span>-</span><span>x</span><span>, </span><span>-</span><span>y</span><span>, </span><span>-</span><span>z</span><span>}; }</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>  </span><span>Quat</span><span> </span><span>normalized</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>47</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>n</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>w</span><span> </span><span>*</span><span> </span><span>w</span><span> </span><span>+</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>y</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>z</span><span>);</span></div></div><div><div><div>48</div></div><div><span>    </span><span>return</span><span> {</span><span>w</span><span> </span><span>/</span><span> </span><span>n</span><span>, </span><span>x</span><span> </span><span>/</span><span> </span><span>n</span><span>, </span><span>y</span><span> </span><span>/</span><span> </span><span>n</span><span>, </span><span>z</span><span> </span><span>/</span><span> </span><span>n</span><span>};</span></div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>// Rotation vector -&gt; unit quaternion.</span></div></div><div><div><div>52</div></div><div><span>  </span><span>static</span><span> </span><span>Quat</span><span> </span><span>exp</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>53</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>th</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>v</span><span>, </span><span>v</span><span>));</span></div></div><div><div><div>54</div></div><div><span>    </span><span>if</span><span> (</span><span>th</span><span> </span><span>&lt;</span><span> </span><span>SMALL</span><span>) </span><span>return</span><span> {</span><span>1</span><span> </span><span>-</span><span> </span><span>th</span><span> </span><span>*</span><span> </span><span>th</span><span> </span><span>/</span><span> </span><span>8</span><span>, </span><span>v</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>/</span><span> </span><span>2</span><span>, </span><span>v</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>/</span><span> </span><span>2</span><span>, </span><span>v</span><span>(</span><span>2</span><span>, </span><span>0</span><span>) </span><span>/</span><span> </span><span>2</span><span>};</span></div></div><div><div><div>55</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> std::</span><span>sin</span><span>(</span><span>th</span><span> </span><span>/</span><span> </span><span>2</span><span>) </span><span>/</span><span> </span><span>th</span><span>;</span></div></div><div><div><div>56</div></div><div><span>    </span><span>return</span><span><span> {</span><span>std</span><span>::</span></span><span>cos</span><span>(</span><span>th</span><span> </span><span>/</span><span> </span><span>2</span><span>), </span><span>s</span><span> </span><span>*</span><span> </span><span>v</span><span>(</span><span>0</span><span>, </span><span>0</span><span>), </span><span>s</span><span> </span><span>*</span><span> </span><span>v</span><span>(</span><span>1</span><span>, </span><span>0</span><span>), </span><span>s</span><span> </span><span>*</span><span> </span><span>v</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)};</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span><span>  </span></span><span>// Unit quaternion -&gt; rotation vector, using the sign with w &gt;= 0 (angle in [0, pi]).</span></div></div><div><div><div>60</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>log</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>61</div></div><div><span>    </span><span>const</span><span> </span><span>Quat</span><span> </span><span>q</span><span> </span><span>=</span><span> </span><span>w</span><span> </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>?</span><span> </span><span>Quat</span><span>{</span><span>-</span><span>w</span><span>, </span><span>-</span><span>x</span><span>, </span><span>-</span><span>y</span><span>, </span><span>-</span><span>z</span><span>} </span><span>:</span><span> </span><span>*</span><span>this</span><span>;</span></div></div><div><div><div>62</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>u</span><span>{{</span><span>q</span><span>.</span><span>x</span><span>, </span><span>q</span><span>.</span><span>y</span><span>, </span><span>q</span><span>.</span><span>z</span><span>}};</span></div></div><div><div><div>63</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>n</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>u</span><span>, </span><span>u</span><span>));</span></div></div><div><div><div>64</div></div><div><span>    </span><span>if</span><span> (</span><span>n</span><span> </span><span>&lt;</span><span> </span><span>SMALL</span><span>) </span><span>return</span><span> </span><span>u</span><span> </span><span>*</span><span> (</span><span>2</span><span> </span><span>/</span><span> </span><span>q</span><span>.</span><span>w</span><span>);</span></div></div><div><div><div>65</div></div><div><span>    </span><span>return</span><span> </span><span>u</span><span> </span><span>*</span><span> (</span><span>2</span><span> </span><span>*</span><span> std::</span><span>atan2</span><span>(</span><span>n</span><span>, </span><span>q</span><span>.</span><span>w</span><span>) </span><span>/</span><span> </span><span>n</span><span>);</span></div></div><div><div><div>66</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>// C(q) = C_wb, body to world.</span></div></div><div><div><div>69</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>dcm</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>70</div></div><div><span>    </span><span>return</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;</span><span>{{</span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (</span><span>y</span><span> </span><span>*</span><span> </span><span>y</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>z</span><span>), </span><span>2</span><span> </span><span>*</span><span> (</span><span>x</span><span> </span><span>*</span><span> </span><span>y</span><span> </span><span>-</span><span> </span><span>w</span><span> </span><span>*</span><span> </span><span>z</span><span>), </span><span>2</span><span> </span><span>*</span><span> (</span><span>x</span><span> </span><span>*</span><span> </span><span>z</span><span> </span><span>+</span><span> </span><span>w</span><span> </span><span>*</span><span> </span><span>y</span><span>),</span></div></div><div><div><div>71</div></div><div><span>             </span><span>2</span><span> </span><span>*</span><span> (</span><span>x</span><span> </span><span>*</span><span> </span><span>y</span><span> </span><span>+</span><span> </span><span>w</span><span> </span><span>*</span><span> </span><span>z</span><span>), </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (</span><span>x</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>z</span><span>), </span><span>2</span><span> </span><span>*</span><span> (</span><span>y</span><span> </span><span>*</span><span> </span><span>z</span><span> </span><span>-</span><span> </span><span>w</span><span> </span><span>*</span><span> </span><span>x</span><span>),</span></div></div><div><div><div>72</div></div><div><span>             </span><span>2</span><span> </span><span>*</span><span> (</span><span>x</span><span> </span><span>*</span><span> </span><span>z</span><span> </span><span>-</span><span> </span><span>w</span><span> </span><span>*</span><span> </span><span>y</span><span>), </span><span>2</span><span> </span><span>*</span><span> (</span><span>y</span><span> </span><span>*</span><span> </span><span>z</span><span> </span><span>+</span><span> </span><span>w</span><span> </span><span>*</span><span> </span><span>x</span><span>), </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (</span><span>x</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>y</span><span>)}};</span></div></div><div><div><div>73</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span><span>  </span></span><span>// C_wb -&gt; unit quaternion with w &gt;= 0 (Shepperd's method).</span></div></div><div><div><div>76</div></div><div><span>  </span><span>static</span><span> </span><span>Quat</span><span> </span><span>from_dcm</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>C</span><span>) {</span></span></div></div><div><div><div>77</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>C</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>1</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>2</span><span>);</span></div></div><div><div><div>78</div></div><div><span>    </span><span>Quat</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>79</div></div><div><span>    </span><span>if</span><span> (</span><span>t</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>80</div></div><div><span>      </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> std::</span><span>sqrt</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>t</span><span>);</span></div></div><div><div><div>81</div></div><div><span>      </span><span>q</span><span> </span><span>=</span><span> {</span><span>s</span><span> </span><span>/</span><span> </span><span>4</span><span>, (</span><span>C</span><span>(</span><span>2</span><span>, </span><span>1</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>2</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, (</span><span>C</span><span>(</span><span>0</span><span>, </span><span>2</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, (</span><span>C</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>0</span><span>, </span><span>1</span><span>)) </span><span>/</span><span> </span><span>s</span><span>};</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>C</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>&gt;=</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>1</span><span>) </span><span>&amp;&amp;</span><span> </span><span>C</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>&gt;=</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>2</span><span>)) {</span></div></div><div><div><div>83</div></div><div><span>      </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> std::</span><span>sqrt</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>C</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>1</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>2</span><span>));</span></div></div><div><div><div>84</div></div><div><span>      </span><span>q</span><span> </span><span>=</span><span> {(</span><span>C</span><span>(</span><span>2</span><span>, </span><span>1</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>2</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, </span><span>s</span><span> </span><span>/</span><span> </span><span>4</span><span>, (</span><span>C</span><span>(</span><span>0</span><span>, </span><span>1</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>0</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, (</span><span>C</span><span>(</span><span>0</span><span>, </span><span>2</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)) </span><span>/</span><span> </span><span>s</span><span>};</span></div></div><div><div><div>85</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>C</span><span>(</span><span>1</span><span>, </span><span>1</span><span>) </span><span>&gt;=</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>2</span><span>)) {</span></div></div><div><div><div>86</div></div><div><span>      </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> std::</span><span>sqrt</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>1</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>2</span><span>));</span></div></div><div><div><div>87</div></div><div><span>      </span><span>q</span><span> </span><span>=</span><span> {(</span><span>C</span><span>(</span><span>0</span><span>, </span><span>2</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, (</span><span>C</span><span>(</span><span>0</span><span>, </span><span>1</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>0</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, </span><span>s</span><span> </span><span>/</span><span> </span><span>4</span><span>, (</span><span>C</span><span>(</span><span>1</span><span>, </span><span>2</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>1</span><span>)) </span><span>/</span><span> </span><span>s</span><span>};</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>89</div></div><div><span>      </span><span>const</span><span> </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>2</span><span> </span><span>*</span><span> std::</span><span>sqrt</span><span>(</span><span>1</span><span> </span><span>+</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>2</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>0</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>1</span><span>, </span><span>1</span><span>));</span></div></div><div><div><div>90</div></div><div><span>      </span><span>q</span><span> </span><span>=</span><span> {(</span><span>C</span><span>(</span><span>1</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>C</span><span>(</span><span>0</span><span>, </span><span>1</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, (</span><span>C</span><span>(</span><span>0</span><span>, </span><span>2</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>0</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, (</span><span>C</span><span>(</span><span>1</span><span>, </span><span>2</span><span>) </span><span>+</span><span> </span><span>C</span><span>(</span><span>2</span><span>, </span><span>1</span><span>)) </span><span>/</span><span> </span><span>s</span><span>, </span><span>s</span><span> </span><span>/</span><span> </span><span>4</span><span>};</span></div></div><div><div><div>91</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>92</div></div><div><span>    </span><span>return</span><span> </span><span>q</span><span>.</span><span>w</span><span> </span><span>&lt;</span><span> </span><span>0</span><span> </span><span>?</span><span> </span><span>Quat</span><span>{</span><span>-</span><span>q</span><span>.</span><span>w</span><span>, </span><span>-</span><span>q</span><span>.</span><span>x</span><span>, </span><span>-</span><span>q</span><span>.</span><span>y</span><span>, </span><span>-</span><span>q</span><span>.</span><span>z</span><span>} </span><span>:</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>93</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>// (yaw, pitch, roll) in radians for C_wb = Rz(yaw) Ry(pitch) Rx(roll).</span></div></div><div><div><div>96</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>euler_zyx</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>97</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>yaw</span><span> </span><span>=</span><span> std::</span><span>atan2</span><span>(</span><span>2</span><span> </span><span>*</span><span> (</span><span>w</span><span> </span><span>*</span><span> </span><span>z</span><span> </span><span>+</span><span> </span><span>x</span><span> </span><span>*</span><span> </span><span>y</span><span>), </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (</span><span>y</span><span> </span><span>*</span><span> </span><span>y</span><span> </span><span>+</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>z</span><span>));</span></div></div><div><div><div>98</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>sp</span><span> </span><span>=</span><span> std::</span><span>fmax</span><span>(</span><span>T</span><span>(</span><span>-</span><span>1</span><span>), std::</span><span>fmin</span><span>(</span><span>T</span><span>(</span><span>1</span><span>), </span><span>2</span><span> </span><span>*</span><span> (</span><span>w</span><span> </span><span>*</span><span> </span><span>y</span><span> </span><span>-</span><span> </span><span>z</span><span> </span><span>*</span><span> </span><span>x</span><span>)));</span></div></div><div><div><div>99</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>roll</span><span> </span><span>=</span><span> std::</span><span>atan2</span><span>(</span><span>2</span><span> </span><span>*</span><span> (</span><span>w</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>z</span><span>), </span><span>1</span><span> </span><span>-</span><span> </span><span>2</span><span> </span><span>*</span><span> (</span><span>x</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>*</span><span> </span><span>y</span><span>));</span></div></div><div><div><div>100</div></div><div><span>    </span><span>return</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>yaw</span><span>, std::</span><span>asin</span><span>(</span><span>sp</span><span>), </span><span>roll</span><span>}};</span></div></div><div><div><div>101</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>102</div></div><div><span>};</span></div></div><div><div><div>103</div></div><div>
</div></div><div><div><div>104</div></div><div><span>// Attitude error vector log(est^-1 (x) truth), in the estimate's body frame.</span></div></div><div><div><div>105</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>106</div></div><div><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>att_error</span><span>(</span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>est</span><span>, </span></span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>truth</span><span>) {</span></span></div></div><div><div><div>107</div></div><div><span>  </span><span>return</span><span> (</span><span>est</span><span>.</span><span>conj</span><span>() </span><span>*</span><span> </span><span>truth</span><span>).</span><span>log</span><span>();</span></div></div><div><div><div>108</div></div><div><span>}</span></div></div><div><div><div>109</div></div><div>
</div></div><div><div><div>110</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Mahony explicit complementary filter with an integral bias term.</span></div></div><div><div><div>2</div></div><div><span>// q &lt;- q (x) exp((w_m - b + kp e) dt), b &lt;- b - ki e dt,</span></div></div><div><div><div>3</div></div><div><span>// e = a_meas x a_pred + m_meas x m_pred (unit vectors in the body frame).</span></div></div><div><div><div>4</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"kl/quat.hpp"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// Reference magnetic field in the NED world frame: unit norm, 50 deg dip, no declination.</span></div></div><div><div><div>10</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>11</div></div><div><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>default_mag_w</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>  </span><span>const</span><span> </span><span>T</span><span> </span><span>dip</span><span> </span><span>=</span><span> </span><span>T</span><span>(</span><span>50</span><span>) </span><span>*</span><span> </span><span>T</span><span>(</span><span>3.14159265358979323846</span><span>) </span><span>/</span><span> </span><span>180</span><span>;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>return</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span><span>{{</span><span>std</span><span>::</span></span><span>cos</span><span>(</span><span>dip</span><span>), </span><span>0</span><span>, std::</span><span>sin</span><span>(</span><span>dip</span><span>)}};</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>17</div></div><div><span>class</span><span> </span><span>Mahony</span><span> {</span></div></div><div><div><div>18</div></div><div><span>public:</span></div></div><div><div><div>19</div></div><div><span>  </span><span>T</span><span> </span><span>kp</span><span>, </span><span>ki</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>q</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>b</span><span>{};</span></div></div><div><div><div>22</div></div><div><span>  </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_w</span><span>;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>  </span><span>Mahony</span><span>(</span><span>T</span><span><span> </span><span>kp</span><span>, </span></span><span>T</span><span><span> </span><span>ki</span><span>, </span></span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q0</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m_w</span><span> </span></span><span>=</span><span> </span><span>default_mag_w</span><span>&lt;</span><span>T</span><span>&gt;())</span></div></div><div><div><div>25</div></div><div><span><span>      </span></span><span>: </span><span>kp</span><span>(</span><span>kp</span><span>), </span><span>ki</span><span>(</span><span>ki</span><span>), </span><span>q</span><span>(</span><span>q0</span><span>), </span><span>m_w</span><span>(</span><span>m_w</span><span>) {}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>  </span><span>const</span><span> </span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span> </span><span>step</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>28</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>Ct</span><span> </span><span>=</span><span> </span><span>q</span><span>.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>29</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>a_pred</span><span> </span><span>=</span><span> </span><span>Ct</span><span> </span><span>*</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}};</span></div></div><div><div><div>30</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_pred</span><span> </span><span>=</span><span> </span><span>Ct</span><span> </span><span>*</span><span> </span><span>m_w</span><span>;</span></div></div><div><div><div>31</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>a_n</span><span> </span><span>=</span><span> </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>)));</span></div></div><div><div><div>32</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>m_n</span><span> </span><span>=</span><span> </span><span>mag</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>)));</span></div></div><div><div><div>33</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>e</span><span> </span><span>=</span><span> </span><span>cross</span><span>(</span><span>a_n</span><span>, </span><span>a_pred</span><span>) </span><span>+</span><span> </span><span>cross</span><span>(</span><span>m_n</span><span>, </span><span>m_pred</span><span>);</span></div></div><div><div><div>34</div></div><div><span>    </span><span>b</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>-</span><span> </span><span>e</span><span> </span><span>*</span><span> (</span><span>ki</span><span> </span><span>*</span><span> </span><span>dt</span><span>);</span><span>  // update the bias before forming the rate</span></div></div><div><div><div>35</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>w</span><span> </span><span>=</span><span> </span><span>gyro</span><span> </span><span>-</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>e</span><span> </span><span>*</span><span> </span><span>kp</span><span>;</span></div></div><div><div><div>36</div></div><div><span>    </span><span>q</span><span> </span><span>=</span><span> (</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>w</span><span> </span><span>*</span><span> </span><span>dt</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>37</div></div><div><span>    </span><span>return</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>};</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Attitude baselines: TRIAD snapshot and gyro-only integration.</span></div></div><div><div><div>2</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/quat.hpp"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// Attitude q (body -&gt; world) from one accelerometer + magnetometer snapshot.</span></div></div><div><div><div>8</div></div><div><span>// Specific force at rest points opposite to gravity, [0, 0, -1] in the NED world frame.</span></div></div><div><div><div>9</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span>Quat</span><span>&lt;</span><span>T</span><span>&gt; </span><span>triad</span><span>(</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>accel</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>mag</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m_w</span><span>) {</span></span></div></div><div><div><div>11</div></div><div><span>  </span><span>auto</span><span> </span><span>frame</span><span> </span><span>=</span><span> [](</span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v1</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>v2</span><span>) {</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>t2</span><span> </span><span>=</span><span> </span><span>cross</span><span>(</span><span>v1</span><span>, </span><span>v2</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>t2</span><span> </span><span>=</span><span> </span><span>t2</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>t2</span><span>, </span><span>t2</span><span>)));</span></div></div><div><div><div>14</div></div><div><span>    </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>t3</span><span> </span><span>=</span><span> </span><span>cross</span><span>(</span><span>v1</span><span>, </span><span>t2</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>f</span><span>{};</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>3</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>      </span><span>f</span><span>(</span><span>i</span><span>, </span><span>0</span><span>) </span><span>=</span><span> </span><span>v1</span><span>(</span><span>i</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>18</div></div><div><span>      </span><span>f</span><span>(</span><span>i</span><span>, </span><span>1</span><span>) </span><span>=</span><span> </span><span>t2</span><span>(</span><span>i</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>19</div></div><div><span>      </span><span>f</span><span>(</span><span>i</span><span>, </span><span>2</span><span>) </span><span>=</span><span> </span><span>t3</span><span>(</span><span>i</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>    </span><span>return</span><span> </span><span>f</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>23</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>b1</span><span> </span><span>=</span><span> </span><span>accel</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>accel</span><span>, </span><span>accel</span><span>)));</span></div></div><div><div><div>24</div></div><div><span>  </span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt; </span><span>b2</span><span> </span><span>=</span><span> </span><span>mag</span><span> </span><span>*</span><span> (</span><span>1</span><span> </span><span>/</span><span> std::</span><span>sqrt</span><span>(</span><span>dot</span><span>(</span><span>mag</span><span>, </span><span>mag</span><span>)));</span></div></div><div><div><div>25</div></div><div><span>  </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>, </span><span>T</span><span>&gt; </span><span>C</span><span> </span><span>=</span><span> </span><span>frame</span><span>(</span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>{{</span><span>0</span><span>, </span><span>0</span><span>, </span><span>-</span><span>1</span><span>}}, </span><span>m_w</span><span>) </span><span>*</span><span> </span><span>frame</span><span>(</span><span>b1</span><span>, </span><span>b2</span><span>).</span><span>transpose</span><span>();</span></div></div><div><div><div>26</div></div><div><span>  </span><span>return</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>from_dcm</span><span>(</span><span>C</span><span>);</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>// One gyro-only integration step, q &lt;- normalize(q (x) exp(gyro dt)).</span></div></div><div><div><div>30</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>31</div></div><div><span>void</span><span> </span><span>gyro_step</span><span>(</span><span>Quat</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>q</span><span>, </span></span><span>const</span><span> </span><span>Vec3</span><span>&lt;</span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>gyro</span><span>, </span></span><span>T</span><span><span> </span><span>dt</span><span>) {</span></span></div></div><div><div><div>32</div></div><div><span>  </span><span>q</span><span> </span><span>=</span><span> (</span><span>q</span><span> </span><span>*</span><span> Quat&lt;</span><span>T</span><span>&gt;::</span><span>exp</span><span>(</span><span>gyro</span><span> </span><span>*</span><span> </span><span>dt</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>w</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>x</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>y</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>z</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>impl</span><span> </span><span>Mul</span><span> </span><span>for</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Quat</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>fn</span><span> </span><span>mul</span><span>(</span><span>self</span><span>, </span><span>b</span><span>: </span><span>Quat</span><span>) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>w</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.w - </span><span>a</span><span>.x * </span><span>b</span><span>.x - </span><span>a</span><span>.y * </span><span>b</span><span>.y - </span><span>a</span><span>.z * </span><span>b</span><span>.z,</span></div></div><div><div><div>15</div></div><div><span>            </span><span>x</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.x + </span><span>a</span><span>.x * </span><span>b</span><span>.w + </span><span>a</span><span>.y * </span><span>b</span><span>.z - </span><span>a</span><span>.z * </span><span>b</span><span>.y,</span></div></div><div><div><div>16</div></div><div><span>            </span><span>y</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.y - </span><span>a</span><span>.x * </span><span>b</span><span>.z + </span><span>a</span><span>.y * </span><span>b</span><span>.w + </span><span>a</span><span>.z * </span><span>b</span><span>.x,</span></div></div><div><div><div>17</div></div><div><span>            </span><span>z</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.z + </span><span>a</span><span>.x * </span><span>b</span><span>.y - </span><span>a</span><span>.y * </span><span>b</span><span>.x + </span><span>a</span><span>.z * </span><span>b</span><span>.w,</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>impl</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>identity</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>24</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>w</span><span>: </span><span>1.0</span><span>,</span></div></div><div><div><div>26</div></div><div><span>            </span><span>x</span><span>: </span><span>0.0</span><span>,</span></div></div><div><div><div>27</div></div><div><span>            </span><span>y</span><span>: </span><span>0.0</span><span>,</span></div></div><div><div><div>28</div></div><div><span>            </span><span>z</span><span>: </span><span>0.0</span><span>,</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>conj</span><span>(</span><span>self</span><span>) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>33</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>34</div></div><div><span>            </span><span>w</span><span>: </span><span>self</span><span>.w,</span></div></div><div><div><div>35</div></div><div><span>            </span><span>x</span><span>: -</span><span>self</span><span>.x,</span></div></div><div><div><div>36</div></div><div><span>            </span><span>y</span><span>: -</span><span>self</span><span>.y,</span></div></div><div><div><div>37</div></div><div><span>            </span><span>z</span><span>: -</span><span>self</span><span>.z,</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>normalized</span><span>(</span><span>self</span><span>) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>42</div></div><div><span>        </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>self</span><span>.w * </span><span>self</span><span>.w + </span><span>self</span><span>.x * </span><span>self</span><span>.x + </span><span>self</span><span>.y * </span><span>self</span><span>.y + </span><span>self</span><span>.z * </span><span>self</span><span>.z);</span></div></div><div><div><div>43</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>44</div></div><div><span>            </span><span>w</span><span>: </span><span>self</span><span>.w / </span><span>n</span><span>,</span></div></div><div><div><div>45</div></div><div><span>            </span><span>x</span><span>: </span><span>self</span><span>.x / </span><span>n</span><span>,</span></div></div><div><div><div>46</div></div><div><span>            </span><span>y</span><span>: </span><span>self</span><span>.y / </span><span>n</span><span>,</span></div></div><div><div><div>47</div></div><div><span>            </span><span>z</span><span>: </span><span>self</span><span>.z / </span><span>n</span><span>,</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>    </span><span>/// Rotation vector -&gt; unit quaternion.</span></div></div><div><div><div>52</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>exp</span><span>(</span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>53</div></div><div><span>        </span><span>let</span><span> </span><span>th</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] * </span><span>v</span><span>[</span><span>0</span><span>] + </span><span>v</span><span>[</span><span>1</span><span>] * </span><span>v</span><span>[</span><span>1</span><span>] + </span><span>v</span><span>[</span><span>2</span><span>] * </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>54</div></div><div><span>        </span><span>if</span><span> </span><span>th</span><span><span> </span><span>&lt;</span><span> </span></span><span>SMALL</span><span> {</span></div></div><div><div><div>55</div></div><div><span>            </span><span>return</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>56</div></div><div><span>                </span><span>w</span><span>: </span><span>1.0</span><span> - </span><span>th</span><span> * </span><span>th</span><span> / </span><span>8.0</span><span>,</span></div></div><div><div><div>57</div></div><div><span>                </span><span>x</span><span>: </span><span>0.5</span><span> * </span><span>v</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>58</div></div><div><span>                </span><span>y</span><span>: </span><span>0.5</span><span> * </span><span>v</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>59</div></div><div><span>                </span><span>z</span><span>: </span><span>0.5</span><span> * </span><span>v</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>60</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>sin</span><span>(</span><span>0.5</span><span> * </span><span>th</span><span>) / </span><span>th</span><span>;</span></div></div><div><div><div>63</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>64</div></div><div><span>            </span><span>w</span><span>: </span><span>cos</span><span>(</span><span>0.5</span><span> * </span><span>th</span><span>),</span></div></div><div><div><div>65</div></div><div><span>            </span><span>x</span><span>: </span><span>s</span><span> * </span><span>v</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>66</div></div><div><span>            </span><span>y</span><span>: </span><span>s</span><span> * </span><span>v</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>67</div></div><div><span>            </span><span>z</span><span>: </span><span>s</span><span> * </span><span>v</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>68</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>70</div></div><div>
</div></div><div><div><div>71</div></div><div><span>    </span><span>/// Unit quaternion -&gt; rotation vector, using the sign with w &gt;= 0.</span></div></div><div><div><div>72</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>log</span><span>(</span><span>self</span><span>) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>73</div></div><div><span>        </span><span>let</span><span> </span><span>q</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>self</span><span><span>.w </span><span>&lt;</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>74</div></div><div><span>            </span><span>Quat</span><span> {</span></div></div><div><div><div>75</div></div><div><span>                </span><span>w</span><span>: -</span><span>self</span><span>.w,</span></div></div><div><div><div>76</div></div><div><span>                </span><span>x</span><span>: -</span><span>self</span><span>.x,</span></div></div><div><div><div>77</div></div><div><span>                </span><span>y</span><span>: -</span><span>self</span><span>.y,</span></div></div><div><div><div>78</div></div><div><span>                </span><span>z</span><span>: -</span><span>self</span><span>.z,</span></div></div><div><div><div>79</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>81</div></div><div><span>            </span><span>self</span></div></div><div><div><div>82</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>83</div></div><div><span>        </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>q</span><span>.x * </span><span>q</span><span>.x + </span><span>q</span><span>.y * </span><span>q</span><span>.y + </span><span>q</span><span>.z * </span><span>q</span><span>.z);</span></div></div><div><div><div>84</div></div><div><span>        </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>n</span><span><span> </span><span>&lt;</span><span> </span></span><span>SMALL</span><span> {</span></div></div><div><div><div>85</div></div><div><span>            </span><span>2.0</span><span> / </span><span>q</span><span>.w</span></div></div><div><div><div>86</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>87</div></div><div><span>            </span><span>2.0</span><span> * </span><span>atan2</span><span>(</span><span>n</span><span>, </span><span>q</span><span>.w) / </span><span>n</span></div></div><div><div><div>88</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>89</div></div><div><span><span>        </span></span><span>[</span><span>k</span><span> * </span><span>q</span><span>.x, </span><span>k</span><span> * </span><span>q</span><span>.y, </span><span>k</span><span> * </span><span>q</span><span>.z]</span></div></div><div><div><div>90</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>step</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>// Predicted body-frame directions: C^T v</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>ct</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>pred</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span><span>]</span><span>|</span><span> -&gt; [</span></span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>o</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>3</span><span>];</span></div></div><div><div><div>6</div></div><div><span>            </span><span>for</span><span> (</span><span>i</span><span>, </span><span>oi</span><span>) </span><span>in</span><span> </span><span>o</span><span>.</span><span>iter_mut</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>*</span><span>oi</span><span><span> </span><span>=</span><span> </span></span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>0</span><span>] * </span><span>v</span><span>[</span><span>0</span><span>] + </span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>1</span><span>] * </span><span>v</span><span>[</span><span>1</span><span>] + </span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>2</span><span>] * </span><span>v</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>            </span><span>o</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>ea</span><span><span> </span><span>=</span><span> </span></span><span>cross</span><span>(</span><span>unit</span><span>(</span><span>accel</span><span>), </span><span>pred</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span>, -</span><span>1.0</span><span>]));</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>em</span><span><span> </span><span>=</span><span> </span></span><span>cross</span><span>(</span><span>unit</span><span>(</span><span>mag</span><span>), </span><span>pred</span><span>(</span><span>self</span><span>.m_w));</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>3</span><span>];</span></div></div><div><div><div>14</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>let</span><span> </span><span>e</span><span><span> </span><span>=</span><span> </span></span><span>ea</span><span>[</span><span>i</span><span>] + </span><span>em</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>16</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>-=</span><span> </span></span><span>self</span><span>.ki * </span><span>e</span><span> * </span><span>dt</span><span>;</span></div></div><div><div><div>17</div></div><div><span>            </span><span>w</span><span>[</span><span>i</span><span><span>] </span><span>=</span><span> (</span></span><span>gyro</span><span>[</span><span>i</span><span>] - </span><span>self</span><span>.b[</span><span>i</span><span>] + </span><span>self</span><span>.kp * </span><span>e</span><span>) * </span><span>dt</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> (</span></span><span>self</span><span>.q * </span><span>Quat</span><span>::</span><span>exp</span><span>(</span><span>w</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Quat</code> 派生 <code>Copy</code>，所以 <code>self.q * Quat::exp(w)</code> 不会”移动走” <code>self.q</code>，赋值回 <code>self.q</code> 也没问题。<code>step</code> 里 <code>pred</code> 是个闭包，用 <code>ct</code>（<span><span>CTC^T</span><span><span><span></span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>）把世界系向量映射到机体系。Rust 版和 C++ 版的数学一样，差别在于：Rust 没有运算符重载到 <code>[Real; 3]</code> 上（数组不是我定义的类型，孤儿规则不允许），所以向量运算是显式的循环；C++ 的 <code>Vec3</code> 是自己的类型，运算符想怎么重载都行。</p>
完整源码：rust/src/quat.rs、mahony.rs、baselines.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Hamilton quaternions, scalar first q = [w, x, y, z].</span></div></div><div><div><div>2</div></div><div><span>//! q rotates body to world, C(q) = C_wb. `exp(v)` maps a rotation vector to the</span></div></div><div><div><div>3</div></div><div><span>//! unit quaternion [cos(|v|/2), sin(|v|/2) v/|v|].</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::</span></span><span>Mat</span><span>;</span></div></div><div><div><div>6</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, asin, atan2, cos, sin, sqrt};</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>ops</span><span>::</span></span><span>Mul</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>/// Below this angle (rad) use the Taylor expansion.</span></div></div><div><div><div>10</div></div><div><span>const</span><span> </span><span>SMALL</span><span>: </span><span>Real</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>e-</span><span>8</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>13</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pub</span><span> </span><span>w</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>x</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>y</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span> </span><span>z</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>impl</span><span> </span><span>Mul</span><span> </span><span>for</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Quat</span><span>;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>fn</span><span> </span><span>mul</span><span>(</span><span>self</span><span>, </span><span>b</span><span>: </span><span>Quat</span><span>) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>;</span></div></div><div><div><div>24</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>w</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.w - </span><span>a</span><span>.x * </span><span>b</span><span>.x - </span><span>a</span><span>.y * </span><span>b</span><span>.y - </span><span>a</span><span>.z * </span><span>b</span><span>.z,</span></div></div><div><div><div>26</div></div><div><span>            </span><span>x</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.x + </span><span>a</span><span>.x * </span><span>b</span><span>.w + </span><span>a</span><span>.y * </span><span>b</span><span>.z - </span><span>a</span><span>.z * </span><span>b</span><span>.y,</span></div></div><div><div><div>27</div></div><div><span>            </span><span>y</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.y - </span><span>a</span><span>.x * </span><span>b</span><span>.z + </span><span>a</span><span>.y * </span><span>b</span><span>.w + </span><span>a</span><span>.z * </span><span>b</span><span>.x,</span></div></div><div><div><div>28</div></div><div><span>            </span><span>z</span><span>: </span><span>a</span><span>.w * </span><span>b</span><span>.z + </span><span>a</span><span>.x * </span><span>b</span><span>.y - </span><span>a</span><span>.y * </span><span>b</span><span>.x + </span><span>a</span><span>.z * </span><span>b</span><span>.w,</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>impl</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>identity</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>35</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>36</div></div><div><span>            </span><span>w</span><span>: </span><span>1.0</span><span>,</span></div></div><div><div><div>37</div></div><div><span>            </span><span>x</span><span>: </span><span>0.0</span><span>,</span></div></div><div><div><div>38</div></div><div><span>            </span><span>y</span><span>: </span><span>0.0</span><span>,</span></div></div><div><div><div>39</div></div><div><span>            </span><span>z</span><span>: </span><span>0.0</span><span>,</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>conj</span><span>(</span><span>self</span><span>) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>44</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>45</div></div><div><span>            </span><span>w</span><span>: </span><span>self</span><span>.w,</span></div></div><div><div><div>46</div></div><div><span>            </span><span>x</span><span>: -</span><span>self</span><span>.x,</span></div></div><div><div><div>47</div></div><div><span>            </span><span>y</span><span>: -</span><span>self</span><span>.y,</span></div></div><div><div><div>48</div></div><div><span>            </span><span>z</span><span>: -</span><span>self</span><span>.z,</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>normalized</span><span>(</span><span>self</span><span>) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>53</div></div><div><span>        </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>self</span><span>.w * </span><span>self</span><span>.w + </span><span>self</span><span>.x * </span><span>self</span><span>.x + </span><span>self</span><span>.y * </span><span>self</span><span>.y + </span><span>self</span><span>.z * </span><span>self</span><span>.z);</span></div></div><div><div><div>54</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>55</div></div><div><span>            </span><span>w</span><span>: </span><span>self</span><span>.w / </span><span>n</span><span>,</span></div></div><div><div><div>56</div></div><div><span>            </span><span>x</span><span>: </span><span>self</span><span>.x / </span><span>n</span><span>,</span></div></div><div><div><div>57</div></div><div><span>            </span><span>y</span><span>: </span><span>self</span><span>.y / </span><span>n</span><span>,</span></div></div><div><div><div>58</div></div><div><span>            </span><span>z</span><span>: </span><span>self</span><span>.z / </span><span>n</span><span>,</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span>    </span><span>/// Rotation vector -&gt; unit quaternion.</span></div></div><div><div><div>63</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>exp</span><span>(</span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>64</div></div><div><span>        </span><span>let</span><span> </span><span>th</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] * </span><span>v</span><span>[</span><span>0</span><span>] + </span><span>v</span><span>[</span><span>1</span><span>] * </span><span>v</span><span>[</span><span>1</span><span>] + </span><span>v</span><span>[</span><span>2</span><span>] * </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>65</div></div><div><span>        </span><span>if</span><span> </span><span>th</span><span><span> </span><span>&lt;</span><span> </span></span><span>SMALL</span><span> {</span></div></div><div><div><div>66</div></div><div><span>            </span><span>return</span><span> </span><span>Quat</span><span> {</span></div></div><div><div><div>67</div></div><div><span>                </span><span>w</span><span>: </span><span>1.0</span><span> - </span><span>th</span><span> * </span><span>th</span><span> / </span><span>8.0</span><span>,</span></div></div><div><div><div>68</div></div><div><span>                </span><span>x</span><span>: </span><span>0.5</span><span> * </span><span>v</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>69</div></div><div><span>                </span><span>y</span><span>: </span><span>0.5</span><span> * </span><span>v</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>70</div></div><div><span>                </span><span>z</span><span>: </span><span>0.5</span><span> * </span><span>v</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>71</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>73</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>sin</span><span>(</span><span>0.5</span><span> * </span><span>th</span><span>) / </span><span>th</span><span>;</span></div></div><div><div><div>74</div></div><div><span>        </span><span>Quat</span><span> {</span></div></div><div><div><div>75</div></div><div><span>            </span><span>w</span><span>: </span><span>cos</span><span>(</span><span>0.5</span><span> * </span><span>th</span><span>),</span></div></div><div><div><div>76</div></div><div><span>            </span><span>x</span><span>: </span><span>s</span><span> * </span><span>v</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>77</div></div><div><span>            </span><span>y</span><span>: </span><span>s</span><span> * </span><span>v</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>78</div></div><div><span>            </span><span>z</span><span>: </span><span>s</span><span> * </span><span>v</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>79</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>81</div></div><div>
</div></div><div><div><div>82</div></div><div><span>    </span><span>/// Unit quaternion -&gt; rotation vector, using the sign with w &gt;= 0.</span></div></div><div><div><div>83</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>log</span><span>(</span><span>self</span><span>) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>84</div></div><div><span>        </span><span>let</span><span> </span><span>q</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>self</span><span><span>.w </span><span>&lt;</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>85</div></div><div><span>            </span><span>Quat</span><span> {</span></div></div><div><div><div>86</div></div><div><span>                </span><span>w</span><span>: -</span><span>self</span><span>.w,</span></div></div><div><div><div>87</div></div><div><span>                </span><span>x</span><span>: -</span><span>self</span><span>.x,</span></div></div><div><div><div>88</div></div><div><span>                </span><span>y</span><span>: -</span><span>self</span><span>.y,</span></div></div><div><div><div>89</div></div><div><span>                </span><span>z</span><span>: -</span><span>self</span><span>.z,</span></div></div><div><div><div>90</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>91</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>92</div></div><div><span>            </span><span>self</span></div></div><div><div><div>93</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>94</div></div><div><span>        </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>q</span><span>.x * </span><span>q</span><span>.x + </span><span>q</span><span>.y * </span><span>q</span><span>.y + </span><span>q</span><span>.z * </span><span>q</span><span>.z);</span></div></div><div><div><div>95</div></div><div><span>        </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>n</span><span><span> </span><span>&lt;</span><span> </span></span><span>SMALL</span><span> {</span></div></div><div><div><div>96</div></div><div><span>            </span><span>2.0</span><span> / </span><span>q</span><span>.w</span></div></div><div><div><div>97</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>98</div></div><div><span>            </span><span>2.0</span><span> * </span><span>atan2</span><span>(</span><span>n</span><span>, </span><span>q</span><span>.w) / </span><span>n</span></div></div><div><div><div>99</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>100</div></div><div><span><span>        </span></span><span>[</span><span>k</span><span> * </span><span>q</span><span>.x, </span><span>k</span><span> * </span><span>q</span><span>.y, </span><span>k</span><span> * </span><span>q</span><span>.z]</span></div></div><div><div><div>101</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div><span>    </span><span>/// Direction cosine matrix C(q) = C_wb (body -&gt; world).</span></div></div><div><div><div>104</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>dcm</span><span>(</span><span>self</span><span>) -&gt; </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>&gt; {</span></div></div><div><div><div>105</div></div><div><span>        </span><span>let</span><span> </span><span>Quat</span><span> { </span><span>w</span><span>, </span><span>x</span><span>, </span><span>y</span><span>, </span><span>z</span><span><span> } </span><span>=</span><span> </span></span><span>self</span><span>;</span></div></div><div><div><div>106</div></div><div><span>        </span><span>Mat</span><span> {</span></div></div><div><div><div>107</div></div><div><span>            </span><span>m</span><span>: [</span></div></div><div><div><div>108</div></div><div><span><span>                </span></span><span>[</span></div></div><div><div><div>109</div></div><div><span>                    </span><span>1.0</span><span> - </span><span>2.0</span><span> * (</span><span>y</span><span> * </span><span>y</span><span> + </span><span>z</span><span> * </span><span>z</span><span>),</span></div></div><div><div><div>110</div></div><div><span>                    </span><span>2.0</span><span> * (</span><span>x</span><span> * </span><span>y</span><span> - </span><span>w</span><span> * </span><span>z</span><span>),</span></div></div><div><div><div>111</div></div><div><span>                    </span><span>2.0</span><span> * (</span><span>x</span><span> * </span><span>z</span><span> + </span><span>w</span><span> * </span><span>y</span><span>),</span></div></div><div><div><div>112</div></div><div><span><span>                </span></span><span>],</span></div></div><div><div><div>113</div></div><div><span><span>                </span></span><span>[</span></div></div><div><div><div>114</div></div><div><span>                    </span><span>2.0</span><span> * (</span><span>x</span><span> * </span><span>y</span><span> + </span><span>w</span><span> * </span><span>z</span><span>),</span></div></div><div><div><div>115</div></div><div><span>                    </span><span>1.0</span><span> - </span><span>2.0</span><span> * (</span><span>x</span><span> * </span><span>x</span><span> + </span><span>z</span><span> * </span><span>z</span><span>),</span></div></div><div><div><div>116</div></div><div><span>                    </span><span>2.0</span><span> * (</span><span>y</span><span> * </span><span>z</span><span> - </span><span>w</span><span> * </span><span>x</span><span>),</span></div></div><div><div><div>117</div></div><div><span><span>                </span></span><span>],</span></div></div><div><div><div>118</div></div><div><span><span>                </span></span><span>[</span></div></div><div><div><div>119</div></div><div><span>                    </span><span>2.0</span><span> * (</span><span>x</span><span> * </span><span>z</span><span> - </span><span>w</span><span> * </span><span>y</span><span>),</span></div></div><div><div><div>120</div></div><div><span>                    </span><span>2.0</span><span> * (</span><span>y</span><span> * </span><span>z</span><span> + </span><span>w</span><span> * </span><span>x</span><span>),</span></div></div><div><div><div>121</div></div><div><span>                    </span><span>1.0</span><span> - </span><span>2.0</span><span> * (</span><span>x</span><span> * </span><span>x</span><span> + </span><span>y</span><span> * </span><span>y</span><span>),</span></div></div><div><div><div>122</div></div><div><span><span>                </span></span><span>],</span></div></div><div><div><div>123</div></div><div><span><span>            </span></span><span>],</span></div></div><div><div><div>124</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>126</div></div><div>
</div></div><div><div><div>127</div></div><div><span>    </span><span>/// C_wb -&gt; unit quaternion with w &gt;= 0 (Shepperd's method).</span></div></div><div><div><div>128</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>from_dcm</span><span>(</span><span>c</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>&gt;) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>129</div></div><div><span>        </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> &amp;</span></span><span>c</span><span>.m;</span></div></div><div><div><div>130</div></div><div><span>        </span><span>let</span><span> </span><span>t</span><span><span> </span><span>=</span><span> </span></span><span>c</span><span>[</span><span>0</span><span>][</span><span>0</span><span>] + </span><span>c</span><span>[</span><span>1</span><span>][</span><span>1</span><span>] + </span><span>c</span><span>[</span><span>2</span><span>][</span><span>2</span><span>];</span></div></div><div><div><div>131</div></div><div><span>        </span><span>let</span><span> </span><span>q</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>t</span><span><span> </span><span>&gt;</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>132</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>2.0</span><span> * </span><span>sqrt</span><span>(</span><span>1.0</span><span> + </span><span>t</span><span>);</span></div></div><div><div><div>133</div></div><div><span>            </span><span>Quat</span><span> {</span></div></div><div><div><div>134</div></div><div><span>                </span><span>w</span><span>: </span><span>0.25</span><span> * </span><span>s</span><span>,</span></div></div><div><div><div>135</div></div><div><span>                </span><span>x</span><span>: (</span><span>c</span><span>[</span><span>2</span><span>][</span><span>1</span><span>] - </span><span>c</span><span>[</span><span>1</span><span>][</span><span>2</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>136</div></div><div><span>                </span><span>y</span><span>: (</span><span>c</span><span>[</span><span>0</span><span>][</span><span>2</span><span>] - </span><span>c</span><span>[</span><span>2</span><span>][</span><span>0</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>137</div></div><div><span>                </span><span>z</span><span>: (</span><span>c</span><span>[</span><span>1</span><span>][</span><span>0</span><span>] - </span><span>c</span><span>[</span><span>0</span><span>][</span><span>1</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>138</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>139</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>c</span><span>[</span><span>0</span><span>][</span><span>0</span><span><span>] </span><span>&gt;=</span><span> </span></span><span>c</span><span>[</span><span>1</span><span>][</span><span>1</span><span><span>] </span><span>&amp;&amp;</span><span> </span></span><span>c</span><span>[</span><span>0</span><span>][</span><span>0</span><span><span>] </span><span>&gt;=</span><span> </span></span><span>c</span><span>[</span><span>2</span><span>][</span><span>2</span><span>] {</span></div></div><div><div><div>140</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>2.0</span><span> * </span><span>sqrt</span><span>(</span><span>1.0</span><span> + </span><span>c</span><span>[</span><span>0</span><span>][</span><span>0</span><span>] - </span><span>c</span><span>[</span><span>1</span><span>][</span><span>1</span><span>] - </span><span>c</span><span>[</span><span>2</span><span>][</span><span>2</span><span>]);</span></div></div><div><div><div>141</div></div><div><span>            </span><span>Quat</span><span> {</span></div></div><div><div><div>142</div></div><div><span>                </span><span>w</span><span>: (</span><span>c</span><span>[</span><span>2</span><span>][</span><span>1</span><span>] - </span><span>c</span><span>[</span><span>1</span><span>][</span><span>2</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>143</div></div><div><span>                </span><span>x</span><span>: </span><span>0.25</span><span> * </span><span>s</span><span>,</span></div></div><div><div><div>144</div></div><div><span>                </span><span>y</span><span>: (</span><span>c</span><span>[</span><span>0</span><span>][</span><span>1</span><span>] + </span><span>c</span><span>[</span><span>1</span><span>][</span><span>0</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>145</div></div><div><span>                </span><span>z</span><span>: (</span><span>c</span><span>[</span><span>0</span><span>][</span><span>2</span><span>] + </span><span>c</span><span>[</span><span>2</span><span>][</span><span>0</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>146</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>147</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>c</span><span>[</span><span>1</span><span>][</span><span>1</span><span><span>] </span><span>&gt;=</span><span> </span></span><span>c</span><span>[</span><span>2</span><span>][</span><span>2</span><span>] {</span></div></div><div><div><div>148</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>2.0</span><span> * </span><span>sqrt</span><span>(</span><span>1.0</span><span> + </span><span>c</span><span>[</span><span>1</span><span>][</span><span>1</span><span>] - </span><span>c</span><span>[</span><span>0</span><span>][</span><span>0</span><span>] - </span><span>c</span><span>[</span><span>2</span><span>][</span><span>2</span><span>]);</span></div></div><div><div><div>149</div></div><div><span>            </span><span>Quat</span><span> {</span></div></div><div><div><div>150</div></div><div><span>                </span><span>w</span><span>: (</span><span>c</span><span>[</span><span>0</span><span>][</span><span>2</span><span>] - </span><span>c</span><span>[</span><span>2</span><span>][</span><span>0</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>151</div></div><div><span>                </span><span>x</span><span>: (</span><span>c</span><span>[</span><span>0</span><span>][</span><span>1</span><span>] + </span><span>c</span><span>[</span><span>1</span><span>][</span><span>0</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>152</div></div><div><span>                </span><span>y</span><span>: </span><span>0.25</span><span> * </span><span>s</span><span>,</span></div></div><div><div><div>153</div></div><div><span>                </span><span>z</span><span>: (</span><span>c</span><span>[</span><span>1</span><span>][</span><span>2</span><span>] + </span><span>c</span><span>[</span><span>2</span><span>][</span><span>1</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>154</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>155</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>156</div></div><div><span>            </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>2.0</span><span> * </span><span>sqrt</span><span>(</span><span>1.0</span><span> + </span><span>c</span><span>[</span><span>2</span><span>][</span><span>2</span><span>] - </span><span>c</span><span>[</span><span>0</span><span>][</span><span>0</span><span>] - </span><span>c</span><span>[</span><span>1</span><span>][</span><span>1</span><span>]);</span></div></div><div><div><div>157</div></div><div><span>            </span><span>Quat</span><span> {</span></div></div><div><div><div>158</div></div><div><span>                </span><span>w</span><span>: (</span><span>c</span><span>[</span><span>1</span><span>][</span><span>0</span><span>] - </span><span>c</span><span>[</span><span>0</span><span>][</span><span>1</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>159</div></div><div><span>                </span><span>x</span><span>: (</span><span>c</span><span>[</span><span>0</span><span>][</span><span>2</span><span>] + </span><span>c</span><span>[</span><span>2</span><span>][</span><span>0</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>160</div></div><div><span>                </span><span>y</span><span>: (</span><span>c</span><span>[</span><span>1</span><span>][</span><span>2</span><span>] + </span><span>c</span><span>[</span><span>2</span><span>][</span><span>1</span><span>]) / </span><span>s</span><span>,</span></div></div><div><div><div>161</div></div><div><span>                </span><span>z</span><span>: </span><span>0.25</span><span> * </span><span>s</span><span>,</span></div></div><div><div><div>162</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>163</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>164</div></div><div><span>        </span><span>if</span><span> </span><span>q</span><span><span>.w </span><span>&lt;</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>165</div></div><div><span>            </span><span>Quat</span><span> {</span></div></div><div><div><div>166</div></div><div><span>                </span><span>w</span><span>: -</span><span>q</span><span>.w,</span></div></div><div><div><div>167</div></div><div><span>                </span><span>x</span><span>: -</span><span>q</span><span>.x,</span></div></div><div><div><div>168</div></div><div><span>                </span><span>y</span><span>: -</span><span>q</span><span>.y,</span></div></div><div><div><div>169</div></div><div><span>                </span><span>z</span><span>: -</span><span>q</span><span>.z,</span></div></div><div><div><div>170</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>171</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>172</div></div><div><span>            </span><span>q</span></div></div><div><div><div>173</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>174</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>175</div></div><div>
</div></div><div><div><div>176</div></div><div><span>    </span><span>/// (yaw, pitch, roll) in radians for C_wb = Rz(yaw) Ry(pitch) Rx(roll).</span></div></div><div><div><div>177</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>euler_zyx</span><span>(</span><span>self</span><span>) -&gt; (</span><span>Real</span><span>, </span><span>Real</span><span>, </span><span>Real</span><span>) {</span></div></div><div><div><div>178</div></div><div><span>        </span><span>let</span><span> </span><span>Quat</span><span> { </span><span>w</span><span>, </span><span>x</span><span>, </span><span>y</span><span>, </span><span>z</span><span><span> } </span><span>=</span><span> </span></span><span>self</span><span>;</span></div></div><div><div><div>179</div></div><div><span>        </span><span>let</span><span> </span><span>yaw</span><span><span> </span><span>=</span><span> </span></span><span>atan2</span><span>(</span><span>2.0</span><span> * (</span><span>w</span><span> * </span><span>z</span><span> + </span><span>x</span><span> * </span><span>y</span><span>), </span><span>1.0</span><span> - </span><span>2.0</span><span> * (</span><span>y</span><span> * </span><span>y</span><span> + </span><span>z</span><span> * </span><span>z</span><span>));</span></div></div><div><div><div>180</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> (</span></span><span>2.0</span><span> * (</span><span>w</span><span> * </span><span>y</span><span> - </span><span>z</span><span> * </span><span>x</span><span>)).</span><span>clamp</span><span>(-</span><span>1.0</span><span>, </span><span>1.0</span><span>);</span></div></div><div><div><div>181</div></div><div><span>        </span><span>let</span><span> </span><span>roll</span><span><span> </span><span>=</span><span> </span></span><span>atan2</span><span>(</span><span>2.0</span><span> * (</span><span>w</span><span> * </span><span>x</span><span> + </span><span>y</span><span> * </span><span>z</span><span>), </span><span>1.0</span><span> - </span><span>2.0</span><span> * (</span><span>x</span><span> * </span><span>x</span><span> + </span><span>y</span><span> * </span><span>y</span><span>));</span></div></div><div><div><div>182</div></div><div><span><span>        </span></span><span>(</span><span>yaw</span><span>, </span><span>asin</span><span>(</span><span>s</span><span>), </span><span>roll</span><span>)</span></div></div><div><div><div>183</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>184</div></div><div>
</div></div><div><div><div>185</div></div><div><span>    </span><span>/// Attitude error rotation vector log(est^-1 (x) truth), in the estimate's body frame.</span></div></div><div><div><div>186</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>att_error</span><span>(</span><span>est</span><span>: </span><span>Quat</span><span>, </span><span>truth</span><span>: </span><span>Quat</span><span>) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>187</div></div><div><span><span>        </span></span><span>(</span><span>est</span><span>.</span><span>conj</span><span>() * </span><span>truth</span><span>).</span><span>log</span><span>()</span></div></div><div><div><div>188</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>189</div></div><div><span>}</span></div></div><div><div><div>190</div></div><div>
</div></div><div><div><div>191</div></div><div><span>/// Cross-product matrix [v]_x.</span></div></div><div><div><div>192</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>skew</span><span>(</span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>&gt; {</span></div></div><div><div><div>193</div></div><div><span>    </span><span>Mat</span><span> {</span></div></div><div><div><div>194</div></div><div><span>        </span><span>m</span><span>: [[</span><span>0.0</span><span>, -</span><span>v</span><span>[</span><span>2</span><span>], </span><span>v</span><span>[</span><span>1</span><span>]], [</span><span>v</span><span>[</span><span>2</span><span>], </span><span>0.0</span><span>, -</span><span>v</span><span>[</span><span>0</span><span>]], [-</span><span>v</span><span>[</span><span>1</span><span>], </span><span>v</span><span>[</span><span>0</span><span>], </span><span>0.0</span><span>]],</span></div></div><div><div><div>195</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>196</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Mahony explicit complementary filter with an integral bias term.</span></div></div><div><div><div>2</div></div><div><span>//! q &lt;- q (x) exp((w_m - b + kp e) dt), b &lt;- b - ki e dt,</span></div></div><div><div><div>3</div></div><div><span>//! e = a_meas x a_pred + m_meas x m_pred (unit vectors in the body frame).</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>quat</span><span>::</span></span><span>Quat</span><span>;</span></div></div><div><div><div>6</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, cos, sin, sqrt};</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/// Reference magnetic field in NED: unit norm, dip 50 deg down, declination 0.</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>mag_w</span><span>() -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>dip</span><span><span> </span><span>=</span><span> </span></span><span>50.0</span><span><span> * </span><span>core</span><span>::</span></span><span>f64</span><span><span>::</span><span>consts</span><span>::</span></span><span>PI</span><span> </span><span>as</span><span> </span><span>Real</span><span> / </span><span>180.0</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>[</span><span>cos</span><span>(</span><span>dip</span><span>), </span><span>0.0</span><span>, </span><span>sin</span><span>(</span><span>dip</span><span>)]</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Mahony</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>kp</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>ki</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span> </span><span>q</span><span>: </span><span>Quat</span><span>,</span></div></div><div><div><div>18</div></div><div><span>    </span><span>pub</span><span> </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>fn</span><span> </span><span>cross</span><span>(</span><span>a</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>24</div></div><div><span>        </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>] - </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>25</div></div><div><span>        </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>] - </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>26</div></div><div><span>        </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>] - </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>fn</span><span> </span><span>unit</span><span>(</span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>31</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] * </span><span>v</span><span>[</span><span>0</span><span>] + </span><span>v</span><span>[</span><span>1</span><span>] * </span><span>v</span><span>[</span><span>1</span><span>] + </span><span>v</span><span>[</span><span>2</span><span>] * </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>[</span><span>v</span><span>[</span><span>0</span><span>] / </span><span>n</span><span>, </span><span>v</span><span>[</span><span>1</span><span>] / </span><span>n</span><span>, </span><span>v</span><span>[</span><span>2</span><span>] / </span><span>n</span><span>]</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>impl</span><span> </span><span>Mahony</span><span> {</span></div></div><div><div><div>36</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>kp</span><span>: </span><span>Real</span><span>, </span><span>ki</span><span>: </span><span>Real</span><span>, </span><span>q0</span><span>: </span><span>Quat</span><span>, </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>37</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>38</div></div><div><span>            </span><span>kp</span><span>,</span></div></div><div><div><div>39</div></div><div><span>            </span><span>ki</span><span>,</span></div></div><div><div><div>40</div></div><div><span>            </span><span>q</span><span>: </span><span>q0</span><span>,</span></div></div><div><div><div>41</div></div><div><span>            </span><span>b</span><span>: [</span><span>0.0</span><span>; </span><span>3</span><span>],</span></div></div><div><div><div>42</div></div><div><span>            </span><span>m_w</span><span>,</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>step</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>47</div></div><div><span>        </span><span>// Predicted body-frame directions: C^T v</span></div></div><div><div><div>48</div></div><div><span>        </span><span>let</span><span> </span><span>ct</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.q.</span><span>dcm</span><span>().</span><span>transpose</span><span>();</span></div></div><div><div><div>49</div></div><div><span>        </span><span>let</span><span> </span><span>pred</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span><span>]</span><span>|</span><span> -&gt; [</span></span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>50</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>o</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>3</span><span>];</span></div></div><div><div><div>51</div></div><div><span>            </span><span>for</span><span> (</span><span>i</span><span>, </span><span>oi</span><span>) </span><span>in</span><span> </span><span>o</span><span>.</span><span>iter_mut</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>52</div></div><div><span><span>                </span></span><span>*</span><span>oi</span><span><span> </span><span>=</span><span> </span></span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>0</span><span>] * </span><span>v</span><span>[</span><span>0</span><span>] + </span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>1</span><span>] * </span><span>v</span><span>[</span><span>1</span><span>] + </span><span>ct</span><span>.m[</span><span>i</span><span>][</span><span>2</span><span>] * </span><span>v</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>53</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>54</div></div><div><span>            </span><span>o</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>56</div></div><div><span>        </span><span>let</span><span> </span><span>ea</span><span><span> </span><span>=</span><span> </span></span><span>cross</span><span>(</span><span>unit</span><span>(</span><span>accel</span><span>), </span><span>pred</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span>, -</span><span>1.0</span><span>]));</span></div></div><div><div><div>57</div></div><div><span>        </span><span>let</span><span> </span><span>em</span><span><span> </span><span>=</span><span> </span></span><span>cross</span><span>(</span><span>unit</span><span>(</span><span>mag</span><span>), </span><span>pred</span><span>(</span><span>self</span><span>.m_w));</span></div></div><div><div><div>58</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>w</span><span><span> </span><span>=</span><span> [</span></span><span>0.0</span><span>; </span><span>3</span><span>];</span></div></div><div><div><div>59</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>60</div></div><div><span>            </span><span>let</span><span> </span><span>e</span><span><span> </span><span>=</span><span> </span></span><span>ea</span><span>[</span><span>i</span><span>] + </span><span>em</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>61</div></div><div><span>            </span><span>self</span><span>.b[</span><span>i</span><span><span>] </span><span>-=</span><span> </span></span><span>self</span><span>.ki * </span><span>e</span><span> * </span><span>dt</span><span>;</span></div></div><div><div><div>62</div></div><div><span>            </span><span>w</span><span>[</span><span>i</span><span><span>] </span><span>=</span><span> (</span></span><span>gyro</span><span>[</span><span>i</span><span>] - </span><span>self</span><span>.b[</span><span>i</span><span>] + </span><span>self</span><span>.kp * </span><span>e</span><span>) * </span><span>dt</span><span>;</span></div></div><div><div><div>63</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>64</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> (</span></span><span>self</span><span>.q * </span><span>Quat</span><span>::</span><span>exp</span><span>(</span><span>w</span><span>)).</span><span>normalized</span><span>();</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>66</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Attitude baselines: TRIAD snapshot and gyro-only integration.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::</span></span><span>Mat</span><span>;</span></div></div><div><div><div>4</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>quat</span><span>::</span></span><span>Quat</span><span>;</span></div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, sqrt};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>cross</span><span>(</span><span>a</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>b</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>9</div></div><div><span>        </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>] - </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>],</span></div></div><div><div><div>10</div></div><div><span>        </span><span>a</span><span>[</span><span>2</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>] - </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>2</span><span>],</span></div></div><div><div><div>11</div></div><div><span>        </span><span>a</span><span>[</span><span>0</span><span>] * </span><span>b</span><span>[</span><span>1</span><span>] - </span><span>a</span><span>[</span><span>1</span><span>] * </span><span>b</span><span>[</span><span>0</span><span>],</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>fn</span><span> </span><span>unit</span><span>(</span><span>v</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; [</span><span>Real</span><span>; </span><span>3</span><span>] {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>v</span><span>[</span><span>0</span><span>] * </span><span>v</span><span>[</span><span>0</span><span>] + </span><span>v</span><span>[</span><span>1</span><span>] * </span><span>v</span><span>[</span><span>1</span><span>] + </span><span>v</span><span>[</span><span>2</span><span>] * </span><span>v</span><span>[</span><span>2</span><span>]);</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>[</span><span>v</span><span>[</span><span>0</span><span>] / </span><span>n</span><span>, </span><span>v</span><span>[</span><span>1</span><span>] / </span><span>n</span><span>, </span><span>v</span><span>[</span><span>2</span><span>] / </span><span>n</span><span>]</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>/// Orthonormal triad with columns [v1, t2, v1 x t2].</span></div></div><div><div><div>21</div></div><div><span>fn</span><span> </span><span>frame</span><span>(</span><span>v1</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>v2</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Mat</span><span>&lt;</span><span>3</span><span>, </span><span>3</span><span>&gt; {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>t2</span><span><span> </span><span>=</span><span> </span></span><span>unit</span><span>(</span><span>cross</span><span>(</span><span>v1</span><span>, </span><span>v2</span><span>));</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>t3</span><span><span> </span><span>=</span><span> </span></span><span>cross</span><span>(</span><span>v1</span><span>, </span><span>t2</span><span>);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>f</span><span>.m[</span><span>i</span><span><span>] </span><span>=</span><span> [</span></span><span>v1</span><span>[</span><span>i</span><span>], </span><span>t2</span><span>[</span><span>i</span><span>], </span><span>t3</span><span>[</span><span>i</span><span>]];</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>    </span><span>f</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>/// Attitude q (body -&gt; world) from one accelerometer + magnetometer snapshot.</span></div></div><div><div><div>32</div></div><div><span>/// Specific force at rest points opposite to gravity, [0, 0, -1] in NED.</span></div></div><div><div><div>33</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>triad</span><span>(</span><span>accel</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>mag</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>m_w</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>]) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>frame</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span>, -</span><span>1.0</span><span>], </span><span>m_w</span><span>) * </span><span>frame</span><span>(</span><span>unit</span><span>(</span><span>accel</span><span>), </span><span>unit</span><span>(</span><span>mag</span><span>)).</span><span>transpose</span><span>();</span></div></div><div><div><div>35</div></div><div><span>    </span><span>Quat</span><span>::</span><span>from_dcm</span><span>(&amp;</span><span>c</span><span>)</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>/// One gyro-only integration step: q &lt;- normalize(q (x) exp(gyro dt)).</span></div></div><div><div><div>39</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>gyro_step</span><span>(</span><span>q</span><span>: </span><span>Quat</span><span>, </span><span>gyro</span><span>: [</span><span>Real</span><span>; </span><span>3</span><span>], </span><span>dt</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Quat</span><span> {</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>(</span><span>q</span><span> * </span><span>Quat</span><span>::</span><span>exp</span><span>([</span><span>gyro</span><span>[</span><span>0</span><span>] * </span><span>dt</span><span>, </span><span>gyro</span><span>[</span><span>1</span><span>] * </span><span>dt</span><span>, </span><span>gyro</span><span>[</span><span>2</span><span>] * </span><span>dt</span><span>])).</span><span>normalized</span><span>()</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>四元数恒等式自检<a href="#四元数恒等式自检"><span>#</span></a></h3><p>三个 app（<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/c/apps/ch04.c" target="_blank"><code>c/apps/ch04.c</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/cpp/apps/ch04.cpp" target="_blank"><code>cpp/apps/ch04.cpp</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/rust/src/bin/ch04.rs" target="_blank"><code>rust/src/bin/ch04.rs</code></a>）在读数据之前先做一轮自检，任何一项失败就以非零码退出。1000 个确定性的旋转向量（长度从接近 0 一直到 <span><span>0.99π0.99\pi</span><span><span><span></span><span>0.99</span><span>π</span></span></span></span>）：</p><ul>
<li><span><span>log⁡(exp⁡(v))=v\log(\exp(v)) = v</span><span><span><span></span><span>lo<span>g</span></span><span>(</span><span>exp</span><span>(</span><span>v</span><span>))</span><span></span><span>=</span><span></span></span><span><span></span><span>v</span></span></span></span>。</li>
<li><span><span>C(q)C(q)</span><span><span><span></span><span>C</span><span>(</span><span>q</span><span>)</span></span></span></span> 正交：<span><span>∣CCT−I∣|CC^T-I|</span><span><span><span></span><span>∣</span><span>C</span><span><span>C</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>I</span><span>∣</span></span></span></span> 和 <span><span>∣det⁡C−1∣|\det C-1|</span><span><span><span></span><span>∣</span><span></span><span>det</span><span></span><span>C</span><span></span><span>−</span><span></span></span><span><span></span><span>1∣</span></span></span></span> 都小于容差。</li>
<li><span><span>q⊗q∗=[1,0,0,0]q\otimes q^* = [1,0,0,0]</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>∗</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>1</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span>。</li>
</ul><p>容差 double 下是 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span>（第 8 篇的 float 构建里是 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span>）。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch04</span></div></div><div><div><div>2</div></div><div><span>quaternion identities hold to 1.78e-15</span></div></div><div><div><div>3</div></div><div><span>ch04 self-checks passed (worst 1.89e-15)</span></div></div><div><div><div>4</div></div><div><span>ok   gyro_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>5</div></div><div><span>ok   gyro_cpp.csv: max diff 3.469e-18 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   gyro_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   mahony_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div><span>ok   mahony_cpp.csv: max diff 4.441e-16 &lt;= 1.0e-09</span></div></div><div><div><div>9</div></div><div><span>ok   mahony_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   triad_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   triad_cpp.csv: max diff 3.331e-16 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>ok   triad_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>truth pitch range: [-77.3, 80.9] deg (gimbal lock is at +-90)</span></div></div><div><div><div>15</div></div><div><span>pitch = 90 deg: (yaw, roll) = (20, 10) deg and (57, 47) deg give the same rotation: max|C1 - C2| = 8.33e-17</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>RMSE over 120 s (deg), seed 1</span></div></div><div><div><div>18</div></div><div><span>method        yaw    pitch     roll    total</span></div></div><div><div><div>19</div></div><div><span>gyro       30.183   10.738   20.931   25.979</span></div></div><div><div><div>20</div></div><div><span>triad       5.312    1.523    2.190    5.702</span></div></div><div><div><div>21</div></div><div><span>mahony      4.390    1.489    2.250    4.881</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>Mahony total error by period (deg RMS)</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>calm  [20,60) s : 0.758</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>accel disturbance [60,70) s: 2.331</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>mag   disturbance [90,100) s: 14.851  (roll/pitch only: 8.437)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C 和 Rust 对 Python 的差是精确的 0：这三个滤波器只用 <code>sin</code>、<code>cos</code>、<code>atan2</code>、<code>sqrt</code> 和加减乘除，我这台机器上这几个函数的结果在 Python 和这两个语言里逐位相同（C++ 有 <span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级的差异，大概是编译器对表达式的合并方式不同）。换一个 libm 实现，这个 0 就不一定能保持，所以容差还是 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>。</p><p></p><figure><img src="/assets/images/kalman-filter/ch04-baselines-error.svg" alt="三个基线在偏航、俯仰、横滚上的误差。红带：线加速度扰动；紫带：磁干扰" /><figcaption>三个基线在偏航、俯仰、横滚上的误差。红带：线加速度扰动；紫带：磁干扰</figcaption></figure><p></p><p><strong>纯陀螺积分。</strong> 误差随时间单调变坏：10 秒时总误差 6.6 度，60 秒 23.6 度，120 秒 41.9 度，RMSE 25.979 度。零偏 <span><span>0.020.02</span><span><span><span></span><span>0.02</span></span></span></span> rad/s 的量级，一阶估算 120 秒会积出 2.4 rad（约 137 度）；实际只有 41.9 度，我猜是因为机体一直在转，零偏造成的误差在世界系里方向不断变化，一部分互相抵消了（这是推测，我没有单独验证）。图里偏航和横滚曲线上的尖峰是欧拉角折到 <span><span>±180∘\pm180^\circ</span><span><span><span></span><span>±</span><span>18</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span> 的缠绕，加上俯仰接近 80 度时的万向锁放大，不是旋转本身的突变。</p><p><strong>TRIAD。</strong> 没有漂移，没有记忆，每个样本的噪声直接进来，所以横滚和俯仰的”毛刺”最大。线加速度扰动段 <span><span>[60,70)[60,70)</span><span><span><span></span><span>[</span><span>60</span><span>,</span><span></span><span>70</span><span>)</span></span></span></span>，总误差 8.247 度，对比平静段 1.044 度：加速度计把线加速度当成了重力的偏转。磁干扰段，TRIAD 的偏航被拉偏，平均偏 <span><span>−17.63∘-17.63^\circ</span><span><span><span></span><span>−</span><span>17.6</span><span><span>3</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>。可以手算验证：磁场的水平分量从 <span><span>[cos⁡50∘,0]=[0.643,0][\cos50^\circ,0] = [0.643,0]</span><span><span><span></span><span>[</span><span>cos</span><span></span><span>5</span><span><span>0</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span><span>,</span><span></span><span>0</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0.643</span><span>,</span><span></span><span>0</span><span>]</span></span></span></span> 变成 <span><span>[0.943,0.3][0.943,0.3]</span><span><span><span></span><span>[</span><span>0.943</span><span>,</span><span></span><span>0.3</span><span>]</span></span></span></span>，方向偏转 <span><span>arctan⁡(0.3/0.943)=17.65∘\arctan(0.3/0.943) = 17.65^\circ</span><span><span><span></span><span>arctan</span><span>(</span><span>0.3/0.943</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>17.6</span><span><span>5</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>。</p><p><strong>Mahony。</strong> 平静段 0.758 度，线加速度段 2.331 度（比 TRIAD 的 8.247 度好得多：陀螺在这十秒里主导，加速度计的坏数据只通过 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 慢慢渗进来）。但磁干扰段的总误差是 14.851 度，其中横滚和俯仰部分是 8.437 度。TRIAD 在这一段，横滚和俯仰不受磁力计影响；Mahony 却会：它把磁干扰当作一个”姿态失配”，用 <span><span>mn×v^mm_n\times\hat v_m</span><span><span><span></span><span><span>m</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 去修正，而这个叉乘有一部分落在横滚和俯仰方向上。偏航被拉偏平均 <span><span>−11.36∘-11.36^\circ</span><span><span><span></span><span>−</span><span>11.3</span><span><span>6</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span>，比 TRIAD 的少，但没有消除。</p><p>整体 RMSE 4.881 度对比 TRIAD 的 5.702 度，好得有限，因为它们共同的最大误差来源是磁干扰那 10 秒。这件事需要的不是更好的增益，而是让滤波器理解”磁力计只能影响偏航”，并在传感器不可信的时候知道。这是第 5、6 篇的主题。</p></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>Mahony 滤波器已经能用，但它的两个增益 <span><span>kpk_p</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>p</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>kik_i</span><span><span><span></span><span><span>k</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是我选的，没有来由，也不会随工况变化。下一篇我们用第 1、2 篇的推导重新做一遍：状态是四元数加陀螺零偏共 7 维，过程模型是 <span><span>q⊗exp⁡(12(ωm−b)Δt)q\otimes\exp(\tfrac12(\omega_m-b)\Delta t)</span><span><span><span></span><span>q</span><span></span><span>⊗</span><span></span></span><span><span></span><span>exp</span><span>(</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span>)</span><span>Δ</span><span>t</span><span>)</span></span></span></span>，测量是加速度计和磁力计的方向向量，非线性，所以要线性化，这就是扩展卡尔曼滤波。它能不能赢过 Mahony，以及为什么 <span><span>4×44\times4</span><span><span><span></span><span>4</span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 的四元数协方差天生有一个方向的特征值是 0，是第 5 篇的内容。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-03-tuning-diagnostics/</id>
      <title type="text">调参与诊断：怎么知道滤波器没在骗你</title>
      <published>2026-10-14T02:00:00.000Z</published>
      <updated>2026-10-14T02:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-03-tuning-diagnostics/"/>
      <summary type="text">卡尔曼滤波系列第 3 篇：用 NEES、NIS、新息自相关检验滤波器是否自洽，用可观性矩阵判断哪些状态估不出来，用 Riccati 迭代求稳态增益，并比较协方差更新的 Joseph 形式与短形式。Python、C、C++、Rust 同步实现。</summary>
      <content type="html"><![CDATA[<p>上一篇的角度加零偏滤波器，我把过程噪声 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 缩小到 <span><span>0.010.01</span><span><span><span></span><span>0.01</span></span></span></span> 倍重新跑了一遍。角度 RMSE 从 0.329 度变成 0.657 度，慢了一倍，但它没有崩溃，没有报错，曲线看上去还是一条像样的正弦。真正出卖这个滤波器的是另一个数：50 次蒙特卡洛平均下来的 NEES（归一化估计误差平方）应该是 2，调对的滤波器是 1.993，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 缩小的那个是 95.667。它在说”我的误差只有 <span><span>±1\pm 1</span><span><span><span></span><span>±</span><span>1</span></span></span></span> 个标准差”，实际误差是它自己报告的 7 倍。</p>
<p>这一篇讲怎么在滤波器”看上去能用”的时候识破它。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch03</code> 通过，<code>out/ch03/</code> 下有 8 组输出（五种调参、稳态增益、两种协方差更新形式下的病态例子），<code>figures/ch03/</code> 下有四张图；<code>kl_kf_update_short</code> / <code>update_short</code> 和稳态增益函数在 C、C++、Rust 中各一份；一份 <code>python/kl/metrics.py</code>（RMSE、NEES、NIS、<span><span>χ2\chi^2</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 界、自相关）。
前置：<a href="/posts/kalman-filter-02-multivariate/">第 2 篇</a>。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>新息序列：零均值、白、协方差已知<a href="#新息序列零均值白协方差已知"><span>#</span></a></h3><p>滤波器每一步的新息（innovation）是 <span><span>yk=zk−Hx^k−y_k = z_k - H\hat x_k^-</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>H</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，它的协方差是 <span><span>Sk=HPk−HT+RS_k = HP_k^-H^T+R</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span></span></span></span>，这是上一篇已经算过的。如果模型是对的，新息有三条性质：</p><ol>
<li>零均值：<span><span>E[yk]=0E[y_k] = 0</span><span><span><span></span><span>E</span><span>[</span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</li>
<li>协方差等于滤波器自己报告的 <span><span>SkS_k</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。</li>
<li>不同时刻不相关：<span><span>E[ykyjT]=0E[y_ky_j^T] = 0</span><span><span><span></span><span>E</span><span>[</span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>j≠kj\neq k</span><span><span><span></span><span>j</span><span></span><span><span><span><span><span><span></span><span><span><span></span></span></span><span></span></span></span></span></span><span></span><span>=</span></span><span></span></span><span><span></span><span>k</span></span></span></span>（“白”）。</li>
</ol><p>第三条的理由：<span><span>yk=Hek−+vky_k = He_k^- + v_k</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，其中 <span><span>ek−=xk−x^k−e_k^- = x_k-\hat x_k^-</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是先验误差，<span><span>vkv_k</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是测量噪声。最优估计满足正交性原理：估计误差和过去所有测量正交。而 <span><span>yjy_j</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（<span><span>j&lt;kj&lt;k</span><span><span><span></span><span>j</span><span></span><span>&lt;</span><span></span></span><span><span></span><span>k</span></span></span></span>）是 <span><span>zjz_j</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与更早测量的线性组合，所以 <span><span>ek−e_k^-</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 和 <span><span>yjy_j</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 正交；<span><span>vkv_k</span><span><span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 与过去独立。于是 <span><span>E[ykyjT]=HE[ek−yjT]+E[vkyjT]=0E[y_ky_j^T] = HE[e_k^-y_j^T] + E[v_ky_j^T] = 0</span><span><span><span></span><span>E</span><span>[</span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>E</span><span>[</span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span></span><span>+</span><span></span></span><span><span></span><span>E</span><span>[</span><span><span>v</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>j</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。直觉：新息是”新信息”，如果它还能被过去的新息预测，说明滤波器没有榨干过去的信息。</p><p>反过来，这三条性质正好是可以拿来检验的东西，因为新息是你手里真正有的量，不需要真值。</p></section><section><h3>NIS 与 NEES<a href="#nis-与-nees"><span>#</span></a></h3><p>把新息按它的协方差归一化：</p><span><span><span>ϵν=ykTSk−1yk\epsilon_\nu = y_k^TS_k^{-1}y_k</span><span><span><span></span><span><span>ϵ</span><span><span><span><span><span><span></span><span><span>ν</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p><span><span>yk∼N(0,Sk)y_k\sim\mathcal N(0,S_k)</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 时，<span><span>Sk−1/2ykS_k^{-1/2}y_k</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span><span>−</span><span>1/2</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span> 维标准正态，所以 <span><span>ϵν\epsilon_\nu</span><span><span><span></span><span><span>ϵ</span><span><span><span><span><span><span></span><span><span>ν</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 服从自由度为 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>（测量维数）的 <span><span>χ2\chi^2</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 分布，均值 <span><span>mm</span><span><span><span></span><span>m</span></span></span></span>，方差 <span><span>2m2m</span><span><span><span></span><span>2</span><span>m</span></span></span></span>。这叫 NIS（normalized innovation squared）。</p><p>同理，如果有真值 <span><span>xkx_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（仿真里有，硬件上通常没有），估计误差 <span><span>ek=xk−x^ke_k = x_k-\hat x_k</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 应该服从 <span><span>N(0,Pk)\mathcal N(0,P_k)</span><span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，</p><span><span><span>ϵx=ekTPk−1ek∼χn2\epsilon_x = e_k^TP_k^{-1}e_k\sim\chi^2_n</span><span><span><span></span><span><span>ϵ</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>T</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>这叫 NEES（normalized estimation error squared），<span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 是状态维数，均值 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>。</p><p><strong>NEES 能看见 NIS 看不见的东西，但它需要真值。</strong> 这是仿真的最大价值：我们可以在仿真里检验滤波器的 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 说的话是不是真的，然后把调好的参数带到没有真值的硬件上。</p></section><section><h3>蒙特卡洛界<a href="#蒙特卡洛界"><span>#</span></a></h3><p>单个 <span><span>ϵx\epsilon_x</span><span><span><span></span><span><span>ϵ</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 抖动很大。取 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 次独立仿真在同一时刻的平均 <span><span>ϵˉx\bar\epsilon_x</span><span><span><span></span><span><span><span><span><span><span><span></span><span>ϵ</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>MϵˉxM\bar\epsilon_x</span><span><span><span></span><span>M</span><span><span><span><span><span><span><span></span><span>ϵ</span></span><span><span></span><span><span>ˉ</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是 <span><span>MM</span><span><span><span></span><span>M</span></span></span></span> 个独立 <span><span>χn2\chi^2_n</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 之和，服从 <span><span>χMn2\chi^2_{Mn}</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>M</span><span>n</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。所以平均值的 95% 双侧界是</p><span><span><span>[χMn2(0.025)M, χMn2(0.975)M]\Big[\frac{\chi^2_{Mn}(0.025)}{M},\ \frac{\chi^2_{Mn}(0.975)}{M}\Big]</span><span><span><span></span><span><span>[</span></span><span><span></span><span><span><span><span><span><span></span><span><span>M</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>M</span><span>n</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>0.025</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span> </span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>M</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>M</span><span>n</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>0.975</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>]</span></span></span></span></span></span><p>这里 <span><span>χMn2(p)\chi^2_{Mn}(p)</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>M</span><span>n</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>p</span><span>)</span></span></span></span> 是 <span><span>χMn2\chi^2_{Mn}</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>M</span><span>n</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 分布的 <span><span>pp</span><span><span><span></span><span>p</span></span></span></span> 分位数。我们的情形是 <span><span>M=50M=50</span><span><span><span></span><span>M</span><span></span><span>=</span><span></span></span><span><span></span><span>50</span></span></span></span>，<span><span>n=2n=2</span><span><span><span></span><span>n</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span>（角度和零偏），界是 <span><span>[1.484,2.591][1.484, 2.591]</span><span><span><span></span><span>[</span><span>1.484</span><span>,</span><span></span><span>2.591</span><span>]</span></span></span></span>。</p><p>一个微妙之处：就算滤波器完美，每一步的平均 NEES 也有 5% 的概率落在界外。而且相邻时间步的 NEES 高度相关，所以”落在界外”成段出现。所以检验的判据不是”一步都不能出界”，而是”绝大多数步在界内，且平均值接近 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>“。我用的判据是至少 90% 的步在界内。</p><p>单次运行也可以检验 NIS：<span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 步的 NIS 之和服从 <span><span>χNm2\chi^2_{Nm}</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span><span>N</span><span>m</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（假设步间独立，这是新息的白性给的）。但是它没有 NEES 灵敏，下面的结果里会看到。</p></section><section><h3>新息自相关<a href="#新息自相关"><span>#</span></a></h3><p>如果新息是白的，它的归一化自相关</p><span><span><span>ρℓ=∑k(yk−yˉ)(yk+ℓ−yˉ)∑k(yk−yˉ)2\rho_\ell = \frac{\sum_k (y_k-\bar y)(y_{k+\ell}-\bar y)}{\sum_k (y_k-\bar y)^2}</span><span><span><span></span><span><span>ρ</span><span><span><span><span><span><span></span><span><span>ℓ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>∑</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span><span><span><span><span></span><span>y</span></span><span><span></span><span><span>ˉ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>∑</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span><span><span><span><span></span><span>y</span></span><span><span></span><span><span>ˉ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span><span>(</span><span><span>y</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>ℓ</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span><span><span><span><span><span><span></span><span>y</span></span><span><span></span><span><span>ˉ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>在 <span><span>ℓ≥1\ell\ge1</span><span><span><span></span><span>ℓ</span><span></span><span>≥</span><span></span></span><span><span></span><span>1</span></span></span></span> 时应该接近 0，样本足够多时每个 <span><span>ρℓ\rho_\ell</span><span><span><span></span><span><span>ρ</span><span><span><span><span><span><span></span><span><span>ℓ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 近似服从 <span><span>N(0,1/N)\mathcal N(0,1/N)</span><span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span>1/</span><span>N</span><span>)</span></span></span></span>，所以 95% 的界是 <span><span>±1.96/N\pm1.96/\sqrt N</span><span><span><span></span><span>±</span><span>1.96/</span><span><span><span><span><span><span></span><span>N</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。有色的新息（比如 <span><span>ρ1\rho_1</span><span><span><span></span><span><span>ρ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 很大）说明模型漏掉了某种动态：滤波器还在”偷懒”，没有把可预测的部分用完。</p></section><section><h3>可观性<a href="#可观性"><span>#</span></a></h3><p>线性定常系统 <span><span>xk+1=Fxkx_{k+1} = Fx_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>zk=Hxkz_k = Hx_k</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的可观性矩阵是</p><span><span><span>O=[HHF⋮HFn−1]\mathcal O = \begin{bmatrix}H\\HF\\\vdots\\HF^{n-1}\end{bmatrix}</span><span><span><span></span><span>O</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span><span><span><span><span><span><span></span><span><span>H</span></span></span><span><span></span><span><span>H</span><span>F</span></span></span><span><span></span><span><span><span>⋮</span><span></span></span></span></span><span><span></span><span><span>H</span><span><span>F</span><span><span><span><span><span><span></span><span><span><span>n</span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span><span><span><span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>rank⁡O=n\operatorname{rank}\mathcal O = n</span><span><span><span></span><span><span>rank</span></span><span></span><span>O</span><span></span><span>=</span><span></span></span><span><span></span><span>n</span></span></span></span> 当且仅当初始状态可以由有限个测量唯一确定。理由：<span><span>zk=HFkx0z_k = HF^kx_0</span><span><span><span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，把前 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span> 个测量叠起来就是 <span><span>Ox0\mathcal Ox_0</span><span><span><span></span><span>O</span><span><span>x</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，凯莱-哈密顿定理保证再多的测量也不会提供新的线性无关方程。如果秩小于 <span><span>nn</span><span><span><span></span><span>n</span></span></span></span>，有一个方向的初始状态对测量没有任何影响，滤波器对那个方向的不确定度永远不会收缩，而且因为过程噪声还在往里加，它会增长。</p><p>匀速模型 <span><span>x=[px,py,vx,vy]Tx=[p_x,p_y,v_x,v_y]^T</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span><span>p</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，如果测的是位置，<span><span>O\mathcal O</span><span><span><span></span><span>O</span></span></span></span> 满秩 4。如果测的是速度：<span><span>H=[0 I]H = [0\ I]</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0</span><span> </span><span>I</span><span>]</span></span></span></span>，<span><span>HFk=[0 I]HF^k = [0\ I]</span><span><span><span></span><span>H</span><span><span>F</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0</span><span> </span><span>I</span><span>]</span></span></span></span> 每一块都一样，<span><span>O\mathcal O</span><span><span><span></span><span>O</span></span></span></span> 的秩只有 2。位置永远估不出来（速度告诉你位置变化多快，不告诉你位置在哪），而且位置的方差随时间线性增长。</p></section><section><h3>稳态增益<a href="#稳态增益"><span>#</span></a></h3><p>当 <span><span>F,H,Q,RF,H,Q,R</span><span><span><span></span><span>F</span><span>,</span><span></span><span>H</span><span>,</span><span></span><span>Q</span><span>,</span><span></span><span>R</span></span></span></span> 都是常数时，<span><span>Pk−P^-_k</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的递推（Riccati 递推）</p><span><span><span>Pk+1−=F(Pk−−KkHPk−)FT+Q,Kk=Pk−HT(HPk−HT+R)−1P^-_{k+1} = F\big(P^-_k - K_kHP^-_k\big)F^T + Q,\qquad K_k = P_k^-H^T(HP_k^-H^T+R)^{-1}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span><span>(</span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span></span><span><span>F</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Q</span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>(</span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span><span><span>)</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><p>不依赖任何测量值，对可检测的系统会收敛到一个定值 <span><span>P∞−P^-_\infty</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（离散代数 Riccati 方程，DARE 的解），对应的增益 <span><span>K∞K_\infty</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 也是常数。我的做法是直接迭代这个递推直到 <span><span>max⁡∣ΔP∣≤tol⋅max⁡∣P∣\max|\Delta P|\le\text{tol}\cdot\max|P|</span><span><span><span></span><span>max</span><span></span><span>∣Δ</span><span>P</span><span>∣</span><span></span><span>≤</span><span></span></span><span><span></span><span><span>tol</span></span><span></span><span>⋅</span><span></span></span><span><span></span><span>max</span><span></span><span>∣</span><span>P</span><span>∣</span></span></span></span>。这个增益可以离线算好，运行时把滤波器退化成 <span><span>x^=x^−+K∞(z−Hx^−)\hat x = \hat x^- + K_\infty(z-H\hat x^-)</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>H</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，不再维护协方差，每步少几十次乘法。代价是丢掉了滤波开始时的瞬态。</p></section><section><h3>数值：Joseph 形式与短形式<a href="#数值joseph-形式与短形式"><span>#</span></a></h3><p>上一篇说 Joseph 形式在任何 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 下都保持对称半正定，短形式 <span><span>(I−KH)P−(I-KH)P^-</span><span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 不保证。这是个理论上的区别。我想亲眼看它在什么情况下变成现实问题，于是构造了一个病态例子：<span><span>P0=106IP_0 = 10^6I</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span><span>I</span></span></span></span>，测量噪声方差 <span><span>r=10−8r = 10^{-8}</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>（标准差 <span><span>10−410^{-4}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>4</span></span></span></span></span></span></span></span></span></span></span></span> rad）。第一次更新时 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 里的量级差了 <span><span>101410^{14}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>：<span><span>10610^6</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 要被收缩到 <span><span>10−810^{-8}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>8</span></span></span></span></span></span></span></span></span></span></span></span>，浮点数的 16 位有效数字刚好够，几乎没有余量。</p></section></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><section><h3>指标<a href="#指标"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Consistency metrics: RMSE, NEES, NIS, chi-square bounds, autocorrelation."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div><span>from</span><span> scipy.linalg </span><span>import</span><span> cho_factor, cho_solve</span></div></div><div><div><div>5</div></div><div><span>from</span><span> scipy.stats </span><span>import</span><span> chi2</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>def</span><span> </span><span>rmse</span><span>(</span><span>e</span><span>):</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>e </span><span>=</span><span> np.</span><span>asarray</span><span>(e)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(np.</span><span>sqrt</span><span>(np.</span><span>mean</span><span>(e</span><span>**</span></span><span>2</span><span>)))</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>def</span><span> </span><span>nees</span><span>(</span><span>e</span><span>,</span><span><span> </span><span>P</span></span><span>):</span></div></div><div><div><div>14</div></div><div><span>    </span><span>"""e' P^-1 e for one step."""</span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(e </span><span>@</span><span> </span><span>cho_solve</span><span>(</span><span>cho_factor</span><span>(P, </span></span><span>lower</span><span>=</span><span>True</span><span>), e))</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>def</span><span> </span><span>nis</span><span>(</span><span>y</span><span>,</span><span><span> </span><span>S</span></span><span>):</span></div></div><div><div><div>19</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(y </span><span>@</span><span> </span><span>cho_solve</span><span>(</span><span>cho_factor</span><span>(np.</span><span>atleast_2d</span><span>(S), </span></span><span>lower</span><span>=</span><span>True</span><span>), y))</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>def</span><span> </span><span>chi2_bounds</span><span>(</span><span>dof</span><span>,</span><span><span> </span><span>runs</span></span><span>,</span><span><span> </span><span>alpha</span></span><span>=</span><span>0.05</span><span>):</span></div></div><div><div><div>23</div></div><div><span>    </span><span>"""Two-sided (1-alpha) bounds for the mean of `runs` independent chi2(dof) values."""</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>lo </span><span>=</span><span> chi2.</span><span>ppf</span><span>(alpha </span><span>/</span><span> </span><span>2</span><span><span>, runs </span><span>*</span><span> dof) </span><span>/</span><span> runs</span></span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>hi </span><span>=</span><span> chi2.</span><span>ppf</span><span>(</span><span>1</span><span><span> </span><span>-</span><span> alpha </span><span>/</span><span> </span></span><span>2</span><span><span>, runs </span><span>*</span><span> dof) </span><span>/</span><span> runs</span></span></div></div><div><div><div>26</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span>(lo), </span><span>float</span><span>(hi)</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>def</span><span> </span><span>autocorr</span><span>(</span><span>x</span><span>,</span><span><span> </span><span>max_lag</span></span><span>):</span></div></div><div><div><div>30</div></div><div><span>    </span><span>"""Normalized autocorrelation of a 1-D sequence for lags 0..max_lag."""</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>asarray</span><span>(x, </span><span>dtype</span><span>=</span><span>float</span><span><span>) </span><span>-</span><span> np.</span><span>mean</span><span>(x)</span></span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>var </span><span>=</span><span> np.</span><span>dot</span><span>(x, x)</span></div></div><div><div><div>33</div></div><div><span>    </span><span>return</span><span><span> np.</span><span>array</span><span>([np.</span><span>dot</span><span>(x[: </span></span><span>len</span><span><span>(x) </span><span>-</span><span> k], x[k:]) </span><span>/</span><span> var </span></span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span><span>(max_lag </span><span>+</span><span> </span></span><span>1</span><span>)])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>chi2_bounds</code> 就是上面的公式，<code>scipy.stats.chi2.ppf</code> 是分位数函数。<code>nees</code> 和 <code>nis</code> 一样用 Cholesky 求解而不是求逆。<code>autocorr</code> 对每个滞后 <span><span>ℓ\ell</span><span><span><span></span><span>ℓ</span></span></span></span> 做一次点积，<span><span>O(Nℓmax)O(N\ell_{max})</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span><span>ℓ</span><span><span><span><span><span><span></span><span><span><span>ma</span><span>x</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，够用。</p></section><section><h3>稳态增益与短形式<a href="#稳态增益与短形式"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># ---- chapter 3 additions (append-only) ----</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>def</span><span> </span><span>_update_short</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>z</span></span><span>):</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"""Textbook short form P = (I - K H) P-: cheaper, but not symmetry/PSD safe."""</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>self</span><span><span>.</span><span>update</span><span>(z, </span></span><span>joseph</span><span>=</span><span>False</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>KalmanFilter.update_short </span><span>=</span><span> _update_short</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>def</span><span> </span><span>steady_state_gain</span><span>(</span><span>F</span><span>,</span><span><span> </span><span>H</span></span><span>,</span><span><span> </span><span>Q</span></span><span>,</span><span><span> </span><span>R</span></span><span>,</span><span><span> </span><span>tol</span></span><span>=</span><span>1e-12</span><span>,</span><span><span> </span><span>max_iter</span></span><span>=</span><span>100000</span><span>):</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"""Iterate the Riccati recursion on the prior covariance until</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>max|dP| &lt;= tol * max|P|. Returns (K, P_prior) at convergence."""</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>F, H, Q, R </span><span>=</span><span> (np.</span><span>asarray</span><span>(a, </span><span>dtype</span><span>=</span><span>float</span><span>) </span><span>for</span><span> a </span><span>in</span><span> (F, H, Q, R))</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>P </span><span>=</span><span> Q.</span><span>copy</span><span>()</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(max_iter):</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>S </span><span>=</span><span> H </span><span>@</span><span> P </span><span>@</span><span> H.T </span><span>+</span><span> R</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>K </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(S, H </span><span>@</span><span> P).T</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>Pn </span><span>=</span><span> F </span><span>@</span><span> (P </span><span>-</span><span> K </span><span>@</span><span> H </span><span>@</span><span> P) </span><span>@</span><span> F.T </span><span>+</span><span> Q</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>done </span><span>=</span><span> np.</span><span>max</span><span>(np.</span><span>abs</span><span>(Pn </span><span>-</span><span> P)) </span><span>&lt;=</span><span> tol </span><span>*</span><span> np.</span><span>max</span><span>(np.</span><span>abs</span><span>(Pn))</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>P </span><span>=</span><span> Pn</span></div></div><div><div><div>22</div></div><div><span>        </span><span>if</span><span> done:</span></div></div><div><div><div>23</div></div><div><span>            </span><span>break</span></div></div><div><div><div>24</div></div><div><span>    </span><span>else</span><span>:</span></div></div><div><div><div>25</div></div><div><span>        </span><span>raise</span><span> </span><span>RuntimeError</span><span>(</span><span>"steady_state_gain did not converge"</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>S </span><span>=</span><span> H </span><span>@</span><span> P </span><span>@</span><span> H.T </span><span>+</span><span> R</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>K </span><span>=</span><span> np.linalg.</span><span>solve</span><span>(S, H </span><span>@</span><span> P).T</span></div></div><div><div><div>28</div></div><div><span>    </span><span>return</span><span> K, P</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>update_short</code> 只是 <code>update(z, joseph=False)</code> 的别名，因为上一篇的 <code>update</code> 里已经留了这个分支：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span> joseph:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>self</span><span><span>.P </span><span>=</span><span> A </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> A.T </span><span>+</span><span> K </span><span>@</span><span> </span></span><span>self</span><span><span>.R </span><span>@</span><span> K.T</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>self</span><span><span>.P </span><span>=</span><span> </span></span><span>0.5</span><span><span> </span><span>*</span><span> (</span></span><span>self</span><span><span>.P </span><span>+</span><span> </span></span><span>self</span><span>.P.T)</span></div></div><div><div><div>4</div></div><div><span>else</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>self</span><span><span>.P </span><span>=</span><span> A </span><span>@</span><span> </span></span><span>self</span><span>.P</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>steady_state_gain</code> 从 <span><span>P=QP=Q</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>Q</span></span></span></span> 起步迭代。它在 <code>np.linalg.solve</code> 里解线性方程，不是 Cholesky，因为离线计算不需要那么讲究。</p></section><section><h3>五种配置，五十次蒙特卡洛<a href="#五种配置五十次蒙特卡洛"><span>#</span></a></h3><p>配置是五组”滤波器相信的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span>“（数据始终用真实噪声生成）：正确、<span><span>Q×0.01Q\times0.01</span><span><span><span></span><span>Q</span><span></span><span>×</span><span></span></span><span><span></span><span>0.01</span></span></span></span>、<span><span>Q×100Q\times100</span><span><span><span></span><span>Q</span><span></span><span>×</span><span></span></span><span><span></span><span>100</span></span></span></span>、<span><span>R×0.1R\times0.1</span><span><span><span></span><span>R</span><span></span><span>×</span><span></span></span><span><span></span><span>0.1</span></span></span></span>、<span><span>R×10R\times10</span><span><span><span></span><span>R</span><span></span><span>×</span><span></span></span><span><span></span><span>10</span></span></span></span>。每次运行的代码和第 2 篇一样，只是把 <code>Q</code>、<code>R</code> 乘了系数。蒙特卡洛一次运行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>mc_one</span><span>(</span><span>args</span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>seed, qs, rs </span><span>=</span><span> args[:</span><span>3</span><span>]</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>gyro_avg </span><span>=</span><span> args[</span><span>3</span><span>] </span><span>if</span><span> </span><span>len</span><span><span>(args) </span><span>&gt;</span><span> </span></span><span>3</span><span> </span><span>else</span><span> </span><span>True</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> </span><span>simulate_tilt_bias</span><span>(seed, </span><span>write</span><span>=</span><span>False</span><span>, </span><span>gyro_avg</span><span><span>=</span><span>gyro_avg)</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>o </span><span>=</span><span> </span><span>run_tilt</span><span>(d, qs, rs)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>e0, e1 </span><span>=</span><span> o[</span><span>"theta"</span><span><span>] </span><span>-</span><span> d[</span></span><span>"theta"</span><span>], o[</span><span>"bias"</span><span><span>] </span><span>-</span><span> d[</span></span><span>"bias"</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>det </span><span>=</span><span> o[</span><span>"P00"</span><span><span>] </span><span>*</span><span> o[</span></span><span>"P11"</span><span><span>] </span><span>-</span><span> o[</span></span><span>"P01"</span><span><span>] </span><span>**</span><span> </span></span><span>2</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>ne </span><span>=</span><span> (o[</span><span>"P11"</span><span><span>] </span><span>*</span><span> e0</span><span>**</span></span><span>2</span><span><span> </span><span>-</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> o[</span></span><span>"P01"</span><span><span>] </span><span>*</span><span> e0 </span><span>*</span><span> e1 </span><span>+</span><span> o[</span></span><span>"P00"</span><span><span>] </span><span>*</span><span> e1</span><span>**</span></span><span>2</span><span><span>) </span><span>/</span><span> det</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span><span> e0</span><span>**</span></span><span>2</span><span>, ne, o[</span><span>"nis"</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>NEES 用 <span><span>2×22\times2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 的解析逆直接写成向量化形式（<code>det</code> 在分母），因为这里一次要处理 6000 步。仿真种子 1 到 50，用 <code>ProcessPoolExecutor</code> 并行跑。</p><p><strong>先说一个我踩的坑。</strong> 我第一次跑这个实验，调对的滤波器平均 NEES 是 2.233，只有 86.1% 的步落在 95% 界内，没有达到我设的 90%。滤波器没问题，我查了很久。原因是仿真器：第 1、2 篇里，我让陀螺仪读数等于角速度在采样时刻的瞬时值 <span><span>ω(tk)\omega(t_k)</span><span><span><span></span><span>ω</span><span>(</span><span><span>t</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，而滤波器的离散模型是 <span><span>θk=θk−1+Δt⋅ωm,k\theta_k = \theta_{k-1}+\Delta t\cdot\omega_{m,k}</span><span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span><span>t</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>m</span><span>,</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，隐含的是”整个区间内的平均角速度”。差别是 <span><span>12Δt2ω˙≈6×10−5\tfrac12\Delta t^2\dot\omega\approx 6\times10^{-5}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span><span><span><span><span><span></span><span>ω</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span> rad 每步，听上去很小，但它是有方向的、随 <span><span>θ˙\dot\theta</span><span><span><span></span><span><span><span><span><span><span></span><span>θ</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span></span></span></span> 摆动的系统误差，零偏状态试图解释它，结果是估计误差系统性地比 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 宣称的大一点点：NEES 偏高 10% 到 15%。把陀螺读数改成区间平均速率 <span><span>(θk−θk−1)/Δt(\theta_k-\theta_{k-1})/\Delta t</span><span><span><span></span><span>(</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/Δ</span><span>t</span></span></span></span> 之后（真实的积分式陀螺输出的就是这个），NEES 回到 1.993。这个修改已经在第 2 篇的仿真器里（<code>gyro_avg=True</code>），旧版本的 86.1% 我保留在 <code>make ch03</code> 的输出里，当作一次对照。</p><p>这是我想强调的一点：NEES 不仅检验滤波器，也检验你的仿真器和滤波器模型是否”说同一种语言”。一个 10% 的偏差，RMSE 上几乎看不出来，NEES 一下就抓住了。</p></section><section><h3>病态例子<a href="#病态例子"><span>#</span></a></h3><p>病态实验在 Python 里还有一份 float32 版本，因为 float64 下我找不到差别（下面会看到）。它用 numpy 的 <code>float32</code> 数组写了一遍标量测量更新：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>run_dtype</span><span>(</span><span>d</span><span>,</span><span><span> </span><span>dtype</span></span><span>,</span><span><span> </span><span>joseph</span></span><span>):</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"""Scalar-measurement update of the ill-conditioned case in an arbitrary numpy dtype."""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>F, B, H, Q, R </span><span>=</span><span> (a.</span><span>astype</span><span>(dtype) </span><span>for</span><span> a </span><span>in</span><span><span> </span><span>tilt_bias_model</span><span>(</span><span>DT</span><span>, </span><span>SG</span><span>, </span><span>SBW</span><span>, </span><span>ILL_SV</span><span>))</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>zeros</span><span>(</span><span>2</span><span><span>, dtype); P </span><span>=</span><span> </span><span>ILL_P0</span><span>.</span><span>astype</span><span>(dtype); I </span><span>=</span><span> np.</span><span>eye</span><span>(</span></span><span>2</span><span>, </span><span>dtype</span><span><span>=</span><span>dtype)</span></span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(d[</span><span>"t"</span><span><span>]); me </span><span>=</span><span> np.</span><span>empty</span><span>(n); asym </span><span>=</span><span> np.</span><span>empty</span><span>(n); nonpd </span><span>=</span><span> </span></span><span>0</span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> F </span><span>@</span><span> x </span><span>+</span><span> B[:, </span><span>0</span><span><span>] </span><span>*</span><span> </span><span>dtype</span><span>(d[</span></span><span>"omega_m"</span><span>][i])</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>P </span><span>=</span><span> F </span><span>@</span><span> P </span><span>@</span><span> F.T </span><span>+</span><span> Q</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> </span><span>dtype</span><span>(d[</span><span>"z"</span><span><span>][i]) </span><span>-</span><span> H </span><span>@</span><span> x</span></span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>S </span><span>=</span><span> (H </span><span>@</span><span> P </span><span>@</span><span> H.T </span><span>+</span><span> R)[</span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>K </span><span>=</span><span> (P </span><span>@</span><span> H.T)[:, </span><span>0</span><span><span>] </span><span>/</span><span> S</span></span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> x </span><span>+</span><span> K </span><span>*</span><span> y[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> I </span><span>-</span><span> np.</span><span>outer</span><span>(K, H[</span><span>0</span><span>])</span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span> joseph:</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>P </span><span>=</span><span> A </span><span>@</span><span> P </span><span>@</span><span> A.T </span><span>+</span><span> np.</span><span>outer</span><span>(K, K) </span><span>*</span><span> R[</span><span>0</span><span>, </span><span>0</span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>P </span><span>=</span><span> </span><span>dtype</span><span>(</span><span>0.5</span><span><span>) </span><span>*</span><span> (P </span><span>+</span><span> P.T)</span></span></div></div><div><div><div>17</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>P </span><span>=</span><span> A </span><span>@</span><span> P</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>me[i] </span><span>=</span><span> </span><span>min_eig_sym</span><span>(P[</span><span>0</span><span>,</span><span>0</span><span>], P[</span><span>0</span><span>,</span><span>1</span><span>], P[</span><span>1</span><span>,</span><span>0</span><span>], P[</span><span>1</span><span>,</span><span>1</span><span><span>]); asym[i] </span><span>=</span><span> </span></span><span>abs</span><span>(P[</span><span>0</span><span>,</span><span>1</span><span><span>]</span><span>-</span><span>P[</span></span><span>1</span><span>,</span><span>0</span><span>])</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span> me, asym</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>这一篇给三种语言各加两样东西：协方差短形式更新，和稳态增益。第 2 篇的 <code>update</code> 已经把”算 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span>、<span><span>SS</span><span><span><span></span><span>S</span></span></span></span>、<span><span>KK</span><span><span><span></span><span>K</span></span></span></span>、<span><span>x+=Kyx \mathrel{+}= Ky</span><span><span><span></span><span>x</span><span></span><span><span>+</span></span></span><span><span></span><span>=</span><span></span></span><span><span></span><span>K</span><span>y</span></span></span></span>“拆成共用部分，所以短形式只是共用部分之后换一个协方差公式。</p><section><h3>C<a href="#c"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>kl_kf_update_short</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>, m </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>m</span><span>;</span></div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span> (</span><span>kf_gain_and_state</span><span>(kf, z, y_out, S_out) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span> </span><span>+</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>A </span><span>=</span><span> K </span><span>+</span><span> n </span><span>*</span><span> m;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> A </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>/* Short form: P = (I - K H) P, no symmetrization */</span></div></div><div><div><div>12</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, </span><span>kf</span><span>-&gt;</span><span>H</span><span>, T, n, m, n);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>kl_mat_identity</span><span>(U, n);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>kl_mat_sub</span><span>(U, T, A, n, n);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>kl_mat_mul</span><span>(A, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, T, n, n, n);</span></div></div><div><div><div>16</div></div><div><span>  </span><span>kl_mat_copy</span><span>(T, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, n, n);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>稳态增益函数要一块自己的工作区，长度 <code>KL_STEADY_WORK_LEN(n, m)</code>，同样由调用者提供：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* S = H P H^T + R and K^T = S^-1 H P for the given P; K is n x m. Returns -1 if S is not SPD. */</span></div></div><div><div><div>2</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>riccati_gain</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span></span></div></div><div><div><div>3</div></div><div><span><span>      </span></span><span>kl_real </span><span>*</span><span><span>S</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>L</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>HP</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>Kt</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>K</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span>  </span><span>kl_mat_mul</span><span>(H, P, HP, m, n, n);</span></div></div><div><div><div>6</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(HP, H, L, m, n, m);</span></div></div><div><div><div>7</div></div><div><span>  </span><span>kl_mat_add</span><span>(L, R, S, m, m);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>kl_mat_copy</span><span>(S, L, m, m);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(L, m) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>kl_mat_copy</span><span>(HP, Kt, m, n);</span></div></div><div><div><div>12</div></div><div><span>  </span><span>kl_chol_solve</span><span>(L, Kt, m, n);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Kt, K, m, n);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>int</span><span> </span><span>kl_kf_steady_gain</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>F</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>Q</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>18</div></div><div><span>          </span><span>int</span><span><span> </span><span>m</span><span>,</span><span> kl_real </span><span>tol</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>max_iter</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>K_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>P_out</span><span>,</span></span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>kl_real </span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>20</div></div><div><span>{</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>P </span><span>=</span><span> work;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> P </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>V </span><span>=</span><span> U </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>S </span><span>=</span><span> V </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>L </span><span>=</span><span> S </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>HP </span><span>=</span><span> L </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Kt </span><span>=</span><span> HP </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> Kt </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>  </span><span>kl_mat_copy</span><span>(Q, P, n, n);</span></div></div><div><div><div>32</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> it </span><span>=</span><span> </span><span>0</span><span>; it </span><span>&lt;</span><span> max_iter; it</span><span>++</span><span>) {</span></div></div><div><div><div>33</div></div><div><span>    </span><span>if</span><span> (</span><span>riccati_gain</span><span>(H, R, P, n, m, S, L, HP, Kt, K) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>34</div></div><div><span>      </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>/* Pn = F (P - K H P) F^T + Q */</span></div></div><div><div><div>36</div></div><div><span>    </span><span>kl_mat_mul</span><span>(K, HP, T, n, m, n);</span></div></div><div><div><div>37</div></div><div><span>    </span><span>kl_mat_sub</span><span>(P, T, U, n, n);</span></div></div><div><div><div>38</div></div><div><span>    </span><span>kl_mat_mul</span><span>(F, U, T, n, n, n);</span></div></div><div><div><div>39</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, F, U, n, n, n);</span></div></div><div><div><div>40</div></div><div><span>    </span><span>kl_mat_add</span><span>(U, Q, V, n, n);</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>kl_real dmax </span><span>=</span><span> </span><span>0</span><span>, pmax </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>43</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>44</div></div><div><span><span>      </span></span><span>kl_real d </span><span>=</span><span> </span><span>KL_FABS</span><span>(</span><span>V</span><span>[i] </span><span>-</span><span> </span><span>P</span><span>[i]), a </span><span>=</span><span> </span><span>KL_FABS</span><span>(</span><span>V</span><span>[i]);</span></div></div><div><div><div>45</div></div><div><span><span>      </span></span><span>dmax </span><span>=</span><span> d </span><span>&gt;</span><span> dmax </span><span>?</span><span> d </span><span>:</span><span> dmax;</span></div></div><div><div><div>46</div></div><div><span><span>      </span></span><span>pmax </span><span>=</span><span> a </span><span>&gt;</span><span> pmax </span><span>?</span><span> a </span><span>:</span><span> pmax;</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>    </span><span>kl_mat_copy</span><span>(V, P, n, n);</span></div></div><div><div><div>49</div></div><div><span>    </span><span>if</span><span> (dmax </span><span>&lt;=</span><span> tol </span><span>*</span><span> pmax) {</span></div></div><div><div><div>50</div></div><div><span>      </span><span>if</span><span> (</span><span>riccati_gain</span><span>(H, R, P, n, m, S, L, HP, Kt, K) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>51</div></div><div><span>        </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>52</div></div><div><span>      </span><span>kl_mat_copy</span><span>(K, K_out, n, m);</span></div></div><div><div><div>53</div></div><div><span>      </span><span>if</span><span> (P_out)</span></div></div><div><div><div>54</div></div><div><span>        </span><span>kl_mat_copy</span><span>(P, P_out, n, n);</span></div></div><div><div><div>55</div></div><div><span>      </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>58</div></div><div><span>  </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>59</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>riccati_gain</code> 重复了 <code>kf_gain_and_state</code> 里”<span><span>SS</span><span><span><span></span><span>S</span></span></span></span>、Cholesky、<span><span>KTK^T</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>“那几行，因为这里没有状态向量也没有测量值，共用起来反而别扭。我宁愿重复十行，也不想搞一个参数多到看不懂的公共函数。</p></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>// Textbook short form P = (I - K H) P-: cheaper, but neither symmetry nor</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>// positive definiteness is preserved. Same failure behavior as update().</span></div></div><div><div><div>3</div></div><div><span>  </span><span>bool</span><span> </span><span>update_short</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>K</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>gain</span><span>(</span><span>z</span><span>, </span><span>K</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span><span> (</span><span>Mat</span><span>&lt;</span></span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>H</span><span>) </span><span>*</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Steady-state gain: iterate the Riccati recursion on the prior covariance until</span></div></div><div><div><div>2</div></div><div><span>// max|dP| &lt;= tol * max|P|. Returns false if it does not converge or S is not SPD.</span></div></div><div><div><div>3</div></div><div><span>// K is the gain for the converged prior P.</span></div></div><div><div><div>4</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, std::</span><span>size_t</span><span> </span><span>M</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span>bool</span><span> </span><span>steady_state_gain</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>F</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>H</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Q</span><span>,</span></span></div></div><div><div><div>6</div></div><div><span>           </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>R</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>K</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>, </span></span><span>T</span><span><span> </span><span>tol</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span><span>1</span><span>e</span></span><span>-</span><span>12</span><span>),</span></div></div><div><div><div>7</div></div><div><span>           </span><span>int</span><span><span> </span><span>max_iter</span><span> </span></span><span>=</span><span> </span><span>100000</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>auto</span><span> </span><span>gain</span><span> </span><span>=</span><span> [</span><span>&amp;</span><span>](</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Pp</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Kp</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>HP</span><span> </span><span>=</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>Pp</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>HP</span><span> </span><span>*</span><span> </span><span>H</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>R</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>12</div></div><div><span>    </span><span>Kp</span><span> </span><span>=</span><span> </span><span>chol_solve</span><span>(</span><span>L</span><span>, </span><span>HP</span><span>).</span><span>transpose</span><span>();</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>15</div></div><div><span>  </span><span>auto</span><span> </span><span>max_abs</span><span> </span><span>=</span><span> [](</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m</span><span>) {</span></span></div></div><div><div><div>16</div></div><div><span>    </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> (</span><span>const</span><span> </span><span>T</span><span> </span><span>v</span><span> : </span><span>m</span><span>.</span><span>a</span><span>) </span><span>r</span><span> </span><span>=</span><span> std::</span><span>fmax</span><span>(</span><span>r</span><span>, std::</span><span>fabs</span><span>(</span><span>v</span><span>));</span></div></div><div><div><div>18</div></div><div><span>    </span><span>return</span><span> </span><span>r</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>20</div></div><div><span>  </span><span>P</span><span> </span><span>=</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>bool</span><span> </span><span>converged</span><span> </span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>it</span><span> </span><span>&lt;</span><span> </span><span>max_iter</span><span> </span><span>&amp;&amp;</span><span> </span><span>!</span><span>converged</span><span>; </span><span>it</span><span>++</span><span>) {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>gain</span><span>(</span><span>P</span><span>, </span><span>K</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>24</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>Pn</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> (</span><span>P</span><span> </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>P</span><span>) </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>25</div></div><div><span>    </span><span>converged</span><span> </span><span>=</span><span> </span><span>max_abs</span><span>(</span><span>Pn</span><span> </span><span>-</span><span> </span><span>P</span><span>) </span><span>&lt;=</span><span> </span><span>tol</span><span> </span><span>*</span><span> </span><span>max_abs</span><span>(</span><span>Pn</span><span>);</span></div></div><div><div><div>26</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>Pn</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>  </span><span>return</span><span> </span><span>converged</span><span> </span><span>&amp;&amp;</span><span> </span><span>gain</span><span>(</span><span>P</span><span>, </span><span>K</span><span>);</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>用 lambda 把”算增益”局部化，捕获 <code>H</code>、<code>R</code>。<code>std::fmax</code> 取最大绝对值（<code>max_abs</code>）。函数返回 <code>bool</code>，参数是输出引用 <code>K</code>、<code>P</code>，这是 C++ 里没有 <code>Result</code> 时的常见写法。</p></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Short-form update P = (I - K H) P-: cheaper, but loses symmetry and</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// positive-definiteness when ill-conditioned. Same failure behavior as `update`.</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_short</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;(), </span><span>CholeskyError</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> (</span><span>y</span><span>, </span><span>s</span><span>, </span><span>k</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>gain</span><span>(</span><span>z</span><span>)?;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span>.x + </span><span>k</span><span> * </span><span>y</span><span>;</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> (</span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>() - </span><span>k</span><span> * </span><span>self</span><span>.h) * </span><span>self</span><span>.p;</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.y </span><span>=</span><span> </span></span><span>y</span><span>;</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span><span>.s </span><span>=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Ok</span><span>(())</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Steady-state gain: iterate the Riccati recursion on the prior covariance until</span></div></div><div><div><div>2</div></div><div><span>/// max|dP| &lt;= tol * max|P|. Returns (K, converged prior P), or `None` if the</span></div></div><div><div><div>3</div></div><div><span>/// iteration does not converge or a Cholesky factorization fails.</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>steady_state_gain</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>5</div></div><div><span>    </span><span>f</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>h</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>q</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>r</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>tol</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>max_iter</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>11</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;)&gt; {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>gain</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>p</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span><span>&gt;</span><span>|</span><span> -&gt; </span></span><span>Option</span><span>&lt;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;&gt; {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>hp</span><span><span> </span><span>=</span><span> *</span></span><span>h</span><span> * *</span><span>p</span><span>;</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>l</span><span><span> </span><span>=</span><span> (</span></span><span>hp</span><span> * </span><span>h</span><span>.</span><span>transpose</span><span>() + *</span><span>r</span><span>).</span><span>cholesky</span><span>()?;</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Some</span><span>(</span><span>chol_solve</span><span>(&amp;</span><span>l</span><span>, &amp;</span><span>hp</span><span>).</span><span>transpose</span><span>())</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> *</span></span><span>q</span><span>;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>max_iter</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>gain</span><span>(&amp;</span><span>p</span><span>)?;</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>pn</span><span><span> </span><span>=</span><span> *</span></span><span>f</span><span> * (</span><span>p</span><span> - </span><span>k</span><span> * *</span><span>h</span><span> * </span><span>p</span><span>) * </span><span>f</span><span>.</span><span>transpose</span><span>() + *</span><span>q</span><span>;</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> (</span><span>mut</span><span> </span><span>dmax</span><span>, </span><span>mut</span><span> </span><span>pmax</span><span>): (</span><span>Real</span><span>, </span><span>Real</span><span><span>) </span><span>=</span><span> (</span></span><span>0.0</span><span>, </span><span>0.0</span><span>);</span></div></div><div><div><div>22</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>24</div></div><div><span>                </span><span>dmax</span><span><span> </span><span>=</span><span> </span></span><span>dmax</span><span>.</span><span>max</span><span>(</span><span>fabs</span><span>(</span><span>pn</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>] - </span><span>p</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>]));</span></div></div><div><div><div>25</div></div><div><span>                </span><span>pmax</span><span><span> </span><span>=</span><span> </span></span><span>pmax</span><span>.</span><span>max</span><span>(</span><span>fabs</span><span>(</span><span>pn</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>]));</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>        </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>pn</span><span>;</span></div></div><div><div><div>29</div></div><div><span>        </span><span>if</span><span> </span><span>dmax</span><span><span> </span><span>&lt;=</span><span> </span></span><span>tol</span><span> * </span><span>pmax</span><span> {</span></div></div><div><div><div>30</div></div><div><span>            </span><span>return</span><span> </span><span>Some</span><span>((</span><span>gain</span><span>(&amp;</span><span>p</span><span>)?, </span><span>p</span><span>));</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>    </span><span>None</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>闭包 <code>gain</code> 返回 <code>Option</code>，里面的 <code>?</code> 在 Cholesky 失败时提前返回 <code>None</code>，外层的 <code>let k = gain(&amp;p)?;</code> 又把 <code>None</code> 传出去。整个函数没有一个 <code>unwrap</code>，也没有 <code>panic</code>。返回类型 <code>Option&lt;(Mat&lt;N, M&gt;, Mat&lt;N, N&gt;)&gt;</code> 把”没收敛”和”S 不正定”统一成一个 <code>None</code>，调用者不需要区分。</p><p>三个 app（<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/c/apps/ch03.c" target="_blank"><code>c/apps/ch03.c</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/cpp/apps/ch03.cpp" target="_blank"><code>cpp/apps/ch03.cpp</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/rust/src/bin/ch03.rs" target="_blank"><code>rust/src/bin/ch03.rs</code></a>）对种子 1 的数据依次跑五种配置、稳态增益滤波器和两个病态例子，写出 <code>out/ch03/&lt;case&gt;_&lt;lang&gt;.csv</code>。Cholesky 失败时，app 保持状态不变并把 NIS 写成 <code>nan</code>；<code>compare.py</code> 把两边同为 <code>nan</code> 当作相等。这一条路径这次没有被触发。</p><p>五十次蒙特卡洛的统计只在 Python 里算。C、C++、Rust 已经在种子 1 上和 Python 逐列对上，同一份算法在其他种子上不会有两样，所以统计结论是可以迁移的。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p>下面是 <code>make -s ch03</code> 的输出。先是逐位对比：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch03</span></div></div><div><div><div>2</div></div><div><span>(24 行里节选)</span></div></div><div><div><div>3</div></div><div><span>ok   illcond_joseph_c.csv: max diff 7.473e-09 &lt;= 1.0e-07</span></div></div><div><div><div>4</div></div><div><span>ok   illcond_joseph_cpp.csv: max diff 7.473e-09 &lt;= 1.0e-07</span></div></div><div><div><div>5</div></div><div><span>ok   illcond_joseph_rs.csv: max diff 7.473e-09 &lt;= 1.0e-07</span></div></div><div><div><div>6</div></div><div><span>ok   illcond_short_c.csv: max diff 4.583e-13 &lt;= 1.0e-07</span></div></div><div><div><div>7</div></div><div><span>ok   illcond_short_cpp.csv: max diff 4.601e-13 &lt;= 1.0e-07</span></div></div><div><div><div>8</div></div><div><span>ok   illcond_short_rs.csv: max diff 4.601e-13 &lt;= 1.0e-07</span></div></div><div><div><div>9</div></div><div><span>ok   steady_c.csv: max diff 1.110e-16 &lt;= 1.0e-09</span></div></div><div><div><div>10</div></div><div><span>ok   steady_cpp.csv: max diff 1.110e-16 &lt;= 1.0e-09</span></div></div><div><div><div>11</div></div><div><span>ok   steady_rs.csv: max diff 1.110e-16 &lt;= 1.0e-09</span></div></div><div><div><div>12</div></div><div><span>ok   tune_tuned_c.csv: max diff 2.487e-14 &lt;= 1.0e-09</span></div></div><div><div><div>13</div></div><div><span>ok   tune_tuned_cpp.csv: max diff 2.665e-14 &lt;= 1.0e-09</span></div></div><div><div><div>14</div></div><div><span>ok   tune_tuned_rs.csv: max diff 2.665e-14 &lt;= 1.0e-09</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>病态例子（<code>illcond_*</code>）的容差我放宽到了 <span><span>10−710^{-7}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，其他仍是 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>。因为这个例子的条件数约 <span><span>101410^{14}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>14</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>10−1610^{-16}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>16</span></span></span></span></span></span></span></span></span></span></span></span> 量级的舍入差异（numpy 里的 BLAS 和我手写的循环求和顺序不同）被放大到 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>：最大差异 <span><span>7.5×10−97.5\times10^{-9}</span><span><span><span></span><span>7.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，出现在 NIS 这一列；<code>bias</code> 和 <code>P11</code> 的最大差异分别是 <span><span>1.3×10−101.3\times10^{-10}</span><span><span><span></span><span>1.3</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>10</span></span></span></span></span></span></span></span></span></span></span></span> 和 <span><span>6.5×10−116.5\times10^{-11}</span><span><span><span></span><span>6.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>11</span></span></span></span></span></span></span></span></span></span></span></span>，其余列在 <span><span>10−1410^{-14}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span> 以下。C 和 C++、Rust 之间的最大差异只有 <span><span>2.7×10−152.7\times10^{-15}</span><span><span><span></span><span>2.7</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>15</span></span></span></span></span></span></span></span></span></span></span></span>，所以不是某个实现的 bug，而是这个问题本身对舍入的敏感度。</p><section><h3>谁在骗你<a href="#谁在骗你"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>NEES 95% bounds for mean of 50 runs, dof 2: [1.484, 2.591]</span></div></div><div><div><div>2</div></div><div><span>config    angle RMSE (deg)  mean NEES  in bounds  mean NIS</span></div></div><div><div><div>3</div></div><div><span>tuned                0.329      1.993      96.9%     1.000</span></div></div><div><div><div>4</div></div><div><span>q001                 0.657     95.667       2.5%     1.047</span></div></div><div><div><div>5</div></div><div><span>q100                 0.658      0.555       0.0%     0.950</span></div></div><div><div><div>6</div></div><div><span>r01                  0.412      6.960       0.0%     9.854</span></div></div><div><div><div>7</div></div><div><span>r10                  0.412      1.283      22.9%     0.101</span></div></div><div><div><div>8</div></div><div><span>tuned, sim with instantaneous gyro rate: mean NEES 2.233, in bounds 86.1%</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这张表是本篇的核心，几点观察：</p><ul>
<li><strong>调对的滤波器</strong>：NEES 1.993（理论值 2），96.9% 的步落在 95% 界内，NIS 1.000（理论值 1）。</li>
<li><strong><span><span>Q×0.01Q\times0.01</span><span><span><span></span><span>Q</span><span></span><span>×</span><span></span></span><span><span></span><span>0.01</span></span></span></span>（过于相信模型）</strong>：NEES 95.667。RMSE 变成两倍，更重要的是滤波器严重过自信。NIS 只有 1.047，几乎看不出问题。</li>
<li><strong><span><span>Q×100Q\times100</span><span><span><span></span><span>Q</span><span></span><span>×</span><span></span></span><span><span></span><span>100</span></span></span></span>（过于怀疑模型）</strong>：NEES 0.555，保守。NIS 0.950，同样不显眼。RMSE 0.658 度，也是两倍。</li>
<li><strong><span><span>R×0.1R\times0.1</span><span><span><span></span><span>R</span><span></span><span>×</span><span></span></span><span><span></span><span>0.1</span></span></span></span> 和 <span><span>R×10R\times10</span><span><span><span></span><span>R</span><span></span><span>×</span><span></span></span><span><span></span><span>10</span></span></span></span></strong>：NIS 9.854 和 0.101，一目了然：测量噪声设错了，新息就是它的直接证人，量级和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 的错误倍数一致。有趣的是 RMSE 在两个方向上都是 0.412，对称地变差。</li>
</ul><p>所以：<strong><span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 设错了，NIS 立刻告诉你；<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 设错了，NIS 几乎沉默，NEES 和新息自相关才会开口。</strong> 原因不难想：新息的大小主要由测量噪声和先验不确定度的比值决定，<span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 错了只通过先验协方差间接影响它。</p><p></p><figure><img src="/assets/images/kalman-filter/ch03-nees-bands.svg" alt="50 次平均 NEES：Q 正确的在界内摆动，Q 缩小到 0.01 倍的升到 100 附近，Q 放大 100 倍的沉到 0.5 附近" /><figcaption>50 次平均 NEES：Q 正确的在界内摆动，Q 缩小到 0.01 倍的升到 100 附近，Q 放大 100 倍的沉到 0.5 附近</figcaption></figure><p></p></section><section><h3>没有真值的时候：新息自相关<a href="#没有真值的时候新息自相关"><span>#</span></a></h3><p>NEES 要真值，真实硬件上你没有。自相关不需要：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>innovation autocorr tuned: 2/100 lags outside +-0.026, lag1 = -0.018</span></div></div><div><div><div>2</div></div><div><span>innovation autocorr q001: 72/100 lags outside +-0.026, lag1 = 0.020</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调对的滤波器 100 个滞后里只有 2 个落在 <span><span>±1.96/N\pm1.96/\sqrt N</span><span><span><span></span><span>±</span><span>1.96/</span><span><span><span><span><span><span></span><span>N</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span> 之外（期望 5 个左右，<span><span>N=5500N=5500</span><span><span><span></span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>5500</span></span></span></span> 个样本，这里取跳过 500 步的瞬态之后）；<span><span>Q×0.01Q\times0.01</span><span><span><span></span><span>Q</span><span></span><span>×</span><span></span></span><span><span></span><span>0.01</span></span></span></span> 的滤波器有 72 个。这就是硬件上能用的诊断：把新息记下来，画一张自相关图。</p><p></p><figure><img src="/assets/images/kalman-filter/ch03-innovation-autocorr.svg" alt="新息自相关：调对的（蓝）基本落在虚线内，Q 过小的（橙）长时间在界外" /><figcaption>新息自相关：调对的（蓝）基本落在虚线内，Q 过小的（橙）长时间在界外</figcaption></figure><p></p></section><section><h3>可观性<a href="#可观性-1"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>observability rank, CV model, position measurement: 4 of 4</span></div></div><div><div><div>2</div></div><div><span>observability rank, CV model, velocity-only measurement: 2 of 4</span></div></div><div><div><div>3</div></div><div><span>velocity-only: P_px at k=50: 10.50, k=100: 11.00, k=200: 12.00 m^2; P_vx settles at 0.0951</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只测速度时，<span><span>O\mathcal O</span><span><span><span></span><span>O</span></span></span></span> 的秩是 2，位置方差从初始的 10 起，每步稳定增加 0.01 m²，不会收敛，速度方差则稳定在 0.0951。滤波器没有崩溃、不会报错，它只是诚实地说”我不知道位置”，而且越来越不知道。</p><p></p><figure><img src="/assets/images/kalman-filter/ch03-unobservable-growth.svg" alt="只测速度时，位置方差线性增长" /><figcaption>只测速度时，位置方差线性增长</figcaption></figure><p></p></section><section><h3>稳态增益<a href="#稳态增益-1"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>K_inf = [0.011762, -0.001988]</span></div></div><div><div><div>2</div></div><div><span>angle RMSE over 60 s: time-varying 0.3479 deg, steady-state gain 0.6476 deg, difference 0.29964 deg</span></div></div><div><div><div>3</div></div><div><span>angle RMSE for t &gt;= 20 s: time-varying 0.3078 deg, steady-state gain 0.3108 deg</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>60 秒整段看，固定增益滤波器差了 0.30 度，因为它一开始没有大增益来快速收敛零偏估计（零偏的初始误差是 0.05 rad/s）。去掉前 20 秒的瞬态，两者是 0.3078 和 0.3108 度，差 0.003 度。如果你的系统启动后有一段时间可以等，固定增益是个好交易：省掉整个协方差递推，精度几乎一样。</p></section><section><h3>短形式 Joseph 形式之争：我没能让短形式崩溃<a href="#短形式-joseph-形式之争我没能让短形式崩溃"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Ill-conditioned case: P0 = 1e6 I, r = 1e-8, 1000 steps (float64)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>joseph: min eig over run = 9.629e-09, max |P01-P10| = 0.000e+00, steps with min eig &lt; 0: 0, failed updates: 0</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>short : min eig over run = 9.629e-09, max |P01-P10| = 1.082e-12, steps with min eig &lt; 0: 0, failed updates: 0</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>Same case, Python-only float32 (C/C++/Rust float32 comes in chapter 8)</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>joseph: min eig over run = -3.638e-12, max |P01-P10| = 0.000e+00, steps with min eig &lt; 0: 1</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>short : min eig over run = 0.000e+00, max |P01-P10| = 4.552e-15, steps with min eig &lt; 0: 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我预想的是短形式在这个例子里会出问题。实际：float64 下两种形式都活得很好，最小特征值一样（<span><span>9.629×10−99.629\times10^{-9}</span><span><span><span></span><span>9.629</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span>，都是对的），区别只有对称性：短形式的 <span><span>∣P01−P10∣|P_{01}-P_{10}|</span><span><span><span></span><span>∣</span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>01</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>10</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>∣</span></span></span></span> 到了 <span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span> 量级，Joseph 形式加了显式对称化，精确为 0。float32 下，Joseph 形式有一步最小特征值是 <span><span>−3.6×10−12-3.6\times10^{-12}</span><span><span><span></span><span>−</span><span>3.6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span>（相对 <span><span>10610^6</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>6</span></span></span></span></span></span></span></span></span></span></span> 的初始量级，这是 float32 能表示的底噪），短形式反而是 0。</p><p>所以这个例子没有把两者分出高下，我也不想夸大。我能说的只有：短形式在 float64 下已经丢掉了对称性（<span><span>10−1210^{-12}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>12</span></span></span></span></span></span></span></span></span></span></span></span>），而且没有机制把它找回来；Joseph 形式的优势是”不依赖 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 是否精确最优”这个<strong>保证</strong>，不是这个特定例子里的实测差距。第 8 篇上 float32 和 UD 分解的时候会再试一次，而且结论会推翻这一节的”float32 下 Joseph 形式只有一步不对”：那里的 Python 是直接做除法求增益，换成 C、C++、Rust 里用的 Cholesky 风格（<span><span>K=(P−HT/S)/SK = (P^-H^T/\sqrt S)/\sqrt S</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>/</span><span><span><span><span><span><span></span><span>S</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span>)</span><span>/</span><span><span><span><span><span><span></span><span>S</span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>）之后，同一个例子在 float32 下几乎每一步都崩溃。我猜这里的 Python 版是因为那一步的中间量恰好是整数，舍入碰巧没有损失，是运气，不是稳健。</p><p></p><figure><img src="/assets/images/kalman-filter/ch03-joseph-vs-short.svg" alt="前 40 步：最小特征值与不对称量（Joseph 形式的不对称量精确为 0，没有画出）" /><figcaption>前 40 步：最小特征值与不对称量（Joseph 形式的不对称量精确为 0，没有画出）</figcaption></figure><p></p></section></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>线性滤波器的准备到此为止：推导、实现、四种语言逐位一致、诊断工具齐全。下一篇开始进入真正的主题，姿态。我们要做的是：一个三轴陀螺加三轴加速度计加三轴磁力计的 IMU 仿真器，描述姿态的数学（旋转矩阵、欧拉角和它的万向锁、四元数、指数映射），以及一个不是卡尔曼滤波的基线：Mahony 互补滤波器，它是第 1 篇结尾那个”常增益就是低通滤波”的三维版本。后面三个卡尔曼滤波器（EKF、ESKF、UKF）都要和它比。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-02-multivariate/</id>
      <title type="text">升到高维：多变量卡尔曼滤波的推导与实现</title>
      <published>2026-10-14T01:00:00.000Z</published>
      <updated>2026-10-14T01:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-02-multivariate/"/>
      <summary type="text">卡尔曼滤波系列第 2 篇：从协方差矩阵和多元高斯的条件分布推导一般形式的卡尔曼滤波，给出 Joseph 形式与 Cholesky 求解，推导连续到离散的过程噪声，并手写一个没有任何第三方依赖的矩阵库，用 Python、C、C++、Rust 实现。</summary>
      <content type="html"><![CDATA[<p>上一篇结尾那个带零偏的数据集，第 1 篇的滤波器在零偏面前几乎没有办法。这一篇把零偏放进状态里，换成二维的滤波器：同样的传感器、同样的噪声，在 6000 步的漂移零偏数据上，第 1 篇的一维滤波器角度 RMSE 是 2.541 度，二维的是 0.348 度，差了 7.3 倍。滤波器多了一个状态，没有多任何”调参”。</p>
<p>要做到这件事，第 1 篇的标量公式必须升级成矩阵公式。这一篇的任务就是推导这个升级，然后把它写成四种语言各一份的、不依赖任何矩阵库的代码。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：<code>make ch02</code> 通过，<code>out/ch02/</code> 下有 <code>tilt_bias_{py,c,cpp,rs}.csv</code> 和 <code>cv_{py,c,cpp,rs}.csv</code>，<code>figures/ch02/</code> 下有三张图；C、C++、Rust 各有一个手写的小矩阵库（<code>kl_mat</code>、<code>mat.hpp</code>、<code>mat.rs</code>）和一个泛型线性卡尔曼滤波器（<code>kl_kf</code>、<code>kf.hpp</code>、<code>kf.rs</code>），这些接口是定稿，后面的章节直接复用。
前置：<a href="/posts/kalman-filter-01-one-dimension/">第 1 篇</a>。需要的数学：矩阵乘法、转置、对称正定矩阵、矩阵迹的求导。</p></div></div>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>随机向量与协方差矩阵<a href="#随机向量与协方差矩阵"><span>#</span></a></h3><p>把标量随机变量换成列向量 <span><span>x∈Rnx\in\mathbb R^n</span><span><span><span></span><span>x</span><span></span><span>∈</span><span></span></span><span><span></span><span><span>R</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span>。它的均值是 <span><span>μ=E[x]\mu = E[x]</span><span><span><span></span><span>μ</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span>[</span><span>x</span><span>]</span></span></span></span>，方差换成协方差矩阵</p><span><span><span>Σ=E[(x−μ)(x−μ)T]\Sigma = E\big[(x-\mu)(x-\mu)^T\big]</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span><span>[</span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>)</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span><p><span><span>Σii\Sigma_{ii}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ii</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个分量的方差，<span><span>Σij\Sigma_{ij}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ij</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是分量 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 与 <span><span>jj</span><span><span><span></span><span>j</span></span></span></span> 的协方差。<span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 对称，半正定。多元高斯分布写成 <span><span>x∼N(μ,Σ)x\sim\mathcal N(\mu,\Sigma)</span><span><span><span></span><span>x</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>μ</span><span>,</span><span></span><span>Σ</span><span>)</span></span></span></span>，概率密度是 <span><span>exp⁡(−12(x−μ)TΣ−1(x−μ))\exp(-\tfrac12 (x-\mu)^T\Sigma^{-1}(x-\mu))</span><span><span><span></span><span>exp</span><span>(</span><span>−</span><span><span></span><span><span><span><span><span><span></span><span><span><span>2</span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>))</span></span></span></span> 乘以归一化常数。</p><p>等概率密度面是 <span><span>(x−μ)TΣ−1(x−μ)=c2(x-\mu)^T\Sigma^{-1}(x-\mu) = c^2</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>c</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，在二维是一个椭圆。对 <span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span> 做特征分解 <span><span>Σ=VΛVT\Sigma = V\Lambda V^T</span><span><span><span></span><span>Σ</span><span></span><span>=</span><span></span></span><span><span></span><span>V</span><span>Λ</span><span><span>V</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，椭圆的轴沿着特征向量 <span><span>VV</span><span><span><span></span><span>V</span></span></span></span> 的列方向，半轴长是 <span><span>cλic\sqrt{\lambda_i}</span><span><span><span></span><span>c</span><span><span><span><span><span><span></span><span><span><span>λ</span><span><span><span><span><span><span></span><span><span>i</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>。后面所有”协方差”的图，画的都是 <span><span>c=1,2,3c = 1,2,3</span><span><span><span></span><span>c</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>3</span></span></span></span> 的椭圆（1、2、3 倍标准差）。</p></section><section><h3>线性变换：预测步<a href="#线性变换预测步"><span>#</span></a></h3><p>设 <span><span>y=Ax+by = Ax + b</span><span><span><span></span><span>y</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span></span></span></span>，<span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 的均值是 <span><span>μ\mu</span><span><span><span></span><span>μ</span></span></span></span>，协方差是 <span><span>Σ\Sigma</span><span><span><span></span><span>Σ</span></span></span></span>。均值直接变换：<span><span>E[y]=Aμ+bE[y] = A\mu + b</span><span><span><span></span><span>E</span><span>[</span><span>y</span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>μ</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span></span></span></span>。协方差：</p><span><span><span>Σy=E[(A(x−μ))(A(x−μ))T]=A E[(x−μ)(x−μ)T]AT=AΣAT\Sigma_y = E\big[(A(x-\mu))(A(x-\mu))^T\big] = A\,E\big[(x-\mu)(x-\mu)^T\big]A^T = A\Sigma A^T</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span><span>[</span></span><span>(</span><span>A</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>))</span><span>(</span><span>A</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>)</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>]</span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span></span><span>E</span><span><span>[</span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span>)</span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>]</span></span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>Σ</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 是常数，不影响协方差。如果再加一个和 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 独立的噪声 <span><span>w∼N(0,Q)w\sim\mathcal N(0,Q)</span><span><span><span></span><span>w</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span>Q</span><span>)</span></span></span></span>，协方差再加 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>。把状态方程 <span><span>xk=Fxk−1+Buk+wkx_{k} = Fx_{k-1} + Bu_k + w_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span><span>u</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 套进去，就是预测步：</p><span><span><span>x^−=Fx^+Bu,P−=FPFT+Q\hat x^- = F\hat x + Bu,\qquad P^- = FPF^T + Q</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>B</span><span>u</span><span>,</span><span></span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span>P</span><span><span>F</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Q</span></span></span></span></span><p>和第 1 篇的 <span><span>P−=P+qP^- = P + q</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span></span><span>+</span><span></span></span><span><span></span><span>q</span></span></span></span> 相比，只是 <span><span>F=1F = 1</span><span><span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 的特例。</p><p></p><figure><img src="/assets/images/kalman-filter/ch02-covariance-ellipse.svg" alt="一个协方差椭圆（蓝色，1、2、3 倍标准差）经过 F 变换之后变成橙色椭圆；F 是一个&quot;位置加上速度乘时间&quot;的剪切" /><figcaption>一个协方差椭圆（蓝色，1、2、3 倍标准差）经过 F 变换之后变成橙色椭圆；F 是一个"位置加上速度乘时间"的剪切</figcaption></figure><p></p><p>图里的 <span><span>F=[1101]F = \begin{bmatrix}1&amp;1\\0&amp;1\end{bmatrix}</span><span><span><span></span><span>F</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span> 是匀速模型的 <span><span>Δt=1\Delta t = 1</span><span><span><span></span><span>Δ</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> 情形。不确定度椭圆被拉长了：位置的不确定度变大，因为速度不确定，位置会越推越不准。同时位置和速度的相关性（椭圆倾斜）也被保留下来，这正是下面更新步用得到的：测量了位置，就顺带修正了速度。</p></section><section><h3>多元高斯的条件分布：更新步<a href="#多元高斯的条件分布更新步"><span>#</span></a></h3><p>状态 <span><span>x∼N(x^−,P−)x\sim\mathcal N(\hat x^-, P^-)</span><span><span><span></span><span>x</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，测量 <span><span>z=Hx+vz = Hx + v</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span>，<span><span>v∼N(0,R)v\sim\mathcal N(0,R)</span><span><span><span></span><span>v</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span>R</span><span>)</span></span></span></span> 与 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 独立。<span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 和 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 联合是高斯，均值与协方差为</p><span><span><span>[xz]∼N ⁣([x^−Hx^−], [P−P−HTHP−S]),S=HP−HT+R\begin{bmatrix}x\\z\end{bmatrix}\sim\mathcal N\!\left(\begin{bmatrix}\hat x^-\\H\hat x^-\end{bmatrix},\ \begin{bmatrix}P^- &amp; P^-H^T\\ HP^- &amp; S\end{bmatrix}\right),\qquad S = HP^-H^T+R</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>x</span></span></span><span><span></span><span><span>z</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span></span><span></span><span><span><span>(</span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>H</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span> </span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>S</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span><span>)</span></span></span><span></span><span>,</span><span></span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span></span></span></span></span><p>（协方差的非对角块：<span><span>Cov⁡(x,z)=Cov⁡(x,Hx+v)=P−HT\operatorname{Cov}(x,z) = \operatorname{Cov}(x,Hx+v) = P^-H^T</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>x</span><span>,</span><span></span><span>z</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Cov</span></span><span>(</span><span>x</span><span>,</span><span></span><span>H</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，因为 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 与 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 独立。）</p><p>我们要的是 <span><span>p(x∣z)p(x\mid z)</span><span><span><span></span><span>p</span><span>(</span><span>x</span><span></span><span>∣</span><span></span></span><span><span></span><span>z</span><span>)</span></span></span></span>。一般的多元高斯条件公式是：若 <span><span>[ab]∼N([μaμb],[ΣaaΣabΣbaΣbb])\begin{bmatrix}a\\b\end{bmatrix}\sim\mathcal N\big(\begin{bmatrix}\mu_a\\\mu_b\end{bmatrix},\begin{bmatrix}\Sigma_{aa}&amp;\Sigma_{ab}\\\Sigma_{ba}&amp;\Sigma_{bb}\end{bmatrix}\big)</span><span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>a</span></span></span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span><span>(</span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>,</span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>aa</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ba</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span><span>)</span></span></span></span></span>，则</p><span><span><span>a∣b ∼ N(μa+ΣabΣbb−1(b−μb), Σaa−ΣabΣbb−1Σba)a\mid b\ \sim\ \mathcal N\big(\mu_a + \Sigma_{ab}\Sigma_{bb}^{-1}(b-\mu_b),\ \Sigma_{aa}-\Sigma_{ab}\Sigma_{bb}^{-1}\Sigma_{ba}\big)</span><span><span><span></span><span>a</span><span></span><span>∣</span><span></span></span><span><span></span><span>b</span><span> </span><span></span><span>∼</span><span> </span><span></span></span><span><span></span><span>N</span><span><span>(</span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>a</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>(</span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span> </span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>aa</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ba</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span></span></span></span></span></span><p>后者叫 <span><span>Σbb\Sigma_{bb}</span><span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 的 Schur 补。这个公式有一个很短的证明，值得写出来。令 <span><span>w=a−ΣabΣbb−1bw = a - \Sigma_{ab}\Sigma_{bb}^{-1}b</span><span><span><span></span><span>w</span><span></span><span>=</span><span></span></span><span><span></span><span>a</span><span></span><span>−</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>b</span></span></span></span>，它和 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 的协方差是</p><span><span><span>Cov⁡(w,b)=Σab−ΣabΣbb−1Σbb=0\operatorname{Cov}(w,b) = \Sigma_{ab} - \Sigma_{ab}\Sigma_{bb}^{-1}\Sigma_{bb} = 0</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>w</span><span>,</span><span></span><span>b</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>联合高斯的两个变量如果不相关，则相互独立。于是 <span><span>a=w+ΣabΣbb−1ba = w + \Sigma_{ab}\Sigma_{bb}^{-1}b</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>w</span><span></span><span>+</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>b</span></span></span></span> 里，给定 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 之后只有第二项是确定的，<span><span>ww</span><span><span><span></span><span>w</span></span></span></span> 的分布不受 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 影响：条件均值是 <span><span>E[w]+ΣabΣbb−1bE[w] + \Sigma_{ab}\Sigma_{bb}^{-1}b</span><span><span><span></span><span>E</span><span>[</span><span>w</span><span>]</span><span></span><span>+</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>b</span></span></span></span>，条件协方差就是 <span><span>Cov⁡(w)\operatorname{Cov}(w)</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>w</span><span>)</span></span></span></span>，展开</p><span><span><span>Cov⁡(w)=Σaa−ΣabΣbb−1Σba\operatorname{Cov}(w) = \Sigma_{aa} - \Sigma_{ab}\Sigma_{bb}^{-1}\Sigma_{ba}</span><span><span><span></span><span><span>Cov</span></span><span>(</span><span>w</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>aa</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ab</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>bb</span></span></span></span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>Σ</span><span><span><span><span><span><span></span><span><span><span>ba</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>代入 <span><span>a=xa = x</span><span><span><span></span><span>a</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span></span></span></span>，<span><span>b=zb = z</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>z</span></span></span></span>，得到</p><span><span><span>K=P−HTS−1,x^=x^−+K(z−Hx^−),P=P−−KSKT=(I−KH)P−K = P^-H^TS^{-1},\qquad \hat x = \hat x^- + K(z-H\hat x^-),\qquad P = P^- - KSK^T = (I-KH)P^-</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>H</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>S</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span></span><p>最后一步用了 <span><span>KS=P−HTKS = P^-H^T</span><span><span><span></span><span>K</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，所以 <span><span>KSKT=P−HTKT=KHP−KSK^T = P^-H^TK^T = KHP^-</span><span><span><span></span><span>K</span><span>S</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>K</span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>。第 1 篇标量情形里 <span><span>K=P−/(P−+r)K = P^-/(P^-+r)</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>/</span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>r</span><span>)</span></span></span></span>，现在 <span><span>H=1H = 1</span><span><span><span></span><span>H</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>、<span><span>S=P−+rS = P^-+r</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>r</span></span></span></span>，是同一个式子。</p></section><section><h3>第二个推导：让迹最小<a href="#第二个推导让迹最小"><span>#</span></a></h3><p>另一条路，不用高斯假设，只假设更新是线性的：<span><span>x^=x^−+K(z−Hx^−)\hat x = \hat x^- + K(z-H\hat x^-)</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>H</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span>，问 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 取什么能让估计误差的总方差最小。先算这个估计的误差协方差。令 <span><span>e−=x−x^−e^- = x-\hat x^-</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>，则</p><span><span><span>e=x−x^=e−−K(Hx+v−Hx^−)=(I−KH)e−−Kve = x - \hat x = e^- - K(Hx+v-H\hat x^-) = (I-KH)e^- - Kv</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>(</span><span>H</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span><span></span><span>−</span><span></span></span><span><span></span><span>H</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>e</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>v</span></span></span></span></span><p><span><span>e−e^-</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 和 <span><span>vv</span><span><span><span></span><span>v</span></span></span></span> 独立，所以</p><span><span><span>P(K)=(I−KH)P−(I−KH)T+KRKTP(K) = (I-KH)P^-(I-KH)^T + KRK^T</span><span><span><span></span><span>P</span><span>(</span><span>K</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span><span>)</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>R</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span></span><p>这就是 Joseph 形式。注意它对任意 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 都成立，没有用到最优性。最小化总方差 <span><span>tr⁡P(K)\operatorname{tr}P(K)</span><span><span><span></span><span><span>tr</span></span><span></span><span>P</span><span>(</span><span>K</span><span>)</span></span></span></span>：用 <span><span>∂tr⁡(KAKT)/∂K=2KA\partial\operatorname{tr}(KAK^T)/\partial K = 2KA</span><span><span><span></span><span>∂</span><span></span><span><span>tr</span></span><span>(</span><span>K</span><span>A</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span>/</span><span>∂</span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span><span>K</span><span>A</span></span></span></span>（<span><span>AA</span><span><span><span></span><span>A</span></span></span></span> 对称）和 <span><span>∂tr⁡(KM)/∂K=MT\partial\operatorname{tr}(KM)/\partial K = M^T</span><span><span><span></span><span>∂</span><span></span><span><span>tr</span></span><span>(</span><span>K</span><span>M</span><span>)</span><span>/</span><span>∂</span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>M</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，展开后求导</p><span><span><span>∂tr⁡P∂K=−2P−HT+2K(HP−HT+R)=−2P−HT+2KS=0 ⇒ K=P−HTS−1\frac{\partial\operatorname{tr}P}{\partial K} = -2P^-H^T + 2K(HP^-H^T+R) = -2P^-H^T+2KS = 0\ \Rightarrow\ K = P^-H^TS^{-1}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>∂</span><span>K</span></span></span><span><span></span><span></span></span><span><span></span><span><span>∂</span><span></span><span><span>tr</span></span><span></span><span>P</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>K</span><span>(</span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>−</span><span>2</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span>K</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span><span> </span><span></span><span>⇒</span><span> </span><span></span></span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span></span><p>和条件分布的推导得到同一个 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span>。这个 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 代回去，因为 <span><span>KS=P−HTKS = P^-H^T</span><span><span><span></span><span>K</span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，Joseph 形式化简成 <span><span>(I−KH)P−(I-KH)P^-</span><span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>。</p><p>两个推导给出的信息不一样：条件分布说明”在高斯假设下这是完整的后验”，迹最小化说明”即使噪声不是高斯，只要均值和协方差对，它是线性估计里最好的”。</p></section><section><h3>为什么要用 Joseph 形式<a href="#为什么要用-joseph-形式"><span>#</span></a></h3><p>化简出来的 <span><span>P=(I−KH)P−P = (I-KH)P^-</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 更便宜，但它只有在 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 恰好是最优增益、没有舍入误差时才等于 Joseph 形式。有舍入误差时，<span><span>(I−KH)P−(I-KH)P^-</span><span><span><span></span><span>(</span><span>I</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>H</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 一般不再对称，也可能丢失正定性，协方差矩阵一旦有负特征值，滤波器就会发散。Joseph 形式是两个半正定矩阵之和（<span><span>APATAPA^T</span><span><span><span></span><span>A</span><span>P</span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 与 <span><span>KRKTKRK^T</span><span><span><span></span><span>K</span><span>R</span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>），对任何 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 都保持半正定，所以 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 有一点误差也不会出事。我再在 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 上显式做一次 <span><span>(P+PT)/2(P+P^T)/2</span><span><span><span></span><span>(</span><span>P</span><span></span><span>+</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>)</span><span>/2</span></span></span></span>，把舍入造成的微小不对称消掉。第 3 篇会拿一个病态例子把两种写法并排跑一遍。</p></section><section><h3>不要求逆：Cholesky<a href="#不要求逆cholesky"><span>#</span></a></h3><p>公式里有 <span><span>S−1S^{-1}</span><span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span>。直接求逆既慢又不稳。<span><span>S=HP−HT+RS = HP^-H^T+R</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>R</span></span></span></span> 对称正定，可以分解成 <span><span>S=LLTS = LL^T</span><span><span><span></span><span>S</span><span></span><span>=</span><span></span></span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，<span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 是下三角。要算的是 <span><span>KT=S−1HP−K^T = S^{-1}HP^-</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>（因为 <span><span>P−P^-</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 对称，<span><span>K=P−HTS−1K = P^-H^TS^{-1}</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span><span>H</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span></span></span></span> 的转置恰好是它），解线性方程组 <span><span>LLTX=HP−LL^TX = HP^-</span><span><span><span></span><span>L</span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>X</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>：先前向代入解 <span><span>LY=HP−LY = HP^-</span><span><span><span></span><span>L</span><span>Y</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>，再后向代入解 <span><span>LTX=YL^TX = Y</span><span><span><span></span><span><span>L</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>X</span><span></span><span>=</span><span></span></span><span><span></span><span>Y</span></span></span></span>，不需要求逆。</p><p>Cholesky 还附带一个好处：分解过程中某个主元不是正数，就说明 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 不是正定的，数值上已经出了问题，这是个免费的健康检查。我的实现在这种情况下返回错误并保持状态不变。</p></section><section><h3>从连续到离散：过程噪声 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span><a href="#从连续到离散过程噪声-qqq"><span>#</span></a></h3><p>怎么确定 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>？物理模型通常是连续时间的：<span><span>x˙=Ax+Gw(t)\dot x = Ax + Gw(t)</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>A</span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>Gw</span><span>(</span><span>t</span><span>)</span></span></span></span>，<span><span>ww</span><span><span><span></span><span>w</span></span></span></span> 是功率谱密度为 <span><span>WW</span><span><span><span></span><span>W</span></span></span></span> 的白噪声。离散化之后，一步的过程噪声协方差是</p><span><span><span>Qd=∫0ΔteAs GWGT eATs dsQ_d = \int_0^{\Delta t} e^{As}\,G W G^T\,e^{A^Ts}\,ds</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>Δ</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>s</span></span></span></span></span></span></span></span></span><span></span><span>G</span><span>W</span><span><span>G</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span><span>A</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span>s</span></span></span></span></span></span></span></span></span><span></span><span>d</span><span>s</span></span></span></span></span><p>直觉：时刻 <span><span>ss</span><span><span><span></span><span>s</span></span></span></span> 注入的噪声，会被系统动力学 <span><span>eA(Δt−s)e^{A(\Delta t-s)}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>(</span><span>Δ</span><span>t</span><span>−</span><span>s</span><span>)</span></span></span></span></span></span></span></span></span></span></span></span> 带着传播到这一步结束，把所有时刻的贡献加起来。积分变量换成”距离步末的时间”，就是上面的形式。</p><p><strong>白噪声加速度（匀速模型）。</strong> 一个轴，<span><span>x=[p,v]Tx = [p, v]^T</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>p</span><span>,</span><span></span><span>v</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，<span><span>p˙=v\dot p = v</span><span><span><span></span><span><span><span><span><span><span></span><span>p</span></span><span><span></span><span><span>˙</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>v</span></span></span></span>，<span><span>v˙=w\dot v = w</span><span><span><span></span><span><span><span><span><span><span></span><span>v</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>w</span></span></span></span>，<span><span>ww</span><span><span><span></span><span>w</span></span></span></span> 的谱密度为 <span><span>qcq_c</span><span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。<span><span>A=[0100]A = \begin{bmatrix}0&amp;1\\0&amp;0\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，<span><span>G=[0,1]TG = [0,1]^T</span><span><span><span></span><span>G</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>0</span><span>,</span><span></span><span>1</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，<span><span>eAs=[1s01]e^{As} = \begin{bmatrix}1&amp;s\\0&amp;1\end{bmatrix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>s</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，所以 <span><span>eAsG=[s,1]Te^{As}G = [s,1]^T</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>s</span></span></span></span></span></span></span></span></span><span>G</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>s</span><span>,</span><span></span><span>1</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>：</p><span><span><span>Qd=qc∫0Δt[s2ss1]ds=qc[Δt3/3Δt2/2Δt2/2Δt]Q_d = q_c\int_0^{\Delta t}\begin{bmatrix}s^2 &amp; s\\ s &amp; 1\end{bmatrix}ds = q_c\begin{bmatrix}\Delta t^3/3 &amp; \Delta t^2/2\\ \Delta t^2/2 &amp; \Delta t\end{bmatrix}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>Δ</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>s</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>d</span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span><span>q</span><span><span><span><span><span><span></span><span><span>c</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span><span><span></span><span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/2</span></span></span><span><span></span><span><span>Δ</span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p><strong>角度加零偏。</strong> 状态 <span><span>x=[θ,b]Tx=[\theta,b]^T</span><span><span><span></span><span>x</span><span></span><span>=</span><span></span></span><span><span></span><span>[</span><span>θ</span><span>,</span><span></span><span>b</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。陀螺读数 <span><span>ωm=ω+b+ng\omega_m = \omega + b + n_g</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>ω</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，所以 <span><span>θ˙=ωm−b−ng\dot\theta = \omega_m - b - n_g</span><span><span><span></span><span><span><span><span><span><span></span><span>θ</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>b</span><span></span><span>−</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>b˙=nb\dot b = n_b</span><span><span><span></span><span><span><span><span><span><span></span><span>b</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。<span><span>ωm\omega_m</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 当控制输入，噪声向量 <span><span>[ng,nb]T[n_g,n_b]^T</span><span><span><span></span><span>[</span><span><span>n</span><span><span><span><span><span><span></span><span><span>g</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>n</span><span><span><span><span><span><span></span><span><span>b</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span> 的谱密度是 <span><span>diag⁡(σg2,σbw2)\operatorname{diag}(\sigma_g^2,\sigma_{bw}^2)</span><span><span><span></span><span><span>diag</span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，<span><span>A=[0−100]A = \begin{bmatrix}0&amp;-1\\0&amp;0\end{bmatrix}</span><span><span><span></span><span>A</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，<span><span>G=diag⁡(−1,1)G = \operatorname{diag}(-1,1)</span><span><span><span></span><span>G</span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>−</span><span>1</span><span>,</span><span></span><span>1</span><span>)</span></span></span></span>，<span><span>eAs=[1−s01]e^{As} = \begin{bmatrix}1&amp;-s\\0&amp;1\end{bmatrix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>s</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>s</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>，<span><span>eAsG=[−1−s01]e^{As}G = \begin{bmatrix}-1&amp;-s\\0&amp;1\end{bmatrix}</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span><span>A</span><span>s</span></span></span></span></span></span></span></span></span><span>G</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span>−</span><span>1</span></span></span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span>s</span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span>：</p><span><span><span>Qd=∫0Δt[σg2+σbw2s2−σbw2s−σbw2sσbw2]ds=[σg2Δt+σbw2Δt3/3−σbw2Δt2/2−σbw2Δt2/2σbw2Δt]Q_d = \int_0^{\Delta t}\begin{bmatrix}\sigma_g^2+\sigma_{bw}^2s^2 &amp; -\sigma_{bw}^2 s\\ -\sigma_{bw}^2 s &amp; \sigma_{bw}^2\end{bmatrix}ds = \begin{bmatrix}\sigma_g^2\Delta t+\sigma_{bw}^2\Delta t^3/3 &amp; -\sigma_{bw}^2\Delta t^2/2\\ -\sigma_{bw}^2\Delta t^2/2 &amp; \sigma_{bw}^2\Delta t\end{bmatrix}</span><span><span><span></span><span><span>Q</span><span><span><span><span><span><span></span><span><span>d</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>∫</span><span><span><span><span><span><span></span><span><span>0</span></span></span><span><span></span><span><span><span>Δ</span><span>t</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>s</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span><span>−</span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>s</span></span></span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span><span></span><span>d</span><span>s</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span>[</span></span><span><span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span></span><span>+</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>3</span></span></span></span></span></span></span></span><span>/3</span></span></span><span><span></span><span><span>−</span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span><span></span><span><span><span><span><span><span></span><span><span>−</span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span><span>t</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/2</span></span></span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span>]</span></span></span></span></span></span></span><p>这里的 <span><span>σg2\sigma_g^2</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是谱密度，不是第 1 篇里的每样本标准差。第 1 篇每个样本的陀螺噪声 <span><span>σn=0.05\sigma_n = 0.05</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.05</span></span></span></span> rad/s，离散化后每步角度噪声的方差是 <span><span>(σnΔt)2(\sigma_n\Delta t)^2</span><span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，要让 <span><span>σg2Δt\sigma_g^2\Delta t</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span></span></span></span> 等于它，<span><span>σg2=σn2Δt\sigma_g^2 = \sigma_n^2\Delta t</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>g</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span></span></span></span>。零偏的随机游走用 <span><span>σbw=10−3\sigma_{bw} = 10^{-3}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span><span>b</span><span>w</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>3</span></span></span></span></span></span></span></span></span></span></span></span> rad/s/<span><span>s\sqrt{\text{s}}</span><span><span><span></span><span><span><span><span><span><span></span><span><span><span>s</span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span>，一秒内零偏的标准差漂移 0.001 rad/s。</p></section></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><section><h3>模型与仿真<a href="#模型与仿真"><span>#</span></a></h3><p>模型矩阵和两个仿真器都在 <code>python/kl/models.py</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Model matrices and simulators for the linear examples (chapters 2 and 3)."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>from</span><span> pathlib </span><span>import</span><span> Path</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>from</span><span> .io </span><span>import</span><span> write_csv</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>ROOT</span><span> </span><span>=</span><span> </span><span>Path</span><span>(</span></span><span>__file__</span><span><span>).</span><span>resolve</span><span>().parents[</span></span><span>2</span><span>]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>def</span><span> </span><span>tilt_bias_model</span><span>(</span><span>dt</span><span>,</span><span><span> </span><span>gyro_sigma</span></span><span>,</span><span><span> </span><span>bias_rw_sigma</span></span><span>,</span><span><span> </span><span>meas_sigma</span></span><span>):</span></div></div><div><div><div>13</div></div><div><span>    </span><span>"""x = [theta, b], u = omega_m, z = theta + v.</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>gyro_sigma is the per-sample gyro noise std (rad/s), as in chapter 1, so its</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>continuous white-noise PSD is gyro_sigma^2 * dt. bias_rw_sigma is in rad/s/sqrt(s).</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>"""</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>F </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span><span>, </span><span>-</span><span>dt], [</span></span><span>0.0</span><span>, </span><span>1.0</span><span>]])</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>B </span><span>=</span><span> np.</span><span>array</span><span>([[dt], [</span><span>0.0</span><span>]])</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>H </span><span>=</span><span> np.</span><span>array</span><span>([[</span><span>1.0</span><span>, </span><span>0.0</span><span>]])</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>qg </span><span>=</span><span> gyro_sigma</span><span>**</span><span>2</span><span><span> </span><span>*</span><span> dt</span></span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>qb </span><span>=</span><span> bias_rw_sigma</span><span>**</span><span>2</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> np.</span><span>array</span><span>(</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>[</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>[qg </span><span>*</span><span> dt </span><span>+</span><span> qb </span><span>*</span><span> dt</span><span>**</span><span>3</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span><span>, </span><span>-</span><span>qb </span><span>*</span><span> dt</span><span>**</span></span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>2</span><span>],</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>[</span><span>-</span><span>qb </span><span>*</span><span> dt</span><span>**</span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>2</span><span><span>, qb </span><span>*</span><span> dt],</span></span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>]</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> np.</span><span>array</span><span>([[meas_sigma</span><span>**</span><span>2</span><span>]])</span></div></div><div><div><div>30</div></div><div><span>    </span><span>return</span><span> F, B, H, Q, R</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>def</span><span> </span><span>cv2d_model</span><span>(</span><span>dt</span><span>,</span><span><span> </span><span>qc</span></span><span>,</span><span><span> </span><span>meas_sigma</span></span><span>):</span></div></div><div><div><div>34</div></div><div><span>    </span><span>"""x = [px, py, vx, vy], position measurements, white-noise acceleration PSD qc."""</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>I2, Z2 </span><span>=</span><span> np.</span><span>eye</span><span>(</span><span>2</span><span><span>), np.</span><span>zeros</span><span>((</span></span><span>2</span><span>, </span><span>2</span><span>))</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>F </span><span>=</span><span> np.</span><span>block</span><span>([[I2, dt </span><span>*</span><span> I2], [Z2, I2]])</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>H </span><span>=</span><span> np.</span><span>block</span><span>([[I2, Z2]])</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>Q </span><span>=</span><span> qc </span><span>*</span><span> np.</span><span>block</span><span>(</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>[[dt</span><span>**</span><span>3</span><span><span> </span><span>/</span><span> </span></span><span>3</span><span><span> </span><span>*</span><span> I2, dt</span><span>**</span></span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> I2], [dt</span><span>**</span></span><span>2</span><span><span> </span><span>/</span><span> </span></span><span>2</span><span><span> </span><span>*</span><span> I2, dt </span><span>*</span><span> I2]]</span></span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>R </span><span>=</span><span> meas_sigma</span><span>**</span><span>2</span><span><span> </span><span>*</span><span> I2</span></span></div></div><div><div><div>42</div></div><div><span>    </span><span>return</span><span> F, </span><span>None</span><span>, H, Q, R</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>tilt_bias_model</code> 里的 <code>Q</code> 就是上面积分的结果，<code>qg = gyro_sigma**2 * dt</code> 完成了每样本标准差到谱密度的换算。<code>cv2d_model</code> 里 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 是两个轴共享同一个 <span><span>2×22\times 2</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span></span></span></span> 块，状态顺序是 <span><span>[px,py,vx,vy][p_x,p_y,v_x,v_y]</span><span><span><span></span><span>[</span><span><span>p</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>p</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>x</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>v</span><span><span><span><span><span><span></span><span><span>y</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>]</span></span></span></span>，所以块结构是 <code>[[a I, b I], [b I, c I]]</code>。<code>cv2d_model</code> 返回 <code>B = None</code>：这个模型没有控制输入。</p><p>仿真器：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>simulate_tilt_bias</span><span>(</span><span>seed</span><span>,</span><span><span> </span><span>n</span></span><span>=</span><span>6000</span><span>,</span><span><span> </span><span>dt</span></span><span>=</span><span>0.01</span><span>,</span><span><span> </span><span>gyro_sigma</span></span><span>=</span><span>0.05</span><span>,</span><span><span> </span><span>meas_sigma</span></span><span>=</span><span>0.05</span><span>,</span></div></div><div><div><div>2</div></div><div><span><span>                       </span></span><span>bias0</span><span>=</span><span>0.05</span><span>,</span><span><span> </span><span>bias_rw_sigma</span></span><span>=</span><span>1e-3</span><span>,</span><span><span> </span><span>write</span></span><span>=</span><span>True</span><span>,</span><span><span> </span><span>gyro_avg</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(seed)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>arange</span><span>(n) </span><span>*</span><span> dt</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> </span><span>2</span><span><span> </span><span>*</span><span> np.pi </span><span>*</span><span> </span></span><span>0.25</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(w </span><span>*</span><span> t)</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> gyro_avg:</span></div></div><div><div><div>8</div></div><div><span>        </span><span># the gyro reports the rate averaged over the sample interval, which is exactly</span></div></div><div><div><div>9</div></div><div><span>        </span><span># what the filter's discrete model theta_k = theta_{k-1} + dt * rate assumes</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>omega </span><span>=</span><span> (theta </span><span>-</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(w </span><span>*</span><span> (t </span><span>-</span><span> dt))) </span><span>/</span><span> dt</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>else</span><span>:</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>omega </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> w </span><span>*</span><span> np.</span><span>cos</span><span>(w </span><span>*</span><span> t)  </span></span><span># instantaneous rate at the sample time</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>bias </span><span>=</span><span> bias0 </span><span>+</span><span> np.</span><span>concatenate</span><span>(([</span><span>0.0</span><span><span>], np.</span><span>cumsum</span><span>(bias_rw_sigma </span><span>*</span><span> np.</span><span>sqrt</span><span>(dt) </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(n </span><span>-</span><span> </span></span><span>1</span><span>))))</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>omega_m </span><span>=</span><span> omega </span><span>+</span><span> bias </span><span>+</span><span> gyro_sigma </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(n)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> theta </span><span>+</span><span> meas_sigma </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(n)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>cols </span><span>=</span><span> {</span><span>"t"</span><span>: t, </span><span>"omega_m"</span><span>: omega_m, </span><span>"z"</span><span>: z, </span><span>"theta"</span><span>: theta, </span><span>"bias"</span><span>: bias}</span></div></div><div><div><div>17</div></div><div><span>    </span><span>if</span><span> write:</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>write_csv</span><span>(</span><span>ROOT</span><span> </span><span>/</span><span> </span><span>"data"</span><span><span> </span><span>/</span><span> </span></span><span>f</span><span>"ch02_tilt_bias_seed</span><span>{</span><span>seed</span><span>}</span><span>.csv"</span><span>, cols)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>return</span><span> cols</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>def</span><span> </span><span>simulate_cv</span><span>(</span><span>seed</span><span>,</span><span><span> </span><span>n</span></span><span>=</span><span>200</span><span>,</span><span><span> </span><span>dt</span></span><span>=</span><span>0.1</span><span>,</span><span><span> </span><span>qc</span></span><span>=</span><span>0.1</span><span>,</span><span><span> </span><span>meas_sigma</span></span><span>=</span><span>1.0</span><span>,</span><span><span> </span><span>write</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(seed)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>F, _, H, Q, R </span><span>=</span><span> </span><span>cv2d_model</span><span>(dt, qc, meas_sigma)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>L </span><span>=</span><span> np.linalg.</span><span>cholesky</span><span>(Q)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> np.</span><span>array</span><span>([</span><span>0.0</span><span>, </span><span>0.0</span><span>, </span><span>1.0</span><span>, </span><span>0.5</span><span>])</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>truth </span><span>=</span><span> np.</span><span>empty</span><span>((n, </span><span>4</span><span>))</span></div></div><div><div><div>28</div></div><div><span>    </span><span>for</span><span> k </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>truth[k] </span><span>=</span><span> x</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>x </span><span>=</span><span> F </span><span>@</span><span> x </span><span>+</span><span> L </span><span>@</span><span> rng.</span><span>standard_normal</span><span>(</span><span>4</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> truth[:, :</span><span>2</span><span><span>] </span><span>+</span><span> meas_sigma </span><span>*</span><span> rng.</span><span>standard_normal</span><span>((n, </span></span><span>2</span><span>))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>cols </span><span>=</span><span> {</span></div></div><div><div><div>33</div></div><div><span>        </span><span>"t"</span><span><span>: np.</span><span>arange</span><span>(n) </span><span>*</span><span> dt,</span></span></div></div><div><div><div>34</div></div><div><span>        </span><span>"zx"</span><span>: z[:, </span><span>0</span><span>], </span><span>"zy"</span><span>: z[:, </span><span>1</span><span>],</span></div></div><div><div><div>35</div></div><div><span>        </span><span>"px"</span><span>: truth[:, </span><span>0</span><span>], </span><span>"py"</span><span>: truth[:, </span><span>1</span><span>], </span><span>"vx"</span><span>: truth[:, </span><span>2</span><span>], </span><span>"vy"</span><span>: truth[:, </span><span>3</span><span>],</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span>    </span><span>if</span><span> write:</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>write_csv</span><span>(</span><span>ROOT</span><span> </span><span>/</span><span> </span><span>"data"</span><span><span> </span><span>/</span><span> </span></span><span>f</span><span>"ch02_cv_seed</span><span>{</span><span>seed</span><span>}</span><span>.csv"</span><span>, cols)</span></div></div><div><div><div>39</div></div><div><span>    </span><span>return</span><span> cols</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>角度加零偏的数据（这里有一个细节，我踩过一次，第 3 篇会讲）：陀螺仪报告的是采样区间内的平均角速度，也就是 <span><span>(θk−θk−1)/Δt(\theta_k-\theta_{k-1})/\Delta t</span><span><span><span></span><span>(</span><span><span>θ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>/Δ</span><span>t</span></span></span></span>，这恰好是滤波器的离散模型 <span><span>θk=θk−1+Δt⋅rate\theta_k = \theta_{k-1}+\Delta t\cdot\text{rate}</span><span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Δ</span><span>t</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span>rate</span></span></span></span></span> 假设的东西。同样的 <span><span>0.5sin⁡(2π⋅0.25t)0.5\sin(2\pi\cdot 0.25t)</span><span><span><span></span><span>0.5</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.25</span><span>t</span><span>)</span></span></span></span> 真值，<span><span>0.010.01</span><span><span><span></span><span>0.01</span></span></span></span> s 步长，6000 步（60 秒），零偏从 0.05 rad/s 起步做随机游走，种子 1。匀速模型的真值是从离散过程里直接采样出来的：<span><span>xk+1=Fxk+wkx_{k+1} = Fx_k + w_k</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>k</span><span>+</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>F</span><span><span>x</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>，<span><span>wk∼N(0,Q)w_k\sim\mathcal N(0,Q)</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span>Q</span><span>)</span></span></span></span>，用 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 的 Cholesky 因子 <span><span>LL</span><span><span><span></span><span>L</span></span></span></span> 采样 <span><span>Lw′Lw'</span><span><span><span></span><span>L</span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>w′∼N(0,I)w'\sim\mathcal N(0,I)</span><span><span><span></span><span><span>w</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span>I</span><span>)</span></span></span></span>。起点 <span><span>(0,0)(0,0)</span><span><span><span></span><span>(</span><span>0</span><span>,</span><span></span><span>0</span><span>)</span></span></span></span>，速度 <span><span>(1.0,0.5)(1.0, 0.5)</span><span><span><span></span><span>(</span><span>1.0</span><span>,</span><span></span><span>0.5</span><span>)</span></span></span></span> m/s，<span><span>Δt=0.1\Delta t = 0.1</span><span><span><span></span><span>Δ</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0.1</span></span></span></span> s，200 步，位置测量噪声 1 m。</p></section><section><h3>滤波器<a href="#滤波器"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Generic linear Kalman filter (predict / update), Joseph form, Cholesky solve."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>4</div></div><div><span>from</span><span> scipy.linalg </span><span>import</span><span> cho_factor, cho_solve</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>class</span><span><span> </span><span>KalmanFilter</span></span><span>:</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"""x- = F x + B u,  P- = F P F' + Q;  K = P- H' S^-1,  S = H P- H' + R."""</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>F</span></span><span>,</span><span><span> </span><span>B</span></span><span>,</span><span><span> </span><span>H</span></span><span>,</span><span><span> </span><span>Q</span></span><span>,</span><span><span> </span><span>R</span></span><span>,</span><span><span> </span><span>x0</span></span><span>,</span><span><span> </span><span>P0</span></span><span>):</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span><span>.F </span><span>=</span><span> np.</span><span>asarray</span><span>(F, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>n </span><span>=</span><span> </span><span>self</span><span>.F.shape[</span><span>0</span><span>]</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span><span>.B </span><span>=</span><span> np.</span><span>zeros</span><span>((n, </span></span><span>0</span><span>)) </span><span>if</span><span> B </span><span>is</span><span> </span><span>None</span><span> </span><span>else</span><span><span> np.</span><span>asarray</span><span>(B, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span><span>.H </span><span>=</span><span> np.</span><span>asarray</span><span>(H, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span><span>.Q </span><span>=</span><span> np.</span><span>asarray</span><span>(Q, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span><span>.R </span><span>=</span><span> np.</span><span>asarray</span><span>(R, </span></span><span>dtype</span><span>=</span><span>float</span><span>)</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> np.</span><span>asarray</span><span>(x0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>18</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> np.</span><span>asarray</span><span>(P0, </span></span><span>dtype</span><span>=</span><span>float</span><span><span>).</span><span>copy</span><span>()</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>def</span><span> </span><span>predict</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>u</span></span><span>=</span><span>None</span><span>):</span></div></div><div><div><div>21</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span><span>.F </span><span>@</span><span> </span></span><span>self</span><span>.x</span></div></div><div><div><div>22</div></div><div><span>        </span><span>if</span><span> u </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span><span> </span><span>and</span><span> </span><span>self</span><span>.B.shape[</span><span>1</span><span>]:</span></div></div><div><div><div>23</div></div><div><span>            </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span><span>.x </span><span>+</span><span> </span></span><span>self</span><span><span>.B </span><span>@</span><span> np.</span><span>atleast_1d</span><span>(u)</span></span></div></div><div><div><div>24</div></div><div><span>        </span><span>self</span><span><span>.P </span><span>=</span><span> </span></span><span>self</span><span><span>.F </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> </span></span><span>self</span><span><span>.F.T </span><span>+</span><span> </span></span><span>self</span><span>.Q</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>z</span></span><span>,</span><span><span> </span><span>joseph</span></span><span>=</span><span>True</span><span>):</span></div></div><div><div><div>27</div></div><div><span>        </span><span>"""Returns (y, S): innovation and its covariance. Raises LinAlgError if S is not SPD."""</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>z </span><span>=</span><span> np.</span><span>atleast_1d</span><span>(z)</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>n </span><span>=</span><span> </span><span>self</span><span>.x.shape[</span><span>0</span><span>]</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>y </span><span>=</span><span> z </span><span>-</span><span> </span><span>self</span><span><span>.H </span><span>@</span><span> </span></span><span>self</span><span>.x</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>S </span><span>=</span><span> </span><span>self</span><span><span>.H </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> </span></span><span>self</span><span><span>.H.T </span><span>+</span><span> </span></span><span>self</span><span>.R</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>c </span><span>=</span><span> </span><span>cho_factor</span><span>(S, </span><span>lower</span><span>=</span><span>True</span><span>)  </span><span># raises LinAlgError when not positive definite</span></div></div><div><div><div>33</div></div><div><span>        </span><span># S K' = H P-  (P- symmetric)  =&gt;  K' = S^-1 H P-</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>K </span><span>=</span><span> </span><span>cho_solve</span><span>(c, </span><span>self</span><span><span>.H </span><span>@</span><span> </span></span><span>self</span><span>.P).T</span></div></div><div><div><div>35</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span><span>.x </span><span>+</span><span> K </span><span>@</span><span> y</span></span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>A </span><span>=</span><span> np.</span><span>eye</span><span>(n) </span><span>-</span><span> K </span><span>@</span><span> </span><span>self</span><span>.H</span></div></div><div><div><div>37</div></div><div><span>        </span><span>if</span><span> joseph:</span></div></div><div><div><div>38</div></div><div><span>            </span><span>self</span><span><span>.P </span><span>=</span><span> A </span><span>@</span><span> </span></span><span>self</span><span><span>.P </span><span>@</span><span> A.T </span><span>+</span><span> K </span><span>@</span><span> </span></span><span>self</span><span><span>.R </span><span>@</span><span> K.T</span></span></div></div><div><div><div>39</div></div><div><span>            </span><span>self</span><span><span>.P </span><span>=</span><span> </span></span><span>0.5</span><span><span> </span><span>*</span><span> (</span></span><span>self</span><span><span>.P </span><span>+</span><span> </span></span><span>self</span><span>.P.T)</span></div></div><div><div><div>40</div></div><div><span>        </span><span>else</span><span>:</span></div></div><div><div><div>41</div></div><div><span>            </span><span>self</span><span><span>.P </span><span>=</span><span> A </span><span>@</span><span> </span></span><span>self</span><span>.P</span></div></div><div><div><div>42</div></div><div><span>        </span><span>return</span><span> y, S</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>def</span><span> </span><span>nis</span><span>(</span><span>y</span><span>,</span><span><span> </span><span>S</span></span><span>):</span></div></div><div><div><div>46</div></div><div><span>    </span><span>"""Normalized innovation squared y' S^-1 y via Cholesky."""</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> </span><span>cho_factor</span><span>(np.</span><span>atleast_2d</span><span>(S), </span><span>lower</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>return</span><span> </span><span>float</span><span><span>(y </span><span>@</span><span> </span><span>cho_solve</span><span>(c, y))</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>predict</code> 和 <code>update</code> 几乎是公式的逐行翻译。有几处值得点出：</p><ul>
<li><code>B</code> 允许为空，用一个 <span><span>n×0n\times 0</span><span><span><span></span><span>n</span><span></span><span>×</span><span></span></span><span><span></span><span>0</span></span></span></span> 的矩阵表示，这样无控制输入的模型（匀速）和有控制输入的模型（角度加零偏）走同一份代码。</li>
<li><code>cho_factor(S, lower=True)</code> 在 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 不正定时抛 <code>LinAlgError</code>，这就是上面说的”免费健康检查”。</li>
<li><code>cho_solve(c, self.H @ self.P)</code> 解的是 <span><span>SX=HP−SX = HP^-</span><span><span><span></span><span>S</span><span>X</span><span></span><span>=</span><span></span></span><span><span></span><span>H</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>，<span><span>X=KTX = K^T</span><span><span><span></span><span>X</span><span></span><span>=</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>。</li>
<li><code>update</code> 返回 <code>(y, S)</code>，让调用者可以算 NIS <span><span>=yTS−1y=y^TS^{-1}y</span><span><span><span></span><span>=</span><span></span></span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>y</span></span></span></span>（第 3 篇的主角）。</li>
</ul></section><section><h3>跑两个模型<a href="#跑两个模型"><span>#</span></a></h3><p>两个模型用同一个类，只是传入的矩阵不同。角度加零偏：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>run_tilt</span><span>(</span><span>d</span><span>):</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>F, B, H, Q, R </span><span>=</span><span> </span><span>tilt_bias_model</span><span>(</span><span>DT</span><span>, </span><span>SG</span><span>, </span><span>SBW</span><span>, </span><span>SV</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>kf </span><span>=</span><span> </span><span>KalmanFilter</span><span>(F, B, H, Q, R, np.</span><span>zeros</span><span>(</span><span>2</span><span><span>), </span><span>TILT_P0</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(d[</span><span>"t"</span><span>])</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>o </span><span>=</span><span> {k: np.</span><span>empty</span><span>(n) </span><span>for</span><span> k </span><span>in</span><span> (</span><span>"theta"</span><span>, </span><span>"bias"</span><span>, </span><span>"P00"</span><span>, </span><span>"P01"</span><span>, </span><span>"P11"</span><span>, </span><span>"nis"</span><span>)}</span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>kf.</span><span>predict</span><span>(d[</span><span>"omega_m"</span><span>][i])</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>y, S </span><span>=</span><span> kf.</span><span>update</span><span>(d[</span><span>"z"</span><span>][i])</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>o[</span><span>"theta"</span><span>][i], o[</span><span>"bias"</span><span><span>][i] </span><span>=</span><span> kf.x</span></span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>o[</span><span>"P00"</span><span>][i], o[</span><span>"P01"</span><span>][i], o[</span><span>"P11"</span><span><span>][i] </span><span>=</span><span> kf.P[</span></span><span>0</span><span>, </span><span>0</span><span>], kf.P[</span><span>0</span><span>, </span><span>1</span><span>], kf.P[</span><span>1</span><span>, </span><span>1</span><span>]</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>o[</span><span>"nis"</span><span><span>][i] </span><span>=</span><span> </span><span>nis</span><span>(y, S)</span></span></div></div><div><div><div>12</div></div><div><span>    </span><span>return</span><span> {</span><span>"t"</span><span>: d[</span><span>"t"</span><span><span>], </span><span>**</span><span>o}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>初值 <span><span>x^0=0\hat x_0 = 0</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>，<span><span>P0=diag⁡(1,0.01)P_0 = \operatorname{diag}(1, 0.01)</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>diag</span></span><span>(</span><span>1</span><span>,</span><span></span><span>0.01</span><span>)</span></span></span></span>（角度很不确定，零偏标准差 0.1 rad/s 的先验）。每一步 <code>predict(omega_m)</code> 然后 <code>update(z)</code>。</p></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>三份实现共享两层结构：一个小矩阵库（乘、加、减、转置、Cholesky、Cholesky 求解），和一个建立在它上面的滤波器。</p><ul>
<li><strong>内存。</strong> C：滤波器 <code>kl_kf</code> 只持有指针，所有数组（<code>x</code>、<code>P</code>、<code>F</code>…）和一块工作区由调用者提供，库里没有 <code>malloc</code>。工作区长度用宏 <code>KL_KF_WORK_LEN(n, m)</code> 给出，等于 <code>update</code> 实际用到的元素个数。C++ 和 Rust：矩阵是编译期定尺寸的值类型（<code>std::array</code> 或 <code>[[Real; C]; R]</code>），放在栈上，没有工作区的概念，临时矩阵由编译器管理。</li>
<li><strong>尺寸。</strong> C 的维度是运行期的 <code>int</code> 参数，写错了编译器不管；C++ 和 Rust 的维度是模板/const 泛型参数，<code>Mat&lt;2,1&gt; * Mat&lt;2,2&gt;</code> 根本编译不过。代价是代码膨胀：每种尺寸组合实例化一份。</li>
<li><strong>标量类型。</strong> <code>kl_real</code> / 模板参数 <code>T</code> / <code>Real</code>，第 8 篇会用到。</li>
<li><strong>错误。</strong> Cholesky 失败：C 返回 <code>-1</code>，C++ 返回 <code>false</code>，Rust 返回 <code>Result&lt;(), CholeskyError&gt;</code>。三者都保证失败时状态不变。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><p>C 的矩阵库全是平坦的行主序数组加显式维度。声明如下，完整实现放在后面折叠里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Small dense matrices: row-major kl_real arrays with explicit dimensions.</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* Output buffers must not alias input buffers (except for the in-place routines). */</span></div></div><div><div><div>3</div></div><div><span>#ifndef</span><span> </span><span>KL_MAT_H</span></div></div><div><div><div>4</div></div><div><span>#define</span><span> </span><span>KL_MAT_H</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/* C = A B; A is n x m, B is m x p, C is n x p. */</span></div></div><div><div><div>9</div></div><div><span>void</span><span> </span><span>kl_mat_mul</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>p</span><span>);</span></span></div></div><div><div><div>10</div></div><div><span>/* C = A B^T; A is n x m, B is p x m, C is n x p. */</span></div></div><div><div><div>11</div></div><div><span>void</span><span> </span><span>kl_mat_mul_bt</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>p</span><span>);</span></span></div></div><div><div><div>12</div></div><div><span>/* C = A + B and C = A - B; all n x m. */</span></div></div><div><div><div>13</div></div><div><span>void</span><span> </span><span>kl_mat_add</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>);</span></span></div></div><div><div><div>14</div></div><div><span>void</span><span> </span><span>kl_mat_sub</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>);</span></span></div></div><div><div><div>15</div></div><div><span>/* C = s A; both n x m. */</span></div></div><div><div><div>16</div></div><div><span>void</span><span> </span><span>kl_mat_scale</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> kl_real </span><span>s</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>);</span></span></div></div><div><div><div>17</div></div><div><span>/* C = A^T; A is n x m, C is m x n. */</span></div></div><div><div><div>18</div></div><div><span>void</span><span> </span><span>kl_mat_transpose</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>);</span></span></div></div><div><div><div>19</div></div><div><span>/* A = I (n x n). */</span></div></div><div><div><div>20</div></div><div><span>void</span><span> </span><span>kl_mat_identity</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>);</span></span></div></div><div><div><div>21</div></div><div><span>/* C = A; both n x m. */</span></div></div><div><div><div>22</div></div><div><span>void</span><span> </span><span>kl_mat_copy</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>);</span></span></div></div><div><div><div>23</div></div><div><span>/* A = (A + A^T) / 2 in place; A is n x n. */</span></div></div><div><div><div>24</div></div><div><span>void</span><span> </span><span>kl_mat_symmetrize</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>);</span></span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>/* In-place lower Cholesky A = L L^T for symmetric positive definite A (n x n).</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>* Only the lower triangle of A is read; on success it holds L and the strict upper</span></div></div><div><div><div>28</div></div><div><span><span> </span></span><span>* triangle is zeroed. Returns 0 on success, -1 if a pivot is &lt;= 0 (A is then garbage). */</span></div></div><div><div><div>29</div></div><div><span>int</span><span> </span><span>kl_chol</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>);</span></span></div></div><div><div><div>30</div></div><div><span>/* Solves (L L^T) X = B in place. L is the n x n factor from kl_chol (lower triangle used),</span></div></div><div><div><div>31</div></div><div><span><span> </span></span><span>* B is n x ncols and is overwritten with X. */</span></div></div><div><div><div>32</div></div><div><span>void</span><span> </span><span>kl_chol_solve</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>L</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>B</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>ncols</span><span>);</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>约定输出不与输入别名，所以 <code>kl_mat_mul(A, B, C, ...)</code> 里的 <code>C</code> 必须是另一块内存。这是 C 里最容易写错的地方，也是 <code>kl_kf_update</code> 里有大量临时数组的原因。<code>kl_mat_mul_bt</code> 专门算 <span><span>ABTAB^T</span><span><span><span></span><span>A</span><span><span>B</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，免得为了转置多拷一份。</p><p>滤波器：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Generic linear Kalman filter. The caller owns every buffer (the library never allocates).</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* All matrices are row-major: x n, P n x n, F n x n, B n x p, H m x n, Q n x n, R m x m. */</span></div></div><div><div><div>3</div></div><div><span>#ifndef</span><span> </span><span>KL_KF_H</span></div></div><div><div><div>4</div></div><div><span>#define</span><span> </span><span>KL_KF_H</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/* Workspace (kl_real count) used by kl_kf_update, laid out in this order:</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>*   y m | S m*m | L m*m | HP m*n | Kt m*n | K n*m | A n*n | T n*n | U n*n | KR n*m</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>* (kl_kf_predict only needs a prefix of the same buffer). */</span></div></div><div><div><div>11</div></div><div><span>#define</span><span> </span><span>KL_KF_WORK_LEN</span><span><span>(</span><span>n</span><span>, </span><span>m</span><span>) ((m) </span></span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (m) </span><span>+</span><span> </span><span>4</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (n) </span><span>+</span><span> </span><span>3</span><span> </span><span>*</span><span> (n) </span><span>*</span><span> (n))</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>14</div></div><div><span>  </span><span>int</span><span> n, m, p;</span><span> /* state, measurement, control dimensions (p may be 0) */</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>x, </span><span>*</span><span>P, </span><span>*</span><span>F, </span><span>*</span><span>B, </span><span>*</span><span>H, </span><span>*</span><span>Q, </span><span>*</span><span>R;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>work;</span><span> /* KL_KF_WORK_LEN(n, m) entries */</span></div></div><div><div><div>17</div></div><div><span>} kl_kf;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>/* x = F x + B u, P = F P F^T + Q. u may be NULL when p == 0. */</span></div></div><div><div><div>20</div></div><div><span>void</span><span> </span><span>kl_kf_predict</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>u</span><span>);</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>/* Joseph-form update. y = z - H x, S = H P H^T + R, K^T = S^-1 (H P) by Cholesky,</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>* x += K y, P = A P A^T + K R K^T with A = I - K H, then P is symmetrized.</span></div></div><div><div><div>24</div></div><div><span><span> </span></span><span>* Returns -1 if S is not positive definite (x and P are left unchanged), else 0.</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>* y_out (m) and S_out (m x m) receive the innovation and its covariance; either may be NULL. */</span></div></div><div><div><div>26</div></div><div><span>int</span><span> </span><span>kl_kf_update</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>);</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>/* Same as kl_kf_update but with the textbook short form P = (I - K H) P:</span></div></div><div><div><div>29</div></div><div><span><span> </span></span><span>* no Joseph terms and no symmetrization. Same failure behavior and workspace. */</span></div></div><div><div><div>30</div></div><div><span>int</span><span> </span><span>kl_kf_update_short</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>);</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>/* Workspace (kl_real count) for kl_kf_steady_gain:</span></div></div><div><div><div>33</div></div><div><span><span> </span></span><span>*   P n*n | T n*n | U n*n | V n*n | S m*m | L m*m | HP m*n | Kt m*n | K n*m */</span></div></div><div><div><div>34</div></div><div><span>#define</span><span> </span><span>KL_STEADY_WORK_LEN</span><span><span>(</span><span>n</span><span>, </span><span>m</span><span>) (</span></span><span>4</span><span> </span><span>*</span><span> (n) </span><span>*</span><span> (n) </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (m) </span><span>+</span><span> </span><span>3</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (n))</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>/* Iterates the Riccati recursion P &lt;- F (P - K H P) F^T + Q from P = Q until</span></div></div><div><div><div>37</div></div><div><span><span> </span></span><span>* max|dP| &lt;= tol * max|P|, then returns the gain K (n x m) and prior covariance P_out (n x n,</span></div></div><div><div><div>38</div></div><div><span><span> </span></span><span>* may be NULL) at convergence. Returns -1 if it does not converge or S is not SPD. */</span></div></div><div><div><div>39</div></div><div><span>int</span><span> </span><span>kl_kf_steady_gain</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>F</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>Q</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>40</div></div><div><span>          </span><span>int</span><span><span> </span><span>m</span><span>,</span><span> kl_real </span><span>tol</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>max_iter</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>K_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>P_out</span><span>,</span></span></div></div><div><div><div>41</div></div><div><span><span>          </span></span><span>kl_real </span><span>*</span><span><span>work</span><span>);</span></span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>预测与更新的实现：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>kl_kf_predict</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>u</span><span>)</span></span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>fx </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span>;</span><span>      /* n   */</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>fp </span><span>=</span><span> fx </span><span>+</span><span> n;</span><span>        /* n*n */</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>fpft </span><span>=</span><span> fp </span><span>+</span><span> n </span><span>*</span><span> n;</span><span>  /* n*n */</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>F</span><span>, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, fx, n, n, </span><span>1</span><span>);</span></div></div><div><div><div>9</div></div><div><span>  </span><span>if</span><span> (</span><span>kf</span><span>-&gt;</span><span>p</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>kl_real </span><span>*</span><span>bu </span><span>=</span><span> fpft </span><span>+</span><span> n </span><span>*</span><span> n;</span><span> /* n */</span></div></div><div><div><div>11</div></div><div><span>    </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>B</span><span>, u, bu, n, </span><span>kf</span><span>-&gt;</span><span>p</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>kl_mat_add</span><span>(fx, bu, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, n, </span><span>1</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>kl_mat_copy</span><span>(fx, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, n, </span><span>1</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>F</span><span>, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, fp, n, n, n);</span></div></div><div><div><div>18</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(fp, </span><span>kf</span><span>-&gt;</span><span>F</span><span>, fpft, n, n, n);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>kl_mat_add</span><span>(fpft, </span><span>kf</span><span>-&gt;</span><span>Q</span><span>, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, n, n);</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>/* Shared by both update forms: y, S, K^T = S^-1 H P by Cholesky, then x += K y.</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>* Returns -1 before touching x on failure. K ends up at the workspace offset used below. */</span></div></div><div><div><div>24</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>kf_gain_and_state</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>)</span></span></div></div><div><div><div>25</div></div><div><span>{</span></div></div><div><div><div>26</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>, m </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>m</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>y </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>S </span><span>=</span><span> y </span><span>+</span><span> m;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>L </span><span>=</span><span> S </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>HP </span><span>=</span><span> L </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Kt </span><span>=</span><span> HP </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> Kt </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> K </span><span>+</span><span> n </span><span>*</span><span> m </span><span>+</span><span> n </span><span>*</span><span> n;</span><span> /* skips A */</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>/* y = z - H x */</span></div></div><div><div><div>37</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>H</span><span>, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, S, m, n, </span><span>1</span><span>);</span><span> /* S is scratch here */</span></div></div><div><div><div>38</div></div><div><span>  </span><span>kl_mat_sub</span><span>(z, S, y, m, </span><span>1</span><span>);</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>/* S = H P H^T + R */</span></div></div><div><div><div>41</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>H</span><span>, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, HP, m, n, n);</span></div></div><div><div><div>42</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(HP, </span><span>kf</span><span>-&gt;</span><span>H</span><span>, L, m, n, m);</span><span> /* L is scratch until the copy below */</span></div></div><div><div><div>43</div></div><div><span>  </span><span>kl_mat_add</span><span>(L, </span><span>kf</span><span>-&gt;</span><span>R</span><span>, S, m, m);</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>/* K^T = S^-1 H P, so K never needs an explicit inverse */</span></div></div><div><div><div>46</div></div><div><span>  </span><span>kl_mat_copy</span><span>(S, L, m, m);</span></div></div><div><div><div>47</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(L, m) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>48</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>49</div></div><div><span>  </span><span>kl_mat_copy</span><span>(HP, Kt, m, n);</span></div></div><div><div><div>50</div></div><div><span>  </span><span>kl_chol_solve</span><span>(L, Kt, m, n);</span></div></div><div><div><div>51</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Kt, K, m, n);</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>  </span><span>if</span><span> (y_out)</span></div></div><div><div><div>54</div></div><div><span>    </span><span>kl_mat_copy</span><span>(y, y_out, m, </span><span>1</span><span>);</span></div></div><div><div><div>55</div></div><div><span>  </span><span>if</span><span> (S_out)</span></div></div><div><div><div>56</div></div><div><span>    </span><span>kl_mat_copy</span><span>(S, S_out, m, m);</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span><span>  </span></span><span>/* x += K y */</span></div></div><div><div><div>59</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, y, T, n, m, </span><span>1</span><span>);</span></div></div><div><div><div>60</div></div><div><span>  </span><span>kl_mat_add</span><span>(</span><span>kf</span><span>-&gt;</span><span>x</span><span>, T, U, n, </span><span>1</span><span>);</span></div></div><div><div><div>61</div></div><div><span>  </span><span>kl_mat_copy</span><span>(U, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, n, </span><span>1</span><span>);</span></div></div><div><div><div>62</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>63</div></div><div><span>}</span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>int</span><span> </span><span>kl_kf_update</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>)</span></span></div></div><div><div><div>66</div></div><div><span>{</span></div></div><div><div><div>67</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>, m </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>m</span><span>;</span></div></div><div><div><div>68</div></div><div><span>  </span><span>if</span><span> (</span><span>kf_gain_and_state</span><span>(kf, z, y_out, S_out) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>69</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span> </span><span>+</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>A </span><span>=</span><span> K </span><span>+</span><span> n </span><span>*</span><span> m;</span></div></div><div><div><div>72</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> A </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>73</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>74</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>KR </span><span>=</span><span> U </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span><span>  </span></span><span>/* Joseph form: P = A P A^T + K R K^T, A = I - K H */</span></div></div><div><div><div>77</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, </span><span>kf</span><span>-&gt;</span><span>H</span><span>, T, n, m, n);</span></div></div><div><div><div>78</div></div><div><span>  </span><span>kl_mat_identity</span><span>(U, n);</span></div></div><div><div><div>79</div></div><div><span>  </span><span>kl_mat_sub</span><span>(U, T, A, n, n);</span></div></div><div><div><div>80</div></div><div><span>  </span><span>kl_mat_mul</span><span>(A, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, T, n, n, n);</span></div></div><div><div><div>81</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(T, A, U, n, n, n);</span></div></div><div><div><div>82</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, </span><span>kf</span><span>-&gt;</span><span>R</span><span>, KR, n, m, m);</span></div></div><div><div><div>83</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(KR, K, T, n, m, n);</span></div></div><div><div><div>84</div></div><div><span>  </span><span>kl_mat_add</span><span>(U, T, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, n, n);</span></div></div><div><div><div>85</div></div><div><span>  </span><span>kl_mat_symmetrize</span><span>(</span><span>kf</span><span>-&gt;</span><span>P</span><span>, n);</span></div></div><div><div><div>86</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>87</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>kf_gain_and_state</code> 负责 <span><span>yy</span><span><span><span></span><span>y</span></span></span></span>、<span><span>SS</span><span><span><span></span><span>S</span></span></span></span>、Cholesky、<span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 和 <span><span>x+=Kyx \mathrel{+}= Ky</span><span><span><span></span><span>x</span><span></span><span><span>+</span></span></span><span><span></span><span>=</span><span></span></span><span><span></span><span>K</span><span>y</span></span></span></span>，<code>kl_kf_update</code> 在它后面接 Joseph 形式。把共同部分拆出来，是因为第 3 篇要在同一个文件里加一个 <code>kl_kf_update_short</code>，它和 <code>update</code> 只在协方差那几行不同。<code>work</code> 被切成互不重叠的子数组，布局写在 <code>kl_kf.h</code> 的注释里；<code>S</code> 和 <code>L</code> 在算 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span> 的过程中兼作临时空间，这是 C 版为了省内存做的小把戏，也是它比另外两个版本难读的原因。<code>ch02.c</code> 里的使用方式如下，所有存储都在栈上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>const</span><span> </span><span>double</span><span> dt </span><span>=</span><span> TILT_DT, qg </span><span>=</span><span> SG </span><span>*</span><span> SG </span><span>*</span><span> dt, qb </span><span>=</span><span> SBW </span><span>*</span><span> SBW;</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>kl_real </span><span>x</span><span>[</span><span>2</span><span>] </span><span>=</span><span> {</span><span>0</span><span>, </span><span>0</span><span>};</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>kl_real </span><span>P</span><span>[</span><span>4</span><span>] </span><span>=</span><span> {</span><span>1</span><span>, </span><span>0</span><span>, </span><span>0</span><span>, </span><span>0.01</span><span>};</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>kl_real </span><span>F</span><span>[</span><span>4</span><span>] </span><span>=</span><span> {</span><span>1</span><span>, (kl_real)</span><span>-</span><span>dt, </span><span>0</span><span>, </span><span>1</span><span>};</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>kl_real </span><span>B</span><span>[</span><span>2</span><span>] </span><span>=</span><span> {(kl_real)dt, </span><span>0</span><span>};</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>kl_real </span><span>H</span><span>[</span><span>2</span><span>] </span><span>=</span><span> {</span><span>1</span><span>, </span><span>0</span><span>};</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>kl_real </span><span>Q</span><span>[</span><span>4</span><span>] </span><span>=</span><span> {(kl_real)(qg </span><span>*</span><span> dt </span><span>+</span><span> qb </span><span>*</span><span> dt </span><span>*</span><span> dt </span><span>*</span><span> dt </span><span>/</span><span> </span><span>3</span><span>), (kl_real)(</span><span>-</span><span>qb </span><span>*</span><span> dt </span><span>*</span><span> dt </span><span>/</span><span> </span><span>2</span><span>),</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>(kl_real)(</span><span>-</span><span>qb </span><span>*</span><span> dt </span><span>*</span><span> dt </span><span>/</span><span> </span><span>2</span><span>), (kl_real)(qb </span><span>*</span><span> dt)};</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>kl_real </span><span>R</span><span>[</span><span>1</span><span>] </span><span>=</span><span> {(kl_real)(SV </span><span>*</span><span> SV)};</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>kl_real </span><span>work</span><span>[</span><span>KL_KF_WORK_LEN</span><span>(</span><span>2</span><span>, </span><span>1</span><span>)];</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>kl_kf kf </span><span>=</span><span> {</span><span>2</span><span>, </span><span>1</span><span>, </span><span>1</span><span>, x, P, F, B, H, Q, R, work};</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：c/src/kl_mat.c<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>void</span><span> </span><span>kl_mat_mul</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>p</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> p; j</span><span>++</span><span>) {</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>8</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> </span><span>A</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>*</span><span> </span><span>B</span><span>[k </span><span>*</span><span> p </span><span>+</span><span> j];</span></div></div><div><div><div>10</div></div><div><span>      </span><span>C</span><span>[i </span><span>*</span><span> p </span><span>+</span><span> j] </span><span>=</span><span> s;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>void</span><span> </span><span>kl_mat_mul_bt</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>p</span><span>)</span></span></div></div><div><div><div>15</div></div><div><span>{</span></div></div><div><div><div>16</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> p; j</span><span>++</span><span>) {</span></div></div><div><div><div>18</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>19</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> m; k</span><span>++</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> </span><span>A</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> k] </span><span>*</span><span> </span><span>B</span><span>[j </span><span>*</span><span> m </span><span>+</span><span> k];</span></div></div><div><div><div>21</div></div><div><span>      </span><span>C</span><span>[i </span><span>*</span><span> p </span><span>+</span><span> j] </span><span>=</span><span> s;</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>void</span><span> </span><span>kl_mat_add</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>)</span></span></div></div><div><div><div>26</div></div><div><span>{</span></div></div><div><div><div>27</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>28</div></div><div><span>    </span><span>C</span><span>[i] </span><span>=</span><span> </span><span>A</span><span>[i] </span><span>+</span><span> </span><span>B</span><span>[i];</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>void</span><span> </span><span>kl_mat_sub</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>B</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>)</span></span></div></div><div><div><div>32</div></div><div><span>{</span></div></div><div><div><div>33</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>34</div></div><div><span>    </span><span>C</span><span>[i] </span><span>=</span><span> </span><span>A</span><span>[i] </span><span>-</span><span> </span><span>B</span><span>[i];</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>void</span><span> </span><span>kl_mat_scale</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> kl_real </span><span>s</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>)</span></span></div></div><div><div><div>38</div></div><div><span>{</span></div></div><div><div><div>39</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>40</div></div><div><span>    </span><span>C</span><span>[i] </span><span>=</span><span> s </span><span>*</span><span> </span><span>A</span><span>[i];</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>void</span><span> </span><span>kl_mat_transpose</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>)</span></span></div></div><div><div><div>44</div></div><div><span>{</span></div></div><div><div><div>45</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>46</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> m; j</span><span>++</span><span>)</span></div></div><div><div><div>47</div></div><div><span>      </span><span>C</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> i] </span><span>=</span><span> </span><span>A</span><span>[i </span><span>*</span><span> m </span><span>+</span><span> j];</span></div></div><div><div><div>48</div></div><div><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>void</span><span> </span><span>kl_mat_identity</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>51</div></div><div><span>{</span></div></div><div><div><div>52</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>53</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>)</span></div></div><div><div><div>54</div></div><div><span>      </span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> (i </span><span>==</span><span> j) </span><span>?</span><span> </span><span>1</span><span> </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>void</span><span> </span><span>kl_mat_copy</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>A</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>C</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>)</span></span></div></div><div><div><div>58</div></div><div><span>{</span></div></div><div><div><div>59</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> m; i</span><span>++</span><span>)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>C</span><span>[i] </span><span>=</span><span> </span><span>A</span><span>[i];</span></div></div><div><div><div>61</div></div><div><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>void</span><span> </span><span>kl_mat_symmetrize</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>64</div></div><div><span>{</span></div></div><div><div><div>65</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>)</span></div></div><div><div><div>66</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> i </span><span>+</span><span> </span><span>1</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>) {</span></div></div><div><div><div>67</div></div><div><span><span>      </span></span><span>kl_real a </span><span>=</span><span> (kl_real)</span><span>0.5</span><span> </span><span>*</span><span> (</span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>+</span><span> </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> i]);</span></div></div><div><div><div>68</div></div><div><span>      </span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> a;</span></div></div><div><div><div>69</div></div><div><span>      </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> i] </span><span>=</span><span> a;</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>71</div></div><div><span>}</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>int</span><span> </span><span>kl_chol</span><span><span>(</span><span>kl_real </span></span><span>*</span><span><span>A</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>74</div></div><div><span>{</span></div></div><div><div><div>75</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> j </span><span>=</span><span> </span><span>0</span><span>; j </span><span>&lt;</span><span> n; j</span><span>++</span><span>) {</span></div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>kl_real d </span><span>=</span><span> </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> j];</span></div></div><div><div><div>77</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> j; k</span><span>++</span><span>)</span></div></div><div><div><div>78</div></div><div><span><span>      </span></span><span>d </span><span>-=</span><span> </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> k] </span><span>*</span><span> </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> k];</span></div></div><div><div><div>79</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>(d </span><span>&gt;</span><span> </span><span>0</span><span>))</span><span> /* also catches NaN */</span></div></div><div><div><div>80</div></div><div><span>      </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>d </span><span>=</span><span> </span><span>KL_SQRT</span><span>(d);</span></div></div><div><div><div>82</div></div><div><span>    </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> d;</span></div></div><div><div><div>83</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> j </span><span>+</span><span> </span><span>1</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>84</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j];</span></div></div><div><div><div>85</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> j; k</span><span>++</span><span>)</span></div></div><div><div><div>86</div></div><div><span><span>        </span></span><span>s </span><span>-=</span><span> </span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> k] </span><span>*</span><span> </span><span>A</span><span>[j </span><span>*</span><span> n </span><span>+</span><span> k];</span></div></div><div><div><div>87</div></div><div><span>      </span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> s </span><span>/</span><span> d;</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>89</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> j; i</span><span>++</span><span>)</span></div></div><div><div><div>90</div></div><div><span>      </span><span>A</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> j] </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>91</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>92</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>93</div></div><div><span>}</span></div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span>void</span><span> </span><span>kl_chol_solve</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>L</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>B</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>ncols</span><span>)</span></span></div></div><div><div><div>96</div></div><div><span>{</span></div></div><div><div><div>97</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> c </span><span>=</span><span> </span><span>0</span><span>; c </span><span>&lt;</span><span> ncols; c</span><span>++</span><span>) {</span></div></div><div><div><div>98</div></div><div><span><span>    </span></span><span>/* forward: L z = b */</span></div></div><div><div><div>99</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>100</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>B</span><span>[i </span><span>*</span><span> ncols </span><span>+</span><span> c];</span></div></div><div><div><div>101</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> </span><span>0</span><span>; k </span><span>&lt;</span><span> i; k</span><span>++</span><span>)</span></div></div><div><div><div>102</div></div><div><span><span>        </span></span><span>s </span><span>-=</span><span> </span><span>L</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> k] </span><span>*</span><span> </span><span>B</span><span>[k </span><span>*</span><span> ncols </span><span>+</span><span> c];</span></div></div><div><div><div>103</div></div><div><span>      </span><span>B</span><span>[i </span><span>*</span><span> ncols </span><span>+</span><span> c] </span><span>=</span><span> s </span><span>/</span><span> </span><span>L</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> i];</span></div></div><div><div><div>104</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>105</div></div><div><span><span>    </span></span><span>/* backward: L^T x = z */</span></div></div><div><div><div>106</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> n </span><span>-</span><span> </span><span>1</span><span>; i </span><span>&gt;=</span><span> </span><span>0</span><span>; i</span><span>--</span><span>) {</span></div></div><div><div><div>107</div></div><div><span><span>      </span></span><span>kl_real s </span><span>=</span><span> </span><span>B</span><span>[i </span><span>*</span><span> ncols </span><span>+</span><span> c];</span></div></div><div><div><div>108</div></div><div><span>      </span><span>for</span><span> (</span><span>int</span><span> k </span><span>=</span><span> i </span><span>+</span><span> </span><span>1</span><span>; k </span><span>&lt;</span><span> n; k</span><span>++</span><span>)</span></div></div><div><div><div>109</div></div><div><span><span>        </span></span><span>s </span><span>-=</span><span> </span><span>L</span><span>[k </span><span>*</span><span> n </span><span>+</span><span> i] </span><span>*</span><span> </span><span>B</span><span>[k </span><span>*</span><span> ncols </span><span>+</span><span> c];</span></div></div><div><div><div>110</div></div><div><span>      </span><span>B</span><span>[i </span><span>*</span><span> ncols </span><span>+</span><span> c] </span><span>=</span><span> s </span><span>/</span><span> </span><span>L</span><span>[i </span><span>*</span><span> n </span><span>+</span><span> i];</span></div></div><div><div><div>111</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>112</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>113</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：c/include/kl/kl_kf.h、c/src/kl_kf.c（文件里还有第 3 篇才讲的 `update_short` 和稳态增益）<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Generic linear Kalman filter. The caller owns every buffer (the library never allocates).</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>* All matrices are row-major: x n, P n x n, F n x n, B n x p, H m x n, Q n x n, R m x m. */</span></div></div><div><div><div>3</div></div><div><span>#ifndef</span><span> </span><span>KL_KF_H</span></div></div><div><div><div>4</div></div><div><span>#define</span><span> </span><span>KL_KF_H</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/* Workspace (kl_real count) used by kl_kf_update, laid out in this order:</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>*   y m | S m*m | L m*m | HP m*n | Kt m*n | K n*m | A n*n | T n*n | U n*n | KR n*m</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>* (kl_kf_predict only needs a prefix of the same buffer). */</span></div></div><div><div><div>11</div></div><div><span>#define</span><span> </span><span>KL_KF_WORK_LEN</span><span><span>(</span><span>n</span><span>, </span><span>m</span><span>) ((m) </span></span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (m) </span><span>+</span><span> </span><span>4</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (n) </span><span>+</span><span> </span><span>3</span><span> </span><span>*</span><span> (n) </span><span>*</span><span> (n))</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>14</div></div><div><span>  </span><span>int</span><span> n, m, p;</span><span> /* state, measurement, control dimensions (p may be 0) */</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>x, </span><span>*</span><span>P, </span><span>*</span><span>F, </span><span>*</span><span>B, </span><span>*</span><span>H, </span><span>*</span><span>Q, </span><span>*</span><span>R;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>work;</span><span> /* KL_KF_WORK_LEN(n, m) entries */</span></div></div><div><div><div>17</div></div><div><span>} kl_kf;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>/* x = F x + B u, P = F P F^T + Q. u may be NULL when p == 0. */</span></div></div><div><div><div>20</div></div><div><span>void</span><span> </span><span>kl_kf_predict</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>u</span><span>);</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>/* Joseph-form update. y = z - H x, S = H P H^T + R, K^T = S^-1 (H P) by Cholesky,</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>* x += K y, P = A P A^T + K R K^T with A = I - K H, then P is symmetrized.</span></div></div><div><div><div>24</div></div><div><span><span> </span></span><span>* Returns -1 if S is not positive definite (x and P are left unchanged), else 0.</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>* y_out (m) and S_out (m x m) receive the innovation and its covariance; either may be NULL. */</span></div></div><div><div><div>26</div></div><div><span>int</span><span> </span><span>kl_kf_update</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>);</span></span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>/* Same as kl_kf_update but with the textbook short form P = (I - K H) P:</span></div></div><div><div><div>29</div></div><div><span><span> </span></span><span>* no Joseph terms and no symmetrization. Same failure behavior and workspace. */</span></div></div><div><div><div>30</div></div><div><span>int</span><span> </span><span>kl_kf_update_short</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>);</span></span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>/* Workspace (kl_real count) for kl_kf_steady_gain:</span></div></div><div><div><div>33</div></div><div><span><span> </span></span><span>*   P n*n | T n*n | U n*n | V n*n | S m*m | L m*m | HP m*n | Kt m*n | K n*m */</span></div></div><div><div><div>34</div></div><div><span>#define</span><span> </span><span>KL_STEADY_WORK_LEN</span><span><span>(</span><span>n</span><span>, </span><span>m</span><span>) (</span></span><span>4</span><span> </span><span>*</span><span> (n) </span><span>*</span><span> (n) </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (m) </span><span>+</span><span> </span><span>3</span><span> </span><span>*</span><span> (m) </span><span>*</span><span> (n))</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>/* Iterates the Riccati recursion P &lt;- F (P - K H P) F^T + Q from P = Q until</span></div></div><div><div><div>37</div></div><div><span><span> </span></span><span>* max|dP| &lt;= tol * max|P|, then returns the gain K (n x m) and prior covariance P_out (n x n,</span></div></div><div><div><div>38</div></div><div><span><span> </span></span><span>* may be NULL) at convergence. Returns -1 if it does not converge or S is not SPD. */</span></div></div><div><div><div>39</div></div><div><span>int</span><span> </span><span>kl_kf_steady_gain</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>F</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>Q</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>40</div></div><div><span>          </span><span>int</span><span><span> </span><span>m</span><span>,</span><span> kl_real </span><span>tol</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>max_iter</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>K_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>P_out</span><span>,</span></span></div></div><div><div><div>41</div></div><div><span><span>          </span></span><span>kl_real </span><span>*</span><span><span>work</span><span>);</span></span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_kf.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/kl_mat.h"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>kl_kf_predict</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>u</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>fx </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span>;</span><span>      /* n   */</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>fp </span><span>=</span><span> fx </span><span>+</span><span> n;</span><span>        /* n*n */</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>fpft </span><span>=</span><span> fp </span><span>+</span><span> n </span><span>*</span><span> n;</span><span>  /* n*n */</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>F</span><span>, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, fx, n, n, </span><span>1</span><span>);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>if</span><span> (</span><span>kf</span><span>-&gt;</span><span>p</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>kl_real </span><span>*</span><span>bu </span><span>=</span><span> fpft </span><span>+</span><span> n </span><span>*</span><span> n;</span><span> /* n */</span></div></div><div><div><div>15</div></div><div><span>    </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>B</span><span>, u, bu, n, </span><span>kf</span><span>-&gt;</span><span>p</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>kl_mat_add</span><span>(fx, bu, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, n, </span><span>1</span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>kl_mat_copy</span><span>(fx, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, n, </span><span>1</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>F</span><span>, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, fp, n, n, n);</span></div></div><div><div><div>22</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(fp, </span><span>kf</span><span>-&gt;</span><span>F</span><span>, fpft, n, n, n);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>kl_mat_add</span><span>(fpft, </span><span>kf</span><span>-&gt;</span><span>Q</span><span>, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, n, n);</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>/* Shared by both update forms: y, S, K^T = S^-1 H P by Cholesky, then x += K y.</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>* Returns -1 before touching x on failure. K ends up at the workspace offset used below. */</span></div></div><div><div><div>28</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>kf_gain_and_state</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>)</span></span></div></div><div><div><div>29</div></div><div><span>{</span></div></div><div><div><div>30</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>, m </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>m</span><span>;</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>y </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span>;</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>S </span><span>=</span><span> y </span><span>+</span><span> m;</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>L </span><span>=</span><span> S </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>HP </span><span>=</span><span> L </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Kt </span><span>=</span><span> HP </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> Kt </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> K </span><span>+</span><span> n </span><span>*</span><span> m </span><span>+</span><span> n </span><span>*</span><span> n;</span><span> /* skips A */</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>/* y = z - H x */</span></div></div><div><div><div>41</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>H</span><span>, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, S, m, n, </span><span>1</span><span>);</span><span> /* S is scratch here */</span></div></div><div><div><div>42</div></div><div><span>  </span><span>kl_mat_sub</span><span>(z, S, y, m, </span><span>1</span><span>);</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>/* S = H P H^T + R */</span></div></div><div><div><div>45</div></div><div><span>  </span><span>kl_mat_mul</span><span>(</span><span>kf</span><span>-&gt;</span><span>H</span><span>, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, HP, m, n, n);</span></div></div><div><div><div>46</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(HP, </span><span>kf</span><span>-&gt;</span><span>H</span><span>, L, m, n, m);</span><span> /* L is scratch until the copy below */</span></div></div><div><div><div>47</div></div><div><span>  </span><span>kl_mat_add</span><span>(L, </span><span>kf</span><span>-&gt;</span><span>R</span><span>, S, m, m);</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>/* K^T = S^-1 H P, so K never needs an explicit inverse */</span></div></div><div><div><div>50</div></div><div><span>  </span><span>kl_mat_copy</span><span>(S, L, m, m);</span></div></div><div><div><div>51</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(L, m) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>52</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>53</div></div><div><span>  </span><span>kl_mat_copy</span><span>(HP, Kt, m, n);</span></div></div><div><div><div>54</div></div><div><span>  </span><span>kl_chol_solve</span><span>(L, Kt, m, n);</span></div></div><div><div><div>55</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Kt, K, m, n);</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>  </span><span>if</span><span> (y_out)</span></div></div><div><div><div>58</div></div><div><span>    </span><span>kl_mat_copy</span><span>(y, y_out, m, </span><span>1</span><span>);</span></div></div><div><div><div>59</div></div><div><span>  </span><span>if</span><span> (S_out)</span></div></div><div><div><div>60</div></div><div><span>    </span><span>kl_mat_copy</span><span>(S, S_out, m, m);</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span><span>  </span></span><span>/* x += K y */</span></div></div><div><div><div>63</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, y, T, n, m, </span><span>1</span><span>);</span></div></div><div><div><div>64</div></div><div><span>  </span><span>kl_mat_add</span><span>(</span><span>kf</span><span>-&gt;</span><span>x</span><span>, T, U, n, </span><span>1</span><span>);</span></div></div><div><div><div>65</div></div><div><span>  </span><span>kl_mat_copy</span><span>(U, </span><span>kf</span><span>-&gt;</span><span>x</span><span>, n, </span><span>1</span><span>);</span></div></div><div><div><div>66</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>67</div></div><div><span>}</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span>int</span><span> </span><span>kl_kf_update</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>)</span></span></div></div><div><div><div>70</div></div><div><span>{</span></div></div><div><div><div>71</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>, m </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>m</span><span>;</span></div></div><div><div><div>72</div></div><div><span>  </span><span>if</span><span> (</span><span>kf_gain_and_state</span><span>(kf, z, y_out, S_out) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>73</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>74</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span> </span><span>+</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>75</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>A </span><span>=</span><span> K </span><span>+</span><span> n </span><span>*</span><span> m;</span></div></div><div><div><div>76</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> A </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>77</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>78</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>KR </span><span>=</span><span> U </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span><span>  </span></span><span>/* Joseph form: P = A P A^T + K R K^T, A = I - K H */</span></div></div><div><div><div>81</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, </span><span>kf</span><span>-&gt;</span><span>H</span><span>, T, n, m, n);</span></div></div><div><div><div>82</div></div><div><span>  </span><span>kl_mat_identity</span><span>(U, n);</span></div></div><div><div><div>83</div></div><div><span>  </span><span>kl_mat_sub</span><span>(U, T, A, n, n);</span></div></div><div><div><div>84</div></div><div><span>  </span><span>kl_mat_mul</span><span>(A, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, T, n, n, n);</span></div></div><div><div><div>85</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(T, A, U, n, n, n);</span></div></div><div><div><div>86</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, </span><span>kf</span><span>-&gt;</span><span>R</span><span>, KR, n, m, m);</span></div></div><div><div><div>87</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(KR, K, T, n, m, n);</span></div></div><div><div><div>88</div></div><div><span>  </span><span>kl_mat_add</span><span>(U, T, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, n, n);</span></div></div><div><div><div>89</div></div><div><span>  </span><span>kl_mat_symmetrize</span><span>(</span><span>kf</span><span>-&gt;</span><span>P</span><span>, n);</span></div></div><div><div><div>90</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>91</div></div><div><span>}</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span>int</span><span> </span><span>kl_kf_update_short</span><span><span>(</span><span>kl_kf </span></span><span>*</span><span><span>kf</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>z</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>y_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>S_out</span><span>)</span></span></div></div><div><div><div>94</div></div><div><span>{</span></div></div><div><div><div>95</div></div><div><span>  </span><span>int</span><span> n </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>n</span><span>, m </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>m</span><span>;</span></div></div><div><div><div>96</div></div><div><span>  </span><span>if</span><span> (</span><span>kf_gain_and_state</span><span>(kf, z, y_out, S_out) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>97</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>98</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> </span><span>kf</span><span>-&gt;</span><span>work</span><span> </span><span>+</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> m </span><span>+</span><span> </span><span>2</span><span> </span><span>*</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>99</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>A </span><span>=</span><span> K </span><span>+</span><span> n </span><span>*</span><span> m;</span></div></div><div><div><div>100</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> A </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>101</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div><span><span>  </span></span><span>/* Short form: P = (I - K H) P, no symmetrization */</span></div></div><div><div><div>104</div></div><div><span>  </span><span>kl_mat_mul</span><span>(K, </span><span>kf</span><span>-&gt;</span><span>H</span><span>, T, n, m, n);</span></div></div><div><div><div>105</div></div><div><span>  </span><span>kl_mat_identity</span><span>(U, n);</span></div></div><div><div><div>106</div></div><div><span>  </span><span>kl_mat_sub</span><span>(U, T, A, n, n);</span></div></div><div><div><div>107</div></div><div><span>  </span><span>kl_mat_mul</span><span>(A, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, T, n, n, n);</span></div></div><div><div><div>108</div></div><div><span>  </span><span>kl_mat_copy</span><span>(T, </span><span>kf</span><span>-&gt;</span><span>P</span><span>, n, n);</span></div></div><div><div><div>109</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>110</div></div><div><span>}</span></div></div><div><div><div>111</div></div><div>
</div></div><div><div><div>112</div></div><div><span>/* S = H P H^T + R and K^T = S^-1 H P for the given P; K is n x m. Returns -1 if S is not SPD. */</span></div></div><div><div><div>113</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>riccati_gain</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>P</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>m</span><span>,</span></span></div></div><div><div><div>114</div></div><div><span><span>      </span></span><span>kl_real </span><span>*</span><span><span>S</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>L</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>HP</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>Kt</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>K</span><span>)</span></span></div></div><div><div><div>115</div></div><div><span>{</span></div></div><div><div><div>116</div></div><div><span>  </span><span>kl_mat_mul</span><span>(H, P, HP, m, n, n);</span></div></div><div><div><div>117</div></div><div><span>  </span><span>kl_mat_mul_bt</span><span>(HP, H, L, m, n, m);</span></div></div><div><div><div>118</div></div><div><span>  </span><span>kl_mat_add</span><span>(L, R, S, m, m);</span></div></div><div><div><div>119</div></div><div><span>  </span><span>kl_mat_copy</span><span>(S, L, m, m);</span></div></div><div><div><div>120</div></div><div><span>  </span><span>if</span><span> (</span><span>kl_chol</span><span>(L, m) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>121</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>122</div></div><div><span>  </span><span>kl_mat_copy</span><span>(HP, Kt, m, n);</span></div></div><div><div><div>123</div></div><div><span>  </span><span>kl_chol_solve</span><span>(L, Kt, m, n);</span></div></div><div><div><div>124</div></div><div><span>  </span><span>kl_mat_transpose</span><span>(Kt, K, m, n);</span></div></div><div><div><div>125</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>126</div></div><div><span>}</span></div></div><div><div><div>127</div></div><div>
</div></div><div><div><div>128</div></div><div><span>int</span><span> </span><span>kl_kf_steady_gain</span><span>(</span><span>const</span><span> kl_real </span><span>*</span><span><span>F</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>H</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>Q</span><span>,</span><span> </span></span><span>const</span><span> kl_real </span><span>*</span><span><span>R</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span></span></div></div><div><div><div>129</div></div><div><span>          </span><span>int</span><span><span> </span><span>m</span><span>,</span><span> kl_real </span><span>tol</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>max_iter</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>K_out</span><span>,</span><span> kl_real </span></span><span>*</span><span><span>P_out</span><span>,</span></span></div></div><div><div><div>130</div></div><div><span><span>          </span></span><span>kl_real </span><span>*</span><span><span>work</span><span>)</span></span></div></div><div><div><div>131</div></div><div><span>{</span></div></div><div><div><div>132</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>P </span><span>=</span><span> work;</span></div></div><div><div><div>133</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>T </span><span>=</span><span> P </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>134</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>U </span><span>=</span><span> T </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>135</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>V </span><span>=</span><span> U </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>136</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>S </span><span>=</span><span> V </span><span>+</span><span> n </span><span>*</span><span> n;</span></div></div><div><div><div>137</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>L </span><span>=</span><span> S </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>138</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>HP </span><span>=</span><span> L </span><span>+</span><span> m </span><span>*</span><span> m;</span></div></div><div><div><div>139</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>Kt </span><span>=</span><span> HP </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>140</div></div><div><span><span>  </span></span><span>kl_real </span><span>*</span><span>K </span><span>=</span><span> Kt </span><span>+</span><span> m </span><span>*</span><span> n;</span></div></div><div><div><div>141</div></div><div>
</div></div><div><div><div>142</div></div><div><span>  </span><span>kl_mat_copy</span><span>(Q, P, n, n);</span></div></div><div><div><div>143</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> it </span><span>=</span><span> </span><span>0</span><span>; it </span><span>&lt;</span><span> max_iter; it</span><span>++</span><span>) {</span></div></div><div><div><div>144</div></div><div><span>    </span><span>if</span><span> (</span><span>riccati_gain</span><span>(H, R, P, n, m, S, L, HP, Kt, K) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>145</div></div><div><span>      </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>146</div></div><div><span><span>    </span></span><span>/* Pn = F (P - K H P) F^T + Q */</span></div></div><div><div><div>147</div></div><div><span>    </span><span>kl_mat_mul</span><span>(K, HP, T, n, m, n);</span></div></div><div><div><div>148</div></div><div><span>    </span><span>kl_mat_sub</span><span>(P, T, U, n, n);</span></div></div><div><div><div>149</div></div><div><span>    </span><span>kl_mat_mul</span><span>(F, U, T, n, n, n);</span></div></div><div><div><div>150</div></div><div><span>    </span><span>kl_mat_mul_bt</span><span>(T, F, U, n, n, n);</span></div></div><div><div><div>151</div></div><div><span>    </span><span>kl_mat_add</span><span>(U, Q, V, n, n);</span></div></div><div><div><div>152</div></div><div>
</div></div><div><div><div>153</div></div><div><span><span>    </span></span><span>kl_real dmax </span><span>=</span><span> </span><span>0</span><span>, pmax </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>154</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n </span><span>*</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>155</div></div><div><span><span>      </span></span><span>kl_real d </span><span>=</span><span> </span><span>KL_FABS</span><span>(</span><span>V</span><span>[i] </span><span>-</span><span> </span><span>P</span><span>[i]), a </span><span>=</span><span> </span><span>KL_FABS</span><span>(</span><span>V</span><span>[i]);</span></div></div><div><div><div>156</div></div><div><span><span>      </span></span><span>dmax </span><span>=</span><span> d </span><span>&gt;</span><span> dmax </span><span>?</span><span> d </span><span>:</span><span> dmax;</span></div></div><div><div><div>157</div></div><div><span><span>      </span></span><span>pmax </span><span>=</span><span> a </span><span>&gt;</span><span> pmax </span><span>?</span><span> a </span><span>:</span><span> pmax;</span></div></div><div><div><div>158</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>159</div></div><div><span>    </span><span>kl_mat_copy</span><span>(V, P, n, n);</span></div></div><div><div><div>160</div></div><div><span>    </span><span>if</span><span> (dmax </span><span>&lt;=</span><span> tol </span><span>*</span><span> pmax) {</span></div></div><div><div><div>161</div></div><div><span>      </span><span>if</span><span> (</span><span>riccati_gain</span><span>(H, R, P, n, m, S, L, HP, Kt, K) </span><span>!=</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>162</div></div><div><span>        </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>163</div></div><div><span>      </span><span>kl_mat_copy</span><span>(K, K_out, n, m);</span></div></div><div><div><div>164</div></div><div><span>      </span><span>if</span><span> (P_out)</span></div></div><div><div><div>165</div></div><div><span>        </span><span>kl_mat_copy</span><span>(P, P_out, n, n);</span></div></div><div><div><div>166</div></div><div><span>      </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>167</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>168</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>169</div></div><div><span>  </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>170</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><p>C++ 把 <code>Mat</code> 做成带尺寸模板参数的值类型，运算符重载之后，滤波器的代码看起来就是公式：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>// x- = F x + B u,  P- = F P F^T + Q</span></div></div><div><div><div>2</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>U</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>u</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>B</span><span> </span><span>*</span><span> </span><span>u</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>// For models without a control input (U == 0).</span></div></div><div><div><div>8</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>() {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>static_assert</span><span>(</span><span>U</span><span> </span><span>==</span><span> </span><span>0</span><span>, </span><span>"this model has a control input"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>// Joseph-form update. Returns false, leaving x and P unchanged, if S is not</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>// positive definite.</span></div></div><div><div><div>16</div></div><div><span>  </span><span>bool</span><span> </span><span>update</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>) {</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>K</span><span>;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>gain</span><span>(</span><span>z</span><span>, </span><span>K</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>19</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>A</span><span> </span><span>=</span><span> Mat&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>H</span><span>;</span></div></div><div><div><div>20</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> (</span><span>A</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>A</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>R</span><span> </span><span>*</span><span> </span><span>K</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>21</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>私有的 <code>gain</code> 是两种更新共用的部分：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>// Shared part of both updates: y, S, K from a Cholesky solve, then x += K y.</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>// Leaves everything unchanged if S is not positive definite.</span></div></div><div><div><div>4</div></div><div><span>  </span><span>bool</span><span> </span><span>gain</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>K</span><span>) {</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>y_new</span><span> </span><span>=</span><span> </span><span>z</span><span> </span><span>-</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>HP</span><span> </span><span>=</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>HP</span><span> </span><span>*</span><span> </span><span>H</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>R</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>S_new</span><span> </span><span>=</span><span> </span><span>L</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>y</span><span> </span><span>=</span><span> </span><span>y_new</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>S</span><span> </span><span>=</span><span> </span><span>S_new</span><span>;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>// K^T = S^-1 (H P), solved with the Cholesky factor, no explicit inverse</span></div></div><div><div><div>14</div></div><div><span>    </span><span>K</span><span> </span><span>=</span><span> </span><span>chol_solve</span><span>(</span><span>L</span><span>, </span><span>HP</span><span>).</span><span>transpose</span><span>();</span></div></div><div><div><div>15</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>F * P * F.transpose() + Q</code> 对应 <span><span>FPFT+QFPF^T + Q</span><span><span><span></span><span>F</span><span>P</span><span><span>F</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>Q</span></span></span></span>，没有一行索引。类型系统在检查维度：<code>H * P</code> 是 <code>Mat&lt;M,N&gt; * Mat&lt;N,N&gt;</code>，<code>HP * H.transpose()</code> 是 <code>Mat&lt;M,N&gt; * Mat&lt;N,M&gt;</code>，结果是 <code>Mat&lt;M,M&gt;</code>，再加 <code>R</code>。运算符返回新值，编译器的返回值优化和内联会消掉大部分拷贝；这个设计比 C 版省心，代价是我没法精确控制临时对象，有性能要求的话得看汇编。</p><p><code>update</code> 和第 3 篇的 <code>update_short</code> 共用私有的 <code>gain</code>：它先在 <code>L</code> 里算 <span><span>SS</span><span><span><span></span><span>S</span></span></span></span>，再原地 Cholesky，失败时返回 <code>false</code>，此时 <code>x</code>、<code>P</code>、<code>y</code>、<code>S</code> 都还是旧值。<code>cholesky</code> 是 <code>constexpr</code>：<code>Mat</code> 的所有运算都是，所以编译期就能算小矩阵，我没有用到这一点，只是顺手成立。</p>
完整源码：cpp/include/kl/mat.hpp<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Small fixed-size row-major matrices. No heap: storage is a std::array.</span></div></div><div><div><div>2</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;array&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;cmath&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;cstddef&gt;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>R</span><span>, std::</span><span>size_t</span><span> </span><span>C</span><span>, </span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>10</div></div><div><span>struct</span><span> </span><span>Mat</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>std::</span><span>array</span><span>&lt;</span><span>T</span><span>, </span><span>R</span><span> </span><span>*</span><span> </span><span>C</span><span>&gt; </span><span>a</span><span>{};</span><span>  // row-major, zero-initialized</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>  </span><span>constexpr</span><span> </span><span>T</span><span>&amp;</span><span> </span><span>operator</span><span>()(std::</span><span>size_t</span><span><span> </span><span>i</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>j</span><span>) { </span></span><span>return</span><span> </span><span>a</span><span>[</span><span>i</span><span> </span><span>*</span><span> </span><span>C</span><span> </span><span>+</span><span> </span><span>j</span><span>]; }</span></div></div><div><div><div>14</div></div><div><span>  </span><span>constexpr</span><span> </span><span>const</span><span> </span><span>T</span><span>&amp;</span><span> </span><span>operator</span><span>()(std::</span><span>size_t</span><span><span> </span><span>i</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>j</span><span>) </span></span><span>const</span><span> { </span><span>return</span><span> </span><span>a</span><span>[</span><span>i</span><span> </span><span>*</span><span> </span><span>C</span><span> </span><span>+</span><span> </span><span>j</span><span>]; }</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>Mat</span><span> </span><span>zeros</span><span>() { </span><span>return</span><span> </span><span>Mat</span><span>{}; }</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>  </span><span>static</span><span> </span><span>constexpr</span><span> </span><span>Mat</span><span> </span><span>identity</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>static_assert</span><span>(</span><span>R</span><span> </span><span>==</span><span> </span><span>C</span><span>, </span><span>"identity needs a square matrix"</span><span>);</span></div></div><div><div><div>20</div></div><div><span>    </span><span>Mat</span><span> </span><span>m</span><span>{};</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>R</span><span>; </span><span>i</span><span>++</span><span>) </span><span>m</span><span>(</span><span>i</span><span>, </span><span>i</span><span>) </span><span>=</span><span> </span><span>T</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span> </span><span>m</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Mat</span><span>&lt;</span><span>C</span><span>, </span><span>R</span><span>, </span><span>T</span><span>&gt; </span><span>transpose</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>C</span><span>, </span><span>R</span><span>, </span><span>T</span><span>&gt; </span><span>t</span><span>{};</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>R</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>28</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>C</span><span>; </span><span>j</span><span>++</span><span>) </span><span>t</span><span>(</span><span>j</span><span>, </span><span>i</span><span>) </span><span>=</span><span> (</span><span>*</span><span>this</span><span>)(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>29</div></div><div><span>    </span><span>return</span><span> </span><span>t</span><span>;</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>// (A + A^T) / 2</span></div></div><div><div><div>33</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Mat</span><span> </span><span>symmetrized</span><span>() </span><span>const</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>static_assert</span><span>(</span><span>R</span><span> </span><span>==</span><span> </span><span>C</span><span>, </span><span>"symmetrized needs a square matrix"</span><span>);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>Mat</span><span> </span><span>s</span><span>{};</span></div></div><div><div><div>36</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>R</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>37</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>C</span><span>; </span><span>j</span><span>++</span><span>) </span><span>s</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>T</span><span>(</span><span>0.5</span><span>) </span><span>*</span><span> ((</span><span>*</span><span>this</span><span>)(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>+</span><span> (</span><span>*</span><span>this</span><span>)(</span><span>j</span><span>, </span><span>i</span><span>));</span></div></div><div><div><div>38</div></div><div><span>    </span><span>return</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Mat</span><span> </span><span>operator</span><span>+(</span><span>const</span><span> </span><span>Mat</span><span>&amp;</span><span><span> </span><span>o</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>42</div></div><div><span>    </span><span>Mat</span><span> </span><span>r</span><span>{};</span></div></div><div><div><div>43</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>R</span><span> </span><span>*</span><span> </span><span>C</span><span>; </span><span>i</span><span>++</span><span>) </span><span>r</span><span>.</span><span>a</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>i</span><span>] </span><span>+</span><span> </span><span>o</span><span>.</span><span>a</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>44</div></div><div><span>    </span><span>return</span><span> </span><span>r</span><span>;</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Mat</span><span> </span><span>operator</span><span>-(</span><span>const</span><span> </span><span>Mat</span><span>&amp;</span><span><span> </span><span>o</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>48</div></div><div><span>    </span><span>Mat</span><span> </span><span>r</span><span>{};</span></div></div><div><div><div>49</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>R</span><span> </span><span>*</span><span> </span><span>C</span><span>; </span><span>i</span><span>++</span><span>) </span><span>r</span><span>.</span><span>a</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>i</span><span>] </span><span>-</span><span> </span><span>o</span><span>.</span><span>a</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>50</div></div><div><span>    </span><span>return</span><span> </span><span>r</span><span>;</span></div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Mat</span><span> </span><span>operator</span><span>*(</span><span>T</span><span><span> </span><span>s</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>54</div></div><div><span>    </span><span>Mat</span><span> </span><span>r</span><span>{};</span></div></div><div><div><div>55</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>R</span><span> </span><span>*</span><span> </span><span>C</span><span>; </span><span>i</span><span>++</span><span>) </span><span>r</span><span>.</span><span>a</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>a</span><span>[</span><span>i</span><span>] </span><span>*</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>56</div></div><div><span>    </span><span>return</span><span> </span><span>r</span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>  </span><span>friend</span><span> </span><span>constexpr</span><span> </span><span>Mat</span><span> </span><span>operator</span><span>*(</span><span>T</span><span><span> </span><span>s</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&amp;</span><span><span> </span><span>m</span><span>) { </span></span><span>return</span><span> </span><span>m</span><span> </span><span>*</span><span> </span><span>s</span><span>; }</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>// (R x C) * (C x K) -&gt; (R x K)</span></div></div><div><div><div>62</div></div><div><span>  </span><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>K</span><span>&gt;</span></div></div><div><div><div>63</div></div><div><span>  </span><span>constexpr</span><span> </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>K</span><span>, </span><span>T</span><span>&gt; </span><span>operator</span><span>*(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>C</span><span>, </span><span>K</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>o</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>64</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>K</span><span>, </span><span>T</span><span>&gt; </span><span>r</span><span>{};</span></div></div><div><div><div>65</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>R</span><span>; </span><span>i</span><span>++</span><span>)</span></div></div><div><div><div>66</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>67</div></div><div><span>        </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>68</div></div><div><span>        </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>C</span><span>; </span><span>k</span><span>++</span><span>) </span><span>s</span><span> </span><span>+=</span><span> (</span><span>*</span><span>this</span><span>)(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>o</span><span>(</span><span>k</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>69</div></div><div><span>        </span><span>r</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>70</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>71</div></div><div><span>    </span><span>return</span><span> </span><span>r</span><span>;</span></div></div><div><div><div>72</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>73</div></div><div><span>};</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>// In-place lower Cholesky, A = L L^T. Only the lower triangle of A is read; on</span></div></div><div><div><div>76</div></div><div><span>// success it holds L and the strict upper triangle is left untouched.</span></div></div><div><div><div>77</div></div><div><span>// Returns false (A partially overwritten) if a pivot is &lt;= 0.</span></div></div><div><div><div>78</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>79</div></div><div><span>constexpr</span><span> </span><span>bool</span><span> </span><span>cholesky</span><span>(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>A</span><span>) {</span></span></div></div><div><div><div>80</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>j</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>j</span><span>++</span><span>) {</span></div></div><div><div><div>81</div></div><div><span>    </span><span>T</span><span> </span><span>d</span><span> </span><span>=</span><span> </span><span>A</span><span>(</span><span>j</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>82</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>k</span><span>++</span><span>) </span><span>d</span><span> </span><span>-=</span><span> </span><span>A</span><span>(</span><span>j</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>A</span><span>(</span><span>j</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>83</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>(</span><span>d</span><span> </span><span>&gt;</span><span> </span><span>T</span><span>(</span><span>0</span><span>))) </span><span>return</span><span> </span><span>false</span><span>;</span><span>  // also rejects NaN</span></div></div><div><div><div>84</div></div><div><span>    </span><span>const</span><span> </span><span>T</span><span> </span><span>l</span><span> </span><span>=</span><span> std::</span><span>sqrt</span><span>(</span><span>d</span><span>);</span></div></div><div><div><div>85</div></div><div><span>    </span><span>A</span><span>(</span><span>j</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>l</span><span>;</span></div></div><div><div><div>86</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>j</span><span> </span><span>+</span><span> </span><span>1</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>87</div></div><div><span>      </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>A</span><span>(</span><span>i</span><span>, </span><span>j</span><span>);</span></div></div><div><div><div>88</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>j</span><span>; </span><span>k</span><span>++</span><span>) </span><span>s</span><span> </span><span>-=</span><span> </span><span>A</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>A</span><span>(</span><span>j</span><span>, </span><span>k</span><span>);</span></div></div><div><div><div>89</div></div><div><span>      </span><span>A</span><span>(</span><span>i</span><span>, </span><span>j</span><span>) </span><span>=</span><span> </span><span>s</span><span> </span><span>/</span><span> </span><span>l</span><span>;</span></div></div><div><div><div>90</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>91</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>92</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>93</div></div><div><span>}</span></div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span>// Solves (L L^T) X = B for every column of B by forward then backward substitution.</span></div></div><div><div><div>96</div></div><div><span>// L is the lower factor from cholesky(); its upper triangle is ignored.</span></div></div><div><div><div>97</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, std::</span><span>size_t</span><span> </span><span>K</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>98</div></div><div><span>constexpr</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>K</span><span>, </span><span>T</span><span>&gt; </span><span>chol_solve</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>L</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>K</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>B</span><span>) {</span></span></div></div><div><div><div>99</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>K</span><span>, </span><span>T</span><span>&gt; </span><span>X</span><span> </span><span>=</span><span> </span><span>B</span><span>;</span></div></div><div><div><div>100</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>c</span><span> </span><span>&lt;</span><span> </span><span>K</span><span>; </span><span>c</span><span>++</span><span>) {</span></div></div><div><div><div>101</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) {</span><span>  // L z = b</span></div></div><div><div><div>102</div></div><div><span>      </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c</span><span>);</span></div></div><div><div><div>103</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>i</span><span>; </span><span>k</span><span>++</span><span>) </span><span>s</span><span> </span><span>-=</span><span> </span><span>L</span><span>(</span><span>i</span><span>, </span><span>k</span><span>) </span><span>*</span><span> </span><span>X</span><span>(</span><span>k</span><span>, </span><span>c</span><span>);</span></div></div><div><div><div>104</div></div><div><span>      </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c</span><span>) </span><span>=</span><span> </span><span>s</span><span> </span><span>/</span><span> </span><span>L</span><span>(</span><span>i</span><span>, </span><span>i</span><span>);</span></div></div><div><div><div>105</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>106</div></div><div><span>    </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>N</span><span>; </span><span>i</span><span>--</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>;) {</span><span>  // L^T x = z</span></div></div><div><div><div>107</div></div><div><span>      </span><span>T</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c</span><span>);</span></div></div><div><div><div>108</div></div><div><span>      </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>k</span><span> </span><span>=</span><span> </span><span>i</span><span> </span><span>+</span><span> </span><span>1</span><span>; </span><span>k</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>k</span><span>++</span><span>) </span><span>s</span><span> </span><span>-=</span><span> </span><span>L</span><span>(</span><span>k</span><span>, </span><span>i</span><span>) </span><span>*</span><span> </span><span>X</span><span>(</span><span>k</span><span>, </span><span>c</span><span>);</span></div></div><div><div><div>109</div></div><div><span>      </span><span>X</span><span>(</span><span>i</span><span>, </span><span>c</span><span>) </span><span>=</span><span> </span><span>s</span><span> </span><span>/</span><span> </span><span>L</span><span>(</span><span>i</span><span>, </span><span>i</span><span>);</span></div></div><div><div><div>110</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>111</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>112</div></div><div><span>  </span><span>return</span><span> </span><span>X</span><span>;</span></div></div><div><div><div>113</div></div><div><span>}</span></div></div><div><div><div>114</div></div><div>
</div></div><div><div><div>115</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：cpp/include/kl/kf.hpp（文件里还有第 3 篇才讲的 `update_short` 和稳态增益）<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Generic linear Kalman filter: N states, M measurements, U control inputs (may be 0).</span></div></div><div><div><div>2</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"kl/mat.hpp"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, std::</span><span>size_t</span><span> </span><span>M</span><span>, std::</span><span>size_t</span><span> </span><span>U</span><span>, </span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>8</div></div><div><span>class</span><span> </span><span>KalmanFilter</span><span> {</span></div></div><div><div><div>9</div></div><div><span>public:</span></div></div><div><div><div>10</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>x</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>P</span><span>, </span><span>F</span><span>, </span><span>Q</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>U</span><span>, </span><span>T</span><span>&gt; </span><span>B</span><span>;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>H</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>R</span><span>;</span></div></div><div><div><div>15</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>y</span><span>;</span><span>  // innovation of the last update</span></div></div><div><div><div>16</div></div><div><span>  </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>S</span><span>;</span><span>  // its covariance</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>// x- = F x + B u,  P- = F P F^T + Q</span></div></div><div><div><div>19</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>U</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>u</span><span>) {</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>B</span><span> </span><span>*</span><span> </span><span>u</span><span>;</span></div></div><div><div><div>21</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>// For models without a control input (U == 0).</span></div></div><div><div><div>25</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>() {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>static_assert</span><span>(</span><span>U</span><span> </span><span>==</span><span> </span><span>0</span><span>, </span><span>"this model has a control input"</span><span>);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>28</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>// Joseph-form update. Returns false, leaving x and P unchanged, if S is not</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>// positive definite.</span></div></div><div><div><div>33</div></div><div><span>  </span><span>bool</span><span> </span><span>update</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>) {</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>K</span><span>;</span></div></div><div><div><div>35</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>gain</span><span>(</span><span>z</span><span>, </span><span>K</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>36</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>A</span><span> </span><span>=</span><span> Mat&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>H</span><span>;</span></div></div><div><div><div>37</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> (</span><span>A</span><span> </span><span>*</span><span> </span><span>P</span><span> </span><span>*</span><span> </span><span>A</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>R</span><span> </span><span>*</span><span> </span><span>K</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>38</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>// Textbook short form P = (I - K H) P-: cheaper, but neither symmetry nor</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>// positive definiteness is preserved. Same failure behavior as update().</span></div></div><div><div><div>43</div></div><div><span>  </span><span>bool</span><span> </span><span>update_short</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>) {</span></span></div></div><div><div><div>44</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>K</span><span>;</span></div></div><div><div><div>45</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>gain</span><span>(</span><span>z</span><span>, </span><span>K</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>46</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span><span> (</span><span>Mat</span><span>&lt;</span></span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;::</span><span>identity</span><span>() </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>H</span><span>) </span><span>*</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>47</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>private:</span></div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>// Shared part of both updates: y, S, K from a Cholesky solve, then x += K y.</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>// Leaves everything unchanged if S is not positive definite.</span></div></div><div><div><div>53</div></div><div><span>  </span><span>bool</span><span> </span><span>gain</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>z</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>K</span><span>) {</span></span></div></div><div><div><div>54</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>, </span><span>T</span><span>&gt; </span><span>y_new</span><span> </span><span>=</span><span> </span><span>z</span><span> </span><span>-</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>55</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>HP</span><span> </span><span>=</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>P</span><span>;</span></div></div><div><div><div>56</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>HP</span><span> </span><span>*</span><span> </span><span>H</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>R</span><span>;</span></div></div><div><div><div>57</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>S_new</span><span> </span><span>=</span><span> </span><span>L</span><span>;</span></div></div><div><div><div>58</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>59</div></div><div><span>    </span><span>y</span><span> </span><span>=</span><span> </span><span>y_new</span><span>;</span></div></div><div><div><div>60</div></div><div><span>    </span><span>S</span><span> </span><span>=</span><span> </span><span>S_new</span><span>;</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>// K^T = S^-1 (H P), solved with the Cholesky factor, no explicit inverse</span></div></div><div><div><div>63</div></div><div><span>    </span><span>K</span><span> </span><span>=</span><span> </span><span>chol_solve</span><span>(</span><span>L</span><span>, </span><span>HP</span><span>).</span><span>transpose</span><span>();</span></div></div><div><div><div>64</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>65</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>66</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>67</div></div><div><span>};</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span>// Steady-state gain: iterate the Riccati recursion on the prior covariance until</span></div></div><div><div><div>70</div></div><div><span>// max|dP| &lt;= tol * max|P|. Returns false if it does not converge or S is not SPD.</span></div></div><div><div><div>71</div></div><div><span>// K is the gain for the converged prior P.</span></div></div><div><div><div>72</div></div><div><span>template</span><span> &lt;std::</span><span>size_t</span><span> </span><span>N</span><span>, std::</span><span>size_t</span><span> </span><span>M</span><span>, </span><span>class</span><span> </span><span>T</span><span>&gt;</span></div></div><div><div><div>73</div></div><div><span>bool</span><span> </span><span>steady_state_gain</span><span>(</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>F</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>H</span><span>, </span></span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Q</span><span>,</span></span></div></div><div><div><div>74</div></div><div><span>           </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>R</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>K</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>P</span><span>, </span></span><span>T</span><span><span> </span><span>tol</span><span> </span></span><span>=</span><span> </span><span>T</span><span>(</span><span><span>1</span><span>e</span></span><span>-</span><span>12</span><span>),</span></div></div><div><div><div>75</div></div><div><span>           </span><span>int</span><span><span> </span><span>max_iter</span><span> </span></span><span>=</span><span> </span><span>100000</span><span>) {</span></div></div><div><div><div>76</div></div><div><span>  </span><span>auto</span><span> </span><span>gain</span><span> </span><span>=</span><span> [</span><span>&amp;</span><span>](</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Pp</span><span>, </span></span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>Kp</span><span>) {</span></span></div></div><div><div><div>77</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>HP</span><span> </span><span>=</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>Pp</span><span>;</span></div></div><div><div><div>78</div></div><div><span>    </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>, </span><span>T</span><span>&gt; </span><span>L</span><span> </span><span>=</span><span> </span><span>HP</span><span> </span><span>*</span><span> </span><span>H</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>R</span><span>;</span></div></div><div><div><div>79</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>cholesky</span><span>(</span><span>L</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>80</div></div><div><span>    </span><span>Kp</span><span> </span><span>=</span><span> </span><span>chol_solve</span><span>(</span><span>L</span><span>, </span><span>HP</span><span>).</span><span>transpose</span><span>();</span></div></div><div><div><div>81</div></div><div><span>    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>83</div></div><div><span>  </span><span>auto</span><span> </span><span>max_abs</span><span> </span><span>=</span><span> [](</span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>m</span><span>) {</span></span></div></div><div><div><div>84</div></div><div><span>    </span><span>T</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>85</div></div><div><span>    </span><span>for</span><span> (</span><span>const</span><span> </span><span>T</span><span> </span><span>v</span><span> : </span><span>m</span><span>.</span><span>a</span><span>) </span><span>r</span><span> </span><span>=</span><span> std::</span><span>fmax</span><span>(</span><span>r</span><span>, std::</span><span>fabs</span><span>(</span><span>v</span><span>));</span></div></div><div><div><div>86</div></div><div><span>    </span><span>return</span><span> </span><span>r</span><span>;</span></div></div><div><div><div>87</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>88</div></div><div><span>  </span><span>P</span><span> </span><span>=</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>89</div></div><div><span>  </span><span>bool</span><span> </span><span>converged</span><span> </span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>90</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>it</span><span> </span><span>&lt;</span><span> </span><span>max_iter</span><span> </span><span>&amp;&amp;</span><span> </span><span>!</span><span>converged</span><span>; </span><span>it</span><span>++</span><span>) {</span></div></div><div><div><div>91</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>gain</span><span>(</span><span>P</span><span>, </span><span>K</span><span>)) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>92</div></div><div><span>    </span><span>const</span><span> </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>, </span><span>T</span><span>&gt; </span><span>Pn</span><span> </span><span>=</span><span> </span><span>F</span><span> </span><span>*</span><span> (</span><span>P</span><span> </span><span>-</span><span> </span><span>K</span><span> </span><span>*</span><span> </span><span>H</span><span> </span><span>*</span><span> </span><span>P</span><span>) </span><span>*</span><span> </span><span>F</span><span>.</span><span>transpose</span><span>() </span><span>+</span><span> </span><span>Q</span><span>;</span></div></div><div><div><div>93</div></div><div><span>    </span><span>converged</span><span> </span><span>=</span><span> </span><span>max_abs</span><span>(</span><span>Pn</span><span> </span><span>-</span><span> </span><span>P</span><span>) </span><span>&lt;=</span><span> </span><span>tol</span><span> </span><span>*</span><span> </span><span>max_abs</span><span>(</span><span>Pn</span><span>);</span></div></div><div><div><div>94</div></div><div><span>    </span><span>P</span><span> </span><span>=</span><span> </span><span>Pn</span><span>;</span></div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>96</div></div><div><span>  </span><span>return</span><span> </span><span>converged</span><span> </span><span>&amp;&amp;</span><span> </span><span>gain</span><span>(</span><span>P</span><span>, </span><span>K</span><span>);</span></div></div><div><div><div>97</div></div><div><span>}</span></div></div><div><div><div>98</div></div><div>
</div></div><div><div><div>99</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// x- = F x + B u,  P- = F P F^T + Q</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>u</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>U</span><span>, </span><span>1</span><span>&gt;) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span>.f * </span><span>self</span><span>.x + </span><span>self</span><span>.b * *</span><span>u</span><span>;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>self</span><span>.f * </span><span>self</span><span>.p * </span><span>self</span><span>.f.</span><span>transpose</span><span>() + </span><span>self</span><span>.q;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>/// Innovation y, covariance S and gain K = (S^-1 H P)^T, shared by both updates.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[allow(clippy</span><span>::</span><span>type_complexity)]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>gain</span><span>(&amp;</span><span>self</span><span>, </span><span>z</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;), </span><span>CholeskyError</span><span>&gt; {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> *</span></span><span>z</span><span> - </span><span>self</span><span>.h * </span><span>self</span><span>.x;</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>hp</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.h * </span><span>self</span><span>.p;</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>hp</span><span> * </span><span>self</span><span>.h.</span><span>transpose</span><span>() + </span><span>self</span><span>.r;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>l</span><span><span> </span><span>=</span><span> </span></span><span>s</span><span>.</span><span>cholesky</span><span>().</span><span>ok_or</span><span>(</span><span>CholeskyError</span><span>)?;</span></div></div><div><div><div>14</div></div><div><span>        </span><span>// K^T = S^-1 (H P), solved without forming S^-1</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Ok</span><span>((</span><span>y</span><span>, </span><span>s</span><span>, </span><span>chol_solve</span><span>(&amp;</span><span>l</span><span>, &amp;</span><span>hp</span><span>).</span><span>transpose</span><span>()))</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>/// Joseph-form update. On Cholesky failure the state is left unchanged.</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;(), </span><span>CholeskyError</span><span>&gt; {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> (</span><span>y</span><span>, </span><span>s</span><span>, </span><span>k</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>gain</span><span>(</span><span>z</span><span>)?;</span></div></div><div><div><div>21</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span>.x + </span><span>k</span><span> * </span><span>y</span><span>;</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>() - </span><span>k</span><span> * </span><span>self</span><span>.h;</span></div></div><div><div><div>23</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> (</span></span><span>a</span><span> * </span><span>self</span><span>.p * </span><span>a</span><span>.</span><span>transpose</span><span>() + </span><span>k</span><span> * </span><span>self</span><span>.r * </span><span>k</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>24</div></div><div><span>        </span><span>self</span><span><span>.y </span><span>=</span><span> </span></span><span>y</span><span>;</span></div></div><div><div><div>25</div></div><div><span>        </span><span>self</span><span><span>.s </span><span>=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>26</div></div><div><span>        </span><span>Ok</span><span>(())</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>和 C++ 的结构几乎一样：<code>Mat&lt;R, C&gt;</code> 是 <code>[[Real; C]; R]</code> 的包装，<code>Mul</code>、<code>Add</code>、<code>Sub</code> 通过 trait 实现，const 泛型保证维度对得上。三点差别：</p><ul>
<li><code>gain</code> 里的 <code>s.cholesky().ok_or(CholeskyError)?</code>：<code>cholesky</code> 返回 <code>Option&lt;Mat&gt;</code>，<code>ok_or</code> 把 <code>None</code> 变成错误，<code>?</code> 在错误时提前返回。<code>gain</code> 只读 <code>self</code>、返回元组 <code>(y, s, k)</code>，<code>update</code> 拿到之后才写 <code>self.x</code>、<code>self.p</code>，所以失败时状态不变，这是 Rust 的 <code>Result</code> 风格直接给出的。</li>
<li><code>Mat</code> 派生了 <code>Copy</code>，所以 <code>self.f * self.x</code> 不会”移动走” <code>self.f</code>。矩阵是小数组，拷贝就是 <code>memcpy</code>。</li>
<li>无控制输入的模型用 <code>Mat&lt;4, 0&gt;</code>，零长度数组在 const 泛型里是合法的。</li>
</ul>
完整源码：rust/src/mat.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Small fixed-size row-major matrices. No heap, `no_std`-compatible.</span></div></div><div><div><div>2</div></div><div><span>#![allow(clippy</span><span>::</span><span>needless_range_loop)]</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, sqrt};</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>ops</span><span>::{</span></span><span>Add</span><span>, </span><span>Mul</span><span>, </span><span>Sub</span><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>8</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Mat</span><span>&lt;</span><span>const</span><span> </span><span>R</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>C</span><span>: </span><span>usize</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>m</span><span>: [[</span><span>Real</span><span>; </span><span>C</span><span>]; </span><span>R</span><span>],</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>R</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>C</span><span>: </span><span>usize</span><span>&gt; </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>C</span><span>&gt; {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>zeros</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Self</span><span> { </span><span>m</span><span>: [[</span><span>0.0</span><span>; </span><span>C</span><span>]; </span><span>R</span><span>] }</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>transpose</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Mat</span><span>&lt;</span><span>C</span><span>, </span><span>R</span><span>&gt; {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>t</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>C</span><span>, </span><span>R</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>19</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>R</span><span> {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>C</span><span> {</span></div></div><div><div><div>21</div></div><div><span>                </span><span>t</span><span>.m[</span><span>j</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>self</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>        </span><span>t</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt; </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt; {</span></div></div><div><div><div>29</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>identity</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Self</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>31</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>32</div></div><div><span>            </span><span>a</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span><span>] </span><span>=</span><span> </span></span><span>1.0</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>        </span><span>a</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>/// (A + A^T) / 2</span></div></div><div><div><div>38</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>symmetrized</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>39</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>Self</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>40</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>41</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>42</div></div><div><span>                </span><span>s</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>0.5</span><span> * (</span><span>self</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>] + </span><span>self</span><span>.m[</span><span>j</span><span>][</span><span>i</span><span>]);</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>        </span><span>s</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>    </span><span>/// Lower Cholesky factor L with A = L L^T. Only the lower triangle of `self`</span></div></div><div><div><div>49</div></div><div><span>    </span><span>/// is read; the returned L has zeros above the diagonal.</span></div></div><div><div><div>50</div></div><div><span>    </span><span>/// Returns `None` if a pivot is not strictly positive.</span></div></div><div><div><div>51</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>cholesky</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Self</span><span>&gt; {</span></div></div><div><div><div>52</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>l</span><span><span> </span><span>=</span><span> </span></span><span>Self</span><span>::</span><span>zeros</span><span>();</span></div></div><div><div><div>53</div></div><div><span>        </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>54</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>d</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.m[</span><span>j</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>55</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>56</div></div><div><span>                </span><span>d</span><span><span> </span><span>-=</span><span> </span></span><span>l</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>] * </span><span>l</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>57</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>58</div></div><div><span>            </span><span>if</span><span> </span><span>d</span><span>.</span><span>is_nan</span><span><span>() </span><span>||</span><span> </span></span><span>d</span><span><span> </span><span>&lt;=</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>59</div></div><div><span>                </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>60</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>61</div></div><div><span>            </span><span>let</span><span> </span><span>ljj</span><span><span> </span><span>=</span><span> </span></span><span>sqrt</span><span>(</span><span>d</span><span>);</span></div></div><div><div><div>62</div></div><div><span>            </span><span>l</span><span>.m[</span><span>j</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>ljj</span><span>;</span></div></div><div><div><div>63</div></div><div><span>            </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>j</span><span> + </span><span>1</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>64</div></div><div><span>                </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>65</div></div><div><span>                </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>j</span><span> {</span></div></div><div><div><div>66</div></div><div><span>                    </span><span>s</span><span><span> </span><span>-=</span><span> </span></span><span>l</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>] * </span><span>l</span><span>.m[</span><span>j</span><span>][</span><span>k</span><span>];</span></div></div><div><div><div>67</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>68</div></div><div><span>                </span><span>l</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>s</span><span> / </span><span>ljj</span><span>;</span></div></div><div><div><div>69</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>70</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>71</div></div><div><span>        </span><span>Some</span><span>(</span><span>l</span><span>)</span></div></div><div><div><div>72</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>73</div></div><div><span>}</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>/// Solves (L L^T) X = B for X, given the lower Cholesky factor `l`,</span></div></div><div><div><div>76</div></div><div><span>/// by forward then backward substitution on every column of B.</span></div></div><div><div><div>77</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>chol_solve</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>K</span><span>: </span><span>usize</span><span>&gt;(</span><span>l</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;, </span><span>b</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>K</span><span>&gt;) -&gt; </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>K</span><span>&gt; {</span></div></div><div><div><div>78</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>x</span><span><span> </span><span>=</span><span> *</span></span><span>b</span><span>;</span></div></div><div><div><div>79</div></div><div><span>    </span><span>for</span><span> </span><span>c</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>K</span><span> {</span></div></div><div><div><div>80</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>81</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>x</span><span>.m[</span><span>i</span><span>][</span><span>c</span><span>];</span></div></div><div><div><div>82</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>i</span><span> {</span></div></div><div><div><div>83</div></div><div><span>                </span><span>s</span><span><span> </span><span>-=</span><span> </span></span><span>l</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>] * </span><span>x</span><span>.m[</span><span>k</span><span>][</span><span>c</span><span>];</span></div></div><div><div><div>84</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>85</div></div><div><span>            </span><span>x</span><span>.m[</span><span>i</span><span>][</span><span>c</span><span><span>] </span><span>=</span><span> </span></span><span>s</span><span> / </span><span>l</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span>];</span></div></div><div><div><div>86</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>87</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> (</span><span>0</span><span>..</span><span>N</span><span>).</span><span>rev</span><span>() {</span></div></div><div><div><div>88</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>x</span><span>.m[</span><span>i</span><span>][</span><span>c</span><span>];</span></div></div><div><div><div>89</div></div><div><span>            </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>i</span><span> + </span><span>1</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>90</div></div><div><span>                </span><span>s</span><span><span> </span><span>-=</span><span> </span></span><span>l</span><span>.m[</span><span>k</span><span>][</span><span>i</span><span>] * </span><span>x</span><span>.m[</span><span>k</span><span>][</span><span>c</span><span>];</span></div></div><div><div><div>91</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>92</div></div><div><span>            </span><span>x</span><span>.m[</span><span>i</span><span>][</span><span>c</span><span><span>] </span><span>=</span><span> </span></span><span>s</span><span> / </span><span>l</span><span>.m[</span><span>i</span><span>][</span><span>i</span><span>];</span></div></div><div><div><div>93</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>94</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>95</div></div><div><span>    </span><span>x</span></div></div><div><div><div>96</div></div><div><span>}</span></div></div><div><div><div>97</div></div><div>
</div></div><div><div><div>98</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>R</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>C</span><span>: </span><span>usize</span><span>&gt; </span><span>Add</span><span> </span><span>for</span><span> </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>C</span><span>&gt; {</span></div></div><div><div><div>99</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Self</span><span>;</span></div></div><div><div><div>100</div></div><div><span>    </span><span>fn</span><span> </span><span>add</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>o</span><span>: </span><span>Self</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>101</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>R</span><span> {</span></div></div><div><div><div>102</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>C</span><span> {</span></div></div><div><div><div>103</div></div><div><span>                </span><span>self</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>+=</span><span> </span></span><span>o</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>104</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>105</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>106</div></div><div><span>        </span><span>self</span></div></div><div><div><div>107</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>108</div></div><div><span>}</span></div></div><div><div><div>109</div></div><div>
</div></div><div><div><div>110</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>R</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>C</span><span>: </span><span>usize</span><span>&gt; </span><span>Sub</span><span> </span><span>for</span><span> </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>C</span><span>&gt; {</span></div></div><div><div><div>111</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Self</span><span>;</span></div></div><div><div><div>112</div></div><div><span>    </span><span>fn</span><span> </span><span>sub</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>o</span><span>: </span><span>Self</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>113</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>R</span><span> {</span></div></div><div><div><div>114</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>C</span><span> {</span></div></div><div><div><div>115</div></div><div><span>                </span><span>self</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>-=</span><span> </span></span><span>o</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>116</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>117</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>118</div></div><div><span>        </span><span>self</span></div></div><div><div><div>119</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>120</div></div><div><span>}</span></div></div><div><div><div>121</div></div><div>
</div></div><div><div><div>122</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>R</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>C</span><span>: </span><span>usize</span><span>&gt; </span><span>Mul</span><span>&lt;</span><span>Real</span><span>&gt; </span><span>for</span><span> </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>C</span><span>&gt; {</span></div></div><div><div><div>123</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Self</span><span>;</span></div></div><div><div><div>124</div></div><div><span>    </span><span>fn</span><span> </span><span>mul</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>s</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>125</div></div><div><span>        </span><span>for</span><span> </span><span>row</span><span> </span><span>in</span><span> </span><span>self</span><span>.m.</span><span>iter_mut</span><span>() {</span></div></div><div><div><div>126</div></div><div><span>            </span><span>for</span><span> </span><span>v</span><span> </span><span>in</span><span> </span><span>row</span><span>.</span><span>iter_mut</span><span>() {</span></div></div><div><div><div>127</div></div><div><span><span>                </span></span><span>*</span><span>v</span><span><span> </span><span>*=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>128</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>129</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>130</div></div><div><span>        </span><span>self</span></div></div><div><div><div>131</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>132</div></div><div><span>}</span></div></div><div><div><div>133</div></div><div>
</div></div><div><div><div>134</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>R</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>C</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>K</span><span>: </span><span>usize</span><span>&gt; </span><span>Mul</span><span>&lt;</span><span>Mat</span><span>&lt;</span><span>C</span><span>, </span><span>K</span><span>&gt;&gt; </span><span>for</span><span> </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>C</span><span>&gt; {</span></div></div><div><div><div>135</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>K</span><span>&gt;;</span></div></div><div><div><div>136</div></div><div><span>    </span><span>fn</span><span> </span><span>mul</span><span>(</span><span>self</span><span>, </span><span>o</span><span>: </span><span>Mat</span><span>&lt;</span><span>C</span><span>, </span><span>K</span><span>&gt;) -&gt; </span><span>Mat</span><span>&lt;</span><span>R</span><span>, </span><span>K</span><span>&gt; {</span></div></div><div><div><div>137</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>R</span><span>, </span><span>K</span><span>&gt;::</span><span>zeros</span><span>();</span></div></div><div><div><div>138</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>R</span><span> {</span></div></div><div><div><div>139</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>K</span><span> {</span></div></div><div><div><div>140</div></div><div><span>                </span><span>let</span><span> </span><span>mut</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>0.0</span><span>;</span></div></div><div><div><div>141</div></div><div><span>                </span><span>for</span><span> </span><span>k</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>C</span><span> {</span></div></div><div><div><div>142</div></div><div><span>                    </span><span>s</span><span><span> </span><span>+=</span><span> </span></span><span>self</span><span>.m[</span><span>i</span><span>][</span><span>k</span><span>] * </span><span>o</span><span>.m[</span><span>k</span><span>][</span><span>j</span><span>];</span></div></div><div><div><div>143</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>144</div></div><div><span>                </span><span>c</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span><span>] </span><span>=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>145</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>146</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>147</div></div><div><span>        </span><span>c</span></div></div><div><div><div>148</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>149</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
完整源码：rust/src/kf.rs（文件里还有第 3 篇才讲的 `update_short` 和稳态增益）<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Generic linear Kalman filter: N states, M measurements, U control inputs.</span></div></div><div><div><div>2</div></div><div><span>#![allow(clippy</span><span>::</span><span>needless_range_loop)]</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>mat</span><span>::{</span></span><span>Mat</span><span>, chol_solve};</span></div></div><div><div><div>5</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::{</span></span><span>Real</span><span>, fabs};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/// The innovation covariance S was not positive definite.</span></div></div><div><div><div>8</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>CholeskyError</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>KalmanFilter</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>U</span><span>: </span><span>usize</span><span>&gt; {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>pub</span><span> </span><span>x</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>p</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pub</span><span> </span><span>f</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>b</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>U</span><span>&gt;,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>h</span><span>: </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span> </span><span>q</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>18</div></div><div><span>    </span><span>pub</span><span> </span><span>r</span><span>: </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>/// Innovation and its covariance from the last successful `update`.</span></div></div><div><div><div>20</div></div><div><span>    </span><span>pub</span><span> </span><span>y</span><span>: </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>21</div></div><div><span>    </span><span>pub</span><span> </span><span>s</span><span>: </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>U</span><span>: </span><span>usize</span><span>&gt; </span><span>KalmanFilter</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>, </span><span>U</span><span>&gt; {</span></div></div><div><div><div>25</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span></div></div><div><div><div>26</div></div><div><span>        </span><span>f</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>27</div></div><div><span>        </span><span>b</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>U</span><span>&gt;,</span></div></div><div><div><div>28</div></div><div><span>        </span><span>h</span><span>: </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>29</div></div><div><span>        </span><span>q</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>30</div></div><div><span>        </span><span>r</span><span>: </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>31</div></div><div><span>        </span><span>x0</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>1</span><span>&gt;,</span></div></div><div><div><div>32</div></div><div><span>        </span><span>p0</span><span>: </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>35</div></div><div><span>            </span><span>x</span><span>: </span><span>x0</span><span>,</span></div></div><div><div><div>36</div></div><div><span>            </span><span>p</span><span>: </span><span>p0</span><span>,</span></div></div><div><div><div>37</div></div><div><span>            </span><span>f</span><span>,</span></div></div><div><div><div>38</div></div><div><span>            </span><span>b</span><span>,</span></div></div><div><div><div>39</div></div><div><span>            </span><span>h</span><span>,</span></div></div><div><div><div>40</div></div><div><span>            </span><span>q</span><span>,</span></div></div><div><div><div>41</div></div><div><span>            </span><span>r</span><span>,</span></div></div><div><div><div>42</div></div><div><span>            </span><span>y</span><span>: </span><span>Mat</span><span>::</span><span>zeros</span><span>(),</span></div></div><div><div><div>43</div></div><div><span>            </span><span>s</span><span>: </span><span>Mat</span><span>::</span><span>zeros</span><span>(),</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>    </span><span>/// x- = F x + B u,  P- = F P F^T + Q</span></div></div><div><div><div>48</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>u</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>U</span><span>, </span><span>1</span><span>&gt;) {</span></div></div><div><div><div>49</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span>.f * </span><span>self</span><span>.x + </span><span>self</span><span>.b * *</span><span>u</span><span>;</span></div></div><div><div><div>50</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>self</span><span>.f * </span><span>self</span><span>.p * </span><span>self</span><span>.f.</span><span>transpose</span><span>() + </span><span>self</span><span>.q;</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>    </span><span>/// Innovation y, covariance S and gain K = (S^-1 H P)^T, shared by both updates.</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>#[allow(clippy</span><span>::</span><span>type_complexity)]</span></div></div><div><div><div>55</div></div><div><span>    </span><span>fn</span><span> </span><span>gain</span><span>(&amp;</span><span>self</span><span>, </span><span>z</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;), </span><span>CholeskyError</span><span>&gt; {</span></div></div><div><div><div>56</div></div><div><span>        </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> *</span></span><span>z</span><span> - </span><span>self</span><span>.h * </span><span>self</span><span>.x;</span></div></div><div><div><div>57</div></div><div><span>        </span><span>let</span><span> </span><span>hp</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.h * </span><span>self</span><span>.p;</span></div></div><div><div><div>58</div></div><div><span>        </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>hp</span><span> * </span><span>self</span><span>.h.</span><span>transpose</span><span>() + </span><span>self</span><span>.r;</span></div></div><div><div><div>59</div></div><div><span>        </span><span>let</span><span> </span><span>l</span><span><span> </span><span>=</span><span> </span></span><span>s</span><span>.</span><span>cholesky</span><span>().</span><span>ok_or</span><span>(</span><span>CholeskyError</span><span>)?;</span></div></div><div><div><div>60</div></div><div><span>        </span><span>// K^T = S^-1 (H P), solved without forming S^-1</span></div></div><div><div><div>61</div></div><div><span>        </span><span>Ok</span><span>((</span><span>y</span><span>, </span><span>s</span><span>, </span><span>chol_solve</span><span>(&amp;</span><span>l</span><span>, &amp;</span><span>hp</span><span>).</span><span>transpose</span><span>()))</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>    </span><span>/// Joseph-form update. On Cholesky failure the state is left unchanged.</span></div></div><div><div><div>65</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;(), </span><span>CholeskyError</span><span>&gt; {</span></div></div><div><div><div>66</div></div><div><span>        </span><span>let</span><span> (</span><span>y</span><span>, </span><span>s</span><span>, </span><span>k</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>gain</span><span>(</span><span>z</span><span>)?;</span></div></div><div><div><div>67</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span>.x + </span><span>k</span><span> * </span><span>y</span><span>;</span></div></div><div><div><div>68</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>() - </span><span>k</span><span> * </span><span>self</span><span>.h;</span></div></div><div><div><div>69</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> (</span></span><span>a</span><span> * </span><span>self</span><span>.p * </span><span>a</span><span>.</span><span>transpose</span><span>() + </span><span>k</span><span> * </span><span>self</span><span>.r * </span><span>k</span><span>.</span><span>transpose</span><span>()).</span><span>symmetrized</span><span>();</span></div></div><div><div><div>70</div></div><div><span>        </span><span>self</span><span><span>.y </span><span>=</span><span> </span></span><span>y</span><span>;</span></div></div><div><div><div>71</div></div><div><span>        </span><span>self</span><span><span>.s </span><span>=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>72</div></div><div><span>        </span><span>Ok</span><span>(())</span></div></div><div><div><div>73</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>    </span><span>/// Short-form update P = (I - K H) P-: cheaper, but loses symmetry and</span></div></div><div><div><div>76</div></div><div><span>    </span><span>/// positive-definiteness when ill-conditioned. Same failure behavior as `update`.</span></div></div><div><div><div>77</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update_short</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>1</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;(), </span><span>CholeskyError</span><span>&gt; {</span></div></div><div><div><div>78</div></div><div><span>        </span><span>let</span><span> (</span><span>y</span><span>, </span><span>s</span><span>, </span><span>k</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>gain</span><span>(</span><span>z</span><span>)?;</span></div></div><div><div><div>79</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span>.x + </span><span>k</span><span> * </span><span>y</span><span>;</span></div></div><div><div><div>80</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> (</span></span><span>Mat</span><span>::&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;::</span><span>identity</span><span>() - </span><span>k</span><span> * </span><span>self</span><span>.h) * </span><span>self</span><span>.p;</span></div></div><div><div><div>81</div></div><div><span>        </span><span>self</span><span><span>.y </span><span>=</span><span> </span></span><span>y</span><span>;</span></div></div><div><div><div>82</div></div><div><span>        </span><span>self</span><span><span>.s </span><span>=</span><span> </span></span><span>s</span><span>;</span></div></div><div><div><div>83</div></div><div><span>        </span><span>Ok</span><span>(())</span></div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>85</div></div><div><span>}</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>/// Steady-state gain: iterate the Riccati recursion on the prior covariance until</span></div></div><div><div><div>88</div></div><div><span>/// max|dP| &lt;= tol * max|P|. Returns (K, converged prior P), or `None` if the</span></div></div><div><div><div>89</div></div><div><span>/// iteration does not converge or a Cholesky factorization fails.</span></div></div><div><div><div>90</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>steady_state_gain</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>, </span><span>const</span><span> </span><span>M</span><span>: </span><span>usize</span><span>&gt;(</span></div></div><div><div><div>91</div></div><div><span>    </span><span>f</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>92</div></div><div><span>    </span><span>h</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>93</div></div><div><span>    </span><span>q</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;,</span></div></div><div><div><div>94</div></div><div><span>    </span><span>r</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>M</span><span>, </span><span>M</span><span>&gt;,</span></div></div><div><div><div>95</div></div><div><span>    </span><span>tol</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>96</div></div><div><span>    </span><span>max_iter</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>97</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;(</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;, </span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span>&gt;)&gt; {</span></div></div><div><div><div>98</div></div><div><span>    </span><span>let</span><span> </span><span>gain</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>p</span><span>: &amp;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>N</span><span><span>&gt;</span><span>|</span><span> -&gt; </span></span><span>Option</span><span>&lt;</span><span>Mat</span><span>&lt;</span><span>N</span><span>, </span><span>M</span><span>&gt;&gt; {</span></div></div><div><div><div>99</div></div><div><span>        </span><span>let</span><span> </span><span>hp</span><span><span> </span><span>=</span><span> *</span></span><span>h</span><span> * *</span><span>p</span><span>;</span></div></div><div><div><div>100</div></div><div><span>        </span><span>let</span><span> </span><span>l</span><span><span> </span><span>=</span><span> (</span></span><span>hp</span><span> * </span><span>h</span><span>.</span><span>transpose</span><span>() + *</span><span>r</span><span>).</span><span>cholesky</span><span>()?;</span></div></div><div><div><div>101</div></div><div><span>        </span><span>Some</span><span>(</span><span>chol_solve</span><span>(&amp;</span><span>l</span><span>, &amp;</span><span>hp</span><span>).</span><span>transpose</span><span>())</span></div></div><div><div><div>102</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>103</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> *</span></span><span>q</span><span>;</span></div></div><div><div><div>104</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>max_iter</span><span> {</span></div></div><div><div><div>105</div></div><div><span>        </span><span>let</span><span> </span><span>k</span><span><span> </span><span>=</span><span> </span></span><span>gain</span><span>(&amp;</span><span>p</span><span>)?;</span></div></div><div><div><div>106</div></div><div><span>        </span><span>let</span><span> </span><span>pn</span><span><span> </span><span>=</span><span> *</span></span><span>f</span><span> * (</span><span>p</span><span> - </span><span>k</span><span> * *</span><span>h</span><span> * </span><span>p</span><span>) * </span><span>f</span><span>.</span><span>transpose</span><span>() + *</span><span>q</span><span>;</span></div></div><div><div><div>107</div></div><div><span>        </span><span>let</span><span> (</span><span>mut</span><span> </span><span>dmax</span><span>, </span><span>mut</span><span> </span><span>pmax</span><span>): (</span><span>Real</span><span>, </span><span>Real</span><span><span>) </span><span>=</span><span> (</span></span><span>0.0</span><span>, </span><span>0.0</span><span>);</span></div></div><div><div><div>108</div></div><div><span>        </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>109</div></div><div><span>            </span><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>N</span><span> {</span></div></div><div><div><div>110</div></div><div><span>                </span><span>dmax</span><span><span> </span><span>=</span><span> </span></span><span>dmax</span><span>.</span><span>max</span><span>(</span><span>fabs</span><span>(</span><span>pn</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>] - </span><span>p</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>]));</span></div></div><div><div><div>111</div></div><div><span>                </span><span>pmax</span><span><span> </span><span>=</span><span> </span></span><span>pmax</span><span>.</span><span>max</span><span>(</span><span>fabs</span><span>(</span><span>pn</span><span>.m[</span><span>i</span><span>][</span><span>j</span><span>]));</span></div></div><div><div><div>112</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>113</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>114</div></div><div><span>        </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>pn</span><span>;</span></div></div><div><div><div>115</div></div><div><span>        </span><span>if</span><span> </span><span>dmax</span><span><span> </span><span>&lt;=</span><span> </span></span><span>tol</span><span> * </span><span>pmax</span><span> {</span></div></div><div><div><div>116</div></div><div><span>            </span><span>return</span><span> </span><span>Some</span><span>((</span><span>gain</span><span>(&amp;</span><span>p</span><span>)?, </span><span>p</span><span>));</span></div></div><div><div><div>117</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>118</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>119</div></div><div><span>    </span><span>None</span></div></div><div><div><div>120</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个 app（<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/c/apps/ch02.c" target="_blank"><code>c/apps/ch02.c</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/cpp/apps/ch02.cpp" target="_blank"><code>cpp/apps/ch02.cpp</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/rust/src/bin/ch02.rs" target="_blank"><code>rust/src/bin/ch02.rs</code></a>）除了读数据、搭模型、写 CSV，还在处理数据之前先跑三个自检：Cholesky 因子相乘还原 <span><span>AA</span><span><span><span></span><span>A</span></span></span></span>，非正定矩阵被拒绝，<code>chol_solve</code> 乘回去还原右端项。任何一个失败进程就以非零码退出。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p><code>make -s ch02</code> 的输出（只保留校验与指标部分）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch02</span></div></div><div><div><div>2</div></div><div><span>ok   cv_c.csv: max diff 7.105e-15 &lt;= 1.0e-09</span></div></div><div><div><div>3</div></div><div><span>ok   cv_cpp.csv: max diff 7.105e-15 &lt;= 1.0e-09</span></div></div><div><div><div>4</div></div><div><span>ok   cv_rs.csv: max diff 7.105e-15 &lt;= 1.0e-09</span></div></div><div><div><div>5</div></div><div><span>ok   tilt_bias_c.csv: max diff 2.665e-14 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   tilt_bias_cpp.csv: max diff 2.665e-14 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   tilt_bias_rs.csv: max diff 2.665e-14 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>Angle RMSE (deg), 6000 steps, drifting bias</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>1D filter (no bias state):  2.541</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>angle+bias filter:          0.348</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>bias inside +-3 sigma after 10 s: 100.0% of steps</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>final bias: true 0.04525, estimate 0.04347, sigma 0.00243</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>2D constant-velocity track, 200 steps</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>position RMSE raw measurement: 1.436 m</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>position RMSE Kalman filter:   0.542 m</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>velocity RMSE Kalman filter:   0.616 m/s</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>mean NIS (dof 2, expect ~2):   2.070</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>mean NIS tilt (dof 1, expect ~1): 0.984</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这次不再是逐位相同了：最大差值是 <span><span>10−1410^{-14}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span> 量级，因为 Cholesky 里有 <code>sqrt</code>，求和顺序也和 numpy 的 LAPACK 不一样。<span><span>10−1410^{-14}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>14</span></span></span></span></span></span></span></span></span></span></span></span> 离我们定的 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 容差远得很。</p><p><strong>零偏。</strong> 第 1 篇的滤波器剩下的 2.541 度，现在变成 0.348 度。下图下半部分是零偏估计：开始时 <span><span>±3σ\pm 3\sigma</span><span><span><span></span><span>±</span><span>3</span><span>σ</span></span></span></span> 的带子很宽（初始标准差 0.1 rad/s），不到两秒就收敛到真值附近，之后一直待在带子里。我让脚本断言”10 秒以后，真实零偏落在 <span><span>±3σ\pm3\sigma</span><span><span><span></span><span>±</span><span>3</span><span>σ</span></span></span></span> 以内的步数不少于 95%“，实际是 100.0%。最后一步的估计是 0.04347，真值 0.04525，差 0.0018，<span><span>σ\sigma</span><span><span><span></span><span>σ</span></span></span></span> 是 0.00243：差不到一个标准差。</p><p></p><figure><img src="/assets/images/kalman-filter/ch02-tilt-bias.svg" alt="上：加速度计角度、真值与角度加零偏滤波；下：零偏估计与 3σ 带（纵轴被截掉了前一秒的大带宽）" /><figcaption>上：加速度计角度、真值与角度加零偏滤波；下：零偏估计与 3σ 带（纵轴被截掉了前一秒的大带宽）</figcaption></figure><p></p><p>我觉得有意思的一点是：加速度计并不直接测零偏，零偏是靠”陀螺仪积分出来的角度和加速度计角度之间的系统性差距”推出来的，<span><span>P01P_{01}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span><span>01</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>（角度与零偏的协方差）就是在传递这个信息，更新步里 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 的第二个元素通过它修正零偏。这种”测量 A，修正没测的 B”的能力，就是第 1 篇标量滤波器没有的东西。</p><p><strong>匀速模型。</strong> 位置 RMSE 从原始测量的 1.436 m 降到 0.542 m，而且滤波器还给出了没有被直接测量的速度，RMSE 0.616 m/s。</p><p></p><figure><img src="/assets/images/kalman-filter/ch02-cv-track.svg" alt="二维匀速运动：真值（黑）、位置测量（点）与卡尔曼滤波（红）" /><figcaption>二维匀速运动：真值（黑）、位置测量（点）与卡尔曼滤波（红）</figcaption></figure><p></p><p>最后两行的平均 NIS 是第 3 篇的预告。NIS 是 <span><span>yTS−1yy^TS^{-1}y</span><span><span><span></span><span><span>y</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span><span><span>S</span><span><span><span><span><span><span></span><span><span><span>−</span><span>1</span></span></span></span></span></span></span></span></span><span>y</span></span></span></span>，如果滤波器的模型和噪声都对，它服从自由度等于测量维数的 <span><span>χ2\chi^2</span><span><span><span></span><span><span>χ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 分布，均值就是测量维数：匀速模型是 2（实测 2.070），倾角模型是 1（实测 0.984）。两个都落在预期附近，第 3 篇会把这件事做得更严格。</p></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>滤波器能跑，不等于滤波器对。这一篇的数据是我自己用已知的噪声参数生成的，滤波器用的也恰好是这些参数。真实传感器上你不知道 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span> 和 <span><span>RR</span><span><span><span></span><span>R</span></span></span></span>，只能猜。第 3 篇回答三个问题：怎么判断滤波器的 <span><span>QQ</span><span><span><span></span><span>Q</span></span></span></span>、<span><span>RR</span><span><span><span></span><span>R</span></span></span></span> 猜得对不对（NEES、NIS、新息自相关）；什么时候某个状态根本估不出来（可观性）；以及上面说的 Joseph 形式到底能救你多少。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/kalman-filter-01-one-dimension/</id>
      <title type="text">一维卡尔曼滤波：两个高斯相乘</title>
      <published>2026-10-14T00:00:00.000Z</published>
      <updated>2026-10-14T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/kalman-filter-01-one-dimension/"/>
      <summary type="text">卡尔曼滤波系列第 1 篇：从高斯分布和贝叶斯公式出发，推导一维卡尔曼滤波的预测与更新，求稳态增益，并用 Python、C、C++、Rust 四种语言实现同一个滤波器，结果逐位一致。</summary>
      <content type="html"><![CDATA[<p>先看一组数。一个单轴倾角，真值是 <span><span>0.5sin⁡(2π⋅0.25t)0.5\sin(2\pi\cdot 0.25t)</span><span><span><span></span><span>0.5</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.25</span><span>t</span><span>)</span></span></span></span> 弧度，陀螺仪积分 20 秒后的均方根误差是 1.097 度，加速度计直接算出来的角度是 2.863 度，把两者交给一个只有几行代码的卡尔曼滤波器，误差降到 0.277 度。比加速度计好 10 倍，比陀螺仪好 4 倍，而且滤波器没有任何”调出来的”魔法数，它的每一个参数都能从传感器的噪声标准差直接算出来。</p>
<p>这个系列要做的事：从这几行出发，一路走到三维姿态（陀螺仪加加速度计加磁力计，四元数状态，估计陀螺零偏）的扩展卡尔曼、误差状态卡尔曼和无迹卡尔曼滤波器。我需要姿态估计去做控制，所以我想把这条路完整地走一遍，每一个公式都自己推，每一段代码都自己写，不依赖 Eigen、nalgebra 这类矩阵库。全程纯软件：传感器是仿真出来的，结论靠软件验证，你不需要任何硬件就能复现每一个数字和每一张图。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：一份能跑的伴生仓库 <code>kalman-lab</code>，<code>make ch01</code> 通过，生成 <code>out/ch01/</code> 下六个 CSV（Python、C、C++、Rust 各自的滤波输出）和 <code>figures/ch01/</code> 下四张图；以及一维卡尔曼滤波的完整推导。
这是第 1 篇，没有前置章节。需要的数学：概率论里的均值、方差和高斯分布，会求导和解一元二次方程就够了。</p></div></div>
<section><h2>这个系列怎么读<a href="#这个系列怎么读"><span>#</span></a></h2><p>下面是整个系列的地图：</p>

<table><thead><tr><th>篇</th><th>标题</th><th>主要内容</th></tr></thead><tbody><tr><td>1</td><td>一维卡尔曼滤波：两个高斯相乘</td><td>高斯、贝叶斯、预测与更新、稳态增益</td></tr><tr><td>2</td><td><a href="/posts/kalman-filter-02-multivariate/">升到高维：多变量卡尔曼滤波的推导与实现</a></td><td>协方差矩阵、Joseph 形式、Cholesky、手写矩阵库</td></tr><tr><td>3</td><td><a href="/posts/kalman-filter-03-tuning-diagnostics/">调参与诊断：怎么知道滤波器没在骗你</a></td><td>NEES、NIS、可观性、稳态增益、数值稳定</td></tr><tr><td>4</td><td><a href="/posts/kalman-filter-04-attitude-imu/">姿态的数学：旋转、四元数与 IMU 仿真器</a></td><td>SO(3)、四元数、IMU 仿真、互补滤波基线</td></tr><tr><td>5</td><td><a href="/posts/kalman-filter-05-ekf/">扩展卡尔曼滤波：把四元数塞进 EKF</a></td><td>线性化、雅可比、EKF 的代价</td></tr><tr><td>6</td><td><a href="/posts/kalman-filter-06-eskf/">误差状态卡尔曼滤波：四元数的正确打开方式</a></td><td>误差状态、切空间、注入与重置</td></tr><tr><td>7</td><td><a href="/posts/kalman-filter-07-ukf/">无迹卡尔曼滤波，以及四个滤波器同台</a></td><td>无迹变换、Mahony/EKF/ESKF/UKF 对比</td></tr><tr><td>8</td><td><a href="/posts/kalman-filter-08-porting/">移植之前：float32、定长内存与每步耗时</a></td><td>float32、UD 分解、无堆内存、耗时</td></tr></tbody></table><p><strong>四种语言。</strong> 每一章的滤波器我都写四遍：Python（numpy，只用来当参考实现和画图）、C（C11，不使用 <code>malloc</code>）、C++（C++20，模板，纯头文件）、Rust（edition 2024，<code>no_std</code> 兼容）。Python 版承担推导和完整讲解；C、C++、Rust 的核心函数我会贴出来，文字只讲它们和 Python 版的区别：内存归谁、尺寸在哪里确定、标量类型怎么切换、语言本身的惯用法。</p><p><strong>同一份数据，不要求随机数一致。</strong> 所有仿真数据都由 Python 生成，写成 CSV，四种语言读同一份文件。这样四种语言不需要共用随机数发生器，而且这恰好就是上硬件之后的工作流：传感器日志进来，滤波器处理。</p><p><strong>真实输出。</strong> 文中所有终端输出、数值、图，都来自我在这台机器上跑 <code>make ch01</code> 的结果；不是我现编的。每个浮点数在 CSV 里都写成 17 位有效数字，C、C++、Rust 的输出和 Python 参考的最大差值要在 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 以内才算通过。</p></section>
<section><h2>装环境<a href="#装环境"><span>#</span></a></h2><p>代码在 <a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1" target="_blank">kalman-lab</a>，本系列固定在 <code>tutorial-v1</code> 标签。仓库里有一个 <code>flake.nix</code>，<code>nix develop</code> 进去就是我用的全部工具：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>https://github.com/bfmhno3/kalman-lab.git</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>kalman-lab</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>checkout</span><span> </span><span>tutorial-v1</span></div></div><div><div><div>4</div></div><div><span>nix</span><span> </span><span>develop</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>kalman-lab dev shell: Python 3.13.15, gcc (GCC) 15.3.0, rustc 1.95.0 (59807616e 2026-04-14) (built from a source tarball)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有 Nix 的话，自己装这几样就行：Python 3 加 <code>numpy</code>、<code>matplotlib</code>、<code>scipy</code>；gcc 13 或更新（要支持 C11 和 C++20）；用 rustup 装的 Rust stable，外加 <code>clippy</code>。画图时中文标签用的字体通过环境变量 <code>KL_CJK_FONT</code> 指定（<code>flake.nix</code> 已经设好），找不到字体时图的标签自动退回英文。</p><p>所有命令都在仓库根目录跑。<code>make ch01</code> 做五件事：用 Python 生成数据并跑参考实现；编译 C、C++、Rust 三个版本；各自读同一份数据并输出；用 <code>python/compare.py</code> 逐列对比；最后打印指标并画图。</p></section>
<section><h2>一维倾角问题<a href="#一维倾角问题"><span>#</span></a></h2><p>我们先用最简单的问题建立直觉：一个物体绕一根轴转动，我们想知道它的倾角 <span><span>θ\theta</span><span><span><span></span><span>θ</span></span></span></span>。手上有两个传感器：</p><ul>
<li>陀螺仪测角速度：<span><span>ωm=θ˙+b+n\omega_m = \dot\theta + b + n</span><span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>θ</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span></span><span>+</span><span></span></span><span><span></span><span>n</span></span></span></span>，<span><span>n∼N(0,σn2)n \sim \mathcal N(0, \sigma_n^2)</span><span><span><span></span><span>n</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span> 是白噪声，<span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 是零偏。这一篇先令 <span><span>b=0b = 0</span><span><span><span></span><span>b</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>。</li>
<li>加速度计在静止附近能反推出倾角：<span><span>z=θ+vz = \theta + v</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>θ</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span>，<span><span>v∼N(0,σv2)v \sim \mathcal N(0, \sigma_v^2)</span><span><span><span></span><span>v</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>v</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>。</li>
</ul><p>两者互补。陀螺仪积分得到的角度很平滑，但每一步的噪声会累积，长时间之后漂走；加速度计没有漂移，但每个样本都很吵。滤波器要做的事，就是把”相信积分”和”相信测量”按各自的可信度混起来。</p><p>仿真参数我固定下来（<code>python/kl/sim1d.py</code>）：</p><ul>
<li>真值 <span><span>θ(t)=0.5sin⁡(2π⋅0.25 t)\theta(t) = 0.5\sin(2\pi\cdot 0.25\,t)</span><span><span><span></span><span>θ</span><span>(</span><span>t</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span><span></span><span>sin</span><span>(</span><span>2</span><span>π</span><span></span><span>⋅</span><span></span></span><span><span></span><span>0.25</span><span></span><span>t</span><span>)</span></span></span></span> rad，周期 4 秒，幅值约 28.6 度。</li>
<li>步长 <span><span>Δt=0.01\Delta t = 0.01</span><span><span><span></span><span>Δ</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0.01</span></span></span></span> s，2000 步（20 秒），种子 1。</li>
<li><span><span>σn=0.05\sigma_n = 0.05</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.05</span></span></span></span> rad/s，<span><span>σv=0.05\sigma_v = 0.05</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>v</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.05</span></span></span></span> rad。</li>
</ul></section>
<section><h2>数学<a href="#数学"><span>#</span></a></h2><section><h3>高斯分布<a href="#高斯分布"><span>#</span></a></h3><p>一个随机变量 <span><span>XX</span><span><span><span></span><span>X</span></span></span></span>，均值 <span><span>μ=E[X]\mu = E[X]</span><span><span><span></span><span>μ</span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span>[</span><span>X</span><span>]</span></span></span></span>，方差 <span><span>σ2=E[(X−μ)2]\sigma^2 = E[(X-\mu)^2]</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>E</span><span>[(</span><span>X</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>]</span></span></span></span>。高斯分布的概率密度是</p><span><span><span>N(x;μ,σ2)=12πσ2exp⁡ ⁣(−(x−μ)22σ2)\mathcal N(x;\mu,\sigma^2) = \frac{1}{\sqrt{2\pi\sigma^2}}\exp\!\Big(-\frac{(x-\mu)^2}{2\sigma^2}\Big)</span><span><span><span></span><span>N</span><span>(</span><span>x</span><span>;</span><span></span><span>μ</span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span><span><span><span></span><span><span>2</span><span>π</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>exp</span><span></span><span></span><span><span>(</span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>x</span><span></span><span>−</span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span></span></span></span></span><p>高斯分布有两条性质，整个卡尔曼滤波都建立在它们上面：两个高斯相乘（归一化之后）还是高斯，两个独立高斯相加还是高斯。下面分别推。</p></section><section><h3>贝叶斯公式：先验乘似然<a href="#贝叶斯公式先验乘似然"><span>#</span></a></h3><p>设我们对状态 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 有一个先验信念 <span><span>p(x)p(x)</span><span><span><span></span><span>p</span><span>(</span><span>x</span><span>)</span></span></span></span>，然后得到一次测量 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>。贝叶斯公式说</p><span><span><span>p(x∣z)=p(z∣x) p(x)p(z)∝p(z∣x) p(x)p(x\mid z) = \frac{p(z\mid x)\,p(x)}{p(z)} \propto p(z\mid x)\,p(x)</span><span><span><span></span><span>p</span><span>(</span><span>x</span><span></span><span>∣</span><span></span></span><span><span></span><span>z</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>p</span><span>(</span><span>z</span><span>)</span></span></span><span><span></span><span></span></span><span><span></span><span><span>p</span><span>(</span><span>z</span><span></span><span>∣</span><span></span><span>x</span><span>)</span><span></span><span>p</span><span>(</span><span>x</span><span>)</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>∝</span><span></span></span><span><span></span><span>p</span><span>(</span><span>z</span><span></span><span>∣</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>p</span><span>(</span><span>x</span><span>)</span></span></span></span></span><p>后验正比于”似然乘先验”。分母 <span><span>p(z)p(z)</span><span><span><span></span><span>p</span><span>(</span><span>z</span><span>)</span></span></span></span> 不依赖 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span>，只是个归一化常数，后面可以忽略。</p><p>在我们的模型里，测量是 <span><span>z=x+vz = x + v</span><span><span><span></span><span>z</span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>v</span></span></span></span>，所以似然 <span><span>p(z∣x)=N(z;x,r)p(z\mid x) = \mathcal N(z; x, r)</span><span><span><span></span><span>p</span><span>(</span><span>z</span><span></span><span>∣</span><span></span></span><span><span></span><span>x</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span>N</span><span>(</span><span>z</span><span>;</span><span></span><span>x</span><span>,</span><span></span><span>r</span><span>)</span></span></span></span>，其中 <span><span>r=σv2r = \sigma_v^2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>v</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。把它看成 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 的函数，因为 <span><span>(z−x)2=(x−z)2(z-x)^2 = (x-z)^2</span><span><span><span></span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span>x</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>z</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，它就是 <span><span>N(x;z,r)\mathcal N(x; z, r)</span><span><span><span></span><span>N</span><span>(</span><span>x</span><span>;</span><span></span><span>z</span><span>,</span><span></span><span>r</span><span>)</span></span></span></span>。设先验是 <span><span>N(x;μ1,σ12)\mathcal N(x; \mu_1, \sigma_1^2)</span><span><span><span></span><span>N</span><span>(</span><span>x</span><span>;</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，先验乘似然就是两个高斯相乘。</p></section><section><h3>两个高斯相乘（更新）<a href="#两个高斯相乘更新"><span>#</span></a></h3><span><span><span>N(x;μ1,σ12) N(x;μ2,σ22)∝exp⁡ ⁣(−12[(x−μ1)2σ12+(x−μ2)2σ22])\mathcal N(x;\mu_1,\sigma_1^2)\,\mathcal N(x;\mu_2,\sigma_2^2) \propto \exp\!\Big(-\frac12\Big[\frac{(x-\mu_1)^2}{\sigma_1^2}+\frac{(x-\mu_2)^2}{\sigma_2^2}\Big]\Big)</span><span><span><span></span><span>N</span><span>(</span><span>x</span><span>;</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>N</span><span>(</span><span>x</span><span>;</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span>∝</span><span></span></span><span><span></span><span>exp</span><span></span><span></span><span><span>(</span></span><span></span><span>−</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>[</span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>x</span><span></span><span>−</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>(</span><span>x</span><span></span><span>−</span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>]</span></span><span><span>)</span></span></span></span></span></span><p>方括号里是 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 的二次函数，把它按 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 的幂展开：</p><span><span><span>(1σ12+1σ22)x2−2(μ1σ12+μ2σ22)x+const\Big(\frac{1}{\sigma_1^2}+\frac{1}{\sigma_2^2}\Big)x^2 - 2\Big(\frac{\mu_1}{\sigma_1^2}+\frac{\mu_2}{\sigma_2^2}\Big)x + \text{const}</span><span><span><span></span><span><span>(</span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span><span>(</span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>)</span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span><span>const</span></span></span></span></span></span><p>我们希望把它写成 <span><span>(x−μ)2/σ2+const′(x-\mu)^2/\sigma^2 + \text{const}'</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span><span>const</span></span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span></span></span></span> 的形式（配方）。展开 <span><span>(x−μ)2/σ2=x2/σ2−2μx/σ2+μ2/σ2(x-\mu)^2/\sigma^2 = x^2/\sigma^2 - 2\mu x/\sigma^2 + \mu^2/\sigma^2</span><span><span><span></span><span>(</span><span>x</span><span></span><span>−</span><span></span></span><span><span></span><span>μ</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>2</span><span>μx</span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>/</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，对照 <span><span>x2x^2</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 和 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 的系数：</p><span><span><span>1σ2=1σ12+1σ22,μσ2=μ1σ12+μ2σ22\frac{1}{\sigma^2} = \frac{1}{\sigma_1^2}+\frac{1}{\sigma_2^2},\qquad \frac{\mu}{\sigma^2} = \frac{\mu_1}{\sigma_1^2}+\frac{\mu_2}{\sigma_2^2}</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>μ</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>解出</p><span><span><span>σ2=σ12σ22σ12+σ22,μ=σ22μ1+σ12μ2σ12+σ22\sigma^2 = \frac{\sigma_1^2\sigma_2^2}{\sigma_1^2+\sigma_2^2},\qquad \mu = \frac{\sigma_2^2\mu_1+\sigma_1^2\mu_2}{\sigma_1^2+\sigma_2^2}</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span>μ</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>两件事一眼就能读出来。精度（方差的倒数）相加：两个信息源合起来，方差一定比任何一个都小。均值是加权平均，谁的方差小，谁的权重大。</p><p>换一种写法更接近代码。令 <span><span>K=σ12/(σ12+σ22)K = \sigma_1^2/(\sigma_1^2+\sigma_2^2)</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>/</span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，则</p><span><span><span>μ=μ1+K(μ2−μ1),σ2=(1−K) σ12\mu = \mu_1 + K(\mu_2-\mu_1),\qquad \sigma^2 = (1-K)\,\sigma_1^2</span><span><span><span></span><span>μ</span><span></span><span>=</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>(</span><span><span>μ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>μ</span><span><span><span><span><span><span></span><span><span>1</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>)</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><p>验证第二式：<span><span>(1−K)σ12=σ22σ12+σ22σ12=σ2(1-K)\sigma_1^2 = \frac{\sigma_2^2}{\sigma_1^2+\sigma_2^2}\sigma_1^2 = \sigma^2</span><span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>)</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>+</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>。</p><p></p><figure><img src="/assets/images/kalman-filter/ch01-gaussian-product.svg" alt="两个高斯相乘：先验（方差 1.5）、似然（方差 1）和后验" /><figcaption>两个高斯相乘：先验（方差 1.5）、似然（方差 1）和后验</figcaption></figure><p></p><p>后验比两者都窄，峰位落在两者之间，更靠近更窄的那个。</p></section><section><h3>两个独立高斯相加（预测）<a href="#两个独立高斯相加预测"><span>#</span></a></h3><p>滤波器每走一步，状态要根据运动模型往前推。设上一步的后验是 <span><span>x∼N(x^,P)x \sim \mathcal N(\hat x, P)</span><span><span><span></span><span>x</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span>,</span><span></span><span>P</span><span>)</span></span></span></span>，我们根据陀螺仪的读数推进：</p><span><span><span>x′=x+uΔt+w,w∼N(0,q)x' = x + u\Delta t + w,\qquad w \sim \mathcal N(0, q)</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>x</span><span></span><span>+</span><span></span></span><span><span></span><span>u</span><span>Δ</span><span>t</span><span></span><span>+</span><span></span></span><span><span></span><span>w</span><span>,</span><span></span><span></span><span>w</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span>q</span><span>)</span></span></span></span></span><p>这里 <span><span>u=ωmu = \omega_m</span><span><span><span></span><span>u</span><span></span><span>=</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span>m</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是陀螺仪读数，当成已知的控制输入；<span><span>ww</span><span><span><span></span><span>w</span></span></span></span> 是过程噪声，它代表”这一步积分不准多少”，和 <span><span>xx</span><span><span><span></span><span>x</span></span></span></span> 独立。独立随机变量之和的均值相加，方差也相加：</p><span><span><span>E[x′]=x^+uΔt,Var⁡[x′]=P+qE[x'] = \hat x + u\Delta t,\qquad \operatorname{Var}[x'] = P + q</span><span><span><span></span><span>E</span><span>[</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>u</span><span>Δ</span><span>t</span><span>,</span><span></span><span></span><span><span>Var</span></span><span>[</span><span><span>x</span><span><span><span><span><span><span></span><span><span><span>′</span></span></span></span></span></span></span></span></span><span>]</span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span></span><span>+</span><span></span></span><span><span></span><span>q</span></span></span></span></span><p>（方差相加：<span><span>Var⁡(X+Y)=Var⁡X+Var⁡Y+2Cov⁡(X,Y)\operatorname{Var}(X+Y) = \operatorname{Var}X + \operatorname{Var}Y + 2\operatorname{Cov}(X,Y)</span><span><span><span></span><span><span>Var</span></span><span>(</span><span>X</span><span></span><span>+</span><span></span></span><span><span></span><span>Y</span><span>)</span><span></span><span>=</span><span></span></span><span><span></span><span><span>Var</span></span><span></span><span>X</span><span></span><span>+</span><span></span></span><span><span></span><span><span>Var</span></span><span></span><span>Y</span><span></span><span>+</span><span></span></span><span><span></span><span>2</span><span></span><span><span>Cov</span></span><span>(</span><span>X</span><span>,</span><span></span><span>Y</span><span>)</span></span></span></span>，独立时协方差为零。和仍是高斯，因为两个高斯密度的卷积还是高斯，这是标准结论，我这里直接引用。）</p><p><span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 怎么取？陀螺仪读数的噪声是 <span><span>n∼N(0,σn2)n \sim \mathcal N(0,\sigma_n^2)</span><span><span><span></span><span>n</span><span></span><span>∼</span><span></span></span><span><span></span><span>N</span><span>(</span><span>0</span><span>,</span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>，每一步角度增量的噪声是 <span><span>nΔtn\Delta t</span><span><span><span></span><span>n</span><span>Δ</span><span>t</span></span></span></span>，所以</p><span><span><span>q=(σnΔt)2q = (\sigma_n \Delta t)^2</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span></span></section><section><h3>一维卡尔曼滤波<a href="#一维卡尔曼滤波"><span>#</span></a></h3><p>把两步合起来。用上标减号表示先验（预测之后、更新之前）：</p><span><span><span>x^−=x^+uΔt,P−=P+q\hat x^- = \hat x + u\Delta t,\qquad P^- = P + q</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>u</span><span>Δ</span><span>t</span><span>,</span><span></span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>P</span><span></span><span>+</span><span></span></span><span><span></span><span>q</span></span></span></span></span><p>测量更新就是把先验 <span><span>N(x^−,P−)\mathcal N(\hat x^-, P^-)</span><span><span><span></span><span>N</span><span>(</span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>,</span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 和似然 <span><span>N(z,r)\mathcal N(z, r)</span><span><span><span></span><span>N</span><span>(</span><span>z</span><span>,</span><span></span><span>r</span><span>)</span></span></span></span> 相乘，套上一节的公式（<span><span>σ12=P−\sigma_1^2 = P^-</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>1</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>，<span><span>σ22=r\sigma_2^2 = r</span><span><span><span></span><span><span>σ</span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>r</span></span></span></span>）：</p><span><span><span>K=P−P−+r,x^=x^−+K(z−x^−),P=(1−K)P−K = \frac{P^-}{P^- + r},\qquad \hat x = \hat x^- + K(z-\hat x^-),\qquad P = (1-K)P^-</span><span><span><span></span><span>K</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>r</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span><span>,</span><span></span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span></span><p><span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 叫卡尔曼增益。<span><span>z−x^−z-\hat x^-</span><span><span><span></span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 叫新息（innovation），它是”测量比预测多出来的那部分信息”。如果 <span><span>P−P^-</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 很大（预测不可信），<span><span>K→1K\to 1</span><span><span><span></span><span>K</span><span></span><span>→</span><span></span></span><span><span></span><span>1</span></span></span></span>，滤波器几乎完全相信测量；如果 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 很大（测量不可信），<span><span>K→0K\to 0</span><span><span><span></span><span>K</span><span></span><span>→</span><span></span></span><span><span></span><span>0</span></span></span></span>，几乎完全相信预测。</p><p>整个滤波器就是上面这几个公式。注意 <span><span>PP</span><span><span><span></span><span>P</span></span></span></span> 的递推根本不依赖测量值 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>，也不依赖 <span><span>uu</span><span><span><span></span><span>u</span></span></span></span>：协方差和增益可以提前算出来。这一点在高维时会变得很重要。</p></section><section><h3>稳态<a href="#稳态"><span>#</span></a></h3><p>如果 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 和 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 是常数，<span><span>P−P^-</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span> 会收敛到一个定值 <span><span>P∞−P^-_\infty</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span>。从一步到下一步：更新后 <span><span>P=P−r/(P−+r)P = P^- r/(P^-+r)</span><span><span><span></span><span>P</span><span></span><span>=</span><span></span></span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>r</span><span>/</span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>r</span><span>)</span></span></span></span>（把 <span><span>KK</span><span><span><span></span><span>K</span></span></span></span> 代入 <span><span>(1−K)P−(1-K)P^-</span><span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span>K</span><span>)</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>），然后预测加 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span>：</p><span><span><span>P∞−=P∞− rP∞−+r+qP^-_\infty = \frac{P^-_\infty\, r}{P^-_\infty + r} + q</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>r</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>q</span></span></span></span></span><p>两边乘 <span><span>(P∞−+r)(P^-_\infty + r)</span><span><span><span></span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>r</span><span>)</span></span></span></span>：</p><span><span><span>(P∞−)2+rP∞−=rP∞−+qP∞−+qr  ⇒  (P∞−)2−qP∞−−qr=0(P^-_\infty)^2 + rP^-_\infty = rP^-_\infty + qP^-_\infty + qr
\;\Rightarrow\; (P^-_\infty)^2 - qP^-_\infty - qr = 0</span><span><span><span></span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>r</span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>r</span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>q</span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>q</span><span>r</span><span></span><span></span><span>⇒</span><span></span><span></span></span><span><span></span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>q</span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span>q</span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span></span><p>取正根：</p><span><span><span>P∞−=q+q2+4qr2,K∞=P∞−P∞−+rP^-_\infty = \frac{q+\sqrt{q^2+4qr}}{2},\qquad K_\infty = \frac{P^-_\infty}{P^-_\infty+r}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span><span><span></span><span></span></span><span><span></span><span><span>q</span><span></span><span>+</span><span></span><span><span><span><span><span><span></span><span><span><span>q</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>4</span><span>q</span><span>r</span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span>,</span><span></span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span><span>r</span></span></span><span><span></span><span></span></span><span><span></span><span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>代入 <span><span>q=(0.05×0.01)2=2.5×10−7q = (0.05\times 0.01)^2 = 2.5\times 10^{-7}</span><span><span><span></span><span>q</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>0.05</span><span></span><span>×</span><span></span></span><span><span></span><span>0.01</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>7</span></span></span></span></span></span></span></span></span></span></span></span>，<span><span>r=0.0025r = 0.0025</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0025</span></span></span></span>，得到 <span><span>K∞=0.009950K_\infty = 0.009950</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.009950</span></span></span></span>，<span><span>P∞−=2.5125×10−5P^-_\infty = 2.5125\times 10^{-5}</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>∞</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>2.5125</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>5</span></span></span></span></span></span></span></span></span></span></span></span>。</p></section><section><h3>常增益就是低通滤波器<a href="#常增益就是低通滤波器"><span>#</span></a></h3><p>把常数 <span><span>K∞K_\infty</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 代回 <span><span>x^=x^−+K(z−x^−)\hat x = \hat x^- + K(z-\hat x^-)</span><span><span><span></span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>K</span><span>(</span><span>z</span><span></span><span>−</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span>)</span></span></span></span> 并整理：</p><span><span><span>x^k=(1−K∞) x^k−+K∞zk,x^k−=x^k−1+ωm,kΔt\hat x_k = (1-K_\infty)\,\hat x^-_k + K_\infty z_k,\qquad \hat x^-_k = \hat x_{k-1} + \omega_{m,k}\Delta t</span><span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span><span>z</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>,</span><span></span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span><span><span></span><span><span>−</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span><span>k</span><span>−</span><span>1</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>ω</span><span><span><span><span><span><span></span><span><span><span>m</span><span>,</span><span>k</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span></span></span></span></span><p>这是一个指数滑动平均（EMA），只不过被平均的不是测量，而是”按陀螺仪推进之后的预测”。<span><span>K∞≈0.00995K_\infty \approx 0.00995</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>0.00995</span></span></span></span> 意味着每一步只把 1% 的权重给加速度计，等效时间常数 <span><span>Δt/K∞≈1.005\Delta t/K_\infty \approx 1.005</span><span><span><span></span><span>Δ</span><span>t</span><span>/</span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1.005</span></span></span></span> 秒：短于 1 秒的变化听陀螺仪的，长于 1 秒的偏差听加速度计的。这就是互补滤波器，第 4 篇会把它推广到三维。卡尔曼滤波的价值在于，它不需要你去猜这个截止频率，它是 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 和 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 的函数。</p></section></section>
<section><h2>Python 实现<a href="#python-实现"><span>#</span></a></h2><section><h3>仿真器<a href="#仿真器"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Single-axis tilt simulator for chapter 1."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>from</span><span> pathlib </span><span>import</span><span> Path</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>from</span><span> .io </span><span>import</span><span> write_csv</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>ROOT</span><span> </span><span>=</span><span> </span><span>Path</span><span>(</span></span><span>__file__</span><span><span>).</span><span>resolve</span><span>().parents[</span></span><span>2</span><span>]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>def</span><span> </span><span>simulate_tilt</span><span>(</span><span>seed</span><span>,</span><span><span> </span><span>n</span></span><span>=</span><span>2000</span><span>,</span><span><span> </span><span>dt</span></span><span>=</span><span>0.01</span><span>,</span><span><span> </span><span>gyro_sigma</span></span><span>=</span><span>0.05</span><span>,</span><span><span> </span><span>meas_sigma</span></span><span>=</span><span>0.05</span><span>,</span><span><span> </span><span>bias</span></span><span>=</span><span>0.0</span><span>):</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>rng </span><span>=</span><span> np.random.</span><span>default_rng</span><span>(seed)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>t </span><span>=</span><span> np.</span><span>arange</span><span>(n) </span><span>*</span><span> dt</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>w </span><span>=</span><span> </span><span>2</span><span><span> </span><span>*</span><span> np.pi </span><span>*</span><span> </span></span><span>0.25</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>theta </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> np.</span><span>sin</span><span>(w </span><span>*</span><span> t)</span></span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>omega </span><span>=</span><span> </span><span>0.5</span><span><span> </span><span>*</span><span> w </span><span>*</span><span> np.</span><span>cos</span><span>(w </span><span>*</span><span> t)</span></span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>omega_m </span><span>=</span><span> omega </span><span>+</span><span> bias </span><span>+</span><span> gyro_sigma </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(n)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>z </span><span>=</span><span> theta </span><span>+</span><span> meas_sigma </span><span>*</span><span> rng.</span><span>standard_normal</span><span>(n)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>cols </span><span>=</span><span> {</span><span>"t"</span><span>: t, </span><span>"omega_m"</span><span>: omega_m, </span><span>"z"</span><span>: z, </span><span>"theta"</span><span>: theta}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>path </span><span>=</span><span> </span><span>ROOT</span><span> </span><span>/</span><span> </span><span>"data"</span><span><span> </span><span>/</span><span> </span></span><span>f</span><span>"ch01_tilt_b</span><span>{</span><span>bias</span><span>:g</span><span>}</span><span>_seed</span><span>{</span><span>seed</span><span>}</span><span>.csv"</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>write_csv</span><span>(path, cols)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span> cols</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>omega</code> 是真值的解析导数 <span><span>θ˙=0.5⋅2πfcos⁡(2πft)\dot\theta = 0.5\cdot 2\pi f\cos(2\pi f t)</span><span><span><span></span><span><span><span><span><span><span></span><span>θ</span></span><span><span></span><span><span>˙</span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0.5</span><span></span><span>⋅</span><span></span></span><span><span></span><span>2</span><span>π</span><span>f</span><span></span><span>cos</span><span>(</span><span>2</span><span>π</span><span>f</span><span>t</span><span>)</span></span></span></span>，陀螺读数是它加零偏加噪声。输出 CSV 的列是 <code>t,omega_m,z,theta</code>，<code>theta</code> 只在评估时用，滤波器看不到。</p></section><section><h3>滤波器<a href="#滤波器"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"""Scalar Kalman filter."""</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>class</span><span><span> </span><span>KF1D</span></span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>def</span><span> </span><span>__init__</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>x0</span></span><span>,</span><span><span> </span><span>p0</span></span><span>,</span><span><span> </span><span>q</span></span><span>,</span><span><span> </span><span>r</span></span><span>):</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> x0</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> p0</span></span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span><span>.q </span><span>=</span><span> q</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>self</span><span><span>.r </span><span>=</span><span> r</span></span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span><span>.k </span><span>=</span><span> </span></span><span>0.0</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>def</span><span> </span><span>predict</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>u_dt</span></span><span>):</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"""x- = x + u*dt, P- = P + q."""</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span><span>.x </span><span>+</span><span> u_dt</span></span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> </span></span><span>self</span><span><span>.p </span><span>+</span><span> </span></span><span>self</span><span>.q</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>def</span><span> </span><span>update</span><span>(</span><span>self</span><span>,</span><span><span> </span><span>z</span></span><span>):</span></div></div><div><div><div>18</div></div><div><span>        </span><span>"""K = P-/(P- + r); x = x- + K (z - x-); P = (1 - K) P-."""</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span><span>.k </span><span>=</span><span> </span></span><span>self</span><span><span>.p </span><span>/</span><span> (</span></span><span>self</span><span><span>.p </span><span>+</span><span> </span></span><span>self</span><span>.r)</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>=</span><span> </span></span><span>self</span><span><span>.x </span><span>+</span><span> </span></span><span>self</span><span><span>.k </span><span>*</span><span> (z </span><span>-</span><span> </span></span><span>self</span><span>.x)</span></div></div><div><div><div>21</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>=</span><span> (</span></span><span>1.0</span><span><span> </span><span>-</span><span> </span></span><span>self</span><span><span>.k) </span><span>*</span><span> </span></span><span>self</span><span>.p</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>和公式一一对应，没有多余的东西。<code>predict</code> 接收的是 <code>u_dt</code>（已经乘过 <span><span>Δt\Delta t</span><span><span><span></span><span>Δ</span><span>t</span></span></span></span> 的角度增量），这样滤波器本身不需要知道步长。<code>update</code> 里先算 <code>k</code> 再覆盖 <code>x</code>，覆盖 <code>p</code> 时用的是 <code>(1 - k) * p</code>，此时 <code>p</code> 还是先验 <span><span>P−P^-</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span></span></span></span>，顺序不能换。</p><p><code>run_filter</code> 是把它放进循环的小包装：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>run_filter</span><span>(</span><span>omega_m</span><span>,</span><span><span> </span><span>z</span></span><span>,</span><span><span> </span><span>dt</span></span><span>,</span><span><span> </span><span>q</span></span><span>,</span><span><span> </span><span>r</span></span><span>,</span><span><span> </span><span>x0</span></span><span>=</span><span>0.0</span><span>,</span><span><span> </span><span>p0</span></span><span>=</span><span>1.0</span><span>):</span></div></div><div><div><div>2</div></div><div><span>    </span><span>import</span><span> numpy </span><span>as</span><span> np</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>kf </span><span>=</span><span> </span><span>KF1D</span><span>(x0, p0, q, r)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>n </span><span>=</span><span> </span><span>len</span><span>(z)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>x, p, k </span><span>=</span><span> np.</span><span>empty</span><span>(n), np.</span><span>empty</span><span>(n), np.</span><span>empty</span><span>(n)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>kf.</span><span>predict</span><span>(omega_m[i] </span><span>*</span><span> dt)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>kf.</span><span>update</span><span>(z[i])</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>x[i], p[i], k[i] </span><span>=</span><span> kf.x, kf.p, kf.k</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> x, p, k</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>初值 <span><span>x0=0x_0 = 0</span><span><span><span></span><span><span>x</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span>（真值在 <span><span>t=0t=0</span><span><span><span></span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 恰好是 0），<span><span>P0=1P_0 = 1</span><span><span><span></span><span><span>P</span><span><span><span><span><span><span></span><span><span>0</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span>（一个很大的不确定度，1 rad<span><span>2^2</span><span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span>，意思是”我不太知道”）。每一步先 <code>predict</code> 再 <code>update</code>。</p></section></section>
<section><h2>C、C++、Rust 实现<a href="#ccrust-实现"><span>#</span></a></h2><p>三个版本的结构完全一样，我把差别列在前面。</p><ul>
<li><strong>标量类型。</strong> 从这一章起我就把 <code>double</code>/<code>float</code> 做成可切换的，这样第 8 篇上 float32 时不用改任何一行滤波器代码。C 用 <code>kl_real.h</code> 里的 <code>typedef</code>，C++ 用模板参数 <code>T</code>（默认 <code>double</code>），Rust 用 <code>real.rs</code> 里的 <code>Real</code> 别名（<code>f32</code> 特性开关）。</li>
<li><strong>内存。</strong> 一维滤波器只有五个标量，是一个小 struct；调用者拥有它，库里没有任何 <code>malloc</code>、<code>new</code>、<code>Vec</code>。</li>
<li><strong>CSV。</strong> 读写 CSV 是主机端的事，放在 <code>c/src/csv.c</code>（用堆）、<code>cpp/include/kl/csv.hpp</code>、<code>rust/src/io.rs</code>（<code>std</code> 特性之后）里，库代码不碰它们。</li>
</ul><section><h3>C<a href="#c"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>kl_real x, p, q, r, k;</span></div></div><div><div><div>3</div></div><div><span>} kl_kf1d;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>kl_kf1d_predict</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>u_dt</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>+</span><span> u_dt;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>q</span><span>;</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>void</span><span> </span><span>kl_kf1d_update</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>z</span><span>)</span></span></div></div><div><div><div>12</div></div><div><span>{</span></div></div><div><div><div>13</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>k</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>/</span><span> (</span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>r</span><span>);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>k</span><span> </span><span>*</span><span> (z </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>x</span><span>);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>=</span><span> (</span><span>1</span><span> </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>k</span><span>) </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>p</span><span>;</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C 里没有方法，所以第一个参数显式传 struct 指针，函数名带 <code>kl_kf1d_</code> 前缀当命名空间。初始化函数 <code>kl_kf1d_init</code> 只是给五个字段赋值。</p>
完整源码：c/include/kl/kl_real.h、c/include/kl/kl_kf1d.h、c/src/kl_kf1d.c<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* Scalar type switch: double by default, float with -DKL_USE_FLOAT. */</span></div></div><div><div><div>2</div></div><div><span>#ifndef</span><span> </span><span>KL_REAL_H</span></div></div><div><div><div>3</div></div><div><span>#define</span><span> </span><span>KL_REAL_H</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;math.h&gt;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#ifdef</span><span> </span><span>KL_USE_FLOAT</span></div></div><div><div><div>8</div></div><div><span>typedef</span><span> </span><span>float</span><span> kl_real;</span></div></div><div><div><div>9</div></div><div><span>#define</span><span> </span><span>KL_SQRT</span><span> sqrtf</span></div></div><div><div><div>10</div></div><div><span>#define</span><span> </span><span>KL_SIN</span><span> sinf</span></div></div><div><div><div>11</div></div><div><span>#define</span><span> </span><span>KL_COS</span><span> cosf</span></div></div><div><div><div>12</div></div><div><span>#define</span><span> </span><span>KL_ATAN2</span><span> atan2f</span></div></div><div><div><div>13</div></div><div><span>#define</span><span> </span><span>KL_ASIN</span><span> asinf</span></div></div><div><div><div>14</div></div><div><span>#define</span><span> </span><span>KL_FABS</span><span> fabsf</span></div></div><div><div><div>15</div></div><div><span>#else</span></div></div><div><div><div>16</div></div><div><span>typedef</span><span> </span><span>double</span><span> kl_real;</span></div></div><div><div><div>17</div></div><div><span>#define</span><span> </span><span>KL_SQRT</span><span> sqrt</span></div></div><div><div><div>18</div></div><div><span>#define</span><span> </span><span>KL_SIN</span><span> sin</span></div></div><div><div><div>19</div></div><div><span>#define</span><span> </span><span>KL_COS</span><span> cos</span></div></div><div><div><div>20</div></div><div><span>#define</span><span> </span><span>KL_ATAN2</span><span> atan2</span></div></div><div><div><div>21</div></div><div><span>#define</span><span> </span><span>KL_ASIN</span><span> asin</span></div></div><div><div><div>22</div></div><div><span>#define</span><span> </span><span>KL_FABS</span><span> fabs</span></div></div><div><div><div>23</div></div><div><span>#endif</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#ifndef</span><span> </span><span>KL_KF1D_H</span></div></div><div><div><div>2</div></div><div><span>#define</span><span> </span><span>KL_KF1D_H</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>"kl/kl_real.h"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>typedef</span><span> </span><span>struct</span><span> {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>kl_real x, p, q, r, k;</span></div></div><div><div><div>8</div></div><div><span>} kl_kf1d;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>void</span><span> </span><span>kl_kf1d_init</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>x0</span><span>,</span><span> kl_real </span><span>p0</span><span>,</span><span> kl_real </span><span>q</span><span>,</span><span> kl_real </span><span>r</span><span>);</span></span></div></div><div><div><div>11</div></div><div><span>/* x- = x + u_dt, P- = P + q */</span></div></div><div><div><div>12</div></div><div><span>void</span><span> </span><span>kl_kf1d_predict</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>u_dt</span><span>);</span></span></div></div><div><div><div>13</div></div><div><span>/* K = P-/(P- + r), x = x- + K (z - x-), P = (1 - K) P- */</span></div></div><div><div><div>14</div></div><div><span>void</span><span> </span><span>kl_kf1d_update</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>z</span><span>);</span></span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>"kl/kl_kf1d.h"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>void</span><span> </span><span>kl_kf1d_init</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>x0</span><span>,</span><span> kl_real </span><span>p0</span><span>,</span><span> kl_real </span><span>q</span><span>,</span><span> kl_real </span><span>r</span><span>)</span></span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>=</span><span> x0;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>=</span><span> p0;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>q</span><span> </span><span>=</span><span> q;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>r</span><span> </span><span>=</span><span> r;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>void</span><span> </span><span>kl_kf1d_predict</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>u_dt</span><span>)</span></span></div></div><div><div><div>13</div></div><div><span>{</span></div></div><div><div><div>14</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>+</span><span> u_dt;</span></div></div><div><div><div>15</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>q</span><span>;</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>void</span><span> </span><span>kl_kf1d_update</span><span><span>(</span><span>kl_kf1d </span></span><span>*</span><span><span>f</span><span>,</span><span> kl_real </span><span>z</span><span>)</span></span></div></div><div><div><div>19</div></div><div><span>{</span></div></div><div><div><div>20</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>k</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>/</span><span> (</span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>r</span><span>);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>=</span><span> </span><span>f</span><span>-&gt;</span><span>x</span><span> </span><span>+</span><span> </span><span>f</span><span>-&gt;</span><span>k</span><span> </span><span>*</span><span> (z </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>x</span><span>);</span></div></div><div><div><div>22</div></div><div><span>  </span><span>f</span><span>-&gt;</span><span>p</span><span> </span><span>=</span><span> (</span><span>1</span><span> </span><span>-</span><span> </span><span>f</span><span>-&gt;</span><span>k</span><span>) </span><span>*</span><span> </span><span>f</span><span>-&gt;</span><span>p</span><span>;</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>C++<a href="#c-1"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>2</div></div><div><span>class</span><span> </span><span>KF1D</span><span> {</span></div></div><div><div><div>3</div></div><div><span>public:</span></div></div><div><div><div>4</div></div><div><span>  </span><span>T</span><span> </span><span>x</span><span>, </span><span>p</span><span>, </span><span>q</span><span>, </span><span>r</span><span>, </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>KF1D</span><span>(</span><span>T</span><span><span> </span><span>x0</span><span>, </span></span><span>T</span><span><span> </span><span>p0</span><span>, </span></span><span>T</span><span><span> </span><span>q</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>) : </span></span><span>x</span><span>(</span><span>x0</span><span>), </span><span>p</span><span>(</span><span>p0</span><span>), </span><span>q</span><span>(</span><span>q</span><span>), </span><span>r</span><span>(</span><span>r</span><span>) {}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>T</span><span><span> </span><span>u_dt</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>u_dt</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>p</span><span> </span><span>=</span><span> </span><span>p</span><span> </span><span>+</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>  </span><span>void</span><span> </span><span>update</span><span>(</span><span>T</span><span><span> </span><span>z</span><span>) {</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>k</span><span> </span><span>=</span><span> </span><span>p</span><span> </span><span>/</span><span> (</span><span>p</span><span> </span><span>+</span><span> </span><span>r</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>k</span><span> </span><span>*</span><span> (</span><span>z</span><span> </span><span>-</span><span> </span><span>x</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>p</span><span> </span><span>=</span><span> (</span><span>1</span><span> </span><span>-</span><span> </span><span>k</span><span>) </span><span>*</span><span> </span><span>p</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>整个类是头文件里的模板，没有任何运行期开销：编译器看得到全部代码，<code>KF1D&lt;float&gt;</code> 和 <code>KF1D&lt;double&gt;</code> 各自生成一份。字段保持 public，因为这个滤波器小到封装只会碍事。</p>
完整源码：cpp/include/kl/kf1d.hpp<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#pragma</span><span> </span><span>once</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>namespace</span><span><span> </span><span>kl</span><span> {</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>T</span><span> </span><span>=</span><span> double&gt;</span></div></div><div><div><div>6</div></div><div><span>class</span><span> </span><span>KF1D</span><span> {</span></div></div><div><div><div>7</div></div><div><span>public:</span></div></div><div><div><div>8</div></div><div><span>  </span><span>T</span><span> </span><span>x</span><span>, </span><span>p</span><span>, </span><span>q</span><span>, </span><span>r</span><span>, </span><span>k</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>  </span><span>KF1D</span><span>(</span><span>T</span><span><span> </span><span>x0</span><span>, </span></span><span>T</span><span><span> </span><span>p0</span><span>, </span></span><span>T</span><span><span> </span><span>q</span><span>, </span></span><span>T</span><span><span> </span><span>r</span><span>) : </span></span><span>x</span><span>(</span><span>x0</span><span>), </span><span>p</span><span>(</span><span>p0</span><span>), </span><span>q</span><span>(</span><span>q</span><span>), </span><span>r</span><span>(</span><span>r</span><span>) {}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>// x- = x + u_dt, P- = P + q</span></div></div><div><div><div>13</div></div><div><span>  </span><span>void</span><span> </span><span>predict</span><span>(</span><span>T</span><span><span> </span><span>u_dt</span><span>) {</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>u_dt</span><span>;</span></div></div><div><div><div>15</div></div><div><span>    </span><span>p</span><span> </span><span>=</span><span> </span><span>p</span><span> </span><span>+</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>// K = P-/(P- + r), x = x- + K (z - x-), P = (1 - K) P-</span></div></div><div><div><div>19</div></div><div><span>  </span><span>void</span><span> </span><span>update</span><span>(</span><span>T</span><span><span> </span><span>z</span><span>) {</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>k</span><span> </span><span>=</span><span> </span><span>p</span><span> </span><span>/</span><span> (</span><span>p</span><span> </span><span>+</span><span> </span><span>r</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>x</span><span> </span><span>=</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>k</span><span> </span><span>*</span><span> (</span><span>z</span><span> </span><span>-</span><span> </span><span>x</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>p</span><span> </span><span>=</span><span> (</span><span>1</span><span> </span><span>-</span><span> </span><span>k</span><span>) </span><span>*</span><span> </span><span>p</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>};</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>}</span><span>  // namespace kl</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>Rust<a href="#rust"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Kf1d</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>x</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>p</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>q</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>r</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>k</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>impl</span><span> </span><span>Kf1d</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>u_dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>+=</span><span> </span></span><span>u_dt</span><span>;</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>+=</span><span> </span></span><span>self</span><span>.q;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span><span>.k </span><span>=</span><span> </span></span><span>self</span><span>.p / (</span><span>self</span><span>.p + </span><span>self</span><span>.r);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>+=</span><span> </span></span><span>self</span><span>.k * (</span><span>z</span><span> - </span><span>self</span><span>.x);</span></div></div><div><div><div>18</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>*=</span><span> </span></span><span>1.0</span><span> - </span><span>self</span><span>.k;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Rust 版把 <code>q</code> 和 <code>r</code> 设成私有，滤波器运行时它们不应该被外部改，状态 <code>x</code>、<code>p</code>、<code>k</code> 则公开可读。<code>&amp;mut self</code> 在类型层面声明”这个方法会改状态”，<code>p *= 1.0 - self.k</code> 用的是上一行刚算出来的新增益，顺序和另外三个版本一致。</p>
完整源码：rust/src/kf1d.rs、rust/src/real.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>real</span><span>::</span></span><span>Real</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Kf1d</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>x</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>p</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>q</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>r</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>pub</span><span> </span><span>k</span><span>: </span><span>Real</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>Kf1d</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>x0</span><span>: </span><span>Real</span><span>, </span><span>p0</span><span>: </span><span>Real</span><span>, </span><span>q</span><span>: </span><span>Real</span><span>, </span><span>r</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Self</span><span> { </span><span>x</span><span>: </span><span>x0</span><span>, </span><span>p</span><span>: </span><span>p0</span><span>, </span><span>q</span><span>, </span><span>r</span><span>, </span><span>k</span><span>: </span><span>0.0</span><span> }</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>/// x- = x + u_dt, P- = P + q</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>predict</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>u_dt</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>+=</span><span> </span></span><span>u_dt</span><span>;</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>+=</span><span> </span></span><span>self</span><span>.q;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>/// K = P-/(P- + r), x = x- + K (z - x-), P = (1 - K) P-</span></div></div><div><div><div>23</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>update</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>z</span><span>: </span><span>Real</span><span>) {</span></div></div><div><div><div>24</div></div><div><span>        </span><span>self</span><span><span>.k </span><span>=</span><span> </span></span><span>self</span><span>.p / (</span><span>self</span><span>.p + </span><span>self</span><span>.r);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>self</span><span><span>.x </span><span>+=</span><span> </span></span><span>self</span><span>.k * (</span><span>z</span><span> - </span><span>self</span><span>.x);</span></div></div><div><div><div>26</div></div><div><span>        </span><span>self</span><span><span>.p </span><span>*=</span><span> </span></span><span>1.0</span><span> - </span><span>self</span><span>.k;</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Scalar type switch: `f64` by default, `f32` under the `f32` cargo feature.</span></div></div><div><div><div>2</div></div><div><span>//! Math goes through `libm` so the library works under `no_std`.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[cfg(not(feature </span><span>=</span><span> </span><span>"f32"</span><span>))]</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>type</span><span> </span><span>Real</span><span><span> </span><span>=</span><span> </span></span><span>f64</span><span>;</span></div></div><div><div><div>6</div></div><div><span>#[cfg(feature </span><span>=</span><span> </span><span>"f32"</span><span>)]</span></div></div><div><div><div>7</div></div><div><span>pub</span><span> </span><span>type</span><span> </span><span>Real</span><span><span> </span><span>=</span><span> </span></span><span>f32</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#[cfg(not(feature </span><span>=</span><span> </span><span>"f32"</span><span>))]</span></div></div><div><div><div>10</div></div><div><span>mod</span><span> m {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>use</span><span><span> </span><span>libm</span><span>::{asin, atan2, cos, fabs, sin, sqrt};</span></span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div><span>#[cfg(feature </span><span>=</span><span> </span><span>"f32"</span><span>)]</span></div></div><div><div><div>14</div></div><div><span>mod</span><span> m {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>use</span><span><span> </span><span>libm</span><span>::{asinf </span></span><span>as</span><span> asin, atan2f </span><span>as</span><span> atan2, cosf </span><span>as</span><span> cos, fabsf </span><span>as</span><span> fabs, sinf </span><span>as</span><span> sin, sqrtf </span><span>as</span><span> sqrt};</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>#[inline]</span></div></div><div><div><div>19</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>sqrt</span><span>(</span><span>x</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>m</span><span>::</span><span>sqrt</span><span>(</span><span>x</span><span>)</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div><span>#[inline]</span></div></div><div><div><div>23</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>sin</span><span>(</span><span>x</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>m</span><span>::</span><span>sin</span><span>(</span><span>x</span><span>)</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div><span>#[inline]</span></div></div><div><div><div>27</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>cos</span><span>(</span><span>x</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>m</span><span>::</span><span>cos</span><span>(</span><span>x</span><span>)</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div><span>#[inline]</span></div></div><div><div><div>31</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>atan2</span><span>(</span><span>y</span><span>: </span><span>Real</span><span>, </span><span>x</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>m</span><span>::</span><span>atan2</span><span>(</span><span>y</span><span>, </span><span>x</span><span>)</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div><div><div><div>34</div></div><div><span>#[inline]</span></div></div><div><div><div>35</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>asin</span><span>(</span><span>x</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>m</span><span>::</span><span>asin</span><span>(</span><span>x</span><span>)</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div><div><div><div>38</div></div><div><span>#[inline]</span></div></div><div><div><div>39</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>fabs</span><span>(</span><span>x</span><span>: </span><span>Real</span><span>) -&gt; </span><span>Real</span><span> {</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>m</span><span>::</span><span>fabs</span><span>(</span><span>x</span><span>)</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个 app（<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/c/apps/ch01.c" target="_blank"><code>c/apps/ch01.c</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/cpp/apps/ch01.cpp" target="_blank"><code>cpp/apps/ch01.cpp</code></a>、<a href="https://github.com/bfmhno3/kalman-lab/tree/tutorial-v1/rust/src/bin/ch01.rs" target="_blank"><code>rust/src/bin/ch01.rs</code></a>）读数据、循环 <code>predict</code>/<code>update</code>、写 CSV，没有任何值得讲的东西，我就不贴了。</p></section></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p>先看一眼滤波输出。黑线是真值，点是加速度计角度 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span>，红线是滤波器：</p><p></p><figure><img src="/assets/images/kalman-filter/ch01-tilt-estimate.svg" alt="倾角估计：加速度计测量、真值与卡尔曼滤波" /><figcaption>倾角估计：加速度计测量、真值与卡尔曼滤波</figcaption></figure><p></p><p>下面是 <code>make -s ch01</code> 的输出（<code>-s</code> 去掉 make 自己的回显，编译日志不在里面）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make -s ch01</span></div></div><div><div><div>2</div></div><div><span>ok   tilt_b0.05_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>3</div></div><div><span>ok   tilt_b0.05_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>4</div></div><div><span>ok   tilt_b0.05_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>5</div></div><div><span>ok   tilt_b0_c.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>6</div></div><div><span>ok   tilt_b0_cpp.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>7</div></div><div><span>ok   tilt_b0_rs.csv: max diff 0.000e+00 &lt;= 1.0e-09</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>RMSE (deg), seed 1, 2000 steps</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>bias (rad/s)  gyro-only  accel-only       KF</span></div></div><div><div><div>11</div></div><div><span><span>             </span></span><span>0      1.097       2.863    0.277</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>0.05     32.090       2.863    2.662</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>K_inf analytic = 0.009950, numeric (k=2000) = 0.009950</span></div></div><div><div><div>15</div></div><div><span>P-_inf analytic = 2.512531e-05</span></div></div><div><div><div>16</div></div><div><span>1-K_inf = 0.990050  (EMA time constant ~ 1.005 s)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三种语言和 Python 的最大差值都是 <code>0.000e+00</code>，一个比特都不差。这不奇怪：这个滤波器只有加减乘除，IEEE 754 规定这几个运算的结果必须是正确舍入的，只要编译器不重排运算（<code>-O2</code> 不会，没有 <code>-ffast-math</code>），四种语言算出来的就是同一串二进制。进了第 2 篇的矩阵运算和第 4 篇的 <code>sin</code>/<code>cos</code> 之后就没有这么好的事了，所以才需要 <span><span>10−910^{-9}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>9</span></span></span></span></span></span></span></span></span></span></span></span> 的容差。</p><p>数值解和解析解对上了：2000 步之后的增益是 0.009950，和公式算出的 <span><span>K∞K_\infty</span><span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 在六位小数内一致。收敛得很快：</p><p></p><figure><img src="/assets/images/kalman-filter/ch01-gain-convergence.svg" alt="增益 K 和协方差 P 在前 200 步内收敛到稳态" /><figcaption>增益 K 和协方差 P 在前 200 步内收敛到稳态</figcaption></figure><p></p><p><strong>零偏会发生什么。</strong> 表里第二行是我故意加了 0.05 rad/s 的陀螺仪零偏之后的结果。陀螺仪单独积分直接飞走：32 度的 RMSE（20 秒内累积 <span><span>0.05×20=10.05\times 20 = 1</span><span><span><span></span><span>0.05</span><span></span><span>×</span><span></span></span><span><span></span><span>20</span><span></span><span>=</span><span></span></span><span><span></span><span>1</span></span></span></span> rad，约 57 度的漂移）。滤波器没有飞，因为加速度计一直在拉它，但是 RMSE 从 0.277 度退化到 2.662 度，几乎和加速度计单独用（2.863 度）一样差。我们几乎白忙了一场。</p><p>原因可以算出来。令 <span><span>ek=x^k−θke_k = \hat x_k - \theta_k</span><span><span><span></span><span><span>e</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span><span><span><span><span><span><span></span><span>x</span></span><span><span></span><span><span>^</span></span></span></span></span></span></span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>−</span><span></span></span><span><span></span><span><span>θ</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span> 是稳态下的估计误差，忽略噪声，每一步预测引入 <span><span>bΔtb\Delta t</span><span><span><span></span><span>b</span><span>Δ</span><span>t</span></span></span></span> 的额外偏差，更新把它缩小到原来的 <span><span>(1−K∞)(1-K_\infty)</span><span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span></span></span></span>：</p><span><span><span>e=(1−K∞)(e+bΔt)  ⇒  e=1−K∞K∞ bΔt=0.990050.00995×0.0005=0.0498 rad≈2.85∘e = (1-K_\infty)(e + b\Delta t)\;\Rightarrow\; e = \frac{1-K_\infty}{K_\infty}\,b\Delta t = \frac{0.99005}{0.00995}\times 0.0005 = 0.0498\ \text{rad}\approx 2.85^\circ</span><span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span>(</span><span>1</span><span></span><span>−</span><span></span></span><span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>)</span><span>(</span><span>e</span><span></span><span>+</span><span></span></span><span><span></span><span>b</span><span>Δ</span><span>t</span><span>)</span><span></span><span></span><span>⇒</span><span></span><span></span></span><span><span></span><span>e</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span><span><span></span><span></span></span><span><span></span><span><span>1</span><span></span><span>−</span><span></span><span><span>K</span><span><span><span><span><span><span></span><span><span>∞</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>b</span><span>Δ</span><span>t</span><span></span><span>=</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>0.00995</span></span></span><span><span></span><span></span></span><span><span></span><span><span>0.99005</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>0.0005</span><span></span><span>=</span><span></span></span><span><span></span><span>0.0498</span><span> </span><span><span>rad</span></span><span></span><span>≈</span><span></span></span><span><span></span><span>2.8</span><span><span>5</span><span><span><span><span><span><span></span><span><span>∘</span></span></span></span></span></span></span></span></span></span></span></span><p>实测最后 1000 步的平均误差是 2.858 度。常数零偏被滤波器当成了一个固定的偏移量：它在系统里是一个没有被建模的状态。要消除它，唯一的办法是把零偏 <span><span>bb</span><span><span><span></span><span>b</span></span></span></span> 也放进状态里去估计。这就是第 2 篇要做的事，一个状态变两个状态，标量变矩阵。</p><p><strong><span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 是不是真的取对了。</strong> 我把 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 乘以 <span><span>10−210^{-2}</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span><span>−</span><span>2</span></span></span></span></span></span></span></span></span></span></span></span> 到 <span><span>10210^2</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 重跑了无零偏的数据：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>q sweep (bias 0)</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>q x 0.01   RMSE 0.464 deg, K_inf 0.0010</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>q x 0.1    RMSE 0.343 deg, K_inf 0.0032</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>q x 1      RMSE 0.277 deg, K_inf 0.0100</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>q x 10     RMSE 0.368 deg, K_inf 0.0311</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>q x 100    RMSE 0.636 deg, K_inf 0.0951</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/kalman-filter/ch01-q-sweep.svg" alt="RMSE 随 q 缩放倍数的变化，最低点在 1 倍（即由传感器噪声直接算出的 q）" /><figcaption>RMSE 随 q 缩放倍数的变化，最低点在 1 倍（即由传感器噪声直接算出的 q）</figcaption></figure><p></p><p>最低点恰好在 <span><span>q×1q\times 1</span><span><span><span></span><span>q</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span></span></span></span>，也就是我们用 <span><span>(σnΔt)2(\sigma_n\Delta t)^2</span><span><span><span></span><span>(</span><span><span>σ</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span>Δ</span><span>t</span><span><span>)</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span></span></span></span> 算出来的值。偏小，滤波器过于相信陀螺仪，对 <span><span>zz</span><span><span><span></span><span>z</span></span></span></span> 的纠正太慢；偏大，对加速度计的噪声太敏感。我不会说”这证明了卡尔曼滤波最优”，只有一个种子，一组参数，但至少它说明：当模型和数据吻合时，不用调参，由噪声标准差直接换算的 <span><span>qq</span><span><span><span></span><span>q</span></span></span></span> 就落在最好的位置。怎么判断”模型和数据吻合”，是第 3 篇的主题。</p></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p>一个状态只能描述一个角度。真实的陀螺仪有零偏，零偏会缓慢漂移，这要求状态变成 <span><span>[θ,b]T[\theta, b]^T</span><span><span><span></span><span>[</span><span>θ</span><span>,</span><span></span><span>b</span><span><span>]</span><span><span><span><span><span><span></span><span><span>T</span></span></span></span></span></span></span></span></span></span></span>，方差变成协方差矩阵，乘法变成矩阵乘法，<span><span>1/(P−+r)1/(P^-+r)</span><span><span><span></span><span>1/</span><span>(</span><span><span>P</span><span><span><span><span><span><span></span><span><span>−</span></span></span></span></span></span></span></span><span></span><span>+</span><span></span></span><span><span></span><span>r</span><span>)</span></span></span></span> 变成矩阵求逆（我们会用 Cholesky 分解绕开它）。第 2 篇从多元高斯的条件分布推导出一般形式的卡尔曼滤波，把第 1 篇的公式一次性升级，并把本篇几行的 C 代码换成一个手写的矩阵库。零偏那一行 2.662 度的 RMSE，到时候会降到什么水平，我们直接看数据。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-15-engineering/</id>
      <title type="text">收尾：Cargo features、测试、lint 与 CI</title>
      <published>2026-10-13T14:00:00.000Z</published>
      <updated>2026-10-13T14:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-15-engineering/"/>
      <summary type="text">最后一篇没有新语法。我们把 pebble repl 做成可选的 Cargo feature，看清单元、集成和文档测试的分工，让 rustdoc、clippy、rustfmt 把风格交给机器，读完 CI 配置，再把整个工作区与 tag 做最后一次逐字节对比。</summary>
      <content type="html"><![CDATA[<p>Pebble 现在有 16 个 crate、10515 行 Rust、214 行 C，<code>Cargo.lock</code> 里锁了 143 个包，<code>cargo test --workspace --all-features</code> 跑 168 个测试。这一篇没有新语法，我想讲的是另一件事：怎样让这堆东西在我不看它的时候也保持正确。打 tag 之前，我在一个干净的工作树里把 CI 里的 rustdoc 命令跑了一遍，它在三个 crate 上报错，原因是我在文档注释里写了三个指向不存在或私有条目的链接。也就是说，如果我当时直接推送，这个仓库的 docs 任务会是红的，而我写了十四篇文章讲”Rust 编译器会替你检查”。这一篇就是讲怎么把”检查”从编译器扩展到整个仓库。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>做完这一篇，你的 <code>pebble-cli</code> 会是最终版：<code>repl</code> 成了一个默认开启的 Cargo feature，<code>cargo build -p pebble-cli --no-default-features</code> 得到一个没有 <code>pebble repl</code> 的、小 24% 的二进制。整个工作区（除了 <code>.github/</code>、文档和许可证文件，它们直接从 tag 复制，以及重建不需要的 <code>lab/</code> 实验室）与 tag <code>tutorial-v1</code> 逐字节一致，<code>cargo test --workspace --all-features</code> 通过 168 个测试。会用到的东西：Cargo features、<code>cfg</code>、单元/集成/文档测试、rustdoc 和 intra-doc 链接、工作区 lint、clippy、rustfmt、<code>rust-toolchain.toml</code>、发布 profile、CI。上一篇在 <a href="/posts/rust-by-pebble-14-concurrency-async/">并发与 async</a>，命令行第五版就是从那里来的。</p></div></div>
<p>这一篇的结构：先用五小步把命令行从第五版升到 tag 的最终版（每一步都让编译器说一句话）；然后过一遍测试、文档、lint 和配置文件，每样东西都用仓库里真实的文件说话；然后读 CI；然后做最后的对比；最后是一张”Rust 特性对应哪一篇”的索引表。</p>
<section><h2>命令行终版：让 <code>repl</code> 变成可选的<a href="#命令行终版让-repl-变成可选的"><span>#</span></a></h2><p>第 9 篇我们给命令行加了 <code>pebble repl</code>，它靠 <code>rustyline</code> 这个第三方库提供行编辑、历史和方向键。这是命令行里唯一一个纯粹为交互而存在的依赖。如果有人只想要 <code>run</code> 和 <code>check</code>（比如在 CI 里、在容器里、在一个没有终端的地方），他仍然要下载和编译 <code>rustyline</code> 以及它的依赖。C 里的做法是 <code>#ifdef WITH_READLINE</code>，用 CMake 的 <code>option()</code> 开关。Cargo 的对应物叫 <strong>feature</strong>。</p><p>我们从第 14 篇结束时的第五版出发。如果你一路跟下来，你的 <code>crates/pebble-cli</code> 就是它；如果你是跳着来的，第四版的完整文件在第 12 篇，第五版 = 第四版加上第 14 篇的那几处改动（<code>CheckArgs</code>、<code>check</code>、<code>check_file</code> 和两个测试）。</p><section><h3>先问一句：现在有没有办法不要 <code>repl</code>？<a href="#先问一句现在有没有办法不要-repl"><span>#</span></a></h3><p>第一反应：Cargo 有 <code>--no-default-features</code>，试试。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-cli --no-default-features</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-cli v0.1.0 (/tmp/pebble-rebuild-15/crates/pebble-cli)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.33s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译通过，听上去成功了。看看依赖：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo tree -p pebble-cli --no-default-features -e normal | grep rustyline</span></div></div><div><div><div>2</div></div><div><span>└── rustyline v18.0.1</span></div></div><div><div><div>3</div></div><div><span>$ ./target/debug/pebble repl &lt;/dev/null</span></div></div><div><div><div>4</div></div><div><span>Pebble REPL. Type :help for commands, :quit to exit.</span></div></div><div><div><div>5</div></div><div><span>pebble&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>rustyline</code> 还在，<code>repl</code> 也还在。原因很朴素：<code>--no-default-features</code> 的意思是”不要开启 <code>default</code> 这个 feature 里列的东西”，而我们的 <code>Cargo.toml</code> 里根本没有写任何 <code>[features]</code>，所以没有什么可关的。Cargo 不会因此报错，这个开关在这里是空操作。</p></section><section><h3>第一步：把依赖标成 <code>optional</code><a href="#第一步把依赖标成-optional"><span>#</span></a></h3><p>在 <code>crates/pebble-cli/Cargo.toml</code> 里，把 <code>rustyline.workspace = true</code> 改成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>rustyline</span><span> = { </span><span>workspace</span><span> = </span><span>true</span><span>, </span><span>optional</span><span> = </span><span>true</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>optional = true</code> 的意思是”这个依赖默认不编译，除非某个 feature 点名要它”。现在不加任何 feature，直接构建：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-cli</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-cli v0.1.0 (/tmp/pebble-rebuild-15/crates/pebble-cli)</span></div></div><div><div><div>3</div></div><div><span>error[E0433]: cannot find module or crate `rustyline` in this scope</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-cli/src/repl.rs:15:5</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>15 | use rustyline::error::ReadlineError;</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|     ^^^^^^^^^ use of unresolved module or unlinked crate `rustyline`</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>= help: if you wanted to use a crate named `rustyline`, use `cargo add rustyline` to add it to your `Cargo.toml`</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>error[E0432]: unresolved import `rustyline`</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div><div><div><div>13</div></div><div><span>error: could not compile `pebble-cli` (bin "pebble") due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个错误是对的：我们声明了一个”默认不编译”的依赖，却没有说什么时候该编译它，<code>repl.rs</code> 里的 <code>use rustyline::...</code> 就悬空了。</p></section><section><h3>第二步：写 <code>[features]</code><a href="#第二步写-features"><span>#</span></a></h3><p>在同一个文件的 <code>[dev-dependencies]</code> 前面加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>features</span><span>]</span></div></div><div><div><div>2</div></div><div><span>default</span><span> = [</span><span>"repl"</span><span>]</span></div></div><div><div><div>3</div></div><div><span># The interactive `pebble repl` subcommand (pulls in the line editor).</span></div></div><div><div><div>4</div></div><div><span>repl</span><span> = [</span><span>"dep:rustyline"</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读法：<code>repl</code> 是一个 feature 的名字，它开启时会启用 <code>rustyline</code> 这个可选依赖（<code>dep:</code> 前缀的意思是”我指的是依赖本身，不是另一个同名 feature”）；<code>default</code> 是一个特殊的 feature 名，列在里面的东西在没人指定时自动开启。所以普通的 <code>cargo build</code> 仍然带 <code>repl</code>，行为不变：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-cli</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-cli v0.1.0 (/tmp/pebble-rebuild-15/crates/pebble-cli)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.45s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而 <code>--no-default-features</code> 这次真的有东西可以关了，于是 <code>rustyline</code> 不再被编译，<code>repl.rs</code> 又找不到它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-cli --no-default-features</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-cli v0.1.0 (/tmp/pebble-rebuild-15/crates/pebble-cli)</span></div></div><div><div><div>3</div></div><div><span>error[E0433]: cannot find module or crate `rustyline` in this scope</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-cli/src/repl.rs:15:5</span></div></div><div><div><div>5</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个错误。现在轮到源代码：不开 <code>repl</code> 的时候，<code>repl.rs</code> 整个文件都不应该参与编译。</p></section><section><h3>第三步：<code>#[cfg(feature = "repl")]</code><a href="#第三步cfgfeature--repl"><span>#</span></a></h3><p>Cargo 把开启的 feature 作为 <code>--cfg 'feature="repl"'</code> 传给 rustc。源码里用 <code>#[cfg(...)]</code> 属性按它来取舍：条件不成立，被标注的那个条目就好像从来没有写过。这有点像 <code>#ifdef</code>，区别有两个：它作用在<strong>语法树的条目</strong>上（一个模块、一个函数、一个枚举变体、一条 <code>match</code> 分支），不是按行的文本替换；而且条目被取舍之前，rustc 仍然把它解析成合法的语法，所以你没法用 <code>#[cfg]</code> 藏一段根本不是 Rust 的东西。</p><p>先只给 <code>main.rs</code> 里的 <code>mod repl;</code> 加上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(feature </span><span>=</span><span> </span><span>"repl"</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> repl;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-cli --no-default-features</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-cli v0.1.0 (/tmp/pebble-rebuild-15/crates/pebble-cli)</span></div></div><div><div><div>3</div></div><div><span>error[E0433]: cannot find module or crate `repl` in this scope</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-cli/src/main.rs:30:26</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>30 |         Command::Repl =&gt; repl::run(),</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|                          ^^^^ use of unresolved module or unlinked crate `repl`</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>note: found an item that was configured out</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-cli/src/main.rs:17:5</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>16 | #[cfg(feature = "repl")]</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|       ---------------- the item is gated behind the `repl` feature</span></div></div><div><div><div>14</div></div><div><span>17 | mod repl;</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>|     ^^^^</span></div></div><div><div><div>16</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器很客气：它不仅指出 <code>repl</code> 找不到，还告诉你”有一个被 configured out 的条目叫这个名字，被 <code>repl</code> feature 守着”。这是 <code>cfg</code> 比 <code>#ifdef</code> 友好的地方：C 里你只会得到 <code>undeclared identifier</code>，然后自己去找是哪个宏把它藏起来了。</p><p>下一处是 <code>main</code> 里的 <code>match</code> 分支。给它加上同样的属性：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>        </span></span><span>#[cfg(feature </span><span>=</span><span> </span><span>"repl"</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>        </span><span>Command</span><span>::</span><span>Repl</span><span><span> =&gt; </span><span>repl</span><span>::</span></span><span>run</span><span>(),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-cli --no-default-features</span></div></div><div><div><div>2</div></div><div><span>error[E0004]: non-exhaustive patterns: `cli::Command::Repl` not covered</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-cli/src/main.rs:28:24</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>28 |     let result = match cli.command {</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|                        ^^^^^^^^^^^ pattern `cli::Command::Repl` not covered</span></div></div><div><div><div>7</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>match</code> 是穷尽性检查的（第 3 篇），分支没了，变体还在，于是编译器拒绝。最后一处是 <code>cli.rs</code> 里的枚举变体本身，把属性放在 <code>Repl,</code> 上面（文档注释前后都行，tag 里它在文档注释后面）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Start an interactive read-eval-print loop.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[cfg(feature </span><span>=</span><span> </span><span>"repl"</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Repl</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-cli --no-default-features</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-cli v0.1.0 (/tmp/pebble-rebuild-15/crates/pebble-cli)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.09s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个 <code>#[cfg(feature = "repl")]</code>，一个在 <code>mod repl;</code> 上，一个在 <code>match</code> 分支上，一个在枚举变体上，加上 <code>Cargo.toml</code> 的两处改动，就是整个”可选 <code>repl</code>”。这三处不是我随便选的：你可以把它们理解成”一个功能在程序里有三个落脚点：代码、入口、命令行的声明”，每一个都被编译器逼着你处理，漏掉哪个，哪个就报错。我在 <code>/tmp/pebble-rebuild-15</code> 里验证过，现在 <code>crates/pebble-cli</code> 下的 <code>Cargo.toml</code>、<code>cli.rs</code>、<code>main.rs</code> 与 tag 逐字节相同。</p></section><section><h3>看看效果<a href="#看看效果"><span>#</span></a></h3><p>没有 <code>repl</code> 的二进制，子命令列表里就没有它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ./target/debug/pebble --help</span></div></div><div><div><div>2</div></div><div><span>Pebble: a small scripting language and toolchain</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>Usage: pebble &lt;COMMAND&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Commands:</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>run     Execute a source file on the tree-walker or the bytecode VM</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>tokens  Print the token stream of a source file</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>parse   Print the parsed abstract syntax tree of a source file</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>check   Lex, parse, and compile source files without running them</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>bench   Time both backends on a source file</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>help    Print this message or the help of the given subcommand(s)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 clap 的派生宏读到的东西：派生宏拿到的枚举定义里，被 <code>cfg</code> 去掉的变体已经不在了，所以不需要任何额外代码。直接运行它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run -q -p pebble-cli --no-default-features -- repl</span></div></div><div><div><div>2</div></div><div><span>error: unrecognized subcommand 'repl'</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>tip: a similar subcommand exists: 'help'</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Usage: pebble &lt;COMMAND&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>For more information, try '--help'.</span></div></div><div><div><div>9</div></div><div><span>$ echo $?</span></div></div><div><div><div>10</div></div><div><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>退出码 2 是 clap 的”用法错误”，和第 7 篇的 <code>unknown_subcommands_are_usage_errors</code> 同一类。两个版本的体积和依赖，用发布 profile 各编一次：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -q -p pebble-cli --release &amp;&amp; ls -l target/release/pebble</span></div></div><div><div><div>2</div></div><div><span>1915528 ... target/release/pebble</span></div></div><div><div><div>3</div></div><div><span>$ cargo build -q -p pebble-cli --release --no-default-features &amp;&amp; ls -l target/release/pebble</span></div></div><div><div><div>4</div></div><div><span>1452928 ... target/release/pebble</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><span><span>(1915528−1452928)/1915528≈24%(1915528 - 1452928) / 1915528 \approx 24\%</span><span><span><span></span><span>(</span><span>1915528</span><span></span><span>−</span><span></span></span><span><span></span><span>1452928</span><span>)</span><span>/1915528</span><span></span><span>≈</span><span></span></span><span><span></span><span>24%</span></span></span></span>。依赖方面，<code>cargo tree -p pebble-cli -e normal --prefix none | sort -u | wc -l</code> 是 56，加上 <code>--no-default-features</code> 是 43，少了 13 个包：<code>cfg-if</code>、<code>endian-type</code>、<code>home</code>、<code>libc</code>、<code>log</code>、<code>memchr</code>、<code>nibble_vec</code>、<code>nix</code>、<code>radix_trie</code>、<code>rustyline</code>、<code>smallvec</code>、<code>unicode-segmentation</code>、<code>unicode-width</code>（两份清单 <code>sort -u</code> 之后 <code>diff</code> 的结果）。</p><p>测试也跟着变了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-cli --no-default-features</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>Running unittests src/main.rs (target/debug/deps/pebble-88f4cb96ddecbc63)</span></div></div><div><div><div>3</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>Running tests/cli.rs (target/debug/deps/cli-c82fb2694cfbd503)</span></div></div><div><div><div>5</div></div><div><span>test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>默认配置下是”3 个单元测试加 12 个集成测试”（第 14 篇的数字），没有 <code>repl</code> 时单元测试变成 0 个：那 3 个测试是 <code>repl.rs</code> 里 <code>MetaCommand</code> 的测试，文件整个不存在了，它们也就不存在了。12 个集成测试一个不少，因为它们只通过二进制的公开行为说话。</p></section><section><h3>为什么 CI 要单独跑一遍 <code>--no-default-features</code><a href="#为什么-ci-要单独跑一遍---no-default-features"><span>#</span></a></h3><p>这就是 CI 里那两行的作用。<code>cargo test --workspace --all-features</code> 只测”全开”这一种组合；“全关”这一种组合是另一个程序，它能因为一个忘了写的 <code>#[cfg]</code> 编不过去，而全开的组合永远发现不了。上面我们按顺序踩过的三个错误，每一个都是”全开通过、全关失败”的典型。所以 CI 的 <code>clippy</code> 任务多一行 <code>cargo clippy -p pebble-cli --no-default-features --all-targets -- -D warnings</code>（连死代码警告也算失败），<code>build</code> 任务多一行 <code>cargo build -p pebble-cli --no-default-features</code>。</p><p>关于这两行里的 <code>-p pebble-cli</code>，我顺手试了 <code>--workspace --no-default-features</code>：<code>cargo tree --workspace --no-default-features -e normal | grep -c rustyline</code> 输出 0，<code>cargo clippy --workspace --no-default-features --all-targets -- -D warnings</code> 也通过。在 resolver 3 下，<code>--no-default-features</code> 对选中的每个包都生效，所以 <code>-p pebble-cli</code> 只是更窄的写法，不是必要的写法。</p><div><div><div></div><div>feature 是”加法”</div></div><div><p>按 Cargo 的约定，feature 只能加不能减：一个 feature 打开时，它只应该增加功能，不应该让别的代码编译失败。原因是同一个依赖被工作区里的好几个 crate 引用时，Cargo 会取它们要求的 feature 的<strong>并集</strong>，只编译一份。如果 feature 能”减”，某个 crate 开了它，另一个 crate 就会莫名其妙地崩。<code>repl</code> 符合这条约定：开了只是多一个子命令。</p></div></div></section><section><h3><code>cfg</code> 还能干什么<a href="#cfg-还能干什么"><span>#</span></a></h3><p><code>cfg</code> 的条件不只有 feature。<code>target_os</code>、<code>target_pointer_width</code>、<code>debug_assertions</code>、<code>test</code> 都是。<code>#[cfg(test)] mod tests</code> 是第 1 篇就写过的。在 <code>~/rust-lab</code> 里的 <code>src/bin/ch15_cfg.rs</code>（完整版在 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch15_cfg.rs" target="_blank">lab/rust-lab/src/bin/ch15_cfg.rs</a>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(target_os </span><span>=</span><span> </span><span>"linux"</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>platform</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"linux"</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#[cfg(target_os </span><span>=</span><span> </span><span>"windows"</span><span>)]</span></div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>platform</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"windows"</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#[cfg(not(any(target_os </span><span>=</span><span> </span><span>"linux"</span><span>, target_os </span><span>=</span><span> </span><span>"windows"</span><span>)))]</span></div></div><div><div><div>12</div></div><div><span>fn</span><span> </span><span>platform</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>"something else"</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>println!</span><span>(</span><span>"compiled for {}"</span><span>, </span><span>platform</span><span>());</span></div></div><div><div><div>18</div></div><div><span>    </span><span>println!</span><span>(</span><span>"cfg! is an ordinary bool: {}"</span><span>, </span><span>cfg!</span><span>(</span><span>target_os</span><span><span> </span><span>=</span><span> </span></span><span>"linux"</span><span>));</span></div></div><div><div><div>19</div></div><div><span>    </span><span>println!</span><span>(</span><span>"debug assertions: {}"</span><span>, </span><span>cfg!</span><span>(</span><span>debug_assertions</span><span>));</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"pointer width: {}"</span><span>, </span><span>if</span><span> </span><span>cfg!</span><span>(</span><span>target_pointer_width</span><span><span> </span><span>=</span><span> </span></span><span>"64"</span><span>) { </span><span>64</span><span> } </span><span>else</span><span> { </span><span>32</span><span> });</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch15_cfg</span></div></div><div><div><div>2</div></div><div><span>compiled for linux</span></div></div><div><div><div>3</div></div><div><span>cfg! is an ordinary bool: true</span></div></div><div><div><div>4</div></div><div><span>debug assertions: true</span></div></div><div><div><div>5</div></div><div><span>pointer width: 64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个名字有三个定义，每次编译只留一个。<code>#[cfg(...)]</code> 是”这个条目存不存在”；<code>cfg!(...)</code> 是一个宏，展开成 <code>true</code> 或 <code>false</code>，两个分支的代码都要通过类型检查，所以它不能用来藏平台相关的 API。<code>ch15_cfg_missing.rs</code> 调用一个只在 Windows 上存在的函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch15_cfg_missing</span></div></div><div><div><div>2</div></div><div><span>error[E0425]: cannot find function `only_on_windows` in this scope</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch15_cfg_missing.rs:7:20</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>7 |     println!("{}", only_on_windows());</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                    ^^^^^^^^^^^^^^^ not found in this scope</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>note: found an item that was configured out</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch15_cfg_missing.rs:2:4</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>1 | #[cfg(target_os = "windows")]</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|       --------------------- the item is gated behind the `windows` feature</span></div></div><div><div><div>13</div></div><div><span>2 | fn only_on_windows() -&gt; &amp;'static str {</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|    ^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>(注意倒数第三行说 “<code>windows</code> feature”，其实是 <code>target_os</code>，这是 rustc 1.95.0 这一条提示的措辞不精确，不影响你读懂它。) Pebble 自己没有平台相关的代码，<code>pebble-sys</code> 里的 C 用 <code>cc</code> 编，在 Linux 和 macOS 上都通过（CI 的 <code>test</code> 任务在这两个系统上各跑一遍，下面会读到）。所以这一节是一个”T”：实验里编过，仓库里没用。</p></section></section>
<section><h2>测试：三种，各管一摊<a href="#测试三种各管一摊"><span>#</span></a></h2><p>整个系列写了这么多测试，我一直没有正式区分过它们。Cargo 认三种，位置不同，能力不同。在 <code>~/lab-ch15-tests</code> 里放一个最小的 crate（完整版在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch15-tests" target="_blank">lab/lab-ch15-tests</a>）：<code>src/lib.rs</code> 有一个公开函数 <code>bump</code>、一个私有函数 <code>secret</code>、一个单元测试模块，<code>bump</code> 的文档注释里有一个示例；<code>tests/api.rs</code> 是一个集成测试。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Adds one.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// ```</span></div></div><div><div><div>4</div></div><div><span>/// assert_eq!(lab_ch15_tests::bump(1), 2);</span></div></div><div><div><div>5</div></div><div><span>/// ```</span></div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>bump</span><span>(</span><span>x</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>secret</span><span>(</span><span>x</span><span>) + </span><span>1</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>secret</span><span>(</span><span>x</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>x</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>15</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>fn</span><span> </span><span>unit_tests_see_private_items</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>secret</span><span>(</span><span>5</span><span>), </span><span>5</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/lab_ch15_tests-70139acc84a05109)</span></div></div><div><div><div>3</div></div><div><span>test tests::unit_tests_see_private_items ... ok</span></div></div><div><div><div>4</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>Running tests/api.rs (target/debug/deps/api-1b9b8190a9ce54a6)</span></div></div><div><div><div>6</div></div><div><span>test integration_tests_use_the_public_api ... ok</span></div></div><div><div><div>7</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>Doc-tests lab_ch15_tests</span></div></div><div><div><div>9</div></div><div><span>test src/lib.rs - bump (line 3) ... ok</span></div></div><div><div><div>10</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个”Running”，三个不同的程序：</p><ul>
<li><strong>单元测试</strong>：写在被测代码同一个文件的 <code>#[cfg(test)] mod tests</code> 里，用 <code>use super::*</code> 进入同一个模块，所以能看见私有函数。它们被编进 crate 自己的测试二进制。Pebble 里的绝大多数测试（span、lexer、parser、value、interp、vm……）是这种。</li>
<li><strong>集成测试</strong>：<code>tests/</code> 目录下每个文件是一个独立的 crate，它像外部用户一样只能用公开 API。<code>tests/api.rs</code> 里写 <code>use lab_ch15_tests::secret;</code>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test --test private</span></div></div><div><div><div>2</div></div><div><span>error[E0603]: function `secret` is private</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; tests/private.rs:1:21</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>1 | use lab_ch15_tests::secret;</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|                     ^^^^^^ private function</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>note: the function `secret` is defined here</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>--&gt; src/lib.rs:10:1</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>10 | fn secret(x: u32) -&gt; u32 {</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>| ^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（仓库的 <code>lab/lab-ch15-tests</code> 里没有 <code>tests/private.rs</code>：它会让朴素的 <code>cargo test</code> 编译失败。要复现这条错误，自己建这个文件，内容就是 <code>use lab_ch15_tests::secret;</code>。）</p><p>Pebble 的集成测试是 <code>pebble-cli/tests/cli.rs</code>（用 <code>assert_cmd</code> 运行真的二进制）和 <code>pebble-bench/tests/differential.rs</code>（用 <code>#[differential]</code> 对比两个后端）。</p><ul>
<li><strong>文档测试</strong>：文档注释里围栏代码块是一个会被编译和运行的程序，它同样只能用公开 API，而且每个示例是单独编译的一个 crate。它保证的是”文档里写的例子是真的能跑的”。</li>
</ul><p>文档测试坏掉时是这样的（我把 <code>src/lib.rs</code> 里文档示例的 <code>2</code> 改成了 <code>3</code>；仓库里的是通过的版本）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test --doc</span></div></div><div><div><div>2</div></div><div><span>---- src/lib.rs - bump (line 3) stdout ----</span></div></div><div><div><div>3</div></div><div><span>Test executable failed (exit status: 101).</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>stderr:</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>thread 'main' (398174) panicked at /tmp/rustdoctestSdCLK2/doctest_bundle_2024.rs:6:1:</span></div></div><div><div><div>8</div></div><div><span>assertion `left == right` failed</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>left: 2</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>right: 3</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div><div><div><div>12</div></div><div><span>test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Pebble 里有 4 个 crate 带文档测试：<code>pebble-bytecode</code>（第 10 篇的 <code>chunk!</code>，真实输出 <code>test crates/pebble-bytecode/src/lib.rs - chunk (line 353) ... ok</code>）、<code>pebble-gc</code>、<code>pebble-nostd</code> 各 1 个，<code>pebble-sys</code> 2 个。<code>pebble-macros</code> 里有 3 个示例，标成了 <code>```ignore</code>，因为过程宏 crate 没法在自己的文档测试里用自己：它们的真正测试是 <code>pebble-ast</code>、<code>pebble-bytecode</code> 和 <code>pebble-bench</code> 的用法。</p><section><h3>全部加起来<a href="#全部加起来"><span>#</span></a></h3><p>在 <code>/tmp/pebble-rebuild-15</code> 里，<code>cargo test --workspace --all-features</code> 的真实结果（单元测试加集成测试，文档测试单列）：</p>

<table><thead><tr><th>crate</th><th>测试数</th><th>crate</th><th>测试数</th></tr></thead><tbody><tr><td><code>pebble-span</code></td><td>6</td><td><code>pebble-gc</code></td><td>13</td></tr><tr><td><code>pebble-diag</code></td><td>4</td><td><code>pebble-vm</code></td><td>14</td></tr><tr><td><code>pebble-lexer</code></td><td>12</td><td><code>pebble-sys</code></td><td>15</td></tr><tr><td><code>pebble-ast</code></td><td>4</td><td><code>pebble-nostd</code></td><td>10</td></tr><tr><td><code>pebble-macros</code></td><td>0</td><td><code>pebble-net</code></td><td>9</td></tr><tr><td><code>pebble-parser</code></td><td>13</td><td><code>pebble-cli</code></td><td>3 单元 + 12 集成</td></tr><tr><td><code>pebble-value</code></td><td>11</td><td><code>pebble-bench</code></td><td>2 单元 + 5 差分</td></tr><tr><td><code>pebble-interp</code></td><td>16</td><td><code>pebble-bytecode</code></td><td>14</td></tr></tbody></table><p>加起来 163 个；文档测试 5 个通过、3 个 <code>ignore</code>。我没有手算，是把 33 行 <code>test result</code> 的 <code>passed</code> 列用 <code>awk</code> 加了一遍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test --workspace --all-features 2&gt;&amp;1 | grep "test result" | awk '{p+=$4; i+=$8} END{print p, i}'</span></div></div><div><div><div>2</div></div><div><span>168 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>168 个通过，3 个被忽略，0 个失败。几个值得一提的细节：</p><ul>
<li>带 <code>#[should_panic]</code> 的测试有四个（<code>pebble-span</code>、<code>pebble-gc</code> 两个、<code>pebble-sys</code>）。它们证明的是”这个调用必须 panic，而且消息里含有这个字符串”。</li>
<li>带 <code>#[tokio::test]</code> 的是 <code>pebble-net</code> 里的四个（第 14 篇）。</li>
<li><code>pebble-bench</code> 的 Criterion 基准（第 12 篇）不在这次 <code>cargo test</code> 里：<code>cargo test</code> 的 <code>Running</code> 列表里没有 <code>backends</code>，要用 <code>cargo bench -p pebble-bench</code> 另外跑。</li>
<li>差分测试（五个）在 <code>Running tests/differential.rs</code> 下，名字都是 <code>*_backends_agree</code>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bench --test differential</span></div></div><div><div><div>2</div></div><div><span>test closures_keep_private_state_backends_agree ... ok</span></div></div><div><div><div>3</div></div><div><span>test lists_are_shared_by_reference_backends_agree ... ok</span></div></div><div><div><div>4</div></div><div><span>test strings_and_negative_indexing_backends_agree ... ok</span></div></div><div><div><div>5</div></div><div><span>test loops_with_break_and_continue_backends_agree ... ok</span></div></div><div><div><div>6</div></div><div><span>test recursion_and_arithmetic_backends_agree ... ok</span></div></div><div><div><div>7</div></div><div><span>test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第六个差分程序是被删掉的那个，原因在第 12 篇里。</p><p>最后一个需要说清的：这些数字都只是”代码被执行过且断言没有失败”。168 个测试不是正确性的证明，是一道下限。第 10 篇里我故意弄坏编译器，看测试漏掉了什么，这个习惯值得留下来。</p></section></section>
<section><h2>文档：<code>///</code>、<code>//!</code> 和 rustdoc<a href="#文档-和-rustdoc"><span>#</span></a></h2><p><code>///</code> 给后面那一项写文档，<code>//!</code> 给包含它的那一项写文档（文件开头的 <code>//!</code> 是整个 crate 的）。内容是 Markdown。仓库的规则（<code>CONTRIBUTING.md</code>）是每个公开条目都有 <code>///</code>。在 Markdown 里用 <code>[</code>Type<code>]</code> 这种写法的叫 <strong>intra-doc link</strong>：rustdoc 会按 Rust 的名字解析规则找 <code>Type</code>，生成指向它的链接。这个机制带来一个好处，也带来一个麻烦：它会检查。</p><p><code>cargo doc --workspace --no-deps</code> 生成 HTML，<code>--no-deps</code> 表示不为依赖生成。<code>RUSTDOCFLAGS="-D warnings"</code> 让 rustdoc 的警告变成错误，CI 里正是这么写的。我在 tag 之前那个提交（<code>ab20d9f</code> 的父提交）的工作树里跑了它，加上 <code>--keep-going</code> 让它把所有失败的 crate 报全：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features --keep-going</span></div></div><div><div><div>2</div></div><div><span>error: unresolved link to `cc`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; crates/pebble-sys/src/lib.rs:3:74</span></div></div><div><div><div>4</div></div><div><span>error: could not document `pebble-sys`</span></div></div><div><div><div>5</div></div><div><span>error: unresolved link to `pebble_vm`</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-value/src/lib.rs:17:51</span></div></div><div><div><div>7</div></div><div><span>error: public documentation for `pebble_parser` links to private item `Parser::synchronize`</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-parser/src/lib.rs:11:50</span></div></div><div><div><div>9</div></div><div><span>error: could not document `pebble-value`</span></div></div><div><div><div>10</div></div><div><span>error: could not document `pebble-parser`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>(这里我只保留了 <code>error</code> 和位置行。) 三个 crate，三个错误，三种原因。逐个看完整的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unresolved link to `pebble_vm`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-value/src/lib.rs:17:51</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>17 | //!   exactly the problem the garbage-collected [`pebble_vm`] backend solves.</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                                                   ^^^^^^^^^ no item named `pebble_vm` in scope</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: `-D rustdoc::broken-intra-doc-links` implied by `-D warnings`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pebble-value</code> 不依赖 <code>pebble-vm</code>（依赖方向反过来，第 12 篇），所以在 <code>pebble-value</code> 里没有叫 <code>pebble_vm</code> 的东西可以链接。<code>cc</code> 那一个同理：它是 <code>pebble-sys/build.rs</code> 的构建依赖，不在 crate 本身的作用域里。第三个更微妙：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: public documentation for `pebble_parser` links to private item `Parser::synchronize`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-parser/src/lib.rs:11:50</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>11 | //! * It **recovers**. When a statement fails, [`Parser::synchronize`] skips to</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                                                  ^^^^^^^^^^^^^^^^^^^ this item is private</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= note: this link will resolve properly if you pass `--document-private-items`</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: `-D rustdoc::private-intra-doc-links` implied by `-D warnings`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>synchronize</code> 是 <code>Parser</code> 的私有方法：公开文档里不该链接到一个读者点不进去的页面。三个都被我改成了普通反引号（<code>`Parser::synchronize`</code>）。这三处修改就是 tag 之前的提交 <code>ab20d9f</code>（<code>docs: describe the tutorial additions</code>）里的三行改动，<code>CHANGELOG.md</code> 的 <code>Fixed</code> 一栏写着它：“Intra-doc links that made <code>cargo doc</code> fail with <code>-D warnings</code>”。tag 上的结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.22s</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Generated /tmp/pebble-rebuild-15/target/doc/pebble_ast/index.html and 15 other files</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>教训很普通：<strong>一条没有被跑过的检查，等于不存在</strong>。CI 里写着这一条命令，但在我动手验证之前，我自己没有在本地跑过它。</p></section>
<section><h2>Lint 与风格：把争论交给机器<a href="#lint-与风格把争论交给机器"><span>#</span></a></h2><section><h3>工作区里的 lint 配置<a href="#工作区里的-lint-配置"><span>#</span></a></h3><p>根 <code>Cargo.toml</code> 里的三段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>workspace</span><span>.</span><span>lints</span><span>.</span><span>rust</span><span>]</span></div></div><div><div><div>2</div></div><div><span>rust_2018_idioms</span><span> = </span><span>"warn"</span></div></div><div><div><div>3</div></div><div><span>unsafe_op_in_unsafe_fn</span><span> = </span><span>"warn"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>[</span><span>workspace</span><span>.</span><span>lints</span><span>.</span><span>clippy</span><span>]</span></div></div><div><div><div>6</div></div><div><span>all</span><span> = </span><span>"warn"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每个 crate 的 <code>Cargo.toml</code> 底部有 <code>[lints] workspace = true</code>（第 1 篇里的写法），继承它们。<code>rust_2018_idioms</code> 是一组”别用旧习惯”的 lint；<code>unsafe_op_in_unsafe_fn</code> 要求 <code>unsafe fn</code> 里面的不安全操作也要用 <code>unsafe {}</code> 框起来（第 11、13 篇）；<code>clippy::all</code> 是 clippy 的默认一大组。它们只是<strong>警告</strong>，是 CI 里的 <code>-D warnings</code> 把警告变成错误：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo clippy --workspace --all-targets --all-features -- -D warnings</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Checking pebble-net v0.1.0 (/tmp/pebble-rebuild-15/crates/pebble-net)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 8.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>警告在本地开发时不阻碍你运行代码，CI 里不允许任何一条留下来，这是一种分工：本地可以带着警告试验，合并不行。</p></section><section><h3>什么时候可以说”我知道，闭嘴”<a href="#什么时候可以说我知道闭嘴"><span>#</span></a></h3><p><code>#[allow(...)]</code> 是对一条 lint 的豁免，它能放在不同的粒度上，Pebble 用了两个：</p><ul>
<li>整个 crate：<code>pebble-value</code> 和 <code>pebble-interp</code> 的开头都有 <code>#![allow(clippy::mutable_key_type)]</code>。<code>pebble-value</code> 里附了五行注释解释为什么键的哈希是稳定的，<code>pebble-interp</code> 里两行注释指回它（第 8 篇里 clippy 抗议的故事：对着一个 <code>RefCell</code> 里有计数器的键类型，规则本身是对的，只是这里的哈希按指针地址算）。</li>
<li>一个测试函数：<code>pebble-sys</code> 里的 <code>fold_empty_range_returns_init</code> 上有 <code>#[allow(clippy::reversed_empty_ranges)]</code>，因为 <code>5..=4</code> 在那里是故意写的空区间。</li>
</ul><p>顺带一提：我在第 8 篇试过，把 crate 级的 <code>allow</code> 删掉后，库代码没有意见，只有 <code>--all-targets</code> 时测试里的 <code>HashMap::new()</code> 触发它，所以更窄的写法是把它只放在测试函数上；tag 选择的是 crate 级一次关掉，我觉得两种都说得过去。</p><p>规矩只有一条：<strong>每一个 <code>allow</code> 旁边必须有一句话说明为什么。</strong> 没有注释的 <code>allow</code> 是在把噪音藏进地毯下面。</p></section><section><h3>把”不安全”限制在能看见的地方<a href="#把不安全限制在能看见的地方"><span>#</span></a></h3><p><code>#![forbid(unsafe_code)]</code> 比 <code>deny</code> 更强：不能被内层的 <code>#[allow]</code> 覆盖。<code>grep -rn "forbid(unsafe_code)" crates</code> 在 tag 里作为属性命中 <code>pebble-span</code>、<code>diag</code>、<code>lexer</code>、<code>ast</code>、<code>parser</code>、<code>value</code>、<code>interp</code>、<code>bytecode</code>、<code>vm</code>、<code>bench</code>、<code>net</code> 和 <code>cli</code>（<code>pebble-nostd</code> 的 <code>lib.rs</code> 里那一处命中是注释，说它不能用这条属性）；没有这个属性的是 <code>pebble-gc</code>（有 <code>bump.rs</code> 里的 <code>unsafe</code>，第 11 篇）、<code>pebble-sys</code> 和 <code>pebble-nostd</code>（第 13 篇），还有 <code>pebble-macros</code>（它里面没有 <code>unsafe</code> 这个词，只是没写这一行）。也就是说，在 16 个 crate 里，需要人来为内存安全签字的代码只可能出现在三个 crate 里，而且 <code>grep unsafe</code> 就能把它们全部找出来。我觉得这是 Rust 里最被低估的一条工程性质：安全性的”审计范围”是机器可计算的。</p></section><section><h3><code>#[must_use]</code><a href="#must_use"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ grep -rn "must_use" crates --include=*.rs | wc -l</span></div></div><div><div><div>2</div></div><div><span>111</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>111 行。<code>#[must_use]</code> 放在一个函数或类型上，调用者丢掉返回值会得到警告。<code>Span::merge</code> 返回新的 <code>Span</code>，如果你写了 <code>a.merge(b);</code> 然后什么都不用，这几乎一定是个 bug（它不修改 <code>a</code>），编译器会告诉你。<code>Result</code> 本身就是 <code>#[must_use]</code> 的，这是为什么忘了处理错误会被警告。</p></section><section><h3>配置文件一览<a href="#配置文件一览"><span>#</span></a></h3><p>这些全是仓库根目录下的小文件。我把它们按读到的内容列出来：</p><div><figure><figcaption><span>rust-toolchain.toml</span></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>toolchain</span><span>]</span></div></div><div><div><div>2</div></div><div><span>channel</span><span> = </span><span>"stable"</span></div></div><div><div><div>3</div></div><div><span>components</span><span> = [</span><span>"rustfmt"</span><span>, </span><span>"clippy"</span><span>]</span></div></div><div><div><div>4</div></div><div><span>profile</span><span> = </span><span>"minimal"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>rustup</code> 进入这个目录时会自动选 <code>stable</code> 通道并保证装了 <code>rustfmt</code> 和 <code>clippy</code>；<code>profile = "minimal"</code> 不装文档和其他组件。注意它<strong>没有</strong>钉死一个具体版本：版本由别的东西钉。</p><div><figure><figcaption><span>rustfmt.toml</span></figcaption><pre><code><div><div><div>1</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>2</div></div><div><span>max_width</span><span> = </span><span>100</span></div></div><div><div><div>3</div></div><div><span>use_field_init_shorthand</span><span> = </span><span>true</span></div></div><div><div><div>4</div></div><div><span>use_try_shorthand</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span>clippy.toml</span></figcaption><pre><code><div><div><div>1</div></div><div><span>msrv</span><span> = </span><span>"1.88"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>msrv</code> 告诉 clippy “这个项目声称最低支持 1.88”，它就不会推荐只在更新的版本才有的写法。根 <code>Cargo.toml</code> 里的 <code>rust-version = "1.88"</code> 是同一个声明，Cargo 用它拒绝在更旧的编译器上构建；第 10 篇的 let 链就是 1.88 才稳定的。（诚实说明：我没有装 1.88 去验证这个声明，我手里只有 1.95.0。）</p><p><code>flake.nix</code> 是 Nix 用户的开发环境，也就是我在第 1 篇里让你 <code>nix develop</code> 的那个。tag 里的内容是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>inputs</span><span>.</span><span>nixpkgs</span><span>.</span><span>url</span><span> </span></span><span>=</span><span> </span><span>"github:NixOS/nixpkgs/nixos-26.05"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span><span>packages</span><span> </span></span><span>=</span><span> </span><span>with</span><span><span> </span><span>pkgs</span></span><span>;</span><span><span> [ </span><span>rustc</span><span> </span><span>cargo</span><span> </span><span>rust-analyzer</span><span> </span><span>clippy</span><span> </span><span>rustfmt</span><span> </span><span>gdb</span><span> </span><span>hyperfine</span><span> </span><span>pkg-config</span><span> </span><span>zlib</span><span> ]</span></span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>flake.lock</code> 把 <code>nixpkgs</code> 钉在一个具体的提交上，所以所有人的 <code>rustc</code> 是同一个。这就是上面说的”版本由别的东西钉”：<code>rust-toolchain.toml</code> 管通道，Nix 的锁文件管版本。<code>.envrc</code> 里只有一行 <code>use flake</code>，配合 <code>direnv</code> 进入目录自动进环境，目录里还有 <code>.editorconfig</code>（缩进、行尾，所有编辑器通用）。</p><p><code>Cargo.lock</code> 提交在仓库里，里面 143 个包的精确版本和校验和（<code>grep -c '^name = ' Cargo.lock</code> 是 143，包括我们自己的 16 个 crate）。老的建议是”库不提交锁文件，因为使用者会自己解析；应用提交”；新版 Cargo 文档的说法是没那么绝对（我没有逐条核对，细节以 Cargo Book 为准）。Pebble 有一个二进制，所以无论哪种说法都该提交：让每次构建拿到一样的依赖。<code>dependabot.yml</code> 每周自己提一个 PR 来更新它。</p></section><section><h3>发布 profile<a href="#发布-profile"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>profile</span><span>.</span><span>release</span><span>]</span></div></div><div><div><div>2</div></div><div><span>lto</span><span> = </span><span>"thin"</span></div></div><div><div><div>3</div></div><div><span>codegen-units</span><span> = </span><span>1</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>[</span><span>profile</span><span>.</span><span>bench</span><span>]</span></div></div><div><div><div>6</div></div><div><span>inherits</span><span> = </span><span>"release"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>lto = "thin"</code> 是链接期优化，允许跨 crate 内联（Pebble 有 16 个 crate，解释器的热点在 <code>pebble-value</code> 和 <code>pebble-vm</code> 之间）；<code>codegen-units = 1</code> 让编译器把整个 crate 当成一个编译单元，牺牲编译速度换一点运行速度；<code>bench</code> profile 继承 <code>release</code>，保证 Criterion 测的是发布构建。我没有单独测过这两个设置各自的收益，所以不给数字。</p></section><section><h3>两个小债<a href="#两个小债"><span>#</span></a></h3><p><strong><code>#[inline]</code>。</strong> Pebble 里没有写 <code>#[inline]</code>，我也没有测过它对 Pebble 有没有帮助。它是一个”建议”：对一个小函数，提示编译器可以内联。在同一个 crate 里，编译器自己会决定；跨 crate 时，一个非泛型函数默认<strong>只有机器码，没有可内联的函数体</strong>，<code>#[inline]</code> 让函数体也被导出，使别的 crate 可以内联它（泛型函数天生就导出函数体，因为要单态化，第 5 篇）。<code>src/bin/ch15_inline.rs</code> 里有三种写法，它们能编译、能运行，我只能保证这么多：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch15_inline</span></div></div><div><div><div>2</div></div><div><span>38 2654435768 608876246</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>#[inline(always)]</code> 和 <code>#[inline(never)]</code> 是更强的版本。这一类属性我的原则是：不测量就不加。</p><p><strong>解引用强制转换。</strong> 第 5 篇讲过它在 <code>Box</code> 和 <code>Vec</code> 上的样子（<code>&amp;Box&lt;Expr&gt;</code> 变 <code>&amp;Expr</code>，<code>&amp;Vec&lt;T&gt;</code> 变 <code>&amp;[T]</code>），第 2、7 篇里 <code>&amp;String</code> 变 <code>&amp;str</code>、<code>Source::load(&amp;args.file)</code> 把一个 <code>&amp;PathBuf</code> 传给要 <code>&amp;Path</code> 的参数，也都用了它，我当时只讲了”<code>PathBuf</code> 对应 <code>&amp;Path</code>，像 <code>String</code> 对应 <code>&amp;str</code>”。这里把它补完整，答案是 <strong>deref coercion</strong>：当 <code>T: Deref&lt;Target = U&gt;</code>，编译器会在需要 <code>&amp;U</code> 的地方自动把 <code>&amp;T</code> 转成 <code>&amp;U</code>，而且可以连续转几步。<code>src/bin/ch15_deref.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>load</span><span>(</span><span>path</span><span>: &amp;</span><span>Path</span><span>) -&gt; </span><span>usize</span><span> { </span><span>path</span><span>.</span><span>as_os_str</span><span>().</span><span>len</span><span>() }</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>shout</span><span>(</span><span>text</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>String</span><span> { </span><span>text</span><span>.</span><span>to_uppercase</span><span>() }</span></div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>total</span><span>(</span><span>numbers</span><span>: &amp;[</span><span>i64</span><span>]) -&gt; </span><span>i64</span><span> { </span><span>numbers</span><span>.</span><span>iter</span><span>().</span><span>sum</span><span>() }</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>owned</span><span><span> </span><span>=</span><span> </span></span><span>PathBuf</span><span>::</span><span>from</span><span>(</span><span>"examples/fib.pebble"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>load</span><span>(&amp;</span><span>owned</span><span>));      </span><span>// &amp;PathBuf where &amp;Path is expected</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"pebble"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>shout</span><span>(&amp;</span><span>name</span><span>));      </span><span>// &amp;String where &amp;str is expected</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>list</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>];</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>total</span><span>(&amp;</span><span>list</span><span>));      </span><span>// &amp;Vec&lt;i64&gt; where &amp;[i64] is expected</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>boxed</span><span><span> </span><span>=</span><span> </span></span><span>Box</span><span>::</span><span>new</span><span><span>(</span><span>String</span><span>::</span></span><span>from</span><span>(</span><span>"boxed"</span><span>));</span></div></div><div><div><div>13</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>shout</span><span>(&amp;</span><span>boxed</span><span>));     </span><span>// &amp;Box&lt;String&gt; -&gt; &amp;String -&gt; &amp;str: two steps</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch15_deref</span></div></div><div><div><div>2</div></div><div><span>19</span></div></div><div><div><div>3</div></div><div><span>PEBBLE</span></div></div><div><div><div>4</div></div><div><span>6</span></div></div><div><div><div>5</div></div><div><span>BOXED</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>(清单里省略了 <code>use std::path::{Path, PathBuf};</code> 这一行。) 这就是为什么 Pebble 的函数参数写 <code>&amp;str</code>、<code>&amp;[T]</code>、<code>&amp;Path</code> 而不是 <code>&amp;String</code>、<code>&amp;Vec&lt;T&gt;</code>、<code>&amp;PathBuf</code>：借用版本更通用，调用者拥有的任何一种形态都能传进来。第 5 篇的 <code>visitor.visit_expr(operand)</code>，<code>operand</code> 是 <code>&amp;Box&lt;Expr&gt;</code> 而参数要 <code>&amp;Expr</code>，也是同一个机制。它与 C++ 的隐式转换不同：它只做 <code>Deref</code> 这一种，不会把一个 <code>i32</code> 悄悄变成 <code>i64</code>。</p></section></section>
<section><h2>CI：把上面所有东西在每次提交时跑一遍<a href="#ci把上面所有东西在每次提交时跑一遍"><span>#</span></a></h2><p>CI 是 <code>.github/workflows/ci.yml</code>，<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/.github/workflows/ci.yml" target="_blank">在 tag 里</a>。我分段读。开头：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>on</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>push</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>branches</span><span>: [</span><span>main</span><span>]</span></div></div><div><div><div>4</div></div><div><span>  </span><span>pull_request</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>branches</span><span>: [</span><span>main</span><span>]</span></div></div><div><div><div>6</div></div><div><span>  </span><span>workflow_dispatch</span><span>:</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>env</span><span>:</span></div></div><div><div><div>9</div></div><div><span>  </span><span>CARGO_TERM_COLOR</span><span>: </span><span>always</span></div></div><div><div><div>10</div></div><div><span>  </span><span>RUST_BACKTRACE</span><span>: </span><span>1</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>concurrency</span><span>:</span></div></div><div><div><div>13</div></div><div><span>  </span><span>group</span><span>: </span><span>${{ github.workflow }}-${{ github.ref }}</span></div></div><div><div><div>14</div></div><div><span>  </span><span>cancel-in-progress</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>推到 <code>main</code>、对 <code>main</code> 开 PR、或者手动触发都会跑。<code>RUST_BACKTRACE: 1</code> 让 panic 带调用栈，CI 里没人能事后复现，这是少数几行不花钱的保险。<code>concurrency</code> 让同一个分支上的新提交取消还在跑的旧任务。</p><p>五个任务，互相独立地并行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>fmt</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>name</span><span>: </span><span>rustfmt</span></div></div><div><div><div>3</div></div><div><span>    </span><span>runs-on</span><span>: </span><span>ubuntu-latest</span></div></div><div><div><div>4</div></div><div><span>    </span><span>steps</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v5</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>dtolnay/rust-toolchain@stable</span></div></div><div><div><div>7</div></div><div><span>        </span><span>with</span><span>:</span></div></div><div><div><div>8</div></div><div><span>          </span><span>components</span><span>: </span><span>rustfmt</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>cargo fmt --all --check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>fmt</code>：最便宜的检查，不编译。<code>--check</code> 不改文件，格式不对就失败。本地验证：<code>cargo fmt --all --check</code> 在 <code>/tmp/pebble-rebuild-15</code> 退出码 0。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>clippy</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>name</span><span>: </span><span>clippy</span></div></div><div><div><div>3</div></div><div><span>    </span><span>runs-on</span><span>: </span><span>ubuntu-latest</span></div></div><div><div><div>4</div></div><div><span>    </span><span>steps</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v5</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>dtolnay/rust-toolchain@stable</span></div></div><div><div><div>7</div></div><div><span>        </span><span>with</span><span>:</span></div></div><div><div><div>8</div></div><div><span>          </span><span>components</span><span>: </span><span>clippy</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>Swatinem/rust-cache@v2</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>cargo clippy --workspace --all-targets --all-features -- -D warnings</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>cargo clippy -p pebble-cli --no-default-features --all-targets -- -D warnings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>clippy</code>：两行。第一行是”全开”，<code>--all-targets</code> 把测试、示例、基准也包括在内；第二行就是本篇前面加的”全关”。<code>Swatinem/rust-cache</code> 缓存 <code>target/</code> 和 <code>~/.cargo</code>，省掉每次从零编译 143 个包的时间（我没有量过省多少）。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>test</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>name</span><span>: </span><span>test (${{ matrix.os }})</span></div></div><div><div><div>3</div></div><div><span>    </span><span>runs-on</span><span>: </span><span>${{ matrix.os }}</span></div></div><div><div><div>4</div></div><div><span>    </span><span>strategy</span><span>:</span></div></div><div><div><div>5</div></div><div><span>      </span><span>fail-fast</span><span>: </span><span>false</span></div></div><div><div><div>6</div></div><div><span>      </span><span>matrix</span><span>:</span></div></div><div><div><div>7</div></div><div><span>        </span><span>os</span><span>: [</span><span>ubuntu-latest</span><span>, </span><span>macos-latest</span><span>]</span></div></div><div><div><div>8</div></div><div><span>    </span><span>steps</span><span>:</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v5</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>dtolnay/rust-toolchain@stable</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>Swatinem/rust-cache@v2</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>cargo test --workspace --all-features</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>- </span><span>name</span><span>: </span><span>End-to-end smoke</span></div></div><div><div><div>14</div></div><div><span>        </span><span>run</span><span>: </span><span>|</span></div></div><div><div><div>15</div></div><div><span><span>          </span></span><span>cargo run --quiet -p pebble-cli -- run examples/fib.pebble</span></div></div><div><div><div>16</div></div><div><span><span>          </span></span><span>cargo run --quiet -p pebble-cli -- run --vm examples/fib.pebble</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>test</code>：一个 <code>matrix</code> 在 Linux 和 macOS 上各跑一遍（<code>fail-fast: false</code> 表示一个失败不取消另一个，这样你能看到两边的情况）。这也是 <code>pebble-sys</code> 的 C 代码在两个系统上都得编译的地方。最后的 “End-to-end smoke” 是真的运行二进制：在本地：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run -q -p pebble-cli -- run examples/fib.pebble</span></div></div><div><div><div>2</div></div><div><span>6765</span></div></div><div><div><div>3</div></div><div><span>$ cargo run -q -p pebble-cli -- run --vm examples/fib.pebble</span></div></div><div><div><div>4</div></div><div><span>6765</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个后端各一次，都是 <code>6765</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>build</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>name</span><span>: </span><span>build (release)</span></div></div><div><div><div>3</div></div><div><span>    </span><span>runs-on</span><span>: </span><span>ubuntu-latest</span></div></div><div><div><div>4</div></div><div><span>    </span><span>steps</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v5</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>dtolnay/rust-toolchain@stable</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>Swatinem/rust-cache@v2</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>cargo build --workspace --release</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>cargo build -p pebble-cli --no-default-features</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>build</code>：发布构建（用的是上面那套 <code>lto = "thin"</code>），以及”全关”的构建。<code>cargo test</code> 编的是 <code>dev</code> profile，这个任务是 CI 里唯一编译发布 profile 的地方；我推测它是为了让链接期优化那一步也被检查，但这是我的推测，不是仓库里写明的。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>docs</span><span>:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>name</span><span>: </span><span>rustdoc</span></div></div><div><div><div>3</div></div><div><span>    </span><span>runs-on</span><span>: </span><span>ubuntu-latest</span></div></div><div><div><div>4</div></div><div><span>    </span><span>steps</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v5</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>dtolnay/rust-toolchain@stable</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>- </span><span>uses</span><span>: </span><span>Swatinem/rust-cache@v2</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>- </span><span>run</span><span>: </span><span>cargo doc --workspace --no-deps --all-features</span></div></div><div><div><div>9</div></div><div><span>        </span><span>env</span><span>:</span></div></div><div><div><div>10</div></div><div><span>          </span><span>RUSTDOCFLAGS</span><span>: </span><span>-D warnings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>docs</code>：就是前面那个会红的任务。五个任务各管一类事：格式、静态检查、行为、发布构建、文档。</p><p>有一点要讲明：<code>dtolnay/rust-toolchain@stable</code> 装的是”当前的 stable”，不是 <code>flake.lock</code> 钉住的版本，也不看 <code>rust-toolchain.toml</code>。CI 和我的开发环境可以因此而用不同的 <code>rustc</code> 小版本。我觉得对一个学习项目这是合理的折中（CI 在新的 stable 上提前发现新的 clippy 警告），但你要知道有这个缝。</p><p>另外两个文件：<code>.github/dependabot.yml</code> 每周为 Cargo 依赖和 GitHub Actions 版本各开 PR（<code>chore(deps)</code>、<code>chore(ci)</code> 前缀）；<code>.github/PULL_REQUEST_TEMPLATE.md</code> 里有一份和 CI 对应的勾选清单，其中有”如果改了语言，两个后端必须一致并且有测试证明”，这一条正是第 12 篇的差分测试存在的原因。</p></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>最后一次对比。我在 <code>/tmp/pebble-rebuild-15</code> 里：先用 <code>git archive tutorial-v1</code> 解开整个 tag（这样 <code>.github/</code>、<code>docs/</code>、<code>README.md</code>、<code>CHANGELOG.md</code>、<code>LICENSE-*</code> 这些我们没写的东西就是复制来的；现在 tag 里还多一个 <code>lab/</code> 目录，下面的对比用 <code>--exclude=lab</code> 跳过它），然后把 <code>crates/pebble-cli</code> 换成第 14 篇结束时的第五版，再按上一节的五步一路改回去。结束后，对着另一份干净解开的 tag 做递归对比：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ diff -rq /tmp/tagcmp /tmp/pebble-rebuild-15 --exclude=target --exclude=lab</span></div></div><div><div><div>2</div></div><div><span>$ echo $?</span></div></div><div><div><div>3</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你自己做这一步，把 tag 克隆到 <code>~/pebble-tag</code>（第 1 篇的命令），在你的工作区根目录跑 <code>diff -rq ~/pebble-tag . --exclude=target --exclude=.git --exclude=lab</code>。<code>lab/</code> 是 tag 里唯一一个被跟踪、但重建不需要复现的目录（它是实验室，不属于 Pebble 的工作区）。另外 tag 里的 <code>.gitignore</code> 末尾比第 1 篇写的多两行（一行注释 <code># Tutorial lab crates (each has its own workspace and target dir)</code> 和 <code>/lab/**/target</code>），给 <code>lab/</code> 里的 <code>target/</code> 用；<code>README.md</code> 也多一段介绍 <code>lab/</code>。这两个文件从 tag 复制就和 tag 一致，自己写的话会在这里看到 <code>.gitignore</code> 的差别，把那两行补上即可。我上面用的是 <code>git archive</code> 解开的两份临时目录，所以路径不同，比较的内容一样。</p><p>没有任何输出，退出码 0：除了 <code>target/</code> 以外，没有一个文件有差别（包括 <code>Cargo.lock</code>：重新构建没有改它）。然后是行为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo fmt --all --check                                                     # exit 0</span></div></div><div><div><div>2</div></div><div><span>$ cargo clippy --workspace --all-targets --all-features -- -D warnings        # exit 0</span></div></div><div><div><div>3</div></div><div><span>$ cargo clippy -p pebble-cli --no-default-features --all-targets -- -D warnings   # exit 0</span></div></div><div><div><div>4</div></div><div><span>$ cargo build -p pebble-cli --no-default-features                             # Finished</span></div></div><div><div><div>5</div></div><div><span>$ RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features   # Finished</span></div></div><div><div><div>6</div></div><div><span>$ cargo test --workspace --all-features                                       # 168 passed, 3 ignored</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>另外验证一下第 14 篇加的 <code>check</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run -q -p pebble-cli -- check examples/fib.pebble examples/errors.pebble examples/hello.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>$ echo $?</span></div></div><div><div><div>9</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只有坏文件出了声，退出码 1。（用 <code>--no-default-features</code> 的二进制跑同一个 <code>run examples/fib.pebble</code> 也是 <code>6765</code>。）</p><p>**这一篇之后，必须与 tag 相同的文件：**整个工作区。这里面列最后一批：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-cli/Cargo.toml" target="_blank"><code>crates/pebble-cli/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-cli/src/cli.rs" target="_blank"><code>crates/pebble-cli/src/cli.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-cli/src/main.rs" target="_blank"><code>crates/pebble-cli/src/main.rs</code></a></li>
</ul><p>其余的文件（<code>.github/</code>、<code>docs/</code>、<code>README.md</code>、<code>CHANGELOG.md</code>、两份许可证、<code>flake.nix</code> 等）是从 tag 复制的，没有中间版本。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的所有实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank">lab/rust-lab</a> 是主实验室）。按第 1 篇的方式把 tag 克隆到 <code>~/pebble-tag</code> 后，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，在那里运行（<code>cd ~/pebble-tag/lab/rust-lab</code>，也可以把这个目录复制成 <code>~/rust-lab</code>）。表里 <code>lab-</code> 开头的文件属于旁边的独立实验 crate，命令里写了完整的目录。</p><p>补丁（<code>lab/exercises/*.patch</code>）要在一份干净的 <code>~/pebble-tag</code> 里从仓库根目录应用，跑完用 <code>git apply -R lab/exercises/ch15-features.patch</code> 之类的命令还原。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch15_cfg.rs" target="_blank"><code>ch15_cfg.rs</code></a></td><td><code>cargo run --quiet --bin ch15_cfg</code></td><td>运行成功：<code>compiled for linux</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch15_cfg_missing.rs" target="_blank"><code>ch15_cfg_missing.rs</code></a></td><td><code>cargo build --bin ch15_cfg_missing</code></td><td>编译失败：<code>E0425</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch15-tests/src/lib.rs" target="_blank"><code>lab-ch15-tests/src/lib.rs</code></a> 和 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch15-tests/tests/api.rs" target="_blank"><code>tests/api.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch15-tests &amp;&amp; cargo test</code></td><td>运行成功：单元测试 <code>1 passed</code>，集成测试 <code>1 passed</code>，文档测试 <code>1 passed</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch15_inline.rs" target="_blank"><code>ch15_inline.rs</code></a></td><td><code>cargo run --quiet --bin ch15_inline</code></td><td>运行成功：<code>38 2654435768 608876246</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch15_deref.rs" target="_blank"><code>ch15_deref.rs</code></a></td><td><code>cargo run --quiet --bin ch15_deref</code></td><td>运行成功：<code>19</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch15-features.patch" target="_blank"><code>exercises/ch15-features.patch</code></a></td><td><code>cd ~/pebble-tag &amp;&amp; git apply lab/exercises/ch15-features.patch &amp;&amp; cargo test -p pebble-cli --test cli</code></td><td>运行成功：<code>13 passed</code></td></tr><tr><td>同一个补丁</td><td><code>cargo test -p pebble-cli --no-default-features --test cli</code></td><td>运行成功：<code>13 passed</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch15-doc-link.patch" target="_blank"><code>exercises/ch15-doc-link.patch</code></a></td><td><code>cd ~/pebble-tag &amp;&amp; git apply lab/exercises/ch15-doc-link.patch &amp;&amp; RUSTDOCFLAGS="-D warnings" cargo doc -p pebble-value --no-deps</code></td><td>失败：<code>error: unresolved link to pebble_vm</code></td></tr></tbody></table><p>文章里把 <code>lab-ch15-tests</code> 弄坏的两个版本没有放进仓库：把 <code>src/lib.rs</code> 文档测试里的 <code>2</code> 改成 <code>3</code> 就是失败的文档测试；<code>tests/private.rs</code>（一行 <code>use lab_ch15_tests::secret;</code>）会让朴素的 <code>cargo test</code> 直接编译失败，所以我没有把它放进 <code>lab/</code>，要复现就自己建这个文件，再跑 <code>cargo test --test private</code>，得到文章里的 <code>E0603</code>。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1.</strong> 给命令行加一对测试，让 feature 开关本身受到测试：开 <code>repl</code> 时 <code>pebble repl --help</code> 成功，关掉时 <code>pebble repl</code> 退出码是 2。（提示：<code>#[cfg(feature = "repl")]</code> 能不能用在集成测试文件里？）</p>
参考答案<p>能。Cargo 对一个包的每个目标（库、二进制、测试）传同一组 <code>--cfg feature="..."</code>，所以 <code>tests/cli.rs</code> 里也能写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(feature </span><span>=</span><span> </span><span>"repl"</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>#[test]</span></div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>the_repl_subcommand_exists_when_the_feature_is_on</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pebble</span><span>().</span><span>args</span><span>([</span><span>"repl"</span><span>, </span><span>"--help"</span><span>]).</span><span>assert</span><span>().</span><span>success</span><span>();</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#[cfg(not(feature </span><span>=</span><span> </span><span>"repl"</span><span>))]</span></div></div><div><div><div>8</div></div><div><span>#[test]</span></div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>the_repl_subcommand_is_gone_when_the_feature_is_off</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pebble</span><span>().</span><span>arg</span><span>(</span><span>"repl"</span><span>).</span><span>assert</span><span>().</span><span>code</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我在 tag 的一份临时副本里加了这两个测试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-cli</span></div></div><div><div><div>2</div></div><div><span>test repl::tests::unknown_commands_are_rejected ... ok</span></div></div><div><div><div>3</div></div><div><span>test repl::tests::arguments_are_kept_whole ... ok</span></div></div><div><div><div>4</div></div><div><span>test repl::tests::quit_has_a_short_form ... ok</span></div></div><div><div><div>5</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>6</div></div><div><span>test the_repl_subcommand_exists_when_the_feature_is_on ... ok</span></div></div><div><div><div>7</div></div><div><span>test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s</span></div></div><div><div><div>8</div></div><div><span>$ cargo test -p pebble-cli --no-default-features</span></div></div><div><div><div>9</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>10</div></div><div><span>test the_repl_subcommand_is_gone_when_the_feature_is_off ... ok</span></div></div><div><div><div>11</div></div><div><span>test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两种配置各有一个测试被编译进来，总数都是 13。（这份副本现在是 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch15-features.patch" target="_blank">lab/exercises/ch15-features.patch</a>，用 <code>git apply</code> 打在 tag 上即可；我没有改重建目录，所以上面的”逐字节一致”仍然成立。）</p><p><strong>2.</strong> 在 <code>crates/pebble-value/src/lib.rs</code> 的文档里把 <code>`pebble_vm`</code> 改回 <code>[`pebble_vm`]</code>（仓库里的补丁 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch15-doc-link.patch" target="_blank">lab/exercises/ch15-doc-link.patch</a> 就是这个改动），不运行任何命令，预测 <code>cargo test -p pebble-value</code> 会不会失败，<code>cargo doc -p pebble-value --no-deps</code> 呢？加上 <code>RUSTDOCFLAGS="-D warnings"</code> 呢？</p>
参考答案<p><code>cargo test</code> 不会失败：文档测试只编译和运行围栏代码块，不解析 intra-doc 链接。<code>cargo doc</code> 不带 <code>-D warnings</code> 会成功，但打印一条警告（<code>unresolved link to pebble_vm</code>），生成的 HTML 里那个链接是死的。只有带 <code>-D warnings</code> 的 <code>cargo doc</code> 才失败，错误就是上一节里的那条，指向 <code>crates/pebble-value/src/lib.rs:17:51</code>。这个差别正是 CI 里 <code>docs</code> 任务单独存在、并且单独设置 <code>RUSTDOCFLAGS</code> 的原因：别的任务都不会碰它。</p><p><strong>3.</strong> 讨论：CI 的 <code>build</code> 任务用 <code>cargo build -p pebble-cli --no-default-features</code>，如果换成 <code>cargo build --workspace --no-default-features</code>，会少检查什么、多检查什么？</p>
参考答案<p>我在 <code>/tmp/pebble-rebuild-15</code> 里试了：<code>cargo tree --workspace --no-default-features -e normal | grep -c rustyline</code> 输出 0（对比 <code>cargo tree --workspace -e normal</code> 是 1），<code>cargo clippy --workspace --no-default-features --all-targets -- -D warnings</code> 也通过。所以在这个工作区里两个写法对 <code>pebble-cli</code> 的检查是一样的：resolver 3 下 <code>--no-default-features</code> 对每个选中的包生效。差别在于其他 crate：它们没有任何 <code>[features]</code>，所以对它们来说这个开关是空操作。要点是：现在 <code>-p pebble-cli</code> 和 <code>--workspace</code> 等价，只是因为其他 crate 还没有自己的 feature；等哪一天 <code>pebble-net</code> 也有了可选的 feature，<code>--workspace --no-default-features</code> 才会自动覆盖它，而 <code>-p pebble-cli</code> 不会。这是我的推断，没有试验过那种情况。</p></section>
<section><h2>Rust 特性索引<a href="#rust-特性索引"><span>#</span></a></h2><p>下面是整个系列涉及的 Rust 特性，每一行指向讲它的那一篇。“形式”一栏：<strong>P</strong> 是写进 Pebble 的真实代码，<strong>L</strong> 是实验室里编译、运行、有真实输出的实验，<strong>T</strong> 是带一小段编译过的代码的文字讲解。我在写这一张表之前，把前 14 篇里每一个被引用的名字都搜了一遍确认它们在文中确实出现（这是机械检查，不代表讲得够透；表里个别写了”只点到”的就是这种）。</p>

<table><thead><tr><th>特性</th><th>篇</th><th>形式</th></tr></thead><tbody><tr><td><code>cargo</code>、workspace、package / crate / module、<code>println!</code></td><td><a href="/posts/rust-by-pebble-01-first-crate/">第 1 篇</a></td><td>P/L</td></tr><tr><td><code>let</code>、<code>mut</code>、遮蔽、整数类型与 <code>as</code>、溢出、表达式与语句、控制流、带标签的 <code>break</code>、<code>loop</code> 的值</td><td><a href="/posts/rust-by-pebble-01-first-crate/">第 1 篇</a></td><td>L</td></tr><tr><td><code>struct</code>、元组结构体（newtype）、<code>impl</code>、方法、关联函数与常量、<code>const fn</code>、<code>derive</code>、<code>#[must_use]</code>、单元测试</td><td><a href="/posts/rust-by-pebble-01-first-crate/">第 1 篇</a></td><td>P</td></tr><tr><td><code>String</code>/<code>&amp;str</code>、移动、<code>Copy</code>/<code>Clone</code>、借用与 NLL、切片、<code>Vec</code>、<code>Option</code>、<code>?</code> 用在 <code>Option</code> 上、闭包初见、UTF-8</td><td><a href="/posts/rust-by-pebble-02-ownership/">第 2 篇</a></td><td>P/L</td></tr><tr><td><code>mem::take</code>/<code>replace</code>/<code>swap</code></td><td><a href="/posts/rust-by-pebble-02-ownership/">第 2 篇</a></td><td>L</td></tr><tr><td><code>Index</code> 运算符重载与其他 <code>std::ops</code></td><td><a href="/posts/rust-by-pebble-02-ownership/">第 2 篇</a></td><td>P/L</td></tr><tr><td>trait 初见、<code>Default</code>、<code>impl Into&lt;_&gt;</code> 参数、<code>no_std</code> / <code>alloc</code> / <code>core</code></td><td><a href="/posts/rust-by-pebble-02-ownership/">第 2 篇</a></td><td>P</td></tr><tr><td>带数据的 <code>enum</code>、穷尽 <code>match</code>、<code>if let</code>、<code>let else</code>、builder、结构体更新语法</td><td><a href="/posts/rust-by-pebble-03-enums-traits-diagnostics/">第 3 篇</a></td><td>P/L</td></tr><tr><td><code>Display</code>/<code>Debug</code>/<code>Error</code>、supertrait、默认方法、孤儿规则、单元结构体、类型别名、blanket impl</td><td><a href="/posts/rust-by-pebble-03-enums-traits-diagnostics/">第 3 篇</a></td><td>P/L/T</td></tr><tr><td>格式化小语言、<code>fmt::Write</code> 与 <code>io::Write</code></td><td><a href="/posts/rust-by-pebble-03-enums-traits-diagnostics/">第 3 篇</a></td><td>P/L</td></tr><tr><td>生命周期、省略规则、<code>'static</code>、<code>Cow</code>、实现 <code>Iterator</code>（关联类型）、match 守卫、范围与或模式、<code>while let</code>、turbofish</td><td><a href="/posts/rust-by-pebble-04-lifetimes-lexer/">第 4 篇</a></td><td>P/L</td></tr><tr><td>高阶生命周期约束（HRTB）与型变</td><td><a href="/posts/rust-by-pebble-04-lifetimes-lexer/">第 4 篇</a>、<a href="/posts/rust-by-pebble-08-rc-refcell-values/">第 8 篇</a></td><td>T</td></tr><tr><td>递归 <code>enum</code> 与 <code>Box</code>、泛型、trait 约束、<code>where</code>、<code>?Sized</code>、单态化、<code>IntoIterator</code>、<code>pub use</code></td><td><a href="/posts/rust-by-pebble-05-ast-generics/">第 5 篇</a></td><td>P/L</td></tr><tr><td>过程宏（derive）、<code>syn</code> / <code>quote</code>、卫生性</td><td><a href="/posts/rust-by-pebble-06-proc-macros/">第 6 篇</a></td><td>P/L</td></tr><tr><td><code>Result</code>、<code>?</code>、<code>panic!</code> / <code>unwrap</code> / <code>expect</code>、<code>todo!</code>、<code>matches!</code>、<code>mem::discriminant</code>、两阶段借用</td><td><a href="/posts/rust-by-pebble-07-parser-errors-cli/">第 7 篇</a></td><td>P/L</td></tr><tr><td><code>clap</code>、<code>anyhow</code>、<code>fs</code>、<code>ExitCode</code>、文件模块、<code>pub(crate)</code>、<code>use crate::</code>、集成测试</td><td><a href="/posts/rust-by-pebble-07-parser-errors-cli/">第 7 篇</a></td><td>P/T</td></tr><tr><td>解引用强制转换（<code>&amp;Box&lt;T&gt;</code>、<code>&amp;Vec&lt;T&gt;</code>、<code>&amp;PathBuf</code> 变 <code>&amp;Path</code>）</td><td><a href="/posts/rust-by-pebble-05-ast-generics/">第 5 篇</a>（<code>Box</code>/<code>Vec</code>）、<a href="/posts/rust-by-pebble-15-engineering/">第 15 篇</a>（补完）</td><td>L</td></tr><tr><td><code>Rc</code>、<code>Rc&lt;str&gt;</code>、<code>RefCell</code>、<code>HashMap</code>、<code>Hash</code>/<code>Eq</code> 契约、手写 <code>PartialEq</code>、<code>PartialOrd</code> 契约、<code>From</code></td><td><a href="/posts/rust-by-pebble-08-rc-refcell-values/">第 8 篇</a></td><td>P/L</td></tr><tr><td>函数指针与闭包、trait 对象与 vtable、<code>static</code>、切片模式、<code>-&gt; impl Trait</code>、<code>Borrow</code>/<code>AsRef</code>、<code>TryFrom</code>/<code>FromStr</code></td><td><a href="/posts/rust-by-pebble-08-rc-refcell-values/">第 8 篇</a></td><td>P/L/T</td></tr><tr><td><code>Fn</code>/<code>FnMut</code>/<code>FnOnce</code>、<code>move</code> 闭包、<code>Weak</code>、<code>thread::Builder</code>、把 <code>collect</code> 收进 <code>Result</code>、实现 <code>FromStr</code></td><td><a href="/posts/rust-by-pebble-09-interpreter-closures/">第 9 篇</a></td><td>P/L</td></tr><tr><td><code>macro_rules!</code>、函数式过程宏、<code>HashMap</code> 的 entry、闭包的分字段捕获、let 链、<code>ref</code> 模式、文档测试</td><td><a href="/posts/rust-by-pebble-10-bytecode-macros/">第 10 篇</a></td><td>P/L</td></tr><tr><td>泛型结构体、<code>dyn FnMut</code>、<code>Cell</code>、<code>Option::take</code>、外部 crate、<code>BTreeSet</code>/<code>VecDeque</code>/<code>HashSet</code>、<code>unsafe</code>、<code>MaybeUninit</code>、裸指针</td><td><a href="/posts/rust-by-pebble-11-gc-unsafe/">第 11 篇</a></td><td>P/L/T</td></tr><tr><td>关联常量、GAT、静态与动态分发、对象安全、属性过程宏、Criterion</td><td><a href="/posts/rust-by-pebble-12-vm-backends/">第 12 篇</a></td><td>P/L</td></tr><tr><td>FFI、<code>build.rs</code>、<code>repr(C)</code>、<code>NonNull</code>、<code>Drop</code>、自动 trait、<code>PhantomData</code>、<code>catch_unwind</code>、<code>Any</code></td><td><a href="/posts/rust-by-pebble-13-ffi-nostd/">第 13 篇</a></td><td>P</td></tr><tr><td><code>no_mangle</code> 与 <code>cdylib</code>、<code>repr(transparent)</code>、<code>repr(u8)</code>、<code>union</code>、<code>ManuallyDrop</code>、<code>static mut</code></td><td><a href="/posts/rust-by-pebble-13-ffi-nostd/">第 13 篇</a></td><td>T</td></tr><tr><td><code>no_std</code>、<code>UnsafeCell</code>、原子操作、<code>unsafe impl Sync</code>、<code>GlobalAlloc</code>、常量泛型、<code>#[global_allocator]</code></td><td><a href="/posts/rust-by-pebble-13-ffi-nostd/">第 13 篇</a></td><td>P</td></tr><tr><td>线程、<code>thread::scope</code>、<code>Send</code>/<code>Sync</code>、<code>Arc</code>、<code>Mutex</code>、<code>mpsc</code></td><td><a href="/posts/rust-by-pebble-14-concurrency-async/">第 14 篇</a></td><td>P/L</td></tr><tr><td><code>RwLock</code>（只点到，一个小实验）</td><td><a href="/posts/rust-by-pebble-14-concurrency-async/">第 14 篇</a></td><td>T</td></tr><tr><td><code>Future</code>、<code>Pin</code>、<code>Waker</code>、<code>async</code>/<code>await</code>、Tokio、<code>select!</code>、<code>spawn_blocking</code>、<code>Error::source</code>、<code>#[non_exhaustive]</code>、<code>thiserror</code>（对比）</td><td><a href="/posts/rust-by-pebble-14-concurrency-async/">第 14 篇</a></td><td>P/L/T</td></tr><tr><td>Cargo feature、<code>cfg</code>、测试的三种形态、rustdoc、lint、clippy、rustfmt、工具链文件、发布 profile、CI、<code>#[inline]</code></td><td><a href="/posts/rust-by-pebble-15-engineering/">第 15 篇</a></td><td>P/T</td></tr></tbody></table></section>
<section><h2>系列之外<a href="#系列之外"><span>#</span></a></h2><p>这个系列教的是”能读懂、能写出 Pebble 这个体量的 Rust”。下面这些东西我没有讲，不是因为它们不重要，是因为 Pebble 没有一个诚实的落点：</p><ul>
<li><code>async fn</code> 在 trait 里、异步流（<code>Stream</code>），以及 <code>Pin</code> 的”投影”这类更深的 async 机制。</li>
<li>特化、<code>dyn Trait</code> 的向上转型、<code>GAT</code> 的高级用法（我们只用了 <code>Backend</code> 里的那一个）。</li>
<li>Miri、<code>loom</code>、<code>cargo fuzz</code> 和 sanitizer：第 11 篇提到了 Miri，但我没有跑过它。</li>
<li>真的把 <code>no_std</code> 放到一块板子上跑，以及 <code>wasm32</code> 目标。</li>
<li>发布到 crates.io、语义化版本的实际管理。</li>
<li>Stacked Borrows / Tree Borrows 这样的 unsafe 别名模型。</li>
</ul><p>如果你要一个顺序，我会选 Rustonomicon 配着第 11、13 篇读，然后是 Tokio 的教程配着第 14 篇。</p></section>
<section><h2>然后呢<a href="#然后呢"><span>#</span></a></h2><p>我写这个系列的起点是一个很具体的不满：C++ 程序员读 Rust 的书，读完还是写不出东西，因为每一章的例子都是玩具。Pebble 不是玩具，也不算大：10515 行 Rust。但它有一个真实的形状：前端、两个后端、一个 GC、一个 FFI 边界、一个网络服务，以及让它们保持正确的那层工程外壳。每个”为什么”后面都能找到一个可运行的东西。</p><p>我最想知道的是，如果你从空目录做到了这里，第 17 个 crate 会长什么样。Pebble 现在是动态类型的；给它加一个类型检查器，大概需要一个新的 AST 标注和一个新的 <code>Visit</code>，我猜还需要一个比 <code>Value</code> 更严格的类型层。解释器和 VM 的差分测试已经是一个粗糙的”两个独立实现互相验证”的模型，同一个思路也许能抓住第 12 篇里那两个真实的分歧（块级 <code>let</code>、列表里字符串的引号）。字节码如果有一个验证器，GC 的根集合也许能比”指令边界”定义得更精细。我不知道哪个该先做。但有一件事我很确定：你会先写出一个编译不过的版本，然后编译器会指给你看是哪一行。这是我学 Rust 学到的最大的东西，比任何单个特性都大。</p><p>去给 Pebble 写第 17 个 crate 吧，别回来查这个系列。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-14-concurrency-async/</id>
      <title type="text">并发与 async：线程、Send/Sync 和一个 Tokio 服务</title>
      <published>2026-10-13T13:00:00.000Z</published>
      <updated>2026-10-13T13:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-14-concurrency-async/"/>
      <summary type="text">让 pebble check 并行检查多个文件，再写一个能同时服务很多连接的 Tokio 求值服务器。从 thread::scope、Send/Sync 讲到手写 Future，最后写 pebble-net：成帧、FrameError、select!。</summary>
      <content type="html"><![CDATA[<p>我写了一个八个线程各自对同一个全局 <code>int</code> 做一百万次 <code>counter++</code> 的 C 程序，跑三遍，得到 1353007、1318802、1344368，期望值是 8000000。不是偶尔错一点：每次都丢掉了大约 83% 的加法，而且每次丢得不一样。C 编译器对这个程序没有任何意见，<code>gcc</code> 连警告都没有给。这一篇的主题就是 Rust 怎样让这类程序根本编不过去。我们先用线程把 <code>pebble check</code> 变成并行的，然后进入 async：手写一个 <code>Future</code>，再用 Tokio 写一个能同时服务很多连接的 Pebble 求值服务器 <code>pebble-net</code>。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>做完这一篇，你会有 <code>pebble-net</code>（一个 Tokio 写的远程求值服务器：长度前缀成帧、带 <code>source()</code> 链的 <code>FrameError</code>、<code>select!</code>、<code>spawn_blocking</code>、超时），和命令行的第五版（<code>pebble check</code> 一次接受多个文件，每个文件一个线程）。<code>pebble-net</code> 与 tag <code>tutorial-v1</code> 逐字节一致，<code>cargo test -p pebble-net</code> 是 9 个测试，命令行是 3 个单元测试加 12 个集成测试。会用到的 Rust 特性：<code>thread::spawn</code>、<code>thread::scope</code>、<code>Send</code>/<code>Sync</code>、<code>Arc</code>、<code>Mutex</code>、<code>RwLock</code>、<code>mpsc</code>、<code>Future</code>、<code>Pin</code>、<code>Waker</code>、<code>async</code>/<code>await</code>、Tokio、<code>Error::source</code>、<code>#[non_exhaustive]</code>。上一篇在 <a href="/posts/rust-by-pebble-13-ffi-nostd/">FFI 与 no_std</a>，命令行第四版在 <a href="/posts/rust-by-pebble-12-vm-backends/">栈式虚拟机</a>。</p></div></div>
<section><h2>先把工作区补齐<a href="#先把工作区补齐"><span>#</span></a></h2><p><code>pebble-net</code> 依赖前面所有写过的 crate：<code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-ast</code>、<code>pebble-parser</code>、<code>pebble-value</code>、<code>pebble-interp</code>。如果你一路跟下来，它们都已经在 <code>crates/</code> 下了；如果你是跳着来的，把它们从 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1" target="_blank">tag</a> 整个目录复制过来（<code>pebble-cli</code> 要用第 12 篇结束时的第四版，下面会升级它）。</p><p>根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 需要多两项：<code>pebble-net</code> 自己，和 Tokio。把它们加进去（<code>tokio</code> 的写法是 TOML 里的”内联表”跨多行，就是这个样子）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-net</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-net"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>tokio</span><span> = { </span><span>version</span><span> = </span><span>"1"</span><span>, </span><span>features</span><span> = [</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"rt-multi-thread"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"net"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"io-util"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"macros"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"sync"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"time"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"signal"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>] }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Tokio 把功能拆成了很多 feature，不开就不编译：<code>rt-multi-thread</code> 是多线程运行时，<code>net</code> 是 TCP，<code>io-util</code> 是 <code>read_exact</code> 这类读写辅助，<code>macros</code> 是 <code>#[tokio::main]</code>、<code>#[tokio::test]</code>、<code>select!</code>，<code>sync</code> 是 Tokio 自己的 <code>Mutex</code> 和 <code>oneshot</code>，<code>time</code> 是 <code>timeout</code> 和 <code>sleep</code>，<code>signal</code> 是 <code>Ctrl-C</code>。这一篇每一个我们都会用到。</p><p>第 1 篇说过，实验在 <code>~/rust-lab</code> 里做，每个实验一个 <code>src/bin/ch14_&lt;主题&gt;.rs</code>。这一篇有一半的实验要用 Tokio，我把它们放在另一个独立的 crate <code>~/lab-ch14-tokio</code> 里（<code>Cargo.toml</code> 在用到的时候给出；完整版在仓库的 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch14-tokio" target="_blank">lab/lab-ch14-tokio</a>，可以直接用），免得给 <code>rust-lab</code> 添一堆依赖。</p></section>
<section><h2>线程<a href="#线程"><span>#</span></a></h2><section><h3>先看 C 里会发生什么<a href="#先看-c-里会发生什么"><span>#</span></a></h3><p>下面是那个丢加法的程序，存成 <code>ch14_race.c</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;pthread.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>static</span><span> </span><span>int</span><span> counter </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>*</span><span>work</span><span>(</span><span>void</span><span> </span><span>*</span><span><span>arg</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>(</span><span>void</span><span>)arg;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>1000000</span><span>; i</span><span>++</span><span>) counter</span><span>++</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pthread_t</span><span> </span><span>t</span><span>[</span><span>8</span><span>];</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>8</span><span>; i</span><span>++</span><span>) </span><span>pthread_create</span><span>(</span><span>&amp;</span><span>t</span><span>[i], </span><span>NULL</span><span>, work, </span><span>NULL</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>8</span><span>; i</span><span>++</span><span>) </span><span>pthread_join</span><span>(</span><span>t</span><span>[i], </span><span>NULL</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>printf</span><span>(</span><span>"counter = </span><span>%d</span><span> (expected 8000000)</span><span>\n</span><span>"</span><span>, counter);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gcc -O0 -Wno-cpp -pthread -o ch14_race ch14_race.c &amp;&amp; ./ch14_race</span></div></div><div><div><div>2</div></div><div><span>counter = 1353007 (expected 8000000)</span></div></div><div><div><div>3</div></div><div><span>$ ./ch14_race</span></div></div><div><div><div>4</div></div><div><span>counter = 1318802 (expected 8000000)</span></div></div><div><div><div>5</div></div><div><span>$ ./ch14_race</span></div></div><div><div><div>6</div></div><div><span>counter = 1344368 (expected 8000000)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>counter++</code> 是读、加、写三步，八个线程交错着做，互相覆盖。这是数据竞争（data race），在 C 和 C++ 里是未定义行为，不只是”结果不对”：编译器可以假设你的程序里没有它，然后基于这个假设优化。检测它要靠运行时工具（ThreadSanitizer），而且只能抓到跑到的那一次。</p></section><section><h3>Rust 的第一道关：spawn 要求闭包拥有它用到的东西<a href="#rust-的第一道关spawn-要求闭包拥有它用到的东西"><span>#</span></a></h3><p>Rust 的标准线程是 <code>std::thread::spawn</code>，它接受一个闭包，在新线程里运行，返回一个 <code>JoinHandle</code>。我们先写一个看起来最自然的版本，实验 <code>ch14_spawn.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::thread;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>files</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>"fib.pebble"</span><span>.</span><span>to_string</span><span>(), </span><span>"hello.pebble"</span><span>.</span><span>to_string</span><span>()];</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>handle</span><span><span> </span><span>=</span><span> </span><span>thread</span><span>::</span></span><span>spawn</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>println!</span><span>(</span><span>"checking {} files"</span><span>, </span><span>files</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>handle</span><span>.</span><span>join</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_spawn</span></div></div><div><div><div>2</div></div><div><span>error[E0373]: closure may outlive the current function, but it borrows `files`, which is owned by the current function</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch14_spawn.rs:6:32</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>6 |     let handle = thread::spawn(|| {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                                ^^ may outlive borrowed value `files`</span></div></div><div><div><div>7</div></div><div><span>7 |         println!("checking {} files", files.len());</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|                                       ----- `files` is borrowed here</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>note: function requires argument type to outlive `'static`</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch14_spawn.rs:6:18</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>6 |       let handle = thread::spawn(|| {</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|  __________________^</span></div></div><div><div><div>15</div></div><div><span>7 | |         println!("checking {} files", files.len());</span></div></div><div><div><div>16</div></div><div><span>8 | |     });</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>| |______^</span></div></div><div><div><div>18</div></div><div><span>help: to force the closure to take ownership of `files` (and any other referenced variables), use the `move` keyword</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>20</div></div><div><span>6 |     let handle = thread::spawn(move || {</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>|                                ++++</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>For more information about this error, try `rustc --explain E0373`.</span></div></div><div><div><div>24</div></div><div><span>error: could not compile `rust-lab` (bin "ch14_spawn") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>在这个程序里 <code>handle.join()</code> 就在下面，<code>files</code> 其实活得足够久。但编译器看的是 <code>thread::spawn</code> 的签名，而不是你的意图。它要求闭包是 <code>'static</code>（第 4 篇讲过：不借用任何会消失的局部变量），因为 <code>spawn</code> 返回之后线程可以活得比当前函数更久，你甚至可以把 <code>JoinHandle</code> 丢掉，让线程自己飞。C 里的 <code>pthread_create(&amp;t, NULL, work, &amp;local)</code> 把指向栈变量的指针传给新线程，是同一类错误，编译器不会拦你，悬空指针要到运行时才爆。</p><p>提示已经给了答案：<code>move</code>。闭包把 <code>files</code> 整个搬进去，主线程再也不能用它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>handle</span><span><span> </span><span>=</span><span> </span><span>thread</span><span>::</span></span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>println!</span><span>(</span><span>"checking {} files"</span><span>, </span><span>files</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>3</div></div><div><span>    </span><span>files</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>4</div></div><div><span>});</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>handle</span><span>.</span><span>join</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>7</div></div><div><span>println!</span><span>(</span><span>"the thread returned {n}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_spawn_move</span></div></div><div><div><div>2</div></div><div><span>checking 2 files</span></div></div><div><div><div>3</div></div><div><span>the thread returned 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（完整文件是 <code>ch14_spawn_move.rs</code>。）<code>join()</code> 返回 <code>Result&lt;T, Box&lt;dyn Any + Send&gt;&gt;</code>，<code>T</code> 是闭包的返回值，<code>Err</code> 是线程 panic 时带出来的 payload，第 13 篇的 <code>catch_unwind</code> 里见过同一个类型。所以 <code>.join().unwrap()</code> 的意思是”等线程结束，拿结果，如果它 panic 了我也 panic”。</p></section><section><h3><code>thread::scope</code>：借用，但保证线程先结束<a href="#threadscope借用但保证线程先结束"><span>#</span></a></h3><p><code>move</code> 的代价是所有权被拿走。<code>pebble check</code> 要做的是让 N 个线程各自读取 <code>args.files</code> 里的一项，读完就结束，我们显然不想把整个 <code>Vec&lt;PathBuf&gt;</code> 克隆 N 份。这正是 <code>thread::scope</code>（Rust 1.63 稳定）的用处，实验 <code>ch14_scope.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::thread;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>files</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>"fib.pebble"</span><span>.</span><span>to_string</span><span>(), </span><span>"hello.pebble"</span><span>.</span><span>to_string</span><span>()];</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>lengths</span><span>: </span><span>Vec</span><span>&lt;</span><span>usize</span><span><span>&gt; </span><span>=</span><span> </span><span>thread</span><span>::</span></span><span>scope</span><span><span>(</span><span>|</span></span><span>scope</span><span><span>|</span><span> {</span></span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>handles</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> </span></span><span>files</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>name</span><span><span>|</span><span> </span></span><span>scope</span><span>.</span><span>spawn</span><span><span>(</span><span>||</span><span> </span></span><span>name</span><span>.</span><span>len</span><span>()))</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>handles</span><span>.</span><span>into_iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>h</span><span><span>|</span><span> </span></span><span>h</span><span>.</span><span>join</span><span>().</span><span>unwrap</span><span>()).</span><span>collect</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{lengths:?}"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"still own {} files"</span><span>, </span><span>files</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_scope</span></div></div><div><div><div>2</div></div><div><span>[10, 12]</span></div></div><div><div><div>3</div></div><div><span>still own 2 files</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>thread::scope</code> 接受一个闭包，闭包拿到一个 <code>scope</code>；通过 <code>scope.spawn</code> 创建的线程可以借用作用域外面的变量，因为 <code>thread::scope</code> 在返回之前一定会等所有这些线程结束。编译器因此可以放心地把 <code>'static</code> 的要求放宽成”至少活到这个 <code>scope</code> 结束”。这是 RAII 在线程上的应用：作用域结束 = <code>join</code>，没法忘记。如果你熟悉 C++20 的 <code>std::jthread</code>，它的析构函数会自动 <code>join</code>，思路接近；区别是 <code>jthread</code> 不阻止你把一个会悬空的引用传进去，<code>scope</code> 阻止。</p><p>注意最后一行：<code>files</code> 还在，而且我们同时让两个线程借用了它。共享的只读借用 <code>&amp;T</code>，天然可以同时存在很多个。那共享的可变借用呢？实验 <code>ch14_race.rs</code> 用 Rust 复刻上面的 C 程序：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>counter</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>u32</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>thread</span><span>::</span></span><span>scope</span><span><span>(</span><span>|</span></span><span>scope</span><span><span>|</span><span> {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>2</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>scope</span><span>.</span><span>spawn</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>6</div></div><div><span>            </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>1000</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>counter</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_race</span></div></div><div><div><div>2</div></div><div><span>error[E0499]: cannot borrow `counter` as mutable more than once at a time</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_race.rs:8:25</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>6 |       thread::scope(|scope| {</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|                      ----- has type `&amp;'1 Scope&lt;'1, '_&gt;`</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>7 |           for _ in 0..2 {</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>8 |               scope.spawn(|| {</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|               -           ^^ `counter` was mutably borrowed here in the previous iteration of the loop</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|  _____________|</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>| |</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>9 | |                 for _ in 0..1000 {</span></div></div><div><div><div>13</div></div><div><span>10 | |                     counter += 1;</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>| |                     ------- borrows occur due to use of `counter` in closure</span></div></div><div><div><div>15</div></div><div><span>11 | |                 }</span></div></div><div><div><div>16</div></div><div><span>12 | |             });</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>| |______________- argument requires that `counter` is borrowed for `'1`</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>19</div></div><div><span>note: requirement that the value outlives `'1` introduced here</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/std/src/thread/scoped.rs:203:34</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>For more information about this error, try `rustc --explain E0499`.</span></div></div><div><div><div>23</div></div><div><span>error: could not compile `rust-lab` (bin "ch14_race") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这就是第 2 篇的”别名与修改不能共存”规则，一个字没变。数据竞争的定义是”两个以上的线程同时访问同一块内存，其中至少一个在写，且没有同步”；“可变借用是独占的”这条规则恰好把前两个条件拆掉了。所以 Rust 里的数据竞争在安全代码里根本写不出来，不需要额外的线程规则。线程专属的规则只有两条，下面讲。</p></section><section><h3>Send 和 Sync<a href="#send-和-sync"><span>#</span></a></h3><p>第 13 篇提过：<code>Send</code> 的意思是”这个值可以被移动到另一个线程”，<code>Sync</code> 的意思是”<code>&amp;T</code> 可以同时被多个线程拿着”。两者都是自动 trait，编译器看字段推导。它们之间有一条恒等式：<code>T: Sync</code> 当且仅当 <code>&amp;T: Send</code>。</p><p><code>thread::spawn</code> 的签名里有 <code>F: Send + 'static</code>，<code>scope.spawn</code> 里有 <code>F: Send + 'scope</code>。所以任何一个被闭包捕获的东西如果不是 <code>Send</code>，就上不了线程。实验 <code>ch14_rc_send.rs</code>，把第 8 篇的 <code>Rc</code> 送进线程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>shared</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span>(</span><span>5</span><span>);</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>clone</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>shared</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>let</span><span> </span><span>handle</span><span><span> </span><span>=</span><span> </span><span>thread</span><span>::</span></span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>println!</span><span>(</span><span>"in the thread: {clone}"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_rc_send</span></div></div><div><div><div>2</div></div><div><span>error[E0277]: `Rc&lt;i32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_rc_send.rs:8:32</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>8 |       let handle = thread::spawn(move || {</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|                    ------------- ^------</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|                    |             |</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|  __________________|_____________within this `{closure@src/bin/ch14_rc_send.rs:8:32: 8:39}`</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>| |                  |</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>| |                  required by a bound introduced by this call</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>9 | |         println!("in the thread: {clone}");</span></div></div><div><div><div>12</div></div><div><span>10 | |     });</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>| |_____^ `Rc&lt;i32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>= help: within `{closure@src/bin/ch14_rc_send.rs:8:32: 8:39}`, the trait `Send` is not implemented for `Rc&lt;i32&gt;`</span></div></div><div><div><div>16</div></div><div><span>note: required because it's used within this closure</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_rc_send.rs:8:32</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>8 |       let handle = thread::spawn(move || {</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>|                                ^^^^^^^</span></div></div><div><div><div>21</div></div><div><span>note: required by a bound in `spawn`</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/std/src/thread/functions.rs:125:0</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>25</div></div><div><span>error: could not compile `rust-lab` (bin "ch14_rc_send") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第 8 篇说过 <code>Rc</code> 的计数是非原子的。两个线程各自 <code>clone</code> 或者 <code>drop</code>，计数器的读改写就会像上面那个 C 程序一样互相覆盖，最后要么泄漏，要么释放两次。<code>Rc</code> 没有实现 <code>Send</code> 这件事，正是为了阻止这种情况，编译器用 <code>Rc</code> 这个类型的名字就拒绝了，不需要分析任何数据流。C++ 的 <code>shared_ptr</code> 选了另一条路：计数永远是原子的，无论你用不用得上；Rust 选择在类型里让你为”用不用得上”付账：单线程用 <code>Rc</code>，便宜；跨线程用 <code>Arc</code>，贵一点。</p><p><code>Sync</code> 的反例是 <code>Cell</code>。第 11 篇的 <code>Cell&lt;bool&gt;</code> 允许通过 <code>&amp;self</code> 修改，单线程里完全安全，但两个线程各拿一个 <code>&amp;Cell</code> 就是数据竞争了。实验 <code>ch14_cell_sync.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>hits</span><span><span> </span><span>=</span><span> </span></span><span>Cell</span><span>::</span><span>new</span><span>(</span><span>0</span><span>u32</span><span>);</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>thread</span><span>::</span></span><span>scope</span><span><span>(</span><span>|</span></span><span>scope</span><span><span>|</span><span> {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>scope</span><span>.</span><span>spawn</span><span><span>(</span><span>||</span><span> </span></span><span>hits</span><span>.</span><span>set</span><span>(</span><span>hits</span><span>.</span><span>get</span><span>() + </span><span>1</span><span>));</span></div></div><div><div><div>5</div></div><div><span>    </span><span>scope</span><span>.</span><span>spawn</span><span><span>(</span><span>||</span><span> </span></span><span>hits</span><span>.</span><span>set</span><span>(</span><span>hits</span><span>.</span><span>get</span><span>() + </span><span>1</span><span>));</span></div></div><div><div><div>6</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_cell_sync</span></div></div><div><div><div>2</div></div><div><span>error[E0277]: `Cell&lt;u32&gt;` cannot be shared between threads safely</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch14_cell_sync.rs:8:21</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>8 |         scope.spawn(|| hits.set(hits.get() + 1));</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|               ----- ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Cell&lt;u32&gt;` cannot be shared between threads safely</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|               |</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|               required by a bound introduced by this call</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>= help: the trait `Sync` is not implemented for `Cell&lt;u32&gt;`</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicU32` instead</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>= note: required for `&amp;Cell&lt;u32&gt;` to implement `Send`</span></div></div><div><div><div>13</div></div><div><span>note: required because it's used within this closure</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch14_cell_sync.rs:8:21</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span>8 |         scope.spawn(|| hits.set(hits.get() + 1));</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|                     ^^</span></div></div><div><div><div>18</div></div><div><span>note: required by a bound in `Scope::&lt;'scope, 'env&gt;::spawn`</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/std/src/thread/scoped.rs:201:4</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>22</div></div><div><span>error: could not compile `rust-lab` (bin "ch14_cell_sync") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意 <code>required for &amp;Cell&lt;u32&gt; to implement Send</code> 这一行，就是上面那条恒等式。编译器甚至推荐了两个替代品：<code>RwLock</code> 和原子类型。</p><p>我们已经见过的类型分成这么几类。实验 <code>ch14_send_sync_table.rs</code> 让编译器当裁判：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>is_send</span><span>&lt;</span><span>T</span><span>: </span><span>Send</span><span>&gt;() {}</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>is_sync</span><span>&lt;</span><span>T</span><span>: </span><span>Sync</span><span>&gt;() {}</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>is_send</span><span>::&lt;</span><span>String</span><span>&gt;();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>is_sync</span><span>::&lt;</span><span>String</span><span>&gt;();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>is_send</span><span>::&lt;</span><span>Arc</span><span>&lt;</span><span>Mutex</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>u32</span><span>&gt;&gt;&gt;&gt;();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>is_sync</span><span>::&lt;</span><span>Arc</span><span>&lt;</span><span>Mutex</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>u32</span><span>&gt;&gt;&gt;&gt;();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>is_send</span><span>::&lt;</span><span>Cell</span><span>&lt;</span><span>u32</span><span>&gt;&gt;();</span></div></div><div><div><div>10</div></div><div><span>    </span><span>is_send</span><span>::&lt;</span><span>RefCell</span><span>&lt;</span><span>u32</span><span>&gt;&gt;();</span></div></div><div><div><div>11</div></div><div><span>    </span><span>is_send</span><span>::&lt;</span><span>Rc</span><span>&lt;</span><span>u32</span><span>&gt;&gt;();</span></div></div><div><div><div>12</div></div><div><span>    </span><span>is_send</span><span>::&lt;</span><span>MutexGuard</span><span>&lt;'</span><span>static</span><span>, </span><span>u32</span><span>&gt;&gt;();</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_send_sync_table</span></div></div><div><div><div>2</div></div><div><span>error[E0277]: `Rc&lt;u32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_send_sync_table.rs:15:15</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>15 |     is_send::&lt;Rc&lt;u32&gt;&gt;();</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|               ^^^^^^^ `Rc&lt;u32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= help: the trait `Send` is not implemented for `Rc&lt;u32&gt;`</span></div></div><div><div><div>9</div></div><div><span>note: required by a bound in `is_send`</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_send_sync_table.rs:5:15</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>5 | fn is_send&lt;T: Send&gt;() {}</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|               ^^^^ required by this bound in `is_send`</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>error[E0277]: `std::sync::MutexGuard&lt;'static, u32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_send_sync_table.rs:16:15</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>18</div></div><div><span>16 |     is_send::&lt;MutexGuard&lt;'static, u32&gt;&gt;();</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>|               ^^^^^^^^^^^^^^^^^^^^^^^^ `std::sync::MutexGuard&lt;'static, u32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>= help: the trait `Send` is not implemented for `std::sync::MutexGuard&lt;'static, u32&gt;`</span></div></div><div><div><div>22</div></div><div><span>note: required by a bound in `is_send`</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_send_sync_table.rs:5:15</span></div></div><div><div><div>24</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>5 | fn is_send&lt;T: Send&gt;() {}</span></div></div><div><div><div>26</div></div><div><span><span>   </span></span><span>|               ^^^^ required by this bound in `is_send`</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>29</div></div><div><span>error: could not compile `rust-lab` (bin "ch14_send_sync_table") due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>八行里只有两行被拒绝：<code>Rc</code> 和 <code>MutexGuard</code>。其余的都过了，包括容易让人意外的两行：<code>Cell&lt;u32&gt;</code> 和 <code>RefCell&lt;u32&gt;</code> 是 <code>Send</code>（把它们整个搬到另一个线程没问题，搬走之后原来的线程再也碰不到它），只是不是 <code>Sync</code>（不能让两个线程同时借用）。<code>Arc&lt;Mutex&lt;Vec&lt;u32&gt;&gt;&gt;</code> 是 <code>Send</code> 加 <code>Sync</code>，我们马上就用它。</p><p><code>MutexGuard</code> 不是 <code>Send</code> 是一个有趣的细节：POSIX 要求”谁加的锁谁解”，把加了锁的 guard 搬到另一个线程去释放，在 pthread 上是未定义行为。Rust 把这个平台约束写进了类型里。这个细节在讲 async 的时候还会再出现一次，而且是个大角色。</p><div><div><div></div><div>编译器怎么知道？</div></div><div><p>这些自动 trait 不是魔法，是组合：一个类型是 <code>Send</code> 当且仅当所有字段是 <code>Send</code>。<code>Rc</code> 内部是一个裸指针（第 13 篇：裸指针故意不是 <code>Send</code>），所以传染到了所有包着 <code>Rc</code> 的类型。<code>Arc</code> 里也有裸指针，但标准库为它写了 <code>unsafe impl&lt;T: Send + Sync&gt; Send for Arc&lt;T&gt; {}</code>，一句话”我保证它没问题，原因是它的计数是原子的”。<code>unsafe impl</code> 是第 13 篇 <code>BumpAllocator</code> 用过的同一个东西。</p></div></div></section><section><h3>Arc 和 Mutex：真正的共享可变<a href="#arc-和-mutex真正的共享可变"><span>#</span></a></h3><p>要让多个线程都改同一个值，需要两个零件：一个能被多个线程拥有的指针（<code>Arc</code>，原子引用计数，<code>Rc</code> 的线程安全版），和一个保证同一时间只有一个线程在改它的锁（<code>Mutex</code>）。实验 <code>ch14_arc_mutex.rs</code>，Rust 版的计数器：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>counter</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>new</span><span><span>(</span><span>Mutex</span><span>::</span></span><span>new</span><span>(</span><span>0</span><span>u32</span><span>));</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>let</span><span> </span><span>handles</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>8</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> {</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>counter</span><span><span> </span><span>=</span><span> </span><span>Arc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>counter</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>thread</span><span>::</span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>7</div></div><div><span>            </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>1000</span><span> {</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>*</span><span>counter</span><span>.</span><span>lock</span><span>().</span><span>unwrap</span><span><span>() </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>for</span><span> </span><span>handle</span><span> </span><span>in</span><span> </span><span>handles</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>handle</span><span>.</span><span>join</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>println!</span><span>(</span><span>"count = {}"</span><span>, *</span><span>counter</span><span>.</span><span>lock</span><span>().</span><span>unwrap</span><span>());</span></div></div><div><div><div>19</div></div><div><span>println!</span><span>(</span><span>"strong count = {}"</span><span>, </span><span>Arc</span><span>::</span><span>strong_count</span><span>(&amp;</span><span>counter</span><span>));</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_arc_mutex</span></div></div><div><div><div>2</div></div><div><span>count = 8000</span></div></div><div><div><div>3</div></div><div><span>strong count = 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>八个线程，每个加 1000，8000，正确，每次都一样。<code>strong count = 1</code> 说明八个线程结束、闭包被丢弃之后，那八个 <code>Arc</code> 克隆都已经释放了。</p><p>和 C++ 的 <code>std::mutex</code> 比较，<code>Mutex&lt;T&gt;</code> 有一个很大的设计差别：<strong>数据在锁里面</strong>。<code>std::mutex</code> 和它保护的变量是两个独立的对象，“哪个锁保护哪个变量”只存在于注释和程序员的脑子里，忘了加锁照样编译。<code>Mutex&lt;u32&gt;</code> 里装着那个 <code>u32</code>，唯一拿到它的办法是 <code>lock()</code>，返回一个 <code>MutexGuard</code>；guard 实现了 <code>Deref</code> 和 <code>DerefMut</code>，所以 <code>*guard += 1</code> 能用；guard 被丢弃的时候自动解锁（第 13 篇的 <code>Drop</code>，对应 C++ 的 <code>lock_guard</code>，只是 Rust 里它不是可选的）。实验 <code>ch14_mutex_forgot_lock.rs</code> 试着绕过：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_mutex_forgot_lock</span></div></div><div><div><div>2</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch14_mutex_forgot_lock.rs:5:22</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>5 |     let guard: u32 = counter.lock().unwrap();</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                ---   ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found `MutexGuard&lt;'_, u32&gt;`</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                |</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|                expected due to this</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>= note: expected type `u32`</span></div></div><div><div><div>11</div></div><div><span><span>           </span></span><span>found struct `std::sync::MutexGuard&lt;'_, u32&gt;`</span></div></div><div><div><div>12</div></div><div><span>help: consider dereferencing the type</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>5 |     let guard: u32 = *counter.lock().unwrap();</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|                      +</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>For more information about this error, try `rustc --explain E0308`.</span></div></div><div><div><div>18</div></div><div><span>error: could not compile `rust-lab` (bin "ch14_mutex_forgot_lock") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>为什么 <code>lock()</code> 后面要 <code>unwrap()</code>？因为 <code>lock()</code> 返回的是 <code>Result&lt;MutexGuard&lt;T&gt;, PoisonError&lt;...&gt;&gt;</code>。如果一个线程在持有锁的时候 panic，那个锁就被标记为”中毒”（poisoned）：被保护的数据可能停在一个只改了一半的状态里，标准库不会替你假装没事。实验 <code>ch14_poison.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>data</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>new</span><span><span>(</span><span>Mutex</span><span>::</span></span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]));</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>let</span><span> </span><span>worker</span><span><span> </span><span>=</span><span> {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>data</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>clone</span><span>(&amp;</span><span>data</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>thread</span><span>::</span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>guard</span><span><span> </span><span>=</span><span> </span></span><span>data</span><span>.</span><span>lock</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>7</div></div><div><span>        </span><span>guard</span><span>.</span><span>push</span><span>(</span><span>4</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>panic!</span><span>(</span><span>"worker died while holding the lock"</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div><div><div><div>11</div></div><div><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>worker</span><span>.</span><span>join</span><span>();</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>println!</span><span>(</span><span>"is_poisoned = {}"</span><span>, </span><span>data</span><span>.</span><span>is_poisoned</span><span>());</span></div></div><div><div><div>14</div></div><div><span>match</span><span> </span><span>data</span><span>.</span><span>lock</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Ok</span><span>(</span><span>guard</span><span>) =&gt; </span><span>println!</span><span>(</span><span>"clean: {guard:?}"</span><span>),</span></div></div><div><div><div>16</div></div><div><span>    </span><span>Err</span><span>(</span><span>poisoned</span><span>) =&gt; {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>let</span><span> </span><span>guard</span><span><span> </span><span>=</span><span> </span></span><span>poisoned</span><span>.</span><span>into_inner</span><span>();</span></div></div><div><div><div>18</div></div><div><span>        </span><span>println!</span><span>(</span><span>"recovered: {guard:?}"</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_poison</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>thread '&lt;unnamed&gt;' (167084) panicked at src/bin/ch14_poison.rs:12:13:</span></div></div><div><div><div>4</div></div><div><span>worker died while holding the lock</span></div></div><div><div><div>5</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>6</div></div><div><span>is_poisoned = true</span></div></div><div><div><div>7</div></div><div><span>recovered: [1, 2, 3, 4]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>PoisonError::into_inner</code> 把 guard 拿出来，由你决定数据还能不能用。在这个例子里，<code>push(4)</code> 已经完成，数据是完整的，所以恢复是合理的。<code>pebble-net</code> 里有一个 <code>SharedOutput</code>，它用的就是这个写法：<code>.lock().unwrap_or_else(|poisoned| poisoned.into_inner())</code>。理由是：它里面只有一个 <code>Vec&lt;String&gt;</code>，每次修改都是单独的一次 <code>push</code>，不会出现”改了一半”，所以不值得因为别的线程 panic 就让整个服务也跟着 panic。（C++ 的 <code>std::mutex</code> 没有中毒的概念：持有锁的线程如果异常展开，<code>lock_guard</code> 的析构函数会解锁，数据被改了一半，下一个线程什么也不知道。）</p><p>另外两个同类零件，不在 Pebble 里出现，我各用一个小实验确认过它们的行为。<code>RwLock</code> 是读写锁：很多读者或者一个写者，对应 C++ 的 <code>std::shared_mutex</code>。读多写少、读的时间长的时候才值得换（实验 <code>ch14_rwlock.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>table</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>new</span><span><span>(</span><span>RwLock</span><span>::</span></span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]));</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>let</span><span> </span><span>readers</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>3</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> {</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>table</span><span><span> </span><span>=</span><span> </span><span>Arc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>table</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>thread</span><span>::</span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> </span></span><span>table</span><span>.</span><span>read</span><span>().</span><span>unwrap</span><span>().</span><span>len</span><span>())</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>table</span><span>.</span><span>write</span><span>().</span><span>unwrap</span><span>().</span><span>push</span><span>(</span><span>4</span><span>);</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>let</span><span> </span><span>lengths</span><span>: </span><span>Vec</span><span>&lt;</span><span>usize</span><span><span>&gt; </span><span>=</span><span> </span></span><span>readers</span><span>.</span><span>into_iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>h</span><span><span>|</span><span> </span></span><span>h</span><span>.</span><span>join</span><span>().</span><span>unwrap</span><span>()).</span><span>collect</span><span>();</span></div></div><div><div><div>13</div></div><div><span>println!</span><span>(</span><span>"every reader saw 3 or 4 elements: {}"</span><span>, </span><span>lengths</span><span>.</span><span>iter</span><span>().</span><span>all</span><span><span>(</span><span>|</span></span><span>n</span><span><span>|</span><span> (</span></span><span>3</span><span>..=</span><span>4</span><span>).</span><span>contains</span><span>(</span><span>n</span><span>)));</span></div></div><div><div><div>14</div></div><div><span>println!</span><span>(</span><span>"final {:?}"</span><span>, </span><span>table</span><span>.</span><span>read</span><span>().</span><span>unwrap</span><span>());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_rwlock</span></div></div><div><div><div>2</div></div><div><span>every reader saw 3 or 4 elements: true</span></div></div><div><div><div>3</div></div><div><span>final [1, 2, 3, 4]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读者看到的是写之前（3 个元素）还是写之后（4 个），取决于调度；我没有断言具体是哪个，因为那是不确定的。确定的是：没有任何读者看到过”半个 push”。</p></section><section><h3>消息传递：mpsc<a href="#消息传递mpsc"><span>#</span></a></h3><p>另一条路是不共享，用消息。<code>std::sync::mpsc</code> 是”多生产者、单消费者”的通道，Go 的 <code>chan</code> 的亲戚。实验 <code>ch14_channel.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> (</span><span>sender</span><span>, </span><span>receiver</span><span><span>) </span><span>=</span><span> </span><span>mpsc</span><span>::</span></span><span>channel</span><span>::&lt;</span><span>String</span><span>&gt;();</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>let</span><span> </span><span>workers</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> [</span></span><span>"fib"</span><span>, </span><span>"hello"</span><span>, </span><span>"errors"</span><span>]</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>.</span><span>into_iter</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>name</span><span><span>|</span><span> {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>sender</span><span><span> </span><span>=</span><span> </span></span><span>sender</span><span>.</span><span>clone</span><span>();</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>thread</span><span>::</span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>8</div></div><div><span>            </span><span>sender</span><span>.</span><span>send</span><span>(</span><span>format!</span><span>(</span><span>"checked {name}.pebble"</span><span>)).</span><span>unwrap</span><span>();</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>12</div></div><div><span>drop</span><span>(</span><span>sender</span><span>);</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>reports</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span><span>&gt; </span><span>=</span><span> </span></span><span>receiver</span><span>.</span><span>iter</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>15</div></div><div><span>reports</span><span>.</span><span>sort</span><span>();</span></div></div><div><div><div>16</div></div><div><span>for</span><span> </span><span>report</span><span> </span><span>in</span><span> &amp;</span><span>reports</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{report}"</span><span>);</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>for</span><span> </span><span>worker</span><span> </span><span>in</span><span> </span><span>workers</span><span> {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>worker</span><span>.</span><span>join</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div><span>println!</span><span>(</span><span>"{} reports, channel closed"</span><span>, </span><span>reports</span><span>.</span><span>len</span><span>());</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_channel</span></div></div><div><div><div>2</div></div><div><span>checked errors.pebble</span></div></div><div><div><div>3</div></div><div><span>checked fib.pebble</span></div></div><div><div><div>4</div></div><div><span>checked hello.pebble</span></div></div><div><div><div>5</div></div><div><span>3 reports, channel closed</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>receiver.iter()</code> 会一直等到所有 <code>Sender</code> 都被丢弃才结束。这里的 <code>drop(sender)</code> 很关键：我们手里还留着一个原始的 <code>sender</code>，不丢掉的话 <code>receiver.iter()</code> 永远等不到结尾。（忘了这一行是 mpsc 最常见的死锁，现象是程序卡在 <code>collect()</code> 上。）三个线程完成的顺序是不确定的，所以我排了序再打印；通道里消息的到达顺序是真实的调度顺序，不是命令行顺序，下面你会看到 <code>pebble check</code> 为什么选择了另一条路：不用通道，而是按顺序 <code>join</code>。</p><p>最后一个零件上一篇已经见过：原子类型。实验 <code>ch14_atomic.rs</code> 把八个线程换成原子计数器，一个 <code>static REQUESTS: AtomicU64</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_atomic</span></div></div><div><div><div>2</div></div><div><span>requests = 8000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>fetch_add(1, Ordering::Relaxed)</code> 是一条不可分割的”读加写”指令，同时不需要给其他内存访问排序，所以用最弱的 <code>Relaxed</code>（第 13 篇解释过为什么 <code>BumpAllocator</code> 里这也够用）。一个独立的计数器，没有锁，没有 <code>Arc</code>，因为 <code>static</code> 本身就活得足够久。本篇后面的 <code>Stats</code> 就是这种东西的候选，这个替换放在练习 2。</p></section></section>
<section><h2>命令行第五版：并行的 <code>check</code><a href="#命令行第五版并行的-check"><span>#</span></a></h2><section><h3>现在的 <code>check</code><a href="#现在的-check"><span>#</span></a></h3><p>第 12 篇结束时，<code>pebble check</code> 只接受一个文件，一次读、词法分析加解析、编译、把两份诊断打印出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>check</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>failed</span><span><span> </span><span>|=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>compiled</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我想让它接受任意多个文件，每个文件互不相干，是教科书上”天生可并行”的活。第一步是命令行的形状：<code>pebble check a.pebble b.pebble c.pebble</code>。在 <code>crates/pebble-cli/src/cli.rs</code> 里，<code>Check</code> 不再用 <code>FileArgs</code>（<code>tokens</code> 和 <code>parse</code> 还在用它），给它一个自己的参数结构：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Arguments to `pebble check`.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Args</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>CheckArgs</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// The source files to check; each one is checked on its own thread.</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[arg(required </span><span>=</span><span> true)]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>files</span><span>: </span><span>Vec</span><span>&lt;</span><span>PathBuf</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Vec&lt;PathBuf&gt;</code> 让 clap 接受一个或多个位置参数；<code>required = true</code> 是说至少要一个（不加的话 <code>Vec</code> 的默认是零个或多个，<code>pebble check</code> 会静悄悄地什么也不做）。<code>Command</code> 枚举里对应的一行也改掉：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Lex, parse, and compile source files without running them.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Check</span><span>(</span><span>CheckArgs</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>doc 注释就是帮助文本（第 7 篇），所以这一句话同时改了 <code>pebble --help</code> 的输出。</p></section><section><h3>第一次尝试：照搬 <code>thread::spawn</code><a href="#第一次尝试照搬-threadspawn"><span>#</span></a></h3><p><code>commands.rs</code> 里的 <code>check</code> 要对每个文件开一个线程，等所有线程结束，按命令行里的顺序打印结果。我的第一版几乎是从上面的实验里抄的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>check</span><span>(</span><span>args</span><span>: &amp;</span><span>CheckArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>reports</span><span>: </span><span>Vec</span><span>&lt;</span><span>Result</span><span>&lt;</span><span>CheckReport</span><span><span>&gt;&gt; </span><span>=</span><span> {</span></span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>handles</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> </span></span><span>args</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>.files</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>path</span><span><span>|</span><span> </span><span>thread</span><span>::</span></span><span>spawn</span><span><span>(</span><span>||</span><span> </span></span><span>check_file</span><span>(</span><span>path</span><span>)))</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>8</div></div><div><span>        </span><span>handles</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>.</span><span>into_iter</span><span>()</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>handle</span><span><span>|</span><span> </span></span><span>handle</span><span>.</span><span>join</span><span>().</span><span>expect</span><span>(</span><span>"a check thread panicked"</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>13</div></div><div><span>    </span><span>// ... print the reports ...</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其中 <code>check_file(path: &amp;Path) -&gt; Result&lt;CheckReport&gt;</code> 是我们马上要写的、检查一个文件的函数。编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0521]: borrowed data escapes outside of function</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-cli/src/commands.rs:106:25</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>101 | pub(crate) fn check(args: &amp;CheckArgs) -&gt; Result&lt;u8&gt; {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                     ----  - let's call the lifetime of this reference `'1`</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|                     |</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|                     `args` is a reference that is only valid in the function body</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div><div><div><div>9</div></div><div><span>106 |             .map(|path| thread::spawn(|| check_file(path)))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|                         |</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>|                         `args` escapes the function body here</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>|                         argument requires that `'1` must outlive `'static`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>和上面的 <code>E0373</code> 同一个原因，只不过这次借用来自函数参数：<code>path</code> 指向 <code>args.files</code> 里面，<code>args</code> 是函数参数借来的引用，<code>thread::spawn</code> 要求 <code>'static</code>，所以不行。两条出路：要么把 <code>PathBuf</code> 克隆一份 <code>move</code> 进去，要么用 <code>thread::scope</code>。克隆在这里没毛病，但 <code>scope</code> 让这件事成了一个类型层面的保证（线程不会逃出函数），而且根本不用想”克隆谁”。</p></section><section><h3>第二次尝试：把编译结果也带回来<a href="#第二次尝试把编译结果也带回来"><span>#</span></a></h3><p>用 <code>thread::scope</code>，<code>check_file</code> 在线程里跑，返回什么？我最先想到的是返回一个包含 <code>Compiled</code> 的结构（“以后别的子命令也许想用”）。我把它写出来试了，作为一次对照：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>CheckReport</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>rendered</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>failed</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>compiled</span><span><span>: </span><span>pebble_bytecode</span><span>::</span></span><span>Compiled</span><span>,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `Rc&lt;FunctionProto&gt;` cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-cli/src/commands.rs:107:31</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>107 |             .map(|path| scope.spawn(|| check_file(path)))</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                               ^^^^^ `Rc&lt;FunctionProto&gt;` cannot be sent between threads safely</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>= help: within `Result&lt;CheckReport, anyhow::Error&gt;`, the trait `Send` is not implemented for `Rc&lt;FunctionProto&gt;`</span></div></div><div><div><div>8</div></div><div><span>note: required because it appears within the type `Compiled`</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-bytecode/src/lib.rs:371:12</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>371 | pub struct Compiled {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>|            ^^^^^^^^</span></div></div><div><div><div>13</div></div><div><span>note: required because it appears within the type `CheckReport`</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-cli/src/commands.rs:90:8</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>90 | struct CheckReport {</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>|        ^^^^^^^^^^^</span></div></div><div><div><div>18</div></div><div><span>note: required because it appears within the type `Result&lt;CheckReport, anyhow::Error&gt;`</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/result.rs:557:9</span></div></div><div><div><div>20</div></div><div><span>note: required by a bound in `Scope::&lt;'scope, 'env&gt;::spawn`</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/std/src/thread/scoped.rs:201:4</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第 12 篇我提过这件事：第 10 篇的 <code>Compiled</code> 里装着 <code>Rc&lt;FunctionProto&gt;</code>，所以它不是 <code>Send</code>。线程的返回值要从子线程交回主线程，所以必须是 <code>Send</code>。编译器的 <code>note: required because it appears within the type ...</code> 一路追到 <code>Rc</code>，和上面 <code>ch14_send_sync_table</code> 的逻辑一模一样。</p><p>那 Pebble 里别的东西谁能过这一关？我写了一个临时的测试文件，让编译器逐个裁决（这是一个探针，不进仓库）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>assert_send</span><span>&lt;</span><span>T</span><span>: </span><span>Send</span><span>&gt;() {}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[test]</span></div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>probe</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>assert_send</span><span><span>::&lt;</span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>&gt;();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>assert_send</span><span><span>::&lt;</span><span>pebble_ast</span><span>::</span></span><span>Program</span><span>&gt;();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>assert_send</span><span>::&lt;</span><span>Vec</span><span>&lt;</span><span>pebble_diag</span><span>::</span><span>Diagnostic</span><span>&gt;&gt;();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>assert_send</span><span><span>::&lt;</span><span>pebble_bytecode</span><span>::</span></span><span>Compiled</span><span>&gt;();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>assert_send</span><span><span>::&lt;</span><span>pebble_interp</span><span>::</span></span><span>Interpreter</span><span>&gt;();</span></div></div><div><div><div>10</div></div><div><span>    </span><span>assert_send</span><span><span>::&lt;</span><span>pebble_value</span><span>::</span></span><span>Value</span><span>&gt;();</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器对前三行一声不吭，对后三行各给了一段错误。我把每个错误的第一行和关键的 <code>help</code> 行摘出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `Rc&lt;FunctionProto&gt;` cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>= help: within `Compiled`, the trait `Send` is not implemented for `Rc&lt;FunctionProto&gt;`</span></div></div><div><div><div>3</div></div><div><span>error[E0277]: `Rc&lt;RefCell&lt;Env&gt;&gt;` cannot be sent between threads safely</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>= help: within `Interpreter`, the trait `Send` is not implemented for `Rc&lt;RefCell&lt;Env&gt;&gt;`</span></div></div><div><div><div>5</div></div><div><span>error[E0277]: `(dyn pebble_value::Output + 'static)` cannot be sent between threads safely</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>= help: the trait `Send` is not implemented for `(dyn pebble_value::Output + 'static)`</span></div></div><div><div><div>7</div></div><div><span>error[E0277]: `Rc&lt;str&gt;` cannot be sent between threads safely</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= help: within `pebble_value::Value`, the trait `Send` is not implemented for `Rc&lt;str&gt;`</span></div></div><div><div><div>9</div></div><div><span>error[E0277]: `Rc&lt;RefCell&lt;Vec&lt;pebble_value::Value&gt;&gt;&gt;` cannot be sent between threads safely</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>= help: within `pebble_value::Value`, the trait `Send` is not implemented for `Rc&lt;RefCell&lt;Vec&lt;pebble_value::Value&gt;&gt;&gt;`</span></div></div><div><div><div>11</div></div><div><span>error[E0277]: `Rc&lt;RefCell&lt;HashMap&lt;MapKey, pebble_value::Value&gt;&gt;&gt;` cannot be sent between threads safely</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>= help: within `pebble_value::Value`, the trait `Send` is not implemented for `Rc&lt;RefCell&lt;HashMap&lt;MapKey, pebble_value::Value&gt;&gt;&gt;`</span></div></div><div><div><div>13</div></div><div><span>error[E0277]: `Rc&lt;pebble_value::Function&gt;` cannot be sent between threads safely</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>= help: within `pebble_value::Value`, the trait `Send` is not implemented for `Rc&lt;pebble_value::Function&gt;`</span></div></div><div><div><div>15</div></div><div><span>error: could not compile `pebble-cli` (test "send_probe") due to 7 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（我省略了每个错误后面那几段 <code>note: required because ...</code> 和 <code>note: required by a bound in assert_send</code>，没有改动任何一行。）</p><p>结论很整齐：<strong>前端是 <code>Send</code>，后端不是。</strong> <code>SourceMap</code>、<code>Program</code>（整个 AST）、<code>Vec&lt;Diagnostic&gt;</code> 都是普通的拥有型数据：<code>String</code>、<code>Vec</code>、<code>Box</code>，没有 <code>Rc</code>。后端的东西，<code>Compiled</code>（<code>Rc&lt;FunctionProto&gt;</code>）、<code>Interpreter</code>（<code>Rc&lt;RefCell&lt;Env&gt;&gt;</code>，加一个 <code>Box&lt;dyn Output&gt;</code>，第 8 篇里没写 <code>+ Send</code>，所以连输出这个 trait 对象都不让过）、<code>Value</code>（第 8 篇的所有 <code>Rc</code>）全都被 <code>Rc</code> 污染了。</p><p>回头看第 5 篇的那个决定：AST 的节点拥有自己的 <code>String</code>，而不是借用源码。我当时说这能让 AST 活得久，还能跨线程。我并没有在那时验证，现在上面这三行 <code>assert_send</code> 就是验证。</p></section><section><h3>解决办法：线程里造，线程里用，只送文本出来<a href="#解决办法线程里造线程里用只送文本出来"><span>#</span></a></h3><p>既然 <code>Compiled</code> 过不了边界，那就别让它过：检查一个文件需要的东西全部在线程里创建，用完在线程里丢弃，只有一份纯文本的报告（一个 <code>String</code> 和一个 <code>bool</code>）穿过线程边界。写一个 <code>CheckReport</code>，再写 <code>check_file</code>，在 <code>crates/pebble-cli/src/commands.rs</code> 里，放在 <code>parse</code> 和 <code>bench</code> 之间：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The outcome of checking one file: its rendered diagnostics and whether any</span></div></div><div><div><div>2</div></div><div><span>/// of them is an error.</span></div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>CheckReport</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>rendered</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>failed</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Checks one file. Everything non-`Send` (the compiled chunk holds `Rc`s)</span></div></div><div><div><div>2</div></div><div><span>/// stays inside this function; only the rendered text leaves the thread.</span></div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>check_file</span><span>(</span><span>path</span><span>: &amp;</span><span>Path</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>CheckReport</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(</span><span>path</span><span>)?;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>render</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>rendered</span><span>.</span><span>push_str</span><span><span>(&amp;</span><span>sources</span><span>::</span></span><span>render</span><span>(&amp;</span><span>compiled</span><span>.diagnostics, &amp;</span><span>source</span><span>.map));</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span></span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>sources</span><span>::</span><span>has_errors</span><span>(&amp;</span><span>parsed</span><span><span>.diagnostics) </span><span>||</span><span> </span><span>sources</span><span>::</span></span><span>has_errors</span><span>(&amp;</span><span>compiled</span><span>.diagnostics);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>Ok</span><span>(</span><span>CheckReport</span><span> { </span><span>rendered</span><span>, </span><span>failed</span><span> })</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>sources::render</code> 和 <code>sources::has_errors</code> 在第 7 篇的 <code>sources.rs</code> 里就有了，只是 <code>emit</code> 把它们粘在了一起，直接打印到 <code>stderr</code>；现在我们要先把文字留着，所以用的是拆开的那一半。<code>Compiled</code> 在函数末尾和 <code>compiled</code> 变量一起被丢弃，线程根本不知道它存在过。<code>Result&lt;CheckReport&gt;</code> 里的错误是 <code>anyhow::Error</code>，它是 <code>Send</code>（上面第二次尝试的报错里没有提它，说明它过了关）。</p><p>然后是 <code>check</code> 本身：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// `pebble check`: lex, parse, and compile every file, each on its own thread,</span></div></div><div><div><div>2</div></div><div><span>/// then print the reports in command-line order.</span></div></div><div><div><div>3</div></div><div><span>///</span></div></div><div><div><div>4</div></div><div><span>/// # Errors</span></div></div><div><div><div>5</div></div><div><span>/// Returns the first host-level failure (such as an unreadable file), in</span></div></div><div><div><div>6</div></div><div><span>/// command-line order.</span></div></div><div><div><div>7</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>check</span><span>(</span><span>args</span><span>: &amp;</span><span>CheckArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>reports</span><span>: </span><span>Vec</span><span>&lt;</span><span>Result</span><span>&lt;</span><span>CheckReport</span><span><span>&gt;&gt; </span><span>=</span><span> </span><span>thread</span><span>::</span></span><span>scope</span><span><span>(</span><span>|</span></span><span>scope</span><span><span>|</span><span> {</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>handles</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> </span></span><span>args</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>.files</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>path</span><span><span>|</span><span> </span></span><span>scope</span><span>.</span><span>spawn</span><span><span>(</span><span>||</span><span> </span></span><span>check_file</span><span>(</span><span>path</span><span>)))</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>14</div></div><div><span>        </span><span>handles</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>.</span><span>into_iter</span><span>()</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>handle</span><span><span>|</span><span> </span></span><span>handle</span><span>.</span><span>join</span><span>().</span><span>expect</span><span>(</span><span>"a check thread panicked"</span><span>))</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>.</span><span>collect</span><span>()</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span></span><span>false</span><span>;</span></div></div><div><div><div>21</div></div><div><span>    </span><span>for</span><span> </span><span>report</span><span> </span><span>in</span><span> </span><span>reports</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>report</span><span><span> </span><span>=</span><span> </span></span><span>report</span><span>?;</span></div></div><div><div><div>23</div></div><div><span>        </span><span>eprint!</span><span>(</span><span>"{}"</span><span>, </span><span>report</span><span>.rendered);</span></div></div><div><div><div>24</div></div><div><span>        </span><span>failed</span><span><span> </span><span>|=</span><span> </span></span><span>report</span><span>.failed;</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>    </span><span>Ok</span><span>(</span><span>u8</span><span>::</span><span>from</span><span>(</span><span>failed</span><span>))</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐段看：</p><ul>
<li>第一个 <code>collect</code> 把每个 <code>scope.spawn(...)</code> 返回的 <code>ScopedJoinHandle</code> 收进一个 <code>Vec</code>。这一步必须先 <code>collect</code> 再 <code>join</code>：迭代器是惰性的，如果写成一条链 <code>.map(spawn).map(join)</code>，每个元素都是”开一个线程，马上等它，再处理下一个”，等于顺序执行。实验 <code>ch14_lazy_join.rs</code> 让四个线程各睡 100 毫秒，链式写法 <code>~400 ms</code>，先 <code>collect</code> 再 <code>join</code> 是 <code>~100 ms</code>（输出的毫秒数我按 100 取了整，两种写法的结果列表都是 <code>[0, 1, 2, 3]</code>）。</li>
<li>第二个 <code>collect</code> 按文件在命令行里的顺序 <code>join</code>，每个 <code>join</code> 得到 <code>Result&lt;CheckReport&gt;</code>，所以 <code>reports</code> 是 <code>Vec&lt;Result&lt;CheckReport&gt;&gt;</code>。输出顺序因此和命令行一致，和哪个线程先完成无关。用上面的 <code>mpsc</code> 做的话，顺序会是完成顺序，每次不同。</li>
<li><code>expect("a check thread panicked")</code>：线程 panic 是 bug，不是用户的输入问题。我们不想把它翻译成漂亮的错误，让它继续 panic。</li>
<li>最后的 <code>for</code> 才打印。第一个遇到的主机错误（比如文件不存在）用 <code>?</code> 直接返回，和之前一样，<code>main</code> 会把它打印成 <code>pebble: ...</code> 并退出码 1。</li>
<li><code>u8::from(failed)</code>：<code>bool</code> 到 <code>u8</code> 的转换，<code>false</code> 是 0，<code>true</code> 是 1，不用写 <code>if failed { 1 } else { 0 }</code>。</li>
</ul><p>模块顶部多了两个 <code>use</code>，<code>use std::path::Path;</code> 和 <code>use std::thread;</code>，<code>cli::{...}</code> 的导入列表里多一个 <code>CheckArgs</code>。<code>main.rs</code> 一个字都不用改：<code>Command::Check(args) =&gt; commands::check(&amp;args)</code> 里的 <code>args</code> 类型是由 <code>Command::Check</code> 的载荷决定的，我们改了载荷，<code>check</code> 的参数类型也跟着改，两边自然对上。</p></section><section><h3>两个新测试<a href="#两个新测试"><span>#</span></a></h3><p>在 <code>crates/pebble-cli/tests/cli.rs</code> 末尾追加两个集成测试（第 7 篇的 <code>pebble()</code> 和 <code>pebble_output()</code> 两个辅助函数已经在了）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>check_reports_only_the_broken_file</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>output</span><span><span> </span><span>=</span><span> </span></span><span>pebble_output</span><span>(&amp;[</span></div></div><div><div><div>4</div></div><div><span>        </span><span>"check"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>"examples/fib.pebble"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>"examples/errors.pebble"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"examples/hello.pebble"</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>]);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>output</span><span>.status.</span><span>code</span><span>(), </span><span>Some</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>10</div></div><div><span>    </span><span>assert!</span><span>(</span><span>output</span><span>.stdout.</span><span>is_empty</span><span>(), </span><span>"check prints nothing to stdout"</span><span>);</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>stderr</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from_utf8</span><span>(</span><span>output</span><span>.stderr).</span><span>expect</span><span>(</span><span>"stderr is UTF-8"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>assert!</span><span>(</span><span>stderr</span><span>.</span><span>contains</span><span>(</span><span>"examples/errors.pebble"</span><span>), </span><span>"{stderr}"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>assert!</span><span><span>(</span><span>!</span></span><span>stderr</span><span>.</span><span>contains</span><span>(</span><span>"examples/fib.pebble"</span><span>), </span><span>"{stderr}"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>#[test]</span></div></div><div><div><div>18</div></div><div><span>fn</span><span> </span><span>check_requires_a_file</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pebble</span><span>().</span><span>arg</span><span>(</span><span>"check"</span><span>).</span><span>assert</span><span>().</span><span>code</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第一个测试检查三件事：退出码是 1（有一个文件坏了），<code>stdout</code> 为空（<code>check</code> 什么也不打印到标准输出），错误只提到坏文件。第二个测试检查 <code>required = true</code>：没有文件，clap 自己报用法错误，退出码 2。</p></section><section><h3>真实运行<a href="#真实运行"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble check examples/fib.pebble examples/errors.pebble examples/hello.pebble; echo "exit=$?"</span></div></div><div><div><div>2</div></div><div><span>error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>exit=1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个文件检查，只有 <code>errors.pebble</code> 有输出，退出码 1。（上面的命令我是用 <code>cargo run -q -p pebble-cli -- check ...</code> 跑的；<code>pebble</code> 是它构建出来的二进制。）</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble check examples/fib.pebble examples/hello.pebble examples/closures.pebble; echo "exit=$?"</span></div></div><div><div><div>2</div></div><div><span>exit=0</span></div></div><div><div><div>3</div></div><div><span>$ pebble check; echo "exit=$?"</span></div></div><div><div><div>4</div></div><div><span>error: the following required arguments were not provided:</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>&lt;FILES&gt;...</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>Usage: pebble check &lt;FILES&gt;...</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>For more information, try '--help'.</span></div></div><div><div><div>10</div></div><div><span>exit=2</span></div></div><div><div><div>11</div></div><div><span>$ pebble check examples/fib.pebble nope.pebble examples/errors.pebble; echo "exit=$?"</span></div></div><div><div><div>12</div></div><div><span>pebble: cannot read nope.pebble: No such file or directory (os error 2)</span></div></div><div><div><div>13</div></div><div><span>exit=1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第三条命令的细节值得停一下：<code>nope.pebble</code> 排在中间，<code>errors.pebble</code> 在它后面也有错，但输出里只有”读不了文件”这一条。原因是 <code>for report in reports</code> 按顺序走，第一个 <code>?</code> 遇到 <code>Err</code> 就返回了，后面的诊断根本没打印。这是一个我选的行为，不是一个必然：也可以收集所有错误一起报，那是另一个设计，这里不做。</p><p>线程到底带来了多少好处？我用 <code>hyperfine</code> 量了一次，被检查的文件是一个脚本生成的 8000 行、630 KB 的 Pebble 文件（每行一个小函数），<code>release</code> 构建：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ hyperfine -N "pebble check big.pebble" "pebble check big.pebble big.pebble big.pebble big.pebble big.pebble big.pebble big.pebble big.pebble"</span></div></div><div><div><div>2</div></div><div><span>Time (mean ± σ):     259.9 ms ±   8.9 ms    [User: 226.5 ms, System: 32.3 ms]</span></div></div><div><div><div>3</div></div><div><span>Time (mean ± σ):     581.2 ms ±   9.0 ms    [User: 3878.7 ms, System: 462.7 ms]</span></div></div><div><div><div>4</div></div><div><span>$ hyperfine "for i in 1 2 3 4 5 6 7 8; do pebble check big.pebble; done"</span></div></div><div><div><div>5</div></div><div><span>Time (mean ± σ):      2.102 s ±  0.015 s    [User: 1.821 s, System: 0.273 s]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个文件 260 ms；八个文件同一次调用并行：581 ms；八次顺序调用：2.102 s。快了 3.6 倍，在一台 16 个逻辑核的机器上，当时机器上还有别的编译任务在跑，所以这个数字不干净。更有意思的是 <code>User</code> 一栏：八份工作总共用了 3.9 s 的 CPU 时间，顺序跑只用 1.8 s。我没有去查原因，我怀疑是八个线程抢同一个内存分配器和缓存，但这是猜测。</p><p>还有一件我顺手量到的事：同样的文件放大到 60000 行（4.8 MB），单个文件的 <code>pebble check</code> 要 34.9 s。行数多了 7.5 倍，时间多了 134 倍，指数大约是 2.4，至少是平方级的增长。我没有去追它在哪里（词法、解析、编译三段里的某一段），这不是这一篇的主题；如果你想练习性能分析，这是一个现成的靶子。</p></section></section>
<section><h2>async：不开线程，也能同时等很多东西<a href="#async不开线程也能同时等很多东西"><span>#</span></a></h2><section><h3>一个线程一个连接，贵在哪<a href="#一个线程一个连接贵在哪"><span>#</span></a></h3><p>线程够用的话，为什么还要 async？先看 <code>pebble-net</code> 要面对的场景：一个服务器，同一时刻有很多客户端连着，其中绝大多数在等：等网络、等对方发下一个字节。如果一个连接占一个线程，大部分线程就是在睡觉。睡觉不是不花钱：每个线程要有自己的栈，内核要为它建调度结构。</p><p>我做了一个实验，不是为了证明”线程不行”（它比我预想的行得多），而是为了知道差距的量级：同时启动 10 万个”睡 500 毫秒然后返回编号”的单位，一边用线程，一边用 Tokio 的任务（<code>tokio::spawn</code>，马上就会讲），<code>release</code> 构建，用 GNU <code>time</code> 量最大内存。这两个程序是 <code>ch14_many_threads.rs</code> 和 <code>ch14_many_tasks.rs</code>，在 <code>~/lab-ch14-tokio</code> 里（完整的 <code>Cargo.toml</code> 在本章后面给出）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ch14_many_threads 100000</span></div></div><div><div><div>2</div></div><div><span>100000 threads, sum 4999950000, took ~3900 ms</span></div></div><div><div><div>3</div></div><div><span>874388 KB max RSS, 3.95 s wall</span></div></div><div><div><div>4</div></div><div><span>$ ch14_many_tasks</span></div></div><div><div><div>5</div></div><div><span>100000 tasks, sum 4999950000, took ~500 ms</span></div></div><div><div><div>6</div></div><div><span>size of one future: 120 bytes</span></div></div><div><div><div>7</div></div><div><span>41128 KB max RSS, 0.54 s wall</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>10 万个线程，3.95 s，近 874 MB；10 万个任务，0.54 s，41 MB。差了一个数量级，而且任务这一边的 0.5 s 里有 0.5 s 是睡眠本身。注意最后一行：一个”睡 500 毫秒再返回”的 <code>async</code> 块，整个状态只有 120 字节。线程的栈在虚拟地址上是 MB 级的。（作为对比：同样的线程程序启动 1 万个线程，在 debug 构建下用 <code>cargo run</code> 跑是 <code>~700 ms</code>，所以线程在几千个这个量级上完全没问题；C10k 问题是 10 万级的问题。）</p><p>C 世界对这个问题的标准回答是 <code>epoll</code> 加手写的状态机：每个连接一个结构体，记录”我读到哪了”，一个循环 <code>epoll_wait</code>，哪个套接字可读就调用对应状态机往前推一步。<code>async</code>/<code>await</code> 的本质是让编译器替你写这些状态机，而你写的是看上去顺序执行的代码。</p></section><section><h3>并发不是并行<a href="#并发不是并行"><span>#</span></a></h3><p>先看一眼它的效果。实验 <code>ch14_join.rs</code>（在 <code>~/lab-ch14-tokio/src/bin/</code>，完整版在 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_join.rs" target="_blank">lab/lab-ch14-tokio/src/bin/ch14_join.rs</a>）模拟两个各要 200 毫秒的”检查”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>time</span><span>::{</span></span><span>Duration</span><span>, </span><span>Instant</span><span>};</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>tokio</span><span>::</span><span>time</span><span>::sleep;</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>check</span><span>(</span><span>name</span><span>: &amp;</span><span>str</span><span>, </span><span>millis</span><span>: </span><span>u64</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>sleep</span><span><span>(</span><span>Duration</span><span>::</span></span><span>from_millis</span><span>(</span><span>millis</span><span>)).</span><span>await</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>format!</span><span>(</span><span>"checked {name}"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#[tokio</span><span>::</span><span>main]</span></div></div><div><div><div>10</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>check</span><span>(</span><span>"fib"</span><span>, </span><span>200</span><span>).</span><span>await</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>check</span><span>(</span><span>"hello"</span><span>, </span><span>200</span><span>).</span><span>await</span><span>;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{a}, {b}: sequential took {} ms"</span><span>, </span><span>start</span><span>.</span><span>elapsed</span><span>().</span><span>as_millis</span><span>() / </span><span>50</span><span> * </span><span>50</span><span>);</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> (</span><span>a</span><span>, </span><span>b</span><span><span>) </span><span>=</span><span> </span><span>tokio</span><span>::</span></span><span>join!</span><span>(</span><span>check</span><span>(</span><span>"fib"</span><span>, </span><span>200</span><span>), </span><span>check</span><span>(</span><span>"hello"</span><span>, </span><span>200</span><span>));</span></div></div><div><div><div>18</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{a}, {b}: join! took {} ms"</span><span>, </span><span>start</span><span>.</span><span>elapsed</span><span>().</span><span>as_millis</span><span>() / </span><span>50</span><span> * </span><span>50</span><span>);</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_join</span></div></div><div><div><div>2</div></div><div><span>checked fib, checked hello: sequential took 400 ms</span></div></div><div><div><div>3</div></div><div><span>checked fib, checked hello: join! took 200 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（毫秒数按 50 取整。）依次 <code>.await</code> 是 400 ms，<code>join!</code> 是 200 ms，而且不需要第二个线程：<code>join!</code> 在同一个任务里轮流 <code>poll</code> 两个 <code>Future</code>。两个 <code>sleep</code> 同时在等，这叫并发（concurrency）：多件事在重叠地”进行中”。并行（parallelism）是多件事在同一时刻真的被不同的核执行。<code>join!</code> 是并发，没有并行；上一节的 <code>thread::scope</code> 是并行。</p><p>要看懂 <code>join!</code> 为什么能做到这一点，得知道 <code>async fn</code> 背后的东西。我们不去读 Tokio 的源码，而是用标准库自己写一遍。</p></section><section><h3>手写一个 <code>Future</code><a href="#手写一个-future"><span>#</span></a></h3><p><code>Future</code> 是标准库里的 trait，简化后是这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>trait</span><span> </span><span>Future</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>poll</span><span>(</span><span>self</span><span>: </span><span>Pin</span><span>&lt;&amp;</span><span>mut</span><span> </span><span>Self</span><span>&gt;, </span><span>cx</span><span>: &amp;</span><span>mut</span><span> </span><span>Context</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Poll</span><span>&lt;</span><span>Self</span><span>::</span><span>Output</span><span>&gt;;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Poll</span><span>&lt;</span><span>T</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Ready</span><span>(</span><span>T</span><span>),</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Pending</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个 <code>Future</code> 就是一个”可以被问 <code>poll</code>”的状态机：被问到的时候，要么说”完成了，结果是 <code>T</code>”（<code>Ready</code>），要么说”还没好”（<code>Pending</code>）。<code>Pending</code> 的同时，它必须保证在将来能继续推进的时候通知执行者，通知的渠道是 <code>Context</code> 里的 <code>Waker</code>。<strong><code>Future</code> 自己什么都不会做</strong>，必须有人反复 <code>poll</code> 它：这是 Rust 的 <code>async</code> 和 JavaScript 的 <code>Promise</code> 最大的区别，<code>Promise</code> 创建就开始跑，<code>Future</code> 不被 <code>poll</code> 就一动不动。</p><p>下面是实验 <code>ch14_manual_future.rs</code>，零依赖，从头写一个能跑起来的最小系统。文件开头是这些导入和一个计数器（<code>POLLS</code> 只用来数一共 <code>poll</code> 了几次，方便下面看输出）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>future</span><span>::</span></span><span>Future</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>pin</span><span>::{</span></span><span>Pin</span><span>, pin};</span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>sync</span><span>::</span><span>atomic</span><span>::{</span></span><span>AtomicUsize</span><span>, </span><span>Ordering</span><span>};</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>sync</span><span>::{</span></span><span>Arc</span><span>, </span><span>Mutex</span><span>};</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>task</span><span>::{</span></span><span>Context</span><span>, </span><span>Poll</span><span>, </span><span>Wake</span><span>, </span><span>Waker</span><span>};</span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>thread</span><span>::{</span></span><span>self</span><span>, </span><span>Thread</span><span>};</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>time</span><span>::{</span></span><span>Duration</span><span>, </span><span>Instant</span><span>};</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>static</span><span> </span><span>POLLS</span><span>: </span><span>AtomicUsize</span><span><span> </span><span>=</span><span> </span></span><span>AtomicUsize</span><span>::</span><span>new</span><span>(</span><span>0</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先来第一个 <code>Future</code>，一个倒数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>CountDown</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>remaining</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>impl</span><span> </span><span>Future</span><span> </span><span>for</span><span> </span><span>CountDown</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> &amp;'</span></span><span>static</span><span> </span><span>str</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>poll</span><span>(</span><span>mut</span><span> </span><span>self</span><span>: </span><span>Pin</span><span>&lt;&amp;</span><span>mut</span><span> </span><span>Self</span><span>&gt;, </span><span>cx</span><span>: &amp;</span><span>mut</span><span> </span><span>Context</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Poll</span><span>&lt;&amp;'</span><span>static</span><span> </span><span>str</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.remaining </span><span>==</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Ready</span><span>(</span><span>"liftoff"</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>println!</span><span>(</span><span>"  CountDown::poll, {} left"</span><span>, </span><span>self</span><span>.remaining);</span></div></div><div><div><div>13</div></div><div><span>            </span><span>self</span><span><span>.remaining </span><span>-=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>14</div></div><div><span>            </span><span>cx</span><span>.</span><span>waker</span><span>().</span><span>wake_by_ref</span><span>();</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Pending</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>每次被 <code>poll</code>，它减一，说”还没好”，同时告诉 <code>Waker</code>”请马上再来问我”（<code>wake_by_ref</code>）。<code>self: Pin&lt;&amp;mut Self&gt;</code> 这个奇怪的接收者类型先放一放，下面讲。</p><p>要运行它，需要一个执行者。最简单的执行者叫 <code>block_on</code>：在当前线程里循环 <code>poll</code>，<code>Pending</code> 就让线程睡觉，等 <code>Waker</code> 把它叫醒。需要一个会”叫醒线程”的 <code>Waker</code>，标准库提供了 <code>Wake</code> trait，实现一个方法就行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>ThreadWaker</span><span>(</span><span>Thread</span><span>);</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>impl</span><span> </span><span>Wake</span><span> </span><span>for</span><span> </span><span>ThreadWaker</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>wake</span><span>(</span><span>self</span><span>: </span><span>Arc</span><span>&lt;</span><span>Self</span><span>&gt;) {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.</span><span>0.</span><span>unpark</span><span>();</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>thread::park()</code> 让当前线程睡到别人 <code>unpark</code> 它为止，<code>unpark</code> 是别的线程（或者自己）发的”醒醒”。<code>Waker::from(Arc&lt;W: Wake&gt;)</code> 把它包装成 <code>Waker</code>。<code>block_on</code> 本身：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>block_on</span><span>&lt;</span><span>F</span><span>: </span><span>Future</span><span>&gt;(</span><span>future</span><span>: </span><span>F</span><span><span>) -&gt; </span><span>F</span><span>::</span></span><span>Output</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>future</span><span><span> </span><span>=</span><span> </span></span><span>pin!</span><span>(</span><span>future</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>waker</span><span><span> </span><span>=</span><span> </span></span><span>Waker</span><span>::</span><span>from</span><span><span>(</span><span>Arc</span><span>::</span></span><span>new</span><span>(</span><span>ThreadWaker</span><span><span>(</span><span>thread</span><span>::</span></span><span>current</span><span>())));</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>cx</span><span><span> </span><span>=</span><span> </span></span><span>Context</span><span>::</span><span>from_waker</span><span>(&amp;</span><span>waker</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>POLLS</span><span>.</span><span>fetch_add</span><span>(</span><span>1</span><span><span>, </span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>match</span><span> </span><span>future</span><span>.</span><span>as_mut</span><span>().</span><span>poll</span><span>(&amp;</span><span>mut</span><span> </span><span>cx</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Ready</span><span>(</span><span>value</span><span>) =&gt; </span><span>return</span><span> </span><span>value</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Pending</span><span><span> =&gt; </span><span>thread</span><span>::</span></span><span>park</span><span>(),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>POLLS</code> 是一个 <code>static AtomicUsize</code>，只用来数一共 <code>poll</code> 了几次，方便下面看输出。）这里有 <code>pin!(future)</code>、<code>as_mut()</code>，也是 <code>Pin</code> 的事情，一会儿解释。先看结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_manual_future</span></div></div><div><div><div>2</div></div><div><span>countdown:</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>CountDown::poll, 3 left</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>CountDown::poll, 2 left</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>CountDown::poll, 1 left</span></div></div><div><div><div>6</div></div><div><span>liftoff after 4 polls</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三次 <code>Pending</code>，第四次 <code>Ready</code>，<code>poll</code> 了 4 次。每一次 <code>Pending</code> 之后，<code>block_on</code> 调用了 <code>thread::park()</code>，但因为 <code>CountDown</code> 已经在 <code>poll</code> 里面调用了 <code>wake_by_ref</code>（先 <code>unpark</code> 了），<code>park</code> 立刻返回，所以这个例子里线程实际上没睡过。要看到真正的睡眠，需要一个真的会等的 <code>Future</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Shared</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>done</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>waker</span><span>: </span><span>Option</span><span>&lt;</span><span>Waker</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>struct</span><span> </span><span>Delay</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>shared</span><span>: </span><span>Arc</span><span>&lt;</span><span>Mutex</span><span>&lt;</span><span>Shared</span><span>&gt;&gt;,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Delay::new(duration)</code> 起一个普通线程，睡 <code>duration</code>，然后设置 <code>done = true</code>，并且如果有人留下了 <code>Waker</code>，就调用它：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Delay</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>new</span><span>(</span><span>duration</span><span>: </span><span>Duration</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>shared</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>new</span><span><span>(</span><span>Mutex</span><span>::</span></span><span>new</span><span>(</span><span>Shared</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>done</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>5</div></div><div><span>            </span><span>waker</span><span>: </span><span>None</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}));</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>thread_shared</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>clone</span><span>(&amp;</span><span>shared</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>thread</span><span>::</span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>thread</span><span>::</span><span>sleep</span><span>(</span><span>duration</span><span>);</span></div></div><div><div><div>10</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>state</span><span><span> </span><span>=</span><span> </span></span><span>thread_shared</span><span>.</span><span>lock</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>11</div></div><div><span>            </span><span>state</span><span><span>.done </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>12</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>waker</span><span><span>) </span><span>=</span><span> </span></span><span>state</span><span>.waker.</span><span>take</span><span>() {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>waker</span><span>.</span><span>wake</span><span>();</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>16</div></div><div><span>        </span><span>Delay</span><span> { </span><span>shared</span><span> }</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Delay</code> 的 <code>poll</code>（它同样 <code>impl Future for Delay</code>，<code>type Output = ();</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>poll</span><span>(</span><span>self</span><span>: </span><span>Pin</span><span>&lt;&amp;</span><span>mut</span><span> </span><span>Self</span><span>&gt;, </span><span>cx</span><span>: &amp;</span><span>mut</span><span> </span><span>Context</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Poll</span><span>&lt;()&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>state</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.shared.</span><span>lock</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span> </span><span>state</span><span>.done {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Poll</span><span>::</span><span>Ready</span><span>(())</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>state</span><span><span>.waker </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>cx</span><span>.</span><span>waker</span><span>().</span><span>clone</span><span>());</span></div></div><div><div><div>7</div></div><div><span>        </span><span>Poll</span><span>::</span><span>Pending</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每次没好的时候，把当前的 <code>Waker</code> 存起来（<code>clone</code> 一份，<code>Waker</code> 是个小的引用计数句柄）。这里线程、<code>Arc</code>、<code>Mutex</code> 都是本章前半部分的东西：<code>Delay</code> 的状态在两个线程之间共享，所以必须是 <code>Arc&lt;Mutex&lt;_&gt;&gt;</code>。（真正的 Tokio 不会为每个定时器起一个线程；它的定时器是挂在运行时的一个全局轮子上。但”没好就留下 Waker，好了就叫它”这个结构是一样的。）</p><p>最后用 <code>async fn</code> 把两个 <code>Delay</code> 串起来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>two_delays</span><span>() -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  two_delays starts"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Delay</span><span>::</span><span>new</span><span><span>(</span><span>Duration</span><span>::</span></span><span>from_millis</span><span>(</span><span>50</span><span>)).</span><span>await</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  first delay done"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Delay</span><span>::</span><span>new</span><span><span>(</span><span>Duration</span><span>::</span></span><span>from_millis</span><span>(</span><span>50</span><span>)).</span><span>await</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  second delay done"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>42</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>main</code> 里先跑上面的 <code>CountDown</code>，再 <code>block_on(two_delays())</code>，打印一共 <code>poll</code> 了几次，以及 <code>size_of_val</code> 一个没被运行过的 <code>two_delays()</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>println!</span><span>(</span><span>"countdown:"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>message</span><span><span> </span><span>=</span><span> </span></span><span>block_on</span><span>(</span><span>CountDown</span><span> { </span><span>remaining</span><span>: </span><span>3</span><span> });</span></div></div><div><div><div>4</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{message} after {} polls"</span><span>, </span><span>POLLS</span><span>.</span><span>swap</span><span>(</span><span>0</span><span><span>, </span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span>));</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"two delays:"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>answer</span><span><span> </span><span>=</span><span> </span></span><span>block_on</span><span>(</span><span>two_delays</span><span>());</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>elapsed</span><span><span> </span><span>=</span><span> </span></span><span>start</span><span>.</span><span>elapsed</span><span>();</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"answer {answer} after {} polls"</span><span>, </span><span>POLLS</span><span>.</span><span>swap</span><span>(</span><span>0</span><span><span>, </span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span>));</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"took at least 100 ms: {}"</span><span>, </span><span>elapsed</span><span><span> </span><span>&gt;=</span><span> </span></span><span>Duration</span><span>::</span><span>from_millis</span><span>(</span><span>100</span><span>));</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size of the async fn future: {} bytes"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of_val</span><span>(&amp;</span><span>two_delays</span><span>()));</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二段的输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>two delays:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>two_delays starts</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>first delay done</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>second delay done</span></div></div><div><div><div>5</div></div><div><span>answer 42 after 3 polls</span></div></div><div><div><div>6</div></div><div><span>took at least 100 ms: true</span></div></div><div><div><div>7</div></div><div><span>size of the async fn future: 16 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三次 <code>poll</code>：第一次开始运行，打印 <code>two_delays starts</code>，创建第一个 <code>Delay</code>，<code>poll</code> 它，<code>Pending</code>，整个 <code>two_delays</code> 也 <code>Pending</code>，<code>block_on</code> 睡眠；50 毫秒后 <code>Delay</code> 的线程叫醒它，第二次 <code>poll</code> 从第一个 <code>.await</code> 之后继续，打印 <code>first delay done</code>，创建第二个 <code>Delay</code>，又 <code>Pending</code>；第三次 <code>poll</code> 跑完，返回 42。总共至少 100 毫秒，<code>true</code>。</p><p>最后一行也有意思。<code>async fn</code> 的返回值是一个编译器生成的匿名类型，这里只有 16 字节，里面存的是”我停在第几个 <code>.await</code>”和跨 <code>.await</code> 活着的局部变量。这就是前面说的”编译器写的状态机”：没有被 <code>poll</code> 过的时候，它什么都没做（那个只用来量大小的 <code>two_delays()</code> 从未被 <code>poll</code>，所以输出里没有第二行 <code>two_delays starts</code>：调用 <code>two_delays()</code> 只是构造了状态机，没有运行）。</p></section><section><h3><code>Pin</code>：为什么 <code>poll</code> 的 self 那么奇怪<a href="#pin为什么-poll-的-self-那么奇怪"><span>#</span></a></h3><p><code>async fn</code> 里的局部变量可以互相引用。看实验 <code>ch14_pin_new.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>pin</span><span>::</span></span><span>Pin</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>answer</span><span>() -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>local</span><span><span> </span><span>=</span><span> [</span></span><span>1</span><span>u32</span><span>, </span><span>2</span><span>, </span><span>3</span><span>];</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>reference</span><span><span> </span><span>=</span><span> &amp;</span></span><span>local</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>std</span><span>::</span><span>future</span><span>::</span><span>ready</span><span>(()).</span><span>await</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>*</span><span>reference</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>future</span><span><span> </span><span>=</span><span> </span></span><span>answer</span><span>();</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>_pinned</span><span><span> </span><span>=</span><span> </span></span><span>Pin</span><span>::</span><span>new</span><span>(&amp;</span><span>mut</span><span> </span><span>future</span><span>);</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>reference</code> 指向 <code>local</code>，两个都跨过了那个 <code>.await</code>，所以它们都要存在状态机里面：状态机里有一个字段指向它自己的另一个字段，一个自引用结构。如果这个状态机在两次 <code>poll</code> 之间被移动（<code>memcpy</code> 到别处），<code>reference</code> 还指着旧地址，就悬空了。C++ 的类似物是：一个对象里存着指向自己成员的指针，然后你又拷贝或者移动了这个对象。C++ 的对策是写拷贝/移动构造函数，或者 <code>= delete</code> 它们。Rust 的对策是 <code>Pin</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_pin_new</span></div></div><div><div><div>2</div></div><div><span>error[E0277]: `{async fn body of answer()}` cannot be unpinned</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_pin_new.rs:12:28</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>3 | async fn answer() -&gt; u32 {</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|                      --- within this `impl Future&lt;Output = u32&gt;`</span></div></div><div><div><div>7</div></div><div><span>...</span></div></div><div><div><div>8</div></div><div><span>12 |     let _pinned = Pin::new(&amp;mut future);</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|                   -------- ^^^^^^^^^^^ within `impl Future&lt;Output = u32&gt;`, the trait `Unpin` is not implemented for `{async fn body of answer()}`</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|                   |</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|                   required by a bound introduced by this call</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>= note: consider using the `pin!` macro</span></div></div><div><div><div>14</div></div><div><span><span>           </span></span><span>consider using `Box::pin` if you need to access the pinned value outside of the current scope</span></div></div><div><div><div>15</div></div><div><span>note: required because it appears within the type `impl Future&lt;Output = u32&gt;`</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch14_pin_new.rs:3:22</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>3 | async fn answer() -&gt; u32 {</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>|                      ^^^</span></div></div><div><div><div>20</div></div><div><span>note: required by a bound in `Pin::&lt;Ptr&gt;::new`</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/pin.rs:1181:4</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>24</div></div><div><span>error: could not compile `rust-lab` (bin "ch14_pin_new") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Pin&lt;P&gt;</code> 是一个包装，包着一个指针（<code>&amp;mut T</code>、<code>Box&lt;T&gt;</code>）并承诺”指针指向的值不会再被移动”。第 13 篇里 <code>PhantomPinned</code> 是一个”我不是 <code>Unpin</code>”的标记，<code>Unpin</code> 是一个自动 trait，意思是”移动这个类型是安全的”，绝大多数类型都是 <code>Unpin</code>（<code>u32</code>、<code>String</code>、<code>Vec</code>……），所以对它们 <code>Pin</code> 是一个空操作。<code>async fn</code> 生成的状态机因为可能有自引用，所以编译器让它不是 <code>Unpin</code>。<code>Pin::new</code> 只接受 <code>Unpin</code> 的东西，所以上面这个报错。</p><p>编译器的提示给了两条正路：<code>pin!</code> 宏，把值钉在当前栈帧里（<code>block_on</code> 里用的就是这个）；<code>Box::pin</code>，钉在堆上。两者都保证你再也拿不到那个值的 <code>&amp;mut</code>，只能拿到 <code>Pin&lt;&amp;mut T&gt;</code>，于是就没法 <code>mem::swap</code> 或者移动它。<code>poll(self: Pin&lt;&amp;mut Self&gt;, ...)</code> 正是这一份保证的收据：要调用 <code>poll</code>，你必须先钉住它。</p><p><code>Pin</code> 是 Rust 里少数几个我觉得真的难的地方，我没有假装它简单。这一篇你需要的只有两条：自己写 <code>Future</code> 的时候，<code>self: Pin&lt;&amp;mut Self&gt;</code> 照抄；调用 <code>poll</code> 的时候，用 <code>pin!</code> 或 <code>Box::pin</code> 先钉住。上面的 <code>CountDown</code>、<code>Delay</code> 都没有自引用字段，所以其实是 <code>Unpin</code> 的，写 <code>self.remaining -= 1</code> 不需要任何 <code>unsafe</code>。</p></section><section><h3>Tokio：别人写好的 <code>block_on</code>，加上整个世界<a href="#tokio别人写好的-block_on加上整个世界"><span>#</span></a></h3><p><code>block_on</code> 是一个单线程、一次只能跑一个 <code>Future</code> 的执行者。Tokio 提供的是一整套运行时（runtime）：一个线程池，每个线程跑一个调度循环，从队列里取任务来 <code>poll</code>；一个反应器（reactor），把”套接字可读了”这样的操作系统事件（据我所知在 Linux 上它通过 <code>epoll</code>）翻译成对应任务的 <code>Waker</code> 调用；定时器；以及一堆异步版本的 I/O、锁和通道。</p><p>你与它打交道的方式：</p><ul>
<li><code>#[tokio::main]</code> 把 <code>async fn main</code> 变成普通的 <code>fn main</code>，里面创建运行时，<code>block_on</code> 你的 <code>async</code> 主体。<code>#[tokio::test]</code> 对测试做同样的事。这两个是属性宏，第 12 篇的 <code>#[differential]</code> 的同类。</li>
<li><code>tokio::spawn(future)</code> 把一个 <code>Future</code> 交给运行时，返回 <code>JoinHandle</code>，和 <code>thread::spawn</code> 的形状几乎一样：你 <code>.await</code> 它得到结果。任务是运行时调度的最小单位，比线程便宜得多（上面那 120 字节）。</li>
<li><code>.await</code> 一个 <code>Future</code> 的意思是”在这里让出，等它好了再回来”。让出的时候，运行时的线程去 <code>poll</code> 别的任务。</li>
</ul><p>任务之间在哪些点上能切换？<strong>只在 <code>.await</code> 上</strong>。这一点非常重要，我用两个实验让它看得见。<code>ch14_block_the_runtime.rs</code> 把运行时设成单线程（<code>flavor = "current_thread"</code>），一个任务每 100 毫秒打印一个 tick，另一个任务调用 <code>std::thread::sleep</code> 睡 500 毫秒：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[tokio</span><span>::</span><span>main(flavor </span><span>=</span><span> </span><span>"current_thread"</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>ticker</span><span><span> </span><span>=</span><span> </span><span>tokio</span><span>::</span></span><span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>tokio</span><span>::</span><span>time</span><span>::</span><span>sleep</span><span><span>(</span><span>Duration</span><span>::</span></span><span>from_millis</span><span>(</span><span>100</span><span>)).</span><span>await</span><span>;</span></div></div><div><div><div>8</div></div><div><span>            </span><span>println!</span><span>(</span><span>"tick at ~{} ms"</span><span>, </span><span>start</span><span>.</span><span>elapsed</span><span>().</span><span>as_millis</span><span>() / </span><span>100</span><span> * </span><span>100</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>blocker</span><span><span> </span><span>=</span><span> </span><span>tokio</span><span>::</span></span><span>spawn</span><span>(</span><span>async</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>println!</span><span>(</span><span>"blocking the only worker thread for 500 ms"</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>std</span><span>::</span><span>thread</span><span>::</span><span>sleep</span><span><span>(</span><span>Duration</span><span>::</span></span><span>from_millis</span><span>(</span><span>500</span><span>));</span></div></div><div><div><div>15</div></div><div><span>        </span><span>println!</span><span>(</span><span>"blocker done"</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span><span>tokio</span><span>::</span></span><span>join!</span><span>(</span><span>ticker</span><span>, </span><span>blocker</span><span>);</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_block_the_runtime</span></div></div><div><div><div>2</div></div><div><span>blocking the only worker thread for 500 ms</span></div></div><div><div><div>3</div></div><div><span>blocker done</span></div></div><div><div><div>4</div></div><div><span>tick at ~500 ms</span></div></div><div><div><div>5</div></div><div><span>tick at ~600 ms</span></div></div><div><div><div>6</div></div><div><span>tick at ~700 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个 tick 本来应该在大约 100、200、300 毫秒。它们在 500、600、700：运行时只有一个线程，那个线程被 <code>std::thread::sleep</code> 占着，<code>sleep</code> 里没有 <code>.await</code>，没有任何让出的机会，计时器没法被检查。这就是”阻塞运行时”，async 代码里最常见的性能 bug。C++ 里有同样的坑：在 <code>asio</code> 的事件循环里调用阻塞的 <code>read()</code>。</p><p>修复：把阻塞的活交给专门的线程池，<code>tokio::task::spawn_blocking</code>。<code>ch14_blocking_ok.rs</code> 只改了一处，<code>blocker</code> 从 <code>tokio::spawn(async { ... })</code> 改成 <code>tokio::task::spawn_blocking(|| { ... })</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_blocking_ok</span></div></div><div><div><div>2</div></div><div><span>blocking on a dedicated thread for 500 ms</span></div></div><div><div><div>3</div></div><div><span>tick at ~100 ms</span></div></div><div><div><div>4</div></div><div><span>tick at ~200 ms</span></div></div><div><div><div>5</div></div><div><span>tick at ~300 ms</span></div></div><div><div><div>6</div></div><div><span>blocker done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>tick 准时了。<code>spawn_blocking</code> 的闭包在一个专门的、允许阻塞的线程上运行，运行时的主线程继续调度别的任务。这是本篇后面 <code>pebble-net</code> 里的核心技巧：Pebble 的求值是 CPU 密集的同步代码，必须用 <code>spawn_blocking</code>。</p></section><section><h3>哪些东西不能跨 <code>.await</code><a href="#哪些东西不能跨-await"><span>#</span></a></h3><p><code>tokio::spawn</code> 要求 <code>F: Future + Send + 'static</code>。<code>Send</code> 是什么意思？运行时的线程池里，一个任务这次在线程 A 上 <code>poll</code>，下次可能被线程 B 拿去接着 <code>poll</code>。所以任务（这个状态机）必须能在线程之间移动，<code>Send</code> 的要求就是这个。而状态机里存的是所有”跨过 <code>.await</code> 还活着”的局部变量。如果一个局部变量不是 <code>Send</code>，并且它跨过了一个 <code>.await</code>，整个 <code>Future</code> 就不是 <code>Send</code>。实验 <code>ch14_rc_across_await.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>handle</span><span><span> </span><span>=</span><span> </span><span>tokio</span><span>::</span></span><span>spawn</span><span>(</span><span>async</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>shared</span><span><span> </span><span>=</span><span> </span><span>Rc</span><span>::</span></span><span>new</span><span>(</span><span>5</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>tokio</span><span>::</span><span>task</span><span>::</span><span>yield_now</span><span>().</span><span>await</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{shared}"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: future cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; src/bin/ch14_rc_across_await.rs:5:18</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>5 |       let handle = tokio::spawn(async {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|  __________________^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>6 | |         let shared = Rc::new(5);</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>7 | |         tokio::task::yield_now().await;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>8 | |         println!("{shared}");</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>9 | |     });</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>| |______^ future created by async block is not `Send`</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>= help: within `{async block@src/bin/ch14_rc_across_await.rs:5:31: 5:36}`, the trait `Send` is not implemented for `Rc&lt;i32&gt;`</span></div></div><div><div><div>13</div></div><div><span>note: future is not `Send` as this value is used across an await</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>--&gt; src/bin/ch14_rc_across_await.rs:7:34</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>6 |         let shared = Rc::new(5);</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>|             ------ has type `Rc&lt;i32&gt;` which is not `Send`</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>7 |         tokio::task::yield_now().await;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|                                  ^^^^^ await occurs here, with `shared` maybe used later</span></div></div><div><div><div>20</div></div><div><span>note: required by a bound in `tokio::spawn`</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>--&gt; /home/bfmhno3/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.2/src/task/spawn.rs:176:21</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>23</div></div><div><span>174 |     pub fn spawn&lt;F&gt;(future: F) -&gt; JoinHandle&lt;F::Output&gt;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>|            ----- required by a bound in this function</span></div></div><div><div><div>25</div></div><div><span>175 |     where</span></div></div><div><div><div>26</div></div><div><span>176 |         F: Future + Send + 'static,</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>|                     ^^^^ required by this bound in `spawn`</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>error: could not compile `lab-ch14-tokio` (bin "ch14_rc_across_await") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>同样的事情发生在 Pebble 自己的解释器上。实验 <code>ch14_interp_across_await.rs</code> 在 <code>~/lab-ch14-tokio</code> 里，用路径依赖直接引用你重建出来的 <code>pebble-interp</code>（它的 <code>Cargo.toml</code> 在本篇最后的”实验用的 crate”一节）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>handle</span><span><span> </span><span>=</span><span> </span><span>tokio</span><span>::</span></span><span>spawn</span><span>(</span><span>async</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span><span>Interpreter</span><span>::</span></span><span>new</span><span>();</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>tokio</span><span>::</span><span>task</span><span>::</span><span>yield_now</span><span>().</span><span>await</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>drop</span><span>(</span><span>interpreter</span><span>);</span></div></div><div><div><div>5</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: future cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; src/bin/ch14_interp_across_await.rs:5:18</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>5 |       let handle = tokio::spawn(async {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|  __________________^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>6 | |         let interpreter = Interpreter::new();</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>7 | |         tokio::task::yield_now().await;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>8 | |         drop(interpreter);</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>9 | |     });</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>| |______^ future created by async block is not `Send`</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>= help: within `{async block@src/bin/ch14_interp_across_await.rs:5:31: 5:36}`, the trait `Send` is not implemented for `Rc&lt;RefCell&lt;pebble_value::Env&gt;&gt;`</span></div></div><div><div><div>13</div></div><div><span>note: future is not `Send` as this value is used across an await</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>--&gt; src/bin/ch14_interp_across_await.rs:7:34</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>6 |         let interpreter = Interpreter::new();</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>|             ----------- has type `Interpreter` which is not `Send`</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>7 |         tokio::task::yield_now().await;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|                                  ^^^^^ await occurs here, with `interpreter` maybe used later</span></div></div><div><div><div>20</div></div><div><span>note: required by a bound in `tokio::spawn`</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>--&gt; /home/bfmhno3/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.2/src/task/spawn.rs:176:21</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>23</div></div><div><span>174 |     pub fn spawn&lt;F&gt;(future: F) -&gt; JoinHandle&lt;F::Output&gt;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>|            ----- required by a bound in this function</span></div></div><div><div><div>25</div></div><div><span>175 |     where</span></div></div><div><div><div>26</div></div><div><span>176 |         F: Future + Send + 'static,</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>|                     ^^^^ required by this bound in `spawn`</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器还给了第二个几乎一样的错误，把原因换成了第 8 篇的 <code>Box&lt;dyn Output&gt;</code>（里面的 trait 对象没写 <code>+ Send</code>），这里不重复。这个实验是后面 <code>pebble-net</code> 的设计约束：<strong>解释器不能活过一个 <code>.await</code></strong>。</p><p>最后一个常见的陷阱：标准库的 <code>Mutex</code>。实验 <code>ch14_std_mutex_await.rs</code>，在持有 <code>std::sync::Mutex</code> 的 guard 的时候 <code>.await</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>tokio</span><span>::</span></span><span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>guard</span><span><span> </span><span>=</span><span> </span></span><span>counter</span><span>.</span><span>lock</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>*</span><span>guard</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>tokio</span><span>::</span><span>time</span><span>::</span><span>sleep</span><span><span>(</span><span>Duration</span><span>::</span></span><span>from_millis</span><span>(</span><span>10</span><span>)).</span><span>await</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>*</span><span>guard</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>6</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: future cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; src/bin/ch14_std_mutex_await.rs:9:9</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>9 | /         tokio::spawn(async move {</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>10 | |             let mut guard = counter.lock().unwrap();</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>11 | |             *guard += 1;</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>12 | |             tokio::time::sleep(Duration::from_millis(10)).await;</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>13 | |             *guard += 1;</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>14 | |         })</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>| |__________^ future created by async block is not `Send`</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>= help: within `{async block@src/bin/ch14_std_mutex_await.rs:9:22: 9:32}`, the trait `Send` is not implemented for `std::sync::MutexGuard&lt;'_, u32&gt;`</span></div></div><div><div><div>13</div></div><div><span>note: future is not `Send` as this value is used across an await</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>--&gt; src/bin/ch14_std_mutex_await.rs:12:59</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>10 |             let mut guard = counter.lock().unwrap();</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>|                 --------- has type `std::sync::MutexGuard&lt;'_, u32&gt;` which is not `Send`</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>11 |             *guard += 1;</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>12 |             tokio::time::sleep(Duration::from_millis(10)).await;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>|                                                           ^^^^^ await occurs here, with `mut guard` maybe used later</span></div></div><div><div><div>21</div></div><div><span>note: required by a bound in `tokio::spawn`</span></div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>--&gt; /home/bfmhno3/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.2/src/task/spawn.rs:176:21</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>24</div></div><div><span>174 |     pub fn spawn&lt;F&gt;(future: F) -&gt; JoinHandle&lt;F::Output&gt;</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>|            ----- required by a bound in this function</span></div></div><div><div><div>26</div></div><div><span>175 |     where</span></div></div><div><div><div>27</div></div><div><span>176 |         F: Future + Send + 'static,</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>|                     ^^^^ required by this bound in `spawn`</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这就是上面那张表里 <code>MutexGuard</code> 不是 <code>Send</code> 的后果。想象一下它为什么必须这样：任务在锁的持有期间让出了线程，然后别的任务在<strong>同一个线程</strong>上跑起来，也想拿同一把锁。这个线程已经”持有”了它（持有者是被挂起的那个任务），第二个任务拿锁就是死锁。更糟的是这个任务下次可能在另一个线程上被唤醒，<code>MutexGuard</code> 要在那里解锁。编译器两种情况一起拒绝了。</p><p>解决办法有两个，选哪个取决于你要不要在持锁时 <code>.await</code>：不需要的话，把 guard 的作用域缩小到 <code>.await</code> 之前（用 <code>{ ... }</code> 包起来，或者 <code>drop(guard)</code>），继续用 <code>std::sync::Mutex</code>，它更快；需要的话，用 <code>tokio::sync::Mutex</code>，它的 <code>lock()</code> 本身是 <code>async fn</code>，guard 是 <code>Send</code> 的。<code>pebble-net</code> 两种都用到了，后面会看到分界线在哪里。</p></section></section>
<section><h2><code>pebble-net</code>：一个会同时服务很多客户端的求值器<a href="#pebble-net一个会同时服务很多客户端的求值器"><span>#</span></a></h2><section><h3>协议<a href="#协议"><span>#</span></a></h3><p>Pebble 的程序是文本，结果也是文本。TCP 是字节流，没有消息边界，客户端写两次可能被服务器一次读到，也可能一次写被拆成三次读到。所以我们需要自己划边界。最简单的办法是<strong>长度前缀</strong>：每一帧是一个 4 字节大端的 <code>u32</code> 长度，后面是恰好那么多字节的 UTF-8 文本。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>client                                            server</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>|  &lt;len:u32&gt;&lt;program bytes&gt;  ------------------&gt;  read_frame</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|                                                 evaluate (spawn_blocking)</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|  &lt;------- &lt;len:u32&gt;&lt;rendered text&gt;             write_frame</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我先用一个原始的 TCP 客户端把真实的线上字节打出来看一看（实验 <code>ch14_wire.rs</code>，它启动我们稍后要写的服务器，发送 <code>1 + 2 * 3;</code>，并把请求和响应用十六进制打印）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_wire</span></div></div><div><div><div>2</div></div><div><span>request  : 00 00 00 0a 31 20 2b 20 32 20 2a 20 33 3b</span></div></div><div><div><div>3</div></div><div><span>response : 00 00 00 01 37</span></div></div><div><div><div>4</div></div><div><span>as text  : "7"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>请求是 4 字节长度（<code>0x0a</code> = 10，<code>1 + 2 * 3;</code> 正好 10 个字符）加 10 字节正文，响应是长度 1 加 <code>0x37</code>（ASCII <code>7</code>）。大端是网络字节序，C 里你会写 <code>htonl</code>；Rust 里是 <code>u32::to_be_bytes</code> 和 <code>u32::from_be_bytes</code>，不需要 <code>unsafe</code>，也不需要判断本机字节序。</p></section><section><h3>写 <code>Cargo.toml</code><a href="#写-cargotoml"><span>#</span></a></h3><p><code>crates/pebble-net/Cargo.toml</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-net"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"An asynchronous remote evaluator for Pebble, built on Tokio."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div><span>pebble-ast.workspace</span><span> = </span><span>true</span></div></div><div><div><div>15</div></div><div><span>pebble-parser.workspace</span><span> = </span><span>true</span></div></div><div><div><div>16</div></div><div><span>pebble-value.workspace</span><span> = </span><span>true</span></div></div><div><div><div>17</div></div><div><span>pebble-interp.workspace</span><span> = </span><span>true</span></div></div><div><div><div>18</div></div><div><span>tokio.workspace</span><span> = </span><span>true</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>21</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>文件头：复制<a href="#文件头复制"><span>#</span></a></h3><p><code>crates/pebble-net/src/lib.rs</code> 的前 73 行是 <code>#![forbid(unsafe_code)]</code>、整个 crate 的文档（上面那张协议图就在里面）、<code>use</code> 列表、两个常量、<code>ServerConfig</code>（带 <code>Default</code>）和 <code>Stats</code>。这些都是你已经会的东西，从 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-net/src/lib.rs" target="_blank">tag 里的 <code>lib.rs</code></a> 把前 73 行复制过来，没有什么要读的。只有两处值得看一眼：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>ServerConfig</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>max_frame</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>request_timeout</span><span>: </span><span>Duration</span><span>,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#[derive(</span><span>Default</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>8</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Stats</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>connections</span><span>: </span><span>u64</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>requests</span><span>: </span><span>u64</span><span>,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>errors</span><span>: </span><span>u64</span><span>,</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（省略了文档注释。）<code>ServerConfig</code> 派生 <code>Clone</code>，因为每个连接任务要拥有一份自己的拷贝：<code>tokio::spawn</code> 要求 <code>'static</code>，借用不行。<code>Stats</code> 是三个计数器，被所有连接共享，所以它将被放进 <code>Arc&lt;Mutex&lt;_&gt;&gt;</code>。</p><p>接着是”纯求值”那一段，从 <code>// Pure evaluation</code> 那条横幅注释开始，到 <code>// Framing</code> 之前结束：<code>Evaluation</code>、<code>SharedOutput</code>（用 <code>Arc&lt;std::sync::Mutex&lt;Vec&lt;String&gt;&gt;&gt;</code> 收集 <code>print</code> 的输出，就是上面讲中毒时提到的那个）、<code>evaluate</code>、<code>eval_to_string</code>、<code>render_diagnostics</code>、<code>render_runtime_error</code>。同样复制过来，它们都是同步函数，不碰网络。<code>evaluate</code> 做的事情和 <code>pebble run</code> 一样：解析，有诊断就渲染诊断，否则解释，把 <code>print</code> 的输出和最终值拼在一起。关键的一行是</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>output</span><span>.</span><span>clone</span><span>()));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出被写进一个克隆出来的 <code>SharedOutput</code>，解释器被丢弃之后，<code>output.text()</code> 还能读到内容。解释器从创建到丢弃都在这个同步函数里，<strong>从不跨 <code>.await</code></strong>，这和上面实验的结论吻合。</p><p>这一段之后你的文件和 tag 的区别是：没有 <code>Framing</code>、<code>Server</code>、<code>Client</code> 三个部分，也没有 <code>tests</code> 模块。把它们一块一块写出来。</p></section><section><h3>成帧：先写一个朴素版本<a href="#成帧先写一个朴素版本"><span>#</span></a></h3><p><code>read_frame</code> 要读一个 4 字节的头，判断长度合不合理，读那么多字节的正文。第一版我用的是 <code>io::Result</code>，很自然：所有 I/O 函数都返回它，长度太大就造一个 <code>io::Error</code>。实验 <code>ch14_naive_frame.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>read_frame</span><span>&lt;</span><span>R</span><span>&gt;(</span><span>reader</span><span>: &amp;</span><span>mut</span><span> </span><span>R</span><span>, </span><span>max_frame</span><span>: </span><span>usize</span><span><span>) -&gt; </span><span>io</span><span>::</span></span><span>Result</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt;&gt;&gt;</span></div></div><div><div><div>2</div></div><div><span>where</span></div></div><div><div><div>3</div></div><div><span>    </span><span>R</span><span>: </span><span>AsyncRead</span><span> + </span><span>Unpin</span><span>,</span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>header</span><span><span> </span><span>=</span><span> [</span></span><span>0</span><span>u8</span><span>; </span><span>4</span><span>];</span></div></div><div><div><div>6</div></div><div><span>    </span><span>match</span><span> </span><span>reader</span><span>.</span><span>read_exact</span><span>(&amp;</span><span>mut</span><span> </span><span>header</span><span>).</span><span>await</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>Ok</span><span>(</span><span>_</span><span>) =&gt; {}</span></div></div><div><div><div>8</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) </span><span>if</span><span> </span><span>error</span><span>.</span><span>kind</span><span><span>() </span><span>==</span><span> </span><span>io</span><span>::</span></span><span>ErrorKind</span><span>::</span><span>UnexpectedEof</span><span> =&gt; </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>None</span><span>),</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; </span><span>return</span><span> </span><span>Err</span><span>(</span><span>error</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>length</span><span><span> </span><span>=</span><span> </span></span><span>u32</span><span>::</span><span>from_be_bytes</span><span>(</span><span>header</span><span>) </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>if</span><span> </span><span>length</span><span><span> </span><span>&gt;</span><span> </span></span><span>max_frame</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span><span>(</span><span>io</span><span>::</span></span><span>Error</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>io</span><span>::</span><span>ErrorKind</span><span>::</span><span>InvalidData</span><span>,</span></div></div><div><div><div>16</div></div><div><span>            </span><span>format!</span><span>(</span><span>"frame length {length} exceeds maximum frame size {max_frame}"</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>));</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>payload</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>0</span><span>u8</span><span>; </span><span>length</span><span>];</span></div></div><div><div><div>21</div></div><div><span>    </span><span>reader</span><span>.</span><span>read_exact</span><span>(&amp;</span><span>mut</span><span> </span><span>payload</span><span>).</span><span>await</span><span>?;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>Ok</span><span>(</span><span>Some</span><span>(</span><span>payload</span><span>))</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>主程序对一个声称有 1024 字节但上限是 16 的帧、和一个声称有 8 字节但实际只有 2 字节的帧各调一次，打印错误的 <code>kind()</code> 和消息：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_naive_frame</span></div></div><div><div><div>2</div></div><div><span>kind    : InvalidData</span></div></div><div><div><div>3</div></div><div><span>message : frame length 1024 exceeds maximum frame size 16</span></div></div><div><div><div>4</div></div><div><span>kind    : UnexpectedEof</span></div></div><div><div><div>5</div></div><div><span>message : early eof</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两种失败都能被区分，靠的是 <code>ErrorKind</code>。但”太大”这个失败带着两个有用的数字（声称的长度和上限），在 <code>io::Error</code> 里它们变成了字符串的一部分；调用者想知道上限是多少，只能解析字符串。另外，想想 <code>handle_connection</code> 要怎么处理这几种情况：连接被对端关了（正常结束，不是错误）、帧太大（要断开连接，记一次错误）、UTF-8 解码失败（记一次错误）、网络真的坏了。如果它们全是 <code>io::Error</code>，每个调用者都要靠 <code>kind()</code> 和字符串匹配来猜。</p><p>这是”错误应该是数据”的又一个场景（第 3 篇你写 <code>Diagnostic</code> 的时候同一个想法）：给它一个类型。</p></section><section><h3><code>FrameError</code>：一个 <code>enum</code>、<code>Display</code>、<code>Error</code>、<code>From</code><a href="#frameerror一个-enumdisplayerrorfrom"><span>#</span></a></h3><p>在文件里 <code>// Framing</code> 那条横幅之下写：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Why a frame could not be read or written.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>#[non_exhaustive]</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>FrameError</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// The underlying connection failed.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Io</span><span><span>(</span><span>io</span><span>::</span></span><span>Error</span><span>),</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// A frame announced or carried more bytes than the configured maximum.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>TooLarge</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>/// The offending length in bytes.</span></div></div><div><div><div>10</div></div><div><span>        </span><span>length</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>/// The largest length that is allowed.</span></div></div><div><div><div>12</div></div><div><span>        </span><span>max</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>14</div></div><div><span>    </span><span>/// A frame payload that should be text was not valid UTF-8.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>InvalidUtf8</span><span><span>(</span><span>std</span><span>::</span><span>string</span><span>::</span></span><span>FromUtf8Error</span><span>),</span></div></div><div><div><div>16</div></div><div><span>    </span><span>/// The peer closed the connection before sending a response.</span></div></div><div><div><div>17</div></div><div><span>    </span><span>Closed</span><span>,</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>#[derive(Debug)]</code> 是必需的（<code>Error</code> 要求 <code>Debug</code>）。四个变体对应上面四种情况：<code>Io</code> 包着真正的网络错误，<code>TooLarge</code> 是带两个字段的结构体式变体（数字现在是数字了），<code>InvalidUtf8</code> 包着 <code>String::from_utf8</code> 的错误，<code>Closed</code> 没有载荷。<code>pub</code> 的 <code>enum</code> 上有一个陌生的属性 <code>#[non_exhaustive]</code>，稍后解释。</p><p><code>Display</code>（第 3 篇的老朋友）给每个变体一句人话：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>std</span><span>::</span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>FrameError</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>std</span><span>::</span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>std</span><span>::</span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Self</span><span>::</span><span>Io</span><span>(</span><span>_</span><span>) =&gt; </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"i/o error on the connection"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Self</span><span>::</span><span>TooLarge</span><span> { </span><span>length</span><span>, </span><span>max</span><span> } =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"frame length {length} exceeds maximum frame size {max}"</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Self</span><span>::</span><span>InvalidUtf8</span><span>(</span><span>_</span><span>) =&gt; </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"frame payload is not valid UTF-8"</span><span>),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Self</span><span>::</span><span>Closed</span><span> =&gt; </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"peer closed the connection without a response"</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意 <code>Io(_)</code> 和 <code>InvalidUtf8(_)</code> 的消息里没有写内层错误的内容。这是有意的，下面讲 <code>source()</code> 的时候你会看到为什么。</p><p><code>std::error::Error</code> trait 的核心是 <code>source()</code>：返回这个错误的”原因”。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>std</span><span>::</span><span>error</span><span>::</span></span><span>Error</span><span> </span><span>for</span><span> </span><span>FrameError</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>source</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;&amp;(</span><span>dyn</span><span> </span><span>std</span><span>::</span><span>error</span><span>::</span><span>Error</span><span> + '</span><span>static</span><span>)&gt; {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Self</span><span>::</span><span>Io</span><span>(</span><span>error</span><span>) =&gt; </span><span>Some</span><span>(</span><span>error</span><span>),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Self</span><span>::</span><span>InvalidUtf8</span><span>(</span><span>error</span><span>) =&gt; </span><span>Some</span><span>(</span><span>error</span><span>),</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Self</span><span>::</span><span>TooLarge</span><span><span> { .. } </span><span>|</span><span> </span></span><span>Self</span><span>::</span><span>Closed</span><span> =&gt; </span><span>None</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Option&lt;&amp;(dyn std::error::Error + 'static)&gt;</code>：一个 trait 对象的借用（第 8 篇），说”我可能有一个底层原因，它是某种 <code>Error</code>”。<code>Io</code> 和 <code>InvalidUtf8</code> 返回内层，另外两个返回 <code>None</code>。<code>Error</code> 要求 <code>Debug + Display</code>（第 3 篇的超 trait），其余的方法都有默认实现。</p><p>为什么不把内层错误的消息拼在自己的消息里？因为这样的话，一个错误链（<code>FrameError</code> 引起自 <code>io::Error</code> 引起自操作系统的 errno）会在每一层重复同一句话。惯例是：每个错误只说自己这一层，用 <code>source()</code> 暴露下一层，由最后打印错误的人决定要不要把整条链走一遍。实验 <code>ch14_error_chain.rs</code> 用 <code>pebble-net</code> 的 <code>FrameError</code>（路径依赖在 lab 的 <code>Cargo.toml</code> 里）做了一次，包括 <code>anyhow</code>（第 7 篇的 CLI 就用它）怎么展示一条链：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>utf8</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from_utf8</span><span>(</span><span>vec!</span><span>[</span><span>0x68</span><span>, </span><span>0xff</span><span>, </span><span>0xfe</span><span>]).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>FrameError</span><span>::</span><span>from</span><span>(</span><span>utf8</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"display : {error}"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>println!</span><span>(</span><span>"debug   : {error:?}"</span><span>);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>error</span><span>.</span><span>source</span><span>();</span></div></div><div><div><div>8</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>depth</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>9</div></div><div><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>cause</span><span><span>) </span><span>=</span><span> </span></span><span>source</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"cause {depth} : {cause}"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>cause</span><span>.</span><span>source</span><span>();</span></div></div><div><div><div>12</div></div><div><span>    </span><span>depth</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>let</span><span> </span><span>wrapped</span><span><span> </span><span>=</span><span> </span><span>anyhow</span><span>::</span></span><span>Error</span><span>::</span><span>new</span><span>(</span><span>error</span><span>).</span><span>context</span><span>(</span><span>"while reading the response from 127.0.0.1:7878"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>println!</span><span>(</span><span>"anyhow {{}}  : {wrapped}"</span><span>);</span></div></div><div><div><div>17</div></div><div><span>println!</span><span>(</span><span>"anyhow {{:#}}: {wrapped:#}"</span><span>);</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>let</span><span> </span><span>io</span><span><span> </span><span>=</span><span> </span></span><span>FrameError</span><span>::</span><span>from</span><span><span>(</span><span>std</span><span>::</span><span>io</span><span>::</span><span>Error</span><span>::</span></span><span>new</span><span><span>(</span><span>std</span><span>::</span><span>io</span><span>::</span><span>ErrorKind</span><span>::</span></span><span>ConnectionReset</span><span>, </span><span>"connection reset by peer"</span><span>));</span></div></div><div><div><div>20</div></div><div><span>println!</span><span>(</span><span>"io      : {io}  &lt;-  {}"</span><span>, </span><span>io</span><span>.</span><span>source</span><span>().</span><span>unwrap</span><span>());</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_error_chain</span></div></div><div><div><div>2</div></div><div><span>display : frame payload is not valid UTF-8</span></div></div><div><div><div>3</div></div><div><span>debug   : InvalidUtf8(FromUtf8Error { bytes: [104, 255, 254], error: Utf8Error { valid_up_to: 1, error_len: Some(1) } })</span></div></div><div><div><div>4</div></div><div><span>cause 1 : invalid utf-8 sequence of 1 bytes from index 1</span></div></div><div><div><div>5</div></div><div><span>anyhow {}  : while reading the response from 127.0.0.1:7878</span></div></div><div><div><div>6</div></div><div><span>anyhow {:#}: while reading the response from 127.0.0.1:7878: frame payload is not valid UTF-8: invalid utf-8 sequence of 1 bytes from index 1</span></div></div><div><div><div>7</div></div><div><span>io      : i/o error on the connection  &lt;-  connection reset by peer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>{}</code> 只打印最外层，<code>{:#}</code>（第 7 篇的 <code>{error:#}</code>）把整条链用冒号连起来：<code>上下文: FrameError: 底层原因</code>。这就是 <code>pebble: {error:#}</code> 在 <code>main.rs</code> 里的工作方式。C 里的 <code>errno</code> 加 <code>strerror</code> 是单层的；多层的原因链要自己维护。</p><p>然后是 <code>From</code> 实现：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;</span><span>io</span><span>::</span><span>Error</span><span>&gt; </span><span>for</span><span> </span><span>FrameError</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>error</span><span><span>: </span><span>io</span><span>::</span></span><span>Error</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>Self</span><span>::</span><span>Io</span><span>(</span><span>error</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;</span><span>std</span><span>::</span><span>string</span><span>::</span><span>FromUtf8Error</span><span>&gt; </span><span>for</span><span> </span><span>FrameError</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>error</span><span><span>: </span><span>std</span><span>::</span><span>string</span><span>::</span></span><span>FromUtf8Error</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Self</span><span>::</span><span>InvalidUtf8</span><span>(</span><span>error</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>impl From&lt;io::Error&gt; for FrameError</code> 的意义是 <code>?</code> 运算符：在一个返回 <code>Result&lt;_, FrameError&gt;</code> 的函数里，对一个 <code>Result&lt;_, io::Error&gt;</code> 写 <code>?</code>，如果出错，<code>?</code> 先调用 <code>FrameError::from(error)</code> 再返回。第 7 篇我说过 <code>?</code> 是”出错就提前返回”，真实的展开其实还多了这一步转换。这样 <code>reader.read_exact(&amp;mut payload).await?</code> 里的 <code>io::Error</code> 自动变成了 <code>FrameError::Io</code>。</p><p><code>#[non_exhaustive]</code> 是写给 crate 的<strong>使用者</strong>的：它们没法再对这个枚举写不带通配符的 <code>match</code>，所以作者以后加变体不是破坏性变更。为了验证，我在 lab crate 里（它是 <code>pebble-net</code> 的外部使用者）写了一个穷举的 <code>match</code>（实验 <code>ch14_non_exhaustive.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch14_non_exhaustive</span></div></div><div><div><div>2</div></div><div><span>error[E0004]: non-exhaustive patterns: `&amp;_` not covered</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>--&gt; src/bin/ch14_non_exhaustive.rs:4:11</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>4 |     match error {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|           ^^^^^ pattern `&amp;_` not covered</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>note: `FrameError` defined here</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>--&gt; /tmp/pebble-rebuild-14/crates/pebble-net/src/lib.rs:200:1</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>200 | pub enum FrameError {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>| ^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>= note: the matched value is of type `&amp;FrameError`</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>= note: `FrameError` is marked as non-exhaustive, so a wildcard `_` is necessary to match exhaustively</span></div></div><div><div><div>15</div></div><div><span>help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>8 ~         FrameError::Closed =&gt; "closed",</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>9 ~         &amp;_ =&gt; todo!(),</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>For more information about this error, try `rustc --explain E0004`.</span></div></div><div><div><div>22</div></div><div><span>error: could not compile `lab-ch14-tokio` (bin "ch14_non_exhaustive") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四个变体全列了，仍然被拒绝。注意它只对<strong>别的 crate</strong> 生效：在 <code>pebble-net</code> 内部（比如 <code>Display</code> 实现里）仍然可以不写通配符，因为同一个 crate 里作者自己知道变体表。这就是为什么我要用一个外部的 lab crate 做这个实验。</p><div><div><div></div><div><code>thiserror</code>：把这一切缩成三行属性</div></div><div><p>上面那五十来行样板代码（<code>Display</code>、<code>Error</code>、两个 <code>From</code>）是 <code>thiserror</code> crate 的 <code>#[derive(Error)]</code> 要替你写的。我把同样的四个变体用它写了一遍（实验 <code>ch14_thiserror.rs</code>，<code>Cargo.toml</code> 里加了 <code>thiserror = "2"</code>），消息和 <code>pebble-net</code> 里的一样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Error</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>FrameError</span><span> {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[error(</span><span>"i/o error on the connection"</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Io</span><span>(</span><span>#[from]</span><span><span> </span><span>io</span><span>::</span></span><span>Error</span><span>),</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[error(</span><span>"frame length {length} exceeds maximum frame size {max}"</span><span>)]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>TooLarge</span><span> { </span><span>length</span><span>: </span><span>usize</span><span>, </span><span>max</span><span>: </span><span>usize</span><span> },</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>#[error(</span><span>"frame payload is not valid UTF-8"</span><span>)]</span></div></div><div><div><div>8</div></div><div><span>    </span><span>InvalidUtf8</span><span>(</span><span>#[from]</span><span><span> </span><span>std</span><span>::</span><span>string</span><span>::</span></span><span>FromUtf8Error</span><span>),</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[error(</span><span>"peer closed the connection without a response"</span><span>)]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Closed</span><span>,</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_thiserror</span></div></div><div><div><div>2</div></div><div><span>frame length 1024 exceeds maximum frame size 16</span></div></div><div><div><div>3</div></div><div><span>frame payload is not valid UTF-8</span></div></div><div><div><div>4</div></div><div><span>has a source: true</span></div></div><div><div><div>5</div></div><div><span>peer closed the connection without a response</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>#[from]</code> 同时生成 <code>From</code> 实现和 <code>source()</code>。它本身也是一个派生宏，和第 6 篇我们写的 <code>#[derive(Spanned)]</code> 是一回事。Pebble 没有用它，是因为这个 crate 的依赖数量我想尽可能少，而且手写一遍对学习更有用。真实项目里我会用 <code>thiserror</code>。</p></div></div><p>最后是 <code>read_frame</code> 和 <code>write_frame</code>。<code>read_frame</code> 返回 <code>Result&lt;Option&lt;Vec&lt;u8&gt;&gt;, FrameError&gt;</code>：外层 <code>Result</code> 是失败，内层 <code>Option</code> 区分”收到一帧”（<code>Some</code>）和”对端在两帧之间干净地关闭了连接”（<code>None</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Reads one length-prefixed frame. Returns `Ok(None)` on a clean end of</span></div></div><div><div><div>2</div></div><div><span>/// stream before any header byte was read.</span></div></div><div><div><div>3</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>read_frame</span><span>&lt;</span><span>R</span><span>&gt;(</span><span>reader</span><span>: &amp;</span><span>mut</span><span> </span><span>R</span><span>, </span><span>max_frame</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt;&gt;, </span><span>FrameError</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span>where</span></div></div><div><div><div>5</div></div><div><span>    </span><span>R</span><span>: </span><span>AsyncRead</span><span> + </span><span>Unpin</span><span>,</span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>header</span><span><span> </span><span>=</span><span> [</span></span><span>0</span><span>u8</span><span>; </span><span>4</span><span>];</span></div></div><div><div><div>8</div></div><div><span>    </span><span>match</span><span> </span><span>reader</span><span>.</span><span>read_exact</span><span>(&amp;</span><span>mut</span><span> </span><span>header</span><span>).</span><span>await</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Ok</span><span>(</span><span>_</span><span>) =&gt; {}</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) </span><span>if</span><span> </span><span>error</span><span>.</span><span>kind</span><span><span>() </span><span>==</span><span> </span><span>io</span><span>::</span></span><span>ErrorKind</span><span>::</span><span>UnexpectedEof</span><span> =&gt; </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>None</span><span>),</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; </span><span>return</span><span> </span><span>Err</span><span>(</span><span>error</span><span>.</span><span>into</span><span>()),</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>length</span><span><span> </span><span>=</span><span> </span></span><span>u32</span><span>::</span><span>from_be_bytes</span><span>(</span><span>header</span><span>) </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>15</div></div><div><span>    </span><span>if</span><span> </span><span>length</span><span><span> </span><span>&gt;</span><span> </span></span><span>max_frame</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span>(</span><span>FrameError</span><span>::</span><span>TooLarge</span><span> {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>length</span><span>,</span></div></div><div><div><div>18</div></div><div><span>            </span><span>max</span><span>: </span><span>max_frame</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>payload</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>0</span><span>u8</span><span>; </span><span>length</span><span>];</span></div></div><div><div><div>23</div></div><div><span>    </span><span>reader</span><span>.</span><span>read_exact</span><span>(&amp;</span><span>mut</span><span> </span><span>payload</span><span>).</span><span>await</span><span>?;</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Ok</span><span>(</span><span>Some</span><span>(</span><span>payload</span><span>))</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几点：</p><ul>
<li><code>R: AsyncRead + Unpin</code>：<code>R</code> 是任何可异步读的东西（<code>TcpStream</code>、<code>OwnedReadHalf</code>、测试里的 <code>&amp;[u8]</code>），<code>Unpin</code> 是上面讲的”可以被移动”；<code>read_exact</code> 需要 <code>&amp;mut R</code> 能被重新借用，不需要钉住。这就是 <code>read_frame</code> 既能读 TCP 又能读字节切片的原因，测试里我们读的是 <code>&amp;[u8]</code>。</li>
<li><code>read_exact(&amp;mut header).await</code>：读满 4 个字节，读不满就 <code>UnexpectedEof</code>。对头部来说，在 0 字节处遇到 EOF 是正常的（对方关了），所以第一个 <code>match</code> 把 <code>UnexpectedEof</code> 映射成 <code>Ok(None)</code>；其他 I/O 错误用 <code>error.into()</code> 转成 <code>FrameError::Io</code>，这里不能用 <code>?</code>，因为这个分支要先看 <code>error.kind()</code>。一个细节：代码分不出”0 字节”和”读了 1 到 3 字节”，<code>read_exact</code> 两种情况都报 <code>UnexpectedEof</code>，所以头部被截断在中间也会被当成干净关闭返回 <code>Ok(None)</code>，文档注释里”before any header byte was read”说得比实际更强。</li>
<li>注意读 payload 的 <code>reader.read_exact(&amp;mut payload).await?</code> <strong>没有</strong>对 <code>UnexpectedEof</code> 特殊处理：头部读完了而正文被截断，是真的出错，应该走 <code>FrameError::Io</code>。</li>
<li>先检查 <code>length &gt; max_frame</code> 再分配：否则一个恶意的客户端发 <code>0xFFFFFFFF</code> 就能让服务器分配 4 GiB。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Writes one length-prefixed frame.</span></div></div><div><div><div>2</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>write_frame</span><span>&lt;</span><span>W</span><span>&gt;(</span><span>writer</span><span>: &amp;</span><span>mut</span><span> </span><span>W</span><span>, </span><span>payload</span><span>: &amp;[</span><span>u8</span><span>], </span><span>max_frame</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Result</span><span>&lt;(), </span><span>FrameError</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span>where</span></div></div><div><div><div>4</div></div><div><span>    </span><span>W</span><span>: </span><span>AsyncWrite</span><span> + </span><span>Unpin</span><span>,</span></div></div><div><div><div>5</div></div><div><span>{</span></div></div><div><div><div>6</div></div><div><span>    </span><span>if</span><span> </span><span>payload</span><span>.</span><span>len</span><span>() &gt; </span><span>max_frame</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span>(</span><span>FrameError</span><span>::</span><span>TooLarge</span><span> {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>length</span><span>: </span><span>payload</span><span>.</span><span>len</span><span>(),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>max</span><span>: </span><span>max_frame</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>length</span><span><span> </span><span>=</span><span> </span></span><span>u32</span><span>::</span><span>try_from</span><span>(</span><span>payload</span><span>.</span><span>len</span><span>()).</span><span>map_err</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> </span><span>FrameError</span><span>::</span></span><span>TooLarge</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>length</span><span>: </span><span>payload</span><span>.</span><span>len</span><span>(),</span></div></div><div><div><div>14</div></div><div><span>        </span><span>max</span><span><span>: </span><span>u32</span><span>::</span></span><span>MAX</span><span> </span><span>as</span><span> </span><span>usize</span><span>,</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>})?;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>writer</span><span>.</span><span>write_all</span><span>(&amp;</span><span>length</span><span>.</span><span>to_be_bytes</span><span>()).</span><span>await</span><span>?;</span></div></div><div><div><div>17</div></div><div><span>    </span><span>writer</span><span>.</span><span>write_all</span><span>(</span><span>payload</span><span>).</span><span>await</span><span>?;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>writer</span><span>.</span><span>flush</span><span>().</span><span>await</span><span>?;</span></div></div><div><div><div>19</div></div><div><span>    </span><span>Ok</span><span>(())</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>write_frame</code> 检查两次：一次是我们的上限，一次是 <code>u32::try_from</code>，因为长度前缀只有 32 位，<code>usize</code> 在 64 位机器上放得下更大的数（第 8 篇的 <code>TryFrom</code>）。<code>write_all</code> 保证写完整，<code>flush</code> 把缓冲区推出去（对 <code>TcpStream</code> 来说是空操作，但对带缓冲的写入者是必要的，所以协议层总是调用它）。</p><p>两个测试放进 <code>mod tests</code>，先把测试模块的头和前面四个 <code>eval_to_string_*</code> 测试复制过来（它们是不碰网络的纯函数测试，从 tag 复制即可）。这里有两个值得写出来的，它们都是 <code>FrameError</code> 的契约：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[tokio</span><span>::</span><span>test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>async</span><span> </span><span>fn</span><span> </span><span>oversized_header_is_a_typed_error</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>reader</span><span>: &amp;[</span><span>u8</span><span><span>] </span><span>=</span><span> &amp;</span></span><span>1024</span><span>u32</span><span>.</span><span>to_be_bytes</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>read_frame</span><span>(&amp;</span><span>mut</span><span> </span><span>reader</span><span>, </span><span>16</span><span>).</span><span>await</span><span>.</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>6</div></div><div><span>            </span><span>matches!</span><span>(</span></div></div><div><div><div>7</div></div><div><span>                </span><span>error</span><span>,</span></div></div><div><div><div>8</div></div><div><span>                </span><span>FrameError</span><span>::</span><span>TooLarge</span><span> {</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>length</span><span>: </span><span>1024</span><span>,</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>max</span><span>: </span><span>16</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>"got {error:?}"</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>16</div></div><div><span>            </span><span>error</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>17</div></div><div><span>            </span><span>"frame length 1024 exceeds maximum frame size 16"</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>22</div></div><div><span>    </span><span>fn</span><span> </span><span>invalid_utf8_keeps_its_source</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>FrameError</span><span>::</span><span>from</span><span><span>(</span><span>String</span><span>::</span></span><span>from_utf8</span><span>(</span><span>vec!</span><span>[</span><span>0xff</span><span>]).</span><span>unwrap_err</span><span>());</span></div></div><div><div><div>24</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>error</span><span>.</span><span>to_string</span><span>(), </span><span>"frame payload is not valid UTF-8"</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>assert!</span><span><span>(</span><span>std</span><span>::</span><span>error</span><span>::</span></span><span>Error</span><span>::</span><span>source</span><span>(&amp;</span><span>error</span><span>).</span><span>is_some</span><span>());</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>let mut reader: &amp;[u8] = &amp;1024u32.to_be_bytes();</code>：<code>&amp;[u8]</code> 实现了 <code>AsyncRead</code>（读完就是 EOF），所以一个字节数组就是一个现成的假连接，不用开 socket。<code>matches!</code>（第 7 篇）在字段上做了模式匹配，<code>TooLarge { length: 1024, max: 16 }</code> 里的字面量是模式。<code>#[tokio::test]</code> 是上面讲过的 <code>#[tokio::main]</code> 的测试版，让一个 <code>async fn</code> 可以作为测试运行。</p><p>到这里，<strong>暂时</strong>把服务器和客户端两节、以及其余三个 TCP 测试都省掉（它们是 <code>oversized_frame_is_rejected</code>、<code>round_trip_over_tcp</code>、<code>concurrent_clients_both_succeed</code>，下面再加回来），只留下上面的内容，运行。输出里我用 <code>grep</code> 只留了每条警告的第一行和测试结果行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-net</span></div></div><div><div><div>2</div></div><div><span>warning: unused imports: `TcpListener` and `TcpStream`</span></div></div><div><div><div>3</div></div><div><span>warning: unused import: `tokio::sync::Mutex`</span></div></div><div><div><div>4</div></div><div><span>warning: unused import: `std::future::Future`</span></div></div><div><div><div>5</div></div><div><span>warning: unused import: `ToSocketAddrs`</span></div></div><div><div><div>6</div></div><div><span>warning: field `is_error` is never read</span></div></div><div><div><div>7</div></div><div><span>warning: function `read_frame` is never used</span></div></div><div><div><div>8</div></div><div><span>warning: function `write_frame` is never used</span></div></div><div><div><div>9</div></div><div><span>warning: `pebble-net` (lib) generated 7 warnings (run `cargo fix --lib -p pebble-net` to apply 2 suggestions)</span></div></div><div><div><div>10</div></div><div><span>...</span></div></div><div><div><div>11</div></div><div><span>test tests::eval_to_string_reports_parse_errors ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::invalid_utf8_keeps_its_source ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::eval_to_string_includes_print_output ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::eval_to_string_reports_runtime_errors ... ok</span></div></div><div><div><div>15</div></div><div><span>test tests::eval_to_string_returns_the_value ... ok</span></div></div><div><div><div>16</div></div><div><span>test tests::oversized_header_is_a_typed_error ... ok</span></div></div><div><div><div>17</div></div><div><span>test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>6 个测试，7 个”没用到”的警告：那些导入和函数是后面的服务器要用的，现在它们是死代码。警告是诚实的，它们在告诉你下面还有活。</p></section><section><h3>服务器：<code>select!</code>、任务和 <code>Arc&lt;Mutex&lt;Stats&gt;&gt;</code><a href="#服务器select任务和-arcmutexstats"><span>#</span></a></h3><p>服务器的主体是一个无限循环：等下一个连接，把它交给一个新任务，回到等待。但它还要能被叫停，不然测试里的服务器永远关不掉。所以每一圈要同时等两件事：有新连接，或者有人说停。这正是 <code>tokio::select!</code> 的用途。先把 <code>// Server</code> 横幅（三行注释）从 tag 复制进来，然后写最里层的 <code>serve_listener</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Serves on an already-bound listener until `shutdown` resolves.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Each accepted connection is handled by a spawned task, so a slow or idle</span></div></div><div><div><div>4</div></div><div><span>/// client never blocks the accept loop.</span></div></div><div><div><div>5</div></div><div><span>///</span></div></div><div><div><div>6</div></div><div><span>/// # Errors</span></div></div><div><div><div>7</div></div><div><span>///</span></div></div><div><div><div>8</div></div><div><span>/// Returns an error if accepting a connection fails at the OS level.</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>async</span><span> </span><span>fn</span><span> </span><span>serve_listener</span><span>(</span></div></div><div><div><div>10</div></div><div><span>    </span><span>listener</span><span>: </span><span>TcpListener</span><span>,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>config</span><span>: </span><span>ServerConfig</span><span>,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>shutdown</span><span>: </span><span>impl</span><span> </span><span>Future</span><span>&lt;</span><span>Output</span><span><span> </span><span>=</span><span> ()&gt; + </span></span><span>Send</span><span> + '</span><span>static</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>) -&gt; </span><span>io</span><span>::</span></span><span>Result</span><span>&lt;()&gt; {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>stats</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>new</span><span><span>(</span><span>Mutex</span><span>::</span></span><span>new</span><span><span>(</span><span>Stats</span><span>::</span></span><span>default</span><span>()));</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>tokio</span><span>::</span><span>pin!</span><span>(</span><span>shutdown</span><span>);</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>tokio</span><span>::</span><span>select!</span><span> {</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>() </span><span>=</span><span> &amp;</span><span>mut</span><span> </span><span>shutdown</span><span> =&gt; </span><span>return</span><span> </span><span>Ok</span><span>(()),</span></div></div><div><div><div>20</div></div><div><span>            </span><span>accepted</span><span><span> </span><span>=</span><span> </span></span><span>listener</span><span>.</span><span>accept</span><span>() =&gt; {</span></div></div><div><div><div>21</div></div><div><span>                </span><span>let</span><span> (</span><span>stream</span><span>, </span><span>_peer</span><span><span>) </span><span>=</span><span> </span></span><span>accepted</span><span>?;</span></div></div><div><div><div>22</div></div><div><span>                </span><span>let</span><span> </span><span>config</span><span><span> </span><span>=</span><span> </span></span><span>config</span><span>.</span><span>clone</span><span>();</span></div></div><div><div><div>23</div></div><div><span>                </span><span>let</span><span> </span><span>stats</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>clone</span><span>(&amp;</span><span>stats</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>                </span></span><span>tokio</span><span>::</span><span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> {</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>handle_connection</span><span>(</span><span>stream</span><span>, </span><span>config</span><span>, </span><span>stats</span><span>).</span><span>await</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>});</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐行：</p><ul>
<li><code>shutdown: impl Future&lt;Output = ()&gt; + Send + 'static</code>：停止信号是任何一个”完成时不产生值”的 <code>Future</code>：一个 <code>oneshot</code> 通道的接收端、<code>Ctrl-C</code> 信号、一个定时器，都可以。<code>Send + 'static</code> 因为整个 <code>serve_listener</code> 本身会被 <code>tokio::spawn</code>（测试里就是这样用的），它的状态机里存着 <code>shutdown</code>，所以 <code>shutdown</code> 必须满足 <code>spawn</code> 的要求。<code>impl Trait</code> 在参数位置是第 5 篇的匿名泛型。</li>
<li><code>tokio::pin!(shutdown)</code>：下面 <code>select!</code> 里要写 <code>&amp;mut shutdown</code>（每一圈都 <code>poll</code> 同一个 <code>shutdown</code>，不能每圈移走它），而 <code>&amp;mut F</code> 要是 <code>Future</code>，<code>F</code> 就得是 <code>Unpin</code>；<code>impl Future</code> 不保证 <code>Unpin</code>，所以先钉住。这是上面 <code>Pin</code> 一节在实战里的出现，<code>pin!</code> 宏就是那一行解决的。</li>
<li><code>tokio::select!</code>：同时 <code>poll</code> 几个 <code>Future</code>，哪个先完成就执行它的分支，<strong>其余的被丢弃</strong>。分支的语法是 <code>模式 = 表达式 =&gt; 代码</code>。<code>() = &amp;mut shutdown</code> 的模式是单元值 <code>()</code>（<code>Output = ()</code>）。这里 <code>listener.accept()</code> 被丢弃时会怎样？Tokio 文档说 <code>accept</code> 是”取消安全”的：被丢弃的那一刻还没拿到连接的话，什么都不会丢。我用一个实验确认被丢弃的分支真的被销毁了（实验 <code>ch14_select_cancel.rs</code>，<code>quick</code> 50 毫秒完成，<code>slow</code> 要 500 毫秒，<code>slow</code> 里有一个会在 <code>Drop</code> 时打印的局部变量）：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_select_cancel</span></div></div><div><div><div>2</div></div><div><span>slow's local dropped</span></div></div><div><div><div>3</div></div><div><span>winner: quick</span></div></div><div><div><div>4</div></div><div><span>main done; "slow finished" never printed</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输掉的 <code>slow()</code> 在 <code>quick</code> 赢的那一刻就被丢弃了：它的局部变量被销毁（<code>Drop</code> 照常运行，打印在 <code>winner: quick</code> 之前），<code>slow finished</code> 永远不会出现。这就是 <code>async</code> 里的取消：丢弃 <code>Future</code> 就是取消它，下一个 <code>.await</code> 点之后的代码不会再运行。（C++ 里取消一个正在进行的异步操作要靠 token 或者 <code>cancel()</code> 方法，由库作者自己实现。）</p><ul>
<li><code>accepted?</code>：<code>accept</code> 失败（比如进程的文件描述符用光了）就整个服务器返回错误。生产服务器大概会重试暂时性的错误，这里没有。</li>
<li><code>tokio::spawn(async move { ... })</code>：每个连接一个任务，<code>async move</code> 把 <code>stream</code>、<code>config</code>、<code>stats</code> 搬进去。<code>config.clone()</code> 和 <code>Arc::clone(&amp;stats)</code> 是在搬之前做的：每个任务拥有自己的一份。任务里 <code>let _ = handle_connection(...).await;</code>：错误被丢弃，因为没有人可以汇报；<code>handle_connection</code> 返回前已经在 <code>Stats</code> 里记过一笔了。</li>
</ul><p>这就是 C 里 <code>accept</code> + <code>fork</code>（或 <code>accept</code> + <code>pthread_create</code>）的位置，只是任务比进程和线程轻得多。</p><p><code>Stats</code> 被所有任务共享，它的包装是 <code>Arc&lt;Mutex&lt;Stats&gt;&gt;</code>，这里的 <code>Mutex</code> 是 <code>tokio::sync::Mutex</code>（文件头里 <code>use tokio::sync::Mutex;</code>）。<code>Arc</code> 的作用和线程那一节一样：多个任务都拥有它，最后一个丢弃它的时候它才被释放。</p><p>外面两层是复制的：<code>serve</code>（等 <code>Ctrl-C</code>，调用下一层）和 <code>serve_with_shutdown</code>（绑定地址，调用 <code>serve_listener</code>）。把 tag 里从 <code>// Server</code> 横幅之后的 <code>serve</code>、<code>serve_with_shutdown</code> 两个函数复制到 <code>serve_listener</code> 的前面，顺序是 <code>serve</code>、<code>serve_with_shutdown</code>、<code>serve_listener</code>。<code>serve</code> 里值得注意的只有一处：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>shutdown</span><span><span> </span><span>=</span><span> </span></span><span>async</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> </span><span>let</span><span> </span><span>Err</span><span>(</span><span>error</span><span><span>) </span><span>=</span><span> </span><span>tokio</span><span>::</span><span>signal</span><span>::</span></span><span>ctrl_c</span><span>().</span><span>await</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>eprintln!</span><span>(</span><span>"pebble-net: could not listen for Ctrl-C: {error}"</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个 <code>async</code> 块本身就是一个 <code>Future</code> 值，没有名字，正适合当作 <code>shutdown</code> 参数。这也是 <code>signal</code> 这个 Tokio feature 的用处。</p></section><section><h3><code>handle_connection</code>：成帧、求值、超时<a href="#handle_connection成帧求值超时"><span>#</span></a></h3><p>最后一块是处理一个连接的循环。我分成四段读。第一段，签名和开头：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Handles a single client connection: reads request frames until the peer</span></div></div><div><div><div>2</div></div><div><span>/// disconnects, evaluating each one with a per-request timeout.</span></div></div><div><div><div>3</div></div><div><span>///</span></div></div><div><div><div>4</div></div><div><span>/// # Errors</span></div></div><div><div><div>5</div></div><div><span>///</span></div></div><div><div><div>6</div></div><div><span>/// Returns a [`FrameError`] when framing fails (including an oversized frame),</span></div></div><div><div><div>7</div></div><div><span>/// the payload is not UTF-8, or the connection breaks mid-frame.</span></div></div><div><div><div>8</div></div><div><span>pub</span><span> </span><span>async</span><span> </span><span>fn</span><span> </span><span>handle_connection</span><span>(</span></div></div><div><div><div>9</div></div><div><span>    </span><span>stream</span><span>: </span><span>TcpStream</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>config</span><span>: </span><span>ServerConfig</span><span>,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>stats</span><span>: </span><span>Arc</span><span>&lt;</span><span>Mutex</span><span>&lt;</span><span>Stats</span><span>&gt;&gt;,</span></div></div><div><div><div>12</div></div><div><span>) -&gt; </span><span>Result</span><span>&lt;(), </span><span>FrameError</span><span>&gt; {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>stats</span><span>.</span><span>lock</span><span>().</span><span>await</span><span><span>.connections </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> (</span><span>mut</span><span> </span><span>reader</span><span>, </span><span>mut</span><span> </span><span>writer</span><span><span>) </span><span>=</span><span> </span></span><span>stream</span><span>.</span><span>into_split</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>返回 <code>Result&lt;(), FrameError&gt;</code>：上面 <code>FrameError</code> 的 doc 里写的四种失败都从这里出来。<code>stats.lock().await</code>：Tokio 的 <code>Mutex</code> 的 <code>lock()</code> 是 <code>async fn</code>，要 <code>.await</code>，拿到的 guard 会在这条语句结束时被丢弃（临时值），所以 <code>connections += 1</code> 是一次性的。<code>stream.into_split()</code> 把 <code>TcpStream</code> 拆成独立的读半边和写半边（可以放到不同任务里），这里同一个任务里用，是为了让读写的借用互不干扰。</p><p>第二段，读帧并解码：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>payload</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>read_frame</span><span>(&amp;</span><span>mut</span><span> </span><span>reader</span><span>, </span><span>config</span><span>.max_frame).</span><span>await</span><span> {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>Ok</span><span>(</span><span>Some</span><span>(</span><span>payload</span><span>)) =&gt; </span><span>payload</span><span>,</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Ok</span><span>(</span><span>None</span><span>) =&gt; </span><span>return</span><span> </span><span>Ok</span><span>(()),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>stats</span><span>.</span><span>lock</span><span>().</span><span>await</span><span><span>.errors </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>7</div></div><div><span>                </span><span>return</span><span> </span><span>Err</span><span>(</span><span>error</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>String</span><span>::</span><span>from_utf8</span><span>(</span><span>payload</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>Ok</span><span>(</span><span>program</span><span>) =&gt; </span><span>program</span><span>,</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; {</span></div></div><div><div><div>14</div></div><div><span>                </span><span>stats</span><span>.</span><span>lock</span><span>().</span><span>await</span><span><span>.errors </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>15</div></div><div><span>                </span><span>return</span><span> </span><span>Err</span><span>(</span><span>error</span><span>.</span><span>into</span><span>());</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>read_frame</code> 的三种结果各有处理：<code>Some(payload)</code> 继续，<code>None</code>（对端正常关闭）返回 <code>Ok(())</code>，<code>Err</code> 记一次错误再返回。注意 <code>stats.lock().await.errors += 1;</code> 是在返回<strong>之前</strong>：每一条错误路径都要记账，所以这行出现了五次，有一点重复，我没有抽成函数，因为每次的上下文不同，抽出来还要传 <code>stats</code>。UTF-8 解码失败时：<code>return Err(error.into())</code>：<code>.into()</code> 就是上面那个 <code>From&lt;FromUtf8Error&gt; for FrameError</code>，这里没有 <code>?</code> 是因为同样要先记账。<code>String::from_utf8(payload)</code> 拿走 <code>payload</code> 的所有权并复用它的缓冲区，不会复制。</p><p>第三段是整个服务器里最关键的两行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>stats</span><span>.</span><span>lock</span><span>().</span><span>await</span><span><span>.requests </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>evaluated</span><span><span> </span><span>=</span><span> </span><span>tokio</span><span>::</span><span>task</span><span>::</span></span><span>spawn_blocking</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> </span></span><span>evaluate</span><span>(&amp;</span><span>program</span><span>));</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>response</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span><span> </span><span>tokio</span><span>::</span><span>time</span><span>::</span></span><span>timeout</span><span>(</span><span>config</span><span>.request_timeout, </span><span>evaluated</span><span>).</span><span>await</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Ok</span><span>(</span><span>Ok</span><span>(</span><span>evaluation</span><span>)) =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>if</span><span> </span><span>evaluation</span><span>.is_error {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>stats</span><span>.</span><span>lock</span><span>().</span><span>await</span><span><span>.errors </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>                </span><span>evaluation</span><span>.text</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Ok</span><span>(</span><span>Err</span><span>(</span><span>join_error</span><span>)) =&gt; {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>stats</span><span>.</span><span>lock</span><span>().</span><span>await</span><span><span>.errors </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>13</div></div><div><span>                </span><span>format!</span><span>(</span><span>"error: evaluator task failed: {join_error}"</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Err</span><span>(</span><span>_elapsed</span><span>) =&gt; {</span></div></div><div><div><div>16</div></div><div><span>                </span><span>stats</span><span>.</span><span>lock</span><span>().</span><span>await</span><span><span>.errors </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>17</div></div><div><span>                </span><span>"error: request timed out"</span><span>.</span><span>to_string</span><span>()</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>tokio::task::spawn_blocking(move || evaluate(&amp;program))</code> 把”求值”这个同步的、CPU 密集的函数丢到 Tokio 的阻塞线程池，立刻返回一个 <code>JoinHandle</code>；<code>tokio::time::timeout(duration, future)</code> 把它包起来：到时间还没完成就返回 <code>Err(Elapsed)</code>。所以 <code>match</code> 有三个分支：</p><ul>
<li><code>Ok(Ok(evaluation))</code>：按时完成了。如果 <code>evaluation.is_error</code>（解析失败或运行时错误），记一次错误；回应是 <code>evaluation.text</code>。</li>
<li><code>Ok(Err(join_error))</code>：<code>JoinHandle</code> 报错，意思是阻塞任务 panic 了（或者被取消了）。回一句 <code>error: evaluator task failed: ...</code>。一个求值器的 bug 不会拖垮整个服务器，只让这一个请求失败。</li>
<li><code>Err(_elapsed)</code>：超时了。回 <code>error: request timed out</code>。</li>
</ul><p>我想知道”为什么一定要 <code>spawn_blocking</code>”，因为文件头的文档说”解释器不是 <code>Send</code>，所以放在 <code>spawn_blocking</code> 里”，而上面的实验显示解释器只要不跨 <code>.await</code> 就不是问题。那么直接在 <code>async fn</code> 里调用 <code>evaluate(&amp;program)</code> 会怎么样？我把 <code>spawn_blocking</code> 那一行换成 <code>let evaluated = async move { Ok::&lt;_, tokio::task::JoinError&gt;(evaluate(&amp;program)) };</code>（保持下面的 <code>match</code> 不变），它<strong>可以编译</strong>。然后我在实验副本里加了一个新测试（只跑了这一个，没有重跑其余 9 个）：把超时设成 50 毫秒，发一个要跑很久的程序（一个数到三百万的 <code>while</code> 循环），测 <code>run_client</code> 多久才拿到回复：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## spawn_blocking</span></div></div><div><div><div>2</div></div><div><span>reply "error: request timed out" after 51 ms</span></div></div><div><div><div>3</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 4.44s</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>## 直接在 async fn 里调用</span></div></div><div><div><div>6</div></div><div><span>reply "3000000" after 4417 ms</span></div></div><div><div><div>7</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 4.42s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（debug 构建；那个循环在这台机器上要 4.4 秒。）这个结果很清楚：直接调用的版本在<strong>能编译</strong>的同时，<strong>超时形同虚设</strong>：回复等了 4417 毫秒，远超 50 毫秒的限制，因为 <code>evaluate</code> 里面没有任何 <code>.await</code>，<code>timeout</code> 的计时器永远没有机会被检查（和上面 <code>ch14_block_the_runtime</code> 里 tick 迟到是同一个原因）。而且在这 4.4 秒里，运行时的那个工作线程完全被占着，同一个线程上的别的连接全部卡住。<code>spawn_blocking</code> 的版本在 51 毫秒就拿到了回复。</p><p>所以更诚实的说法是：<code>!Send</code> 的解释器不能跨 <code>.await</code>，这是一个约束，<code>evaluate</code> 是同步函数、内部创建内部丢弃，天然满足；而使用 <code>spawn_blocking</code> 的<strong>真正理由</strong>是：求值是 CPU 密集的、不会让出的，不能放在异步任务里直接做，否则超时和公平都做不到。</p><p>还有一个坏消息，在上面第二行的 <code>finished in 4.44s</code>：即使 <code>spawn_blocking</code> 的版本在 51 毫秒就回复了客户端，那个测试要 4.44 秒才结束，因为测试的运行时在退出时要等阻塞线程跑完。<strong>超时只是放弃等待，并没有取消计算。</strong> 实验 <code>ch14_timeout_blocking.rs</code> 用一个会睡 2 秒的阻塞任务把这件事单独量了出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_timeout_blocking</span></div></div><div><div><div>2</div></div><div><span>timeout fired: true (after ~200 ms)</span></div></div><div><div><div>3</div></div><div><span>block_on returned at ~200 ms</span></div></div><div><div><div>4</div></div><div><span>the blocking job finally finished</span></div></div><div><div><div>5</div></div><div><span>runtime dropped at ~2000 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>timeout</code> 200 毫秒时触发了，<code>block_on</code> 也在 200 毫秒时返回了，但 <code>drop(runtime)</code> 一直等到阻塞任务在第 2000 毫秒结束。线程没法被外面强行终止（Rust 标准库根本没有 <code>kill_thread</code>，因为强行终止会跳过 <code>Drop</code>，留下持着锁的状态），要想真的取消，被取消的代码必须自己配合，比如在循环里检查一个标志。Pebble 的解释器没有这样的钩子，这是练习 1。</p><p>第四段，写回响应：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>write_frame</span><span>(&amp;</span><span>mut</span><span> </span><span>writer</span><span>, </span><span>response</span><span>.</span><span>as_bytes</span><span>(), </span><span>config</span><span>.max_frame).</span><span>await</span><span>?;</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后的 <code>}</code> 收尾 <code>loop</code>，函数没有结尾的返回值：循环只通过 <code>return</code> 离开。</p><p>整个服务器里 <code>std::sync::Mutex</code> 和 <code>tokio::sync::Mutex</code> 都出现了：<code>SharedOutput</code> 里是 <code>std</code> 的（只在同步的 <code>evaluate</code> 里用，从不跨 <code>.await</code>），<code>Stats</code> 是 <code>tokio</code> 的。我留了一个余地给自己诚实：<code>Stats</code> 在这里的每个 guard 都是临时值，从不跨 <code>.await</code>，所以换成 <code>std::sync::Mutex</code> 也行。我试了：把 <code>use tokio::sync::Mutex;</code> 换成 <code>use std::sync::Mutex;</code>，把所有 <code>.lock().await</code> 换成 <code>.lock().unwrap()</code>，9 个测试照样通过。Tokio 文档的建议是，锁持有时间很短、不跨 <code>.await</code> 的时候 <code>std</code> 的更快；持锁的时候要 <code>.await</code> 才用 <code>tokio</code> 的（上面 <code>ch14_std_mutex_await</code> 的编译错误就是原因）。仓库里选了 <code>tokio</code> 的，因为它不用处理中毒，写起来整齐一些；我尊重这个选择，没有改它。</p></section><section><h3>客户端：复制<a href="#客户端复制"><span>#</span></a></h3><p><code>run_client</code> 是客户端：连接，发一帧，读一帧，返回文本。从 tag 复制 <code>// Client</code> 横幅和 <code>run_client</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>async</span><span> </span><span>fn</span><span> </span><span>run_client</span><span>(</span><span>addr</span><span>: </span><span>impl</span><span> </span><span>ToSocketAddrs</span><span>, </span><span>program</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>String</span><span>, </span><span>FrameError</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>stream</span><span><span> </span><span>=</span><span> </span></span><span>TcpStream</span><span>::</span><span>connect</span><span>(</span><span>addr</span><span>).</span><span>await</span><span>?;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>write_frame</span><span>(&amp;</span><span>mut</span><span> </span><span>stream</span><span>, </span><span>program</span><span>.</span><span>as_bytes</span><span>(), </span><span>DEFAULT_MAX_FRAME</span><span>).</span><span>await</span><span>?;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>payload</span><span><span> </span><span>=</span><span> </span></span><span>read_frame</span><span>(&amp;</span><span>mut</span><span> </span><span>stream</span><span>, </span><span>DEFAULT_MAX_FRAME</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>.</span><span>await</span><span>?</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>.</span><span>ok_or</span><span><span>(</span><span>FrameError</span><span>::</span></span><span>Closed</span><span>)?;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>Ok</span><span>(</span><span>String</span><span>::</span><span>from_utf8</span><span>(</span><span>payload</span><span>)?)</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它返回 <code>Result&lt;String, FrameError&gt;</code>，所以 <code>?</code> 把 <code>connect</code> 的 <code>io::Error</code> 和 <code>String::from_utf8</code> 的错误都转成了 <code>FrameError</code>；<code>read_frame(...).await?.ok_or(FrameError::Closed)?</code> 里 <code>Option::ok_or</code> 把 <code>None</code>（对端没回就关了）变成 <code>Err(Closed)</code>，第四个变体终于有了用处。</p></section><section><h3>三个 TCP 测试<a href="#三个-tcp-测试"><span>#</span></a></h3><p>把剩下的 <code>bound_listener</code> 辅助函数和三个测试（<code>round_trip_over_tcp</code>、<code>concurrent_clients_both_succeed</code>、<code>oversized_frame_is_rejected</code>）从 tag 复制过来放进 <code>mod tests</code>。它们都在 <code>127.0.0.1:0</code> 上绑定（端口 0 让操作系统挑一个空闲端口，<code>local_addr()</code> 再把它问出来），所以测试之间不会抢端口。<code>round_trip_over_tcp</code> 用一个 <code>oneshot</code> 通道当作停止信号；<code>concurrent_clients_both_succeed</code> 用 <code>tokio::join!</code> 同时发两个请求，并检查 <code>Stats</code> 里的 <code>requests == 2</code>；<code>oversized_frame_is_rejected</code> 手工写一个声称 1024 字节的头，期望服务器关闭连接而不是等那 1024 字节。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-net</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>test tests::eval_to_string_includes_print_output ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::eval_to_string_reports_parse_errors ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::eval_to_string_returns_the_value ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::invalid_utf8_keeps_its_source ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::eval_to_string_reports_runtime_errors ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::oversized_header_is_a_typed_error ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::oversized_frame_is_rejected ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::concurrent_clients_both_succeed ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::round_trip_over_tcp ... ok</span></div></div><div><div><div>12</div></div><div><span>test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后看一次真实的并发运行。实验 <code>ch14_server_demo.rs</code> 在一个进程里起这个服务器，用 <code>tokio::join!</code> 同时发四个请求（一个 <code>fib(24)</code>、一个赋值、一个 <code>print</code>、一个有语法错误的），再打印 <code>Stats</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_server_demo</span></div></div><div><div><div>2</div></div><div><span>fib(24)  -&gt; 46368</span></div></div><div><div><div>3</div></div><div><span>x        -&gt; 42</span></div></div><div><div><div>4</div></div><div><span>print    -&gt; "hello\n1"</span></div></div><div><div><div>5</div></div><div><span>syntax   -&gt; error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>6</div></div><div><span>Stats { connections: 4, requests: 4, errors: 1 }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个连接、四个请求、一个错误（语法错误那个）。<code>print("hello"); 1;</code> 的响应是 <code>"hello\n1"</code>：<code>print</code> 的输出和最终值用换行拼在一起，这就是 <code>evaluate</code> 里的 <code>parts.join("\n")</code>。</p></section><section><h3>实验用的 crate<a href="#实验用的-crate"><span>#</span></a></h3><p>这一篇后半的实验都在 <code>~/lab-ch14-tokio</code> 里，<code>Cargo.toml</code> 是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"lab-ch14-tokio"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>7</div></div><div><span>tokio</span><span> = { </span><span>version</span><span> = </span><span>"1"</span><span>, </span><span>features</span><span> = [</span><span>"rt-multi-thread"</span><span>, </span><span>"net"</span><span>, </span><span>"io-util"</span><span>, </span><span>"macros"</span><span>, </span><span>"sync"</span><span>, </span><span>"time"</span><span>, </span><span>"signal"</span><span>] }</span></div></div><div><div><div>8</div></div><div><span>pebble-interp</span><span> = { </span><span>path</span><span> = </span><span>"/tmp/pebble-rebuild-14/crates/pebble-interp"</span><span> }</span></div></div><div><div><div>9</div></div><div><span>pebble-net</span><span> = { </span><span>path</span><span> = </span><span>"/tmp/pebble-rebuild-14/crates/pebble-net"</span><span> }</span></div></div><div><div><div>10</div></div><div><span>thiserror</span><span> = </span><span>"2"</span></div></div><div><div><div>11</div></div><div><span>anyhow</span><span> = </span><span>"1"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个 <code>pebble-*</code> 依赖指向我的重建目录，换成你自己的路径。（仓库里 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/Cargo.toml" target="_blank">lab/lab-ch14-tokio/Cargo.toml</a> 的写法是 <code>path = "../../crates/pebble-interp"</code>，指向 tag 里的 crate。）每个实验是 <code>src/bin/ch14_&lt;主题&gt;.rs</code>，用 <code>cargo run --quiet --bin ch14_&lt;主题&gt;</code> 运行（编译失败的演示用 <code>cargo build --bin</code>）。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在重建目录里：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span><span> </span><span>-p</span><span> </span><span>pebble-net</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span><span> </span><span>-p</span><span> </span><span>pebble-net</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一条命令的输出（我用 <code>grep</code> 只留了 <code>Running</code> 和结果行）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>Running unittests src/main.rs (target/debug/deps/pebble-f848d73f4f46c5f3)</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running tests/cli.rs (target/debug/deps/cli-8a92629c34332c03)</span></div></div><div><div><div>4</div></div><div><span>test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_net-6babef61d7c70edc)</span></div></div><div><div><div>6</div></div><div><span>test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>Doc-tests pebble_net</span></div></div><div><div><div>8</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>命令行：3 个单元测试（<code>repl.rs</code> 里的，第 9 篇），12 个集成测试（比第 12 篇多了本篇的两个：<code>check_reports_only_the_broken_file</code> 和 <code>check_requires_a_file</code>）。<code>pebble-net</code>：9 个测试，没有文档测试。第二条命令（clippy，<code>-D warnings</code>）和第三条（<code>cargo fmt --all --check</code>，退出码 0）在我的重建目录里都通过，没有输出任何警告。</p><p>然后手动跑一遍这一篇的新行为：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span><span> </span><span>--</span><span> </span><span>check</span><span> </span><span>examples/fib.pebble</span><span> </span><span>examples/errors.pebble</span><span> </span><span>examples/hello.pebble</span><span>; </span><span>echo</span><span> </span><span>"exit=</span><span>$?</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该只看到 <code>examples/errors.pebble</code> 的诊断和 <code>exit=1</code>，上面的”真实运行”一节里有完整输出。</p><p>这些文件现在必须和 tag 逐字节一致。把 tag 克隆到 <code>~/pebble-tag</code>（第 1 篇做过就跳过：<code>git clone --branch tutorial-v1 https://github.com/bfmhno3/pebble ~/pebble-tag</code>），然后在你的工作区根目录里：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>-r</span><span> </span><span>crates/pebble-net</span><span> </span><span>~/pebble-tag/crates/pebble-net</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有任何输出。这两个文件是”完成品”：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-net/Cargo.toml" target="_blank"><code>crates/pebble-net/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-net/src/lib.rs" target="_blank"><code>crates/pebble-net/src/lib.rs</code></a></li>
</ul><p>根目录的 <code>Cargo.toml</code> 现在还不用和 tag 一致（它在第 15 篇才完成），只要有 <code>pebble-net</code> 和 <code>tokio</code> 这两项。</p><p>中间状态的文件（命令行第五版，编译、测试都通过，但还没到最终版）：</p><ul>
<li><code>crates/pebble-cli/</code> 下的全部八个文件（<code>Cargo.toml</code>、<code>src/</code> 下六个、<code>tests/cli.rs</code>）：第五版。相对第四版只改了这几处：<code>cli.rs</code> 多一个 <code>CheckArgs</code>、<code>Check</code> 变体改用它；<code>commands.rs</code> 的 <code>check</code> 重写，多出 <code>CheckReport</code> 和 <code>check_file</code>，多两行 <code>use</code>；<code>tests/cli.rs</code> 多两个测试；<code>main.rs</code>、<code>backend.rs</code>、<code>repl.rs</code>、<code>sources.rs</code>、<code>Cargo.toml</code> 一个字都没动。最终版多出来的东西：可选的 <code>repl</code> 特性（第 15 篇）。</li>
<li><code>examples/*.pebble</code>：和第 12 篇相同，没有变。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的所有实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank">lab/rust-lab</a> 是主实验室）。按第 1 篇的方式把 tag 克隆到 <code>~/pebble-tag</code> 后，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，在那里运行（<code>cd ~/pebble-tag/lab/rust-lab</code>，也可以把这个目录复制成 <code>~/rust-lab</code>）。表里 <code>lab-</code> 开头的文件属于旁边的独立实验 crate，命令里写了完整的目录。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_race.rs" target="_blank"><code>ch14_race.rs</code></a></td><td><code>cargo build --bin ch14_race</code></td><td>编译失败：<code>E0499</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_spawn.rs" target="_blank"><code>ch14_spawn.rs</code></a></td><td><code>cargo build --bin ch14_spawn</code></td><td>编译失败：<code>E0373</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_spawn_move.rs" target="_blank"><code>ch14_spawn_move.rs</code></a></td><td><code>cargo run --quiet --bin ch14_spawn_move</code></td><td>运行成功：<code>checking 2 files</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_scope.rs" target="_blank"><code>ch14_scope.rs</code></a></td><td><code>cargo run --quiet --bin ch14_scope</code></td><td>运行成功：<code>[10, 12]</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_rc_send.rs" target="_blank"><code>ch14_rc_send.rs</code></a></td><td><code>cargo build --bin ch14_rc_send</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_cell_sync.rs" target="_blank"><code>ch14_cell_sync.rs</code></a></td><td><code>cargo build --bin ch14_cell_sync</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_send_sync_table.rs" target="_blank"><code>ch14_send_sync_table.rs</code></a></td><td><code>cargo build --bin ch14_send_sync_table</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_arc_mutex.rs" target="_blank"><code>ch14_arc_mutex.rs</code></a></td><td><code>cargo run --quiet --bin ch14_arc_mutex</code></td><td>运行成功：<code>count = 8000</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_mutex_forgot_lock.rs" target="_blank"><code>ch14_mutex_forgot_lock.rs</code></a></td><td><code>cargo build --bin ch14_mutex_forgot_lock</code></td><td>编译失败：<code>E0308</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_poison.rs" target="_blank"><code>ch14_poison.rs</code></a></td><td><code>cargo run --quiet --bin ch14_poison</code></td><td>运行成功：<code>is_poisoned = true; stderr: thread panicked at src/bin/ch14_poison.rs:12:13:</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_rwlock.rs" target="_blank"><code>ch14_rwlock.rs</code></a></td><td><code>cargo run --quiet --bin ch14_rwlock</code></td><td>运行成功：<code>every reader saw 3 or 4 elements: true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_channel.rs" target="_blank"><code>ch14_channel.rs</code></a></td><td><code>cargo run --quiet --bin ch14_channel</code></td><td>运行成功：<code>checked errors.pebble</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_atomic.rs" target="_blank"><code>ch14_atomic.rs</code></a></td><td><code>cargo run --quiet --bin ch14_atomic</code></td><td>运行成功：<code>requests = 8000</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_lazy_join.rs" target="_blank"><code>ch14_lazy_join.rs</code></a></td><td><code>cargo run --quiet --bin ch14_lazy_join</code></td><td>运行成功：<code>chained spawn+join:    [0, 1, 2, 3] in ~400 ms</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_many_tasks.rs" target="_blank"><code>lab-ch14-tokio/ch14_many_tasks.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_many_tasks</code></td><td>运行成功：<code>100000 tasks, sum 4999950000, took ~600 ms</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_many_threads.rs" target="_blank"><code>lab-ch14-tokio/ch14_many_threads.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_many_threads</code></td><td>运行成功：<code>10000 threads, sum 49995000, took ~700 ms</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_join.rs" target="_blank"><code>lab-ch14-tokio/ch14_join.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_join</code></td><td>运行成功：<code>checked fib, checked hello: sequential took 400 ms</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_manual_future.rs" target="_blank"><code>ch14_manual_future.rs</code></a></td><td><code>cargo run --quiet --bin ch14_manual_future</code></td><td>运行成功：<code>countdown:</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch14_pin_new.rs" target="_blank"><code>ch14_pin_new.rs</code></a></td><td><code>cargo build --bin ch14_pin_new</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_block_the_runtime.rs" target="_blank"><code>lab-ch14-tokio/ch14_block_the_runtime.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_block_the_runtime</code></td><td>运行成功：<code>blocking the only worker thread for 500 ms</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_blocking_ok.rs" target="_blank"><code>lab-ch14-tokio/ch14_blocking_ok.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_blocking_ok</code></td><td>运行成功：<code>blocking on a dedicated thread for 500 ms</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_rc_across_await.rs" target="_blank"><code>lab-ch14-tokio/ch14_rc_across_await.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo build --bin ch14_rc_across_await</code></td><td>编译失败：<code>error: future cannot be sent between threads safely</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_interp_across_await.rs" target="_blank"><code>lab-ch14-tokio/ch14_interp_across_await.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo build --bin ch14_interp_across_await</code></td><td>编译失败：<code>error: future cannot be sent between threads safely</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_std_mutex_await.rs" target="_blank"><code>lab-ch14-tokio/ch14_std_mutex_await.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo build --bin ch14_std_mutex_await</code></td><td>编译失败：<code>error: future cannot be sent between threads safely</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_wire.rs" target="_blank"><code>lab-ch14-tokio/ch14_wire.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_wire</code></td><td>运行成功：<code>request  : 00 00 00 0a 31 20 2b 20 32 20 2a 20 33 3b</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_naive_frame.rs" target="_blank"><code>lab-ch14-tokio/ch14_naive_frame.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_naive_frame</code></td><td>运行成功：<code>kind    : InvalidData</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_error_chain.rs" target="_blank"><code>lab-ch14-tokio/ch14_error_chain.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_error_chain</code></td><td>运行成功：<code>display : frame payload is not valid UTF-8</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_non_exhaustive.rs" target="_blank"><code>lab-ch14-tokio/ch14_non_exhaustive.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo build --bin ch14_non_exhaustive</code></td><td>编译失败：<code>E0004</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_thiserror.rs" target="_blank"><code>lab-ch14-tokio/ch14_thiserror.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_thiserror</code></td><td>运行成功：<code>frame length 1024 exceeds maximum frame size 16</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_select_cancel.rs" target="_blank"><code>lab-ch14-tokio/ch14_select_cancel.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_select_cancel</code></td><td>运行成功：<code>slow's local dropped</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_timeout_blocking.rs" target="_blank"><code>lab-ch14-tokio/ch14_timeout_blocking.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_timeout_blocking</code></td><td>运行成功：<code>timeout fired: true (after ~200 ms)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_server_demo.rs" target="_blank"><code>lab-ch14-tokio/ch14_server_demo.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_server_demo</code></td><td>运行成功：<code>fib(24)  -&gt; 46368</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_cooperative_cancel.rs" target="_blank"><code>lab-ch14-tokio/ch14_cooperative_cancel.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_cooperative_cancel</code></td><td>运行成功：<code>timeout fired, flag set (~200 ms)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch14-tokio/src/bin/ch14_local_set.rs" target="_blank"><code>lab-ch14-tokio/ch14_local_set.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch14-tokio &amp;&amp; cargo run --quiet --bin ch14_local_set</code></td><td>运行成功：<code>an Rc across an await: 5</code></td></tr></tbody></table><p>两个计时实验（<code>ch14_many_tasks</code>、<code>ch14_many_threads</code>）的毫秒数是约值，随机器和构建方式（debug 或 release）变化；表里是 debug 构建、默认参数下的结果（<code>ch14_many_threads</code> 默认只启动 1 万个线程）；文章 10 万个线程的 <code>~3900 ms</code> 是 release 构建、命令行带 <code>100000</code> 量的，机器噪声也会让数字有出入。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>练习 1：</strong> 请求超时之后，服务器给客户端回了 <code>error: request timed out</code>，但那个阻塞线程还在继续跑。怎么才能真的让它停下来？</p>
参考答案<p>没有办法从外面把线程停下来：标准库没有 <code>kill_thread</code>，因为强行终止会跳过 <code>Drop</code>，让持着锁的数据停在一个中间状态。唯一的办法是被取消的代码自己配合：它得时不时检查一个标志。实验 <code>ch14_cooperative_cancel.rs</code> 在上面 <code>ch14_timeout_blocking</code> 的基础上给阻塞任务一个 <code>Arc&lt;AtomicBool&gt;</code>，循环里每 10 毫秒检查一次；<code>timeout</code> 触发之后，主任务把它设成 <code>true</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_cooperative_cancel</span></div></div><div><div><div>2</div></div><div><span>timeout fired, flag set (~200 ms)</span></div></div><div><div><div>3</div></div><div><span>the job noticed the flag after 20 steps and stopped</span></div></div><div><div><div>4</div></div><div><span>runtime dropped at ~200 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对比 <code>ch14_timeout_blocking</code> 里 <code>runtime dropped at ~2000 ms</code>：这次运行时在 200 毫秒就能退出。对 Pebble 来说，这意味着解释器需要一个钩子：<code>execute_stmt</code> 和 <code>while</code> 的每一圈检查一次这个标志，类似第 9 篇的 <code>with_call_depth</code> 检查调用深度的做法。解释器现在没有这样的钩子，我没有加它。</p><p><strong>练习 2：</strong> 用原子类型替换 <code>Stats</code> 外面的 <code>Arc&lt;Mutex&lt;Stats&gt;&gt;</code>，让 <code>handle_connection</code> 里不再有 <code>.lock().await</code>。</p>
参考答案<p>字段换成 <code>AtomicU64</code>，外面只需要 <code>Arc&lt;Stats&gt;</code>（原子类型自己就能通过 <code>&amp;</code> 修改）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>sync</span><span>::</span><span>atomic</span><span>::{</span></span><span>AtomicU64</span><span>, </span><span>Ordering</span><span>};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[derive(</span><span>Default</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Stats</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>connections</span><span>: </span><span>AtomicU64</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>requests</span><span>: </span><span>AtomicU64</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>errors</span><span>: </span><span>AtomicU64</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>use tokio::sync::Mutex;</code> 删掉；<code>serve_listener</code> 里 <code>Arc::new(Mutex::new(Stats::default()))</code> 变成 <code>Arc::new(Stats::default())</code>；<code>handle_connection</code> 的签名参数 <code>Arc&lt;Mutex&lt;Stats&gt;&gt;</code> 变成 <code>Arc&lt;Stats&gt;</code>；每一处 <code>stats.lock().await.errors += 1;</code> 变成 <code>stats.errors.fetch_add(1, Ordering::Relaxed);</code>，<code>connections</code> 和 <code>requests</code> 同理；测试里 <code>stats.lock().await.requests</code> 变成 <code>stats.requests.load(Ordering::Relaxed)</code>，<code>Arc::new(Mutex::new(Stats::default()))</code> 变成 <code>Arc::new(Stats::default())</code>。我在一份拷贝里这样改完，<code>cargo test -p pebble-net</code> 是 9 个测试全部通过。</p><p>用 <code>Relaxed</code> 够了，因为三个计数器彼此独立，没有哪个的值被用来保护别的数据（第 13 篇的解释）。代价是读三个计数器不再是一个原子的快照：你可能读到 <code>requests</code> 已经加了而 <code>errors</code> 还没加。<code>Stats</code> 是公开的字段，类型从 <code>u64</code> 变成 <code>AtomicU64</code>，对 crate 的使用者是一个破坏性变更，这也是仓库里没有这样改的原因之一。</p><p><strong>练习 3：</strong> 为什么 <code>handle_connection</code> 返回的 <code>Future</code> 必须是 <code>Send</code>？如果我在里面写一个跨 <code>.await</code> 的 <code>Rc</code>，编译器会在哪里报错？</p>
参考答案<p>因为 <code>serve_listener</code> 用 <code>tokio::spawn</code> 启动每个连接任务，而 <code>spawn</code> 要求 <code>Future + Send + 'static</code>：多线程运行时可以在两次 <code>poll</code> 之间把任务换到另一个线程上。我在一份拷贝里的 <code>handle_connection</code> 里加了一行 <code>let _unsend = std::rc::Rc::new(0);</code>，放在 <code>stats.requests</code> 那一行之前（也就是 <code>stats.lock().await.requests += 1;</code> 之前）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: future cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-net/src/lib.rs:354:17</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>354 | /                 tokio::spawn(async move {</span></div></div><div><div><div>5</div></div><div><span>355 | |                     let _ = handle_connection(stream, config, stats).await;</span></div></div><div><div><div>6</div></div><div><span>356 | |                 });</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>| |__________________^ future created by async block is not `Send`</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>= help: within `{async block@crates/pebble-net/src/lib.rs:354:30: 354:40}`, the trait `Send` is not implemented for `Rc&lt;i32&gt;`</span></div></div><div><div><div>10</div></div><div><span>note: future is not `Send` as this value is used across an await</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-net/src/lib.rs:396:22</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>395 |         let _unsend = std::rc::Rc::new(0);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>|             ------- has type `Rc&lt;i32&gt;` which is not `Send`</span></div></div><div><div><div>15</div></div><div><span>396 |         stats.lock().await.requests += 1;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>|                      ^^^^^ await occurs here, with `_unsend` maybe used later</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>错误报在 <code>serve_listener</code> 里的 <code>tokio::spawn</code> 那一行，不是 <code>handle_connection</code> 里：<code>handle_connection</code> 本身是一个合法的 <code>async fn</code>，它不知道将来谁会 <code>spawn</code> 它。<code>Send</code> 的要求沿着调用链一路传到了 <code>spawn</code>。</p><p>如果真的需要在任务里放 <code>!Send</code> 的东西，有一个出口：<code>LocalSet</code> 加 <code>spawn_local</code>，它们把任务固定在创建它们的那一个线程上，所以不要求 <code>Send</code>。实验 <code>ch14_local_set.rs</code> 在 <code>current_thread</code> 运行时里让一个 <code>Rc</code> 跨了 <code>.await</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch14_local_set</span></div></div><div><div><div>2</div></div><div><span>an Rc across an await: 5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>代价是这些任务没法在线程之间搬运，吞吐量上限就是一个线程。</p></section>
<section><h2>为什么 Pebble 的解释器还是 <code>Rc</code><a href="#为什么-pebble-的解释器还是-rc"><span>#</span></a></h2><p>写这一篇之前我有一个担心：把线程和 async 都学完，会不会觉得第 8 篇选 <code>Rc</code> 是个错误，应该一开始就用 <code>Arc&lt;Mutex&lt;_&gt;&gt;</code>？做完所有的实验之后我的结论正相反。<code>pebble check</code> 的并行没有要求解释器是 <code>Send</code>：前端是 <code>Send</code>，把所有后端的东西留在线程里面创建、线程里面丢弃就够了。<code>pebble-net</code> 的并发也没有要求：解释器只活在 <code>spawn_blocking</code> 的闭包里。<code>Rc</code> 的计数是普通的加减，<code>Arc</code> 的计数是原子指令，这个差别在一个频繁克隆 <code>Value</code> 的解释器里会被放大；我没有量过放大多少，所以不给数字。</p><p>不过这也是一条路的终点：如果有一天 Pebble 想让两个线程共享同一个全局环境，就必须把整个 <code>Value</code> 换成 <code>Arc&lt;Mutex&lt;_&gt;&gt;</code>（或者重新设计成消息传递），那会是一次系统性的重写，编译器会逐个指出每一处要改的地方（上面 <code>send_probe</code> 的七条错误只是开头）。我的看法是，现在不值得。</p><p>下一篇是最后一篇，也是最没有新语法的一篇：Cargo 特性怎么让 <code>pebble repl</code> 变成可选的，单元测试、集成测试、文档测试怎么分工，<code>clippy</code> 和 <code>rustfmt</code> 怎么把风格交给机器，CI 配置怎么把所有这些检查在每次提交时跑一遍。然后我们把 <code>/tmp/pebble-rebuild</code> 和 tag 做最后一次对比，列一张”Rust 特性，第几篇”的索引表，看看我们到底学了多少。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-13-ffi-nostd/</id>
      <title type="text">越过边界：FFI 调 C 与 no_std</title>
      <published>2026-10-13T12:00:00.000Z</published>
      <updated>2026-10-13T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-13-ffi-nostd/"/>
      <summary type="text">Pebble 的最后两条边界：向下调用一个手写的 C 库，再向下脱掉操作系统。写 pebble-sys（build.rs、unsafe extern、Drop、catch_unwind）和 pebble-nostd（no_std、原子操作、GlobalAlloc、const 泛型），再用实验补上 repr、union、cdylib。</summary>
      <content type="html"><![CDATA[<p>到上一篇为止，Pebble 里每一个字节都归 Rust 管：<code>String</code> 归 <code>Rc</code>，<code>Rc</code> 归编译器，编译器说了算。这一篇我们把边界从两头戳穿。向下，调用一个用 C 写的库（一个 FNV-1a 哈希、一个 arena 分配器、一个接收回调的 <code>fold</code>，总共两百来行的 C99）；再向下，连操作系统也不要了，让词法分析器和一个整数求值器跑在我们自己写的分配器上，对 <code>(1 + 2) * 3 - 4 / 2</code> 求值总共只向这个分配器要了 2000 字节。边界上的一切都是 <code>unsafe</code> 的，所以这篇的主题其实只有一个：怎么把 <code>unsafe</code> 关进一个小笼子里，让笼子外面的人不需要知道它存在。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>做完这一篇，你会有 <code>pebble-sys</code>（<code>build.rs</code>、C 源码、<code>fnv1a</code>、<code>Arena</code>、<code>fold</code>）和 <code>pebble-nostd</code>（<code>no_std</code> 的求值器加一个 const 泛型的 bump 分配器），两个 crate 都和 tag <code>tutorial-v1</code> 逐字节一致，<code>cargo test -p pebble-sys -p pebble-nostd</code> 通过。会用到的 Rust 特性：<code>unsafe extern</code>、<code>repr(C)</code>、<code>NonNull</code>、<code>Drop</code>、<code>Send</code>/<code>Sync</code> 与 <code>PhantomData</code>、<code>catch_unwind</code>、<code>UnsafeCell</code>、原子操作、<code>GlobalAlloc</code>、const 泛型。上一篇在 <a href="/posts/rust-by-pebble-12-vm-backends/">栈式虚拟机</a>，VM 自己是 <code>forbid(unsafe_code)</code> 的，<code>unsafe</code> 上一次出现是第 11 篇的 GC；这一篇的 <code>unsafe</code> 全是跨边界的。</p></div></div>
<section><h2>先把工作区补齐<a href="#先把工作区补齐"><span>#</span></a></h2><p>这两个 crate 依赖前面几篇的成果：<code>pebble-nostd</code> 用的是第 4 篇写的词法分析器，词法分析器又依赖第 3 篇的 <code>pebble-diag</code> 和第 1、2 篇的 <code>pebble-span</code>。如果你是一路跟下来的，这三个 crate 已经在 <code>crates/</code> 下了；如果你是直接跳到这一篇，把它们从 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1" target="_blank">tag</a> 的 <code>crates/pebble-span</code>、<code>crates/pebble-diag</code>、<code>crates/pebble-lexer</code> 整个目录复制过来即可。</p><p>根 <code>Cargo.toml</code> 需要认识两个新 crate 和一个新的外部依赖。在 <code>[workspace.dependencies]</code> 里加三行（路径依赖的写法第 1 篇讲过）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-sys</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-sys"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>2</div></div><div><span>pebble-nostd</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-nostd"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>cc</span><span> = </span><span>"1"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cc</code> 是调用 C 编译器的库，一会儿 <code>build.rs</code> 要用它。然后建目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-sys</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-nostd</span></div></div><div><div><div>3</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>crates/pebble-sys/csrc/include</span><span> </span><span>crates/pebble-nostd/examples</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>C 这一半<a href="#c-这一半"><span>#</span></a></h2><p>FFI 的第一课和 Rust 无关：你得先有一份 C 代码。我们不依赖系统里已有的库（它们在每台机器上长得不一样），而是自己写一个小的，随 crate 一起发布。先写头文件 <code>crates/pebble-sys/csrc/include/pebble_shim.h</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* pebble_shim.h - hand-written C99 shims linked into the Pebble runtime.</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>* Everything in this header has a safe Rust wrapper in `pebble-sys`'s lib.rs.</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>* The ABI is plain C: integers by value, pointers by address.</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>6</div></div><div><span>#ifndef</span><span> </span><span>PEBBLE_SHIM_H</span></div></div><div><div><div>7</div></div><div><span>#define</span><span> </span><span>PEBBLE_SHIM_H</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#include</span><span> </span><span>&lt;stddef.h&gt;</span></div></div><div><div><div>10</div></div><div><span>#include</span><span> </span><span>&lt;stdint.h&gt;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#ifdef</span><span> </span><span>__cplusplus</span></div></div><div><div><div>13</div></div><div><span>extern</span><span> </span><span>"C"</span><span> {</span></div></div><div><div><div>14</div></div><div><span>#endif</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>/* FNV-1a, 64-bit variant (offset basis 0xcbf29ce484222325, prime 0x100000001b3).</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>* Reads `len` bytes through `data` and returns the resulting digest. `data` may</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>* be NULL when `len` is 0. The function keeps no reference to `data`.</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>21</div></div><div><span>uint64_t</span><span> </span><span>pebble_fnv1a</span><span>(</span><span>const</span><span> </span><span>uint8_t</span><span> </span><span>*</span><span><span>data</span><span>,</span><span> </span></span><span>size_t</span><span><span> </span><span>len</span><span>);</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>/* A bump (arena) allocator over a single upfront allocation.</span></div></div><div><div><div>24</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>* The struct is opaque: callers only ever hold a `PebbleArena *`. Every</span></div></div><div><div><div>26</div></div><div><span><span> </span></span><span>* function tolerates a NULL arena. Arenas are not thread-safe.</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>28</div></div><div><span>typedef</span><span> </span><span>struct</span><span> PebbleArena PebbleArena;</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>/* Creates an arena with room for `capacity` bytes.</span></div></div><div><div><div>31</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>32</div></div><div><span><span> </span></span><span>* Returns NULL if memory cannot be obtained. A capacity of 0 is allowed: the</span></div></div><div><div><div>33</div></div><div><span><span> </span></span><span>* arena exists, `pebble_arena_used` stays 0 and every `pebble_arena_alloc`</span></div></div><div><div><div>34</div></div><div><span><span> </span></span><span>* fails. Release the result with `pebble_arena_destroy`.</span></div></div><div><div><div>35</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>36</div></div><div><span>PebbleArena </span><span>*</span><span>pebble_arena_create</span><span>(</span><span>size_t</span><span><span> </span><span>capacity</span><span>);</span></span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>/* Bump-allocates `len` uninitialised bytes.</span></div></div><div><div><div>39</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>40</div></div><div><span><span> </span></span><span>* The returned region is aligned to the widest scalar alignment the platform</span></div></div><div><div><div>41</div></div><div><span><span> </span></span><span>* requires (16 bytes on x86-64) and</span></div></div><div><div><div>42</div></div><div><span><span> </span></span><span>* never overlaps a region handed out by a previous call that has not been</span></div></div><div><div><div>43</div></div><div><span><span> </span></span><span>* invalidated by `pebble_arena_reset`. Contents are unspecified.</span></div></div><div><div><div>44</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>45</div></div><div><span><span> </span></span><span>* Returns NULL when the arena is NULL, when the arena has no backing storage</span></div></div><div><div><div>46</div></div><div><span><span> </span></span><span>* (capacity 0) or when `len` bytes plus alignment padding do not fit in the</span></div></div><div><div><div>47</div></div><div><span><span> </span></span><span>* remaining capacity.</span></div></div><div><div><div>48</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>49</div></div><div><span>void</span><span> </span><span>*</span><span>pebble_arena_alloc</span><span><span>(</span><span>PebbleArena </span></span><span>*</span><span><span>arena</span><span>,</span><span> </span></span><span>size_t</span><span><span> </span><span>len</span><span>);</span></span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>/* Bytes handed out so far, including alignment padding between allocations.</span></div></div><div><div><div>52</div></div><div><span><span> </span></span><span>* Returns 0 for a NULL arena. */</span></div></div><div><div><div>53</div></div><div><span>size_t</span><span> </span><span>pebble_arena_used</span><span>(</span><span>const</span><span> PebbleArena </span><span>*</span><span><span>arena</span><span>);</span></span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>/* Total number of bytes the arena can hand out. Returns 0 for a NULL arena. */</span></div></div><div><div><div>56</div></div><div><span>size_t</span><span> </span><span>pebble_arena_capacity</span><span>(</span><span>const</span><span> PebbleArena </span><span>*</span><span><span>arena</span><span>);</span></span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>/* Rewinds the arena so all of its capacity becomes available again.</span></div></div><div><div><div>59</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>60</div></div><div><span><span> </span></span><span>* Every pointer previously returned by `pebble_arena_alloc` is invalidated by</span></div></div><div><div><div>61</div></div><div><span><span> </span></span><span>* this call: using one afterwards is undefined behaviour. No-op for NULL.</span></div></div><div><div><div>62</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>63</div></div><div><span>void</span><span> </span><span>pebble_arena_reset</span><span><span>(</span><span>PebbleArena </span></span><span>*</span><span><span>arena</span><span>);</span></span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>/* Releases the arena and its backing storage. No-op for NULL. Afterwards the</span></div></div><div><div><div>66</div></div><div><span><span> </span></span><span>* arena pointer and every pointer it handed out are dangling. */</span></div></div><div><div><div>67</div></div><div><span>void</span><span> </span><span>pebble_arena_destroy</span><span><span>(</span><span>PebbleArena </span></span><span>*</span><span><span>arena</span><span>);</span></span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span>/* Callback signature used by `pebble_fold`. `ctx` is the opaque pointer the</span></div></div><div><div><div>70</div></div><div><span><span> </span></span><span>* caller passed to `pebble_fold`; it may be NULL. */</span></div></div><div><div><div>71</div></div><div><span>typedef</span><span> </span><span>int64_t</span><span> (</span><span>*</span><span>pebble_fold_fn)(</span><span>int64_t</span><span> acc, </span><span>int64_t</span><span> value, </span><span>void</span><span> </span><span>*</span><span>ctx);</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>/* Folds the integers in the inclusive range [lo, hi].</span></div></div><div><div><div>74</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>75</div></div><div><span><span> </span></span><span>* Calls `cb(acc, value, ctx)` once per integer, in ascending order, threading</span></div></div><div><div><div>76</div></div><div><span><span> </span></span><span>* `init` as the initial accumulator and the callback's result as the next</span></div></div><div><div><div>77</div></div><div><span><span> </span></span><span>* accumulator. Returns the final accumulator, or `init` when `lo &gt; hi`.</span></div></div><div><div><div>78</div></div><div><span><span> </span></span><span>*</span></div></div><div><div><div>79</div></div><div><span><span> </span></span><span>* `cb` must not be NULL. `pebble_fold` does not catch anything: how a callback</span></div></div><div><div><div>80</div></div><div><span><span> </span></span><span>* failure is handled is up to the callback.</span></div></div><div><div><div>81</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>82</div></div><div><span>int64_t</span><span> </span><span>pebble_fold</span><span>(</span><span>int64_t</span><span><span> </span><span>init</span><span>,</span><span> </span></span><span>int64_t</span><span><span> </span><span>lo</span><span>,</span><span> </span></span><span>int64_t</span><span><span> </span><span>hi</span><span>,</span><span> pebble_fold_fn </span><span>cb</span><span>,</span><span> </span></span><span>void</span><span> </span><span>*</span><span><span>ctx</span><span>);</span></span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>#ifdef</span><span> </span><span>__cplusplus</span></div></div><div><div><div>85</div></div><div><span>}</span></div></div><div><div><div>86</div></div><div><span>#endif</span></div></div><div><div><div>87</div></div><div>
</div></div><div><div><div>88</div></div><div><span>#endif</span><span> /* PEBBLE_SHIM_H */</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这个头文件里有三件事值得看。</p><p>第一，<code>#ifdef __cplusplus</code> 那一对 <code>extern "C"</code>：C++ 会对函数名做修饰（name mangling），不加这个，C++ 调用方找不到我们的符号。这是 C 世界里最古老的 FFI 约定：函数名就是符号名，参数按 System V ABI 放进寄存器，没有重载，没有异常。Rust 的 <code>extern "C"</code> 选的就是这个 ABI，所以它能和 C 对话，也能和任何会说 C ABI 的语言对话。</p><p>第二，<code>typedef struct PebbleArena PebbleArena;</code> 只声明不定义：这是 C 的不透明类型（opaque type）惯用法，调用者只能持有指针，不知道里面有什么。后面你会看到 Rust 怎么对应这件事。</p><p>第三，<code>pebble_fold_fn</code> 的最后一个参数 <code>void *ctx</code>：C 没有闭包，所以每个接受回调的 C API 都会让你顺便传一个 <code>void *</code>，回调时原样还给你。<code>qsort_r</code>、<code>pthread_create</code>、<code>sigaction</code> 都是这个套路。第 9 篇我们说 Rust 的闭包是”函数加捕获的环境”，这里 C 的做法是把这两样分开传。</p><p>再写实现 <code>crates/pebble-sys/csrc/pebble_shim.c</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* pebble_shim.c - implementation of the C half of `pebble-sys`. Portable C99. */</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>"pebble_shim.h"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;stdlib.h&gt;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/* --- FNV-1a ----------------------------------------------------------------- */</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#define</span><span> </span><span>PEBBLE_FNV_OFFSET_BASIS</span><span> </span><span>UINT64_C</span><span>(</span><span><span>0x</span><span>cbf29ce484222325</span></span><span>)</span></div></div><div><div><div>10</div></div><div><span>#define</span><span> </span><span>PEBBLE_FNV_PRIME</span><span> </span><span>UINT64_C</span><span>(</span><span><span>0x</span><span>100000001b3</span></span><span>)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>uint64_t</span><span> </span><span>pebble_fnv1a</span><span>(</span><span>const</span><span> </span><span>uint8_t</span><span> </span><span>*</span><span><span>data</span><span>,</span><span> </span></span><span>size_t</span><span><span> </span><span>len</span><span>) {</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>uint64_t</span><span> hash </span><span>=</span><span> PEBBLE_FNV_OFFSET_BASIS;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>size_t</span><span> i;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> len; i</span><span>++</span><span>) {</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>hash </span><span>^=</span><span> (</span><span>uint64_t</span><span>)</span><span>data</span><span>[i];</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>hash </span><span>*=</span><span> PEBBLE_FNV_PRIME;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span> hash;</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>/* --- Arena ------------------------------------------------------------------ */</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>/* C99 has no max_align_t, so probe the platform's widest scalar alignment with</span></div></div><div><div><div>26</div></div><div><span><span> </span></span><span>* a union instead. Its size is a multiple of that alignment (16 on x86-64). */</span></div></div><div><div><div>27</div></div><div><span>typedef</span><span> </span><span>union</span><span> pebble_align_probe {</span></div></div><div><div><div>28</div></div><div><span>    </span><span>long</span><span> </span><span>double</span><span> ld;</span></div></div><div><div><div>29</div></div><div><span>    </span><span>void</span><span> </span><span>*</span><span>ptr;</span></div></div><div><div><div>30</div></div><div><span>    </span><span>int64_t</span><span> i64;</span></div></div><div><div><div>31</div></div><div><span>    </span><span>size_t</span><span> sz;</span></div></div><div><div><div>32</div></div><div><span>} pebble_align_probe;</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>#define</span><span> </span><span>PEBBLE_ALIGNMENT</span><span> ((</span><span>size_t</span><span>)</span><span>sizeof</span><span>(pebble_align_probe))</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>struct</span><span> PebbleArena {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>unsigned</span><span> </span><span>char</span><span> </span><span>*</span><span>base;</span></div></div><div><div><div>38</div></div><div><span>    </span><span>size_t</span><span> capacity;</span></div></div><div><div><div>39</div></div><div><span>    </span><span>size_t</span><span> used;</span></div></div><div><div><div>40</div></div><div><span>};</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>/* Rounds `n` up to the next multiple of PEBBLE_ALIGNMENT. Overflow is handled</span></div></div><div><div><div>43</div></div><div><span><span> </span></span><span>* by the capacity check in pebble_arena_alloc. */</span></div></div><div><div><div>44</div></div><div><span>static</span><span> </span><span>size_t</span><span> </span><span>pebble_align_up</span><span>(</span><span>size_t</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>45</div></div><div><span>    </span><span>size_t</span><span> rem </span><span>=</span><span> n </span><span>%</span><span> PEBBLE_ALIGNMENT;</span></div></div><div><div><div>46</div></div><div><span>    </span><span>return</span><span> rem </span><span>==</span><span> </span><span>0</span><span> </span><span>?</span><span> n </span><span>:</span><span> n </span><span>+</span><span> (PEBBLE_ALIGNMENT </span><span>-</span><span> rem);</span></div></div><div><div><div>47</div></div><div><span>}</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>PebbleArena </span><span>*</span><span>pebble_arena_create</span><span>(</span><span>size_t</span><span><span> </span><span>capacity</span><span>) {</span></span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>PebbleArena </span><span>*</span><span>arena </span><span>=</span><span> (PebbleArena </span><span>*</span><span>)</span><span>malloc</span><span>(</span><span>sizeof</span><span>(PebbleArena));</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>    </span><span>if</span><span> (arena </span><span>==</span><span> </span><span>NULL</span><span>) {</span></div></div><div><div><div>53</div></div><div><span>        </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>55</div></div><div><span>    </span><span>arena</span><span>-&gt;</span><span>base</span><span> </span><span>=</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>56</div></div><div><span>    </span><span>arena</span><span>-&gt;</span><span>capacity</span><span> </span><span>=</span><span> capacity;</span></div></div><div><div><div>57</div></div><div><span>    </span><span>arena</span><span>-&gt;</span><span>used</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>    </span><span>if</span><span> (capacity </span><span>&gt;</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>60</div></div><div><span>        </span><span>arena</span><span>-&gt;</span><span>base</span><span> </span><span>=</span><span> (</span><span>unsigned</span><span> </span><span>char</span><span> </span><span>*</span><span>)</span><span>malloc</span><span>(capacity);</span></div></div><div><div><div>61</div></div><div><span>        </span><span>if</span><span> (</span><span>arena</span><span>-&gt;</span><span>base</span><span> </span><span>==</span><span> </span><span>NULL</span><span>) {</span></div></div><div><div><div>62</div></div><div><span>            </span><span>free</span><span>(arena);</span></div></div><div><div><div>63</div></div><div><span>            </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>64</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>66</div></div><div><span>    </span><span>return</span><span> arena;</span></div></div><div><div><div>67</div></div><div><span>}</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span>void</span><span> </span><span>*</span><span>pebble_arena_alloc</span><span><span>(</span><span>PebbleArena </span></span><span>*</span><span><span>arena</span><span>,</span><span> </span></span><span>size_t</span><span><span> </span><span>len</span><span>) {</span></span></div></div><div><div><div>70</div></div><div><span>    </span><span>size_t</span><span> start;</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>    </span><span>if</span><span> (arena </span><span>==</span><span> </span><span>NULL</span><span> </span><span>||</span><span> </span><span>arena</span><span>-&gt;</span><span>base</span><span> </span><span>==</span><span> </span><span>NULL</span><span>) {</span></div></div><div><div><div>73</div></div><div><span>        </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>start </span><span>=</span><span> </span><span>pebble_align_up</span><span>(</span><span>arena</span><span>-&gt;</span><span>used</span><span>);</span></div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>/* Both comparisons are overflow-safe: `start &lt;= capacity` is established</span></div></div><div><div><div>77</div></div><div><span><span>     </span></span><span>* before `capacity - start` is computed. */</span></div></div><div><div><div>78</div></div><div><span>    </span><span>if</span><span> (start </span><span>&gt;</span><span> </span><span>arena</span><span>-&gt;</span><span>capacity</span><span> </span><span>||</span><span> len </span><span>&gt;</span><span> </span><span>arena</span><span>-&gt;</span><span>capacity</span><span> </span><span>-</span><span> start) {</span></div></div><div><div><div>79</div></div><div><span>        </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span>    </span><span>arena</span><span>-&gt;</span><span>used</span><span> </span><span>=</span><span> start </span><span>+</span><span> len;</span></div></div><div><div><div>82</div></div><div><span>    </span><span>return</span><span> </span><span>arena</span><span>-&gt;</span><span>base</span><span> </span><span>+</span><span> start;</span></div></div><div><div><div>83</div></div><div><span>}</span></div></div><div><div><div>84</div></div><div>
</div></div><div><div><div>85</div></div><div><span>size_t</span><span> </span><span>pebble_arena_used</span><span>(</span><span>const</span><span> PebbleArena </span><span>*</span><span><span>arena</span><span>) {</span></span></div></div><div><div><div>86</div></div><div><span>    </span><span>return</span><span> arena </span><span>==</span><span> </span><span>NULL</span><span> </span><span>?</span><span> </span><span>0</span><span> </span><span>:</span><span> </span><span>arena</span><span>-&gt;</span><span>used</span><span>;</span></div></div><div><div><div>87</div></div><div><span>}</span></div></div><div><div><div>88</div></div><div>
</div></div><div><div><div>89</div></div><div><span>size_t</span><span> </span><span>pebble_arena_capacity</span><span>(</span><span>const</span><span> PebbleArena </span><span>*</span><span><span>arena</span><span>) {</span></span></div></div><div><div><div>90</div></div><div><span>    </span><span>return</span><span> arena </span><span>==</span><span> </span><span>NULL</span><span> </span><span>?</span><span> </span><span>0</span><span> </span><span>:</span><span> </span><span>arena</span><span>-&gt;</span><span>capacity</span><span>;</span></div></div><div><div><div>91</div></div><div><span>}</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span>void</span><span> </span><span>pebble_arena_reset</span><span><span>(</span><span>PebbleArena </span></span><span>*</span><span><span>arena</span><span>) {</span></span></div></div><div><div><div>94</div></div><div><span>    </span><span>if</span><span> (arena </span><span>!=</span><span> </span><span>NULL</span><span>) {</span></div></div><div><div><div>95</div></div><div><span>        </span><span>arena</span><span>-&gt;</span><span>used</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>96</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>97</div></div><div><span>}</span></div></div><div><div><div>98</div></div><div>
</div></div><div><div><div>99</div></div><div><span>void</span><span> </span><span>pebble_arena_destroy</span><span><span>(</span><span>PebbleArena </span></span><span>*</span><span><span>arena</span><span>) {</span></span></div></div><div><div><div>100</div></div><div><span>    </span><span>if</span><span> (arena </span><span>!=</span><span> </span><span>NULL</span><span>) {</span></div></div><div><div><div>101</div></div><div><span>        </span><span>free</span><span>(</span><span>arena</span><span>-&gt;</span><span>base</span><span>);</span></div></div><div><div><div>102</div></div><div><span>        </span><span>free</span><span>(arena);</span></div></div><div><div><div>103</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>104</div></div><div><span>}</span></div></div><div><div><div>105</div></div><div>
</div></div><div><div><div>106</div></div><div><span>/* --- Fold ------------------------------------------------------------------- */</span></div></div><div><div><div>107</div></div><div>
</div></div><div><div><div>108</div></div><div><span>int64_t</span><span> </span><span>pebble_fold</span><span>(</span><span>int64_t</span><span><span> </span><span>init</span><span>,</span><span> </span></span><span>int64_t</span><span><span> </span><span>lo</span><span>,</span><span> </span></span><span>int64_t</span><span><span> </span><span>hi</span><span>,</span><span> pebble_fold_fn </span><span>cb</span><span>,</span><span> </span></span><span>void</span><span> </span><span>*</span><span><span>ctx</span><span>) {</span></span></div></div><div><div><div>109</div></div><div><span>    </span><span>int64_t</span><span> acc </span><span>=</span><span> init;</span></div></div><div><div><div>110</div></div><div><span>    </span><span>int64_t</span><span> value;</span></div></div><div><div><div>111</div></div><div>
</div></div><div><div><div>112</div></div><div><span><span>    </span></span><span>/* Walking hi upwards would be pointless: the range is inclusive and `lo &gt;</span></div></div><div><div><div>113</div></div><div><span><span>     </span></span><span>* hi` means "empty". */</span></div></div><div><div><div>114</div></div><div><span>    </span><span>if</span><span> (lo </span><span>&gt;</span><span> hi) {</span></div></div><div><div><div>115</div></div><div><span>        </span><span>return</span><span> acc;</span></div></div><div><div><div>116</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>117</div></div><div><span><span>    </span></span><span>/* Structured so that the loop variable never overflows, even for hi ==</span></div></div><div><div><div>118</div></div><div><span><span>     </span></span><span>* INT64_MAX: the increment is skipped on the final iteration. */</span></div></div><div><div><div>119</div></div><div><span>    </span><span>for</span><span> (value </span><span>=</span><span> lo;; value</span><span>++</span><span>) {</span></div></div><div><div><div>120</div></div><div><span><span>        </span></span><span>acc </span><span>=</span><span> </span><span>cb</span><span>(acc, value, ctx);</span></div></div><div><div><div>121</div></div><div><span>        </span><span>if</span><span> (value </span><span>==</span><span> hi) {</span></div></div><div><div><div>122</div></div><div><span>            </span><span>break</span><span>;</span></div></div><div><div><div>123</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>124</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>125</div></div><div><span>    </span><span>return</span><span> acc;</span></div></div><div><div><div>126</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个细节。<code>PebbleArena</code> 的真实布局只在这个 <code>.c</code> 文件里（<code>base</code>、<code>capacity</code>、<code>used</code>），Rust 永远看不到。对齐用的是一个 <code>union</code> 探针：C99 没有 <code>max_align_t</code>，所以拿几个标量类型塞进一个 union，取它的 <code>sizeof</code> 当作最大对齐（x86-64 上是 16，因为有 <code>long double</code>）。<code>pebble_arena_alloc</code> 里的两个比较都小心地避免了加法溢出：先确认 <code>start &lt;= capacity</code>，再算 <code>capacity - start</code>。<code>pebble_fold</code> 的循环写成”先调用，再判断是否到头，最后才自增”，这样 <code>hi == INT64_MAX</code> 时 <code>value++</code> 永远不会溢出。这些全是 C 的功课，不是这一篇的重点；我把它们写出来只是为了让这个 crate 完整，也让你看清楚 Rust 那一侧到底在给什么承诺。</p><div><div><div></div><div>C 代码也要照抄</div></div><div><p>这两个文件是 C，不是 Rust，所以没有”先写个简化版再演化”这回事：照抄即可。它们在 tag 里的位置是 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-sys/csrc/include/pebble_shim.h" target="_blank"><code>pebble_shim.h</code></a> 和 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-sys/csrc/pebble_shim.c" target="_blank"><code>pebble_shim.c</code></a>。</p></div></div></section>
<section><h2>build.rs：让 Cargo 替你调 C 编译器<a href="#buildrs让-cargo-替你调-c-编译器"><span>#</span></a></h2><p>C++ 项目里，这一步是 CMake 的 <code>add_library(pebble_shim STATIC csrc/pebble_shim.c)</code> 加 <code>target_link_libraries</code>。Cargo 没有 CMake 那样的第二门语言，它的做法是：在 crate 根目录放一个叫 <code>build.rs</code> 的普通 Rust 程序，Cargo 在编译这个 crate 之前先编译并运行它，它往标准输出打印以 <code>cargo:</code> 开头的指令，Cargo 照着办。写 <code>crates/pebble-sys/build.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Compiles the C shim in `csrc/` into a static library linked into this crate.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>/// Source files that make up the C shim, relative to this crate's root.</span></div></div><div><div><div>4</div></div><div><span>const</span><span> </span><span>C_SOURCES</span><span>: &amp;[&amp;</span><span>str</span><span><span>] </span><span>=</span><span> &amp;[</span></span><span>"csrc/pebble_shim.c"</span><span>, </span><span>"csrc/include/pebble_shim.h"</span><span>];</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> </span><span>source</span><span> </span><span>in</span><span> </span><span>C_SOURCES</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>println!</span><span>(</span><span>"cargo:rerun-if-changed={source}"</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>cc</span><span>::</span><span>Build</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>.</span><span>file</span><span>(</span><span>"csrc/pebble_shim.c"</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>.</span><span>include</span><span>(</span><span>"csrc/include"</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>.</span><span>flag_if_supported</span><span>(</span><span>"-std=c99"</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>.</span><span>warnings</span><span>(</span><span>true</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>.</span><span>compile</span><span>(</span><span>"pebble_shim"</span><span>);</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>cc::Build</code> 找到系统的 C 编译器（读 <code>CC</code> 环境变量，没有就用 <code>cc</code>），把 <code>pebble_shim.c</code> 编译成静态库 <code>libpebble_shim.a</code>，放到 Cargo 给的 <code>OUT_DIR</code>，并且替你打印 <code>cargo:rustc-link-lib=static=pebble_shim</code> 和 <code>cargo:rustc-link-search=...</code>，也就是 CMake 里 <code>target_link_libraries</code> 那两件事。<code>println!("cargo:rerun-if-changed=...")</code> 告诉 Cargo：只有这些文件变了才需要重跑 <code>build.rs</code>；不写的话，包里任何文件变了都会重跑。<code>C_SOURCES</code> 里故意把头文件也列上了，因为头文件变了 <code>.c</code> 也得重编。</p><p><code>crates/pebble-sys/Cargo.toml</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-sys"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"FFI shims: a small C library linked into the Pebble runtime."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>build-dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>cc.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>15</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>[build-dependencies]</code> 是只给 <code>build.rs</code> 用的依赖，不会链接进最终的库。这是 Cargo 把”构建期的工具链”和”运行期的依赖”分开的方式：<code>cc</code> 不会出现在你的二进制里。</p><p>来看一次干净构建的真实输出：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>-p</span><span> </span><span>pebble-sys</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling find-msvc-tools v0.1.14</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling shlex v2.0.1</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Compiling cc v1.6.0</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>Compiling pebble-sys v0.1.0 (/tmp/pebble-rebuild-13/crates/pebble-sys)</span></div></div><div><div><div>5</div></div><div><span>warning: pebble-sys@0.1.0: In file included from /nix/store/nl1lkxkfx6466dlbv0v1azh2dqa7j1k6-glibc-2.42-84-dev/include/bits/libc-header-start.h:33,</span></div></div><div><div><div>6</div></div><div><span>warning: pebble-sys@0.1.0:                  from /nix/store/nl1lkxkfx6466dlbv0v1azh2dqa7j1k6-glibc-2.42-84-dev/include/stdint.h:26,</span></div></div><div><div><div>7</div></div><div><span>warning: pebble-sys@0.1.0:                  from /nix/store/w4910jj1pb63k12d3gbnl03n1ixcf1js-gcc-15.3.0/lib/gcc/x86_64-unknown-linux-gnu/15.3.0/include/stdint.h:11,</span></div></div><div><div><div>8</div></div><div><span>warning: pebble-sys@0.1.0:                  from csrc/include/pebble_shim.h:10,</span></div></div><div><div><div>9</div></div><div><span>warning: pebble-sys@0.1.0:                  from csrc/pebble_shim.c:3:</span></div></div><div><div><div>10</div></div><div><span>warning: pebble-sys@0.1.0: /nix/store/nl1lkxkfx6466dlbv0v1azh2dqa7j1k6-glibc-2.42-84-dev/include/features.h:435:4: warning: #warning _FORTIFY_SOURCE requires compiling with optimization (-O) [-Wcpp]</span></div></div><div><div><div>11</div></div><div><span>warning: pebble-sys@0.1.0:   435 | #  warning _FORTIFY_SOURCE requires compiling with optimization (-O)</span></div></div><div><div><div>12</div></div><div><span>warning: pebble-sys@0.1.0:       |    ^~~~~~~</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.77s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译 C 的那些 <code>warning: pebble-sys@0.1.0:</code> 行是 <code>build.rs</code> 里 <code>.warnings(true)</code>（相当于 <code>-Wall -Wextra</code>）把 gcc 的输出原样转发了。这一条是我这台 nix 环境特有的：nix 的 gcc 包装器默认打开 <code>_FORTIFY_SOURCE</code>，而 debug 构建是 <code>-O0</code>，两者冲突，gcc 就抱怨。换一台普通机器，我猜你什么都看不到。顺便看看 <code>build.rs</code> 的产物：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ls</span><span> </span><span>target/debug/build/pebble-sys-</span><span>*</span><span>/out/</span></div></div><div><div><div>2</div></div><div><span>nm</span><span> </span><span>target/debug/build/pebble-sys-</span><span>*</span><span>/out/libpebble_shim.a</span><span> | </span><span>grep</span><span> </span><span>" T "</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>faea18488176a15d-pebble_shim.o</span></div></div><div><div><div>2</div></div><div><span>flag_check</span></div></div><div><div><div>3</div></div><div><span>flag_check.c</span></div></div><div><div><div>4</div></div><div><span>libpebble_shim.a</span></div></div><div><div><div>5</div></div><div><span>0000000000000000 T pebble_arena_alloc</span></div></div><div><div><div>6</div></div><div><span>0000000000000000 T pebble_arena_capacity</span></div></div><div><div><div>7</div></div><div><span>0000000000000000 T pebble_arena_create</span></div></div><div><div><div>8</div></div><div><span>0000000000000000 T pebble_arena_destroy</span></div></div><div><div><div>9</div></div><div><span>0000000000000000 T pebble_arena_reset</span></div></div><div><div><div>10</div></div><div><span>0000000000000000 T pebble_arena_used</span></div></div><div><div><div>11</div></div><div><span>0000000000000000 T pebble_fnv1a</span></div></div><div><div><div>12</div></div><div><span>0000000000000000 T pebble_fold</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>libpebble_shim.a</code> 就是那个静态库，<code>nm</code> 里 <code>T</code> 表示”在 text 段里定义的全局符号”，八个函数一个不少。<code>flag_check</code> 那两个文件是 <code>cc</code> 探测 <code>-std=c99</code> 是否被支持（<code>flag_if_supported</code>）时留下的。</p></section>
<section><h2>第一次调用 C：从 strlen 开始<a href="#第一次调用-c从-strlen-开始"><span>#</span></a></h2><p>在写正式代码之前，先在第 1 篇建的实验室里（<code>~/rust-lab</code>）试一次最小的 FFI，看看编译器怎么拦我们。文件 <code>src/bin/ch13_extern_plain.rs</code>，用 C 标准库里现成的 <code>strlen</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>ffi</span><span>::{</span></span><span>CString</span><span>, c_char};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>extern</span><span> </span><span>"C"</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>strlen</span><span>(</span><span>s</span><span>: *</span><span>const</span><span> </span><span>c_char</span><span>) -&gt; </span><span>usize</span><span>;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span></span><span>CString</span><span>::</span><span>new</span><span>(</span><span>"pebble"</span><span>).</span><span>unwrap</span><span>();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>strlen</span><span>(</span><span>s</span><span>.</span><span>as_ptr</span><span>()) };</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"strlen = {n}"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch13_extern_plain</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: extern blocks must be unsafe</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch13_extern_plain.rs:3:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>3 | / extern "C" {</span></div></div><div><div><div>5</div></div><div><span>4 | |     fn strlen(s: *const c_char) -&gt; usize;</span></div></div><div><div><div>6</div></div><div><span>5 | | }</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>| |_^</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>error: could not compile `rust-lab` (bin "ch13_extern_plain") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>edition 2024 起，<code>extern</code> 块必须写成 <code>unsafe extern</code>。道理是：这个块里的每一个声明都是你对编译器的一个”断言”，说”链接器会找到一个叫 <code>strlen</code>、签名是这样的符号”。编译器看不到 C 的函数体，没法验证你的断言，写错了（比如把 <code>usize</code> 写成 <code>i32</code>）就是未定义行为。所以这个块本身要标成 <code>unsafe</code>，表示”这里是我在担保”。改成 <code>unsafe extern "C"</code>，再故意不加 <code>unsafe</code> 去调用（<code>ch13_call_unsafe.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0133]: call to unsafe function `strlen` is unsafe and requires unsafe block</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch13_call_unsafe.rs:9:13</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>9 |     let n = strlen(s.as_ptr());</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|             ^^^^^^^^^^^^^^^^^^ call to unsafe function</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= note: consult the function's documentation for information on how to avoid undefined behavior</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>For more information about this error, try `rustc --explain E0133`.</span></div></div><div><div><div>10</div></div><div><span>error: could not compile `rust-lab` (bin "ch13_call_unsafe") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两层 <code>unsafe</code> 各管各的：块上的 <code>unsafe</code> 是”声明是否正确”的责任，调用点的 <code>unsafe</code> 是”这次调用的前置条件是否满足”的责任。<code>strlen</code> 要求参数是以 NUL 结尾的有效字符串，<code>CString</code> 保证这一点，所以我们补上 <code>unsafe { }</code> 并写一句 <code>SAFETY</code> 注释（<code>ch13_strlen.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// SAFETY: `s` is a valid NUL-terminated string that outlives the call.</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>strlen</span><span>(</span><span>s</span><span>.</span><span>as_ptr</span><span>()) };</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>strlen = 6</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>类型对应表也在这里：<code>*const c_char</code> 是 <code>const char *</code>，<code>usize</code> 是 <code>size_t</code>，<code>i64</code> 是 <code>int64_t</code>，<code>std::ffi::c_void</code> 是 <code>void</code>。这些都在 <code>std::ffi</code> 里（<code>c_int</code>、<code>c_long</code> 等同理），不要手写 <code>i32</code> 代替 <code>c_int</code>，虽然在你的机器上它们碰巧一样。</p><div><div><div></div><div>safe fn</div></div><div><p>如果一个 C 函数真的没有任何前置条件，edition 2024 允许在 <code>unsafe extern</code> 块里把它标成 <code>safe fn</code>，调用方就不用写 <code>unsafe</code> 了。实验 <code>ch13_safe_fn.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>unsafe</span><span> </span><span>extern</span><span> </span><span>"C"</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>// getpid takes no arguments and has no preconditions: nothing to get wrong.</span></div></div><div><div><div>3</div></div><div><span>    </span><span>safe</span><span> </span><span>fn</span><span> </span><span>getpid</span><span>() -&gt; </span><span>c_int</span><span>;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运行输出：<code>getpid() &gt; 0: true</code>。Pebble 里一个 <code>safe fn</code> 都没有：我们的 C 函数全都收指针，都有前置条件。</p></div></div></section>
<section><h2>fnv1a：值进、值出<a href="#fnv1a值进值出"><span>#</span></a></h2><p><code>pebble-sys</code> 里最简单的一个包装。文件 <code>crates/pebble-sys/src/lib.rs</code>，先只写这一个函数和它的测试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! FFI shims: a small hand-written C library linked into the Pebble runtime.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>unsafe</span><span> </span><span>extern</span><span> </span><span>"C"</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Reads `len` bytes at `data`; `data` may be null when `len` is 0.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_fnv1a</span><span>(</span><span>data</span><span>: *</span><span>const</span><span> </span><span>u8</span><span>, </span><span>len</span><span>: </span><span>usize</span><span>) -&gt; </span><span>u64</span><span>;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后是包装函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Computes the 64-bit FNV-1a hash of `data` using the C implementation.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// ```</span></div></div><div><div><div>4</div></div><div><span>/// # use pebble_sys::fnv1a;</span></div></div><div><div><div>5</div></div><div><span>/// let digest = fnv1a(b"hello");</span></div></div><div><div><div>6</div></div><div><span>/// assert_eq!(digest, 0xa430_d846_80aa_bd0b);</span></div></div><div><div><div>7</div></div><div><span>/// ```</span></div></div><div><div><div>8</div></div><div><span>#[must_use]</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>fnv1a</span><span>(</span><span>data</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; </span><span>u64</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>// SAFETY: `data.as_ptr()` is valid for `data.len()` reads for the duration</span></div></div><div><div><div>11</div></div><div><span>    </span><span>// of the call and the C function only reads through it. A shared slice</span></div></div><div><div><div>12</div></div><div><span>    </span><span>// never yields a null pointer, and `len == 0` is explicitly allowed.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>unsafe</span><span> { </span><span>pebble_fnv1a</span><span>(</span><span>data</span><span>.</span><span>as_ptr</span><span>(), </span><span>data</span><span>.</span><span>len</span><span>()) }</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>data.as_ptr()</code> 对一个切片给出首元素的指针，<code>data.len()</code> 是元素个数，这正是 C 的 <code>(const uint8_t *data, size_t len)</code>。<code>#[must_use]</code> 第 1 篇见过。Rust 的切片永远不会给出空指针（空切片给的是一个对齐的悬空指针），而 C 头文件里写了”<code>len == 0</code> 时 <code>data</code> 可以为 NULL”，所以两边的约定是兼容的，这就是 <code>SAFETY</code> 注释要逐条核对的东西。</p><p>测试怎么写？FNV-1a 有公开的参考值，空串的哈希就是起始常数 <code>0xcbf29ce484222325</code>，<code>"hello"</code> 是 <code>0xa430d84680aabd0b</code>。我们再加一个对拍：用纯 Rust 实现一遍（几行），喂给两边 1000 个字节，要求结果相同。把测试写到同一个文件末尾：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>fnv1a_empty_is_offset_basis</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>fnv1a</span><span>(</span><span>b""</span><span>), </span><span>0xcbf2_9ce4_8422_2325</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>fn</span><span> </span><span>fnv1a_hello_matches_reference</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>fnv1a</span><span>(</span><span>b"hello"</span><span>), </span><span>0xa430_d846_80aa_bd0b</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>fn</span><span> </span><span>fnv1a_foobar_matches_reference</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>fnv1a</span><span>(</span><span>b"foobar"</span><span>), </span><span>0x8594_4171_f739_67e8</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>21</div></div><div><span>    </span><span>fn</span><span> </span><span>fnv1a_long_input_matches_rust_reference</span><span>() {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>fn</span><span> </span><span>reference</span><span>(</span><span>data</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; </span><span>u64</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>data</span><span>.</span><span>iter</span><span>().</span><span>fold</span><span>(</span><span>0xcbf2_9ce4_8422_2325</span><span>u64</span><span><span>, </span><span>|</span></span><span>hash</span><span>, </span><span>byte</span><span><span>|</span><span> {</span></span></div></div><div><div><div>24</div></div><div><span><span>                </span></span><span>(</span><span>hash</span><span><span> </span><span>^</span><span> </span><span>u64</span><span>::</span></span><span>from</span><span>(*</span><span>byte</span><span>)).</span><span>wrapping_mul</span><span>(</span><span>0x0000_0100_0000_01b3</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>})</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>        </span><span>let</span><span> </span><span>data</span><span>: </span><span>Vec</span><span>&lt;</span><span>u8</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..=</span><span>255</span><span>u8</span><span>).</span><span>cycle</span><span>().</span><span>take</span><span>(</span><span>1000</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>29</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>fnv1a</span><span>(&amp;</span><span>data</span><span>), </span><span>reference</span><span>(&amp;</span><span>data</span><span>));</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-sys</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test tests::fnv1a_foobar_matches_reference ... ok</span></div></div><div><div><div>2</div></div><div><span>test tests::fnv1a_hello_matches_reference ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::fnv1a_empty_is_offset_basis ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::fnv1a_long_input_matches_rust_reference ... ok</span></div></div><div><div><div>5</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>Doc-tests pebble_sys</span></div></div><div><div><div>7</div></div><div><span>test crates/pebble-sys/src/lib.rs - fnv1a (line 10) ... ok</span></div></div><div><div><div>8</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>4 个单元测试加 1 个文档测试（<code>fnv1a</code> 上方 <code>///</code> 里的那段代码，<code>cargo test</code> 会把它当成独立程序编译运行，第 10 篇讲过）。注意最后一个对拍测试里 <code>wrapping_mul</code>：C 的无符号乘法本来就是回绕的，Rust 里普通的 <code>*</code> 在 debug 构建下溢出会 panic（第 1 篇的实验），所以必须明说”我要回绕”。</p></section>
<section><h2>不透明类型和 repr(C)<a href="#不透明类型和-reprc"><span>#</span></a></h2><p>下一个要包装的是 arena。C 函数签名里出现了 <code>PebbleArena *</code>，Rust 的 <code>extern</code> 块里也得有一个类型来写这个指针。直觉做法是把 C 的结构体照抄一遍：<code>base</code>、<code>capacity</code>、<code>used</code>。别抄。头文件说了这个结构是不透明的，它的布局随时可能变，抄一份等于把一个 C 内部细节焊死在 Rust 里。</p><p>在讲怎么不抄之前，先搞清楚 <code>repr(C)</code> 是什么，因为它是 FFI 里最常见的属性。实验 <code>ch13_repr_c.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Rusty</span><span> { </span><span>a</span><span>: </span><span>u8</span><span>, </span><span>b</span><span>: </span><span>u32</span><span>, </span><span>c</span><span>: </span><span>u8</span><span> }</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[repr(</span><span>C</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>struct</span><span> </span><span>CLike</span><span> { </span><span>a</span><span>: </span><span>u8</span><span>, </span><span>b</span><span>: </span><span>u32</span><span>, </span><span>c</span><span>: </span><span>u8</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（实验里还加了 <code>#[allow(dead_code)]</code> 和打印 <code>size_of</code>/<code>align_of</code> 的 <code>main</code>，完整文件在实验室里。）</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Rusty: size 8 align 4</span></div></div><div><div><div>2</div></div><div><span>CLike: size 12 align 4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同样三个字段，Rust 默认布局 8 字节，<code>repr(C)</code> 12 字节。Rust 的默认布局没有承诺任何顺序：编译器可以重排字段来省 padding（这里把两个 <code>u8</code> 并排放了）。C 的布局按声明顺序，<code>a</code> 后面补 3 字节对齐到 <code>b</code>，<code>c</code> 后面补 3 字节凑够 4 的倍数。所以凡是要和 C 共享内存布局的结构体，都必须写 <code>#[repr(C)]</code>。反过来说，只要你不打算让 C 读写这块内存，默认布局更省空间。</p><p>Pebble 用的是另一种办法，Rustonomicon 里推荐的写法：只声明一个”放不下任何东西”的类型，并且只通过指针使用它。文件 <code>lib.rs</code> 里在 <code>fnv1a</code> 之前加上：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Opaque handle for the C arena type.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// The layout is deliberately unknown: Rust only ever names this type inside a</span></div></div><div><div><div>4</div></div><div><span>/// pointer, and the C side of the header is the single source of truth for what</span></div></div><div><div><div>5</div></div><div><span>/// it actually contains.</span></div></div><div><div><div>6</div></div><div><span>///</span></div></div><div><div><div>7</div></div><div><span>/// The marker follows the Rustonomicon's pattern for opaque FFI types: the raw</span></div></div><div><div><div>8</div></div><div><span>/// pointer makes the type `!Send` and `!Sync`, and `PhantomPinned` makes it</span></div></div><div><div><div>9</div></div><div><span>/// `!Unpin`, so safe code can never move or share it by accident.</span></div></div><div><div><div>10</div></div><div><span>#[repr(</span><span>C</span><span>)]</span></div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>PebbleArena</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>/// Makes the type impossible to construct in Rust.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>_data</span><span>: [</span><span>u8</span><span>; </span><span>0</span><span>],</span></div></div><div><div><div>14</div></div><div><span>    </span><span>_marker</span><span>: </span><span>PhantomData</span><span>&lt;(*</span><span>mut</span><span> </span><span>u8</span><span>, </span><span>PhantomPinned</span><span>)&gt;,</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>[u8; 0]</code> 是零长度数组，不占空间，只是让这个结构体”不是空的”（<code>#[repr(C)]</code> 的空结构体在 C 里是不合法的）。更重要的是，它的字段是私有的，所以别的代码没办法在 Rust 里构造一个 <code>PebbleArena</code>，只能拿到 C 给的指针。<code>PhantomData&lt;(*mut u8, PhantomPinned)&gt;</code> 是一个零大小的”幽灵字段”：它不存放任何数据，只是让编译器认为这个结构体”里面有一个 <code>*mut u8</code> 和一个 <code>PhantomPinned</code>”。这有什么用？先看三个 trait。</p><section><h3>Send、Sync 与”自动 trait”<a href="#sendsync-与自动-trait"><span>#</span></a></h3><p><code>Send</code> 的意思是”这个值可以被移动到另一个线程”，<code>Sync</code> 的意思是”<code>&amp;T</code> 可以同时被多个线程拿着”。第 14 篇会好好讲线程；这里先要知道它们的特别之处：它们是自动 trait（auto trait）。你不用写 <code>impl Send for Foo</code>，编译器看字段：所有字段都是 <code>Send</code>，结构体就是 <code>Send</code>；有一个不是，整个结构体就不是。裸指针 <code>*const T</code> 和 <code>*mut T</code> 是故意不实现 <code>Send</code> 和 <code>Sync</code> 的：编译器不知道指针背后的东西能不能跨线程，宁可拒绝。C++ 里没有这种东西，任何类型你想往 <code>std::thread</code> 里塞都可以，出了数据竞争是你的事。</p><p><code>Unpin</code> 是第三个自动 trait，意思是”移动这个值是安全的”。绝大多数类型都是 <code>Unpin</code>，<code>PhantomPinned</code> 是专门用来取消这一点的标记（第 14 篇的 <code>Pin</code> 会用到）。对一个 C 管着的结构体来说，Rust 把它按值挪来挪去显然是错的，因为 C 的指针可能指着它的地址。</p><p>实验 <code>ch13_opaque.rs</code> 把同样的结构体抄过来，让编译器证明三件事：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>assert_send</span><span>&lt;</span><span>T</span><span>: </span><span>Send</span><span>&gt;() {}</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>assert_sync</span><span>&lt;</span><span>T</span><span>: </span><span>Sync</span><span>&gt;() {}</span></div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>assert_unpin</span><span>&lt;</span><span>T</span><span>: </span><span>Unpin</span><span>&gt;() {}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>assert_send</span><span>::&lt;</span><span>Opaque</span><span>&gt;();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>assert_sync</span><span>::&lt;</span><span>Opaque</span><span>&gt;();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>assert_unpin</span><span>::&lt;</span><span>Opaque</span><span>&gt;();</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `*mut u8` cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:14:19</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>14 |     assert_send::&lt;Opaque&gt;();</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                   ^^^^^^ `*mut u8` cannot be sent between threads safely</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= help: within `Opaque`, the trait `Send` is not implemented for `*mut u8`</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: required because it appears within the type `(*mut u8, PhantomPinned)`</span></div></div><div><div><div>9</div></div><div><span>note: required because it appears within the type `PhantomData&lt;(*mut u8, PhantomPinned)&gt;`</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/marker.rs:814:11</span></div></div><div><div><div>11</div></div><div><span>note: required because it appears within the type `Opaque`</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:4:8</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>4 | struct Opaque {</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>|        ^^^^^^</span></div></div><div><div><div>16</div></div><div><span>note: required by a bound in `assert_send`</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:9:19</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>9 | fn assert_send&lt;T: Send&gt;() {}</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>|                   ^^^^ required by this bound in `assert_send`</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>error[E0277]: `*mut u8` cannot be shared between threads safely</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:15:19</span></div></div><div><div><div>24</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>25</div></div><div><span>15 |     assert_sync::&lt;Opaque&gt;();</span></div></div><div><div><div>26</div></div><div><span><span>   </span></span><span>|                   ^^^^^^ `*mut u8` cannot be shared between threads safely</span></div></div><div><div><div>27</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>28</div></div><div><span><span>   </span></span><span>= help: within `Opaque`, the trait `Sync` is not implemented for `*mut u8`</span></div></div><div><div><div>29</div></div><div><span><span>   </span></span><span>= note: required because it appears within the type `(*mut u8, PhantomPinned)`</span></div></div><div><div><div>30</div></div><div><span>note: required because it appears within the type `PhantomData&lt;(*mut u8, PhantomPinned)&gt;`</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/marker.rs:814:11</span></div></div><div><div><div>32</div></div><div><span>note: required because it appears within the type `Opaque`</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:4:8</span></div></div><div><div><div>34</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>35</div></div><div><span><span> </span></span><span>4 | struct Opaque {</span></div></div><div><div><div>36</div></div><div><span><span>   </span></span><span>|        ^^^^^^</span></div></div><div><div><div>37</div></div><div><span>note: required by a bound in `assert_sync`</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:10:19</span></div></div><div><div><div>39</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>40</div></div><div><span>10 | fn assert_sync&lt;T: Sync&gt;() {}</span></div></div><div><div><div>41</div></div><div><span><span>   </span></span><span>|                   ^^^^ required by this bound in `assert_sync`</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>error[E0277]: `PhantomPinned` cannot be unpinned</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:16:20</span></div></div><div><div><div>45</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>46</div></div><div><span>16 |     assert_unpin::&lt;Opaque&gt;();</span></div></div><div><div><div>47</div></div><div><span><span>   </span></span><span>|                    ^^^^^^ within `Opaque`, the trait `Unpin` is not implemented for `PhantomPinned`</span></div></div><div><div><div>48</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>49</div></div><div><span><span>   </span></span><span>= note: consider using the `pin!` macro</span></div></div><div><div><div>50</div></div><div><span><span>           </span></span><span>consider using `Box::pin` if you need to access the pinned value outside of the current scope</span></div></div><div><div><div>51</div></div><div><span><span>   </span></span><span>= note: required because it appears within the type `(*mut u8, PhantomPinned)`</span></div></div><div><div><div>52</div></div><div><span>note: required because it appears within the type `PhantomData&lt;(*mut u8, PhantomPinned)&gt;`</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/marker.rs:814:11</span></div></div><div><div><div>54</div></div><div><span>note: required because it appears within the type `Opaque`</span></div></div><div><div><div>55</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:4:8</span></div></div><div><div><div>56</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>57</div></div><div><span><span> </span></span><span>4 | struct Opaque {</span></div></div><div><div><div>58</div></div><div><span><span>   </span></span><span>|        ^^^^^^</span></div></div><div><div><div>59</div></div><div><span>note: required by a bound in `assert_unpin`</span></div></div><div><div><div>60</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_opaque.rs:11:20</span></div></div><div><div><div>61</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>62</div></div><div><span>11 | fn assert_unpin&lt;T: Unpin&gt;() {}</span></div></div><div><div><div>63</div></div><div><span><span>   </span></span><span>|                    ^^^^^ required by this bound in `assert_unpin`</span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>66</div></div><div><span>error: could not compile `rust-lab` (bin "ch13_opaque") due to 3 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个都被拒绝，原因行都指向 <code>*mut u8</code> 或 <code>PhantomPinned</code>。一个诚实的说明：在 <code>pebble-sys</code> 里，这个标记其实是”腰带加背带”。我实测过，删掉 <code>_marker</code> 之后 <code>Arena</code> 照样不是 <code>Send</code>（下面会看到），因为 <code>Arena</code> 里装的是 <code>NonNull&lt;PebbleArena&gt;</code>，而 <code>NonNull</code> 本身就不是 <code>Send</code>。标记保护的是另一种情况：以后有人把 <code>&amp;PebbleArena</code> 或者 <code>PebbleArena</code> 的别的指针包装放进一个新的结构体里，没有 <code>NonNull</code> 兜底，这个类型照样会被判成不能跨线程、不能移动。成本是零（<code>PhantomData</code> 不占空间），收益是类型自己把话说清楚。</p></section></section>
<section><h2>把 C 的堆内存变成 Rust 的值：Arena<a href="#把-c-的堆内存变成-rust-的值arena"><span>#</span></a></h2><p>现在把 C 的函数声明搬进 Rust。<code>lib.rs</code> 里 <code>PebbleArena</code> 之后加上这个 <code>extern</code> 块（同时把 <code>fnv1a</code> 的那一条并进来，一个块里放所有 C 函数就行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>unsafe</span><span> </span><span>extern</span><span> </span><span>"C"</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Reads `len` bytes at `data`; `data` may be null when `len` is 0.</span></div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_fnv1a</span><span>(</span><span>data</span><span>: *</span><span>const</span><span> </span><span>u8</span><span>, </span><span>len</span><span>: </span><span>usize</span><span>) -&gt; </span><span>u64</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>/// Returns null on allocation failure.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_arena_create</span><span>(</span><span>capacity</span><span>: </span><span>usize</span><span>) -&gt; *</span><span>mut</span><span> </span><span>PebbleArena</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// Returns null when the request does not fit.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_arena_alloc</span><span>(</span><span>arena</span><span>: *</span><span>mut</span><span> </span><span>PebbleArena</span><span>, </span><span>len</span><span>: </span><span>usize</span><span>) -&gt; *</span><span>mut</span><span> </span><span>c_void</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_arena_used</span><span>(</span><span>arena</span><span>: *</span><span>const</span><span> </span><span>PebbleArena</span><span>) -&gt; </span><span>usize</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_arena_capacity</span><span>(</span><span>arena</span><span>: *</span><span>const</span><span> </span><span>PebbleArena</span><span>) -&gt; </span><span>usize</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// Invalidates every pointer the arena has handed out.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_arena_reset</span><span>(</span><span>arena</span><span>: *</span><span>mut</span><span> </span><span>PebbleArena</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>/// Frees the arena and its backing storage.</span></div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_arena_destroy</span><span>(</span><span>arena</span><span>: *</span><span>mut</span><span> </span><span>PebbleArena</span><span>);</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意文档注释写在 <code>extern</code> 声明上，这是要给将来读这个文件的人看的：每个 C 函数的”返回 NULL 的条件”和”哪些指针会失效”。这里没有 <code>fold</code> 那一条，我们最后再加。</p><p>接着写包装。要解决三个问题：谁负责释放，<code>alloc</code> 返回什么，<code>Arena</code> 能不能跨线程。</p><section><h3>谁来 free：Drop<a href="#谁来-freedrop"><span>#</span></a></h3><p>C++ 里 <code>Arena</code> 会是一个类，构造函数调用 <code>pebble_arena_create</code>，析构函数调用 <code>pebble_arena_destroy</code>，再写好拷贝构造或者把它删掉，否则默认的按位拷贝会造成双重释放，这就是”五法则”。Rust 里的对应物是 <code>Drop</code> trait，而且移动是按位复制且不会调用被移动对象的析构（第 2 篇的”没有 moved-from 对象”），所以不存在”忘了写移动构造”或者”用了被移走的对象”的问题：整个值要么还在原处，要么已经整个换了主人，<code>drop</code> 恰好执行一次。</p><p>先是结构体，它里面只有一个字段，就是 C 给的那个指针：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// An owned C bump allocator.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Created by [`Arena::with_capacity`] and destroyed (C `free`) when the value</span></div></div><div><div><div>4</div></div><div><span>/// is dropped. The arena is `!Send + !Sync`; the C struct has no locking.</span></div></div><div><div><div>5</div></div><div><span>///</span></div></div><div><div><div>6</div></div><div><span>/// At most one slice returned by [`Arena::alloc`] may be alive at a time,</span></div></div><div><div><div>7</div></div><div><span>/// because it borrows the arena exclusively. Call [`Arena::reset`] to make all</span></div></div><div><div><div>8</div></div><div><span>/// of the capacity available again, which invalidates previously handed out</span></div></div><div><div><div>9</div></div><div><span>/// slices.</span></div></div><div><div><div>10</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>11</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Arena</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>ptr</span><span>: </span><span>NonNull</span><span>&lt;</span><span>PebbleArena</span><span>&gt;,</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后是 <code>Drop</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Drop</span><span> </span><span>for</span><span> </span><span>Arena</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>drop</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>// SAFETY: `self.ptr` came from `pebble_arena_create` and has not been</span></div></div><div><div><div>4</div></div><div><span>        </span><span>// destroyed yet (this is the only place that destroys it), so the C</span></div></div><div><div><div>5</div></div><div><span>        </span><span>// function receives a valid, uniquely owned arena.</span></div></div><div><div><div>6</div></div><div><span>        </span><span>unsafe</span><span> { </span><span>pebble_arena_destroy</span><span>(</span><span>self</span><span>.ptr.</span><span>as_ptr</span><span>()) };</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>字段 <code>ptr</code> 是私有的，所以只有这个模块能构造一个 <code>Arena</code>，而构造只有 <code>with_capacity</code> 一个入口。这就是”笼子”的第一根栏杆：<code>Arena</code> 里的指针一定来自 <code>pebble_arena_create</code>，而且一定没被销毁过，所以 <code>Drop</code> 里的 <code>SAFETY</code> 注释能写得出来。</p></section><section><h3>Option 和空指针<a href="#option-和空指针"><span>#</span></a></h3><p><code>with_capacity</code> 要处理 C 返回 <code>NULL</code> 的情况（<code>malloc</code> 失败）。C 里你会 <code>if (p == NULL)</code>，Rust 里我们用 <code>NonNull::new(ptr)</code>：它接收一个裸指针，空就返回 <code>None</code>，否则返回 <code>Some(NonNull)</code>。下面的方法都放在 <code>impl Arena { ... }</code> 块里。结合第 2 篇的 <code>?</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Creates an arena with room for `bytes` bytes of allocations.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Returns `None` if the C side cannot obtain memory. A capacity of zero is</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// valid: the returned arena exists and every [`Arena::alloc`] fails.</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_capacity</span><span>(</span><span>bytes</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Arena</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>// SAFETY: `pebble_arena_create` has no preconditions and returns either</span></div></div><div><div><div>8</div></div><div><span>        </span><span>// a live arena owned by the caller or null.</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>ptr</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>pebble_arena_create</span><span>(</span><span>bytes</span><span>) };</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Some</span><span>(</span><span>Arena</span><span> {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>ptr</span><span>: </span><span>NonNull</span><span>::</span><span>new</span><span>(</span><span>ptr</span><span>)?,</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>NonNull&lt;T&gt;</code> 是”保证非空的 <code>*mut T</code>”，这个保证让编译器可以把 <code>Option&lt;NonNull&lt;T&gt;&gt;</code> 压成和裸指针一样大：用全零的位模式表示 <code>None</code>。实验 <code>ch13_niche.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>*mut u8             8</span></div></div><div><div><div>2</div></div><div><span>NonNull&lt;u8&gt;         8</span></div></div><div><div><div>3</div></div><div><span>Option&lt;NonNull&lt;u8&gt;&gt; 8</span></div></div><div><div><div>4</div></div><div><span>Option&lt;*mut u8&gt;     16</span></div></div><div><div><div>5</div></div><div><span>Option&lt;&amp;u8&gt;         8</span></div></div><div><div><div>6</div></div><div><span>Option&lt;u32&gt;         8</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Option&lt;NonNull&lt;u8&gt;&gt;</code> 是 8 字节，和 <code>NonNull&lt;u8&gt;</code> 一样，而 <code>Option&lt;*mut u8&gt;</code> 要 16 字节（裸指针本身可以是 0，所以没有空位留给 <code>None</code>，只好多加一个标签）。这个技巧在 Rust 里叫”niche 优化”，<code>Option&lt;&amp;T&gt;</code> 和 <code>Option&lt;Box&lt;T&gt;&gt;</code> 也享受它。所以 <code>Option&lt;NonNull&lt;T&gt;&gt;</code> 和 C 的可空指针 <code>T *</code> 在 ABI 上等价，这也是为什么你可以在 FFI 签名里直接写 <code>Option&lt;NonNull&lt;T&gt;&gt;</code> 或 <code>Option&lt;&amp;T&gt;</code>。</p></section><section><h3>一个带依赖的实验 crate<a href="#一个带依赖的实验-crate"><span>#</span></a></h3><p>接下来的几个实验需要用到我们自己的 <code>pebble-sys</code>，所以不能放进 <code>~/rust-lab</code>（那里只依赖标准库）。另开一个实验 crate（完整版在仓库的 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch13-sys" target="_blank">lab/lab-ch13-sys</a>，下面的文件都在里面，可以直接用）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>~/lab-ch13-sys</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把 <code>~/lab-ch13-sys/Cargo.toml</code> 改成（路径换成你的 pebble 目录）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"lab-ch13-sys"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>7</div></div><div><span>pebble-sys</span><span> = { </span><span>path</span><span> = </span><span>"/path/to/pebble/crates/pebble-sys"</span><span> }</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>[</span><span>workspace</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>末尾那个空的 <code>[workspace]</code> 是告诉 Cargo：这个 crate 自己是一个独立的工作区，不要往上找父目录里的工作区（否则实验放在 pebble 目录里会被当成成员，报错）。每个实验是 <code>src/bin/&lt;名字&gt;.rs</code>，用 <code>cargo run --quiet --bin &lt;名字&gt;</code> 运行。输出里的路径是我机器上的，你的会不同。</p></section><section><h3>alloc：返回 &amp;mut [u8]，借谁的<a href="#alloc返回-mut-u8借谁的"><span>#</span></a></h3><p><code>pebble_arena_alloc</code> 返回一个裸指针，长度是我们自己传进去的那个 <code>len</code>，我们想把这对东西变成 Rust 的切片 <code>&amp;mut [u8]</code>。关键问题是这个切片的生命周期绑在谁身上。最顺手的写法，C 程序员的直觉：反正 C 函数通过指针修改 arena，我又没改 Rust 里的任何字段，<code>&amp;self</code> 就够了。我把签名改成 <code>pub fn alloc(&amp;self, len: usize) -&gt; Option&lt;&amp;mut [u8]&gt;</code> 看看：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>-p</span><span> </span><span>pebble-sys</span><span> </span><span>--lib</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling shlex v2.0.1</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling find-msvc-tools v0.1.14</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Compiling cc v1.6.0</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>Compiling pebble-sys v0.1.0 (/tmp/lab-ch13-scratch/crates/pebble-sys)</span></div></div><div><div><div>5</div></div><div><span>warning: mutable borrow from immutable input(s)</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-sys/src/lib.rs:136:47</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>136 |     pub fn alloc(&amp;self, len: usize) -&gt; Option&lt;&amp;mut [u8]&gt; {</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|                                               ^^^^^^^^^</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>note: immutable borrow here</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-sys/src/lib.rs:136:18</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>136 |     pub fn alloc(&amp;self, len: usize) -&gt; Option&lt;&amp;mut [u8]&gt; {</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>|                  ^^^^^</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#mut_from_ref</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>= note: `-W clippy::mut-from-ref` implied by `-W clippy::all`</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>= help: to override `-W clippy::all` add `#[allow(clippy::mut_from_ref)]`</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>warning: `pebble-sys` (lib) generated 1 warning</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.10s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器本身一声没吭，是 clippy 的 <code>mut_from_ref</code> 拦下来的：从 <code>&amp;self</code> 变出 <code>&amp;mut</code> 几乎一定是错的，因为共享引用可以有很多个，每个都能变出一个独占引用。到底会错在哪里？只要 <code>reset</code> 也写成 <code>&amp;self</code>（它只是改一下 C 里的偏移，看起来无害），就有这个程序（<code>examples/overlap.rs</code>，放在这个”错误版本”里；这个版本存成了补丁 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch13-scratch.patch" target="_blank">lab/exercises/ch13-scratch.patch</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>Arena</span><span>::</span><span>with_capacity</span><span>(</span><span>64</span><span>).</span><span>expect</span><span>(</span><span>"no memory"</span><span>);</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>8</span><span>).</span><span>expect</span><span>(</span><span>"full"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>arena</span><span>.</span><span>reset</span><span>();</span></div></div><div><div><div>4</div></div><div><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>8</span><span>).</span><span>expect</span><span>(</span><span>"full"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>a</span><span>[</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>6</div></div><div><span>b</span><span>[</span><span>0</span><span><span>] </span><span>=</span><span> </span></span><span>2</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>a and b start at the same address: true</span></div></div><div><div><div>2</div></div><div><span>a[0] = 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译通过，运行也不崩，但 <code>a</code> 和 <code>b</code> 是两个同时活着的 <code>&amp;mut [u8]</code>，指向同一块内存。<code>a[0]</code> 明明写的是 1，读出来是 2。这就是 Rust 的核心规则”独占引用不能别名”被我们自己亲手破坏了。更糟的是，编译器会按”独占引用不会别名”这个假设去优化，这样的程序在 release 构建下会出什么行为，没有人保证。这是 <code>unsafe</code> 里最典型的错：不是某个操作本身不行，而是你对外承诺的安全接口留了个洞。</p><p>修法是让 <code>alloc</code> 要 <code>&amp;mut self</code>，返回的切片借用着整个 <code>Arena</code>，这样在这个切片活着的时候，没有人能再调用 <code>alloc</code> 或 <code>reset</code>。这是要付代价的，实验 <code>arena_two_slices.rs</code>（在 <code>~/lab-ch13-sys</code> 里，用真的 <code>pebble-sys</code> 作依赖）试图同时拿两个切片：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0499]: cannot borrow `arena` as mutable more than once at a time</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/arena_two_slices.rs:6:13</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 |     let a = arena.alloc(8).expect("full");</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|             ----- first mutable borrow occurs here</span></div></div><div><div><div>6</div></div><div><span>6 |     let b = arena.alloc(8).expect("full");</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|             ^^^^^ second mutable borrow occurs here</span></div></div><div><div><div>8</div></div><div><span>7 |     a[0] = 1;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|     ---- first borrow later used here</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>For more information about this error, try `rustc --explain E0499`.</span></div></div><div><div><div>12</div></div><div><span>error: could not compile `lab-ch13-sys` (bin "arena_two_slices") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一时刻只能手里拿一个切片。对 bump 分配器来说这个限制偏紧：真正的 arena 库（据我所知，比如 <code>bumpalo</code>）走的是另一条路，<code>alloc</code> 取 <code>&amp;self</code>，靠”每次给出的区域互不重叠”来保证健全，而 <code>reset</code> 取 <code>&amp;mut self</code>。我们选了更保守的一边：正确优先，且不需要读者去证明区域不重叠。写出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Bump-allocates `len` uninitialised bytes and returns them as a slice.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// The returned slice borrows the arena exclusively, so no other allocation</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// (and no other method of this arena) can be used while it is alive. This</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// is what makes the returned `&amp;mut [u8]` sound even though the C allocator</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// hands out raw pointers: Rust cannot observe two live regions at once.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>///</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Returns `None` when the remaining capacity cannot satisfy the request</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// (including any request on a zero-capacity arena).</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>alloc</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>len</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Option</span><span>&lt;&amp;</span><span>mut</span><span> [</span><span>u8</span><span>]&gt; {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>// SAFETY: `self.ptr` is a live arena owned by `self`, and the C function</span></div></div><div><div><div>13</div></div><div><span>        </span><span>// neither retains the arena nor frees it.</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>raw</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>pebble_arena_alloc</span><span>(</span><span>self</span><span>.ptr.</span><span>as_ptr</span><span>(), </span><span>len</span><span>) };</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>raw</span><span><span> </span><span>=</span><span> </span></span><span>NonNull</span><span>::</span><span>new</span><span>(</span><span>raw</span><span>)?.</span><span>cast</span><span>::&lt;</span><span>u8</span><span>&gt;();</span></div></div><div><div><div>16</div></div><div><span>        </span><span>// SAFETY: `raw` points into the arena's storage and the C allocator</span></div></div><div><div><div>17</div></div><div><span>        </span><span>// guarantees at least `len` bytes there, aligned for any scalar. The</span></div></div><div><div><div>18</div></div><div><span>        </span><span>// region is not aliased: any other borrow of `self` would have to go</span></div></div><div><div><div>19</div></div><div><span>        </span><span>// through this `&amp;mut self` borrow. The pointer is never null.</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Some</span><span>(</span><span>unsafe</span><span><span> { </span><span>slice</span><span>::</span></span><span>from_raw_parts_mut</span><span>(</span><span>raw</span><span>.</span><span>as_ptr</span><span>(), </span><span>len</span><span>) })</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两处 <code>unsafe</code> 各有一句 <code>SAFETY</code>。第一处调用 C，前置条件是”arena 是活的”，<code>self.ptr</code> 保证了。第二处 <code>slice::from_raw_parts_mut</code>（第 11 篇的老朋友，把”指针加长度”变成切片）要求指针非空、对齐、指向 <code>len</code> 个已初始化的字节且没有别名。“已初始化”这一条值得多想：C 的 <code>malloc</code> 给出的是没初始化的内存，用 <code>&amp;mut [u8]</code> 去表示它严格来说是个灰色地带（Rust 把读取未初始化字节算作未定义行为）。我们的 <code>alloc</code> 文档写了”contents are unspecified”，调用者应该先写后读，这也是我把它标成”uninitialised bytes”的原因。这里有个我没有解决的小问题：更严格的写法应该返回 <code>&amp;mut [MaybeUninit&lt;u8&gt;]</code>（第 11 篇的 <code>MaybeUninit</code>），而不是 <code>&amp;mut [u8]</code>，代价是调用方每次都得面对 <code>MaybeUninit</code>。</p></section><section><h3>对齐：看一眼 C 在做什么<a href="#对齐看一眼-c-在做什么"><span>#</span></a></h3><p><code>used()</code> 为什么说”包括对齐的 padding”？实验 <code>arena_bytes.rs</code>（同一个 lab 里，依赖 <code>pebble-sys</code>）建一个容量 64 字节的 <code>Arena</code>，连续分配 1、1、24 个字节，再要 8 个：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_sys</span><span>::</span></span><span>Arena</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>Arena</span><span>::</span><span>with_capacity</span><span>(</span><span>64</span><span>).</span><span>expect</span><span>(</span><span>"no memory"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>for</span><span> </span><span>len</span><span> </span><span>in</span><span> [</span><span>1</span><span>, </span><span>1</span><span>, </span><span>24</span><span>] {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>slice</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>len</span><span>).</span><span>expect</span><span>(</span><span>"full"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>println!</span><span>(</span><span>"len {len:&gt;2} at address ...{:02x}"</span><span>, </span><span>slice</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span> &amp; </span><span>0xff</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>println!</span><span>(</span><span>"   used = {}"</span><span>, </span><span>arena</span><span>.</span><span>used</span><span>());</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"a fourth, 8-byte request: {:?}"</span><span>, </span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>8</span><span>));</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>len  1 at address ...f0</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>used = 1</span></div></div><div><div><div>3</div></div><div><span>len  1 at address ...00</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>used = 17</span></div></div><div><div><div>5</div></div><div><span>len 24 at address ...10</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>used = 56</span></div></div><div><div><div>7</div></div><div><span>a fourth, 8-byte request: None</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一个 1 字节从 <code>...f0</code> 开始，用掉 1；第二个也是 1 字节，但它的地址是 <code>...00</code>，比上一个多了 16，不是 1：C 的分配器把偏移对齐到 16 的倍数（<code>PEBBLE_ALIGNMENT</code>），所以 <code>used</code> 是 <code>16 + 1 = 17</code>。第三个从 32 开始，24 字节，<code>used = 56</code>。第四个要 8 字节，下一个对齐点是 64，<code>64 + 8 &gt; 64</code>，放不下，返回 <code>None</code>。这四个数字和 C 里的 <code>pebble_align_up</code> 完全吻合。</p></section><section><h3>其余的方法与 Send<a href="#其余的方法与-send"><span>#</span></a></h3><p><code>used</code>、<code>capacity</code>、<code>reset</code> 都是一行 <code>unsafe</code> 调用加一句 <code>SAFETY</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Bytes handed out so far, including alignment padding between allocations.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>used</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>// SAFETY: `self.ptr` is a live arena and the C function only reads it.</span></div></div><div><div><div>5</div></div><div><span>        </span><span>unsafe</span><span> { </span><span>pebble_arena_used</span><span>(</span><span>self</span><span>.ptr.</span><span>as_ptr</span><span>()) }</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>/// Total bytes this arena can hand out.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>capacity</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>// SAFETY: `self.ptr` is a live arena and the C function only reads it.</span></div></div><div><div><div>12</div></div><div><span>        </span><span>unsafe</span><span> { </span><span>pebble_arena_capacity</span><span>(</span><span>self</span><span>.ptr.</span><span>as_ptr</span><span>()) }</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>/// Rewinds the arena, making all of its capacity available again.</span></div></div><div><div><div>16</div></div><div><span>    </span><span>///</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// Every slice previously returned by [`Arena::alloc`] is invalidated, which</span></div></div><div><div><div>18</div></div><div><span>    </span><span>/// the exclusive `&amp;mut self` borrow already enforces.</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>reset</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>// SAFETY: `self.ptr` is a live arena. The C function only updates the</span></div></div><div><div><div>21</div></div><div><span>        </span><span>// bump offset; `&amp;mut self` guarantees no outstanding slice.</span></div></div><div><div><div>22</div></div><div><span>        </span><span>unsafe</span><span> { </span><span>pebble_arena_reset</span><span>(</span><span>self</span><span>.ptr.</span><span>as_ptr</span><span>()) };</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>reset</code> 要 <code>&amp;mut self</code>，这样已经借出去的切片都必须先死掉，这一条在上面的 <code>overlap</code> 示例里就是缺的那个。</p><p>最后，<code>Arena</code> 能不能跨线程？C 的 arena 没有锁，所以不能。实验 <code>arena_send.rs</code>（同一个 lab）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `NonNull&lt;pebble_sys::PebbleArena&gt;` cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; src/bin/arena_send.rs:6:32</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>6 |     let handle = thread::spawn(move || arena.used());</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                  ------------- -------^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|                  |             |</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|                  |             `NonNull&lt;pebble_sys::PebbleArena&gt;` cannot be sent between threads safely</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>|                  |             within this `{closure@src/bin/arena_send.rs:6:32: 6:39}`</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|                  required by a bound introduced by this call</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>= help: within `{closure@src/bin/arena_send.rs:6:32: 6:39}`, the trait `Send` is not implemented for `NonNull&lt;pebble_sys::PebbleArena&gt;`</span></div></div><div><div><div>12</div></div><div><span>note: required because it appears within the type `Arena`</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>--&gt; /tmp/pebble-rebuild-13/crates/pebble-sys/src/lib.rs:107:12</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>107 | pub struct Arena {</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>|            ^^^^^</span></div></div><div><div><div>17</div></div><div><span>note: required because it's used within this closure</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>--&gt; src/bin/arena_send.rs:6:32</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>6 |     let handle = thread::spawn(move || arena.used());</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>|                                ^^^^^^^</span></div></div><div><div><div>22</div></div><div><span>note: required by a bound in `spawn`</span></div></div><div><div><div>23</div></div><div><span><span>   </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/std/src/thread/functions.rs:125:0</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>26</div></div><div><span>error: could not compile `lab-ch13-sys` (bin "arena_send") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>NonNull&lt;PebbleArena&gt;</code> 不是 <code>Send</code>，所以包着它的 <code>Arena</code> 不是，编译器当场拒绝了。这个报错的理由前面已经讲完了，这里的价值是确认：不需要写一行代码，保护就在了。线程的事第 14 篇再细讲。</p><p>文件头的 <code>use</code> 现在是 <code>std::ffi::c_void</code>、<code>std::marker::{PhantomData, PhantomPinned}</code>、<code>std::ptr::NonNull</code>、<code>std::slice</code>，编译器会告诉你缺哪个。测试：写 <code>arena_alloc_used_and_reset</code>（分配、写入、读回、<code>reset</code>、再整块分配），写 <code>arena_exhaustion_returns_none</code>；另外三个（<code>arena_reports_capacity</code>、<code>arena_allocations_do_not_overlap</code>、<code>zero_capacity_arena_allocation_fails</code>）复制 tag 里的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>arena_alloc_used_and_reset</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>Arena</span><span>::</span><span>with_capacity</span><span>(</span><span>64</span><span>).</span><span>expect</span><span>(</span><span>"arena creation failed"</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>region</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>8</span><span>).</span><span>expect</span><span>(</span><span>"first allocation failed"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>region</span><span>.</span><span>len</span><span>(), </span><span>8</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>region</span><span>.</span><span>copy_from_slice</span><span>(&amp;[</span><span>7</span><span>u8</span><span>; </span><span>8</span><span>]);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>region</span><span>, &amp;[</span><span>7</span><span>u8</span><span>; </span><span>8</span><span>]);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>used_after_first</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>used</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert!</span><span>(</span><span>used_after_first</span><span><span> </span><span>&gt;=</span><span> </span></span><span>8</span><span>, </span><span>"used = {used_after_first}"</span><span>);</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>_second</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>8</span><span>).</span><span>expect</span><span>(</span><span>"second allocation failed"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert!</span><span>(</span><span>arena</span><span>.</span><span>used</span><span>() &gt; </span><span>used_after_first</span><span>);</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>        </span><span>arena</span><span>.</span><span>reset</span><span>();</span></div></div><div><div><div>17</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>arena</span><span>.</span><span>used</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>        </span><span>// After a reset the whole capacity is available again.</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>whole</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>64</span><span>).</span><span>expect</span><span>(</span><span>"allocation after reset failed"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>whole</span><span>.</span><span>len</span><span>(), </span><span>64</span><span>);</span></div></div><div><div><div>22</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>arena</span><span>.</span><span>used</span><span>(), </span><span>64</span><span>);</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>26</div></div><div><span>    </span><span>fn</span><span> </span><span>arena_exhaustion_returns_none</span><span>() {</span></div></div><div><div><div>27</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>Arena</span><span>::</span><span>with_capacity</span><span>(</span><span>16</span><span>).</span><span>expect</span><span>(</span><span>"arena creation failed"</span><span>);</span></div></div><div><div><div>28</div></div><div><span>        </span><span>assert!</span><span>(</span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>16</span><span>).</span><span>is_some</span><span>());</span></div></div><div><div><div>29</div></div><div><span>        </span><span>assert!</span><span>(</span><span>arena</span><span>.</span><span>alloc</span><span>(</span><span>1</span><span>).</span><span>is_none</span><span>());</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-sys</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test tests::arena_alloc_used_and_reset ... ok</span></div></div><div><div><div>2</div></div><div><span>test tests::arena_exhaustion_returns_none ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::arena_allocations_do_not_overlap ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::arena_reports_capacity ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::fnv1a_hello_matches_reference ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::fnv1a_foobar_matches_reference ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::fnv1a_empty_is_offset_basis ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::zero_capacity_arena_allocation_fails ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::fnv1a_long_input_matches_rust_reference ... ok</span></div></div><div><div><div>10</div></div><div><span>test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>Doc-tests pebble_sys</span></div></div><div><div><div>12</div></div><div><span>test crates/pebble-sys/src/lib.rs - fnv1a (line 42) ... ok</span></div></div><div><div><div>13</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>9 个单元测试加 1 个文档测试。这是我在重建目录里真的跑过的”只含 <code>fnv1a</code> 和 <code>Arena</code>”的中间状态；<code>arena_alloc_used_and_reset</code> 里最后那句 <code>assert_eq!(arena.used(), 64)</code> 证明了 <code>reset</code> 之后整块容量都回来了。</p></section></section>
<section><h2>回调：闭包怎么穿过一个 void*<a href="#回调闭包怎么穿过一个-void"><span>#</span></a></h2><p>最后一个包装是 <code>fold</code>：C 里 <code>pebble_fold(init, lo, hi, cb, ctx)</code> 对 <code>lo..=hi</code> 里的每个整数调用 <code>cb(acc, value, ctx)</code>。我们希望 Rust 这边的接口是 <code>fold(0, 1..=100, |acc, value| acc + value)</code>，传一个闭包。</p><p>直接传不行。试试把闭包赋给 C 回调的类型（<code>closure_cb.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>scale</span><span><span> </span><span>=</span><span> </span></span><span>3</span><span>;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>cb</span><span>: </span><span>extern</span><span> </span><span>"C"</span><span> </span><span>fn</span><span>(</span><span>i64</span><span>, </span><span>i64</span><span>, *</span><span>mut</span><span> </span><span>c_void</span><span>) -&gt; </span><span>i64</span><span><span> </span><span>=</span><span> </span><span>|</span></span><span>acc</span><span>, </span><span>value</span><span>, </span><span>_ctx</span><span><span>|</span><span> </span></span><span>acc</span><span> + </span><span>value</span><span> * </span><span>scale</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/closure_cb.rs:5:59</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 |     let cb: extern "C" fn(i64, i64, *mut c_void) -&gt; i64 = |acc, value, _ctx| acc + value * scale;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|             -------------------------------------------   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected fn pointer, found closure</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|             |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|             expected due to this</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>= note: expected fn pointer `extern "C" fn(i64, i64, *mut c_void) -&gt; i64`</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>found closure `{closure@src/bin/closure_cb.rs:5:59: 5:77}`</span></div></div><div><div><div>11</div></div><div><span>note: closures can only be coerced to `fn` types if they do not capture any variables</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>--&gt; src/bin/closure_cb.rs:5:92</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>5 |     let cb: extern "C" fn(i64, i64, *mut c_void) -&gt; i64 = |acc, value, _ctx| acc + value * scale;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|                                                                                            ^^^^^ `scale` captured here</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>For more information about this error, try `rustc --explain E0308`.</span></div></div><div><div><div>18</div></div><div><span>error: could not compile `lab-ch13-sys` (bin "closure_cb") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>“闭包只有在不捕获任何变量时才能转成 <code>fn</code> 类型”，原因第 8 篇讲过：闭包是一个带数据的结构体，函数指针只是一个地址，没地方放 <code>scale</code>。而且即使不捕获，闭包的调用约定是 Rust 的，也不是 C 的。所以我们需要两件东西：一个真正的 <code>extern "C" fn</code>（蹦床，trampoline），和一条把闭包传过去的通道。通道就是 C 给我们的 <code>void *ctx</code>：我们把”闭包加上别的状态”打成一个结构体，把它的地址当成 <code>void *</code> 交给 C，C 原样还回来，蹦床把它还原成引用，调用闭包。</p><p>在 <code>lib.rs</code> 里先加类型别名，再加到 <code>extern</code> 块里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The callback signature `pebble_fold` calls for every value.</span></div></div><div><div><div>2</div></div><div><span>type</span><span> </span><span>FoldFn</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> </span><span>extern</span><span> </span><span>"C"</span><span> </span><span>fn</span><span>(</span><span>i64</span><span>, </span><span>i64</span><span>, *</span><span>mut</span><span> </span><span>c_void</span><span>) -&gt; </span><span>i64</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>extern</code> 块里再加一条：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Calls `cb` once for every integer in `[lo, hi]`, in order.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>pebble_fold</span><span>(</span><span>init</span><span>: </span><span>i64</span><span>, </span><span>lo</span><span>: </span><span>i64</span><span>, </span><span>hi</span><span>: </span><span>i64</span><span>, </span><span>cb</span><span>: </span><span>FoldFn</span><span>, </span><span>ctx</span><span>: *</span><span>mut</span><span> </span><span>c_void</span><span>) -&gt; </span><span>i64</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>上下文结构体和蹦床：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// State shared between [`fold`] and its C trampoline.</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>FoldCtx</span><span>&lt;'</span><span>a</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// The caller's closure.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>f</span><span>: &amp;'</span><span>a</span><span> </span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>i64</span><span>, </span><span>i64</span><span>) -&gt; </span><span>i64</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// Set by the trampoline if the closure panicked.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>panic</span><span>: </span><span>Option</span><span>&lt;</span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Any</span><span> + </span><span>Send</span><span> + '</span><span>static</span><span>&gt;&gt;,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>&amp;'a mut dyn FnMut(i64, i64) -&gt; i64</code> 是第 8、9 篇的老朋友：一个指向”任何可变闭包”的胖指针。<code>'a</code> 是生命周期参数（第 4 篇）：上下文借用着闭包，不能比它活得久。<code>panic</code> 字段先放着，一会儿讲。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The Rust function C calls once per value, cast to `pebble_fold_fn`.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// A panic must not unwind into C, so the closure runs inside</span></div></div><div><div><div>4</div></div><div><span>/// [`catch_unwind`]; the payload is stashed in the context and the panic is</span></div></div><div><div><div>5</div></div><div><span>/// resumed in [`fold`] after C has returned.</span></div></div><div><div><div>6</div></div><div><span>extern</span><span> </span><span>"C"</span><span> </span><span>fn</span><span> </span><span>fold_trampoline</span><span>(</span><span>acc</span><span>: </span><span>i64</span><span>, </span><span>value</span><span>: </span><span>i64</span><span>, </span><span>ctx</span><span>: *</span><span>mut</span><span> </span><span>c_void</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>// SAFETY: `fold` passes a pointer to a `FoldCtx` that outlives the whole</span></div></div><div><div><div>8</div></div><div><span>    </span><span>// `pebble_fold` call, passes the same pointer to every invocation, and the</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// trampoline is the only callback C is given for that call.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>ctx</span><span>: &amp;</span><span>mut</span><span> </span><span>FoldCtx</span><span>&lt;'</span><span>_</span><span><span>&gt; </span><span>=</span><span> </span></span><span>unsafe</span><span> { &amp;</span><span>mut</span><span> *</span><span>ctx</span><span>.</span><span>cast</span><span>() };</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> </span><span>ctx</span><span>.panic.</span><span>is_some</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>// The closure already panicked; do not call into possibly-broken state</span></div></div><div><div><div>13</div></div><div><span>        </span><span>// again while C keeps walking the range.</span></div></div><div><div><div>14</div></div><div><span>        </span><span>return</span><span> </span><span>acc</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>    </span><span>match</span><span> </span><span>catch_unwind</span><span>(</span><span>AssertUnwindSafe</span><span><span>(</span><span>||</span><span> (</span></span><span>ctx</span><span>.f)(</span><span>acc</span><span>, </span><span>value</span><span>))) {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>Ok</span><span>(</span><span>next</span><span>) =&gt; </span><span>next</span><span>,</span></div></div><div><div><div>18</div></div><div><span>        </span><span>Err</span><span>(</span><span>payload</span><span>) =&gt; {</span></div></div><div><div><div>19</div></div><div><span>            </span><span>ctx</span><span><span>.panic </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>payload</span><span>);</span></div></div><div><div><div>20</div></div><div><span>            </span><span>acc</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>函数签名里的 <code>extern "C" fn</code> 表示”这个 Rust 函数使用 C 的调用约定”，所以它的地址可以当成 <code>pebble_fold_fn</code> 交给 C。（<code>FoldFn</code> 别名里写的是 <code>unsafe extern "C" fn</code>，一个安全的 <code>extern "C" fn</code> 可以自动转成它。）第一行 <code>unsafe { &amp;mut *ctx.cast() }</code> 把 <code>void *</code> 转回 <code>&amp;mut FoldCtx</code>：<code>cast()</code> 是裸指针之间的类型转换，类型由左边的标注 <code>&amp;mut FoldCtx&lt;'_&gt;</code> 推出来。这里的 <code>SAFETY</code> 是整个模块里最重要的一句：我们声明”<code>fold</code> 传进来的就是一个活着的 <code>FoldCtx</code>，并且每次回调都是同一个”。编译器对此毫无办法验证，C 那边的 <code>void *</code> 不带类型也不带生命周期。</p><section><h3>如果闭包 panic 了<a href="#如果闭包-panic-了"><span>#</span></a></h3><p>蹦床的 <code>catch_unwind</code> 是干嘛的？先看看没有它会怎样。实验 <code>fold_nocatch.rs</code>（同一个 lab，开头加一行 <code>use pebble_sys as _;</code> 让链接器带上 C 库）直接声明 <code>pebble_fold</code>，传一个遇到 <code>value == 3</code> 就 panic 的 <code>extern "C"</code> 回调：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>extern</span><span> </span><span>"C"</span><span> </span><span>fn</span><span> </span><span>exploding</span><span>(</span><span>acc</span><span>: </span><span>i64</span><span>, </span><span>value</span><span>: </span><span>i64</span><span>, </span><span>_ctx</span><span>: *</span><span>mut</span><span> </span><span>c_void</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>assert_ne!</span><span>(</span><span>value</span><span>, </span><span>3</span><span>, </span><span>"callback exploded"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>acc</span><span> + </span><span>value</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出（原始输出里有一段二十多行的回溯，我保留了头尾，中间用 <code>...</code> 标出）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>thread 'main' (64208) panicked at src/bin/fold_nocatch.rs:15:5:</span></div></div><div><div><div>2</div></div><div><span>assertion `left != right` failed: callback exploded</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>left: 3</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>right: 3</span></div></div><div><div><div>5</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>thread 'main' (64208) panicked at /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/panicking.rs:225:5:</span></div></div><div><div><div>8</div></div><div><span>panic in a function that cannot unwind</span></div></div><div><div><div>9</div></div><div><span>stack backtrace:</span></div></div><div><div><div>10</div></div><div><span>...</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>12:     0x5c5c915621f6 - fold_nocatch::exploding::heb0ddd44d5d3d388</span></div></div><div><div><div>12</div></div><div><span><span>                               </span></span><span>at /tmp/lab-ch13-sys/src/bin/fold_nocatch.rs:14:1</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>13:     0x5c5c91562396 - pebble_fold</span></div></div><div><div><div>14</div></div><div><span><span>                               </span></span><span>at /tmp/pebble-rebuild-13/crates/pebble-sys/csrc/pebble_shim.c:120:15</span></div></div><div><div><div>15</div></div><div><span>...</span></div></div><div><div><div>16</div></div><div><span>thread caused non-unwinding panic. aborting.</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>进程被 <code>abort</code> 了（退出码 134）。注意第 13 帧：回溯里同时有 Rust 的 <code>exploding</code> 和 C 的 <code>pebble_fold</code>（带着 <code>pebble_shim.c:120</code> 的行号），说明回调确实是从 C 栈帧里被调用的。</p><p>Rust 的 panic 默认通过”栈展开”（unwinding）实现：沿着调用栈往回走，逐帧运行析构函数。C 的栈帧不是为 Rust 的展开准备的：C 没有析构函数可调用，编译器也不保证它带着展开所需的信息（C++ 的异常穿过 C 帧同理）。所以语言规定，panic 一旦要穿过 <code>extern "C"</code> 边界，就是未定义行为，不能放行。我这台机器的 rustc 1.95.0 的做法是：在 <code>extern "C" fn</code> 的边界上把它变成 <code>abort</code>，也就是上面的 “panic in a function that cannot unwind”。我听说更老的编译器对此是未定义行为或者直接展开进 C 的栈帧，但我没装老版本去验证。</p><p>不想进程被杀，就必须在边界上把 panic 拦下来。这就是 <code>catch_unwind</code>：运行一个闭包，如果里面 panic 了，不展开出去，而是把 panic 变成一个 <code>Err(payload)</code> 返回。</p></section><section><h3>AssertUnwindSafe 和 Box&lt;dyn Any + Send&gt;<a href="#assertunwindsafe-和-boxdyn-any--send"><span>#</span></a></h3><p><code>catch_unwind</code> 有两个陌生的细节。第一，直接把我们的闭包放进去会被编译器拒绝，实验 <code>ch13_unwind_safe.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>total</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>catch_unwind</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>total</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>panic!</span><span>(</span><span>"boom"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: the type `&amp;mut i32` may not be safely transferred across an unwind boundary</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch13_unwind_safe.rs:5:31</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 |       let result = catch_unwind(|| {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                    ------------ ^-</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                    |            |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|  __________________|____________within this `{closure@src/bin/ch13_unwind_safe.rs:5:31: 5:33}`</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>| |                  |</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>| |                  required by a bound introduced by this call</span></div></div><div><div><div>10</div></div><div><span>6 | |         total += 1;</span></div></div><div><div><div>11</div></div><div><span>7 | |         panic!("boom");</span></div></div><div><div><div>12</div></div><div><span>8 | |     });</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>| |_____^ `&amp;mut i32` may not be safely transferred across an unwind boundary</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>= help: within `{closure@src/bin/ch13_unwind_safe.rs:5:31: 5:33}`, the trait `UnwindSafe` is not implemented for `&amp;mut i32`</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>= note: `UnwindSafe` is implemented for `&amp;i32`, but not for `&amp;mut i32`</span></div></div><div><div><div>17</div></div><div><span>note: required because it's used within this closure</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch13_unwind_safe.rs:5:31</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>20</div></div><div><span>5 |     let result = catch_unwind(|| {</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>|                               ^^</span></div></div><div><div><div>22</div></div><div><span>note: required by a bound in `std::panic::catch_unwind`</span></div></div><div><div><div>23</div></div><div><span><span> </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/std/src/panic.rs:358:0</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>26</div></div><div><span>error: could not compile `rust-lab` (bin "ch13_unwind_safe") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>UnwindSafe</code> 是另一个标记 trait，意思是”panic 之后你还能放心地观察这个值”。可变引用不满足它：闭包可能改了一半就 panic，改到一半的数据被外面看到，不变式就破了。这不是内存安全问题（所以这个 trait 不是 <code>unsafe</code>），而是逻辑安全。我们的蹦床里，panic 之后 <code>ctx.panic</code> 被设置，之后的调用全部跳过闭包，闭包的捕获状态不会再被读取；所以我们可以向编译器担保没问题，写 <code>AssertUnwindSafe(|| ...)</code>，这是一个什么也不做的包装，仅仅用来表态：“我检查过了”。</p><p>第二，<code>catch_unwind</code> 返回的 <code>Err</code> 里装的是 <code>Box&lt;dyn Any + Send + 'static&gt;</code>：panic 可以携带任意类型的值。<code>Any</code> 是”可以在运行时问你是什么类型”的 trait（第 8 篇的 trait 对象再加一个 <code>downcast_ref</code>），实验 <code>ch13_any_payload.rs</code> 看三种 panic 带的是什么：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>literal</span><span><span> </span><span>=</span><span> </span></span><span>catch_unwind</span><span><span>(</span><span>||</span><span> </span></span><span>panic!</span><span>(</span><span>"a literal"</span><span>)).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>3</span><span>;</span></div></div><div><div><div>3</div></div><div><span>let</span><span> </span><span>formatted</span><span><span> </span><span>=</span><span> </span></span><span>catch_unwind</span><span><span>(</span><span>||</span><span> </span></span><span>panic!</span><span>(</span><span>"formatted {n}"</span><span>)).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>4</div></div><div><span>let</span><span> </span><span>custom</span><span><span> </span><span>=</span><span> </span></span><span>catch_unwind</span><span><span>(</span><span>||</span><span> </span><span>std</span><span>::</span><span>panic</span><span>::</span></span><span>panic_any</span><span>(</span><span>42_</span><span>i32</span><span>)).</span><span>unwrap_err</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&amp;str: a literal</span></div></div><div><div><div>2</div></div><div><span>String: formatted 3</span></div></div><div><div><div>3</div></div><div><span>something else</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>thread 'main' (60807) panicked at src/bin/ch13_any_payload.rs:15:35:</span></div></div><div><div><div>6</div></div><div><span>a literal</span></div></div><div><div><div>7</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>thread 'main' (60807) panicked at src/bin/ch13_any_payload.rs:17:37:</span></div></div><div><div><div>10</div></div><div><span>formatted 3</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>thread 'main' (60807) panicked at src/bin/ch13_any_payload.rs:18:34:</span></div></div><div><div><div>13</div></div><div><span>Box&lt;dyn Any&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>字面量消息的负载是 <code>&amp;'static str</code>，格式化消息的负载是 <code>String</code>，<code>panic_any(42)</code> 的负载是 <code>i32</code>，所以第三个哪个 <code>downcast_ref</code> 都不中，只能打印”something else”。（输出里三条 <code>panicked at</code> 是默认的 panic 钩子在往 stderr 打印，不是我们的代码打的。）蹦床不关心负载是什么，把整个 <code>Box&lt;dyn Any + Send&gt;</code> 存进 <code>FoldCtx.panic</code>，等 C 返回之后原样交还给 Rust。</p></section><section><h3>resume_unwind：等 C 走完再重新抛<a href="#resume_unwind等-c-走完再重新抛"><span>#</span></a></h3><p>现在 <code>fold</code> 本体就好写了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Folds `range` through `f` inside C, returning the final accumulator.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// C calls `f(acc, value)` once per integer in `range`, in ascending order, with</span></div></div><div><div><div>4</div></div><div><span>/// `init` as the initial accumulator; each call receives the previous call's</span></div></div><div><div><div>5</div></div><div><span>/// result. An empty range (`start &gt; end`) returns `init` without invoking `f`.</span></div></div><div><div><div>6</div></div><div><span>///</span></div></div><div><div><div>7</div></div><div><span>/// The closure is boxed and passed to C as an opaque context pointer, so it may</span></div></div><div><div><div>8</div></div><div><span>/// capture state mutably.</span></div></div><div><div><div>9</div></div><div><span>///</span></div></div><div><div><div>10</div></div><div><span>/// # Panics</span></div></div><div><div><div>11</div></div><div><span>///</span></div></div><div><div><div>12</div></div><div><span>/// A panic in `f` is caught at the FFI boundary and resumed on this side once C</span></div></div><div><div><div>13</div></div><div><span>/// has unwound its own frames: unwinding through C would be undefined</span></div></div><div><div><div>14</div></div><div><span>/// behaviour. The panic payload is preserved.</span></div></div><div><div><div>15</div></div><div><span>///</span></div></div><div><div><div>16</div></div><div><span>/// ```</span></div></div><div><div><div>17</div></div><div><span>/// # use pebble_sys::fold;</span></div></div><div><div><div>18</div></div><div><span>/// assert_eq!(fold(0, 1..=100, |acc, value| acc + value), 5050);</span></div></div><div><div><div>19</div></div><div><span>/// ```</span></div></div><div><div><div>20</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>fold</span><span>(</span><span>init</span><span>: </span><span>i64</span><span>, </span><span>range</span><span>: </span><span>RangeInclusive</span><span>&lt;</span><span>i64</span><span>&gt;, </span><span>f</span><span>: </span><span>impl</span><span> </span><span>FnMut</span><span>(</span><span>i64</span><span>, </span><span>i64</span><span>) -&gt; </span><span>i64</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>i64</span><span>, </span><span>i64</span><span>) -&gt; </span><span>i64</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Box</span><span>::</span><span>new</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>ctx</span><span><span> </span><span>=</span><span> </span></span><span>FoldCtx</span><span> {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>f</span><span>: &amp;</span><span>mut</span><span> *</span><span>f</span><span>,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>panic</span><span>: </span><span>None</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>26</div></div><div><span>    </span><span>// SAFETY: `fold_trampoline` is a valid `pebble_fold_fn` and `ctx` outlives</span></div></div><div><div><div>27</div></div><div><span>    </span><span>// the call. C invokes the callback only while `pebble_fold` runs, only with</span></div></div><div><div><div>28</div></div><div><span>    </span><span>// `ctx` as given, and retains nothing after it returns. `ctx` is not shared:</span></div></div><div><div><div>29</div></div><div><span>    </span><span>// C is single-threaded here and the range walk is sequential.</span></div></div><div><div><div>30</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> {</span></div></div><div><div><div>31</div></div><div><span>        </span><span>pebble_fold</span><span>(</span></div></div><div><div><div>32</div></div><div><span>            </span><span>init</span><span>,</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>*</span><span>range</span><span>.</span><span>start</span><span>(),</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>*</span><span>range</span><span>.</span><span>end</span><span>(),</span></div></div><div><div><div>35</div></div><div><span>            </span><span>fold_trampoline</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>std</span><span>::</span><span>ptr</span><span>::</span><span>from_mut</span><span>(&amp;</span><span>mut</span><span> </span><span>ctx</span><span>).</span><span>cast</span><span>::&lt;</span><span>c_void</span><span>&gt;(),</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>payload</span><span><span>) </span><span>=</span><span> </span></span><span>ctx</span><span>.panic.</span><span>take</span><span>() {</span></div></div><div><div><div>41</div></div><div><span>        </span><span>// `ctx` is no longer borrowed by anything, and C has returned, so it is</span></div></div><div><div><div>42</div></div><div><span>        </span><span>// safe to unwind now with the original payload.</span></div></div><div><div><div>43</div></div><div><span>        </span><span>resume_unwind</span><span>(</span><span>payload</span><span>);</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>    </span><span>result</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐段读：<code>Box::new(f)</code> 把闭包放到堆上再转成 <code>Box&lt;dyn FnMut&gt;</code>；<code>&amp;mut *f</code> 取出里面的 <code>&amp;mut dyn FnMut</code> 放进上下文。（坦白：这个 <code>Box</code> 是多余的。我在重建目录里把它换成 <code>let mut f = f;</code> 加 <code>f: &amp;mut f</code>，15 个测试 2 个文档测试照样全过，因为 <code>&amp;mut impl FnMut</code> 可以直接转成 <code>&amp;mut dyn FnMut</code>。tag 里就是上面的写法，所以我也照抄；把它删掉可以当成一个练习。）<code>std::ptr::from_mut(&amp;mut ctx).cast::&lt;c_void&gt;()</code> 把上下文的地址变成 <code>*mut c_void</code>，这是把”引用”变成”裸指针”的现代写法，比 <code>as *mut _</code> 更不容易手滑。<code>ctx</code> 活在 <code>fold</code> 的栈帧里，C 的调用结束前不会被移动或销毁，这就是 <code>SAFETY</code> 注释里”<code>ctx</code> outlives the call”的依据。</p><p>C 返回之后，如果 <code>ctx.panic</code> 里有负载，就用 <code>resume_unwind(payload)</code> 在 Rust 这一侧重新抛出。<code>resume_unwind</code> 和 <code>panic!</code> 的区别是它不会再调用 panic 钩子，不会再打印第二遍消息。实验 <code>fold_panic.rs</code> 从外面用 <code>catch_unwind</code> 包住 <code>fold</code>，验证负载原样保留：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>caught on the Rust side: assertion `left != right` failed: closure exploded</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>left: 3</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>right: 3</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>thread 'main' (62791) panicked at src/bin/fold_panic.rs:6:13:</span></div></div><div><div><div>6</div></div><div><span>assertion `left != right` failed: closure exploded</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>left: 3</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>right: 3</span></div></div><div><div><div>9</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>panicked at</code> 只出现一次（来自原始的 <code>assert_ne!</code>），而外面收到的 <code>String</code> 负载正是 <code>assertion ... closure exploded</code>。</p><p>一个小细节，蹦床里 <code>if ctx.panic.is_some() { return acc; }</code>：C 的 <code>pebble_fold</code> 不知道出过事，会继续把剩下的整数一个个喂给回调。第一次 panic 之后我们不想再碰那个很可能处于半坏状态的闭包，所以后面的调用空转，直接把累加值还回去。</p><p>最后补 <code>lib.rs</code> 里 <code>fold</code> 需要的 <code>use</code>，然后写三个 <code>fold</code> 的测试：<code>fold_sums_inclusive_range</code>、<code>fold_closure_can_capture_mutable_state</code>（闭包里 <code>calls += 1</code>，证明蹦床能带着可变状态）、<code>fold_resumes_closure_panic</code>（<code>#[should_panic(expected = "closure exploded")]</code>），追加到 <code>mod tests</code> 里；其余三个（空区间、访问顺序、<code>i64::MAX</code> 边界）复制 tag：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fold_sums_inclusive_range</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>fold</span><span>(</span><span>0</span><span>, </span><span>1</span><span>..=</span><span>100</span><span><span>, </span><span>|</span></span><span>acc</span><span>, </span><span>value</span><span><span>|</span><span> </span></span><span>acc</span><span> + </span><span>value</span><span>), </span><span>5050</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>fold_closure_can_capture_mutable_state</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>calls</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>fold</span><span>(</span><span>1</span><span>, </span><span>0</span><span>..=</span><span>5</span><span><span>, </span><span>|</span></span><span>acc</span><span>, </span><span>value</span><span><span>|</span><span> {</span></span></div></div><div><div><div>10</div></div><div><span>            </span><span>calls</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>11</div></div><div><span>            </span><span>acc</span><span> * (</span><span>value</span><span> + </span><span>1</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>13</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>calls</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>result</span><span>, </span><span>720</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>#[should_panic(expected </span><span>=</span><span> </span><span>"closure exploded"</span><span>)]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>fn</span><span> </span><span>fold_resumes_closure_panic</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>fold</span><span>(</span><span>0</span><span>, </span><span>1</span><span>..=</span><span>10</span><span><span>, </span><span>|</span></span><span>acc</span><span>, </span><span>value</span><span><span>|</span><span> {</span></span></div></div><div><div><div>21</div></div><div><span>            </span><span>assert_ne!</span><span>(</span><span>value</span><span>, </span><span>3</span><span>, </span><span>"closure exploded"</span><span>);</span></div></div><div><div><div>22</div></div><div><span>            </span><span>acc</span><span> + </span><span>value</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>fold_handles_int64_max_bound</code> 和 <code>fold_empty_range_returns_init</code> 这两个复制来的测试，正是测试 C 代码里”循环变量不溢出”和”<code>lo &gt; hi</code> 返回 <code>init</code>”那两个细节。</p><p>到这里，<code>pebble-sys</code> 的所有代码都有了。<code>lib.rs</code> 的最后几件杂事：顶部那段 34 行的 <code>//!</code> 模块文档（描述这个 crate 的健全性论证，纯注释，不影响编译）照抄 tag，文件头的 <code>use</code> 一共七行，最终是这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>any</span><span>::</span></span><span>Any</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>ffi</span><span>::c_void;</span></span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>marker</span><span>::{</span></span><span>PhantomData</span><span>, </span><span>PhantomPinned</span><span>};</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>ops</span><span>::</span></span><span>RangeInclusive</span><span>;</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>panic</span><span>::{</span></span><span>AssertUnwindSafe</span><span>, catch_unwind, resume_unwind};</span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>ptr</span><span>::</span></span><span>NonNull</span><span>;</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::slice;</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个文件的顺序是：<code>//!</code> 文档、<code>use</code>、<code>PebbleArena</code>、<code>FoldFn</code>、<code>extern</code> 块、<code>fnv1a</code>、<code>Arena</code> 与 <code>impl</code>、<code>impl Drop</code>、<code>FoldCtx</code>、<code>fold_trampoline</code>、<code>fold</code>、<code>mod tests</code>。检查点里会对着 tag 逐字节确认。</p></section></section>
<section><h2>反过来：让 C 调用 Rust，以及五件 Pebble 没用到的东西<a href="#反过来让-c-调用-rust以及五件-pebble-没用到的东西"><span>#</span></a></h2><p><code>pebble-sys</code> 只做了一个方向：Rust 调 C。反方向（C 或 Python 或 Go 调你的 Rust 库）和 FFI 的另外几个常见零件，Pebble 里没有地方自然地用到它们，我不想为了凑特性往仓库里硬塞，所以这一节用实验室里的小程序讲。每个例子都编译运行过，输出是真的。</p><section><h3>no_mangle 与 cdylib：做一个 C 能链接的共享库<a href="#no_mangle-与-cdylib做一个-c-能链接的共享库"><span>#</span></a></h3><p>C 要调用 Rust 函数，需要两样：一个符号名固定的函数，和一个 C 链接器认识的库格式。Rust 的函数名默认会被修饰（mangle，规则和 C++ 的类似），所以要用 <code>#[unsafe(no_mangle)]</code> 关掉；函数要用 <code>extern "C"</code> 声明调用约定；crate 要在 <code>Cargo.toml</code> 里声明 <code>crate-type = ["cdylib"]</code> 才会产出 <code>.so</code>。注意 <code>no_mangle</code> 在 edition 2024 里要包在 <code>unsafe(...)</code> 里写，因为”符号名撞车”是编译器看不见的风险。这个实验必须是独立的 crate（<code>~/lab-ch13-cdylib</code>，完整版在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch13-cdylib" target="_blank">lab/lab-ch13-cdylib</a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble_ffi_demo"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[</span><span>lib</span><span>]</span></div></div><div><div><div>7</div></div><div><span>crate-type</span><span> = [</span><span>"cdylib"</span><span>]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>[</span><span>workspace</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Callable from C as `int64_t pebble_add(int64_t, int64_t)`.</span></div></div><div><div><div>2</div></div><div><span>#[</span><span>unsafe</span><span>(no_mangle)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>extern</span><span> </span><span>"C"</span><span> </span><span>fn</span><span> </span><span>pebble_add</span><span>(</span><span>a</span><span>: </span><span>i64</span><span>, </span><span>b</span><span>: </span><span>i64</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>a</span><span> + </span><span>b</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/// Not exported under a stable name: Rust mangles it.</span></div></div><div><div><div>8</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>mangled</span><span>(</span><span>a</span><span>: </span><span>i64</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>a</span><span> * </span><span>2</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C 这一侧，<code>main.c</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;stdint.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int64_t</span><span> </span><span>pebble_add</span><span>(</span><span>int64_t</span><span><span> </span><span>a</span><span>,</span><span> </span></span><span>int64_t</span><span><span> </span><span>b</span><span>);</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>printf</span><span>(</span><span>"pebble_add(40, 2) = </span><span>%lld</span><span>\n</span><span>"</span><span>, (</span><span>long</span><span> </span><span>long</span><span>)</span><span>pebble_add</span><span>(</span><span>40</span><span>, </span><span>2</span><span>));</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span></div></div><div><div><div>2</div></div><div><span>nm</span><span> </span><span>-D</span><span> </span><span>target/debug/libpebble_ffi_demo.so</span><span> | </span><span>grep</span><span> </span><span>pebble_add</span></div></div><div><div><div>3</div></div><div><span>nm</span><span> </span><span>target/debug/libpebble_ffi_demo.so</span><span> | </span><span>grep</span><span> </span><span>-c</span><span> </span><span>mangled</span></div></div><div><div><div>4</div></div><div><span>gcc</span><span> </span><span>main.c</span><span> </span><span>-L</span><span> </span><span>target/debug</span><span> </span><span>-lpebble_ffi_demo</span><span> </span><span>-o</span><span> </span><span>main</span></div></div><div><div><div>5</div></div><div><span>LD_LIBRARY_PATH</span><span>=</span><span>target/debug</span><span> </span><span>./main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>0000000000007b30 T pebble_add</span></div></div><div><div><div>2</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble_add(40, 2) = 42</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行是 <code>nm -D</code>（动态符号表）里的 <code>pebble_add</code>，<code>T</code> 表示导出的函数；第二行的 <code>0</code> 是我数了一下符号表里有几个名字带 <code>mangled</code> 的：没有被 <code>no_mangle</code> 标记的那个函数根本没有出现在 <code>.so</code> 里（我猜是因为它既没导出、也没人调用，所以被丢掉了）。最后一行是 C 程序真的调用了 Rust 写的函数。<code>cdylib</code> 和 <code>-shared</code> 编出来的 <code>.so</code> 在 C 的一侧没有区别，这是 Python 的 <code>ctypes</code>、Node 的 <code>ffi</code>、Go 的 cgo 都能直接调用 Rust 库的原因。</p></section><section><h3>repr(transparent)：包一层但不改 ABI<a href="#reprtransparent包一层但不改-abi"><span>#</span></a></h3><p>给 C 句柄起一个有类型的名字，是很常见的需求：<code>struct Handle(u64)</code>。默认布局下，编译器不承诺这个包装和裸的 <code>u64</code> 在函数调用里被同样传递。<code>#[repr(transparent)]</code> 承诺：只有一个”非零大小”字段的结构体，布局和 ABI 与那个字段完全相同。实验 <code>ch13_repr_transparent.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::{align_of, size_of};</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[repr(transparent)]</span></div></div><div><div><div>4</div></div><div><span>struct</span><span> </span><span>Handle</span><span>(</span><span>u64</span><span>);</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>#[allow(dead_code)]</span></div></div><div><div><div>7</div></div><div><span>struct</span><span> </span><span>Plain</span><span>(</span><span>u64</span><span>, ());</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>unsafe</span><span> </span><span>extern</span><span> </span><span>"C"</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>abs</span><span>(</span><span>x</span><span>: </span><span>i32</span><span>) -&gt; </span><span>i32</span><span>;</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Handle: size {} align {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Handle</span><span>&gt;(), </span><span>align_of</span><span>::&lt;</span><span>Handle</span><span>&gt;());</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Plain:  size {} align {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Plain</span><span>&gt;(), </span><span>align_of</span><span>::&lt;</span><span>Plain</span><span>&gt;());</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>h</span><span><span> </span><span>=</span><span> </span></span><span>Handle</span><span>(</span><span>7</span><span>);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>println!</span><span>(</span><span>"inner = {}"</span><span>, </span><span>h</span><span>.</span><span>0</span><span>);</span></div></div><div><div><div>18</div></div><div><span>    </span><span>// SAFETY: abs has no preconditions beyond i32::MIN overflow.</span></div></div><div><div><div>19</div></div><div><span>    </span><span>println!</span><span>(</span><span>"abs(-5) = {}"</span><span>, </span><span>unsafe</span><span> { </span><span>abs</span><span>(-</span><span>5</span><span>) });</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Handle: size 8 align 8</span></div></div><div><div><div>2</div></div><div><span>Plain:  size 8 align 8</span></div></div><div><div><div>3</div></div><div><span>inner = 7</span></div></div><div><div><div>4</div></div><div><span>abs(-5) = 5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个输出看起来没什么：两者都是 8 字节（我没有骗你，实测 <code>Plain(u64, ())</code> 在这台机器上也是 8，布局碰巧一样）。区别在于承诺：<code>Handle</code> 可以放心地出现在 <code>extern "C"</code> 函数的签名里，当 <code>u64</code> 用；<code>Plain</code> 的布局和 ABI 则没有任何保证，哪天编译器改了也不算 bug。<code>repr(transparent)</code> 的 C++ 对应物大概是”标准布局的单成员包装类”，但 C++ 不替你承诺调用约定。</p></section><section><h3>repr(u8) 的 enum：和 C 的 enum 对表<a href="#repru8-的-enum和-c-的-enum-对表"><span>#</span></a></h3><p>第 3 篇说过，Rust 的 <code>enum</code> 默认是个带标签的联合，和 C 的 <code>enum</code>（就是个整数）不是一回事。要和 C 交换一个枚举值，用 <code>#[repr(u8)]</code>（或 <code>repr(i32)</code> 等）固定它的整数表示。实验 <code>ch13_repr_u8.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::size_of;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>#[repr(</span><span>u8</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>enum</span><span> </span><span>Status</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Ok</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Full</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Closed</span><span><span> </span><span>=</span><span> </span></span><span>7</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>TryFrom</span><span>&lt;</span><span>u8</span><span>&gt; </span><span>for</span><span> </span><span>Status</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>type</span><span> </span><span>Error</span><span><span> </span><span>=</span><span> </span></span><span>u8</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>try_from</span><span>(</span><span>raw</span><span>: </span><span>u8</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>, </span><span>u8</span><span>&gt; {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>match</span><span> </span><span>raw</span><span> {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>0</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Status</span><span>::</span><span>Ok</span><span>),</span></div></div><div><div><div>17</div></div><div><span>            </span><span>1</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Status</span><span>::</span><span>Full</span><span>),</span></div></div><div><div><div>18</div></div><div><span>            </span><span>7</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Status</span><span>::</span><span>Closed</span><span>),</span></div></div><div><div><div>19</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>Err</span><span>(</span><span>other</span><span>),</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;Status&gt;() = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Status</span><span>&gt;());</span></div></div><div><div><div>26</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Closed as u8 = {}"</span><span>, </span><span>Status</span><span>::</span><span>Closed</span><span> </span><span>as</span><span> </span><span>u8</span><span>);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>Status</span><span>::</span><span>try_from</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>28</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>Status</span><span>::</span><span>try_from</span><span>(</span><span>3</span><span>));</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size_of::&lt;Status&gt;() = 1</span></div></div><div><div><div>2</div></div><div><span>Closed as u8 = 7</span></div></div><div><div><div>3</div></div><div><span>Ok(Full)</span></div></div><div><div><div>4</div></div><div><span>Err(3)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>size_of::&lt;Status&gt;()</code> 是 1，<code>Status::Closed as u8</code> 是 7，和 C 的 <code>enum : uint8_t</code>（C++ 的 <code>enum class Status : uint8_t</code>）一致。反方向才是陷阱：C 给你一个 <code>uint8_t</code>，值是 3，你想把它当作 <code>Status</code>。如果用 <code>std::mem::transmute</code>，Rust 会把一个不是任何变体的字节当成 <code>Status</code>，这是未定义行为，Rust 的 enum 不像 C 那样允许”范围外的值”。所以正确做法是像实验里这样写一个 <code>TryFrom&lt;u8&gt;</code>，对每个合法值逐个匹配，不认识的返回 <code>Err</code>，也就是输出里的 <code>Err(3)</code>。Pebble 的 <code>Arena</code> 没有枚举值要跨越边界，所以这一节没有出现在仓库里。</p></section><section><h3>union：和 C 一样，读它是 unsafe<a href="#union和-c-一样读它是-unsafe"><span>#</span></a></h3><p>Rust 的 <code>union</code> 就是 C 的 <code>union</code>：几个字段共享同一块内存。区别是 Rust 里读取联合体字段必须 <code>unsafe</code>，因为编译器不知道你上次写的是哪个字段。实验 <code>ch13_union.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[repr(</span><span>C</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>union</span><span> </span><span>Bits</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>float</span><span>: </span><span>f32</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>int</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>bits</span><span><span> </span><span>=</span><span> </span></span><span>Bits</span><span> { </span><span>float</span><span>: </span><span>1.0</span><span> };</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// SAFETY: every bit pattern is a valid u32, and the union was initialised</span></div></div><div><div><div>10</div></div><div><span>    </span><span>// through the f32 field, which fills all four bytes.</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>int</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>bits</span><span>.int };</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"1.0f32 as bits = {int:#010x}"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;Bits&gt;() = {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>Bits</span><span>&gt;());</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1.0f32 as bits = 0x3f800000</span></div></div><div><div><div>2</div></div><div><span>size_of::&lt;Bits&gt;() = 4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>1.0f32</code> 的位模式是 <code>0x3f800000</code>，这是 IEEE 754 的老知识，用 union 来读位模式是 C 里常见的手法。Rust 里更常用的做法是 <code>f32::to_bits()</code>，它没有 <code>unsafe</code>；<code>union</code> 在 Rust 里主要用来给 FFI 描述 C 里的联合体。Pebble 的 <code>Value</code> 是个 <code>enum</code>（第 8 篇），这就是为什么没有 <code>union</code>。</p></section><section><h3>ManuallyDrop：关掉自动析构<a href="#manuallydrop关掉自动析构"><span>#</span></a></h3><p>Rust 在值离开作用域时自动调用 <code>drop</code>，这就是 RAII。有时你想自己决定什么时候（或者根本不）析构，比如把所有权通过 FFI 交给 C，由 C 来释放；<code>std::mem::ManuallyDrop&lt;T&gt;</code> 就是一个”不要自动 drop”的包装。实验 <code>ch13_manually_drop.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>ManuallyDrop</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>Noisy</span><span>(&amp;'</span><span>static</span><span> </span><span>str</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>impl</span><span> </span><span>Drop</span><span> </span><span>for</span><span> </span><span>Noisy</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>drop</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>println!</span><span>(</span><span>"drop {}"</span><span>, </span><span>self</span><span>.</span><span>0</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>_a</span><span><span> </span><span>=</span><span> </span></span><span>Noisy</span><span>(</span><span>"a: ordinary"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>_b</span><span><span> </span><span>=</span><span> </span></span><span>ManuallyDrop</span><span>::</span><span>new</span><span>(</span><span>Noisy</span><span>(</span><span>"b: manually dropped"</span><span>));</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>c</span><span><span> </span><span>=</span><span> </span></span><span>ManuallyDrop</span><span>::</span><span>new</span><span>(</span><span>Noisy</span><span>(</span><span>"c: dropped by hand"</span><span>));</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"before the explicit drop"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>// SAFETY: `c` is never used again after this call.</span></div></div><div><div><div>17</div></div><div><span>    </span><span>unsafe</span><span> { </span><span>ManuallyDrop</span><span>::</span><span>drop</span><span>(&amp;</span><span>mut</span><span> </span><span>c</span><span>) };</span></div></div><div><div><div>18</div></div><div><span>    </span><span>println!</span><span>(</span><span>"end of main"</span><span>);</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>before the explicit drop</span></div></div><div><div><div>2</div></div><div><span>drop c: dropped by hand</span></div></div><div><div><div>3</div></div><div><span>end of main</span></div></div><div><div><div>4</div></div><div><span>drop a: ordinary</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个值里，<code>a</code> 在 <code>main</code> 结尾按正常顺序析构；<code>b</code> 被 <code>ManuallyDrop</code> 包着，永远没有被析构（泄漏了，没有任何输出）；<code>c</code> 在我们明确调用 <code>ManuallyDrop::drop</code> 时析构，这个调用是 <code>unsafe</code> 的，因为你要保证之后再也不会使用 <code>c</code>，也不会再析构一次。它的 C++ 对应物是”显式调用析构函数加 placement new”这套老办法，只不过 Rust 把它放进了类型里。</p></section><section><h3>static mut：Pebble 为什么不用<a href="#static-mutpebble-为什么不用"><span>#</span></a></h3><p>C 里的全局变量，在 Rust 里最直接的对应物是 <code>static mut</code>。实验 <code>ch13_static_mut.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>static</span><span> </span><span>mut</span><span> </span><span>COUNTER</span><span>: </span><span>u32</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>bump</span><span>() -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>unsafe</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>COUNTER</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> &amp;</span></span><span>COUNTER</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>*</span><span>r</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>bump</span><span>());</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: creating a shared reference to mutable static</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch13_static_mut.rs:6:17</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>6 |         let r = &amp;COUNTER;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                 ^^^^^^^^ shared reference to mutable static</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= note: for more information, see &lt;https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html&gt;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>= note: `#[deny(static_mut_refs)]` (part of `#[deny(rust_2024_compatibility)]`) on by default</span></div></div><div><div><div>10</div></div><div><span>help: use `&amp;raw const` instead to create a raw pointer</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>6 |         let r = &amp;raw const COUNTER;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|                  +++++++++</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>error: could not compile `rust-lab` (bin "ch13_static_mut") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>edition 2024 里，对 <code>static mut</code> 取引用直接是编译错误（<code>static_mut_refs</code> 在这一版是 deny）。原因很实际：全局变量没有所有者，随便哪个线程都能同时拿到它，<code>&amp;COUNTER</code> 和 <code>COUNTER += 1</code> 同时存在就违反了”共享引用和可变修改不能共存”。编译器建议的 <code>&amp;raw const COUNTER</code> 绕过的是这条检查，不是这个问题。Pebble 一个 <code>static mut</code> 都没有，下一节你会看到它想要”全局的、可变的”东西时用的是什么：<code>UnsafeCell</code> 加原子操作。</p></section></section>
<section><h2>往下：把操作系统也去掉<a href="#往下把操作系统也去掉"><span>#</span></a></h2><p>FFI 是往下一层（C 库），<code>no_std</code> 是再往下一层：没有操作系统，没有标准库。嵌入式固件、内核、引导程序、WebAssembly 的最小环境都是这样。第 2 篇我们提过 Rust 标准库其实是三层：<code>core</code>（不需要任何系统支持：<code>Option</code>、<code>Iterator</code>、切片、<code>fmt</code>……）、<code>alloc</code>（需要一个能分配内存的东西：<code>Vec</code>、<code>String</code>、<code>Box</code>、<code>Rc</code>……）和 <code>std</code>（需要操作系统：线程、文件、网络、<code>println!</code>）。<code>#![no_std]</code> 就是告诉编译器：别把 <code>std</code> 自动引进来，我只用前两层。C 里对应的是 <code>-ffreestanding -nostdlib</code>：没有 <code>printf</code>、没有 <code>malloc</code>，你得自己提供。</p><p><code>pebble-nostd</code> 要证明一件事：Pebble 的前端并不是和操作系统焊死的。它复用第 4 篇的词法分析器（同一份代码、一个字节都没改），在 <code>no_std</code> 里对整数算术表达式求值，数据结构只有 <code>Vec</code>。</p><p><code>crates/pebble-nostd/Cargo.toml</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-nostd"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Pebble on bare metal: a no_std evaluator with a bump allocator."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-lexer.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>16</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>只依赖两个自己的 crate，没有任何外部依赖。再写 <code>crates/pebble-nostd/src/lib.rs</code>，整个文件：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![cfg_attr(not(test), no_std)]</span></div></div><div><div><div>2</div></div><div><span>#![deny(unsafe_op_in_unsafe_fn)]</span></div></div><div><div><div>3</div></div><div><span>//! Pebble on bare metal.</span></div></div><div><div><div>4</div></div><div><span>//!</span></div></div><div><div><div>5</div></div><div><span>//! This crate demonstrates that the front end of the toolchain is not welded</span></div></div><div><div><div>6</div></div><div><span>//! to an operating system:</span></div></div><div><div><div>7</div></div><div><span>//!</span></div></div><div><div><div>8</div></div><div><span>//! 1. [`eval`] runs the *same* [`pebble_lexer`] used by the rest of the</span></div></div><div><div><div>9</div></div><div><span>//!    compiler, then evaluates integer arithmetic with nothing but `core` and</span></div></div><div><div><div>10</div></div><div><span>//!    `alloc`.</span></div></div><div><div><div>11</div></div><div><span>//! 2. [`bump`] provides a real [`core::alloc::GlobalAlloc`] backed by a static</span></div></div><div><div><div>12</div></div><div><span>//!    arena, so the evaluator's `Vec`s have somewhere to live on a target that</span></div></div><div><div><div>13</div></div><div><span>//!    has no `std` allocator.</span></div></div><div><div><div>14</div></div><div><span>//!</span></div></div><div><div><div>15</div></div><div><span>//! The crate is `#![no_std]` except while testing. Under `cargo test` the test</span></div></div><div><div><div>16</div></div><div><span>//! harness needs `std`, so the attribute is conditional; the library itself</span></div></div><div><div><div>17</div></div><div><span>//! never references `std`.</span></div></div><div><div><div>18</div></div><div><span>//!</span></div></div><div><div><div>19</div></div><div><span>//! Unlike the other front-end crates this one cannot `#![forbid(unsafe_code)]`:</span></div></div><div><div><div>20</div></div><div><span>//! implementing a [`GlobalAlloc`](core::alloc::GlobalAlloc) is inherently</span></div></div><div><div><div>21</div></div><div><span>//! unsafe. Instead we localize the unsafe code to [`bump`] and require every</span></div></div><div><div><div>22</div></div><div><span>//! unsafe operation to sit in an explicit `unsafe` block via</span></div></div><div><div><div>23</div></div><div><span>//! `#![deny(unsafe_op_in_unsafe_fn)]`.</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>extern</span><span> </span><span>crate</span><span> alloc;</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>pub</span><span> </span><span>mod</span><span> bump;</span></div></div><div><div><div>28</div></div><div><span>pub</span><span> </span><span>mod</span><span> eval;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三处值得停一下。</p><p><code>#![cfg_attr(not(test), no_std)]</code>：条件属性。“如果不是测试，就加 <code>#![no_std]</code>”。原因写在文档注释里：<code>cargo test</code> 的测试框架本身需要 <code>std</code>（它要线程、要打印），所以测试构建不能是 <code>no_std</code>；正式构建才是。注释里说”库本身从来不引用 <code>std</code>”，这句话是怎么保证的？实验：在重建目录的一份拷贝里，把 <code>eval.rs</code> 的 <code>use alloc::vec::Vec;</code> 改成 <code>use std::vec::Vec;</code>，然后 <code>cargo build -p pebble-nostd</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling pebble-span v0.1.0 (/tmp/lab-ch13-scratch/crates/pebble-span)</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-diag v0.1.0 (/tmp/lab-ch13-scratch/crates/pebble-diag)</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Compiling pebble-lexer v0.1.0 (/tmp/lab-ch13-scratch/crates/pebble-lexer)</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>Compiling pebble-nostd v0.1.0 (/tmp/lab-ch13-scratch/crates/pebble-nostd)</span></div></div><div><div><div>5</div></div><div><span>error[E0433]: cannot find module or crate `std` in this scope</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-nostd/src/eval.rs:21:5</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>21 | use std::vec::Vec;</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|     ^^^ use of unresolved module or unlinked crate `std`</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>= help: if you wanted to use a crate named `std`, use `cargo add std` to add it to your `Cargo.toml`</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>warning: unused extern crate</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-nostd/src/lib.rs:25:1</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span>25 | extern crate alloc;</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>| ^^^^^^^^^^^^^^^^^^^ unused</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>= note: `-W unused-extern-crates` implied by `-W rust-2018-idioms`</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>= help: to override `-W rust-2018-idioms` add `#[allow(unused_extern_crates)]`</span></div></div><div><div><div>21</div></div><div><span>help: remove the unused `extern crate`</span></div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>23</div></div><div><span>25 - extern crate alloc;</span></div></div><div><div><div>24</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>error[E0614]: type `Op` cannot be dereferenced</span></div></div><div><div><div>27</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-nostd/src/eval.rs:187:19</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>29</div></div><div><span>187 |         let top = *top;</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>|                   ^^^^ can't be dereferenced</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>Some errors have detailed explanations: E0433, E0614.</span></div></div><div><div><div>33</div></div><div><span>For more information about an error, try `rustc --explain E0433`.</span></div></div><div><div><div>34</div></div><div><span>warning: `pebble-nostd` (lib) generated 1 warning</span></div></div><div><div><div>35</div></div><div><span>error: could not compile `pebble-nostd` (lib) due to 2 previous errors; 1 warning emitted</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>“use of unresolved module or unlinked crate <code>std</code>”，<code>no_std</code> 之后根本没有 <code>std</code> 这个名字。最后那个 <code>E0614</code> 是前面错误引起的连带报错，可以忽略。另一个方向，干脆把 <code>use</code> 删掉，<code>Vec</code> 也不在了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling pebble-nostd v0.1.0 (/tmp/lab-ch13-scratch/crates/pebble-nostd)</span></div></div><div><div><div>2</div></div><div><span>error[E0425]: cannot find type `Vec` in this scope</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-nostd/src/eval.rs:100:44</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>100 | fn compile(tokens: &amp;[Token&lt;'_&gt;]) -&gt; Result&lt;Vec&lt;Rpn&gt;, EvalError&gt; {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|                                            ^^^ not found in this scope</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>help: consider importing this struct</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>22 + use alloc::vec::Vec;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>error[E0425]: cannot find type `Vec` in this scope</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-nostd/src/eval.rs:101:21</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span>101 |     let mut output: Vec&lt;Rpn&gt; = Vec::new();</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>|                     ^^^ not found in this scope</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>19</div></div><div><span>help: consider importing this struct</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>21</div></div><div><span><span> </span></span><span>22 + use alloc::vec::Vec;</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>error[E0433]: cannot find type `Vec` in this scope</span></div></div><div><div><div>25</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-nostd/src/eval.rs:101:32</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>27</div></div><div><span>101 |     let mut output: Vec&lt;Rpn&gt; = Vec::new();</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>|                                ^^^ use of undeclared type `Vec`</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>30</div></div><div><span>help: consider importing this struct</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器很贴心地告诉你该导入 <code>alloc::vec::Vec</code>。这里的 <code>Vec</code> 和 <code>std::vec::Vec</code> 是同一个类型，<code>std</code> 只是把 <code>alloc</code> 的东西重新导出了一遍。（第二个输出是我用 <code>cargo build -p pebble-nostd 2&gt;&amp;1 | head -30</code> 截短的，这是命令的一部分，不是编辑。）</p><p><code>extern crate alloc;</code>：<code>alloc</code> 不像 <code>core</code> 那样自动可用，需要显式声明（这是 edition 2018 之后仅剩的少数 <code>extern crate</code> 用途）。声明之后，<code>alloc::vec::Vec</code>、<code>alloc::string::String</code> 等就可以用了；但你必须自己提供一个全局分配器，否则最终链接会失败。这就是下一节的内容。</p><p><code>#![deny(unsafe_op_in_unsafe_fn)]</code>：workspace 的 lint 里已经是 <code>warn</code>，这个 crate 把它升成 <code>deny</code>。<code>unsafe fn</code> 的函数体在老版本里整个都是 unsafe 上下文，现在要求在函数里每个不安全操作单独包一个 <code>unsafe { }</code> 块，让每处不安全的地方都有位置写 <code>SAFETY</code> 注释。</p><section><h3>一个 bump 分配器<a href="#一个-bump-分配器"><span>#</span></a></h3><p><code>#[global_allocator]</code> 要求你提供一个实现了 <code>GlobalAlloc</code> trait 的值。这个 trait 只有两个必须的方法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>unsafe</span><span> </span><span>fn</span><span> </span><span>alloc</span><span>(&amp;</span><span>self</span><span>, </span><span>layout</span><span>: </span><span>Layout</span><span>) -&gt; *</span><span>mut</span><span> </span><span>u8</span><span>;</span></div></div><div><div><div>2</div></div><div><span>unsafe</span><span> </span><span>fn</span><span> </span><span>dealloc</span><span>(&amp;</span><span>self</span><span>, </span><span>ptr</span><span>: *</span><span>mut</span><span> </span><span>u8</span><span>, </span><span>layout</span><span>: </span><span>Layout</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Layout</code> 是”大小加对齐”，<code>alloc</code> 返回的指针必须满足对齐，失败返回空指针。我们写最简单的分配器：bump（也叫 arena 或 linear allocator）：一块固定的内存，一个游标，每次分配把游标往前推一格，释放什么也不做。C++ 里它的近亲是 <code>std::pmr::monotonic_buffer_resource</code>。这看起来像偷懒，但在固件里完全是合理的设计：程序跑完就重启，中途回收内存换不来任何东西，反而要付出记账的成本。</p><p><code>UnsafeCell&lt;T&gt;</code> 是 <code>Cell</code> 和 <code>RefCell</code> 底下的原语，也是 Rust 里唯一合法的”从共享引用改内容”的入口：<code>get()</code> 接受 <code>&amp;self</code>，返回 <code>*mut T</code>，之后别名和并发访问是否安全，全由你自己负责。<code>Cell</code>、<code>RefCell</code>、<code>Mutex</code>、原子类型内部都是它加上各自的规则。C++ 里没有对应物，因为 C++ 没有”共享引用不能改”这条规则，也就不需要一个特殊的逃生口。</p><p>写 <code>crates/pebble-nostd/src/bump.rs</code>。先写一个朴素版本，让编译器告诉我们哪里不对。游标和内存都放进一个结构体，做成 <code>static</code>（<code>GlobalAlloc</code> 要求全局的）。最直接的写法：游标用 <code>Cell&lt;usize&gt;</code>（第 11 篇讲过的内部可变性），内存用 <code>UnsafeCell&lt;[u8; N]&gt;</code>。实验 <code>ch13_static_sync.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::{</span></span><span>Cell</span><span>, </span><span>UnsafeCell</span><span>};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>CursorOnly</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>used</span><span>: </span><span>Cell</span><span>&lt;</span><span>usize</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>struct</span><span> </span><span>Arena</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>heap</span><span>: </span><span>UnsafeCell</span><span>&lt;[</span><span>u8</span><span>; </span><span>16</span><span>]&gt;,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>static</span><span> </span><span>CURSOR</span><span>: </span><span>CursorOnly</span><span><span> </span><span>=</span><span> </span></span><span>CursorOnly</span><span> { </span><span>used</span><span>: </span><span>Cell</span><span>::</span><span>new</span><span>(</span><span>0</span><span>) };</span></div></div><div><div><div>12</div></div><div><span>static</span><span> </span><span>ARENA</span><span>: </span><span>Arena</span><span><span> </span><span>=</span><span> </span></span><span>Arena</span><span> { </span><span>heap</span><span>: </span><span>UnsafeCell</span><span>::</span><span>new</span><span>([</span><span>0</span><span>; </span><span>16</span><span>]) };</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>CURSOR</span><span>.used.</span><span>set</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>ARENA</span><span>.heap.</span><span>get</span><span>() </span><span>as</span><span> </span><span>usize</span><span><span> </span><span>!=</span><span> </span></span><span>0</span><span>);</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `Cell&lt;usize&gt;` cannot be shared between threads safely</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_static_sync.rs:11:16</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>11 | static CURSOR: CursorOnly = CursorOnly { used: Cell::new(0) };</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                ^^^^^^^^^^ `Cell&lt;usize&gt;` cannot be shared between threads safely</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= help: within `CursorOnly`, the trait `Sync` is not implemented for `Cell&lt;usize&gt;`</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicUsize` instead</span></div></div><div><div><div>9</div></div><div><span>note: required because it appears within the type `CursorOnly`</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_static_sync.rs:3:8</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>3 | struct CursorOnly {</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|        ^^^^^^^^^^</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>= note: shared static variables must have a type that implements `Sync`</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>error[E0277]: `UnsafeCell&lt;[u8; 16]&gt;` cannot be shared between threads safely</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_static_sync.rs:12:15</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>19</div></div><div><span>12 | static ARENA: Arena = Arena { heap: UnsafeCell::new([0; 16]) };</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>|               ^^^^^ `UnsafeCell&lt;[u8; 16]&gt;` cannot be shared between threads safely</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>= help: within `Arena`, the trait `Sync` is not implemented for `UnsafeCell&lt;[u8; 16]&gt;`</span></div></div><div><div><div>23</div></div><div><span>note: required because it appears within the type `Arena`</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch13_static_sync.rs:7:8</span></div></div><div><div><div>25</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>26</div></div><div><span><span> </span></span><span>7 | struct Arena {</span></div></div><div><div><div>27</div></div><div><span><span>   </span></span><span>|        ^^^^^</span></div></div><div><div><div>28</div></div><div><span><span>   </span></span><span>= note: shared static variables must have a type that implements `Sync`</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>31</div></div><div><span>error: could not compile `rust-lab` (bin "ch13_static_sync") due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>static</code> 的值可以被所有线程同时访问，所以它的类型必须是 <code>Sync</code>（又是自动 trait：所有字段都 <code>Sync</code> 才行）。<code>Cell</code> 和 <code>UnsafeCell</code> 都故意不是 <code>Sync</code>：它们允许在共享引用下修改内容，却没有任何保护，两个线程同时改就是数据竞争。错误信息里已经给了提示：“use <code>std::sync::RwLock</code> or <code>std::sync::atomic::AtomicUsize</code> instead”。游标我们用 <code>AtomicUsize</code>（原子整数，硬件保证读改写是不可分割的），这一个字段解决了。</p><p>内存那块 <code>UnsafeCell&lt;[u8; N]&gt;</code> 没有对应的安全替代品：我们本来就是要把它当作”别人可以随便写”的原始内存交出去。这时只能手写 <code>unsafe impl Sync</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// SAFETY: the only shared state that is ever mutated is the atomic cursor, so</span></div></div><div><div><div>2</div></div><div><span>// racing `alloc` calls are well defined. Two successful allocations never</span></div></div><div><div><div>3</div></div><div><span>// overlap, because the atomic update reserves each slice before it is handed</span></div></div><div><div><div>4</div></div><div><span>// out; the caller of `alloc` receives a pointer it is free to write through.</span></div></div><div><div><div>5</div></div><div><span>unsafe</span><span> </span><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt; </span><span>Sync</span><span> </span><span>for</span><span> </span><span>BumpAllocator</span><span>&lt;</span><span>N</span><span>&gt; {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这句话是我们对编译器的承诺，等价于 C++ 里”这个类是线程安全的，信我”，区别是 Rust 逼你把这句承诺写下来，并且放在一个 <code>unsafe</code> 后面，让 <code>grep unsafe</code> 能找到。承诺的依据写在注释里：被多线程共享并修改的只有原子游标，而游标保证每次成功的分配都拿到不重叠的区间，拿到区间的线程只写自己的那一段。</p></section><section><h3>常量泛型：数组大小做参数<a href="#常量泛型数组大小做参数"><span>#</span></a></h3><p>分配器的内存大小最直接的写法是写死一个 <code>pub const HEAP_SIZE: usize = 4096</code>（这个 crate 最初就是这样）。要让用户自己选大小，C++ 里你会写 <code>template &lt;size_t N = 4096&gt; class BumpAllocator</code>。Rust 的对应物是 const 泛型：泛型参数不是类型，而是一个值。语法是 <code>&lt;const N: usize&gt;</code>，还可以有默认值。实验 <code>ch13_const_generic.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::size_of;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>Buf</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>4</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>data</span><span>: [</span><span>u8</span><span>; </span><span>N</span><span>],</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt; </span><span>Buf</span><span>&lt;</span><span>N</span><span>&gt; {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>const</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Self</span><span> { </span><span>data</span><span>: [</span><span>0</span><span>; </span><span>N</span><span>] }</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>const</span><span> </span><span>fn</span><span> </span><span>capacity</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span>.data.</span><span>len</span><span>()</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>default</span><span>: </span><span>Buf</span><span><span> </span><span>=</span><span> </span></span><span>Buf</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>big</span><span><span> </span><span>=</span><span> </span></span><span>Buf</span><span>::&lt;</span><span>4096</span><span>&gt;::</span><span>new</span><span>();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"default: capacity {} size_of {}"</span><span>, </span><span>default</span><span>.</span><span>capacity</span><span>(), </span><span>size_of</span><span>::&lt;</span><span>Buf</span><span>&gt;());</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"4096:    capacity {} size_of {}"</span><span>, </span><span>big</span><span>.</span><span>capacity</span><span>(), </span><span>size_of</span><span>::&lt;</span><span>Buf</span><span>&lt;</span><span>4096</span><span>&gt;&gt;());</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>default: capacity 4 size_of 4</span></div></div><div><div><div>2</div></div><div><span>4096:    capacity 4096 size_of 4096</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Buf&lt;4&gt;</code> 和 <code>Buf&lt;4096&gt;</code> 是不同的类型，<code>size_of</code> 分别是 4 和 4096：<code>[u8; N]</code> 的 <code>N</code> 是类型的一部分，所以数组直接内联在结构体里，没有堆分配，这也是 <code>static</code> 能直接放下整块内存的前提。因为是不同的类型，互相赋值是编译错误（<code>ch13_const_generic_mismatch.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch13_const_generic_mismatch.rs:7:25</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>7 |     let small: Buf&lt;4&gt; = big;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                ------   ^^^ expected `4`, found `4096`</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                expected due to this</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>= note: expected struct `Buf&lt;4&gt;`</span></div></div><div><div><div>10</div></div><div><span><span>             </span></span><span>found struct `Buf&lt;4096&gt;`</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>For more information about this error, try `rustc --explain E0308`.</span></div></div><div><div><div>13</div></div><div><span>error: could not compile `rust-lab` (bin "ch13_const_generic_mismatch") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>像第 5 篇的泛型一样，编译器对每个不同的 <code>N</code> 单独生成一份代码（单态化），所以没有运行时代价。</p><p>现在写 <code>bump.rs</code> 的主体。文件最前面那 13 行 <code>//!</code> 模块文档（“A bump allocator over a fixed, static arena…”）照抄 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-nostd/src/bump.rs" target="_blank">tag</a>，下面从 <code>use</code> 开始。先是常量和游标：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>alloc</span><span>::{</span></span><span>GlobalAlloc</span><span>, </span><span>Layout</span><span>};</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>cell</span><span>::</span></span><span>UnsafeCell</span><span>;</span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>core</span><span>::ptr;</span></span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>sync</span><span>::</span><span>atomic</span><span>::{</span></span><span>AtomicUsize</span><span>, </span><span>Ordering</span><span>};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/// The arena size, in bytes, of a [`BumpAllocator`] that does not name one.</span></div></div><div><div><div>7</div></div><div><span>pub</span><span> </span><span>const</span><span> </span><span>DEFAULT_HEAP_SIZE</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>4096</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>/// The mutable cursor of a bump allocator.</span></div></div><div><div><div>10</div></div><div><span>///</span></div></div><div><div><div>11</div></div><div><span>/// Kept as one [`UnsafeCell`] so the allocator has interior mutability without</span></div></div><div><div><div>12</div></div><div><span>/// making the whole [`BumpAllocator`] `mut`.</span></div></div><div><div><div>13</div></div><div><span>struct</span><span> </span><span>BumpState</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>/// Bytes handed out so far, measured from the start of the arena.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>used</span><span>: </span><span>AtomicUsize</span><span>,</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>DEFAULT_HEAP_SIZE</code> 是不指定大小时的默认值。<code>BumpState</code> 里只有一个原子游标：从 arena 起点算起已经交出去多少字节。（一个我不太有把握的疑点：外面又包了一层 <code>UnsafeCell</code>，原子类型本身就有内部可变性，这层多半是多余的；<code>bytes_used</code> 也只是用共享引用读它。tag 里是这样写的，我照抄，不改。）</p><p>分配器本体：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A [`GlobalAlloc`] that bumps a cursor through a fixed-size static arena.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Each instance owns `N` bytes inline (4096 unless you say otherwise), which</span></div></div><div><div><div>4</div></div><div><span>/// is what makes it usable as a `static` without a separate `static mut` buffer:</span></div></div><div><div><div>5</div></div><div><span>///</span></div></div><div><div><div>6</div></div><div><span>/// ```</span></div></div><div><div><div>7</div></div><div><span>/// use pebble_nostd::bump::BumpAllocator;</span></div></div><div><div><div>8</div></div><div><span>///</span></div></div><div><div><div>9</div></div><div><span>/// static HEAP: BumpAllocator = BumpAllocator::new();</span></div></div><div><div><div>10</div></div><div><span>/// ```</span></div></div><div><div><div>11</div></div><div><span>///</span></div></div><div><div><div>12</div></div><div><span>/// Allocation is lock-free: concurrent callers race on the cursor with a</span></div></div><div><div><div>13</div></div><div><span>/// compare-exchange loop, and whoever wins reserves its slice exclusively.</span></div></div><div><div><div>14</div></div><div><span>/// Exhaustion is reported as a null pointer, as [`GlobalAlloc`] requires.</span></div></div><div><div><div>15</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>BumpAllocator</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>DEFAULT_HEAP_SIZE</span><span>&gt; {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>state</span><span>: </span><span>UnsafeCell</span><span>&lt;</span><span>BumpState</span><span>&gt;,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>heap</span><span>: </span><span>UnsafeCell</span><span>&lt;[</span><span>u8</span><span>; </span><span>N</span><span>]&gt;,</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>BumpAllocator&lt;const N: usize = DEFAULT_HEAP_SIZE&gt;</code>：<code>N</code> 有默认值，所以写 <code>BumpAllocator</code> 就是 <code>BumpAllocator&lt;4096&gt;</code>，写 <code>BumpAllocator&lt;{ 1024 * 1024 }&gt;</code> 就是 1 MiB（表达式必须用花括号括起来）。<code>heap</code> 是 <code>UnsafeCell&lt;[u8; N]&gt;</code>，整块内存就在结构体里面。放进 <code>static</code> 里，链接器会把它放进 <code>.bss</code> 段（全零初始化的静态数据，不占可执行文件的体积）。</p><p>然后是 <code>impl&lt;const N: usize&gt; BumpAllocator&lt;N&gt;</code> 和 <code>Default</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt; </span><span>BumpAllocator</span><span>&lt;</span><span>N</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Creates an allocator whose entire arena is free.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>state</span><span>: </span><span>UnsafeCell</span><span>::</span><span>new</span><span>(</span><span>BumpState</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>used</span><span><span>: </span><span>AtomicUsize</span><span>::</span></span><span>new</span><span>(</span><span>0</span><span>),</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>heap</span><span>: </span><span>UnsafeCell</span><span>::</span><span>new</span><span>([</span><span>0</span><span>; </span><span>N</span><span>]),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>/// Returns how many bytes have been handed out so far.</span></div></div><div><div><div>14</div></div><div><span>    </span><span>///</span></div></div><div><div><div>15</div></div><div><span>    </span><span>/// This is the arena high-water mark since the allocator never reuses</span></div></div><div><div><div>16</div></div><div><span>    </span><span>/// space; it is useful for asserting that a workload fits, or for sizing an</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// arena on a device with a known memory budget.</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>bytes_used</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>// SAFETY: `state` lives as long as `self`, and reading the atomic needs</span></div></div><div><div><div>21</div></div><div><span>        </span><span>// no unique access.</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>state</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { &amp;*</span><span>self</span><span>.state.</span><span>get</span><span>() };</span></div></div><div><div><div>23</div></div><div><span>        </span><span>state</span><span>.used.</span><span>load</span><span><span>(</span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>/// The size in bytes of the arena: the const parameter `N`.</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>28</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>capacity</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>N</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>/// Address of the first byte of the arena.</span></div></div><div><div><div>33</div></div><div><span>    </span><span>fn</span><span> </span><span>base</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>self</span><span>.heap.</span><span>get</span><span>().</span><span>cast</span><span>::&lt;</span><span>u8</span><span>&gt;() </span><span>as</span><span> </span><span>usize</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt; </span><span>Default</span><span> </span><span>for</span><span> </span><span>BumpAllocator</span><span>&lt;</span><span>N</span><span>&gt; {</span></div></div><div><div><div>39</div></div><div><span>    </span><span>fn</span><span> </span><span>default</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>40</div></div><div><span>        </span><span>Self</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>new</code> 是 <code>const fn</code>（第 1 篇）：<code>static</code> 的初始值必须在编译期算出来，所以构造函数得是 <code>const</code>；<code>[0; N]</code> 在 <code>const fn</code> 里随便写。<code>capacity</code> 直接返回 <code>N</code>，它是 <code>const fn</code> 因为 <code>N</code> 是编译期常量。</p><p>最后是整个文件的核心，<code>GlobalAlloc</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// SAFETY: the implementation upholds the `GlobalAlloc` contract.</span></div></div><div><div><div>2</div></div><div><span>//</span></div></div><div><div><div>3</div></div><div><span>// - `alloc` returns null on exhaustion and otherwise a pointer aligned to</span></div></div><div><div><div>4</div></div><div><span>//   `layout.align()` that is not aliased by any other live allocation.</span></div></div><div><div><div>5</div></div><div><span>// - The returned pointer is valid for writes of `layout.size()` bytes, since the</span></div></div><div><div><div>6</div></div><div><span>//   arena is an owned `[u8; N]` and the cursor stays within it.</span></div></div><div><div><div>7</div></div><div><span>// - `dealloc` is a no-op, which is permitted: freeing is optional.</span></div></div><div><div><div>8</div></div><div><span>unsafe</span><span> </span><span>impl</span><span>&lt;</span><span>const</span><span> </span><span>N</span><span>: </span><span>usize</span><span>&gt; </span><span>GlobalAlloc</span><span> </span><span>for</span><span> </span><span>BumpAllocator</span><span>&lt;</span><span>N</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>unsafe</span><span> </span><span>fn</span><span> </span><span>alloc</span><span>(&amp;</span><span>self</span><span>, </span><span>layout</span><span>: </span><span>Layout</span><span>) -&gt; *</span><span>mut</span><span> </span><span>u8</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>base</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>base</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>align</span><span><span> </span><span>=</span><span> </span></span><span>layout</span><span>.</span><span>align</span><span>();</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>size</span><span><span> </span><span>=</span><span> </span></span><span>layout</span><span>.</span><span>size</span><span>();</span></div></div><div><div><div>13</div></div><div><span>        </span><span>// SAFETY: `state` outlives `self` and the closure only touches the</span></div></div><div><div><div>14</div></div><div><span>        </span><span>// atomic cursor, so it needs no exclusive access.</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>state</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { &amp;*</span><span>self</span><span>.state.</span><span>get</span><span>() };</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>        </span><span>// Reserve our slice by atomically advancing the cursor. `fetch_update`</span></div></div><div><div><div>18</div></div><div><span>        </span><span>// retries until either the update succeeds or the cursor cannot grow</span></div></div><div><div><div>19</div></div><div><span>        </span><span>// enough for this request, in which case it yields `Err` and we report</span></div></div><div><div><div>20</div></div><div><span>        </span><span>// exhaustion.</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>reserved</span><span><span> </span><span>=</span><span> </span></span><span>state</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>.used</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>.</span><span>fetch_update</span><span><span>(</span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span><span>, </span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span><span>, </span><span>|</span></span><span>used</span><span><span>|</span><span> {</span></span></div></div><div><div><div>24</div></div><div><span>                </span><span>let</span><span> </span><span>cursor</span><span><span> </span><span>=</span><span> </span></span><span>base</span><span>.</span><span>checked_add</span><span>(</span><span>used</span><span>)?;</span></div></div><div><div><div>25</div></div><div><span>                </span><span>let</span><span> </span><span>aligned</span><span><span> </span><span>=</span><span> </span></span><span>cursor</span><span>.</span><span>checked_add</span><span>(</span><span>align</span><span> - </span><span>1</span><span><span>)? &amp; </span><span>!</span><span>(</span></span><span>align</span><span> - </span><span>1</span><span>);</span></div></div><div><div><div>26</div></div><div><span>                </span><span>let</span><span> </span><span>offset</span><span><span> </span><span>=</span><span> </span></span><span>aligned</span><span> - </span><span>base</span><span>;</span></div></div><div><div><div>27</div></div><div><span>                </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>offset</span><span>.</span><span>checked_add</span><span>(</span><span>size</span><span>)?;</span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>(</span><span>end</span><span><span> </span><span>&lt;=</span><span> </span></span><span>N</span><span>).</span><span>then_some</span><span>(</span><span>end</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>        </span><span>match</span><span> </span><span>reserved</span><span> {</span></div></div><div><div><div>32</div></div><div><span>            </span><span>// `Err` carries the (unchanged) current cursor: no room left.</span></div></div><div><div><div>33</div></div><div><span>            </span><span>Err</span><span>(</span><span>_</span><span><span>) =&gt; </span><span>ptr</span><span>::</span></span><span>null_mut</span><span>(),</span></div></div><div><div><div>34</div></div><div><span>            </span><span>// `Ok(previous)` is the cursor we reserved from. Recomputing the</span></div></div><div><div><div>35</div></div><div><span>            </span><span>// aligned address is infallible here because the reservation above</span></div></div><div><div><div>36</div></div><div><span>            </span><span>// already succeeded with these same values.</span></div></div><div><div><div>37</div></div><div><span>            </span><span>Ok</span><span>(</span><span>previous</span><span>) =&gt; {</span></div></div><div><div><div>38</div></div><div><span>                </span><span>let</span><span> </span><span>aligned</span><span><span> </span><span>=</span><span> (</span></span><span>base</span><span> + </span><span>previous</span><span> + </span><span>align</span><span> - </span><span>1</span><span><span>) &amp; </span><span>!</span><span>(</span></span><span>align</span><span> - </span><span>1</span><span>);</span></div></div><div><div><div>39</div></div><div><span>                </span><span>aligned</span><span> </span><span>as</span><span> *</span><span>mut</span><span> </span><span>u8</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span>unsafe</span><span> </span><span>fn</span><span> </span><span>dealloc</span><span>(&amp;</span><span>self</span><span>, </span><span>_ptr</span><span>: *</span><span>mut</span><span> </span><span>u8</span><span>, </span><span>_layout</span><span>: </span><span>Layout</span><span>) {</span></div></div><div><div><div>45</div></div><div><span>        </span><span>// A bump allocator cannot free an individual allocation: the cursor</span></div></div><div><div><div>46</div></div><div><span>        </span><span>// only moves forward, and there is no free list to recover a hole. The</span></div></div><div><div><div>47</div></div><div><span>        </span><span>// whole arena is reclaimed at once when the allocator is dropped or the</span></div></div><div><div><div>48</div></div><div><span>        </span><span>// program is reset. This is intentional, not an oversight.</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>先看 <code>fetch_update</code>：它是一个”读-改-写”的循环（compare-and-swap 重试）。读当前游标 <code>used</code>，调用闭包算出新值，如果算出 <code>Some(新值)</code> 就尝试把游标换成新值；如果别的线程在这中间改了游标，换失败，重新读、重新算。闭包返回 <code>None</code> 表示”放不下”，<code>fetch_update</code> 就以 <code>Err</code> 返回，游标不变。C++ 里等价的是 <code>std::atomic&lt;size_t&gt;</code> 加一个 <code>compare_exchange_weak</code> 循环。</p><p>闭包里做的事：游标加上 arena 的基地址得到当前地址，向上取整到 <code>align</code> 的倍数（<code>(addr + align - 1) &amp; !(align - 1)</code>，对齐必须是 2 的幂，<code>Layout</code> 保证），再换回偏移，加上 <code>size</code> 得到 <code>end</code>，只有 <code>end &lt;= N</code> 才算放得下。每一步加法都是 <code>checked_add</code>，溢出就当作放不下，不会绕回去踩到别处。</p><p><strong>为什么用 <code>Ordering::Relaxed</code></strong>：原子操作的内存顺序（<code>std::memory_order</code> 在 Rust 里叫 <code>Ordering</code>）控制的是”这个原子操作周围的其他内存读写，对别的线程来说怎么排序”。<code>Relaxed</code> 是最弱的：只保证这一个原子变量自己的修改是原子的，不对其他内存建立任何先后关系。这里够用，理由是：被保护的数据只有游标本身，没有”线程 A 写了某些数据，再通过游标告诉线程 B 去读”的发布-订阅关系；线程拿到自己的区间之后，写的是只属于自己的内存，别的线程不会读它（如果之后需要读，需要通过别的同步手段传递这块内存，那一次传递会自带顺序保证）。这是我的推理，不是测出来的；下面的多线程实验是个旁证。</p><p><code>dealloc</code> 是空函数，注释里写明这是故意的，不是漏写。</p><p>最后看测试：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::{</span><span>BumpAllocator</span><span>, </span><span>DEFAULT_HEAP_SIZE</span><span>};</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span><span> </span><span>core</span><span>::</span><span>alloc</span><span>::{</span></span><span>GlobalAlloc</span><span>, </span><span>Layout</span><span>};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>static</span><span> </span><span>ALLOC</span><span>: </span><span>BumpAllocator</span><span><span> </span><span>=</span><span> </span></span><span>BumpAllocator</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>distinct_layouts_do_not_overlap</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>// SAFETY: the layouts are valid (`size` is a multiple of `align`, both</span></div></div><div><div><div>11</div></div><div><span>        </span><span>// powers of two), and a null result is handled below.</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>ALLOC</span><span>.</span><span>alloc</span><span><span>(</span><span>Layout</span><span>::</span></span><span>from_size_align</span><span>(</span><span>64</span><span>, </span><span>8</span><span>).</span><span>unwrap</span><span>()) };</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>ALLOC</span><span>.</span><span>alloc</span><span><span>(</span><span>Layout</span><span>::</span></span><span>from_size_align</span><span>(</span><span>128</span><span>, </span><span>16</span><span>).</span><span>unwrap</span><span>()) };</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert!</span><span><span>(</span><span>!</span></span><span>a</span><span>.</span><span>is_null</span><span><span>() </span><span>&amp;&amp;</span><span> </span><span>!</span></span><span>b</span><span>.</span><span>is_null</span><span>(), </span><span>"allocations must succeed"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>a</span><span> </span><span>as</span><span> </span><span>usize</span><span> % </span><span>8</span><span>, </span><span>0</span><span>, </span><span>"first block must honour alignment"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>b</span><span> </span><span>as</span><span> </span><span>usize</span><span> % </span><span>16</span><span>, </span><span>0</span><span>, </span><span>"second block must honour alignment"</span><span>);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>// The ranges [a, a+64) and [b, b+128) must be disjoint.</span></div></div><div><div><div>18</div></div><div><span>        </span><span>let</span><span> (</span><span>a</span><span>, </span><span>b</span><span><span>) </span><span>=</span><span> (</span></span><span>a</span><span> </span><span>as</span><span> </span><span>usize</span><span>, </span><span>b</span><span> </span><span>as</span><span> </span><span>usize</span><span>);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>20</div></div><div><span>            </span><span>a</span><span> + </span><span>64</span><span><span> </span><span>&lt;=</span><span> </span></span><span>b</span><span><span> </span><span>||</span><span> </span></span><span>b</span><span> + </span><span>128</span><span><span> </span><span>&lt;=</span><span> </span></span><span>a</span><span>,</span></div></div><div><div><div>21</div></div><div><span>            </span><span>"allocations at {a:#x} and {b:#x} overlap"</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>23</div></div><div><span>        </span><span>assert!</span><span>(</span><span>ALLOC</span><span>.</span><span>bytes_used</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>64</span><span> + </span><span>128</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>27</div></div><div><span>    </span><span>fn</span><span> </span><span>exhaustion_returns_null</span><span>() {</span></div></div><div><div><div>28</div></div><div><span>        </span><span>// A fresh allocator so this test cannot be perturbed by the other one.</span></div></div><div><div><div>29</div></div><div><span>        </span><span>static</span><span> </span><span>SMALL</span><span>: </span><span>BumpAllocator</span><span>&lt;</span><span>64</span><span><span>&gt; </span><span>=</span><span> </span></span><span>BumpAllocator</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>handed_out</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>31</div></div><div><span>        </span><span>loop</span><span> {</span></div></div><div><div><div>32</div></div><div><span>            </span><span>// SAFETY: `Layout::new::&lt;u8&gt;()` is valid; the returned pointer is</span></div></div><div><div><div>33</div></div><div><span>            </span><span>// only inspected, never written.</span></div></div><div><div><div>34</div></div><div><span>            </span><span>let</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>SMALL</span><span>.</span><span>alloc</span><span><span>(</span><span>Layout</span><span>::</span></span><span>new</span><span>::&lt;</span><span>u8</span><span>&gt;()) };</span></div></div><div><div><div>35</div></div><div><span>            </span><span>if</span><span> </span><span>p</span><span>.</span><span>is_null</span><span>() {</span></div></div><div><div><div>36</div></div><div><span>                </span><span>break</span><span>;</span></div></div><div><div><div>37</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>            </span><span>handed_out</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>39</div></div><div><span>            </span><span>assert!</span><span>(</span><span>handed_out</span><span><span> </span><span>&lt;=</span><span> </span></span><span>64</span><span>, </span><span>"allocator overran its arena"</span><span>);</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>handed_out</span><span>, </span><span>64</span><span>, </span><span>"one byte at a time fills the arena"</span><span>);</span></div></div><div><div><div>42</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>SMALL</span><span>.</span><span>bytes_used</span><span>(), </span><span>64</span><span>);</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>46</div></div><div><span>    </span><span>fn</span><span> </span><span>dealloc_is_a_no_op</span><span>() {</span></div></div><div><div><div>47</div></div><div><span>        </span><span>static</span><span> </span><span>HEAP</span><span>: </span><span>BumpAllocator</span><span><span> </span><span>=</span><span> </span></span><span>BumpAllocator</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>48</div></div><div><span>        </span><span>// SAFETY: `alloc` returns a pointer that `dealloc` may be called on;</span></div></div><div><div><div>49</div></div><div><span>        </span><span>// deallocating a bump allocation is a documented no-op.</span></div></div><div><div><div>50</div></div><div><span>        </span><span>let</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { </span><span>HEAP</span><span>.</span><span>alloc</span><span><span>(</span><span>Layout</span><span>::</span></span><span>new</span><span>::&lt;</span><span>u64</span><span>&gt;()) };</span></div></div><div><div><div>51</div></div><div><span>        </span><span>assert!</span><span><span>(</span><span>!</span></span><span>p</span><span>.</span><span>is_null</span><span>());</span></div></div><div><div><div>52</div></div><div><span>        </span><span>let</span><span> </span><span>used</span><span><span> </span><span>=</span><span> </span></span><span>HEAP</span><span>.</span><span>bytes_used</span><span>();</span></div></div><div><div><div>53</div></div><div><span>        </span><span>unsafe</span><span> { </span><span>HEAP</span><span>.</span><span>dealloc</span><span>(</span><span>p</span><span><span>, </span><span>Layout</span><span>::</span></span><span>new</span><span>::&lt;</span><span>u64</span><span>&gt;()) };</span></div></div><div><div><div>54</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>HEAP</span><span>.</span><span>bytes_used</span><span>(), </span><span>used</span><span>, </span><span>"dealloc must not move the cursor"</span><span>);</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>58</div></div><div><span>    </span><span>fn</span><span> </span><span>capacity_is_the_const_parameter</span><span>() {</span></div></div><div><div><div>59</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>BumpAllocator</span><span>::&lt;</span><span>128</span><span>&gt;::</span><span>new</span><span>().</span><span>capacity</span><span>(), </span><span>128</span><span>);</span></div></div><div><div><div>60</div></div><div><span>        </span><span>let</span><span> </span><span>default</span><span>: </span><span>BumpAllocator</span><span><span> </span><span>=</span><span> </span></span><span>BumpAllocator</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>61</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>default</span><span>.</span><span>capacity</span><span>(), </span><span>DEFAULT_HEAP_SIZE</span><span>);</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四个测试：<code>distinct_layouts_do_not_overlap</code>（两个不同对齐的分配不重叠且对齐正确）、<code>exhaustion_returns_null</code>（用 <code>BumpAllocator&lt;64&gt;</code> 一字节一字节地分配，恰好 64 次之后返回空指针）、<code>dealloc_is_a_no_op</code>、<code>capacity_is_the_const_parameter</code>（<code>BumpAllocator::&lt;128&gt;::new().capacity() == 128</code>，不带参数的是 <code>DEFAULT_HEAP_SIZE</code>）。<code>exhaustion_returns_null</code> 里的 <code>static SMALL: BumpAllocator&lt;64&gt;</code> 只有 64 字节，所以这个测试不会被另一个测试里的 <code>ALLOC</code> 影响。</p></section><section><h3>实验：它真的并发安全吗<a href="#实验它真的并发安全吗"><span>#</span></a></h3><p>“锁无关”不应该只靠读代码相信。实验 <code>threads.rs</code>（在 <code>~/lab-ch13-bump</code> 里，依赖 <code>pebble-nostd</code>，建法同 <code>lab-ch13-sys</code>；完整版在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch13-bump" target="_blank">lab/lab-ch13-bump</a>）：四个线程各分配 1000 次，每次 16 字节，把拿到的 4000 个地址收集起来排序去重：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>4000 allocations, 4000 distinct addresses</span></div></div><div><div><div>2</div></div><div><span>closest pair is 16 bytes apart</span></div></div><div><div><div>3</div></div><div><span>bytes_used = 64000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>4000 次分配得到 4000 个不同的地址，相邻地址最近也相距 16 字节（正好是请求的大小），<code>bytes_used</code> 是 <code>4000 × 16 = 64000</code>。一个跑了一次的实验不是证明（数据竞争是概率事件，一次没事说明不了多少），但它和代码推理指向同一个结论。真要认真验证，应该用 Miri 或者 loom 这类工具，我没有在这篇里跑它们。</p></section><section><h3>它的代价是什么<a href="#它的代价是什么"><span>#</span></a></h3><p>bump 分配器永远不释放，代价要实际量一下。实验 <code>vec_growth.rs</code>：把一个 <code>Vec&lt;u64&gt;</code> 一个个元素推到 1000，每次容量变化时打印一次 arena 的用量：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>len    1: capacity    4, arena used     32</span></div></div><div><div><div>2</div></div><div><span>len    5: capacity    8, arena used   1120</span></div></div><div><div><div>3</div></div><div><span>len    9: capacity   16, arena used   1248</span></div></div><div><div><div>4</div></div><div><span>len   17: capacity   32, arena used   1504</span></div></div><div><div><div>5</div></div><div><span>len   33: capacity   64, arena used   2016</span></div></div><div><div><div>6</div></div><div><span>len   65: capacity  128, arena used   3040</span></div></div><div><div><div>7</div></div><div><span>len  129: capacity  256, arena used   5088</span></div></div><div><div><div>8</div></div><div><span>len  257: capacity  512, arena used   9184</span></div></div><div><div><div>9</div></div><div><span>len  513: capacity 1024, arena used  17376</span></div></div><div><div><div>10</div></div><div><span>live data: 8000 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Vec</code> 的容量按 2 倍增长（4、8、16……1024），每次增长都要分配一块新的更大的内存，把旧内容搬过去，旧块不释放，所以用量是等比数列的和：<code>32 + 64 + 128 + ... + 8192 = 16352</code> 字节，加上第一次 <code>println!</code> 时标准库给标准输出分配的 1024 字节缓冲区，正好是 <code>len 513</code> 那一行的 17376。最终存活的数据只有 8000 字节，arena 里累计花掉的接近它的 2.2 倍。这就是”设计上不释放”的真实价格。再看耗尽时发生什么，<code>exhaust.rs</code> 用一个只有 64 KiB 的 arena 要 100 KiB：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>arena already used by the runtime: 552 bytes</span></div></div><div><div><div>2</div></div><div><span>memory allocation of 102400 bytes failed</span></div></div><div><div><div>3</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>4</div></div><div><span>bash: line 1: 63140 Aborted                    (core dumped) cargo run --quiet --bin exhaust</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>alloc</code> 返回空指针之后，标准库的 <code>Vec</code> 调用 <code>handle_alloc_error</code>，打印 “memory allocation of 102400 bytes failed” 并 <code>abort</code>。<code>GlobalAlloc</code> 的约定就是这样：分配器报告失败，由调用者决定怎么办，标准库的选择是死掉。（那行 <code>arena already used by the runtime: 552 bytes</code> 说明 Rust 运行时在 <code>main</code> 之前就已经向全局分配器要了 552 字节。）</p></section><section><h3>把分配器装上：#[global_allocator]<a href="#把分配器装上global_allocator"><span>#</span></a></h3><p>写 <code>crates/pebble-nostd/examples/bump_global.rs</code>，让整个进程（包括标准库自己的所有 <code>Vec</code>、<code>String</code>、<code>Box</code>）都用这个分配器：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Runs the `no_std` evaluator on top of the bump allocator.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! The whole program, including the standard library's own allocations, is</span></div></div><div><div><div>4</div></div><div><span>//! served from one 1 MiB static arena: `#[global_allocator]` replaces the</span></div></div><div><div><div>5</div></div><div><span>//! system allocator for every `Vec`, `String`, and `Box` in the process.</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! ```text</span></div></div><div><div><div>8</div></div><div><span>//! cargo run -p pebble-nostd --example bump_global</span></div></div><div><div><div>9</div></div><div><span>//! ```</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_nostd</span><span>::</span><span>bump</span><span>::</span></span><span>BumpAllocator</span><span>;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#[global_allocator]</span></div></div><div><div><div>14</div></div><div><span>static</span><span> </span><span>HEAP</span><span>: </span><span>BumpAllocator</span><span>&lt;{ </span><span>1024</span><span> * </span><span>1024</span><span><span> }&gt; </span><span>=</span><span> </span></span><span>BumpAllocator</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>before</span><span><span> </span><span>=</span><span> </span></span><span>HEAP</span><span>.</span><span>bytes_used</span><span>();</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span><span>pebble_nostd</span><span>::</span><span>eval</span><span>::</span></span><span>eval_source</span><span>(</span><span>"(1 + 2) * 3 - 4 / 2"</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>after</span><span><span> </span><span>=</span><span> </span></span><span>HEAP</span><span>.</span><span>bytes_used</span><span>();</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"result = {result:?}"</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>println!</span><span>(</span></div></div><div><div><div>23</div></div><div><span>        </span><span>"the evaluator allocated {} bytes from the bump arena"</span><span>,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>after</span><span> - </span><span>before</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>#[global_allocator]</code> 标在一个 <code>static</code> 上，要求它的类型实现了 <code>GlobalAlloc</code>，之后整个程序里所有堆分配都会走它的 <code>alloc</code>。C 里的对应物是用链接器 <code>--wrap=malloc</code> 或者自己定义一个 <code>malloc</code>/<code>free</code>，C++ 里是重载全局 <code>operator new</code>。<code>BumpAllocator&lt;{ 1024 * 1024 }&gt;</code> 是 1 MiB。注意这个例子里没有任何 <code>unsafe</code>：<code>unsafe</code> 都被关在 <code>bump.rs</code> 里了。那块 1 MiB 的内存到底放在哪？用 <code>nm</code> 看一眼编译出来的例子（在重建目录里跑 <code>cargo build -p pebble-nostd --example bump_global</code> 之后）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>nm</span><span> </span><span>-S</span><span> </span><span>--size-sort</span><span> </span><span>target/debug/examples/bump_global</span><span> | </span><span>tail</span><span> </span><span>-1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>000000000007b9c8 0000000000100008 b _ZN11bump_global4HEAP17hf608c719176c5af5E</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>符号 <code>HEAP</code>，类型 <code>b</code>（小写 <code>b</code> 是 <code>.bss</code> 段里的局部符号），大小 <code>0x100008</code> 字节，也就是 1 MiB 加上 8 字节的游标。零初始化的静态数据放进 <code>.bss</code>，只在可执行文件里记一个长度，不占文件体积。</p></section></section>
<section><h2>求值器：真正在这个分配器上跑的东西<a href="#求值器真正在这个分配器上跑的东西"><span>#</span></a></h2><p><code>crates/pebble-nostd/src/eval.rs</code> 是一个整数表达式求值器。它不是树遍历：词法分析器吐出的 token 流先被”调度场算法”（shunting-yard）翻译成逆波兰表达式（RPN），再用一个 <code>Vec&lt;i64&gt;</code> 当栈执行。对 <code>(1 + 2) * 3 - 4 / 2</code>，RPN 是 <code>1 2 + 3 * 4 2 / -</code>。</p><p>这个文件我们大部分直接复制：<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-nostd/src/eval.rs" target="_blank"><code>eval.rs</code></a> 里除了 <code>eval_source</code> 和 <code>eval_rpn</code> 两个函数以外，数据类型（<code>EvalError</code>、<code>Op</code>、<code>Rpn</code>、<code>StackItem</code>）、<code>compile</code>（调度场）、<code>push_operator</code> 和 6 个测试都照抄。理由：调度场算法是教科书内容，和这一篇的主题（边界）没有关系；而那两个函数一个是和词法分析器、<code>SourceMap</code> 的接口，一个是用检查算术做栈机的执行，是这篇真正想让你读的部分。我在重建目录里按这个方案做了：复制整个文件，再逐字对照 tag。</p><section><h3>eval_source：复用同一个词法分析器<a href="#eval_source复用同一个词法分析器"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Lexes and evaluates `src`, returning its integer value.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// A fresh [`SourceMap`] is created internally, so callers do not have to know</span></div></div><div><div><div>4</div></div><div><span>/// about source ids just to evaluate a snippet.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>eval_source</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>i64</span><span>, </span><span>EvalError</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"&lt;nostd&gt;"</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span><span> </span><span>!</span></span><span>diagnostics</span><span>.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>// The lexer keeps going after a bad character, so a non-empty</span></div></div><div><div><div>11</div></div><div><span>        </span><span>// diagnostic list means the stream is not the expression we asked for.</span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span>(</span><span>EvalError</span><span>::</span><span>UnexpectedToken</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>    </span><span>eval_rpn</span><span>(&amp;</span><span>compile</span><span>(&amp;</span><span>tokens</span><span>)?)</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里把前面几篇的零件接起来：<code>SourceMap::new()</code>（第 2 篇）建一个空的源文件表，<code>add</code> 登记一个叫 <code>&lt;nostd&gt;</code> 的”文件”得到 <code>SourceId</code>，<code>tokenize</code>（第 4 篇）吐出 token 和诊断信息，诊断非空说明有非法字符，直接拒绝。注意 <code>TokenKind</code> 里的字符串是借用自 <code>src</code> 的 <code>&amp;str</code>（第 4 篇的 <code>'src</code> 生命周期），所以 <code>tokenize</code> 本身没有拷贝文本。</p><p>一个自然的疑问：2000 字节都用在哪了？实验 <code>stages.rs</code>（同一个 lab）量了前两步：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SourceMap::add       352 bytes</span></div></div><div><div><div>2</div></div><div><span>tokenize            1160 bytes (12 tokens, 0 diagnostics)</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;Token&gt;() = 40</span></div></div><div><div><div>4</div></div><div><span>eval_source (all)   2000 bytes -&gt; Ok(7)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>SourceMap::add</code> 花了 352 字节（它自己拥有文件名、源文本和行首表，再加上内部 <code>Vec</code> 的第一次分配，细账我没拆），<code>tokenize</code> 花了 1160 字节：12 个 token，每个 40 字节，<code>Vec</code> 增长时旧块不释放，所以是 <code>160 + 320 + 640 = 1120</code> 再加一点零头。这两步一共 1512，离 2000 还差 488，剩下的是 <code>compile</code> 里的两个 <code>Vec</code>（输出的 RPN 和运算符栈）加 <code>eval_rpn</code> 里的栈。我没有再往下细拆，这个数字是实测的，细账没做。</p></section><section><h3>eval_rpn：用 Vec 当栈，用检查算术当护栏<a href="#eval_rpn用-vec-当栈用检查算术当护栏"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Runs the compiled program on a [`Vec`] used as the machine stack.</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>eval_rpn</span><span>(</span><span>rpn</span><span>: &amp;[</span><span>Rpn</span><span>]) -&gt; </span><span>Result</span><span>&lt;</span><span>i64</span><span>, </span><span>EvalError</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>stack</span><span>: </span><span>Vec</span><span>&lt;</span><span>i64</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>for</span><span> </span><span>instruction</span><span> </span><span>in</span><span> </span><span>rpn</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>match</span><span> *</span><span>instruction</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Rpn</span><span>::</span><span>Push</span><span>(</span><span>value</span><span>) =&gt; </span><span>stack</span><span>.</span><span>push</span><span>(</span><span>value</span><span>),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Rpn</span><span>::</span><span>Apply</span><span><span>(</span><span>Op</span><span>::</span></span><span>Neg</span><span>) =&gt; {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>stack</span><span>.</span><span>pop</span><span>().</span><span>ok_or</span><span><span>(</span><span>EvalError</span><span>::</span></span><span>UnexpectedEnd</span><span>)?;</span></div></div><div><div><div>9</div></div><div><span>                </span><span>stack</span><span>.</span><span>push</span><span>(</span><span>value</span><span>.</span><span>checked_neg</span><span>().</span><span>ok_or</span><span><span>(</span><span>EvalError</span><span>::</span></span><span>Overflow</span><span>)?);</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Rpn</span><span>::</span><span>Apply</span><span>(</span><span>op</span><span>) =&gt; {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>let</span><span> </span><span>rhs</span><span><span> </span><span>=</span><span> </span></span><span>stack</span><span>.</span><span>pop</span><span>().</span><span>ok_or</span><span><span>(</span><span>EvalError</span><span>::</span></span><span>UnexpectedEnd</span><span>)?;</span></div></div><div><div><div>13</div></div><div><span>                </span><span>let</span><span> </span><span>lhs</span><span><span> </span><span>=</span><span> </span></span><span>stack</span><span>.</span><span>pop</span><span>().</span><span>ok_or</span><span><span>(</span><span>EvalError</span><span>::</span></span><span>UnexpectedEnd</span><span>)?;</span></div></div><div><div><div>14</div></div><div><span>                </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>15</div></div><div><span>                    </span><span>Op</span><span>::</span><span>Add</span><span> =&gt; </span><span>lhs</span><span>.</span><span>checked_add</span><span>(</span><span>rhs</span><span>),</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>Op</span><span>::</span><span>Sub</span><span> =&gt; </span><span>lhs</span><span>.</span><span>checked_sub</span><span>(</span><span>rhs</span><span>),</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>Op</span><span>::</span><span>Mul</span><span> =&gt; </span><span>lhs</span><span>.</span><span>checked_mul</span><span>(</span><span>rhs</span><span>),</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>Op</span><span>::</span><span>Div</span><span> =&gt; {</span></div></div><div><div><div>19</div></div><div><span>                        </span><span>if</span><span> </span><span>rhs</span><span><span> </span><span>==</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>20</div></div><div><span>                            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>EvalError</span><span>::</span><span>DivisionByZero</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>                        </span><span>lhs</span><span>.</span><span>checked_div</span><span>(</span><span>rhs</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>                    </span><span>Op</span><span>::</span><span>Rem</span><span> =&gt; {</span></div></div><div><div><div>25</div></div><div><span>                        </span><span>if</span><span> </span><span>rhs</span><span><span> </span><span>==</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>26</div></div><div><span>                            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>EvalError</span><span>::</span><span>DivisionByZero</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>                        </span><span>lhs</span><span>.</span><span>checked_rem</span><span>(</span><span>rhs</span><span>)</span></div></div><div><div><div>29</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>                    </span><span>Op</span><span>::</span><span>Neg</span><span> =&gt; </span><span>unreachable!</span><span>(</span><span>"handled by the arm above"</span><span>),</span></div></div><div><div><div>31</div></div><div><span><span>                </span></span><span>};</span></div></div><div><div><div>32</div></div><div><span>                </span><span>stack</span><span>.</span><span>push</span><span>(</span><span>value</span><span>.</span><span>ok_or</span><span><span>(</span><span>EvalError</span><span>::</span></span><span>Overflow</span><span>)?);</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>match</span><span> </span><span>stack</span><span>.</span><span>len</span><span>() {</span></div></div><div><div><div>38</div></div><div><span>        </span><span>1</span><span> =&gt; </span><span>Ok</span><span>(</span><span>stack</span><span>[</span><span>0</span><span>]),</span></div></div><div><div><div>39</div></div><div><span>        </span><span>0</span><span> =&gt; </span><span>Err</span><span>(</span><span>EvalError</span><span>::</span><span>UnexpectedEnd</span><span>),</span></div></div><div><div><div>40</div></div><div><span>        </span><span>// Leftover operands, e.g. `1 2`.</span></div></div><div><div><div>41</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>Err</span><span>(</span><span>EvalError</span><span>::</span><span>UnexpectedToken</span><span>),</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读几个地方。<code>match *instruction</code> 里 <code>Rpn::Apply(Op::Neg)</code> 在 <code>Rpn::Apply(op)</code> 之前：<code>match</code> 从上往下找第一个匹配，特殊情形放前面，所以后面那个臂里的 <code>Op::Neg =&gt; unreachable!(...)</code> 真的不可达。<code>stack.pop().ok_or(EvalError::UnexpectedEnd)?</code>：<code>pop</code> 返回 <code>Option</code>，<code>ok_or</code> 把它变成 <code>Result</code>，<code>?</code> 提前返回（第 7 篇的老套路）：栈里没有操作数就是表达式写错了，比如 <code>1 +</code>。所有算术都是 <code>checked_*</code>：<code>checked_add</code> 溢出时返回 <code>None</code> 而不是回绕或者 panic，所以 <code>9223372036854775807 + 1</code> 变成 <code>Err(EvalError::Overflow)</code>。最后一个小例子，<code>i64::MIN</code> 取负也会溢出，所以 <code>Neg</code> 用 <code>checked_neg</code>。第 1 篇说过 C 里有符号溢出是未定义行为，Rust 里每一种处理（panic、回绕、<code>checked</code>、<code>saturating</code>）都是你明确选的。</p><p><code>compile</code> 里还有一处新写法，复制代码的时候留意：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>kind</span><span> @ (</span><span>TokenKind</span><span>::</span><span>Plus</span></div></div><div><div><div>2</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Minus</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Star</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Slash</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Percent</span><span>) =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>let</span><span> </span><span>op</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>kind</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>TokenKind</span><span>::</span><span>Plus</span><span> =&gt; </span><span>Op</span><span>::</span><span>Add</span><span>,</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>TokenKind</span><span>::</span><span>Minus</span><span> =&gt; </span><span>Op</span><span>::</span><span>Sub</span><span>,</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>TokenKind</span><span>::</span><span>Star</span><span> =&gt; </span><span>Op</span><span>::</span><span>Mul</span><span>,</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>TokenKind</span><span>::</span><span>Slash</span><span> =&gt; </span><span>Op</span><span>::</span><span>Div</span><span>,</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>TokenKind</span><span>::</span><span>Percent</span><span> =&gt; </span><span>Op</span><span>::</span><span>Rem</span><span>,</span></div></div><div><div><div>12</div></div><div><span>                    </span><span>_</span><span> =&gt; </span><span>unreachable!</span><span>(</span><span>"guarded by the match arm above"</span><span>),</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>kind @ (A | B | ...)</code> 是”绑定加模式”：先用括号里的或模式判断 token 是不是五个运算符之一，匹配的话再把整个匹配到的值绑定到名字 <code>kind</code> 上，下面的内层 <code>match</code> 就可以直接用它。内层最后一个 <code>_ =&gt; unreachable!(...)</code> 的存在，说明编译器没法从外层的模式推出内层已经穷尽，所以只能手写一个”不可能”，这是这种写法的小代价。</p><div><div><div></div><div>跑一遍</div></div><div><p><code>cargo test -p pebble-nostd</code> 之前，确认 <code>eval.rs</code> 里的 <code>use alloc::vec::Vec;</code> 还在，而且整个文件没有任何 <code>std::</code> 导入：<code>pebble-nostd</code> 里的”正文”只有 <code>core</code> 和 <code>alloc</code>。</p></div></div><p>运行例子：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>-p</span><span> </span><span>pebble-nostd</span><span> </span><span>--example</span><span> </span><span>bump_global</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>result = Ok(7)</span></div></div><div><div><div>2</div></div><div><span>the evaluator allocated 2000 bytes from the bump arena</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>result = Ok(7)</code>：<code>(1 + 2) * 3 - 4 / 2 = 9 - 2 = 7</code>。对一个 19 个字符的表达式，词法分析加求值向 1 MiB 的 arena 要了 2000 字节，不到 0.2%。把这个分配器换成一个 16 KiB 的、放在微控制器 RAM 里的，同样的代码照样跑。我没有验证这一点（我没有装任何裸机目标，也没有交叉编译过它），所以这只是推测；能验证的部分是：这个 crate 的库代码没有引用 <code>std</code>（上面那个编译失败的实验），而且它真的在一个自己写的全局分配器上跑出了正确答案。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在重建目录里：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-sys</span><span> </span><span>-p</span><span> </span><span>pebble-nostd</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>-p</span><span> </span><span>pebble-sys</span><span> </span><span>-p</span><span> </span><span>pebble-nostd</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一条命令的输出（我用 <code>grep</code> 只留了关键行）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_nostd-bfc6c2d04f19dc0b)</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_sys-9e3357754a97ae75)</span></div></div><div><div><div>4</div></div><div><span>test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>Doc-tests pebble_nostd</span></div></div><div><div><div>6</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>Doc-tests pebble_sys</span></div></div><div><div><div>8</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pebble-sys</code> 15 个单元测试加 2 个文档测试（<code>fnv1a</code> 和 <code>fold</code> 各一个），<code>pebble-nostd</code> 10 个单元测试（6 个求值器的，4 个分配器的）加 1 个文档测试（<code>BumpAllocator</code> 上的 <code>static HEAP: BumpAllocator = ...</code>）。第二条命令（clippy，<code>-D warnings</code> 把所有警告当作错误）在我的重建目录里通过，输出只有 <code>Checking</code>/<code>Finished</code> 几行，没有任何警告。</p><p>这些文件现在必须和 tag 逐字节一致。把 tag 克隆到 <code>~/pebble-tag</code>（第 1 篇做过就跳过：<code>git clone --branch tutorial-v1 https://github.com/bfmhno3/pebble ~/pebble-tag</code>），然后在你的工作区根目录里对每个目录跑 <code>diff -r</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>-r</span><span> </span><span>crates/pebble-sys</span><span> </span><span>~/pebble-tag/crates/pebble-sys</span></div></div><div><div><div>2</div></div><div><span>diff</span><span> </span><span>-r</span><span> </span><span>crates/pebble-nostd</span><span> </span><span>~/pebble-tag/crates/pebble-nostd</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两条命令都没有任何输出。这十个文件是”完成品”：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-sys/Cargo.toml" target="_blank"><code>crates/pebble-sys/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-sys/build.rs" target="_blank"><code>crates/pebble-sys/build.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-sys/csrc/include/pebble_shim.h" target="_blank"><code>crates/pebble-sys/csrc/include/pebble_shim.h</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-sys/csrc/pebble_shim.c" target="_blank"><code>crates/pebble-sys/csrc/pebble_shim.c</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-sys/src/lib.rs" target="_blank"><code>crates/pebble-sys/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-nostd/Cargo.toml" target="_blank"><code>crates/pebble-nostd/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-nostd/src/lib.rs" target="_blank"><code>crates/pebble-nostd/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-nostd/src/bump.rs" target="_blank"><code>crates/pebble-nostd/src/bump.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-nostd/src/eval.rs" target="_blank"><code>crates/pebble-nostd/src/eval.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-nostd/examples/bump_global.rs" target="_blank"><code>crates/pebble-nostd/examples/bump_global.rs</code></a></li>
</ul><p>根目录的 <code>Cargo.toml</code> 现在还不用和 tag 一致（它在第 15 篇才完成），只要有这两个 crate 和 <code>cc</code> 的三行。没有中间状态的文件：这一篇所有 crate 都是一次到位。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的所有实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank">lab/rust-lab</a> 是主实验室）。按第 1 篇的方式把 tag 克隆到 <code>~/pebble-tag</code> 后，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，在那里运行（<code>cd ~/pebble-tag/lab/rust-lab</code>，也可以把这个目录复制成 <code>~/rust-lab</code>）。表里 <code>lab-</code> 开头的文件属于旁边的独立实验 crate，命令里写了完整的目录。</p><p>补丁（<code>lab/exercises/*.patch</code>）要在一份干净的 <code>~/pebble-tag</code> 里从仓库根目录应用，跑完用 <code>git apply -R lab/exercises/ch13-s2.patch</code> 之类的命令还原。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_extern_plain.rs" target="_blank"><code>ch13_extern_plain.rs</code></a></td><td><code>cargo build --bin ch13_extern_plain</code></td><td>编译失败：<code>error: extern blocks must be unsafe</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_call_unsafe.rs" target="_blank"><code>ch13_call_unsafe.rs</code></a></td><td><code>cargo build --bin ch13_call_unsafe</code></td><td>编译失败：<code>E0133</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_strlen.rs" target="_blank"><code>ch13_strlen.rs</code></a></td><td><code>cargo run --quiet --bin ch13_strlen</code></td><td>运行成功：<code>strlen = 6</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_safe_fn.rs" target="_blank"><code>ch13_safe_fn.rs</code></a></td><td><code>cargo run --quiet --bin ch13_safe_fn</code></td><td>运行成功：<code>getpid() &gt; 0: true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_repr_c.rs" target="_blank"><code>ch13_repr_c.rs</code></a></td><td><code>cargo run --quiet --bin ch13_repr_c</code></td><td>运行成功：<code>Rusty: size 8 align 4</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_opaque.rs" target="_blank"><code>ch13_opaque.rs</code></a></td><td><code>cargo build --bin ch13_opaque</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-bump/src/bin/threads.rs" target="_blank"><code>lab-ch13-bump/threads.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-bump &amp;&amp; cargo run --quiet --bin threads</code></td><td>运行成功：<code>4000 allocations, 4000 distinct addresses</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_niche.rs" target="_blank"><code>ch13_niche.rs</code></a></td><td><code>cargo run --quiet --bin ch13_niche</code></td><td>运行成功：<code>*mut u8             8</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch13-scratch.patch" target="_blank"><code>exercises/ch13-scratch.patch</code></a></td><td><code>cd ~/pebble-tag &amp;&amp; git apply lab/exercises/ch13-scratch.patch &amp;&amp; cargo clippy -p pebble-sys --lib</code></td><td>clippy 警告：<code>warning: mutable borrow from immutable input(s)</code></td></tr><tr><td>同一个补丁（<code>examples/overlap.rs</code>）</td><td><code>cargo run -p pebble-sys --example overlap</code></td><td>运行成功：<code>a and b start at the same address: true</code>，下一行 <code>a[0] = 2</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-sys/src/bin/arena_two_slices.rs" target="_blank"><code>lab-ch13-sys/arena_two_slices.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-sys &amp;&amp; cargo build --bin arena_two_slices</code></td><td>编译失败：<code>E0499</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-sys/src/bin/arena_bytes.rs" target="_blank"><code>lab-ch13-sys/arena_bytes.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-sys &amp;&amp; cargo run --quiet --bin arena_bytes</code></td><td>运行成功：<code>len  1 at address ...f0</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-sys/src/bin/arena_send.rs" target="_blank"><code>lab-ch13-sys/arena_send.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-sys &amp;&amp; cargo build --bin arena_send</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-sys/src/bin/closure_cb.rs" target="_blank"><code>lab-ch13-sys/closure_cb.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-sys &amp;&amp; cargo build --bin closure_cb</code></td><td>编译失败：<code>E0308</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-sys/src/bin/fold_nocatch.rs" target="_blank"><code>lab-ch13-sys/fold_nocatch.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-sys &amp;&amp; cargo run --quiet --bin fold_nocatch</code></td><td>退出码 134（<code>SIGABRT</code>）：<code>thread 'main' panicked at src/bin/fold_nocatch.rs:15:5:</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_unwind_safe.rs" target="_blank"><code>ch13_unwind_safe.rs</code></a></td><td><code>cargo build --bin ch13_unwind_safe</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_any_payload.rs" target="_blank"><code>ch13_any_payload.rs</code></a></td><td><code>cargo run --quiet --bin ch13_any_payload</code></td><td>运行成功：<code>&amp;str: a literal; stderr: thread panicked at src/bin/ch13_any_payload.rs:15:35:</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-sys/src/bin/fold_panic.rs" target="_blank"><code>lab-ch13-sys/fold_panic.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-sys &amp;&amp; cargo run --quiet --bin fold_panic</code></td><td>运行成功：<code>caught on the Rust side: assertion `left != right` failed: closure exploded; stderr: thread panicked at src/bin/fold_panic.rs:6:13:</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-cdylib/src/lib.rs" target="_blank"><code>lab-ch13-cdylib/src/lib.rs</code></a> 和 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-cdylib/main.c" target="_blank"><code>main.c</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-cdylib &amp;&amp; cargo build &amp;&amp; cc main.c -Ltarget/debug -lpebble_ffi_demo -o /tmp/ffi_main &amp;&amp; LD_LIBRARY_PATH=target/debug /tmp/ffi_main</code></td><td>构建无报错；打印 <code>pebble_add(40, 2) = 42</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_repr_transparent.rs" target="_blank"><code>ch13_repr_transparent.rs</code></a></td><td><code>cargo run --quiet --bin ch13_repr_transparent</code></td><td>运行成功：<code>Handle: size 8 align 8</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_repr_u8.rs" target="_blank"><code>ch13_repr_u8.rs</code></a></td><td><code>cargo run --quiet --bin ch13_repr_u8</code></td><td>运行成功：<code>size_of::&lt;Status&gt;() = 1</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_union.rs" target="_blank"><code>ch13_union.rs</code></a></td><td><code>cargo run --quiet --bin ch13_union</code></td><td>运行成功：<code>1.0f32 as bits = 0x3f800000</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_manually_drop.rs" target="_blank"><code>ch13_manually_drop.rs</code></a></td><td><code>cargo run --quiet --bin ch13_manually_drop</code></td><td>运行成功：<code>before the explicit drop</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_static_mut.rs" target="_blank"><code>ch13_static_mut.rs</code></a></td><td><code>cargo build --bin ch13_static_mut</code></td><td>编译失败：<code>error: creating a shared reference to mutable static</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_static_sync.rs" target="_blank"><code>ch13_static_sync.rs</code></a></td><td><code>cargo build --bin ch13_static_sync</code></td><td>编译失败：<code>E0277</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_const_generic.rs" target="_blank"><code>ch13_const_generic.rs</code></a></td><td><code>cargo run --quiet --bin ch13_const_generic</code></td><td>运行成功：<code>default: capacity 4 size_of 4</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_const_generic_mismatch.rs" target="_blank"><code>ch13_const_generic_mismatch.rs</code></a></td><td><code>cargo build --bin ch13_const_generic_mismatch</code></td><td>编译失败：<code>E0308</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-bump/src/bin/vec_growth.rs" target="_blank"><code>lab-ch13-bump/vec_growth.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-bump &amp;&amp; cargo run --quiet --bin vec_growth</code></td><td>运行成功：<code>len    1: capacity    4, arena used     32</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-bump/src/bin/exhaust.rs" target="_blank"><code>lab-ch13-bump/exhaust.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-bump &amp;&amp; cargo run --quiet --bin exhaust</code></td><td>退出码 134：<code>arena already used by the runtime: 552 bytes</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch13-bump/src/bin/stages.rs" target="_blank"><code>lab-ch13-bump/stages.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch13-bump &amp;&amp; cargo run --quiet --bin stages</code></td><td>运行成功：<code>SourceMap::add       352 bytes</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch13-s2.patch" target="_blank"><code>exercises/ch13-s2.patch</code></a></td><td><code>cd ~/pebble-tag &amp;&amp; git apply lab/exercises/ch13-s2.patch &amp;&amp; cargo test -p pebble-nostd -p pebble-sys</code></td><td>全部通过：nostd 11 个测试，sys 16 个测试，文档测试 1 个加 2 个</td></tr></tbody></table><p>仓库里还有 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch13_extern_panic.rs" target="_blank"><code>ch13_extern_panic.rs</code></a>（<code>cargo run --quiet --bin ch13_extern_panic</code>，退出码 134，先打印 <code>callback(1) = 1</code>），文章没有用到。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 为 C 的 arena 加一个新函数：<code>pebble_arena_remaining</code>，返回还没用掉的字节数（不算对齐）。</strong> 要改哪几个文件？为什么 <code>build.rs</code> 里的 <code>rerun-if-changed</code> 列表里有头文件？</p>
参考答案<p>要改四处：头文件里声明 <code>size_t pebble_arena_remaining(const PebbleArena *arena);</code>，<code>.c</code> 里实现，Rust 的 <code>extern</code> 块里加声明，再写一个 <code>Arena::remaining</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size_t</span><span> </span><span>pebble_arena_remaining</span><span>(</span><span>const</span><span> PebbleArena </span><span>*</span><span><span>arena</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>return</span><span> arena </span><span>==</span><span> </span><span>NULL</span><span> </span><span>?</span><span> </span><span>0</span><span> </span><span>:</span><span> </span><span>arena</span><span>-&gt;</span><span>capacity</span><span> </span><span>-</span><span> </span><span>arena</span><span>-&gt;</span><span>used</span><span>;</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Bytes still unused, ignoring alignment padding.</span></div></div><div><div><div>2</div></div><div><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>remaining</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>// SAFETY: `self.ptr` is a live arena and the C function only reads it.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>unsafe</span><span> { </span><span>pebble_arena_remaining</span><span>(</span><span>self</span><span>.ptr.</span><span>as_ptr</span><span>()) }</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>加一个测试 <code>remaining_plus_used_is_capacity</code>：新建 64 字节的 arena，断言 <code>remaining() == 64</code>，分配 10 字节之后断言 <code>remaining() + used() == capacity()</code>。我在重建目录的一份拷贝里做了这些改动（存成了补丁 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch13-s2.patch" target="_blank">lab/exercises/ch13-s2.patch</a>），<code>cargo test -p pebble-sys</code> 是 16 个单元测试加 2 个文档测试，全部通过。</p><p><code>rerun-if-changed</code> 里列头文件，是因为 <code>build.rs</code> 只在列出的文件变化时才重跑；<code>.c</code> 文件 <code>#include</code> 了头文件，如果头文件改了而没有列出来，<code>.c</code> 不会被重新编译，C 和 Rust 对函数签名的理解就会不一致，而这种不一致编译器和链接器都发现不了。</p><p><strong>2. 如果 <code>fold_trampoline</code> 不用 <code>catch_unwind</code>，闭包 panic 时会发生什么？</strong></p>
参考答案<p>进程被 <code>abort</code>。因为蹦床是一个 <code>extern "C" fn</code>，panic 要从它里面展开出去就必须穿过 C 的 <code>pebble_fold</code> 栈帧，rustc 1.95.0 会在 <code>extern "C"</code> 边界上插一个检查，直接中止，并打印 “panic in a function that cannot unwind”。实验 <code>fold_nocatch.rs</code> 的输出就是证据：退出码 134（<code>SIGABRT</code>），回溯里同时有 <code>fold_nocatch::exploding</code> 和 <code>pebble_fold</code>（带着 <code>pebble_shim.c:120</code> 的行号）。</p><p>这不是一个”更糟的错误信息”的问题：进程里别的线程、别的未写入的数据都被一起带走了。<code>catch_unwind</code> 的价值是把”进程中止”降级为”这次调用以 panic 失败”，而且保留原始的 panic 负载，这样调用者仍然可以决定要不要继续传播。</p><p><strong>3. 让 <code>BumpAllocator</code> 有一个 <code>reset</code>，把整块 arena 还原成空闲，并且是健全的（sound）。</strong></p>
参考答案<p><code>reset</code> 必须取 <code>&amp;mut self</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>reset</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>*</span><span>self</span><span>.state.</span><span>get_mut</span><span>().used.</span><span>get_mut</span><span><span>() </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>UnsafeCell::get_mut</code> 和 <code>AtomicUsize::get_mut</code> 都是安全函数：只要你持有 <code>&amp;mut</code>，编译器就保证没有别人能同时访问，所以不用原子操作，直接写零即可。</p><p>为什么必须是 <code>&amp;mut self</code>：<code>reset</code> 之后，之前交出去的所有指针所指的内存都会被再次分配，所以调用 <code>reset</code> 的时候不能有任何还在用的指针；<code>&amp;mut self</code> 是 Rust 里表达”没有其他人在用它”的方式。代价是它没法用在 <code>#[global_allocator]</code> 上：<code>static</code> 只能以共享引用的方式访问。我写了一个测试，对 <code>static SHARED: BumpAllocator&lt;64&gt;</code> 调用 <code>SHARED.reset()</code>，编译器给出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling pebble-span v0.1.0 (/tmp/lab-ch13-s2/crates/pebble-span)</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-diag v0.1.0 (/tmp/lab-ch13-s2/crates/pebble-diag)</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Compiling pebble-lexer v0.1.0 (/tmp/lab-ch13-s2/crates/pebble-lexer)</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>Compiling pebble-nostd v0.1.0 (/tmp/lab-ch13-s2/crates/pebble-nostd)</span></div></div><div><div><div>5</div></div><div><span>error[E0596]: cannot borrow immutable static item `SHARED` as mutable</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-nostd/src/bump.rs:238:9</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>237 |         static SHARED: BumpAllocator&lt;64&gt; = BumpAllocator::new();</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|         -------------------------------- this `static` cannot be borrowed as mutable</span></div></div><div><div><div>10</div></div><div><span>238 |         SHARED.reset();</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|         ^^^^^^ cannot borrow as mutable</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0596`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `pebble-nostd` (lib test) due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在一份拷贝里，对一个局部的 <code>BumpAllocator&lt;64&gt;</code>（分配 8 字节，<code>reset</code>，<code>bytes_used()</code> 归零）的测试通过（<code>nostd</code> 变成 11 个测试）。如果真想要”全局分配器可 reset”，就需要调用者保证（自己写 <code>unsafe</code>）：此刻程序里没有任何仍然活着的分配。那个契约编译器检查不了，这是 <code>unsafe fn reset(&amp;self)</code> 的领地，我不打算在 Pebble 里写它。</p></section>
<section><h2>从 FFI 到下一步<a href="#从-ffi-到下一步"><span>#</span></a></h2><p>写这一篇的时候，我数了数 <code>pebble-sys</code> 里 <code>unsafe</code> 的行数：<code>lib.rs</code> 里 10 处 <code>unsafe</code> 块，每一处都有一句 <code>SAFETY</code>，加起来不到总行数的十分之一。调用这个 crate 的人（仓库里别的 crate，包括你自己写的）不需要在任何地方写 <code>unsafe</code>；一个包装层做得好不好，就看它能不能把”这个函数是不是 unsafe”这个问题从调用者那里拿走。同样的结构出现在 <code>pebble-nostd</code>：<code>bump.rs</code> 里有 <code>unsafe impl</code> 和几处 <code>unsafe</code> 块，<code>eval.rs</code> 一处都没有，<code>bump_global.rs</code> 也一处都没有。</p><p>下一步其实是反过来的问题：我们已经有了一个会在多个线程上被调用的 <code>BumpAllocator</code>，但整个 Pebble 的解释器还是单线程、<code>Rc</code> 的世界。第 14 篇我们把线程、<code>Send</code> 与 <code>Sync</code>、<code>Arc</code> 和 <code>Mutex</code> 一次讲清楚，让 <code>pebble check</code> 能并行检查多个文件，然后再上一层：用 Tokio 写一个能同时服务很多连接的 Pebble 求值服务器，并看看为什么 <code>Rc</code> 做的解释器偏偏是 <code>!Send</code>，我们要靠 <code>spawn_blocking</code> 绕开它。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-12-vm-backends/</id>
      <title type="text">栈式虚拟机：GC 根、两个后端与 Criterion</title>
      <published>2026-10-13T11:00:00.000Z</published>
      <updated>2026-10-13T11:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-12-vm-backends/"/>
      <summary type="text">把第 10 篇的字节码放到第 11 篇的 GC 堆上执行：写栈式虚拟机、挑出 GC 根，再用 Backend trait 和 #[differential] 属性宏把两个后端并排摆好，最后用 Criterion 量出真实的速度差。</summary>
      <content type="html"><![CDATA[<p>前三篇我们造了三个互不相干的零件：一张线性的字节码清单（<a href="/posts/rust-by-pebble-10-bytecode-macros/">第 10 篇</a>），一个会找出环的垃圾回收器（<a href="/posts/rust-by-pebble-11-gc-unsafe/">第 11 篇</a>），还有一个会泄漏环的树遍历解释器（<a href="/posts/rust-by-pebble-09-interpreter-closures/">第 9 篇</a>）。这一篇把它们拼起来：一台读字节码、把对象放在 GC 堆上的虚拟机。拼完之后有两个自然的问题，我都拿真实的数字回答了。第一个：它快多少？<code>fib(20)</code> 在树遍历解释器上是 7.76 ms，在 VM 上是 5.00 ms，快 1.55 倍。不是 10 倍，甚至不到 2 倍。第二个：它们算出来的东西一样吗？不一样，而且我手里有五个现成的程序。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇我们写完 <code>pebble-vm</code>（栈式虚拟机，GC 根，14 个测试）和 <code>pebble-bench</code>（共享的基准程序，Criterion 基准，差分测试），给 <code>pebble-macros</code> 加上第三种过程宏 <code>#[differential]</code>（属性宏），并把命令行升到第四版：<code>Backend</code> trait、<code>run --vm</code>、<code>bench</code> 子命令。涉及的 Rust 特性：<code>Copy</code> 的值枚举与句柄、GC 根与借用冲突、静态分发与动态分发、关联常量、泛型关联类型（GAT）、dyn 兼容性（E0038）、属性宏（<code>ItemFn</code>、<code>format_ident!</code>）、Criterion。上一篇：<a href="/posts/rust-by-pebble-11-gc-unsafe/">手写垃圾回收器</a>。做完之后 <code>pebble-vm</code>、<code>pebble-bench</code>、<code>pebble-macros</code> 与 <code>tutorial-v1</code> 标签逐字节一致。</p></div></div>
<section><h2>要解决什么问题<a href="#要解决什么问题"><span>#</span></a></h2><p>字节码已经有了。第 10 篇的反汇编器能把 <code>fib</code> 打印成一张清单，但没有任何东西执行它。执行的办法是教科书式的：一个循环，每圈取一条指令，用一个 <code>match</code> 分派，操作一个<strong>值栈</strong>。<code>1 + 2</code> 编译成 <code>Const 1, Const 2, Add</code>：前两条各压一个数，<code>Add</code> 弹出两个、压回一个和。</p><p>麻烦在对象。数字、布尔、<code>nil</code> 可以直接放在栈上，但字符串、列表、映射、闭包的环境不行：它们的大小不固定，会被多处引用，还会互相引用成环。第 9 篇我们用 <code>Rc&lt;RefCell&lt;..&gt;&gt;</code> 放这些东西，付出的代价是环泄漏。这一次它们放进第 11 篇的 <code>Heap&lt;VmObject&gt;</code>，栈上只保存 <code>GcRef</code> 句柄。所以 VM 有三块内存：</p><div><div><div><div><span><p>根: 回收器从这里出发</p></span></div><div><span><p>VmValue::Ref</p></span></div><div><span><p>每帧一个环境 GcRef</p></span></div><div><span></span></div><div><span></span></div><div><span><p>trace</p></span></div><div><span></span></div><div><span><p>值栈<br /><br />Vec of VmValue</p></span></div><div><span><p>调用帧<br /><br />Vec of Frame</p></span></div><div><span><p>全局环境<br /><br />一个 GcRef</p></span></div><div><span><p>GC 堆<br /><br />Heap of VmObject</p></span></div><div><span></span></div><div><span></span></div>
</div><div><div><span><p>根: 回收器从这里出发</p></span></div><div><span><p>VmValue::Ref</p></span></div><div><span><p>每帧一个环境 GcRef</p></span></div><div><span></span></div><div><span></span></div><div><span><p>trace</p></span></div><div><span></span></div><div><span><p>值栈<br /><br />Vec of VmValue</p></span></div><div><span><p>调用帧<br /><br />Vec of Frame</p></span></div><div><span><p>全局环境<br /><br />一个 GcRef</p></span></div><div><span><p>GC 堆<br /><br />Heap of VmObject</p></span></div><div><span></span></div><div><span></span></div>
</div></div></div><p>三个箭头指向堆的东西就是<strong>根</strong>。第 11 篇说过，回收器自己不知道什么是栈，根是嵌入方的事。这一篇我们就是嵌入方，所以”哪些东西是根”这个问题得由我们答对，答错的后果在后面的实验里有真实的 panic。</p></section>
<section><h2>建 crate<a href="#建-crate"><span>#</span></a></h2><p>在 workspace 根目录（你的是 <code>~/pebble</code>）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-vm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把 <code>crates/pebble-vm/Cargo.toml</code> 整个换成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-vm"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"A stack-based virtual machine for Pebble bytecode."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div><span>pebble-bytecode.workspace</span><span> = </span><span>true</span></div></div><div><div><div>15</div></div><div><span>pebble-gc.workspace</span><span> = </span><span>true</span></div></div><div><div><div>16</div></div><div><span>pebble-value.workspace</span><span> = </span><span>true</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>19</div></div><div><span>pebble-parser.workspace</span><span> = </span><span>true</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>22</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>五个正常依赖：<code>span</code>（报错要位置）、<code>diag</code>、<code>bytecode</code>（输入）、<code>gc</code>（堆）、<code>value</code>（<code>RuntimeError</code>、<code>Output</code>、内建函数名表）。<code>pebble-parser</code> 只在测试里把字符串变成 AST，放在 <code>[dev-dependencies]</code>，和第 10 篇的 <code>pebble-bytecode</code> 一样。</p><p>根 <code>Cargo.toml</code> 要多三样东西。在 <code>[workspace.dependencies]</code> 里，给内部 crate 那段加一行 <code>pebble-vm</code>，给外部那段加 <code>criterion</code>（本篇后面的基准要用；第 11 篇你已经加过 <code>pebble-gc</code> 和 <code>rand</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>workspace</span><span>.</span><span>dependencies</span><span>]</span></div></div><div><div><div>2</div></div><div><span># --- internal crates ---------------------------------------------------------</span></div></div><div><div><div>3</div></div><div><span>pebble-span</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-span"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>4</div></div><div><span>pebble-diag</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-diag"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>5</div></div><div><span>pebble-lexer</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-lexer"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>6</div></div><div><span>pebble-ast</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-ast"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>7</div></div><div><span>pebble-macros</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-macros"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>8</div></div><div><span>pebble-parser</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-parser"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>9</div></div><div><span>pebble-value</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-value"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>10</div></div><div><span>pebble-interp</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-interp"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>11</div></div><div><span>pebble-bytecode</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-bytecode"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>12</div></div><div><span>pebble-gc</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-gc"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>13</div></div><div><span>pebble-vm</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-vm"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span># --- external ----------------------------------------------------------------</span></div></div><div><div><div>16</div></div><div><span>anyhow</span><span> = </span><span>"1"</span></div></div><div><div><div>17</div></div><div><span>clap</span><span> = { </span><span>version</span><span> = </span><span>"4"</span><span>, </span><span>features</span><span> = [</span><span>"derive"</span><span>, </span><span>"wrap_help"</span><span>] }</span></div></div><div><div><div>18</div></div><div><span>criterion</span><span> = { </span><span>version</span><span> = </span><span>"0.8"</span><span>, </span><span>features</span><span> = [</span><span>"html_reports"</span><span>] }</span></div></div><div><div><div>19</div></div><div><span>proc-macro2</span><span> = </span><span>"1"</span></div></div><div><div><div>20</div></div><div><span>quote</span><span> = </span><span>"1"</span></div></div><div><div><div>21</div></div><div><span>rand</span><span> = </span><span>"0.10"</span></div></div><div><div><div>22</div></div><div><span>rustyline</span><span> = </span><span>"18"</span></div></div><div><div><div>23</div></div><div><span>syn</span><span> = { </span><span>version</span><span> = </span><span>"3"</span><span>, </span><span>features</span><span> = [</span><span>"full"</span><span>] }</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>最后在文件末尾追加两段 profile（第 15 篇细讲它们，这里先说一句理由：<code>cargo bench</code> 默认用的 <code>bench</code> profile 继承 <code>release</code>，而 <code>release</code> 里的 <code>lto = "thin"</code> 和 <code>codegen-units = 1</code> 会让编译器做更激进的优化，我们的数字是在这个设置下量的，你要复现就得有同样的设置）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>profile</span><span>.</span><span>release</span><span>]</span></div></div><div><div><div>2</div></div><div><span>lto</span><span> = </span><span>"thin"</span></div></div><div><div><div>3</div></div><div><span>codegen-units</span><span> = </span><span>1</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>[</span><span>profile</span><span>.</span><span>bench</span><span>]</span></div></div><div><div><div>6</div></div><div><span>inherits</span><span> = </span><span>"release"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>值和对象：Copy 的枚举，加一个句柄<a href="#值和对象copy-的枚举加一个句柄"><span>#</span></a></h2><p>写 <code>crates/pebble-vm/src/lib.rs</code>。从文件开头开始：属性、crate 文档、<code>use</code>，然后是栈上的值：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div><span>//! A stack-based virtual machine for Pebble bytecode.</span></div></div><div><div><div>3</div></div><div><span>//!</span></div></div><div><div><div>4</div></div><div><span>//! The VM is the second backend. Where the tree-walker in `pebble-interp` walks</span></div></div><div><div><div>5</div></div><div><span>//! the AST with `Rc&lt;RefCell&lt;..&gt;&gt;` environments (and therefore leaks recursive</span></div></div><div><div><div>6</div></div><div><span>//! closures through reference cycles), the VM allocates its runtime objects on</span></div></div><div><div><div>7</div></div><div><span>//! the [`pebble_gc::Heap`], so cycles are collectable.</span></div></div><div><div><div>8</div></div><div><span>//!</span></div></div><div><div><div>9</div></div><div><span>//! The value stack holds [`VmValue`]s. Scalars are inline; everything with a</span></div></div><div><div><div>10</div></div><div><span>//! dynamic shape (strings, lists, maps, environments, closures) is a</span></div></div><div><div><div>11</div></div><div><span>//! [`GcRef`] index into the heap. Before each instruction the VM may collect;</span></div></div><div><div><div>12</div></div><div><span>//! the roots are the value stack, every live frame's environment, and the</span></div></div><div><div><div>13</div></div><div><span>//! globals.</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>use</span><span><span> </span><span>pebble_bytecode</span><span>::{</span></span><span>Chunk</span><span>, </span><span>Compiled</span><span>, </span><span>Constant</span><span>, </span><span>FunctionProto</span><span>, </span><span>Opcode</span><span>};</span></div></div><div><div><div>18</div></div><div><span>use</span><span><span> </span><span>pebble_gc</span><span>::{</span></span><span>GcObject</span><span>, </span><span>GcRef</span><span>, </span><span>Heap</span><span>};</span></div></div><div><div><div>19</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div><div><div><div>20</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::{</span></span><span>Output</span><span>, </span><span>RuntimeError</span><span>, </span><span>Stdout</span><span>};</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>/// A VM value.</span></div></div><div><div><div>23</div></div><div><span>///</span></div></div><div><div><div>24</div></div><div><span>/// `Copy`, so pushing one around never clones a heap object; the heap object</span></div></div><div><div><div>25</div></div><div><span>/// lives behind the [`GcRef`].</span></div></div><div><div><div>26</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>27</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>VmValue</span><span> {</span></div></div><div><div><div>28</div></div><div><span>    </span><span>Nil</span><span>,</span></div></div><div><div><div>29</div></div><div><span>    </span><span>Bool</span><span>(</span><span>bool</span><span>),</span></div></div><div><div><div>30</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>31</div></div><div><span>    </span><span>Float</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>32</div></div><div><span>    </span><span>/// A reference to a heap object.</span></div></div><div><div><div>33</div></div><div><span>    </span><span>Ref</span><span>(</span><span>GcRef</span><span>),</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>impl</span><span> </span><span>VmValue</span><span> {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>fn</span><span> </span><span>is_truthy</span><span>(</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>!</span><span>matches!</span><span>(</span><span>self</span><span>, </span><span>VmValue</span><span>::</span><span>Nil</span><span><span> </span><span>|</span><span> </span></span><span>VmValue</span><span>::</span><span>Bool</span><span>(</span><span>false</span><span>))</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span>fn</span><span> </span><span>as_ref</span><span>(</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>GcRef</span><span>&gt; {</span></div></div><div><div><div>42</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>43</div></div><div><span>            </span><span>VmValue</span><span>::</span><span>Ref</span><span>(</span><span>reference</span><span>) =&gt; </span><span>Some</span><span>(</span><span>reference</span><span>),</span></div></div><div><div><div>44</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>None</span><span>,</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这段有几处值得停一下。</p><p><strong><code>#![forbid(unsafe_code)]</code></strong>：这个 crate 里一行 <code>unsafe</code> 都不允许。第 11 篇的 GC 在内部用了 <code>unsafe</code>（bump arena），但 <code>Heap</code> 的接口是安全的，所以嵌入它的 VM 可以保持全安全。这就是 <code>unsafe</code> 的正确用法：把它关在小盒子里，盒子的接口是安全的。</p><p><strong><code>VmValue</code> 是 <code>Copy</code></strong>。五个变体：<code>Nil</code>、<code>Bool</code>、<code>Int</code>、<code>Float</code>，外加 <code>Ref(GcRef)</code>：一个指向堆对象的句柄。因为 <code>GcRef</code> 本身是 <code>Copy</code>（一个 <code>u32</code> 下标，第 11 篇），整个枚举也能是 <code>Copy</code>。这件事的好处是<strong>每一次入栈出栈都不涉及所有权</strong>：<code>let a = self.stack.pop()</code> 之后 <code>a</code> 随便用几次，不会有”值被移走”的问题。C++ 里最接近的东西是一个 <code>std::variant&lt;std::monostate, bool, int64_t, double, GcRef&gt;</code>，所有成员都是平凡类型，所以整个 variant 可以 <code>memcpy</code>。我在一个临时测试里量了大小：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size_of::&lt;VmValue&gt;()  = 16</span></div></div><div><div><div>2</div></div><div><span>size_of::&lt;VmObject&gt;() = 32</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;Frame&gt;()    = 24</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>16 字节：8 字节放 <code>i64</code>/<code>f64</code>，8 字节放判别标签和对齐填充。（<code>GcRef</code> 只有 4 字节，所以没有任何变体需要超过 8 字节的载荷。）</p><p><strong><code>PartialEq</code> 但没有 <code>Eq</code></strong>：因为有 <code>f64</code>，<code>NaN != NaN</code>，不能承诺自反性。这是第 4 篇的老话题。这里 <code>PartialEq</code> 的用处很具体：第四版命令行里 <code>value != VmValue::Nil</code> 要用它。</p><p><strong><code>is_truthy</code> 和 <code>as_ref</code></strong>：两个没有 <code>pub</code> 的小方法，注意 <code>fn is_truthy(self)</code> 拿的是 <code>self</code> 而不是 <code>&amp;self</code>：因为 <code>VmValue</code> 是 <code>Copy</code>，按值传递和按引用传递一样便宜，还省得写解引用。<code>matches!(self, VmValue::Nil | VmValue::Bool(false))</code> 是第 7 篇的老朋友。Pebble 的真值规则是：只有 <code>nil</code> 和 <code>false</code> 是假的，<code>0</code> 和 <code>""</code> 都是真的。</p><p>接着是堆里的对象：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A heap-allocated VM object.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>VmObject</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// An immutable string.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Str</span><span>(</span><span>String</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// A mutable list.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>List</span><span>(</span><span>Vec</span><span>&lt;</span><span>VmValue</span><span>&gt;),</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// A map, stored as insertion-ordered key/value pairs and searched linearly.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Map</span><span>(</span><span>Vec</span><span>&lt;(</span><span>VmValue</span><span>, </span><span>VmValue</span><span>)&gt;),</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// A function value: a prototype plus the environment it closed over.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Closure</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>proto</span><span>: </span><span>Rc</span><span>&lt;</span><span>FunctionProto</span><span>&gt;,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>env</span><span>: </span><span>GcRef</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>14</div></div><div><span>    </span><span>/// A frame of bindings with a link to the enclosing frame.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Env</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>parent</span><span>: </span><span>Option</span><span>&lt;</span><span>GcRef</span><span>&gt;,</span></div></div><div><div><div>17</div></div><div><span>        </span><span>slots</span><span>: </span><span>Vec</span><span>&lt;(</span><span>String</span><span>, </span><span>VmValue</span><span>)&gt;,</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>19</div></div><div><span>    </span><span>/// A builtin function, identified by name.</span></div></div><div><div><div>20</div></div><div><span>    </span><span>Native</span><span>(&amp;'</span><span>static</span><span> </span><span>str</span><span>),</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>六种对象，每种都对应第 9 篇树遍历解释器里的某个 <code>Rc&lt;RefCell&lt;..&gt;&gt;</code>：</p><ul>
<li><code>Str</code>、<code>List</code>、<code>Map</code>：字符串、列表、映射。映射是按插入顺序存的键值对数组，<strong>线性查找</strong>。（<code>HashMap</code> 需要给 <code>VmValue</code> 实现 <code>Hash</code>，而 <code>Hash</code> 对列表键要看堆里的内容，句柄自己做不到：第 8 篇的 <code>MapKey</code> 那一套在这里行不通。这是我偷的一个懒，代价是 O(n) 查找，后面量性能时它不是主要问题。）</li>
<li><code>Closure { proto, env }</code>：一个函数原型加上它定义时所在的环境。<code>proto</code> 是 <code>Rc&lt;FunctionProto&gt;</code>，和第 10 篇字节码里的常量是同一份：原型是<strong>不可变</strong>的，不会引用 <code>GcRef</code>，所以不会成环，用 <code>Rc</code> 共享它是安全的，也不需要被回收器追踪。<code>env</code> 是 <code>GcRef</code>，指向堆上的一个 <code>Env</code> 对象。</li>
<li><code>Env { parent, slots }</code>：一帧变量绑定，加一条指向外层的链。<code>slots</code> 是 <code>Vec&lt;(String, VmValue)&gt;</code>：名字加值的数组，同样线性查找。第 8 篇的 <code>Env</code> 是 <code>HashMap</code>，这里换成了数组，因为大多数环境只有几个名字，数组扫一遍比哈希快，这是个真实的权衡，不过后面你会看到全局环境不属于”几个名字”的情况。</li>
<li><code>Native(&amp;'static str)</code>：内建函数，只记名字。<code>&amp;'static str</code> 来自第 8 篇的 <code>builtin_names()</code>，那些名字活在整个程序的生命周期里，所以引用可以直接存进对象。</li>
</ul><p>然后是 <code>GcObject</code> 实现，也就是每个对象告诉回收器”我引用了谁”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>GcObject</span><span> </span><span>for</span><span> </span><span>VmObject</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>)) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>VmObject</span><span>::</span><span>Str</span><span>(</span><span>_</span><span><span>) </span><span>|</span><span> </span></span><span>VmObject</span><span>::</span><span>Native</span><span>(</span><span>_</span><span>) =&gt; {}</span></div></div><div><div><div>5</div></div><div><span>            </span><span>VmObject</span><span>::</span><span>List</span><span>(</span><span>items</span><span>) =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>items</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>if</span><span> </span><span>let</span><span> </span><span>VmValue</span><span>::</span><span>Ref</span><span>(</span><span>reference</span><span><span>) </span><span>=</span><span> </span></span><span>item</span><span> {</span></div></div><div><div><div>8</div></div><div><span>                        </span><span>mark</span><span>(*</span><span>reference</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>            </span><span>VmObject</span><span>::</span><span>Map</span><span>(</span><span>entries</span><span>) =&gt; {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>for</span><span> (</span><span>key</span><span>, </span><span>value</span><span>) </span><span>in</span><span> </span><span>entries</span><span> {</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>if</span><span> </span><span>let</span><span> </span><span>VmValue</span><span>::</span><span>Ref</span><span>(</span><span>reference</span><span><span>) </span><span>=</span><span> </span></span><span>key</span><span> {</span></div></div><div><div><div>15</div></div><div><span>                        </span><span>mark</span><span>(*</span><span>reference</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>if</span><span> </span><span>let</span><span> </span><span>VmValue</span><span>::</span><span>Ref</span><span>(</span><span>reference</span><span><span>) </span><span>=</span><span> </span></span><span>value</span><span> {</span></div></div><div><div><div>18</div></div><div><span>                        </span><span>mark</span><span>(*</span><span>reference</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>            </span><span>VmObject</span><span>::</span><span>Closure</span><span> { </span><span>env</span><span>, .. } =&gt; </span><span>mark</span><span>(*</span><span>env</span><span>),</span></div></div><div><div><div>23</div></div><div><span>            </span><span>VmObject</span><span>::</span><span>Env</span><span> { </span><span>parent</span><span>, </span><span>slots</span><span> } =&gt; {</span></div></div><div><div><div>24</div></div><div><span>                </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>parent</span><span><span>) </span><span>=</span><span> </span></span><span>parent</span><span> {</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>mark</span><span>(*</span><span>parent</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>                </span><span>for</span><span> (</span><span>_</span><span>, </span><span>value</span><span>) </span><span>in</span><span> </span><span>slots</span><span> {</span></div></div><div><div><div>28</div></div><div><span>                    </span><span>if</span><span> </span><span>let</span><span> </span><span>VmValue</span><span>::</span><span>Ref</span><span>(</span><span>reference</span><span><span>) </span><span>=</span><span> </span></span><span>value</span><span> {</span></div></div><div><div><div>29</div></div><div><span>                        </span><span>mark</span><span>(*</span><span>reference</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>trace</code> 的参数 <code>mark: &amp;mut dyn FnMut(GcRef)</code> 是第 11 篇的 trait 对象闭包：回收器给我们一个回调，我们对每个子引用调用一次。这个 <code>match</code> 是<strong>穷尽</strong>的，所以将来给 <code>VmObject</code> 加第七个变体时，编译器会逼你来这里，想清楚新对象引用不引用别的对象。这是 <code>trace</code> 唯一不可以写错的地方：漏标一个子引用，那个子对象在下一次回收时就会被释放，而一个活着的对象里还留着指向它的句柄。</p><p><code>VmObject::Env</code> 这一支里 <code>if let Some(parent) = parent { mark(*parent); }</code>：<code>parent</code> 的类型是 <code>&amp;Option&lt;GcRef&gt;</code>（因为 <code>match self</code> 里 <code>self</code> 是引用，默认绑定模式把字段变成引用），<code>if let Some(parent) = parent</code> 里的 <code>parent</code> 于是是 <code>&amp;GcRef</code>，所以要 <code>*parent</code>。这是第 5 篇讲 <code>match self</code> 绑定模式时的同一个现象。</p></section>
<section><h2>帧和 Vm 本体<a href="#帧和-vm-本体"><span>#</span></a></h2><p>接着往下写：调用帧、<code>Vm</code> 结构体、它的构造函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// One activation record.</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Frame</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>proto</span><span>: </span><span>Rc</span><span>&lt;</span><span>FunctionProto</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>env</span><span>: </span><span>GcRef</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>ip</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/// The virtual machine.</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Vm</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>heap</span><span>: </span><span>Heap</span><span>&lt;</span><span>VmObject</span><span>&gt;,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>stack</span><span>: </span><span>Vec</span><span>&lt;</span><span>VmValue</span><span>&gt;,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>frames</span><span>: </span><span>Vec</span><span>&lt;</span><span>Frame</span><span>&gt;,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>globals</span><span>: </span><span>GcRef</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>out</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>max_frames</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>gc_threshold</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>impl</span><span> </span><span>Default</span><span> </span><span>for</span><span> </span><span>Vm</span><span> {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>fn</span><span> </span><span>default</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>Self</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Frame</code> 记录三件事：我在执行哪个函数（<code>proto</code>），我的变量环境是堆上哪个对象（<code>env</code>），我执行到第几条指令（<code>ip</code>，instruction pointer）。<code>Vm</code> 把全部状态放在一个结构体里：堆、值栈、帧栈、全局环境、输出、两个阈值。<strong>调用栈在这里是一个 <code>Vec&lt;Frame&gt;</code></strong>，不是 Rust 的函数调用栈。这一点我们后面再谈，它的后果比看上去大。</p><p><code>out: Box&lt;dyn Output&gt;</code> 是第 8 篇的 trait 对象：<code>print</code> 往哪里写由嵌入方决定（命令行写标准输出，测试写缓冲区）。<code>max_frames</code> 是递归深度上限，<code>gc_threshold</code> 是”自上次回收以来允许分配多少个对象才触发下一次回收”。</p><p><code>impl Default for Vm</code> 是 clippy 要求的：有一个无参 <code>new()</code> 就该有 <code>Default</code>，和第 9 篇的 <code>Interpreter</code> 一样。</p><p>接着是 <code>impl Vm</code> 的开头：构造函数和几个访问器。注意这里<strong>不写右花括号</strong>，后面所有方法都放进同一个 <code>impl</code> 块，最后才闭合：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Vm</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Creates a VM with default limits, printing to standard output.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Self</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>Stdout</span><span>))</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>/// Creates a VM with a custom output sink.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_output</span><span>(</span><span>out</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span><span> </span><span>=</span><span> </span></span><span>Heap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>globals</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>Env</span><span> {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>parent</span><span>: </span><span>None</span><span>,</span></div></div><div><div><div>14</div></div><div><span>            </span><span>slots</span><span><span>: </span><span>Vec</span><span>::</span></span><span>new</span><span>(),</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>16</div></div><div><span>        </span><span>for</span><span> </span><span>name</span><span> </span><span>in</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>builtin_names</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>let</span><span> </span><span>native</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>Native</span><span>(</span><span>name</span><span>));</span></div></div><div><div><div>18</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>VmObject</span><span>::</span><span>Env</span><span> { </span><span>slots</span><span><span>, .. } </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>get_mut</span><span>(</span><span>globals</span><span>) {</span></div></div><div><div><div>19</div></div><div><span>                </span><span>slots</span><span>.</span><span>push</span><span>((</span><span>name</span><span>.</span><span>to_string</span><span><span>(), </span><span>VmValue</span><span>::</span></span><span>Ref</span><span>(</span><span>native</span><span>)));</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>heap</span><span>,</span></div></div><div><div><div>24</div></div><div><span>            </span><span>stack</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>25</div></div><div><span>            </span><span>frames</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>26</div></div><div><span>            </span><span>globals</span><span>,</span></div></div><div><div><div>27</div></div><div><span>            </span><span>out</span><span>,</span></div></div><div><div><div>28</div></div><div><span>            </span><span>max_frames</span><span>: </span><span>256</span><span>,</span></div></div><div><div><div>29</div></div><div><span>            </span><span>gc_threshold</span><span>: </span><span>1024</span><span>,</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>/// Sets the recursion limit.</span></div></div><div><div><div>34</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>set_max_frames</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>frames</span><span>: </span><span>usize</span><span>) {</span></div></div><div><div><div>35</div></div><div><span>        </span><span>self</span><span><span>.max_frames </span><span>=</span><span> </span></span><span>frames</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span>/// Sets how many allocations may accumulate before a collection.</span></div></div><div><div><div>39</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>set_gc_threshold</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>threshold</span><span>: </span><span>usize</span><span>) {</span></div></div><div><div><div>40</div></div><div><span>        </span><span>self</span><span><span>.gc_threshold </span><span>=</span><span> </span></span><span>threshold</span><span>;</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>    </span><span>/// Number of live heap objects.</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>45</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>heap_live</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>46</div></div><div><span>        </span><span>self</span><span>.heap.</span><span>live_count</span><span>()</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span>/// Estimated heap bytes.</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>51</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>heap_bytes</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>52</div></div><div><span>        </span><span>self</span><span>.heap.</span><span>bytes_allocated</span><span>()</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>with_output</code> 里有一件小事：全局环境先建好，然后对每个内建函数名各分配一个 <code>Native</code> 对象并塞进全局环境的槽位。<code>for name in pebble_value::builtin_names()</code> 用的就是第 8 篇那个 <code>-&gt; impl Iterator&lt;Item = &amp;'static str&gt;</code> 函数，我们的 VM 因此不再自己维护一份内建名字列表：新增一个内建函数，只要改 <code>pebble-value</code> 里的表一处。（仓库的历史里，VM 原来有一份独立的 <code>BUILTINS</code> 常量，两份列表迟早会不同步，所以我把它换成了这个函数。）</p><p>循环体里为什么是 <code>heap.alloc(...)</code> 之后再 <code>heap.get_mut(globals)</code>，而不是一步到位？两者都要 <code>&amp;mut self.heap</code>，借用检查器不允许同时存在，所以必须分成两个语句。你会在这个文件里反复看到这个形状：<strong>先从堆里读出你要的东西，让借用结束，再去分配或修改。</strong> 后面它会咬我们一口。</p><p>这一段可以编译了。给 <code>impl Vm</code> 补上右花括号 <code>}</code>，然后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>-p</span><span> </span><span>pebble-vm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>warning: unused imports: `Chunk`, `Compiled`, `Constant`, and `Opcode`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-vm/src/lib.rs:17:23</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>17 | use pebble_bytecode::{Chunk, Compiled, Constant, FunctionProto, Opcode};</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                       ^^^^^  ^^^^^^^^  ^^^^^^^^                 ^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>...</span></div></div><div><div><div>10</div></div><div><span>warning: `pebble-vm` (lib) generated 6 warnings (run `cargo fix --lib -p pebble-vm` to apply 3 suggestions)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.20s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只有警告，没有错误。每一条都是同一个意思：“这个东西你定义了但还没用”，编译器在帮我们列出接下来要写什么。</p></section>
<section><h2>主循环：run<a href="#主循环run"><span>#</span></a></h2><p><code>run</code> 是整个 VM 的心脏。我分四段写，每段只加一类指令。先是循环的骨架：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Runs a compiled program and returns its value.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// # Errors</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Returns a [`RuntimeError`] for any type error, undefined name, arity</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// mismatch, or recursion-limit breach.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>run</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>compiled</span><span>: &amp;</span><span>Compiled</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>VmValue</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span>.stack.</span><span>clear</span><span>();</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span>.frames.</span><span>clear</span><span>();</span></div></div><div><div><div>9</div></div><div><span>        </span><span>self</span><span>.frames.</span><span>push</span><span>(</span><span>Frame</span><span> {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>proto</span><span><span>: </span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>compiled</span><span>.main),</span></div></div><div><div><div>11</div></div><div><span>            </span><span>env</span><span>: </span><span>self</span><span>.globals,</span></div></div><div><div><div>12</div></div><div><span>            </span><span>ip</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>        </span><span>loop</span><span> {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.frames.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>                </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>self</span><span>.stack.</span><span>pop</span><span>().</span><span>unwrap_or</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>));</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.heap.</span><span>allocations_since_gc</span><span>() &gt; </span><span>self</span><span>.gc_threshold {</span></div></div><div><div><div>20</div></div><div><span>                </span><span>self</span><span>.</span><span>collect_garbage</span><span>();</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>            </span><span>let</span><span> </span><span>frame_index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.frames.</span><span>len</span><span>() - </span><span>1</span><span>;</span></div></div><div><div><div>24</div></div><div><span>            </span><span>let</span><span> </span><span>proto</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>self</span><span>.frames[</span><span>frame_index</span><span>].proto);</span></div></div><div><div><div>25</div></div><div><span>            </span><span>let</span><span> </span><span>ip</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.frames[</span><span>frame_index</span><span>].ip;</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>            </span><span>if</span><span> </span><span>ip</span><span><span> </span><span>&gt;=</span><span> </span></span><span>proto</span><span>.chunk.</span><span>len</span><span>() {</span></div></div><div><div><div>28</div></div><div><span>                </span><span>// Falling off the end of a chunk returns its top-of-stack.</span></div></div><div><div><div>29</div></div><div><span>                </span><span>self</span><span>.frames.</span><span>pop</span><span>();</span></div></div><div><div><div>30</div></div><div><span>                </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.stack.</span><span>pop</span><span>().</span><span>unwrap_or</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>31</div></div><div><span>                </span><span>if</span><span> </span><span>self</span><span>.frames.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>32</div></div><div><span>                    </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>result</span><span>);</span></div></div><div><div><div>33</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>                </span><span>self</span><span>.stack.</span><span>push</span><span>(</span><span>result</span><span>);</span></div></div><div><div><div>35</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>            </span><span>let</span><span> </span><span>instruction</span><span><span> </span><span>=</span><span> </span></span><span>proto</span><span>.chunk.</span><span>code</span><span>()[</span><span>ip</span><span>];</span></div></div><div><div><div>39</div></div><div><span>            </span><span>self</span><span>.frames[</span><span>frame_index</span><span><span>].ip </span><span>=</span><span> </span></span><span>ip</span><span> + </span><span>1</span><span>;</span></div></div><div><div><div>40</div></div><div><span>            </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>proto</span><span>.chunk.</span><span>span_at</span><span>(</span><span>ip</span><span>);</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>            </span><span>match</span><span> </span><span>instruction</span><span> {</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐块看。</p><p><strong>初始化</strong>：清空栈和帧，压入主程序的帧，环境是全局环境。<code>Rc::clone(&amp;compiled.main)</code> 是第 8 篇的 <code>Rc</code>：增加一个引用计数，不复制函数原型。</p><p><strong>每圈的第一件事：看要不要回收</strong>。<code>allocations_since_gc() &gt; gc_threshold</code> 就调用 <code>collect_garbage</code>。注意回收只发生在<strong>这个位置</strong>：两条指令之间。我们稍后专门谈为什么。</p><p><strong>取指</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>proto</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>self</span><span>.frames[</span><span>frame_index</span><span>].proto);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为什么要克隆一个 <code>Rc</code> 出来，而不是直接借 <code>self.frames[frame_index].proto</code>？因为后面的 <code>match</code> 里要调用 <code>self.pop(span)</code>、<code>self.call(count, span)</code> 这类 <code>&amp;mut self</code> 方法，如果这里还握着一个指向 <code>self.frames</code> 的借用，借用检查器会拒绝。克隆 <code>Rc</code> 是一次非原子的计数加一（第 8 篇：不是 <code>Arc</code>，没有原子指令），把”proto 的所有权”从 <code>self</code> 里摘出来变成一个局部变量。这是一次小小的性能代价，换取借用检查器满意。我怀疑它在热循环里不是免费的，后面量性能时会回来。</p><p><code>proto.chunk.code()[ip]</code> 取出的 <code>Opcode</code> 是 <code>Copy</code> 的（第 10 篇：8 字节），所以 <code>let instruction = ...</code> 是一份值拷贝，之后随便用。紧接着 <code>ip + 1</code> 先写回帧里：<strong>先递增再执行</strong>，这样跳转指令只要覆盖 <code>ip</code> 就行，其他指令什么都不用管。</p><p><strong>执行到末尾</strong>：如果 <code>ip &gt;= proto.chunk.len()</code>，函数”掉出了末尾”，等于隐式返回栈顶（或者 <code>nil</code>）。第 10 篇我们在跳转目标里提过，“跳到末尾之后”是合法的，这里就是兑现。</p><p>现在是 <code>match instruction</code>。第一批：常量和名字。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>index</span><span>) =&gt; </span><span>self</span><span>.</span><span>push_constant</span><span>(&amp;</span><span>proto</span><span>.chunk, </span><span>index</span><span>, </span><span>span</span><span>)?,</span></div></div><div><div><div>2</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Nil</span><span> =&gt; </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>),</span></div></div><div><div><div>3</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>True</span><span> =&gt; </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Bool</span><span>(</span><span>true</span><span>)),</span></div></div><div><div><div>4</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>False</span><span> =&gt; </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Bool</span><span>(</span><span>false</span><span>)),</span></div></div><div><div><div>5</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Pop</span><span> =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>pop</span><span>();</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>DefineName</span><span>(</span><span>index</span><span>) =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>constant_name</span><span>(&amp;</span><span>proto</span><span>.chunk, </span><span>index</span><span>);</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.stack.</span><span>pop</span><span>().</span><span>unwrap_or</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>self</span><span>.</span><span>define</span><span>(</span><span>name</span><span>, </span><span>value</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>GetName</span><span>(</span><span>index</span><span>) =&gt; {</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>constant_name</span><span>(&amp;</span><span>proto</span><span>.chunk, </span><span>index</span><span>);</span></div></div><div><div><div>15</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>lookup</span><span>(&amp;</span><span>name</span><span>).</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>16</div></div><div><span><span>                        </span></span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span><span>span</span><span>, </span><span>format!</span><span>(</span><span>"undefined variable `{name}`"</span><span>))</span></div></div><div><div><div>17</div></div><div><span><span>                    </span></span><span>})?;</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>SetName</span><span>(</span><span>index</span><span>) =&gt; {</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>constant_name</span><span>(&amp;</span><span>proto</span><span>.chunk, </span><span>index</span><span>);</span></div></div><div><div><div>22</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> *</span></span><span>self</span><span>.stack.</span><span>last</span><span>().</span><span>unwrap_or</span><span><span>(&amp;</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>23</div></div><div><span>                    </span><span>if</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>assign</span><span>(&amp;</span><span>name</span><span>, </span><span>value</span><span>) {</span></div></div><div><div><div>24</div></div><div><span>                        </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>25</div></div><div><span>                            </span><span>span</span><span>,</span></div></div><div><div><div>26</div></div><div><span>                            </span><span>format!</span><span>(</span><span>"cannot assign to undefined variable `{name}`"</span><span>),</span></div></div><div><div><div>27</div></div><div><span><span>                        </span></span><span>));</span></div></div><div><div><div>28</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>MakeClosure</span><span>(</span><span>index</span><span>) =&gt; {</span></div></div><div><div><div>31</div></div><div><span>                    </span><span>let</span><span> </span><span>Constant</span><span>::</span><span>Function</span><span>(</span><span>proto_of_closure</span><span><span>) </span><span>=</span></span></div></div><div><div><div>32</div></div><div><span><span>                        </span></span><span>&amp;</span><span>proto</span><span>.chunk.</span><span>constants</span><span>()[</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>]</span></div></div><div><div><div>33</div></div><div><span>                    </span><span>else</span><span> {</span></div></div><div><div><div>34</div></div><div><span>                        </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span><span>span</span><span>, </span><span>"malformed closure constant"</span><span>));</span></div></div><div><div><div>35</div></div><div><span><span>                    </span></span><span>};</span></div></div><div><div><div>36</div></div><div><span>                    </span><span>let</span><span> </span><span>env</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.frames[</span><span>frame_index</span><span>].env;</span></div></div><div><div><div>37</div></div><div><span>                    </span><span>let</span><span> </span><span>reference</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>Closure</span><span> {</span></div></div><div><div><div>38</div></div><div><span>                        </span><span>proto</span><span><span>: </span><span>Rc</span><span>::</span></span><span>clone</span><span>(</span><span>proto_of_closure</span><span>),</span></div></div><div><div><div>39</div></div><div><span>                        </span><span>env</span><span>,</span></div></div><div><div><div>40</div></div><div><span><span>                    </span></span><span>});</span></div></div><div><div><div>41</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Ref</span><span>(</span><span>reference</span><span>));</span></div></div><div><div><div>42</div></div><div><span><span>                </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Opcode::Const(index) =&gt; self.push_constant(&amp;proto.chunk, index, span)?,</code> 这一行有一个 Rust 细节：<code>?</code> 之后整个表达式的类型是 <code>()</code>，所以这个 arm 的值是 <code>()</code>，和别的 arm 一致。而 <code>Opcode::Pop =&gt; { self.stack.pop(); }</code> 要写成块，因为 <code>Vec::pop</code> 返回 <code>Option&lt;VmValue&gt;</code>，加分号把它丢掉才得到 <code>()</code>。</p><p><code>GetName</code> 里 <code>ok_or_else(|| RuntimeError::new(...))</code>：<code>lookup</code> 返回 <code>Option</code>，<code>ok_or_else</code> 把 <code>None</code> 变成 <code>Err</code>，然后 <code>?</code> 把错误冒泡出 <code>run</code>。<code>ok_or_else</code> 的参数是个闭包，只有在 <code>None</code> 时才会调用：如果你写 <code>ok_or(RuntimeError::new(...))</code>，错误值（包括 <code>format!</code> 出来的字符串）每次查找都会先构造一遍，白白浪费。这是 <code>_else</code> 系列函数存在的全部理由。</p><p><code>MakeClosure(index)</code> 里是个 <code>let ... else</code>（第 3 篇）：常量池里第 <code>index</code> 个必须是 <code>Constant::Function</code>，否则返回一个 <code>malformed closure constant</code> 错误。成功时，我们从<strong>当前帧</strong>拿 <code>env</code>，把”原型加环境”打包成一个 <code>Closure</code> 对象放上堆：这就是第 9 篇那个 <code>Function::new(.., Rc::clone(env))</code> 的字节码版本，只是 <code>Rc&lt;RefCell&lt;Env&gt;&gt;</code> 变成了 <code>GcRef</code>。</p><p>第二批：算术、比较和逻辑。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Add</span><span> =&gt; </span><span>self</span><span>.</span><span>binary_add</span><span>(</span><span>span</span><span>)?,</span></div></div><div><div><div>2</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Sub</span><span> =&gt; </span><span>self</span><span>.</span><span>binary_numeric</span><span>(</span><span>span</span><span>, </span><span>"-"</span><span><span>, </span><span>i64</span><span>::</span></span><span>checked_sub</span><span><span>, </span><span>|</span></span><span>a</span><span>, </span><span>b</span><span><span>|</span><span> </span></span><span>a</span><span> - </span><span>b</span><span>)?,</span></div></div><div><div><div>3</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Mul</span><span> =&gt; </span><span>self</span><span>.</span><span>binary_numeric</span><span>(</span><span>span</span><span>, </span><span>"*"</span><span><span>, </span><span>i64</span><span>::</span></span><span>checked_mul</span><span><span>, </span><span>|</span></span><span>a</span><span>, </span><span>b</span><span><span>|</span><span> </span></span><span>a</span><span> * </span><span>b</span><span>)?,</span></div></div><div><div><div>4</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Div</span><span> =&gt; </span><span>self</span><span>.</span><span>binary_div</span><span>(</span><span>span</span><span>)?,</span></div></div><div><div><div>5</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Rem</span><span> =&gt; </span><span>self</span><span>.</span><span>binary_rem</span><span>(</span><span>span</span><span>)?,</span></div></div><div><div><div>6</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Neg</span><span> =&gt; {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>value</span><span> {</span></div></div><div><div><div>9</div></div><div><span>                        </span><span>VmValue</span><span>::</span><span>Int</span><span>(</span><span>inner</span><span>) =&gt; </span><span>inner</span></div></div><div><div><div>10</div></div><div><span><span>                            </span></span><span>.</span><span>checked_neg</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>                            </span></span><span>.</span><span>map</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Int</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>                            </span></span><span>.</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> </span></span><span>self</span><span>.</span><span>overflow</span><span>(</span><span>span</span><span>))?,</span></div></div><div><div><div>13</div></div><div><span>                        </span><span>VmValue</span><span>::</span><span>Float</span><span>(</span><span>inner</span><span>) =&gt; </span><span>VmValue</span><span>::</span><span>Float</span><span>(-</span><span>inner</span><span>),</span></div></div><div><div><div>14</div></div><div><span>                        </span><span>other</span><span> =&gt; {</span></div></div><div><div><div>15</div></div><div><span>                            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>16</div></div><div><span>                                </span><span>span</span><span>,</span></div></div><div><div><div>17</div></div><div><span>                                </span><span>format!</span><span>(</span><span>"cannot negate {}"</span><span>, </span><span>self</span><span>.</span><span>type_name</span><span>(</span><span>other</span><span>)),</span></div></div><div><div><div>18</div></div><div><span><span>                            </span></span><span>));</span></div></div><div><div><div>19</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>                    </span></span><span>};</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span>(</span><span>result</span><span>);</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Not</span><span> =&gt; {</span></div></div><div><div><div>24</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Bool</span><span><span>(</span><span>!</span></span><span>value</span><span>.</span><span>is_truthy</span><span>()));</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Equal</span><span> =&gt; {</span></div></div><div><div><div>28</div></div><div><span>                    </span><span>let</span><span> </span><span>rhs</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>29</div></div><div><span>                    </span><span>let</span><span> </span><span>lhs</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>30</div></div><div><span>                    </span><span>let</span><span> </span><span>equal</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>value_equal</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>);</span></div></div><div><div><div>31</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Bool</span><span>(</span><span>equal</span><span>));</span></div></div><div><div><div>32</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>NotEqual</span><span> =&gt; {</span></div></div><div><div><div>34</div></div><div><span>                    </span><span>let</span><span> </span><span>rhs</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>35</div></div><div><span>                    </span><span>let</span><span> </span><span>lhs</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>36</div></div><div><span>                    </span><span>let</span><span> </span><span>equal</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>value_equal</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>);</span></div></div><div><div><div>37</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Bool</span><span><span>(</span><span>!</span></span><span>equal</span><span>));</span></div></div><div><div><div>38</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Less</span><span><span> </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>LessEqual</span><span><span> </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>Greater</span><span><span> </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>GreaterEqual</span><span> =&gt; {</span></div></div><div><div><div>40</div></div><div><span>                    </span><span>let</span><span> </span><span>ordering</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>binary_compare</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>41</div></div><div><span>                    </span><span>use</span><span><span> </span><span>std</span><span>::</span><span>cmp</span><span>::</span></span><span>Ordering</span><span>;</span></div></div><div><div><div>42</div></div><div><span>                    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>instruction</span><span> {</span></div></div><div><div><div>43</div></div><div><span>                        </span><span>Opcode</span><span>::</span><span>Less</span><span> =&gt; </span><span>ordering</span><span><span> </span><span>==</span><span> </span></span><span>Ordering</span><span>::</span><span>Less</span><span>,</span></div></div><div><div><div>44</div></div><div><span>                        </span><span>Opcode</span><span>::</span><span>LessEqual</span><span> =&gt; </span><span>ordering</span><span><span> </span><span>!=</span><span> </span></span><span>Ordering</span><span>::</span><span>Greater</span><span>,</span></div></div><div><div><div>45</div></div><div><span>                        </span><span>Opcode</span><span>::</span><span>Greater</span><span> =&gt; </span><span>ordering</span><span><span> </span><span>==</span><span> </span></span><span>Ordering</span><span>::</span><span>Greater</span><span>,</span></div></div><div><div><div>46</div></div><div><span>                        </span><span>Opcode</span><span>::</span><span>GreaterEqual</span><span> =&gt; </span><span>ordering</span><span><span> </span><span>!=</span><span> </span></span><span>Ordering</span><span>::</span><span>Less</span><span>,</span></div></div><div><div><div>47</div></div><div><span>                        </span><span>_</span><span> =&gt; </span><span>unreachable!</span><span>(),</span></div></div><div><div><div>48</div></div><div><span><span>                    </span></span><span>};</span></div></div><div><div><div>49</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Bool</span><span>(</span><span>result</span><span>));</span></div></div><div><div><div>50</div></div><div><span><span>                </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四则运算全部交给辅助方法（稍后复制），<code>binary_numeric</code> 的两个函数指针参数 <code>i64::checked_sub</code> 和 <code>|a, b| a - b</code> 是第 8 篇的内容：不捕获任何东西的闭包可以强转成 <code>fn</code> 指针。<code>Neg</code> 里 <code>checked_neg</code> 对 <code>i64::MIN</code> 返回 <code>None</code>（因为 <code>-i64::MIN</code> 装不下），所以 <code>-x</code> 也会报”integer overflow”而不是悄悄回绕。</p><p>比较那一支值得多看一眼：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Opcode</span><span>::</span><span>Less</span><span><span> </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>LessEqual</span><span><span> </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>Greater</span><span><span> </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>GreaterEqual</span><span> =&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>ordering</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>binary_compare</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span><span> </span><span>std</span><span>::</span><span>cmp</span><span>::</span></span><span>Ordering</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>instruction</span><span> {</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>...</span></div></div><div><div><div>6</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>unreachable!</span><span>(),</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个或模式匹配四条指令，里面再按 <code>instruction</code> 分一次。内层的 <code>_ =&gt; unreachable!()</code> 是我对这段代码最不满意的地方：编译器没法证明另外那些指令不会进来，所以我用一个”不可能”的 arm 把它堵住。更干净的做法是四个独立的 arm，各自调用一个小函数，代价是多写几行。我留着它，是因为它是一个诚实的例子：<strong>穷尽匹配并不是免费的午餐</strong>，当你用或模式把几个分支合在一起之后，你就把一部分检查责任从编译器手里拿回了自己手里。</p><p><code>use std::cmp::Ordering;</code> 写在块中间也是合法的：<code>use</code> 声明是一个”项”，可以出现在任何块里，作用域就是那个块。</p><p>第三批：跳转、调用、返回。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Jump</span><span>(</span><span>target</span><span>) =&gt; </span><span>self</span><span>.frames[</span><span>frame_index</span><span><span>].ip </span><span>=</span><span> </span></span><span>target</span><span> </span><span>as</span><span> </span><span>usize</span><span>,</span></div></div><div><div><div>2</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>JumpIfFalse</span><span>(</span><span>target</span><span>) =&gt; {</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>if</span><span><span> </span><span>!</span></span><span>value</span><span>.</span><span>is_truthy</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>                        </span><span>self</span><span>.frames[</span><span>frame_index</span><span><span>].ip </span><span>=</span><span> </span></span><span>target</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>JumpIfFalseKeep</span><span>(</span><span>target</span><span>) =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> *</span></span><span>self</span><span>.stack.</span><span>last</span><span>().</span><span>unwrap_or</span><span><span>(&amp;</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>if</span><span> </span><span>value</span><span>.</span><span>is_truthy</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>                        </span><span>self</span><span>.stack.</span><span>pop</span><span>();</span></div></div><div><div><div>12</div></div><div><span><span>                    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>13</div></div><div><span>                        </span><span>self</span><span>.frames[</span><span>frame_index</span><span><span>].ip </span><span>=</span><span> </span></span><span>target</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>JumpIfTrueKeep</span><span>(</span><span>target</span><span>) =&gt; {</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> *</span></span><span>self</span><span>.stack.</span><span>last</span><span>().</span><span>unwrap_or</span><span><span>(&amp;</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>if</span><span> </span><span>value</span><span>.</span><span>is_truthy</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>                        </span><span>self</span><span>.frames[</span><span>frame_index</span><span><span>].ip </span><span>=</span><span> </span></span><span>target</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>                    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>21</div></div><div><span>                        </span><span>self</span><span>.stack.</span><span>pop</span><span>();</span></div></div><div><div><div>22</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Call</span><span>(</span><span>count</span><span>) =&gt; </span><span>self</span><span>.</span><span>call</span><span>(</span><span>count</span><span>, </span><span>span</span><span>)?,</span></div></div><div><div><div>25</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Return</span><span> =&gt; {</span></div></div><div><div><div>26</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.stack.</span><span>pop</span><span>().</span><span>unwrap_or</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>27</div></div><div><span>                    </span><span>self</span><span>.frames.</span><span>pop</span><span>();</span></div></div><div><div><div>28</div></div><div><span>                    </span><span>if</span><span> </span><span>self</span><span>.frames.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>29</div></div><div><span>                        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>                </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三条跳转，每一条都只有一两行，这就是第 10 篇花了那么多功夫回填的回报：<code>Jump(target)</code> 的全部实现就是 <code>self.frames[frame_index].ip = target as usize</code>。<code>JumpIfFalseKeep</code> 和 <code>JumpIfTrueKeep</code> 是第 10 篇 <code>and</code>/<code>or</code> 的短路形式：真值时（对 <code>and</code>）弹掉栈顶继续，假值时<strong>保留</strong>栈顶并跳转，所以 <code>nil or 3</code> 的结果是 <code>3</code>，<code>1 and 2</code> 的结果是 <code>2</code>：和 Python、Lua 一样，逻辑运算符返回操作数而不是布尔。</p><p><code>Call(count)</code> 把一切交给 <code>self.call</code>。<code>Return</code> 弹出返回值，<strong>弹掉当前帧</strong>，再把返回值压回栈顶，交给调用者的帧继续。如果弹掉之后帧栈空了，说明主程序结束，直接返回。</p><p>第四批：集合和下标，然后闭合 <code>loop</code>、<code>match</code> 和 <code>run</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>BuildList</span><span>(</span><span>count</span><span>) =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                    </span><span>let</span><span> </span><span>items</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.stack.</span><span>split_off</span><span>(</span><span>self</span><span>.stack.</span><span>len</span><span>() - </span><span>count</span><span> </span><span>as</span><span> </span><span>usize</span><span>);</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>let</span><span> </span><span>reference</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>List</span><span>(</span><span>items</span><span>));</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Ref</span><span>(</span><span>reference</span><span>));</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>BuildMap</span><span>(</span><span>count</span><span>) =&gt; {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>let</span><span> </span><span>flat</span><span><span> </span><span>=</span><span> </span></span><span>self</span></div></div><div><div><div>8</div></div><div><span><span>                        </span></span><span>.stack</span></div></div><div><div><div>9</div></div><div><span><span>                        </span></span><span>.</span><span>split_off</span><span>(</span><span>self</span><span>.stack.</span><span>len</span><span>() - (</span><span>count</span><span> </span><span>as</span><span> </span><span>usize</span><span> * </span><span>2</span><span>));</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>let</span><span> </span><span>mut</span><span> </span><span>entries</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>with_capacity</span><span>(</span><span>count</span><span> </span><span>as</span><span> </span><span>usize</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>let</span><span> </span><span>mut</span><span> </span><span>iter</span><span><span> </span><span>=</span><span> </span></span><span>flat</span><span>.</span><span>into_iter</span><span>();</span></div></div><div><div><div>12</div></div><div><span>                    </span><span>while</span><span> </span><span>let</span><span> (</span><span>Some</span><span>(</span><span>key</span><span>), </span><span>Some</span><span>(</span><span>value</span><span><span>)) </span><span>=</span><span> (</span></span><span>iter</span><span>.</span><span>next</span><span>(), </span><span>iter</span><span>.</span><span>next</span><span>()) {</span></div></div><div><div><div>13</div></div><div><span>                        </span><span>entries</span><span>.</span><span>push</span><span>((</span><span>key</span><span>, </span><span>value</span><span>));</span></div></div><div><div><div>14</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>                    </span><span>let</span><span> </span><span>reference</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>Map</span><span>(</span><span>entries</span><span>));</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Ref</span><span>(</span><span>reference</span><span>));</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Index</span><span> =&gt; {</span></div></div><div><div><div>19</div></div><div><span>                    </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>let</span><span> </span><span>container</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>index_value</span><span>(</span><span>container</span><span>, </span><span>index</span><span>, </span><span>span</span><span>)?;</span></div></div><div><div><div>22</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span>(</span><span>result</span><span>);</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>SetIndex</span><span> =&gt; {</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>26</div></div><div><span>                    </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>27</div></div><div><span>                    </span><span>let</span><span> </span><span>container</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>28</div></div><div><span>                    </span><span>self</span><span>.</span><span>set_index_value</span><span>(</span><span>container</span><span>, </span><span>index</span><span>, </span><span>value</span><span>, </span><span>span</span><span>)?;</span></div></div><div><div><div>29</div></div><div><span>                    </span><span>self</span><span>.stack.</span><span>push</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>BuildList(count)</code> 用的是 <code>self.stack.split_off(self.stack.len() - count as usize)</code>：把栈顶 <code>count</code> 个值一次拆成新的 <code>Vec</code>。这一行里有个借用小花招：<code>self.stack.split_off(...)</code> 要 <code>&amp;mut self.stack</code>，参数里又读 <code>self.stack.len()</code>。两者是对同一个字段的借用，这是第 7 篇讲过的<strong>两阶段借用</strong>：<code>&amp;mut</code> 借用先”预约”，等参数求值完才真正激活，所以这样写合法。如果你把长度先放进一个局部变量，也一样对，只是多一行。</p><p><code>BuildMap</code> 里 <code>while let (Some(key), Some(value)) = (iter.next(), iter.next())</code>：每圈从迭代器里取两个，元组模式一次解开。栈上 map 的布局是 <code>key1 value1 key2 value2 ...</code>，所以两个一对。</p><p>好了，<code>run</code> 写完了。给它编译一次：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>-p</span><span> </span><span>pebble-vm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0599]: no method named `collect_garbage` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-vm/src/lib.rs:204:22</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>204 |                 self.collect_garbage();</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                      ^^^^^^^^^^^^^^^ method not found in `&amp;mut Vm`</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>error[E0599]: no method named `push_constant` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-vm/src/lib.rs:227:46</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>227 |                 Opcode::Const(index) =&gt; self.push_constant(&amp;proto.chunk, index, span)?,</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|                                              ^^^^^^^^^^^^^ method not found in `&amp;mut Vm`</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div><div><div><div>13</div></div><div><span>error: could not compile `pebble-vm` (lib) due to 33 previous errors; 2 warnings emitted</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>33 个错误。每一个都是同一件事：<code>run</code> 调用了 <code>self.pop</code>、<code>self.call</code>、<code>self.lookup</code>…，而这些方法一个也不存在。我数了一下，一共是 18 种不同的缺失方法：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>-p</span><span> </span><span>pebble-vm</span><span> 2&gt;&amp;1 | </span><span>grep</span><span> </span><span>'^error'</span><span> | </span><span>sort</span><span> | </span><span>uniq</span><span> </span><span>-c</span><span> | </span><span>sort</span><span> </span><span>-rn</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>12 error[E0599]: no method named `pop` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>3 error[E0599]: no method named `constant_name` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>3</div></div><div><span><span>      </span></span><span>2 error[E0599]: no method named `value_equal` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>2 error[E0599]: no method named `binary_numeric` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `type_name` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `set_index_value` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `push_constant` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `overflow` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `lookup` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `index_value` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `define` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `collect_garbage` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `call` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_rem` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_div` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_compare` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_add` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>18</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `assign` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>1 error: could not compile `pebble-vm` (lib) due to 33 previous errors; 2 warnings emitted</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pop</code> 出现 12 次，它是整个 VM 里被调用最多的方法。这份清单就是我们接下来的待办列表：<strong>让编译器告诉你还差什么</strong>，是 Rust 里写大函数时非常顺手的一种节奏（C++ 里你得等链接器报 undefined reference，而且只会报一个函数一次）。</p></section>
<section><h2>补齐 run 要用的方法<a href="#补齐-run-要用的方法"><span>#</span></a></h2><p>按重要性，先写最小的三个：从栈里弹值、把常量池里的常量压栈、取常量池里的名字。把它们放在 <code>run</code> 的后面（还在同一个 <code>impl Vm</code> 里，一个空行隔开）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- helpers: stack, constants ---------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>pop</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>VmValue</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.stack</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>pop</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> </span><span>RuntimeError</span><span>::</span></span><span>new</span><span>(</span><span>span</span><span>, </span><span>"internal error: value stack underflow"</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>push_constant</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>chunk</span><span>: &amp;</span><span>Chunk</span><span>, </span><span>index</span><span>: </span><span>u32</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;(), </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>match</span><span> &amp;</span><span>chunk</span><span>.</span><span>constants</span><span>()[</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>] {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Nil</span><span> =&gt; </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Nil</span><span>),</span></div></div><div><div><div>12</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Bool</span><span>(</span><span>value</span><span>) =&gt; </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Bool</span><span>(*</span><span>value</span><span>)),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Int</span><span>(</span><span>value</span><span>) =&gt; </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Int</span><span>(*</span><span>value</span><span>)),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Float</span><span>(</span><span>value</span><span>) =&gt; </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Float</span><span>(*</span><span>value</span><span>)),</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Str</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>16</div></div><div><span>                </span><span>let</span><span> </span><span>reference</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>Str</span><span>(</span><span>value</span><span>.</span><span>clone</span><span>()));</span></div></div><div><div><div>17</div></div><div><span>                </span><span>self</span><span>.stack.</span><span>push</span><span><span>(</span><span>VmValue</span><span>::</span></span><span>Ref</span><span>(</span><span>reference</span><span>));</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Function</span><span>(</span><span>_</span><span>) =&gt; {</span></div></div><div><div><div>20</div></div><div><span>                </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span><span>span</span><span>, </span><span>"function constant used as a value"</span><span>));</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>        </span><span>Ok</span><span>(())</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>fn</span><span> </span><span>constant_name</span><span>(&amp;</span><span>self</span><span>, </span><span>chunk</span><span>: &amp;</span><span>Chunk</span><span>, </span><span>index</span><span>: </span><span>u32</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>27</div></div><div><span>        </span><span>match</span><span> &amp;</span><span>chunk</span><span>.</span><span>constants</span><span>()[</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>] {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Str</span><span>(</span><span>name</span><span>) =&gt; </span><span>name</span><span>.</span><span>clone</span><span>(),</span></div></div><div><div><div>29</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>format!</span><span>(</span><span>"&lt;constant {index}&gt;"</span><span>),</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pop</code> 在栈空时返回一个 <code>RuntimeError</code>，而不是 <code>panic!</code>。栈下溢意味着<strong>编译器</strong>生成了坏的字节码，不是用户的错；但这是 VM，用户的程序是一个黑盒，我宁可让它带着一个”internal error”优雅地失败，也不让整个进程崩溃（比如 REPL 里一次错误不该杀死整个会话）。这是第 7 篇讨论过的取舍：<code>panic</code> 留给”程序逻辑自己的矛盾”，<code>Result</code> 留给”输入的问题”，这里两种都沾一点，我选了更保守的那一边。</p><p><code>push_constant</code> 里有一件值得记住的事：<code>Constant::Str(value)</code> 这一支每次执行都 <code>value.clone()</code> 再 <code>heap.alloc</code>：<strong>每次执行一个字符串字面量，都会在堆上新分配一个字符串对象</strong>。<code>constant_name</code> 也是：它返回 <code>String</code>，每次调用都克隆一份名字。也就是说，<code>GetName</code> 每读一次变量，就分配一次内存。这是第 10 篇说过的”按名字找变量”的真实代价的一部分，我们等会儿有数字。</p><p>接着是环境的三个操作。定义、读取、赋值：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- helpers: environments -------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>current_env</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>GcRef</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.frames.</span><span>last</span><span>().</span><span>map_or</span><span>(</span><span>self</span><span><span>.globals, </span><span>|</span></span><span>frame</span><span><span>|</span><span> </span></span><span>frame</span><span>.env)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>define</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: </span><span>String</span><span>, </span><span>value</span><span>: </span><span>VmValue</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>env</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>current_env</span><span>();</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>VmObject</span><span>::</span><span>Env</span><span> { </span><span>slots</span><span><span>, .. } </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>get_mut</span><span>(</span><span>env</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>slot</span><span><span>) </span><span>=</span><span> </span></span><span>slots</span><span>.</span><span>iter_mut</span><span>().</span><span>find</span><span><span>(</span><span>|</span><span>(</span></span><span>existing</span><span>, </span><span>_</span><span><span>)</span><span>|</span><span> *</span></span><span>existing</span><span><span> </span><span>==</span><span> </span></span><span>name</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>                </span><span>slot</span><span>.</span><span>1</span><span><span> </span><span>=</span><span> </span></span><span>value</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>slots</span><span>.</span><span>push</span><span>((</span><span>name</span><span>, </span><span>value</span><span>));</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>lookup</span><span>(&amp;</span><span>self</span><span>, </span><span>name</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>VmValue</span><span>&gt; {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>current</span><span><span> </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>self</span><span>.</span><span>current_env</span><span>());</span></div></div><div><div><div>20</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>env</span><span><span>) </span><span>=</span><span> </span></span><span>current</span><span> {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>let</span><span> </span><span>VmObject</span><span>::</span><span>Env</span><span> { </span><span>parent</span><span>, </span><span>slots</span><span><span> } </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>get</span><span>(</span><span>env</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>22</div></div><div><span>                </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>24</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>((</span><span>_</span><span>, </span><span>value</span><span><span>)) </span><span>=</span><span> </span></span><span>slots</span><span>.</span><span>iter</span><span>().</span><span>find</span><span><span>(</span><span>|</span><span>(</span></span><span>existing</span><span>, </span><span>_</span><span><span>)</span><span>|</span><span> </span></span><span>existing</span><span><span> </span><span>==</span><span> </span></span><span>name</span><span>) {</span></div></div><div><div><div>25</div></div><div><span>                </span><span>return</span><span> </span><span>Some</span><span>(*</span><span>value</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>            </span><span>current</span><span><span> </span><span>=</span><span> *</span></span><span>parent</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>        </span><span>None</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>fn</span><span> </span><span>assign</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: &amp;</span><span>str</span><span>, </span><span>value</span><span>: </span><span>VmValue</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>33</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>current</span><span><span> </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>self</span><span>.</span><span>current_env</span><span>());</span></div></div><div><div><div>34</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>env</span><span><span>) </span><span>=</span><span> </span></span><span>current</span><span> {</span></div></div><div><div><div>35</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>VmObject</span><span>::</span><span>Env</span><span> { </span><span>parent</span><span>, </span><span>slots</span><span><span> } </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>get_mut</span><span>(</span><span>env</span><span>) {</span></div></div><div><div><div>36</div></div><div><span>                </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>slot</span><span><span>) </span><span>=</span><span> </span></span><span>slots</span><span>.</span><span>iter_mut</span><span>().</span><span>find</span><span><span>(</span><span>|</span><span>(</span></span><span>existing</span><span>, </span><span>_</span><span><span>)</span><span>|</span><span> </span></span><span>existing</span><span><span> </span><span>==</span><span> </span></span><span>name</span><span>) {</span></div></div><div><div><div>37</div></div><div><span>                    </span><span>slot</span><span>.</span><span>1</span><span><span> </span><span>=</span><span> </span></span><span>value</span><span>;</span></div></div><div><div><div>38</div></div><div><span>                    </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>                </span><span>current</span><span><span> </span><span>=</span><span> *</span></span><span>parent</span><span>;</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>42</div></div><div><span>                </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>        </span><span>false</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>lookup</code> 沿着 <code>parent</code> 链往上走，每一层在 <code>slots</code> 里线性扫一遍。<code>while let Some(env) = current</code> 加 <code>current = *parent</code> 是第 4 篇的 <code>while let</code> 模式。<code>let VmObject::Env { parent, slots } = self.heap.get(env) else { return None; };</code>：从堆里借出一个对象，如果不是 <code>Env</code> 就放弃。<code>define</code> 的行为值得注意：<strong>如果当前环境里已经有同名绑定，就覆盖它</strong>。这一条规则，加上”块不创建新环境”，就是后面”块级作用域”那个不一致的全部原因，先记在心里。</p><p>现在是 <code>call</code>，整个 VM 里最重要的函数之一。先看前半：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- helpers: calls ---------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>call</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>count</span><span>: </span><span>u32</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;(), </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>count</span><span><span> </span><span>=</span><span> </span></span><span>count</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.stack.</span><span>len</span><span>() &lt; </span><span>count</span><span> + </span><span>1</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>7</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>8</div></div><div><span>                </span><span>"internal error: call stack underflow"</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>));</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>args</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.stack.</span><span>split_off</span><span>(</span><span>self</span><span>.stack.</span><span>len</span><span>() - </span><span>count</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>callee</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>pop</span><span>(</span><span>span</span><span>)?;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>        </span><span>enum</span><span> </span><span>Target</span><span> {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Native</span><span>(&amp;'</span><span>static</span><span> </span><span>str</span><span>),</span></div></div><div><div><div>16</div></div><div><span>            </span><span>Closure</span><span>(</span><span>Rc</span><span>&lt;</span><span>FunctionProto</span><span>&gt;, </span><span>GcRef</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>target</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>callee</span><span>.</span><span>as_ref</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>reference</span><span><span>|</span><span> </span></span><span>self</span><span>.heap.</span><span>get</span><span>(</span><span>reference</span><span>)) {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>Some</span><span>(</span><span>VmObject</span><span>::</span><span>Native</span><span>(</span><span>name</span><span>)) =&gt; </span><span>Target</span><span>::</span><span>Native</span><span>(</span><span>name</span><span>),</span></div></div><div><div><div>21</div></div><div><span>            </span><span>Some</span><span>(</span><span>VmObject</span><span>::</span><span>Closure</span><span> { </span><span>proto</span><span>, </span><span>env</span><span> }) =&gt; </span><span>Target</span><span>::</span><span>Closure</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(</span><span>proto</span><span>), *</span><span>env</span><span>),</span></div></div><div><div><div>22</div></div><div><span>            </span><span>_</span><span> =&gt; {</span></div></div><div><div><div>23</div></div><div><span>                </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>24</div></div><div><span>                    </span><span>span</span><span>,</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>format!</span><span>(</span><span>"{} is not callable"</span><span>, </span><span>self</span><span>.</span><span>type_name</span><span>(</span><span>callee</span><span>)),</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>));</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>调用约定</strong>：字节码编译器（第 10 篇）先压被调用者，再按顺序压每个参数，最后 <code>Call(n)</code>。所以栈顶是 <code>[.., callee, arg1, ..., argN]</code>。<code>split_off(len - count)</code> 先拆出 N 个参数，再 <code>pop</code> 一次得到被调用者。</p><p>然后一个有趣的细节：局部枚举 <code>Target</code>。它只在这个函数里用，所以直接定义在函数体里。为什么需要它？因为接下来要从堆里读被调用者的信息（是个内建函数，还是个闭包，闭包的话 <code>proto</code> 和 <code>env</code> 是什么），读完要<strong>修改</strong>堆（分配新环境），读和改不能同时发生。<code>Target</code> 就是”从堆里拷贝出的一份快照”：<code>Native(&amp;'static str)</code> 的名字是 <code>'static</code> 引用，不借用堆；<code>Closure(Rc&lt;FunctionProto&gt;, GcRef)</code> 里 <code>Rc::clone</code> 是一份新的所有权，<code>GcRef</code> 是 <code>Copy</code>。拷贝出来之后，对 <code>self.heap</code> 的借用就结束了，后面可以随便改。</p><p>我当初没有这么写。最自然的写法是不要快照，直接在 <code>match</code> 里用借出来的引用，要什么拿什么：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>VmObject</span><span>::</span><span>Closure</span><span> { </span><span>proto</span><span>, </span><span>env</span><span><span> }) </span><span>=</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>callee</span><span>.</span><span>as_ref</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>reference</span><span><span>|</span><span> </span></span><span>self</span><span>.heap.</span><span>get</span><span>(</span><span>reference</span><span>))</span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>slots</span><span><span> </span><span>=</span><span> </span></span><span>proto</span><span>.params.</span><span>iter</span><span>().</span><span>cloned</span><span>().</span><span>zip</span><span>(</span><span>args</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>new_env</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>Env</span><span> { </span><span>parent</span><span>: </span><span>Some</span><span>(*</span><span>env</span><span>), </span><span>slots</span><span> });</span></div></div><div><div><div>6</div></div><div><span>    </span><span>self</span><span>.frames.</span><span>push</span><span>(</span><span>Frame</span><span> { </span><span>proto</span><span><span>: </span><span>Rc</span><span>::</span></span><span>clone</span><span>(</span><span>proto</span><span>), </span><span>env</span><span>: </span><span>new_env</span><span>, </span><span>ip</span><span>: </span><span>0</span><span> });</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> </span><span>Ok</span><span>(());</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把这段放进 <code>call</code> 里 <code>let callee = self.pop(span)?;</code> 的后面，编译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0502]: cannot borrow `self.heap` as mutable because it is also borrowed as immutable</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-vm/src/lib.rs:480:27</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>477 |               callee.as_ref().map(|reference| self.heap.get(reference))</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                                   ----------- --------- first borrow occurs due to use of `self.heap` in closure</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|                                   |</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|                                   immutable borrow occurs here</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div><div><div><div>9</div></div><div><span>480 |               let new_env = self.heap.alloc(VmObject::Env {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|  ___________________________^</span></div></div><div><div><div>11</div></div><div><span>481 | |                 parent: Some(*env),</span></div></div><div><div><div>12</div></div><div><span>482 | |                 slots,</span></div></div><div><div><div>13</div></div><div><span>483 | |             });</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>| |______________^ mutable borrow occurs here</span></div></div><div><div><div>15</div></div><div><span>484 |               self.frames.push(Frame {</span></div></div><div><div><div>16</div></div><div><span>485 |                   proto: Rc::clone(proto),</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>|                                    ----- immutable borrow later used here</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>For more information about this error, try `rustc --explain E0502`.</span></div></div><div><div><div>20</div></div><div><span>error: could not compile `pebble-vm` (lib) due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这是 Rust 最著名的错误之一，而且这次它<strong>说得对</strong>。<code>proto</code> 和 <code>env</code> 是指向 <code>self.heap</code> 里某个槽位的引用；<code>self.heap.alloc(..)</code> 可能让底层的 <code>Vec</code> 重新分配、搬家，那个槽位就换地方了，<code>proto</code> 和 <code>env</code> 就悬空了。C++ 里这是迭代器失效：在遍历 <code>std::vector</code> 的同时 <code>push_back</code>，标准说是未定义行为，实践中是偶发的崩溃。Rust 在编译期就把它拦下，代价是你必须改变代码的形状：<strong>先把要用的东西拷出来，借用结束，再改。</strong> <code>Target</code> 枚举就是这个形状。</p><p>（这条规矩在整个文件里反复出现。你很快会在 <code>index_value</code> 里再见到它，那里的”先拷出来”代价高得多，后面的性能一节会讲。）</p><p>后半是真正的调用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>match</span><span> </span><span>target</span><span> {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>Target</span><span>::</span><span>Native</span><span>(</span><span>name</span><span>) =&gt; {</span></div></div><div><div><div>3</div></div><div><span>                </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>call_builtin</span><span>(</span><span>name</span><span>, &amp;</span><span>args</span><span>, </span><span>span</span><span>)?;</span></div></div><div><div><div>4</div></div><div><span>                </span><span>self</span><span>.stack.</span><span>push</span><span>(</span><span>result</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Target</span><span>::</span><span>Closure</span><span>(</span><span>proto</span><span>, </span><span>env</span><span>) =&gt; {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>if</span><span> </span><span>args</span><span>.</span><span>len</span><span><span>() </span><span>!=</span><span> </span></span><span>proto</span><span>.params.</span><span>len</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>9</div></div><div><span>                        </span><span>span</span><span>,</span></div></div><div><div><div>10</div></div><div><span>                        </span><span>format!</span><span>(</span></div></div><div><div><div>11</div></div><div><span>                            </span><span>"{} expects {} argument(s), got {}"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>                            </span><span>proto</span><span>.</span><span>display_name</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>                            </span><span>proto</span><span>.params.</span><span>len</span><span>(),</span></div></div><div><div><div>14</div></div><div><span>                            </span><span>args</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>15</div></div><div><span><span>                        </span></span><span>),</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>));</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>                </span><span>if</span><span> </span><span>self</span><span>.frames.</span><span>len</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>self</span><span>.max_frames {</span></div></div><div><div><div>19</div></div><div><span>                    </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>20</div></div><div><span>                        </span><span>span</span><span>,</span></div></div><div><div><div>21</div></div><div><span>                        </span><span>format!</span><span>(</span><span>"call stack overflow (limit {})"</span><span>, </span><span>self</span><span>.max_frames),</span></div></div><div><div><div>22</div></div><div><span><span>                    </span></span><span>));</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>                </span><span>let</span><span> </span><span>slots</span><span><span> </span><span>=</span><span> </span></span><span>proto</span><span>.params.</span><span>iter</span><span>().</span><span>cloned</span><span>().</span><span>zip</span><span>(</span><span>args</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>25</div></div><div><span>                </span><span>let</span><span> </span><span>new_env</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>alloc</span><span><span>(</span><span>VmObject</span><span>::</span></span><span>Env</span><span> {</span></div></div><div><div><div>26</div></div><div><span>                    </span><span>parent</span><span>: </span><span>Some</span><span>(</span><span>env</span><span>),</span></div></div><div><div><div>27</div></div><div><span>                    </span><span>slots</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>});</span></div></div><div><div><div>29</div></div><div><span>                </span><span>self</span><span>.frames.</span><span>push</span><span>(</span><span>Frame</span><span> {</span></div></div><div><div><div>30</div></div><div><span>                    </span><span>proto</span><span>,</span></div></div><div><div><div>31</div></div><div><span>                    </span><span>env</span><span>: </span><span>new_env</span><span>,</span></div></div><div><div><div>32</div></div><div><span>                    </span><span>ip</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>33</div></div><div><span><span>                </span></span><span>});</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>        </span><span>Ok</span><span>(())</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>内建函数：直接调用，把结果压栈。闭包：检查参数个数，检查帧数是否超过 <code>max_frames</code>，然后做三件事：用 <code>params.iter().cloned().zip(args).collect()</code> 把形参名和实参配成 <code>slots</code>（<code>zip</code> 把两个迭代器逐对合并，<code>collect</code> 到 <code>Vec&lt;(String, VmValue)&gt;</code>），在堆上分配一个以闭包的 <code>env</code> 为父的新环境，最后压入新帧。<strong>注意没有递归</strong>：<code>call</code> 只是往 <code>self.frames</code> 里 push 了一个帧就返回了，真正的执行由 <code>run</code> 的外层循环继续，下一圈会从新帧的第 0 条指令开始。</p><div><div><div></div><div>为什么这值得强调</div></div><div><p>第 9 篇的树遍历解释器里，Pebble 的每一次函数调用对应一次 Rust 的函数调用（<code>call_value</code> → <code>run_function</code> → <code>execute_block</code> → <code>eval</code> → <code>call_value</code>…），所以递归深度受 Rust 栈大小限制，我们不得不给测试开一个大栈的线程。这个 VM 里 Pebble 的调用栈是一个 <code>Vec&lt;Frame&gt;</code>，Rust 栈的深度和 Pebble 的递归深度<strong>无关</strong>。<code>max_frames</code> 的 256 是一个策略，不是技术限制。</p></div></div><p>给 <code>impl Vm</code> 补上这些方法之后，再编译一次，看看还差什么：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>      </span></span><span>2 error[E0599]: no method named `value_equal` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>2 error[E0599]: no method named `type_name` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>3</div></div><div><span><span>      </span></span><span>2 error[E0599]: no method named `binary_numeric` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `set_index_value` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `overflow` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `index_value` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `collect_garbage` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `call_builtin` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_rem` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_div` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>11</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_compare` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>1 error[E0599]: no method named `binary_add` found for mutable reference `&amp;mut Vm` in the current scope</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>1 error: could not compile `pebble-vm` (lib) due to 15 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>从 33 个错误减到了 15 个，缺的方法只剩两类：运算符和下标（<code>binary_add</code>、<code>binary_div</code>、<code>index_value</code>…）、内建函数和辅助（<code>call_builtin</code>、<code>type_name</code>、<code>value_equal</code>…），外加 <code>collect_garbage</code>。先做前者。</p></section>
<section><h2>复制：运算符、下标、内建函数、显示<a href="#复制运算符下标内建函数显示"><span>#</span></a></h2><p>这些方法的共同点是：它们都在用上面已经讲过的东西，没有新的 Rust 知识。我们<strong>复制</strong>它们。把 <code>https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-vm/src/lib.rs</code> 的<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-vm/src/lib.rs#L531-L1101" target="_blank">第 531 行到第 1101 行</a>（或者在 <code>~/pebble-tag</code> 里 <code>sed -n '531,1101p' ~/pebble-tag/crates/pebble-vm/src/lib.rs</code>）原样粘贴到 <code>call</code> 的后面，中间空一行。这一整块是：</p><ul>
<li><code>call_builtin</code>：按名字分派 11 个内建函数的大 <code>match</code>（<code>print</code>、<code>len</code>、<code>type</code>、<code>str</code>、<code>int</code>、<code>float</code>、<code>range</code>、<code>push</code>、<code>keys</code>、<code>clock</code>、<code>assert</code>）。</li>
<li><code>expect_args</code>、<code>expect_int</code>、<code>allocate_string</code>：三个小辅助。</li>
<li><code>overflow</code>、<code>binary_add</code>、<code>binary_numeric</code>、<code>binary_div</code>、<code>binary_rem</code>、<code>binary_compare</code>：算术和比较，每个都是”弹两个，匹配类型对，压结果”。</li>
<li><code>is_number</code>、<code>as_f64</code>、<code>is_str</code>、<code>is_list</code>、<code>as_string</code>、<code>as_list</code>、<code>value_equal</code>、<code>type_name</code>：类型判断和相等。</li>
<li><code>index_value</code>、<code>set_index_value</code>、<code>resolve_index</code>：下标读写，支持负下标。</li>
<li><code>display</code> 和 <code>write_display</code>：把值渲染成字符串（<code>display</code> 是 <code>pub</code>，第四版命令行要用）。</li>
</ul><p>我要指给你看三个地方，因为它们值得你读一遍，而不是只粘贴：</p><ol>
<li><code>call_builtin</code> 的 <code>match name { "print" =&gt; ..., "len" =&gt; ... }</code> 是<strong>按字符串匹配</strong>：每次调用内建函数，都要拿函数名和十一个字符串依次比较（编译器具体怎么优化这个 <code>match</code>，我没有去看生成的代码）。</li>
<li><code>index_value</code> 开头那段 <code>// Snapshot the contents so the heap borrow ends before we allocate.</code>，对列表是 <code>items.clone()</code>：<strong>读一次 <code>xs[i]</code>，把整个列表克隆一遍。</strong> 这就是上面 E0502 那条规矩的昂贵版本。我是在分析基准数字的时候注意到它的，后面的性能一节里有一个实验。</li>
<li><code>write_display</code> 里 <code>VmObject::Str(text) =&gt; out.push_str(text)</code>：字符串直接输出，<strong>不加引号</strong>，不管它是顶层的还是嵌在列表里的。树遍历解释器的 <code>Display</code> 在列表里会给字符串加引号。这是下面”两个后端不一致”一节的第一个例子。</li>
</ol></section>
<section><h2>GC 根：什么时候可以回收<a href="#gc-根什么时候可以回收"><span>#</span></a></h2><p>现在补上最后一个方法 <code>collect_garbage</code>，放在复制来的那一大块的后面，同时闭合 <code>impl Vm</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- garbage collection ----------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>/// Marks every root and sweeps.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>///</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// Called between instructions, the only point where every live value is</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// reachable from the stack, a frame, or the globals.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>collect_garbage</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>for</span><span> </span><span>value</span><span> </span><span>in</span><span> &amp;</span><span>self</span><span>.stack {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>VmValue</span><span>::</span><span>Ref</span><span>(</span><span>reference</span><span><span>) </span><span>=</span><span> </span></span><span>value</span><span> {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>self</span><span>.heap.</span><span>mark</span><span>(*</span><span>reference</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> </span><span>frame</span><span> </span><span>in</span><span> &amp;</span><span>self</span><span>.frames {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>self</span><span>.heap.</span><span>mark</span><span>(</span><span>frame</span><span>.env);</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.heap.</span><span>mark</span><span>(</span><span>self</span><span>.globals);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span>.heap.</span><span>collect</span><span>();</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第 11 篇的 <code>Heap</code> 把回收分成两步：嵌入方对每个根调用 <code>mark</code>，然后调用 <code>collect</code>。VM 的根有三类：值栈里的每个 <code>Ref</code>、每个活动帧的环境、全局环境。就这些。</p><p>为什么<strong>只在指令之间</strong>回收？因为在一条指令的执行中间，有些活着的值<strong>不在任何根里</strong>：<code>call</code> 里刚 <code>split_off</code> 出来的 <code>args</code> 是个局部 <code>Vec</code>，<code>binary_add</code> 里弹出来的 <code>lhs</code> 和 <code>rhs</code> 是两个局部变量，它们引用的堆对象对回收器是不可见的。如果回收恰好发生在那时，这些对象会被当成垃圾释放，然后指令继续用着已经被释放的句柄。C++ 里写过精确 GC 的人都认识这个问题：它叫”根的完备性”，一般的解法有两种，要么维护一个”影子栈”（每个局部变量都注册一下），要么规定回收只发生在局部变量都已经落到根上的<strong>安全点</strong>。我们选后者，安全点就是 <code>run</code> 循环的开头：此时每个活着的值，要么在 <code>self.stack</code> 里，要么挂在某个帧的环境（或全局环境）下面。</p><p>这条规矩能成立，还有赖于第 11 篇里的一个设计：<strong><code>Heap::alloc</code> 自己从不触发回收</strong>，它只是计数（<code>allocations_since_gc</code>）。“要不要回收”永远是 <code>run</code> 循环开头那一句 <code>if</code> 在决定。</p><p>我不想只凭论证说”根是完备的”，所以做了两个实验。第一个：把阈值改成 0，也就是自上次回收以来<strong>只要分配过任何东西</strong>，下一条指令之前就回收。这是一个压力模式，它把回收频率提到最高，任何缺失的根都会立刻暴露：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.47s</span></div></div><div><div><div>3</div></div><div><span>test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div><div><div><div>4</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>5</div></div><div><span>test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pebble-vm</code> 的 14 个测试、<code>pebble-bench</code> 的 2 个单元测试和 5 个差分测试（本篇后面才写）、<code>pebble-cli</code> 的 3 个单元测试和 10 个集成测试，在压力模式下全部通过（我是在临时副本里把 <code>Vm::with_output</code> 里的 <code>gc_threshold: 1024</code> 改成 <code>0</code> 跑的，跑完恢复）。</p><p>第二个实验是反过来的：故意漏掉一类根。我把 <code>collect_garbage</code> 里”标记每个帧的环境”那个 <code>for</code> 循环删掉，仍然是压力模式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test tests::closures_capture_their_environment ... FAILED</span></div></div><div><div><div>2</div></div><div><span>test tests::recursion_via_global_function ... FAILED</span></div></div><div><div><div>3</div></div><div><span>test tests::recursion_limit_is_enforced ... FAILED</span></div></div><div><div><div>4</div></div><div><span>GcRef(13) does not point to a live object</span></div></div><div><div><div>5</div></div><div><span>GcRef(13) does not point to a live object</span></div></div><div><div><div>6</div></div><div><span>GcRef(13) does not point to a live object</span></div></div><div><div><div>7</div></div><div><span>test result: FAILED. 11 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>thread 'tests::closures_capture_their_environment' (154722) panicked at crates/pebble-gc/src/lib.rs:161:32:</span></div></div><div><div><div>2</div></div><div><span>GcRef(13) does not point to a live object</span></div></div><div><div><div>3</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个测试挂了，信息是第 11 篇 <code>Heap::get_mut</code> 里的 <code>GcRef(13) does not point to a live object</code>：VM 去读一个函数的环境，而那个环境已经被当成垃圾回收掉了。这就是”漏一个根”的真实后果：没有 UB（第 11 篇的句柄是下标，不是裸指针），没有悄悄的错误结果，是一个干净的 panic。这是句柄设计的好处。顺便注意，漏掉帧之后，<strong>不压力模式</strong>的测试大多还是通过的：阈值是 1024，小程序根本触发不了回收。这类 bug 如果没有压力模式，在小测试里一辈子也看不见。</p></section>
<section><h2>测试，以及一个证明环会被回收的测试<a href="#测试以及一个证明环会被回收的测试"><span>#</span></a></h2><p>文件末尾是测试模块。先写它的骨架：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_parser</span><span>::parse;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>Buffer</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>struct</span><span> </span><span>Shared</span><span>(</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Buffer</span><span>&gt;&gt;);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>impl</span><span> </span><span>Output</span><span> </span><span>for</span><span> </span><span>Shared</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>self</span><span>.</span><span>0.</span><span>borrow_mut</span><span>().</span><span>write_line</span><span>(</span><span>line</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>/// Compiles and runs, returning the VM (for `display`), its result, and the</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// captured output.</span></div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>run_full</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;(</span><span>Vm</span><span>, </span><span>VmValue</span><span>, </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;), </span><span>String</span><span>&gt; {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>shared</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"&lt;test&gt;"</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>23</div></div><div><span>        </span><span>if</span><span><span> </span><span>!</span></span><span>parsed</span><span>.</span><span>is_ok</span><span>() {</span></div></div><div><div><div>24</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>parsed</span></div></div><div><div><div>25</div></div><div><span><span>                </span></span><span>.diagnostics</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>27</div></div><div><span><span>                </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>diagnostic</span><span><span>|</span><span> </span></span><span>diagnostic</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>))</span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>.</span><span>collect</span><span>::&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;()</span></div></div><div><div><div>29</div></div><div><span><span>                </span></span><span>.</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>));</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span><span>pebble_bytecode</span><span>::</span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>32</div></div><div><span>        </span><span>if</span><span><span> </span><span>!</span></span><span>compiled</span><span>.</span><span>is_ok</span><span>() {</span></div></div><div><div><div>33</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>compiled</span></div></div><div><div><div>34</div></div><div><span><span>                </span></span><span>.diagnostics</span></div></div><div><div><div>35</div></div><div><span><span>                </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>36</div></div><div><span><span>                </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>diagnostic</span><span><span>|</span><span> </span></span><span>diagnostic</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>))</span></div></div><div><div><div>37</div></div><div><span><span>                </span></span><span>.</span><span>collect</span><span>::&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;()</span></div></div><div><div><div>38</div></div><div><span><span>                </span></span><span>.</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>));</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>vm</span><span><span> </span><span>=</span><span> </span></span><span>Vm</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>Shared</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>shared</span><span>))));</span></div></div><div><div><div>41</div></div><div><span>        </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>vm</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>.</span><span>run</span><span>(&amp;</span><span>compiled</span><span>.compiled)</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>.</span><span>map_err</span><span><span>(</span><span>|</span></span><span>error</span><span><span>|</span><span> </span></span><span>error</span><span>.diagnostic.</span><span>render</span><span>(&amp;</span><span>sources</span><span>))?;</span></div></div><div><div><div>44</div></div><div><span>        </span><span>let</span><span> </span><span>lines</span><span><span> </span><span>=</span><span> </span></span><span>shared</span><span>.</span><span>borrow</span><span>().</span><span>lines</span><span>().</span><span>to_vec</span><span>();</span></div></div><div><div><div>45</div></div><div><span>        </span><span>Ok</span><span>((</span><span>vm</span><span>, </span><span>value</span><span>, </span><span>lines</span><span>))</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>VmValue</span><span>, </span><span>String</span><span>&gt; {</span></div></div><div><div><div>49</div></div><div><span>        </span><span>run_full</span><span>(</span><span>src</span><span>).</span><span>map</span><span><span>(</span><span>|</span><span>(</span></span><span>_</span><span>, </span><span>value</span><span>, </span><span>_</span><span><span>)</span><span>|</span><span> </span></span><span>value</span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>    </span><span>fn</span><span> </span><span>run_display</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>String</span><span>, </span><span>String</span><span>&gt; {</span></div></div><div><div><div>53</div></div><div><span>        </span><span>run_full</span><span>(</span><span>src</span><span>).</span><span>map</span><span><span>(</span><span>|</span><span>(</span></span><span>vm</span><span>, </span><span>value</span><span>, </span><span>_</span><span><span>)</span><span>|</span><span> </span></span><span>vm</span><span>.</span><span>display</span><span>(</span><span>value</span><span>))</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>    </span><span>fn</span><span> </span><span>run_output</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>String</span><span>&gt; {</span></div></div><div><div><div>57</div></div><div><span>        </span><span>run_full</span><span>(</span><span>src</span><span>).</span><span>map</span><span><span>(</span><span>|</span><span>(</span></span><span>_</span><span>, </span><span>_</span><span>, </span><span>lines</span><span><span>)</span><span>|</span><span> </span></span><span>lines</span><span>)</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Shared(Rc&lt;RefCell&lt;Buffer&gt;&gt;)</code> 是第 9 篇测试里用过的同一个小技巧：<code>Vm</code> 拿走了 <code>Box&lt;dyn Output&gt;</code> 的所有权，而测试之后要读回输出，所以让输出对象和测试各持一份 <code>Rc&lt;RefCell&lt;Buffer&gt;&gt;</code>。<code>run_full</code> 把整个流水线串起来：解析、编译、运行，任何一步的诊断都渲染成字符串当作 <code>Err</code>，成功时返回 VM 本身（<code>display</code> 需要它）、结果值和捕获到的输出。后面三个函数是它的三个窄视图。</p><p>现在是 13 个”VM 的行为和树遍历解释器一致”的测试：<code>arithmetic_matches_the_tree_walker</code>、<code>variables_and_assignment</code>、<code>recursion_via_global_function</code>、<code>closures_capture_their_environment</code>、<code>lists_maps_and_indexing</code>、<code>strings_concatenate_and_index</code>、<code>control_flow_matches_the_tree_walker</code>、<code>while_loop_accumulates</code>、<code>print_reaches_the_output_sink</code>、<code>division_by_zero_is_an_error</code>、<code>arity_errors_are_reported</code>、<code>undefined_variables_are_reported</code>、<code>recursion_limit_is_enforced</code>。它们没有新概念，从 tag 的<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-vm/src/lib.rs#L1182-L1300" target="_blank">第 1182 到 1300 行</a>复制过来，接在 <code>run_output</code> 的后面。</p><p>只有一条我想让你读：<code>recursion_limit_is_enforced</code> 把测试放进了一个 64 MiB 栈的线程里，注释说”VM 的递归在堆上，但 Rust 的帧不是”。上面那个”为什么这值得强调”的框说的恰恰是<strong>这条注释过于谨慎了</strong>：VM 的递归不消耗 Rust 栈。我写了一个临时测试来确认（放进测试模块，跑完删掉）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>deep_recursion_on_the_default_thread</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>"fn down(n) { if n == 0 { return 0; } return 1 + down(n - 1); } down(100000);"</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"&lt;test&gt;"</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span><span>pebble_bytecode</span><span>::</span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>vm</span><span><span> </span><span>=</span><span> </span></span><span>Vm</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>9</div></div><div><span>    </span><span>vm</span><span>.</span><span>set_max_frames</span><span>(</span><span>1_000_000</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>.</span><span>run</span><span>(&amp;</span><span>compiled</span><span>.compiled).</span><span>unwrap</span><span>();</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"result = {}"</span><span>, </span><span>vm</span><span>.</span><span>display</span><span>(</span><span>value</span><span>));</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.86s</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (/tmp/ch12-stage-target/debug/deps/pebble_vm-32c6a9188a572458)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>running 1 test</span></div></div><div><div><div>5</div></div><div><span>result = 100000</span></div></div><div><div><div>6</div></div><div><span>test tests::deep_recursion_on_the_default_thread ... ok</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 14 filtered out; finished in 0.56s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>十万层递归，跑在 <code>cargo test</code> 默认的小栈线程上，没有问题。（对照第 9 篇：树遍历解释器在默认的 2 MiB 测试线程上，撑得过 128 层，撑不过 192 层。）所以那条 64 MiB 的线程是多余的保险，我把它留在了 tag 里，因为”多余但无害”不值得改动。</p><p>最后一个测试是我们自己写的，它证明回收器真的能回收一个环：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>garbage_collection_reclaims_cycles</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>"</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>fn make() {</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>fn inner() { return inner; }</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>return 1;</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>make();</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>"</span><span>;</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"&lt;test&gt;"</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span><span>pebble_bytecode</span><span>::</span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>vm</span><span><span> </span><span>=</span><span> </span></span><span>Vm</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>15</div></div><div><span>        </span><span>vm</span><span>.</span><span>set_gc_threshold</span><span><span>(</span><span>usize</span><span>::</span></span><span>MAX</span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>vm</span><span>.</span><span>run</span><span>(&amp;</span><span>compiled</span><span>.compiled).</span><span>unwrap</span><span>();</span></div></div><div><div><div>17</div></div><div><span>        </span><span>let</span><span> </span><span>before</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>.</span><span>heap_live</span><span>();</span></div></div><div><div><div>18</div></div><div><span>        </span><span>vm</span><span>.</span><span>collect_garbage</span><span>();</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>after</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>.</span><span>heap_live</span><span>();</span></div></div><div><div><div>20</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>21</div></div><div><span>            </span><span>before</span><span> - </span><span>after</span><span>,</span></div></div><div><div><div>22</div></div><div><span>            </span><span>2</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>"make's call env and `inner` form an unreachable cycle"</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>vm</span><span>.</span><span>collect_garbage</span><span>();</span></div></div><div><div><div>26</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>vm</span><span>.</span><span>heap_live</span><span>(), </span><span>after</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>程序里 <code>make</code> 在自己的函数体里定义了一个内部函数 <code>inner</code>，<code>inner</code> 返回它自己。<code>inner</code> 这个闭包捕获的环境就是 <code>make</code> 这次调用的环境；而 <code>make</code> 的环境里存着 <code>inner</code>。一个环：环境引用闭包，闭包引用环境。<code>make()</code> 返回之后，没有任何根能走到它们。</p><p><code>vm.set_gc_threshold(usize::MAX)</code> 关掉自动回收，让数字稳定。<code>run</code> 之后手动量：回收前的存活对象数，回收后的存活对象数，<strong>差恰好是 2</strong>：<code>make</code> 的调用环境和 <code>inner</code> 闭包，不多不少。再回收一次，数量不变（第二次没有东西可以回收）。
对比第 9 篇：那里我们用 <code>Weak</code> 证明了，树遍历解释器里存进环境的函数会让环境和函数互相拽住，永远不会释放（<code>top_level_functions_keep_globals_alive</code> 断言泄漏）；这里的测试断言回收。这是 Pebble 有两个后端的一个理由：它们执行同一个语言，却有两种相反的内存行为。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-vm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-vm</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_vm-32c6a9188a572458)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 14 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::arithmetic_matches_the_tree_walker ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::division_by_zero_is_an_error ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::control_flow_matches_the_tree_walker ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::arity_errors_are_reported ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::closures_capture_their_environment ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::garbage_collection_reclaims_cycles ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::print_reaches_the_output_sink ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::lists_maps_and_indexing ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::recursion_via_global_function ... ok</span></div></div><div><div><div>15</div></div><div><span>test tests::undefined_variables_are_reported ... ok</span></div></div><div><div><div>16</div></div><div><span>test tests::strings_concatenate_and_index ... ok</span></div></div><div><div><div>17</div></div><div><span>test tests::variables_and_assignment ... ok</span></div></div><div><div><div>18</div></div><div><span>test tests::while_loop_accumulates ... ok</span></div></div><div><div><div>19</div></div><div><span>test tests::recursion_limit_is_enforced ... ok</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>   </span></span><span>Doc-tests pebble_vm</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>running 0 tests</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>14 个测试通过。</p></section>
<section><h2>弄坏它，让编译器说话<a href="#弄坏它让编译器说话"><span>#</span></a></h2><p>最后再做一次实验，回到 <code>run</code> 里那个大 <code>match</code>。把 <code>Opcode::Return =&gt; { ... }</code> 那个 arm 整个删掉：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0004]: non-exhaustive patterns: `Opcode::Return` not covered</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-vm/src/lib.rs:226:19</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>226 |             match instruction {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                   ^^^^^^^^^^^ pattern `Opcode::Return` not covered</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>note: `Opcode` defined here</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-bytecode/src/lib.rs:26:1</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>26 | / opcode_set! {</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>27 | |     /// One Pebble instruction.</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>28 | |     ///</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>29 | |     /// Variants with a `u32` operand index into the chunk's constant pool, or</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>30 | |     /// name a jump target (an instruction index).</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>31 | |     pub enum Opcode {</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>| |___________________^</span></div></div><div><div><div>17</div></div><div><span>...</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>89 |           Return,</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|           ------ not covered</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>= note: the matched value is of type `Opcode`</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>= note: this error originates in the macro `opcode_set` (in Nightly builds, run with -Z macro-backtrace for more info)</span></div></div><div><div><div>22</div></div><div><span>help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>24</div></div><div><span>372 ~                 },</span></div></div><div><div><div>25</div></div><div><span>373 +                 Opcode::Return =&gt; todo!()</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>For more information about this error, try `rustc --explain E0004`.</span></div></div><div><div><div>29</div></div><div><span>error: could not compile `pebble-vm` (lib) due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器不只是说”漏了一个”，它还指给你看 <code>Return</code> 在 <code>opcode_set!</code> 的哪一行（第 89 行），还给了一个 <code>todo!()</code> 的修补建议。第 10 篇的练习里我们看过给 <code>Opcode</code> 加 <code>Dup</code> 时的同一条错误。这是第 10 篇用宏造 <code>Opcode</code> 枚举换来的回报：<strong>每一条新指令都会强迫你在 VM 里实现它</strong>，没有一个地方可以悄悄漏掉。C 的 <code>switch</code> 加 <code>-Wswitch</code> 也能做到类似的事，但它是个可以关掉的警告，这个是错误。</p></section>
<section><h2>命令行第四版（上）：同一件事做两次<a href="#命令行第四版上同一件事做两次"><span>#</span></a></h2><p>VM 写完，没有人调用它。<code>pebble run</code> 还只会走树遍历。我要的是 <code>pebble run --vm file.pebble</code>，外加一个 <code>pebble bench file.pebble</code>，后者在两个引擎上各跑若干遍并报告用时。</p><p>两个引擎要做的事情是一样的：把解析好的程序变成引擎能吃的东西（树遍历吃 AST 本身，VM 吃编译出的字节码），新建一个引擎，让它跑，把最后的值打印出来或者报错。C 程序员的第一反应大概是在 <code>run</code> 里写一个 <code>if args.vm { ... } else { ... }</code>，两边各抄一份流程，<code>bench</code> 里再抄一遍。三份几乎相同的代码，迟早有一份被改了而另两份没改。</p><p>更干净的做法是把”引擎”抽成一个接口。Rust 里接口是 trait，我们在第 3 篇已经认识了它。但这一次有一个真正的设计问题，在 Rust 里和在 C++ 里答案不一样：这个接口要用<strong>静态分发</strong>还是<strong>动态分发</strong>？</p><section><h3>静态分发与动态分发<a href="#静态分发与动态分发"><span>#</span></a></h3><p>C++ 里你有两种办法：模板（编译期给每个具体类型生成一份代码）和虚函数（一份代码，通过 vtable 间接调用）。Rust 的 trait 同样支持这两种，语法上只差一个关键字。我写了一个实验，把两种方式放在一起，也量了指针的大小。把下面的内容写进 <code>~/rust-lab/src/bin/ch12_dispatch.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::size_of;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>trait</span><span> </span><span>Engine</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>name</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>u32</span><span>;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>struct</span><span> </span><span>Fast</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>9</div></div><div><span>struct</span><span> </span><span>Slow</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>Engine</span><span> </span><span>for</span><span> </span><span>Fast</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>name</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"fast"</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.</span><span>0</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span>.</span><span>0</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>impl</span><span> </span><span>Engine</span><span> </span><span>for</span><span> </span><span>Slow</span><span> {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>fn</span><span> </span><span>name</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>"slow"</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>0</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>// Static dispatch: the compiler writes one copy per concrete type.</span></div></div><div><div><div>31</div></div><div><span>fn</span><span> </span><span>run_static</span><span>&lt;</span><span>E</span><span>: </span><span>Engine</span><span>&gt;(</span><span>engine</span><span>: &amp;</span><span>mut</span><span> </span><span>E</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>engine</span><span>.</span><span>run</span><span>()</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>// Dynamic dispatch: one copy, the call goes through a vtable.</span></div></div><div><div><div>36</div></div><div><span>fn</span><span> </span><span>run_dynamic</span><span>(</span><span>engine</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Engine</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>engine</span><span>.</span><span>run</span><span>()</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>41</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>fast</span><span><span> </span><span>=</span><span> </span></span><span>Fast</span><span>(</span><span>41</span><span>);</span></div></div><div><div><div>42</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>slow</span><span><span> </span><span>=</span><span> </span></span><span>Slow</span><span>;</span></div></div><div><div><div>43</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{} -&gt; {}"</span><span>, </span><span>fast</span><span>.</span><span>name</span><span>(), </span><span>run_static</span><span>(&amp;</span><span>mut</span><span> </span><span>fast</span><span>));</span></div></div><div><div><div>44</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{} -&gt; {}"</span><span>, </span><span>slow</span><span>.</span><span>name</span><span>(), </span><span>run_static</span><span>(&amp;</span><span>mut</span><span> </span><span>slow</span><span>));</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>engines</span><span>: </span><span>Vec</span><span>&lt;</span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Engine</span><span><span>&gt;&gt; </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>Box</span><span>::</span><span>new</span><span>(</span><span>Fast</span><span>(</span><span>0</span><span>)), </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>Slow</span><span>)];</span></div></div><div><div><div>47</div></div><div><span>    </span><span>for</span><span> </span><span>engine</span><span> </span><span>in</span><span> &amp;</span><span>mut</span><span> </span><span>engines</span><span> {</span></div></div><div><div><div>48</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{} -&gt; {}"</span><span>, </span><span>engine</span><span>.</span><span>name</span><span>(), </span><span>run_dynamic</span><span>(</span><span>engine</span><span>.</span><span>as_mut</span><span>()));</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;&amp;Fast&gt;()       = {}"</span><span>, </span><span>size_of</span><span>::&lt;&amp;</span><span>Fast</span><span>&gt;());</span></div></div><div><div><div>52</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;&amp;dyn Engine&gt;() = {}"</span><span>, </span><span>size_of</span><span>::&lt;&amp;</span><span>dyn</span><span> </span><span>Engine</span><span>&gt;());</span></div></div><div><div><div>53</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;Fast&gt;()        = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Fast</span><span>&gt;());</span></div></div><div><div><div>54</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;Slow&gt;()        = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Slow</span><span>&gt;());</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch12_dispatch</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fast -&gt; 42</span></div></div><div><div><div>2</div></div><div><span>slow -&gt; 0</span></div></div><div><div><div>3</div></div><div><span>fast -&gt; 1</span></div></div><div><div><div>4</div></div><div><span>slow -&gt; 0</span></div></div><div><div><div>5</div></div><div><span>size_of::&lt;&amp;Fast&gt;()       = 8</span></div></div><div><div><div>6</div></div><div><span>size_of::&lt;&amp;dyn Engine&gt;() = 16</span></div></div><div><div><div>7</div></div><div><span>size_of::&lt;Fast&gt;()        = 4</span></div></div><div><div><div>8</div></div><div><span>size_of::&lt;Slow&gt;()        = 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>run_static&lt;E: Engine&gt;(engine: &amp;mut E)</code> 是泛型函数：编译器为每一个传进来的具体类型各生成一份机器码，这叫<strong>单态化</strong>（monomorphization），和 C++ 模板的实例化是一回事。调用直接绑定到具体的函数，可以内联。代价是代码体积：N 种类型，N 份函数。<code>run_dynamic(engine: &amp;mut dyn Engine)</code> 只有一份机器码，调用 <code>engine.run()</code> 要先读 vtable 里的函数指针再间接跳转，不能内联。第 8 篇我们见过 <code>&amp;dyn Output</code> 是 16 字节，这里再看一次：<code>&amp;Fast</code> 是 8 字节（一个普通指针），<code>&amp;dyn Engine</code> 是 16 字节（数据指针加 vtable 指针）。<code>Vec&lt;Box&lt;dyn Engine&gt;&gt;</code> 能把不同类型放在一个数组里，<code>Vec&lt;E&gt;</code> 做不到：这是动态分发唯一不可替代的能力，<strong>异构集合</strong>。</p><p>我们的命令行不需要异构集合。<code>run</code> 和 <code>bench</code> 在编译期就知道要用哪个引擎，没有”运行时才决定、放进一个数组里”的场合。所以选静态分发：每个引擎一份代码，没有 vtable。</p></section><section><h3>一个 trait 要说清楚什么<a href="#一个-trait-要说清楚什么"><span>#</span></a></h3><p>两个引擎在细节上并不相同，trait 要把差异抽出来。我列了一下：</p><ul>
<li>打印基准结果时要一个标签（<code>"tree-walker"</code>、<code>"vm"</code>）。这是一个<strong>类型的属性</strong>，不属于任何一个具体的值，所以用<strong>关联常量</strong>（associated const）：<code>const NAME: &amp;'static str;</code>。C++ 里是 <code>static constexpr const char* name</code>。</li>
<li>树遍历执行的是<strong>借来的 AST</strong>（<code>&amp;Program</code>），VM 执行的是<strong>编译出来、拥有所有权的字节码</strong>（<code>Compiled</code>）。同一个”准备”步骤，产物一个带生命周期，一个不带。</li>
</ul><p>第二点就是<strong>泛型关联类型</strong>（GAT, generic associated type）存在的理由。先看如果没有它会怎样。我把 trait 写成关联类型不带生命周期的最朴素形式（<code>~/rust-lab/src/bin/ch12_gat_missing.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// What if `Prepared` had no lifetime parameter?</span></div></div><div><div><div>2</div></div><div><span>trait</span><span> </span><span>Backend</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>source</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Self</span><span>::</span><span>Prepared</span><span>;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>struct</span><span> </span><span>TreeWalker</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>impl</span><span> </span><span>Backend</span><span> </span><span>for</span><span> </span><span>TreeWalker</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>// The tree-walker wants to keep borrowing the program it was given.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span><span> </span><span>=</span><span> &amp;</span></span><span>str</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>source</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>source</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>fn</span><span> </span><span>main</span><span>() {}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>--bin</span><span> </span><span>ch12_gat_missing</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: missing lifetime in associated type</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch12_gat_missing.rs:12:21</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>10 | impl Backend for TreeWalker {</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|     - you could add a lifetime on the impl block, if the trait or the self type could have one</span></div></div><div><div><div>6</div></div><div><span>11 |     // The tree-walker wants to keep borrowing the program it was given.</span></div></div><div><div><div>7</div></div><div><span>12 |     type Prepared = &amp;str;</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|                     ^ this lifetime must come from the implemented type</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>= note: in the trait the associated type is declared without lifetime parameters, so using a borrowed type for them requires that lifetime to come from the implemented type</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>error: could not compile `rust-lab` (bin "ch12_gat_missing") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器说得很直白：trait 里 <code>Prepared</code> 声明时没有生命周期参数，所以实现里的 <code>&amp;str</code> 的那个生命周期无处可来。关联类型是一个”每个实现者选一次”的类型，选的时候没有任何输入的生命周期可用。</p><p>解法是让关联类型自己带一个生命周期参数：<code>type Prepared&lt;'p&gt;;</code>。这就是 GAT。现在每次使用它的时候可以说”这一次借用多久”：<code>Self::Prepared&lt;'_&gt;</code>。<code>'_</code> 是第 4 篇的省略生命周期，规则也一样：输入里的借用决定输出里的生命周期。这个版本的实验（<code>~/rust-lab/src/bin/ch12_gat.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The working shape: a generic associated type, plus an associated const read</span></div></div><div><div><div>2</div></div><div><span>// through a type parameter.</span></div></div><div><div><div>3</div></div><div><span>trait</span><span> </span><span>Backend</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span>&gt;;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>source</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Self</span><span>::</span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(</span><span>prepared</span><span>: &amp;</span><span>Self</span><span>::</span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>usize</span><span>;</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>Borrowing</span><span>;</span></div></div><div><div><div>12</div></div><div><span>struct</span><span> </span><span>Owning</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>impl</span><span> </span><span>Backend</span><span> </span><span>for</span><span> </span><span>Borrowing</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"borrowing"</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span><span>&gt; </span><span>=</span><span> &amp;'</span></span><span>p</span><span> </span><span>str</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>source</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>source</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(</span><span>prepared</span><span><span>: </span><span>&amp;&amp;</span></span><span>str</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>prepared</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>impl</span><span> </span><span>Backend</span><span> </span><span>for</span><span> </span><span>Owning</span><span> {</span></div></div><div><div><div>28</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"owning"</span><span>;</span></div></div><div><div><div>29</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Vec</span><span>&lt;</span><span>char</span><span>&gt;;</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>source</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Vec</span><span>&lt;</span><span>char</span><span>&gt; {</span></div></div><div><div><div>32</div></div><div><span>        </span><span>source</span><span>.</span><span>chars</span><span>().</span><span>collect</span><span>()</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(</span><span>prepared</span><span>: &amp;</span><span>Vec</span><span>&lt;</span><span>char</span><span>&gt;) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>prepared</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>fn</span><span> </span><span>report</span><span>&lt;</span><span>B</span><span>: </span><span>Backend</span><span>&gt;(</span><span>source</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>41</div></div><div><span>    </span><span>let</span><span> </span><span>prepared</span><span><span> </span><span>=</span><span> </span></span><span>B</span><span>::</span><span>prepare</span><span>(</span><span>source</span><span>);</span></div></div><div><div><div>42</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:&lt;12} {}"</span><span>, </span><span>format!</span><span>(</span><span>"{}:"</span><span>, </span><span>B</span><span>::</span><span>NAME</span><span>), </span><span>B</span><span>::</span><span>run</span><span>(&amp;</span><span>prepared</span><span>));</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>46</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"pebble"</span><span>);</span></div></div><div><div><div>47</div></div><div><span>    </span><span>report</span><span>::&lt;</span><span>Borrowing</span><span>&gt;(&amp;</span><span>source</span><span>);</span></div></div><div><div><div>48</div></div><div><span>    </span><span>report</span><span>::&lt;</span><span>Owning</span><span>&gt;(&amp;</span><span>source</span><span>);</span></div></div><div><div><div>49</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch12_gat</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>borrowing:   6</span></div></div><div><div><div>2</div></div><div><span>owning:      6</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个实现，一个 <code>type Prepared&lt;'p&gt; = &amp;'p str;</code>（借用），一个 <code>type Prepared&lt;'p&gt; = Vec&lt;char&gt;;</code>（拥有，完全不用 <code>'p</code>）。同一个泛型函数 <code>report::&lt;B: Backend&gt;</code> 对两种都能用，而且读 <code>B::NAME</code> 时不需要任何值：关联常量可以通过<strong>类型参数</strong>直接读出来，<code>B::NAME</code> 在编译期就替换成对应类型的那个字符串。</p></section><section><h3>为什么不能 <code>Box&lt;dyn Backend&gt;</code><a href="#为什么不能-boxdyn-backend"><span>#</span></a></h3><p>既然 trait 摆好了，有人（比如过去的我）会想：干脆让 <code>run</code> 接受一个 <code>Box&lt;dyn Backend&gt;</code>，在运行时按 <code>--vm</code> 选哪个，不是更简单？我试了（<code>~/rust-lab/src/bin/ch12_object_safety.rs</code>，是 <code>Backend</code> 的缩小版，没有 Pebble 的类型）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A miniature of the `Backend` trait from pebble-cli, without the Pebble types.</span></div></div><div><div><div>2</div></div><div><span>trait</span><span> </span><span>Backend</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span>&gt;;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>source</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Self</span><span>::</span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>with_output</span><span>() -&gt; </span><span>Self</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>execute</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>prepared</span><span>: &amp;</span><span>Self</span><span>::</span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;;</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>Echo</span><span>;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>impl</span><span> </span><span>Backend</span><span> </span><span>for</span><span> </span><span>Echo</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"echo"</span><span>;</span></div></div><div><div><div>15</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span><span>&gt; </span><span>=</span><span> &amp;'</span></span><span>p</span><span> </span><span>str</span><span>;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>source</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>source</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>fn</span><span> </span><span>with_output</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>Echo</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span>fn</span><span> </span><span>execute</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>prepared</span><span><span>: </span><span>&amp;&amp;</span></span><span>str</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt; {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>Some</span><span>(</span><span>prepared</span><span>.</span><span>to_uppercase</span><span>())</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>31</div></div><div><span>    </span><span>let</span><span> </span><span>backend</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Backend</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Box</span><span>::</span><span>new</span><span>(</span><span>Echo</span><span>);</span></div></div><div><div><div>32</div></div><div><span>    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>backend</span><span>;</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>--bin</span><span> </span><span>ch12_object_safety</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0038]: the trait `Backend` is not dyn compatible</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch12_object_safety.rs:31:26</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>31 |     let backend: Box&lt;dyn Backend&gt; = Box::new(Echo);</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                          ^^^^^^^ `Backend` is not dyn compatible</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>note: for a trait to be dyn compatible it needs to allow building a vtable</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>for more information, visit &lt;https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility&gt;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch12_object_safety.rs:3:11</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>2 | trait Backend {</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|       ------- this trait is not dyn compatible...</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>3 |     const NAME: &amp;'static str;</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>|           ^^^^ ...because it contains associated const `NAME`</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>4 |     type Prepared&lt;'p&gt;;</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>|          ^^^^^^^^ ...because it contains generic associated type `Prepared`</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>5 |</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>6 |     fn prepare(source: &amp;str) -&gt; Self::Prepared&lt;'_&gt;;</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>|        ^^^^^^^ ...because associated function `prepare` has no `self` parameter</span></div></div><div><div><div>20</div></div><div><span><span> </span></span><span>7 |     fn with_output() -&gt; Self;</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>|        ^^^^^^^^^^^ ...because associated function `with_output` has no `self` parameter</span></div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>= help: consider moving `NAME` to another trait</span></div></div><div><div><div>23</div></div><div><span><span>   </span></span><span>= help: consider moving `Prepared` to another trait</span></div></div><div><div><div>24</div></div><div><span><span>   </span></span><span>= help: only type `Echo` implements `Backend`; consider using it directly instead.</span></div></div><div><div><div>25</div></div><div><span>help: consider turning `prepare` into a method by giving it a `&amp;self` argument</span></div></div><div><div><div>26</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>6 |     fn prepare(&amp;self, source: &amp;str) -&gt; Self::Prepared&lt;'_&gt;;</span></div></div><div><div><div>28</div></div><div><span><span>   </span></span><span>|                ++++++</span></div></div><div><div><div>29</div></div><div><span>help: alternatively, consider constraining `prepare` so it does not apply to trait objects</span></div></div><div><div><div>30</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>31</div></div><div><span><span> </span></span><span>6 |     fn prepare(source: &amp;str) -&gt; Self::Prepared&lt;'_&gt; where Self: Sized;</span></div></div><div><div><div>32</div></div><div><span><span>   </span></span><span>|                                                    +++++++++++++++++</span></div></div><div><div><div>33</div></div><div><span>help: consider turning `with_output` into a method by giving it a `&amp;self` argument</span></div></div><div><div><div>34</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>35</div></div><div><span><span> </span></span><span>7 |     fn with_output(&amp;self) -&gt; Self;</span></div></div><div><div><div>36</div></div><div><span><span>   </span></span><span>|                    +++++</span></div></div><div><div><div>37</div></div><div><span>help: alternatively, consider constraining `with_output` so it does not apply to trait objects</span></div></div><div><div><div>38</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>39</div></div><div><span><span> </span></span><span>7 |     fn with_output() -&gt; Self where Self: Sized;</span></div></div><div><div><div>40</div></div><div><span><span>   </span></span><span>|                              +++++++++++++++++</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>For more information about this error, try `rustc --explain E0038`.</span></div></div><div><div><div>43</div></div><div><span>error: could not compile `rust-lab` (bin "ch12_object_safety") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>E0038</code>：<code>Backend</code> 不是 <strong>dyn 兼容</strong>（dyn compatible）的。这个概念在旧资料里叫”对象安全”（object safety），第 8 篇我们提前预告过，新版编译器改了名字，说的是同一件事：要给 trait 造 vtable，trait 里的每一项都得能通过一个已经存在的 <code>self</code> 值去调用。看编译器列出的原因：</p><ul>
<li><strong>关联常量 <code>NAME</code></strong>：vtable 是”每个类型一份”的，而 <code>dyn Backend</code> 擦掉了具体类型，你没法对一个 <code>dyn Backend</code> 问 <code>NAME</code> 是什么，因为它不属于任何一个值。</li>
<li><strong>泛型关联类型 <code>Prepared</code></strong>：<code>dyn Backend</code> 这个类型要求把每个关联类型都固定下来，而 <code>Prepared&lt;'p&gt;</code> 随调用点的生命周期变化，没法固定。（编译器只说了”包含泛型关联类型”，这个解释是我自己的理解。）</li>
<li><strong>没有 <code>self</code> 的函数 <code>prepare</code> 和 <code>with_output</code></strong>：vtable 里的函数都是通过某个值去调用的，“构造函数”没有值可用。编译器给了一个逃生口：加 <code>where Self: Sized</code>，把这两个方法从 vtable 里排除。但关联常量和 GAT 没有这样的出口，编译器的建议是”把 <code>NAME</code> 移到另一个 trait 里”。</li>
</ul><p>所以这个 trait <strong>天生</strong>只能静态分发，这和我们前面做的选择是一致的，不是巧合：我们需要的那些东西（关联常量、关联类型、构造函数）全都是”在类型层面”的，而动态分发擦掉的恰好是类型。C++ 里同样如此：模板函数可以读 <code>T::name</code>，虚函数的基类指针不能。</p></section><section><h3>backend.rs：写出来<a href="#backendrs写出来"><span>#</span></a></h3><p>现在把它写成真的。新建 <code>crates/pebble-cli/src/backend.rs</code>，先是文件头文档、<code>use</code>、trait：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! One trait over both execution engines.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! `pebble run` and `pebble bench` do the same three things with either</span></div></div><div><div><div>4</div></div><div><span>//! engine: turn the AST into something executable, build a fresh engine, run</span></div></div><div><div><div>5</div></div><div><span>//! it. [`Backend`] names those three steps so the command code is written once</span></div></div><div><div><div>6</div></div><div><span>//! and instantiated per engine (static dispatch, no `dyn`).</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::</span></span><span>Program</span><span>;</span></div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>pebble_bytecode</span><span>::{</span></span><span>Compiled</span><span>, compile};</span></div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::</span></span><span>Diagnostic</span><span>;</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_interp</span><span>::</span></span><span>Interpreter</span><span>;</span></div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::{</span></span><span>Output</span><span>, </span><span>RuntimeError</span><span>, </span><span>Value</span><span>};</span></div></div><div><div><div>13</div></div><div><span>use</span><span><span> </span><span>pebble_vm</span><span>::{</span></span><span>Vm</span><span>, </span><span>VmValue</span><span>};</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>/// An execution engine for Pebble programs.</span></div></div><div><div><div>16</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>trait</span><span> </span><span>Backend</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// Label used by `pebble bench`.</span></div></div><div><div><div>18</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span>;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>/// What the engine executes: the borrowed AST for the tree-walker, owned</span></div></div><div><div><div>21</div></div><div><span>    </span><span>/// bytecode for the VM.</span></div></div><div><div><div>22</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span>&gt;;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>/// Turns a parsed program into the engine's input, or reports why it cannot.</span></div></div><div><div><div>25</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>::</span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;, </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;&gt;;</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span>/// Builds a fresh engine whose `print` goes to `out`.</span></div></div><div><div><div>28</div></div><div><span>    </span><span>fn</span><span> </span><span>with_output</span><span>(</span><span>out</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;) -&gt; </span><span>Self</span><span>;</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span>/// Runs the program; `Ok(None)` when its final value is `nil`.</span></div></div><div><div><div>31</div></div><div><span>    </span><span>fn</span><span> </span><span>execute</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>prepared</span><span>: &amp;</span><span>Self</span><span>::</span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>RuntimeError</span><span>&gt;;</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个签名值得多看一眼。<code>fn prepare(program: &amp;Program) -&gt; Result&lt;Self::Prepared&lt;'_&gt;, Vec&lt;Diagnostic&gt;&gt;;</code>：返回值里的 <code>'_</code> 是省略的生命周期，省略规则（第 4 篇的规则 2）把它连到唯一的输入引用 <code>program</code> 上，意思是”产物最多活得和这个 <code>&amp;Program</code> 一样久”。<code>fn with_output(out: Box&lt;dyn Output&gt;) -&gt; Self;</code> 是一个<strong>关联函数</strong>（没有 <code>self</code>），C++ 的静态工厂函数；这里 <code>Box&lt;dyn Output&gt;</code> 是我们愿意付费的动态分发：输出对象每次启动一次，调用的是 <code>write_line</code>，无关紧要。</p><p>然后是第一个引擎，树遍历：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The tree-walking interpreter.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>TreeWalker</span><span>(</span><span>Interpreter</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>impl</span><span> </span><span>Backend</span><span> </span><span>for</span><span> </span><span>TreeWalker</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"tree-walker"</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span><span>&gt; </span><span>=</span><span> &amp;'</span></span><span>p</span><span> </span><span>Program</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>Result</span><span>&lt;&amp;</span><span>Program</span><span>, </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Ok</span><span>(</span><span>program</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>with_output</span><span>(</span><span>out</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Self</span><span>(</span><span>Interpreter</span><span>::</span><span>with_output</span><span>(</span><span>out</span><span>))</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>fn</span><span> </span><span>execute</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>prepared</span><span><span>: </span><span>&amp;&amp;</span></span><span>Program</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>0.</span><span>interpret</span><span>(</span><span>prepared</span><span>)?;</span></div></div><div><div><div>18</div></div><div><span>        </span><span>// A script usually ends in a statement, so its value is `nil`;</span></div></div><div><div><div>19</div></div><div><span>        </span><span>// printing that for every program would just be noise.</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Ok</span><span><span>((</span><span>!</span></span><span>matches!</span><span>(</span><span>value</span><span>, </span><span>Value</span><span>::</span><span>Nil</span><span>)).</span><span>then</span><span><span>(</span><span>||</span><span> </span></span><span>value</span><span>.</span><span>to_string</span><span>()))</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pub(crate) struct TreeWalker(Interpreter);</code> 是第 1 篇的元组结构体新类型。两个包装类型不是必须的：trait 在这个 crate 里是本地的，孤儿规则（第 3 篇）允许直接 <code>impl Backend for Interpreter</code>。我选择包装，是为了让”后端”这个概念在命令行这个 crate 里有自己的名字，而不是借用引擎类型的名字。我没有试过另一种，不确定哪种更好。</p><p><code>type Prepared&lt;'p&gt; = &amp;'p Program;</code>：树遍历的”准备”什么也不做，直接借用。<code>fn prepare(program: &amp;Program) -&gt; Result&lt;&amp;Program, Vec&lt;Diagnostic&gt;&gt; { Ok(program) }</code>：实现里的签名可以比 trait 里写得更具体，<code>Self::Prepared&lt;'_&gt;</code> 就是 <code>&amp;Program</code>。<code>execute(&amp;mut self, prepared: &amp;&amp;Program)</code>：<code>&amp;Self::Prepared&lt;'_&gt;</code> 展开成 <code>&amp;&amp;Program</code>，一个指向指针的指针，C 程序员对 <code>**p</code> 不陌生。<code>self.0.interpret(prepared)?</code> 里的 <code>prepared</code> 是 <code>&amp;&amp;Program</code>，<code>interpret</code> 要 <code>&amp;Program</code>：第 5 篇的<strong>解引用强制转换</strong>把外层引用自动剥掉。</p><p>最后一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Ok</span><span><span>((</span><span>!</span></span><span>matches!</span><span>(</span><span>value</span><span>, </span><span>Value</span><span>::</span><span>Nil</span><span>)).</span><span>then</span><span><span>(</span><span>||</span><span> </span></span><span>value</span><span>.</span><span>to_string</span><span>()))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>bool::then</code> 是个小东西：<code>true.then(|| x)</code> 得到 <code>Some(x)</code>，<code>false.then(...)</code> 得到 <code>None</code>，闭包只在 <code>true</code> 时才求值。所以整行是”值不是 <code>nil</code> 才渲染成字符串”，等价于一个四行的 <code>if</code>/<code>else</code>。</p><p>第二个引擎，VM：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The bytecode compiler plus the stack VM.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>BytecodeVm</span><span>(</span><span>Vm</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>impl</span><span> </span><span>Backend</span><span> </span><span>for</span><span> </span><span>BytecodeVm</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>const</span><span> </span><span>NAME</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"vm"</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>type</span><span> </span><span>Prepared</span><span>&lt;'</span><span>p</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Compiled</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>prepare</span><span>(</span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Compiled</span><span>, </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>compile</span><span>(</span><span>program</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> </span><span>result</span><span>.diagnostics.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Ok</span><span>(</span><span>result</span><span>.compiled)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Err</span><span>(</span><span>result</span><span>.diagnostics)</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>with_output</span><span>(</span><span>out</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>Self</span><span>(</span><span>Vm</span><span>::</span><span>with_output</span><span>(</span><span>out</span><span>))</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>fn</span><span> </span><span>execute</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>prepared</span><span>: &amp;</span><span>Compiled</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>0.</span><span>run</span><span>(</span><span>prepared</span><span>)?;</span></div></div><div><div><div>23</div></div><div><span>        </span><span>Ok</span><span>((</span><span>value</span><span><span> </span><span>!=</span><span> </span></span><span>VmValue</span><span>::</span><span>Nil</span><span>).</span><span>then</span><span><span>(</span><span>||</span><span> </span></span><span>self</span><span>.</span><span>0.</span><span>display</span><span>(</span><span>value</span><span>)))</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>type Prepared&lt;'p&gt; = Compiled;</code>：VM 的产物拥有自己的数据，<code>'p</code> 根本没用上，这是合法的，GAT 的参数可以不用。<code>prepare</code> 调用第 10 篇的 <code>compile</code>，如果有诊断，把它们当 <code>Err</code> 返回，没有就交出字节码。<code>execute</code> 里 <code>(value != VmValue::Nil).then(|| self.0.display(value))</code>：这就是前面说的 <code>PartialEq</code> 的用处，要 <code>display</code> 是因为 VM 的值是个句柄，必须回到堆里才能渲染。</p><p>顺便说一件后面会用到的事：<code>Compiled</code> 里有 <code>Rc</code>（第 10 篇的 <code>Rc&lt;FunctionProto&gt;</code>），所以它不是 <code>Send</code>，不能被送到别的线程。第 14 篇我们做并行的 <code>check</code> 时会碰到它。</p></section><section><h3>commands.rs：写一次，实例化两次<a href="#commandsrs写一次实例化两次"><span>#</span></a></h3><p>打开 <code>crates/pebble-cli/src/commands.rs</code>。第四版把原来直接调用 <code>Interpreter</code> 的 <code>run</code> 换掉，新增 <code>bench</code>，加上三四个泛型小函数。先是文件头的 <code>use</code>：把 <code>use anyhow::Result;</code> 那一块换成（多了 <code>Duration</code>、<code>Instant</code>、<code>Program</code>、<code>Buffer</code>、<code>Stdout</code>、<code>Backend</code> 这几个，少了 <code>Interpreter</code> 和 <code>Value</code>，因为它们现在只在 <code>backend.rs</code> 里用）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>time</span><span>::{</span></span><span>Duration</span><span>, </span><span>Instant</span><span>};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>anyhow</span><span>::</span></span><span>Result</span><span>;</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::{</span></span><span>Program</span><span>, pretty};</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>pebble_bytecode</span><span>::compile;</span></span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>pebble_lexer</span><span>::tokenize;</span></span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>pebble_parser</span><span>::parse </span></span><span>as</span><span> parse_program;</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::{</span></span><span>Buffer</span><span>, </span><span>Stdout</span><span>};</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>backend</span><span>::{</span></span><span>Backend</span><span>, </span><span>BytecodeVm</span><span>, </span><span>TreeWalker</span><span>};</span></div></div><div><div><div>11</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>cli</span><span>::{</span></span><span>BenchArgs</span><span>, </span><span>FileArgs</span><span>, </span><span>RunArgs</span><span>};</span></div></div><div><div><div>12</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>sources</span><span>::{</span></span><span>self</span><span>, </span><span>Source</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后把 <code>run</code> 整个换成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// `pebble run`: parse a file, then execute it on one of the two backends.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// # Errors</span></div></div><div><div><div>4</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>run</span><span>(</span><span>args</span><span>: &amp;</span><span>RunArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map) {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>if</span><span> </span><span>args</span><span>.ast {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>pretty</span><span>(&amp;</span><span>parsed</span><span>.program));</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span> </span><span>args</span><span>.vm {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>prepared</span><span><span>) </span><span>=</span><span> </span></span><span>prepare_or_report</span><span>::&lt;</span><span>BytecodeVm</span><span>&gt;(&amp;</span><span>parsed</span><span>.program, &amp;</span><span>source</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>20</div></div><div><span>        </span><span>if</span><span> </span><span>args</span><span>.dump_bytecode {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>prepared</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>());</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>execute</span><span>::&lt;</span><span>BytecodeVm</span><span>&gt;(&amp;</span><span>prepared</span><span>, &amp;</span><span>source</span><span>, </span><span>args</span><span>.quiet));</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>if</span><span> </span><span>args</span><span>.dump_bytecode {</span></div></div><div><div><div>27</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>prepared</span><span><span>) </span><span>=</span><span> </span></span><span>prepare_or_report</span><span>::&lt;</span><span>BytecodeVm</span><span>&gt;(&amp;</span><span>parsed</span><span>.program, &amp;</span><span>source</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>30</div></div><div><span>        </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>prepared</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>());</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>    </span><span>let</span><span> </span><span>Some</span><span>(</span><span>prepared</span><span><span>) </span><span>=</span><span> </span></span><span>prepare_or_report</span><span>::&lt;</span><span>TreeWalker</span><span>&gt;(&amp;</span><span>parsed</span><span>.program, &amp;</span><span>source</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>33</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>35</div></div><div><span>    </span><span>Ok</span><span>(</span><span>execute</span><span>::&lt;</span><span>TreeWalker</span><span>&gt;(&amp;</span><span>prepared</span><span>, &amp;</span><span>source</span><span>, </span><span>args</span><span>.quiet))</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逻辑是：先解析，有错就报；<code>--ast</code> 先打印语法树；<code>--vm</code> 就走字节码，准备一次，要 <code>--dump-bytecode</code> 就打印反汇编，然后执行；没有 <code>--vm</code> 的话，如果给了 <code>--dump-bytecode</code>，<strong>只</strong>为了打印清单而编译一次，再走树遍历。每一条路径上编译最多发生一次。<code>prepared.chunk()</code> 是第 10 篇的 <code>Compiled::chunk</code>。</p><p><code>prepare_or_report::&lt;BytecodeVm&gt;(...)</code> 里那个 <code>::&lt;BytecodeVm&gt;</code> 是第 4 篇的 turbofish：函数的类型参数 <code>B</code> 无法从实参推断出来（实参是 <code>&amp;Program</code>，和 <code>B</code> 没关系），所以必须显式指定。<code>let Some(prepared) = ... else { return Ok(1); };</code> 是第 3 篇的 <code>let else</code>。</p><p><code>tokens</code>、<code>parse</code>、<code>check</code> 三个函数不用改。在 <code>check</code> 的后面追加新的一大段：<code>bench</code>，以及它和 <code>run</code> 共用的四个泛型函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// `pebble bench`: time both backends on the same file.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// # Errors</span></div></div><div><div><div>4</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>bench</span><span>(</span><span>args</span><span>: &amp;</span><span>BenchArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map) {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>Some</span><span>(</span><span>tree</span><span><span>) </span><span>=</span><span> </span></span><span>prepare_or_report</span><span>::&lt;</span><span>TreeWalker</span><span>&gt;(&amp;</span><span>parsed</span><span>.program, &amp;</span><span>source</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>Some</span><span>(</span><span>bytecode</span><span><span>) </span><span>=</span><span> </span></span><span>prepare_or_report</span><span>::&lt;</span><span>BytecodeVm</span><span>&gt;(&amp;</span><span>parsed</span><span>.program, &amp;</span><span>source</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>iterations</span><span><span> </span><span>=</span><span> </span></span><span>args</span><span>.iters.</span><span>max</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>Some</span><span>(</span><span>tree_walker</span><span><span>) </span><span>=</span><span> </span></span><span>time</span><span>::&lt;</span><span>TreeWalker</span><span>&gt;(&amp;</span><span>tree</span><span>, </span><span>iterations</span><span>, &amp;</span><span>source</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>Some</span><span>(</span><span>virtual_machine</span><span><span>) </span><span>=</span><span> </span></span><span>time</span><span>::&lt;</span><span>BytecodeVm</span><span>&gt;(&amp;</span><span>bytecode</span><span>, </span><span>iterations</span><span>, &amp;</span><span>source</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>report</span><span>::&lt;</span><span>TreeWalker</span><span>&gt;(</span><span>tree_walker</span><span>, </span><span>iterations</span><span>);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>report</span><span>::&lt;</span><span>BytecodeVm</span><span>&gt;(</span><span>virtual_machine</span><span>, </span><span>iterations</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>Ok</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>/// Prepares `program` for backend `B`, printing diagnostics on failure.</span></div></div><div><div><div>32</div></div><div><span>fn</span><span> </span><span>prepare_or_report</span><span>&lt;'</span><span>p</span><span>, </span><span>B</span><span>: </span><span>Backend</span><span>&gt;(</span></div></div><div><div><div>33</div></div><div><span>    </span><span>program</span><span>: &amp;'</span><span>p</span><span> </span><span>Program</span><span>,</span></div></div><div><div><div>34</div></div><div><span>    </span><span>source</span><span>: &amp;</span><span>Source</span><span>,</span></div></div><div><div><div>35</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>B</span><span>::</span><span>Prepared</span><span>&lt;'</span><span>p</span><span>&gt;&gt; {</span></div></div><div><div><div>36</div></div><div><span>    </span><span>match</span><span> </span><span>B</span><span>::</span><span>prepare</span><span>(</span><span>program</span><span>) {</span></div></div><div><div><div>37</div></div><div><span>        </span><span>Ok</span><span>(</span><span>prepared</span><span>) =&gt; </span><span>Some</span><span>(</span><span>prepared</span><span>),</span></div></div><div><div><div>38</div></div><div><span>        </span><span>Err</span><span>(</span><span>diagnostics</span><span>) =&gt; {</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>sources</span><span>::</span><span>emit</span><span>(&amp;</span><span>diagnostics</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>40</div></div><div><span>            </span><span>None</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>/// Runs a prepared program on a fresh `B` that prints to stdout, and returns</span></div></div><div><div><div>46</div></div><div><span>/// the exit code.</span></div></div><div><div><div>47</div></div><div><span>fn</span><span> </span><span>execute</span><span>&lt;</span><span>B</span><span>: </span><span>Backend</span><span>&gt;(</span><span>prepared</span><span><span>: &amp;</span><span>B</span><span>::</span></span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;, </span><span>source</span><span>: &amp;</span><span>Source</span><span>, </span><span>quiet</span><span>: </span><span>bool</span><span>) -&gt; </span><span>u8</span><span> {</span></div></div><div><div><div>48</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>backend</span><span><span> </span><span>=</span><span> </span></span><span>B</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>Stdout</span><span>));</span></div></div><div><div><div>49</div></div><div><span>    </span><span>match</span><span> </span><span>backend</span><span>.</span><span>execute</span><span>(</span><span>prepared</span><span>) {</span></div></div><div><div><div>50</div></div><div><span>        </span><span>Ok</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>51</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>text</span><span><span>) </span><span>=</span><span> </span></span><span>value</span></div></div><div><div><div>52</div></div><div><span><span>                </span></span><span>&amp;&amp;</span><span> </span><span>!</span><span>quiet</span></div></div><div><div><div>53</div></div><div><span><span>            </span></span><span>{</span></div></div><div><div><div>54</div></div><div><span>                </span><span>println!</span><span>(</span><span>"{text}"</span><span>);</span></div></div><div><div><div>55</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>56</div></div><div><span>            </span><span>0</span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>58</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; {</span></div></div><div><div><div>59</div></div><div><span><span>            </span></span><span>sources</span><span>::</span><span>emit_runtime</span><span>(&amp;</span><span>error</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>60</div></div><div><span>            </span><span>1</span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span>}</span></div></div><div><div><div>64</div></div><div>
</div></div><div><div><div>65</div></div><div><span>/// Runs a prepared program `iterations` times on fresh engines and returns the</span></div></div><div><div><div>66</div></div><div><span>/// total time, or `None` after reporting a runtime error.</span></div></div><div><div><div>67</div></div><div><span>///</span></div></div><div><div><div>68</div></div><div><span>/// The backends print their own output; a buffer keeps the timings readable.</span></div></div><div><div><div>69</div></div><div><span>fn</span><span> </span><span>time</span><span>&lt;</span><span>B</span><span>: </span><span>Backend</span><span>&gt;(</span></div></div><div><div><div>70</div></div><div><span>    </span><span>prepared</span><span><span>: &amp;</span><span>B</span><span>::</span></span><span>Prepared</span><span>&lt;'</span><span>_</span><span>&gt;,</span></div></div><div><div><div>71</div></div><div><span>    </span><span>iterations</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>72</div></div><div><span>    </span><span>source</span><span>: &amp;</span><span>Source</span><span>,</span></div></div><div><div><div>73</div></div><div><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Duration</span><span>&gt; {</span></div></div><div><div><div>74</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>75</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>iterations</span><span> {</span></div></div><div><div><div>76</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>backend</span><span><span> </span><span>=</span><span> </span></span><span>B</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>77</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Err</span><span>(</span><span>error</span><span><span>) </span><span>=</span><span> </span></span><span>backend</span><span>.</span><span>execute</span><span>(</span><span>prepared</span><span>) {</span></div></div><div><div><div>78</div></div><div><span><span>            </span></span><span>sources</span><span>::</span><span>emit_runtime</span><span>(&amp;</span><span>error</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>79</div></div><div><span>            </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>82</div></div><div><span>    </span><span>Some</span><span>(</span><span>start</span><span>.</span><span>elapsed</span><span>())</span></div></div><div><div><div>83</div></div><div><span>}</span></div></div><div><div><div>84</div></div><div>
</div></div><div><div><div>85</div></div><div><span>/// Prints one `pebble bench` result line.</span></div></div><div><div><div>86</div></div><div><span>fn</span><span> </span><span>report</span><span>&lt;</span><span>B</span><span>: </span><span>Backend</span><span>&gt;(</span><span>total</span><span>: </span><span>Duration</span><span>, </span><span>iterations</span><span>: </span><span>u32</span><span>) {</span></div></div><div><div><div>87</div></div><div><span>    </span><span>let</span><span> </span><span>total</span><span><span> </span><span>=</span><span> </span></span><span>millis</span><span>(</span><span>total</span><span>);</span></div></div><div><div><div>88</div></div><div><span>    </span><span>let</span><span> </span><span>per_iteration</span><span><span> </span><span>=</span><span> </span></span><span>total</span><span> / </span><span>f64</span><span>::</span><span>from</span><span>(</span><span>iterations</span><span>);</span></div></div><div><div><div>89</div></div><div><span>    </span><span>println!</span><span>(</span></div></div><div><div><div>90</div></div><div><span>        </span><span>"{:&lt;12} {total:.3} ms over {iterations} iterations ({per_iteration:.3} ms/iteration)"</span><span>,</span></div></div><div><div><div>91</div></div><div><span>        </span><span>format!</span><span>(</span><span>"{}:"</span><span>, </span><span>B</span><span>::</span><span>NAME</span><span>)</span></div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>93</div></div><div><span>}</span></div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span>/// A duration in milliseconds, as a float.</span></div></div><div><div><div>96</div></div><div><span>fn</span><span> </span><span>millis</span><span>(</span><span>duration</span><span>: </span><span>Duration</span><span>) -&gt; </span><span>f64</span><span> {</span></div></div><div><div><div>97</div></div><div><span>    </span><span>duration</span><span>.</span><span>as_secs_f64</span><span>() * </span><span>1000.0</span></div></div><div><div><div>98</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这四个泛型函数是整套设计的回报：</p><ul>
<li><code>prepare_or_report&lt;'p, B: Backend&gt;(program: &amp;'p Program, source: &amp;Source) -&gt; Option&lt;B::Prepared&lt;'p&gt;&gt;</code>：这里生命周期要显式写出来，因为函数有两个输入引用（<code>program</code> 和 <code>source</code>），省略规则没法替我们选。<code>'p</code> 把返回的 <code>Prepared</code> 和 <code>program</code> 连在一起，和 <code>source</code> 无关。</li>
<li><code>execute&lt;B: Backend&gt;(prepared: &amp;B::Prepared&lt;'_&gt;, ...)</code>：新建一个往标准输出打印的引擎，执行，渲染。<code>if let Some(text) = value &amp;&amp; !quiet</code> 是第 10 篇的 let-chain（edition 2024）。</li>
<li><code>time&lt;B&gt;</code>：每次迭代新建一个引擎，输出重定向到 <code>Buffer</code>，免得 <code>print</code> 污染计时。</li>
<li><code>report&lt;B&gt;</code>：<code>format!("{}:", B::NAME)</code> 先拼出标签，<code>{:&lt;12}</code> 左对齐到 12 格，这样 <code>"tree-walker:"</code> 恰好占满、<code>"vm:"</code> 后面补 9 个空格，两行的数字对齐。</li>
</ul><p><code>bench</code> 里调用的是 <code>time::&lt;TreeWalker&gt;(&amp;tree, iterations, &amp;source)</code>、<code>time::&lt;BytecodeVm&gt;(...)</code>。<strong>同一个函数体，编译器生成两份</strong>，一份内联 <code>TreeWalker</code> 的 <code>with_output</code>/<code>execute</code>，一份内联 <code>BytecodeVm</code> 的。<code>args.iters.max(1)</code> 防止 <code>--iters 0</code> 除以零（<code>f64::from(iterations)</code> 是从 <code>u32</code> 到 <code>f64</code> 的无损转换，第 8 篇的 <code>From</code>）。先把两个引擎都<strong>准备</strong>好再开始计时，所以编译的耗时不会混进 VM 的数字里，任何一个出错都在打印任何结果之前就退出。</p></section><section><h3>命令行第四版（下）：其余的改动<a href="#命令行第四版下其余的改动"><span>#</span></a></h3><p>剩下的改动都很小。<code>cli.rs</code>、<code>main.rs</code>、<code>Cargo.toml</code> 和测试，每个一两处：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -21,5 +21,5 @@</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>#[derive(Debug, Subcommand)]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>pub(crate) enum Command {</span></div></div><div><div><div>4</div></div><div><span>-    /// Execute a source file with the tree-walking interpreter.</span></div></div><div><div><div>5</div></div><div><span>+    /// Execute a source file on the tree-walker or the bytecode VM.</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>Run(RunArgs),</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>/// Start an interactive read-eval-print loop.</span></div></div><div><div><div>8</div></div><div><span>@@ -31,4 +31,6 @@</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>/// Lex and parse a source file without running it.</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>Check(FileArgs),</span></div></div><div><div><div>11</div></div><div><span>+    /// Time both backends on a source file.</span></div></div><div><div><div>12</div></div><div><span>+    Bench(BenchArgs),</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>@@ -45,4 +47,7 @@</span></div></div><div><div><div>16</div></div><div><span><span>     </span></span><span>/// The source file to execute.</span></div></div><div><div><div>17</div></div><div><span><span>     </span></span><span>pub(crate) file: PathBuf,</span></div></div><div><div><div>18</div></div><div><span>+    /// Execute on the bytecode VM instead of the tree-walking interpreter.</span></div></div><div><div><div>19</div></div><div><span>+    #[arg(long)]</span></div></div><div><div><div>20</div></div><div><span>+    pub(crate) vm: bool,</span></div></div><div><div><div>21</div></div><div><span><span>     </span></span><span>/// Print the parsed syntax tree before running.</span></div></div><div><div><div>22</div></div><div><span><span>     </span></span><span>#[arg(long)]</span></div></div><div><div><div>23</div></div><div><span>@@ -55,2 +60,12 @@</span></div></div><div><div><div>24</div></div><div><span><span>     </span></span><span>pub(crate) quiet: bool,</span></div></div><div><div><div>25</div></div><div><span><span> </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>+</span></div></div><div><div><div>27</div></div><div><span>+/// Arguments to `pebble bench`.</span></div></div><div><div><div>28</div></div><div><span>+#[derive(Debug, Args)]</span></div></div><div><div><div>29</div></div><div><span>+pub(crate) struct BenchArgs {</span></div></div><div><div><div>30</div></div><div><span>+    /// The source file to time.</span></div></div><div><div><div>31</div></div><div><span>+    pub(crate) file: PathBuf,</span></div></div><div><div><div>32</div></div><div><span>+    /// How many times to run the file on each backend.</span></div></div><div><div><div>33</div></div><div><span>+    #[arg(long, default_value_t = 5)]</span></div></div><div><div><div>34</div></div><div><span>+    pub(crate) iters: u32,</span></div></div><div><div><div>35</div></div><div><span>+}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>#[arg(long)] pub(crate) vm: bool</code> 让 clap 生成 <code>--vm</code> 开关。<code>#[arg(long, default_value_t = 5)]</code> 给 <code>--iters</code> 一个默认值。文档注释照旧变成帮助文字。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -2,6 +2,6 @@</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>//!</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>//! This binary is the front door to the toolchain: it wires the lexer, the</span></div></div><div><div><div>4</div></div><div><span>-//! parser, the interpreter, and the diagnostics renderer together behind a</span></div></div><div><div><div>5</div></div><div><span>-//! handful of subcommands.</span></div></div><div><div><div>6</div></div><div><span>+//! parser, both execution backends, and the diagnostics renderer together</span></div></div><div><div><div>7</div></div><div><span>+//! behind a handful of subcommands.</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>//!</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>//! Exit codes follow the usual convention: `0` for success, `1` when the input</span></div></div><div><div><div>10</div></div><div><span>@@ -11,4 +11,5 @@</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>+mod backend;</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>mod cli;</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>mod commands;</span></div></div><div><div><div>16</div></div><div><span>@@ -30,4 +31,5 @@</span></div></div><div><div><div>17</div></div><div><span><span>         </span></span><span>Command::Parse(args) =&gt; commands::parse(&amp;args),</span></div></div><div><div><div>18</div></div><div><span><span>         </span></span><span>Command::Check(args) =&gt; commands::check(&amp;args),</span></div></div><div><div><div>19</div></div><div><span>+        Command::Bench(args) =&gt; commands::bench(&amp;args),</span></div></div><div><div><div>20</div></div><div><span><span>     </span></span><span>};</span></div></div><div><div><div>21</div></div><div><span><span>     </span></span><span>match result {</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -22,4 +22,5 @@</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>pebble-interp.workspace = true</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>pebble-bytecode.workspace = true</span></div></div><div><div><div>4</div></div><div><span>+pebble-vm.workspace = true</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>anyhow.workspace = true</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>clap.workspace = true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试里新增一条，也是这一版最重要的测试：同一个文件两个后端各跑一次，stdout 必须逐字节相同：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -38,4 +38,17 @@</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span> </span></span><span>#[test]</span></div></div><div><div><div>4</div></div><div><span>+fn the_vm_and_the_interpreter_agree() {</span></div></div><div><div><div>5</div></div><div><span>+    let interpreted = pebble_output(&amp;["run", "examples/fib.pebble"]);</span></div></div><div><div><div>6</div></div><div><span>+    let compiled = pebble_output(&amp;["run", "--vm", "examples/fib.pebble"]);</span></div></div><div><div><div>7</div></div><div><span>+</span></div></div><div><div><div>8</div></div><div><span>+    assert!(</span></div></div><div><div><div>9</div></div><div><span>+        interpreted.status.success(),</span></div></div><div><div><div>10</div></div><div><span>+        "interpreter failed: {interpreted:?}"</span></div></div><div><div><div>11</div></div><div><span>+    );</span></div></div><div><div><div>12</div></div><div><span>+    assert!(compiled.status.success(), "vm failed: {compiled:?}");</span></div></div><div><div><div>13</div></div><div><span>+    assert_eq!(interpreted.stdout, compiled.stdout);</span></div></div><div><div><div>14</div></div><div><span>+}</span></div></div><div><div><div>15</div></div><div><span>+</span></div></div><div><div><div>16</div></div><div><span>+#[test]</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>fn run_reports_parse_errors_on_stderr() {</span></div></div><div><div><div>18</div></div><div><span><span>     </span></span><span>pebble()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>sources.rs</code> 和 <code>repl.rs</code> 没有变化。回到命令行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-cli</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.06s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/main.rs (target/debug/deps/pebble-f848d73f4f46c5f3)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 3 tests</span></div></div><div><div><div>6</div></div><div><span>test repl::tests::arguments_are_kept_whole ... ok</span></div></div><div><div><div>7</div></div><div><span>test repl::tests::quit_has_a_short_form ... ok</span></div></div><div><div><div>8</div></div><div><span>test repl::tests::unknown_commands_are_rejected ... ok</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>Running tests/cli.rs (target/debug/deps/cli-8a92629c34332c03)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>running 10 tests</span></div></div><div><div><div>15</div></div><div><span>test check_is_silent_on_a_valid_file ... ok</span></div></div><div><div><div>16</div></div><div><span>test parse_prints_the_syntax_tree ... ok</span></div></div><div><div><div>17</div></div><div><span>test quiet_suppresses_the_final_value ... ok</span></div></div><div><div><div>18</div></div><div><span>test run_reports_parse_errors_on_stderr ... ok</span></div></div><div><div><div>19</div></div><div><span>test check_reports_compile_errors ... ok</span></div></div><div><div><div>20</div></div><div><span>test tokens_prints_one_line_per_token ... ok</span></div></div><div><div><div>21</div></div><div><span>test unknown_subcommands_are_usage_errors ... ok</span></div></div><div><div><div>22</div></div><div><span>test run_evaluates_fibonacci ... ok</span></div></div><div><div><div>23</div></div><div><span>test dump_bytecode_prints_the_listing ... ok</span></div></div><div><div><div>24</div></div><div><span>test the_vm_and_the_interpreter_agree ... ok</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>3 个单元测试加 10 个集成测试，比第 10 篇多了一个 <code>the_vm_and_the_interpreter_agree</code>。用真正的二进制跑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run examples/fib.pebble</span></div></div><div><div><div>2</div></div><div><span>6765</span></div></div><div><div><div>3</div></div><div><span>$ pebble run --vm examples/fib.pebble</span></div></div><div><div><div>4</div></div><div><span>6765</span></div></div><div><div><div>5</div></div><div><span>$ pebble bench examples/fib.pebble --iters 20</span></div></div><div><div><div>6</div></div><div><span>tree-walker: 222.987 ms over 20 iterations (11.149 ms/iteration)</span></div></div><div><div><div>7</div></div><div><span>vm:          150.991 ms over 20 iterations (7.550 ms/iteration)</span></div></div><div><div><div>8</div></div><div><span>$ cargo run -q -p pebble-cli -- bench examples/fib.pebble --iters 20</span></div></div><div><div><div>9</div></div><div><span>tree-walker: 1016.535 ms over 20 iterations (50.827 ms/iteration)</span></div></div><div><div><div>10</div></div><div><span>vm:          619.305 ms over 20 iterations (30.965 ms/iteration)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个后端对 <code>fib(20)</code> 都打印 <code>6765</code>。<code>bench</code> 的两行标签对得整整齐齐，这是 <code>{:&lt;12}</code> 的功劳。数字本身是这一节我最想让你怀疑的东西：同样的命令（release 构建，20 遍），我刚才跑了一次，树遍历每遍 7.637 ms、VM 5.197 ms；上面这次是 11.149 ms 和 7.550 ms。同一台机器，同一个二进制，差了五成。两次的比例倒是稳定的（1.47 和 1.48），但绝对数字不能当真。debug 构建（最后两行）树遍历每遍 50.8 ms，VM 31.0 ms，慢了好几倍，这就是为什么基准一定要用 release。要拿到靠得住的数字，需要一个知道怎么对付噪声的工具，这是本篇后面 Criterion 的活。</p></section></section>
<section><h2>两个后端放在一起：pebble-bench<a href="#两个后端放在一起pebble-bench"><span>#</span></a></h2><p>现在有两个引擎了，一个自然的问题是它们对不对得上。第 10 篇的结尾我说过：“证明这些指令顺序正确要靠执行它们的测试。这也是为什么第 12 篇要写一套差分测试：同一个程序在两个后端跑，结果必须一致。“差分测试（differential testing）的想法很朴素：你不需要知道正确答案是什么，你只需要有两个应该给出相同答案的实现，然后让它们对着跑。一个出错的时候，两个通常不会错得一样。</p><p>这套测试和基准要共用同一批程序和同一组入口函数，所以放进一个新的 crate，叫 <code>pebble-bench</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-bench</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先把 <code>crates/pebble-bench/Cargo.toml</code> 换成下面这个，它还不完整（基准和差分测试用到的依赖我们到那里再加）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-bench"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Backend benchmarks: the tree-walking interpreter versus the bytecode VM."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div><span>publish</span><span> = </span><span>false</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>[</span><span>lib</span><span>]</span></div></div><div><div><div>13</div></div><div><span>bench</span><span> = </span><span>false</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>16</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>17</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>18</div></div><div><span>pebble-parser.workspace</span><span> = </span><span>true</span></div></div><div><div><div>19</div></div><div><span>pebble-value.workspace</span><span> = </span><span>true</span></div></div><div><div><div>20</div></div><div><span>pebble-interp.workspace</span><span> = </span><span>true</span></div></div><div><div><div>21</div></div><div><span>pebble-bytecode.workspace</span><span> = </span><span>true</span></div></div><div><div><div>22</div></div><div><span>pebble-vm.workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>文件的最后，和别的 crate 一样，要有 <code>[lints]</code> 一段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>2</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两行不寻常的设置。<code>publish = false</code>：这个 crate 永远不发布到 crates.io，写上它之后 <code>cargo publish</code> 会直接拒绝。<code>[lib] bench = false</code>：不让 <code>cargo bench</code> 把库本身当成基准目标去跑。后面我们会用 Criterion 自己的 <code>main</code>（<code>harness = false</code>），而 Criterion 的命令行参数（比如 <code>--save-baseline</code>）会被 Rust 默认的测试框架拒绝；官方文档建议给同一个包里的库目标关掉 <code>bench</code>，免得两个框架撞在一起。（我的机器上这个 crate 的 <code>cargo bench</code> 没有这个问题，是因为我从一开始就关了；我没有试过不关会怎样。）</p><p>现在是 <code>src/lib.rs</code>。文件头文档、<code>use</code>、三个基准程序：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div><span>//! Shared programs and helpers for benchmarking Pebble's two backends.</span></div></div><div><div><div>3</div></div><div><span>//!</span></div></div><div><div><div>4</div></div><div><span>//! The benchmark harness (`benches/backends.rs`) and the correctness test at</span></div></div><div><div><div>5</div></div><div><span>//! the bottom of this file both consume the same three programs and the same</span></div></div><div><div><div>6</div></div><div><span>//! entry points, so the numbers and the equivalence check can never drift</span></div></div><div><div><div>7</div></div><div><span>//! apart.</span></div></div><div><div><div>8</div></div><div><span>//!</span></div></div><div><div><div>9</div></div><div><span>//! Both backends write to an in-memory [`Buffer`] rather than standard output,</span></div></div><div><div><div>10</div></div><div><span>//! so a program that calls `print` cannot pollute the benchmark transcript.</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>pebble_interp</span><span>::</span></span><span>Interpreter</span><span>;</span></div></div><div><div><div>13</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>;</span></div></div><div><div><div>14</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>Buffer</span><span>;</span></div></div><div><div><div>15</div></div><div><span>use</span><span><span> </span><span>pebble_vm</span><span>::</span></span><span>Vm</span><span>;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>/// A recursive `fib(20)`: the classic call/depth benchmark.</span></div></div><div><div><div>18</div></div><div><span>pub</span><span> </span><span>const</span><span> </span><span>FIB</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>r#"</span></div></div><div><div><div>19</div></div><div><span>fn fib(n) {</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>if n &lt; 2 { return n; }</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>return fib(n - 1) + fib(n - 2);</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div><span>fib(20);</span></div></div><div><div><div>24</div></div><div><span>"#</span><span>;</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>/// Sums `0..100_000` with a `while` loop: raw statement/arithmetic throughput.</span></div></div><div><div><div>27</div></div><div><span>pub</span><span> </span><span>const</span><span> </span><span>LOOP_SUM</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>r#"</span></div></div><div><div><div>28</div></div><div><span>let i = 0;</span></div></div><div><div><div>29</div></div><div><span>let sum = 0;</span></div></div><div><div><div>30</div></div><div><span>while i &lt; 100000 {</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>sum = sum + i;</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>i = i + 1;</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div><div><div><div>34</div></div><div><span>sum;</span></div></div><div><div><div>35</div></div><div><span>"#</span><span>;</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>/// Builds a 1000-element list with `push`, then reads it back by index.</span></div></div><div><div><div>38</div></div><div><span>pub</span><span> </span><span>const</span><span> </span><span>COLLECTIONS</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>r#"</span></div></div><div><div><div>39</div></div><div><span>let xs = [];</span></div></div><div><div><div>40</div></div><div><span>let i = 0;</span></div></div><div><div><div>41</div></div><div><span>while i &lt; 1000 {</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>push(xs, i);</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>i = i + 1;</span></div></div><div><div><div>44</div></div><div><span>}</span></div></div><div><div><div>45</div></div><div><span>let total = 0;</span></div></div><div><div><div>46</div></div><div><span>let j = 0;</span></div></div><div><div><div>47</div></div><div><span>while j &lt; 1000 {</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>total = total + xs[j];</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>j = j + 1;</span></div></div><div><div><div>50</div></div><div><span>}</span></div></div><div><div><div>51</div></div><div><span>total;</span></div></div><div><div><div>52</div></div><div><span>"#</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个程序，三种负载。<code>FIB</code> 是递归调用（<code>fib(20)</code> 一共发生 21,891 次调用，后面会用到这个数字）。<code>LOOP_SUM</code> 是十万次 <code>while</code> 循环，每次三条语句：测”语句和算术的吞吐”。<code>COLLECTIONS</code> 往列表里 <code>push</code> 一千个元素，再按下标读回来：测分配和下标。</p><p><code>pub const FIB: &amp;str = r#"..."#;</code> 里的 <code>r#"</code> 是<strong>原始字符串字面量</strong>：里面的反斜杠和引号都不转义，不需要 <code>\"</code>，以 <code>"#</code> 结束，所以字符串里即使出现 <code>"</code> 也不会提前结束。C++11 也有 <code>R"(...)"</code>，是同一回事。</p><p>接着是三个入口。把程序编译成字节码：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Parses and compiles `src` to bytecode, panicking on any diagnostic.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// # Panics</span></div></div><div><div><div>4</div></div><div><span>/// Panics with the rendered diagnostics if lexing, parsing, or compilation</span></div></div><div><div><div>5</div></div><div><span>/// reports an error. These programs are fixed constants, so a panic here means</span></div></div><div><div><div>6</div></div><div><span>/// the language or a backend changed incompatibly.</span></div></div><div><div><div>7</div></div><div><span>#[must_use]</span></div></div><div><div><div>8</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>compile_source</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span><span>) -&gt; </span><span>pebble_bytecode</span><span>::</span></span><span>Compiled</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"&lt;bench&gt;"</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span><span>pebble_parser</span><span>::</span></span><span>parse</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>if</span><span><span> </span><span>!</span></span><span>parsed</span><span>.diagnostics.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>panic_diagnostics</span><span>(&amp;</span><span>sources</span><span>, &amp;</span><span>parsed</span><span>.diagnostics);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span><span>pebble_bytecode</span><span>::</span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>if</span><span><span> </span><span>!</span></span><span>result</span><span>.diagnostics.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>panic_diagnostics</span><span>(&amp;</span><span>sources</span><span>, &amp;</span><span>result</span><span>.diagnostics);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>    </span><span>result</span><span>.compiled</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code># Panics</code> 这一节是 Rust 文档的惯例（clippy 的 pedantic 组里有一条 <code>missing_panics_doc</code> 专门检查它，我们没开 pedantic，靠自觉）：只要函数可能 panic，就写下它在什么条件下 panic。这里 panic 是对的：这些程序是写死的常量，出了诊断说明语言或编译器被改坏了，而基准代码没有任何合理的方式”处理”这件事。</p><p><code>#[must_use]</code> 我们在第 1 篇见过：忽略返回值几乎一定是个 bug。解释器和 VM 的入口：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Runs `src` on the tree-walking interpreter and returns the `Display` of the</span></div></div><div><div><div>2</div></div><div><span>/// program's final value.</span></div></div><div><div><div>3</div></div><div><span>///</span></div></div><div><div><div>4</div></div><div><span>/// # Panics</span></div></div><div><div><div>5</div></div><div><span>/// Panics on a parse or runtime error.</span></div></div><div><div><div>6</div></div><div><span>#[must_use]</span></div></div><div><div><div>7</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>run_interp</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"&lt;bench&gt;"</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span><span>pebble_parser</span><span>::</span></span><span>parse</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span><span> </span><span>!</span></span><span>parsed</span><span>.diagnostics.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>panic_diagnostics</span><span>(&amp;</span><span>sources</span><span>, &amp;</span><span>parsed</span><span>.diagnostics);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>interp</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>interp</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>.</span><span>interpret</span><span>(&amp;</span><span>parsed</span><span>.program)</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>.</span><span>unwrap_or_else</span><span><span>(</span><span>|</span></span><span>error</span><span><span>|</span><span> </span></span><span>panic!</span><span>(</span><span>"interpreter error: {error}"</span><span>));</span></div></div><div><div><div>18</div></div><div><span>    </span><span>value</span><span>.</span><span>to_string</span><span>()</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>/// Runs `src` on the bytecode VM and returns the rendered final value.</span></div></div><div><div><div>22</div></div><div><span>///</span></div></div><div><div><div>23</div></div><div><span>/// # Panics</span></div></div><div><div><div>24</div></div><div><span>/// Panics on a parse, compile, or runtime error.</span></div></div><div><div><div>25</div></div><div><span>#[must_use]</span></div></div><div><div><div>26</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>run_vm</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_source</span><span>(</span><span>src</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>vm</span><span><span> </span><span>=</span><span> </span></span><span>Vm</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>29</div></div><div><span>    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>vm</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>.</span><span>run</span><span>(&amp;</span><span>compiled</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>.</span><span>unwrap_or_else</span><span><span>(</span><span>|</span></span><span>error</span><span><span>|</span><span> </span></span><span>panic!</span><span>(</span><span>"vm error: {error}"</span><span>));</span></div></div><div><div><div>32</div></div><div><span>    </span><span>vm</span><span>.</span><span>display</span><span>(</span><span>value</span><span>)</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个函数返回的都是 <code>String</code>，是最终值的<strong>渲染结果</strong>，不是 <code>Value</code> 也不是 <code>VmValue</code>。原因是这两种值类型根本不同：一个是 <code>Rc&lt;RefCell&lt;..&gt;&gt;</code> 构成的树，一个是堆句柄，没有可比较的公共类型。比较渲染后的字符串是最朴素的公共标准，也是整个差分测试里最弱的一环，下面”两个后端不一致”一节会回到这件事上。</p><p>两个后端都写入 <code>Buffer</code> 而不是标准输出，所以程序里的 <code>print</code> 不会弄脏基准的输出。<code>unwrap_or_else(|error| panic!("interpreter error: {error}"))</code> 是 <code>unwrap_or_else</code> 接一个<strong>从不返回的闭包</strong>：<code>panic!</code> 的类型是 <code>!</code>（never 类型），它能转成任何类型，所以闭包的返回类型可以是 <code>Value</code>。</p><p>最后一个辅助函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>panic_diagnostics</span><span>(</span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>, </span><span>diagnostics</span><span><span>: &amp;[</span><span>pebble_diag</span><span>::</span></span><span>Diagnostic</span><span><span>]) -&gt; </span><span>!</span><span> {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>diagnostics</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>diagnostic</span><span><span>|</span><span> </span></span><span>diagnostic</span><span>.</span><span>render</span><span>(</span><span>sources</span><span>))</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>.</span><span>collect</span><span>::&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;()</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>.</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>panic!</span><span>(</span><span>"pebble diagnostics:</span><span>\n</span><span>{rendered}"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-&gt; !</code> 是函数签名里的 never 类型：这个函数永远不返回。因为所有调用它的地方都是 <code>if ... { panic_diagnostics(..); }</code> 之后接着往下写，编译器知道那条路径没有后续，不需要伪造一个返回值。<code>diagnostics.iter().map(..).collect::&lt;Vec&lt;_&gt;&gt;().join("\n")</code> 是第 3 篇的 <code>render</code>。</p><p>测试：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::{</span><span>COLLECTIONS</span><span>, </span><span>FIB</span><span>, </span><span>LOOP_SUM</span><span>, run_interp, run_vm};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>both_backends_agree</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>for</span><span> (</span><span>name</span><span>, </span><span>src</span><span>) </span><span>in</span><span> [</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>(</span><span>"fib"</span><span>, </span><span>FIB</span><span>),</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>(</span><span>"loop_sum"</span><span>, </span><span>LOOP_SUM</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>(</span><span>"collections"</span><span>, </span><span>COLLECTIONS</span><span>),</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>] {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>13</div></div><div><span>                </span><span>run_interp</span><span>(</span><span>src</span><span>),</span></div></div><div><div><div>14</div></div><div><span>                </span><span>run_vm</span><span>(</span><span>src</span><span>),</span></div></div><div><div><div>15</div></div><div><span>                </span><span>"backends disagree on `{name}`"</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>21</div></div><div><span>    </span><span>fn</span><span> </span><span>programs_produce_the_expected_values</span><span>() {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run_interp</span><span>(</span><span>FIB</span><span>), </span><span>"6765"</span><span>);</span></div></div><div><div><div>23</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run_vm</span><span>(</span><span>FIB</span><span>), </span><span>"6765"</span><span>);</span></div></div><div><div><div>24</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run_interp</span><span>(</span><span>LOOP_SUM</span><span>), </span><span>"4999950000"</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run_vm</span><span>(</span><span>LOOP_SUM</span><span>), </span><span>"4999950000"</span><span>);</span></div></div><div><div><div>26</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run_interp</span><span>(</span><span>COLLECTIONS</span><span>), </span><span>"499500"</span><span>);</span></div></div><div><div><div>27</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run_vm</span><span>(</span><span>COLLECTIONS</span><span>), </span><span>"499500"</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第一个测试遍历三个程序并断言两个后端的渲染一致；第二个把三个期望值写死（<code>6765</code>、<code>4999950000</code>、<code>499500</code>），这样”两个后端一致但都错了”也会被抓住。<code>[(..), (..), (..)]</code> 是一个元组数组，<code>for (name, src) in [...]</code> 直接解构，第 4 篇的老技巧。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-bench</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.63s</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (/tmp/ch12-stage-target/debug/deps/pebble_bench-951f0937881af549)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>running 2 tests</span></div></div><div><div><div>5</div></div><div><span>test tests::programs_produce_the_expected_values ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::both_backends_agree ... ok</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.44s</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>Doc-tests pebble_bench</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>running 0 tests</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>属性宏：<code>#[differential]</code><a href="#属性宏differential"><span>#</span></a></h2><p>基准程序只有三个，而差分测试想要的是<strong>很多个</strong>程序，每个都是独立的、名字有意义的测试，失败时能一眼看出是哪一个。最直接的写法是每个程序手写一个测试函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>closures_keep_private_state_backends_agree</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>r#"..."#</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>run_interp</span><span>(</span><span>src</span><span>), </span><span>run_vm</span><span>(</span><span>src</span><span>), </span><span>"backends disagree on `closures_keep_private_state`"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每个都是同样的三行样板，改名字的时候三处都要改，比如函数名里的 <code>closures_keep_private_state</code>、断言消息里的 <code>closures_keep_private_state</code>。这是宏的典型用法。但我们想要的写法其实是：<strong>一个返回程序文本的普通函数</strong>，加一个标注，别的都不管：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[differential]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>closures_keep_private_state</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>r#"..."#</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个形状天然是<strong>属性宏</strong>（attribute macro）：它附在一个项上，拿到这个项，吐出替换它的东西。前面我们已经见过两种过程宏，第 6 篇的<strong>派生宏</strong>（<code>#[derive(Spanned)]</code>，给结构体追加一个 <code>impl</code>）和第 10 篇的<strong>函数式宏</strong>（<code>opcode_set! { .. }</code>，像函数一样调用，吐出一个枚举）。属性宏是第三种，也是最灵活的一种：派生宏只能<strong>追加</strong>，属性宏可以<strong>替换</strong>整个项。</p><p>属性宏的签名有<strong>两个</strong>输入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[proc_macro_attribute]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>differential</span><span>(</span><span>attr</span><span>: </span><span>TokenStream</span><span>, </span><span>item</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>attr</code> 是属性括号里的内容（<code>#[differential(fast)]</code> 里的 <code>fast</code>；<code>#[differential]</code> 时为空），<code>item</code> 是它附着的整个项（我们的函数）。返回值<strong>替换</strong> <code>item</code>：所以如果你想保留原来的函数，必须自己把它再吐出来。这一点是属性宏和派生宏最大的差别，也是新手最容易漏的地方。</p><section><h3>写它<a href="#写它"><span>#</span></a></h3><p>在 <code>crates/pebble-macros/src/lib.rs</code> 里做三处改动。第一处是文件开头的 crate 文档：现在有三种宏了。把文件开头那 13 行 <code>//!</code> 文档（第 10 篇的版本，以括号注释 “(A third, <strong>declarative</strong> kind …” 的最后一行结尾；不含后面的空行和 <code>use proc_macro::TokenStream;</code>）换成下面这 15 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Procedural macros for the Pebble toolchain.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! This crate must be a `proc-macro = true` crate: procedural macros run at</span></div></div><div><div><div>4</div></div><div><span>//! compile time, in the compiler's process, and speak in tokens. Three flavours</span></div></div><div><div><div>5</div></div><div><span>//! live here:</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! * [`macro@Spanned`] - a **derive** macro that writes the boring</span></div></div><div><div><div>8</div></div><div><span>//!   `impl pebble_span::Spanned` for AST nodes.</span></div></div><div><div><div>9</div></div><div><span>//! * [`opcode_set`] - a **function-like** macro that turns a declarative list of</span></div></div><div><div><div>10</div></div><div><span>//!   opcodes into an enum plus a `name()` table.</span></div></div><div><div><div>11</div></div><div><span>//! * [`differential`] - an **attribute** macro that turns a function returning a</span></div></div><div><div><div>12</div></div><div><span>//!   Pebble program into a test comparing both backends.</span></div></div><div><div><div>13</div></div><div><span>//!</span></div></div><div><div><div>14</div></div><div><span>//! (A fourth, **declarative** kind, `macro_rules!`, lives next to the code</span></div></div><div><div><div>15</div></div><div><span>//! that uses it in `pebble-bytecode`.)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（对比第 10 篇的版本：“So far two flavours” 变成了 “Three flavours”，列表里多了 <code>differential</code> 一项，最后的 “A third” 变成 “A fourth”。）第二处，<code>use</code> 行里多拿一个 <code>ItemFn</code>（<code>syn</code> 里”一个函数项”的类型），把原来的 <code>use syn::{...}</code> 那行换成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>syn</span><span>::{</span></span><span>Data</span><span>, </span><span>DeriveInput</span><span>, </span><span>Fields</span><span>, </span><span>ItemEnum</span><span>, </span><span>ItemFn</span><span>, parse_macro_input};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第三处：在文件末尾（<code>expand_opcodes</code> 的右花括号之后，空一行）追加属性宏本体：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Turns a function that returns a Pebble program into a test that runs it on both backends and compares the rendered results.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// ```ignore</span></div></div><div><div><div>4</div></div><div><span>/// #[differential]</span></div></div><div><div><div>5</div></div><div><span>/// fn sums() -&gt; &amp;'static str {</span></div></div><div><div><div>6</div></div><div><span>///     "1 + 2;"</span></div></div><div><div><div>7</div></div><div><span>/// }</span></div></div><div><div><div>8</div></div><div><span>/// ```</span></div></div><div><div><div>9</div></div><div><span>///</span></div></div><div><div><div>10</div></div><div><span>/// expands to the function itself plus a `#[test] fn sums_backends_agree()`</span></div></div><div><div><div>11</div></div><div><span>/// that feeds the program to `pebble_bench::run_interp` and</span></div></div><div><div><div>12</div></div><div><span>/// `pebble_bench::run_vm` and asserts the two renderings are equal.</span></div></div><div><div><div>13</div></div><div><span>#[proc_macro_attribute]</span></div></div><div><div><div>14</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>differential</span><span>(</span><span>attr</span><span>: </span><span>TokenStream</span><span>, </span><span>item</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>attr</span><span><span> </span><span>=</span><span> </span></span><span>TokenStream2</span><span>::</span><span>from</span><span>(</span><span>attr</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>function</span><span><span> </span><span>=</span><span> </span></span><span>parse_macro_input!</span><span>(</span><span>item</span><span> </span><span>as</span><span> </span><span>ItemFn</span><span>);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>match</span><span> </span><span>expand_differential</span><span>(&amp;</span><span>attr</span><span>, &amp;</span><span>function</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>Ok</span><span>(</span><span>tokens</span><span>) =&gt; </span><span>tokens</span><span>.</span><span>into</span><span>(),</span></div></div><div><div><div>19</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; </span><span>error</span><span>.</span><span>to_compile_error</span><span>().</span><span>into</span><span>(),</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>fn</span><span> </span><span>expand_differential</span><span>(</span><span>attr</span><span>: &amp;</span><span>TokenStream2</span><span>, </span><span>function</span><span>: &amp;</span><span>ItemFn</span><span><span>) -&gt; </span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>TokenStream2</span><span>&gt; {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>if</span><span><span> </span><span>!</span></span><span>attr</span><span>.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span><span>(</span><span>syn</span><span>::</span></span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>26</div></div><div><span>            </span><span>attr</span><span>,</span></div></div><div><div><div>27</div></div><div><span>            </span><span>"`#[differential]` takes no arguments"</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>));</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>    </span><span>if</span><span><span> </span><span>!</span></span><span>function</span><span>.sig.inputs.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>31</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span><span>(</span><span>syn</span><span>::</span></span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>&amp;</span><span>function</span><span>.sig.inputs,</span></div></div><div><div><div>33</div></div><div><span>            </span><span>"a `#[differential]` program takes no parameters"</span><span>,</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>));</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>function</span><span>.sig.ident;</span></div></div><div><div><div>38</div></div><div><span>    </span><span>let</span><span> </span><span>test_name</span><span><span> </span><span>=</span><span> </span><span>quote</span><span>::</span></span><span>format_ident!</span><span>(</span><span>"{}_backends_agree"</span><span>, </span><span>name</span><span>);</span></div></div><div><div><div>39</div></div><div><span>    </span><span>let</span><span> </span><span>label</span><span><span> </span><span>=</span><span> </span></span><span>name</span><span>.</span><span>to_string</span><span>();</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span>Ok</span><span>(</span><span>quote!</span><span> {</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>#</span><span>function</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>#[test]</span></div></div><div><div><div>45</div></div><div><span>        </span><span>fn</span><span> #</span><span>test_name</span><span>() {</span></div></div><div><div><div>46</div></div><div><span>            </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> #</span></span><span>name</span><span>();</span></div></div><div><div><div>47</div></div><div><span>            </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>48</div></div><div><span><span>                </span></span><span>::</span><span>pebble_bench</span><span>::</span><span>run_interp</span><span>(</span><span>src</span><span>),</span></div></div><div><div><div>49</div></div><div><span><span>                </span></span><span>::</span><span>pebble_bench</span><span>::</span><span>run_vm</span><span>(</span><span>src</span><span>),</span></div></div><div><div><div>50</div></div><div><span>                </span><span>"backends disagree on `{}`"</span><span>,</span></div></div><div><div><div>51</div></div><div><span><span>                </span></span><span>#</span><span>label</span></div></div><div><div><div>52</div></div><div><span><span>            </span></span><span>);</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐块看。入口函数 <code>differential</code> 和第 6 篇的 <code>derive_spanned</code> 完全同一个形状：把 <code>proc_macro::TokenStream</code> 转成 <code>proc_macro2::TokenStream</code>，用 <code>parse_macro_input!(item as ItemFn)</code> 把 <code>item</code> 解析成一个 <code>syn::ItemFn</code>（解析失败时这个宏会自己返回一个编译错误，我们不用管），然后把真正的活交给一个返回 <code>syn::Result&lt;TokenStream2&gt;</code> 的普通函数 <code>expand_differential</code>，出错就 <code>error.to_compile_error().into()</code>。这个两层结构我们在第 6 篇解释过：真正干活的函数不依赖 <code>proc_macro</code> 这个只能在宏里用的类型，将来可以单独测试。</p><p><code>expand_differential</code> 先做两个检查。<code>attr</code> 不空，说明用户写了 <code>#[differential(fast)]</code> 之类，这个宏不接受参数。<code>function.sig.inputs</code> 不空，说明函数带了参数，我们没法给它提供实参。两种情况都用 <code>syn::Error::new_spanned(..., "...")</code> 造一个错误，<strong>错误指向用户写的那几个 token</strong>。</p><p>我想试一下两种错用，这需要 <code>pebble-bench</code> 的测试能用到 <code>pebble-macros</code>。先给 <code>crates/pebble-bench/Cargo.toml</code> 补一段 <code>[dev-dependencies]</code>，放在 <code>[dependencies]</code> 和 <code>[lints]</code> 之间。<code>pebble-macros</code> 是后面这些实验和差分套件要用的；<code>criterion</code> 是再下一节的基准要用的，顺手一起写上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>2</div></div><div><span>criterion.workspace</span><span> = </span><span>true</span></div></div><div><div><div>3</div></div><div><span>pebble-macros.workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后把下面的文件放进一个临时测试文件，跑完删掉：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_macros</span><span>::differential;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[differential(fast)]</span></div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>a</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"1;"</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#[differential]</span></div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>b</span><span>(</span><span>x</span><span>: </span><span>u32</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"2;"</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: `#[differential]` takes no arguments</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; crates/pebble-bench/tests/scratch.rs:3:16</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>3 | #[differential(fast)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                ^^^^</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>error: a `#[differential]` program takes no parameters</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>--&gt; crates/pebble-bench/tests/scratch.rs:9:6</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>9 | fn b(x: u32) -&gt; &amp;'static str {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|      ^^^^^^</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>error: could not compile `pebble-bench` (test "scratch") due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个错误，各自精确地指在出错的 token 上（<code>fast</code>，和 <code>x: u32</code>），一次编译同时报告。这就是 <code>new_spanned</code> 的价值：你的宏的错误信息和 rustc 自己的错误一样好用。</p><p>检查通过之后，真正的展开是一个 <code>quote!</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>function</span><span>.sig.ident;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>test_name</span><span><span> </span><span>=</span><span> </span><span>quote</span><span>::</span></span><span>format_ident!</span><span>(</span><span>"{}_backends_agree"</span><span>, </span><span>name</span><span>);</span></div></div><div><div><div>3</div></div><div><span>let</span><span> </span><span>label</span><span><span> </span><span>=</span><span> </span></span><span>name</span><span>.</span><span>to_string</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>format_ident!</code> 用格式字符串<strong>拼一个标识符</strong>：<code>closures_keep_private_state</code> 变成 <code>closures_keep_private_state_backends_agree</code>。这是普通字符串拼接做不到的事，因为 <code>quote!</code> 里的 <code>#name</code> 插入的是 token，不是文本。<code>label</code> 是 <code>String</code>，在 <code>quote!</code> 里插进去会变成一个字符串字面量，用来在断言消息里点名。</p><p>然后是 <code>quote! { #function  #[test] fn #test_name() { ... } }</code>。第一项 <code>#function</code> 把原来那个函数原样吐出来（<code>ItemFn</code> 实现了 <code>ToTokens</code>，所以可以直接插值）：这就是上面说的”属性宏必须自己把原来的项再吐出来”。第二项是新增的测试函数。<strong>路径写成 <code>::pebble_bench::run_interp</code>，前面有两个冒号</strong>：宏生成的代码落在使用者的 crate 里，那里的名字解析不归我们管。<code>::</code> 开头的路径是”从 crate 根（外部 crate 名）开始”，不会被使用者在作用域里碰巧定义的同名东西抢走。</p><p>注意这个宏的 crate 里<strong>没有</strong> <code>pebble-bench</code> 这个依赖，生成的代码只是<strong>提到</strong>它的名字。<code>pebble-bench</code> 的测试依赖 <code>pebble-macros</code>，宏的展开结果又引用 <code>pebble-bench</code>，如果宏 crate 反过来也依赖 <code>pebble-bench</code>，两边就互相依赖了。我没有试 Cargo 会怎么处理这种环，也没有必要试：生成的代码只需要<strong>提到</strong>名字，不需要依赖。这个名字能解析，是因为展开后的代码落在 <code>pebble-bench</code> 自己的集成测试里，那里 <code>pebble_bench</code> 恰好就是被测的库。别的 crate 想用这个宏，必须自己也能引用到 <code>pebble_bench</code>。这是属性宏常见的一种隐含契约，写在 <code># Examples</code> 的文档里比什么都有用。</p><p>最后是 <code>Cargo.toml</code>：宏 crate 的描述改成”derive + attribute”，并多一段 <code>[dev-dependencies]</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -1,5 +1,5 @@</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>[package]</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>name = "pebble-macros"</span></div></div><div><div><div>4</div></div><div><span>-description = "Procedural macros (derive only, so far) used across the Pebble crates."</span></div></div><div><div><div>5</div></div><div><span>+description = "Procedural macros (derive + attribute) used across the Pebble crates."</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>version.workspace = true</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>edition.workspace = true</span></div></div><div><div><div>8</div></div><div><span>@@ -17,4 +17,7 @@</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>syn.workspace = true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>+[dev-dependencies]</span></div></div><div><div><div>12</div></div><div><span>+pebble-span.workspace = true</span></div></div><div><div><div>13</div></div><div><span>+</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>[lints]</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>workspace = true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pebble-span</code> 那一行是 tag 里就有的，我们没用到它（宏 crate 里的 <code>ignore</code> 文档示例不会被编译），留着只是为了和 tag 逐字节相同。</p></section><section><h3>看看它展开成什么<a href="#看看它展开成什么"><span>#</span></a></h3><p>宏最大的恐惧是”我不知道它到底生成了什么”。稳定版 rustc 没有 <code>cargo expand</code>，但有一个取巧的办法：<code>RUSTC_BOOTSTRAP=1</code> 让稳定版编译器接受 nightly 的 <code>-Zunpretty=expanded</code>。（这个环境变量本来是给 rustc 自己的开发者用的，拿来看展开是个好用的灰色手段，不要放进 CI。）先写一个最小的测试文件 <code>crates/pebble-bench/tests/scratch.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_macros</span><span>::differential;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[differential]</span></div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>sums</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"1 + 2;"</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>RUSTC_BOOTSTRAP</span><span>=</span><span>1</span><span> </span><span>cargo</span><span> </span><span>rustc</span><span> </span><span>-p</span><span> </span><span>pebble-bench</span><span> </span><span>--test</span><span> </span><span>scratch</span><span> </span><span>--</span><span> </span><span>-Zunpretty=expanded</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出里（我删掉了和宏无关的部分，保留的行都是原样的）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn sums() -&gt; &amp;'static str { "1 + 2;" }</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>fn sums_backends_agree() {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>let src = sums();</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>match (&amp;::pebble_bench::run_interp(src), &amp;::pebble_bench::run_vm(src)) {</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>(left_val, right_val) =&gt; {</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>if !(*left_val == *right_val) {</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>let kind = ::core::panicking::AssertKind::Eq;</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>::core::panicking::assert_failed(kind, &amp;*left_val,</span></div></div><div><div><div>10</div></div><div><span><span>                    </span></span><span>&amp;*right_val,</span></div></div><div><div><div>11</div></div><div><span><span>                    </span></span><span>::core::option::Option::Some(format_args!("backends disagree on `{0}`",</span></div></div><div><div><div>12</div></div><div><span><span>                            </span></span><span>"sums")));</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第一行 <code>fn sums()</code> 是我们原样吐回的函数；最后一个函数就是 <code>quote!</code> 里的那段，<code>assert_eq!</code> 已经被展开成 <code>match (&amp;a, &amp;b) { ... }</code>，消息里的 <code>{0}</code> 是 <code>#label</code> 变成的 <code>"sums"</code>。中间那一大堆 <code>test::TestDescAndFn</code> 是 <code>#[test]</code> 自己的展开：测试函数被登记到测试框架里。属性宏产生的 <code>#[test]</code> 和你手写的 <code>#[test]</code> 没有区别，因为宏展开发生在测试框架收集测试<strong>之前</strong>。</p><p>确认完了，把 <code>scratch.rs</code> 删掉。</p></section><section><h3>差分测试套件<a href="#差分测试套件"><span>#</span></a></h3><p><code>pebble-bench</code> 的 <code>Cargo.toml</code> 里的 <code>[dev-dependencies]</code> 段，前面做错误实验时已经补过了（<code>pebble-macros</code> 在这一节用，<code>criterion</code> 是下一节的基准要用）。</p><p>新建 <code>crates/pebble-bench/tests/differential.rs</code>。<code>tests/</code> 下的每个文件是一个独立的集成测试 crate，会自动把被测的库（<code>pebble_bench</code>）链接进来。这就是前面展开里 <code>::pebble_bench::run_interp</code> 能解析的原因：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Differential tests: every program here runs on both the tree-walking</span></div></div><div><div><div>2</div></div><div><span>//! interpreter and the bytecode VM, and the two rendered results must match.</span></div></div><div><div><div>3</div></div><div><span>//!</span></div></div><div><div><div>4</div></div><div><span>//! Each function returns a Pebble program; `#[differential]` keeps the</span></div></div><div><div><div>5</div></div><div><span>//! function and adds a `#[test]` named `&lt;function&gt;_backends_agree`.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>pebble_macros</span><span>::differential;</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#[differential]</span></div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>closures_keep_private_state</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>r#"</span></div></div><div><div><div>12</div></div><div><span>fn make_counter() {</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>let count = 0;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>return fn() {</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>count = count + 1;</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>return count;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div><span>let next = make_counter();</span></div></div><div><div><div>20</div></div><div><span>[next(), next(), next()];</span></div></div><div><div><div>21</div></div><div><span>"#</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>#[differential]</span></div></div><div><div><div>25</div></div><div><span>fn</span><span> </span><span>recursion_and_arithmetic</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>r#"</span></div></div><div><div><div>27</div></div><div><span>fn fib(n) {</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>if n &lt; 2 {</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>return n;</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>return fib(n - 1) + fib(n - 2);</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div><span>fib(15);</span></div></div><div><div><div>34</div></div><div><span>"#</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>#[differential]</span></div></div><div><div><div>38</div></div><div><span>fn</span><span> </span><span>loops_with_break_and_continue</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>39</div></div><div><span>    </span><span>r#"</span></div></div><div><div><div>40</div></div><div><span>let i = 0;</span></div></div><div><div><div>41</div></div><div><span>let total = 0;</span></div></div><div><div><div>42</div></div><div><span>while i &lt; 20 {</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>i = i + 1;</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>if i % 2 == 0 {</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>continue;</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>if i &gt; 15 {</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>break;</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>total = total + i;</span></div></div><div><div><div>51</div></div><div><span>}</span></div></div><div><div><div>52</div></div><div><span>total;</span></div></div><div><div><div>53</div></div><div><span>"#</span></div></div><div><div><div>54</div></div><div><span>}</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>#[differential]</span></div></div><div><div><div>57</div></div><div><span>fn</span><span> </span><span>strings_and_negative_indexing</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>58</div></div><div><span>    </span><span>r#"let s = "pebble"; s[-1] + s[0];"#</span></div></div><div><div><div>59</div></div><div><span>}</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span>#[differential]</span></div></div><div><div><div>62</div></div><div><span>fn</span><span> </span><span>lists_are_shared_by_reference</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>63</div></div><div><span>    </span><span>"let a = []; let b = a; push(b, 1); push(a, 2); a;"</span></div></div><div><div><div>64</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>五个程序，每个覆盖语言的一块：闭包的私有状态、递归和算术、<code>while</code> 里的 <code>break</code>/<code>continue</code>、字符串和负下标、列表按引用共享。每个都只是一个<strong>返回字符串的函数</strong>，一个 <code>#[differential]</code>，没有一行样板。</p><div><div><div></div><div>为什么是五个，不是六个</div></div><div><p>我最初写的是六个。第六个是 <code>logical_operators_return_operands</code>，<code>[nil or 3, false or "x", 1 and 2];</code>，想证明 <code>or</code>/<code>and</code> 返回操作数而不是布尔值。结果它失败了，不是因为逻辑运算符，而是别的原因，下一节展开。我没有为了让它通过而去改任何一个后端，而是把这个程序从文件里删掉了。</p></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-bench</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bench</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_bench-045df968f730d13b)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 2 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::both_backends_agree ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::programs_produce_the_expected_values ... ok</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.58s</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>Running tests/differential.rs (target/debug/deps/differential-145949794168dc6a)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>running 5 tests</span></div></div><div><div><div>14</div></div><div><span>test lists_are_shared_by_reference_backends_agree ... ok</span></div></div><div><div><div>15</div></div><div><span>test strings_and_negative_indexing_backends_agree ... ok</span></div></div><div><div><div>16</div></div><div><span>test closures_keep_private_state_backends_agree ... ok</span></div></div><div><div><div>17</div></div><div><span>test loops_with_break_and_continue_backends_agree ... ok</span></div></div><div><div><div>18</div></div><div><span>test recursion_and_arithmetic_backends_agree ... ok</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>Doc-tests pebble_bench</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>running 0 tests</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>2 个单元测试加 5 个差分测试。</p><p>性能留到本篇最后，现在先看那个被删掉的第六个程序。</p></section></section>
<section><h2>两个后端并不一致<a href="#两个后端并不一致"><span>#</span></a></h2><p>把第六个程序加回 <code>differential.rs</code>（末尾追加）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[differential]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>logical_operators_return_operands</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>r#"[nil or 3, false or "x", 1 and 2];"#</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 14.19s</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>Running tests/differential.rs (/tmp/ch12-stage-target/debug/deps/differential-145949794168dc6a)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>running 6 tests</span></div></div><div><div><div>5</div></div><div><span>test lists_are_shared_by_reference_backends_agree ... ok</span></div></div><div><div><div>6</div></div><div><span>test logical_operators_return_operands_backends_agree ... FAILED</span></div></div><div><div><div>7</div></div><div><span>test strings_and_negative_indexing_backends_agree ... ok</span></div></div><div><div><div>8</div></div><div><span>test closures_keep_private_state_backends_agree ... ok</span></div></div><div><div><div>9</div></div><div><span>test loops_with_break_and_continue_backends_agree ... ok</span></div></div><div><div><div>10</div></div><div><span>test recursion_and_arithmetic_backends_agree ... ok</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>failures:</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>---- logical_operators_return_operands_backends_agree stdout ----</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>thread 'logical_operators_return_operands_backends_agree' (157257) panicked at crates/pebble-bench/tests/differential.rs:66:1:</span></div></div><div><div><div>17</div></div><div><span>assertion `left == right` failed: backends disagree on `logical_operators_return_operands`</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>left: "[3, \"x\", 2]"</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>right: "[3, x, 2]"</span></div></div><div><div><div>20</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>failures:</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>logical_operators_return_operands_backends_agree</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>test result: FAILED. 5 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>error: test failed, to rerun pass `-p pebble-bench --test differential`</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两边的第一个和第三个元素都是 <code>3</code> 和 <code>2</code>，逻辑运算符没问题。差别在中间那个元素：树遍历解释器渲染成 <code>"x"</code>（带引号），VM 渲染成 <code>x</code>（不带）。<strong>同一个值，两种显示。</strong> 这个差分测试的失败信息也值得看一眼：断言指向 <code>differential.rs:66:1</code>，是 <code>#[differential]</code> 属性本身所在的那一行，而不是宏内部。这是因为 <code>quote!</code> 生成的 token 默认带着 <code>Span::call_site()</code>，也就是宏被调用的位置。测试失败时你会被引到出问题的那个程序，而不是某个看不懂的宏内部。</p><p>这是<strong>显示</strong>上的不一致，不是语义上的，但差分测试并不关心：它比较的是渲染后的字符串。这件事教了我两点。一是 VM 的 <code>write_display</code>（我们在复制的那块里指给你看过的第 3 点）确实不给嵌套的字符串加引号；二是差分测试<strong>自己的比较标准</strong>限制了它能发现什么：它拿 <code>Display</code> 的结果当标准，所以 <code>Display</code> 里的差别和语义上的差别会混在一起。</p><p>更严重的是语义上的不一致。下面是我在写这篇文章时核实的另外四个。每一个我都在 tag 上亲自跑过。先在 workspace 根目录建一个 <code>scratch/</code> 目录，把下面每个输出里 <code>cat</code> 显示的那几行存成同名的 <code>.pebble</code> 文件；输出里的 <code>pebble</code> 指 <code>cargo build -p pebble-cli</code> 之后的 <code>target/debug/pebble</code>（或者用 <code>cargo run -q -p pebble-cli --</code> 代替它）。</p><p><strong>一、块级作用域。</strong> 这个我们在第 9 篇预告过：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat scratch/scope.pebble</span></div></div><div><div><div>2</div></div><div><span>let x = 1;</span></div></div><div><div><div>3</div></div><div><span>if true {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>let x = 2;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>print(x);</span></div></div><div><div><div>7</div></div><div><span>$ pebble run scratch/scope.pebble</span></div></div><div><div><div>8</div></div><div><span>1</span></div></div><div><div><div>9</div></div><div><span>$ pebble run --vm scratch/scope.pebble</span></div></div><div><div><div>10</div></div><div><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>树遍历解释器给每个块一个子环境，里层的 <code>let x = 2;</code> 只是在子环境里定义了一个新的 <code>x</code>，出了块就没了，外面的 <code>x</code> 还是 <code>1</code>。VM 每次函数调用只有一个环境（<code>define</code> 总是写到 <code>current_env()</code>，而块不创建新环境），里层的 <code>let x = 2;</code> 覆盖了外层同名的绑定，所以 <code>print(x)</code> 打印 <code>2</code>。这是一个<strong>语言语义</strong>上的分歧，不是显示问题。两个后端对”块”这个概念有不同的理解。</p><p><strong>二、嵌套的 <code>for</code>。</strong> 第 10 篇的”两个我自己撞上的毛病”里已经详细讲过，原因是脱糖用的隐藏变量名 <code>" #iter"</code> 和 <code>" #idx"</code> 在两层循环里重名。这是同一份程序在两个后端上的真实输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat scratch/nested.pebble</span></div></div><div><div><div>2</div></div><div><span>for i in [1, 2] {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>for j in [10, 20] {</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>print(i * j);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>$ pebble run scratch/nested.pebble</span></div></div><div><div><div>8</div></div><div><span>10</span></div></div><div><div><div>9</div></div><div><span>20</span></div></div><div><div><div>10</div></div><div><span>20</span></div></div><div><div><div>11</div></div><div><span>40</span></div></div><div><div><div>12</div></div><div><span>$ pebble run --vm scratch/nested.pebble</span></div></div><div><div><div>13</div></div><div><span>10</span></div></div><div><div><div>14</div></div><div><span>20</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>VM 少打了两行。</p><p><strong>三、遮蔽 <code>len</code>。</strong> 同样是第 10 篇讲过的：<code>for</code> 的脱糖调用名为 <code>len</code> 的全局变量，用户定义了自己的 <code>len</code>，循环就坏了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat scratch/shadow.pebble</span></div></div><div><div><div>2</div></div><div><span>let len = 3;</span></div></div><div><div><div>3</div></div><div><span>for x in [10, 20] {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>print(x);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>$ pebble run scratch/shadow.pebble</span></div></div><div><div><div>7</div></div><div><span>10</span></div></div><div><div><div>8</div></div><div><span>20</span></div></div><div><div><div>9</div></div><div><span>$ pebble run --vm scratch/shadow.pebble</span></div></div><div><div><div>10</div></div><div><span>error[E2001]: int is not callable</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>--&gt; scratch/shadow.pebble:2:1</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>2 | for x in [10, 20] {</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>四、映射的键顺序。</strong> 这个第 10 篇没有提到，是我这次新发现的。映射在树遍历解释器里是第 8 篇的 <code>HashMap</code>，迭代顺序<strong>每次运行都可能不同</strong>；在 VM 里是按插入顺序存的数组：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cat scratch/order.pebble</span></div></div><div><div><div>2</div></div><div><span>let m = #{ "b": 1, "a": 2, "c": 3 };</span></div></div><div><div><div>3</div></div><div><span>print(keys(m));</span></div></div><div><div><div>4</div></div><div><span>$ pebble run scratch/order.pebble</span></div></div><div><div><div>5</div></div><div><span>["b", "c", "a"]</span></div></div><div><div><div>6</div></div><div><span>$ pebble run scratch/order.pebble</span></div></div><div><div><div>7</div></div><div><span>["b", "a", "c"]</span></div></div><div><div><div>8</div></div><div><span>$ pebble run scratch/order.pebble</span></div></div><div><div><div>9</div></div><div><span>["b", "a", "c"]</span></div></div><div><div><div>10</div></div><div><span>$ pebble run scratch/order.pebble</span></div></div><div><div><div>11</div></div><div><span>["b", "a", "c"]</span></div></div><div><div><div>12</div></div><div><span>$ pebble run --vm scratch/order.pebble</span></div></div><div><div><div>13</div></div><div><span>[b, a, c]</span></div></div><div><div><div>14</div></div><div><span>$ pebble run --vm scratch/order.pebble</span></div></div><div><div><div>15</div></div><div><span>[b, a, c]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>树遍历解释器的四次运行里有一次是 <code>["b", "c", "a"]</code>，别的是 <code>["b", "a", "c"]</code>（这是 Rust 标准库 <code>HashMap</code> 默认的随机种子在起作用，它是为了防御哈希碰撞攻击；C++ 的 <code>unordered_map</code> 没有这个特性，但同样不承诺任何顺序）。VM 则始终是插入顺序。我最初还尝试过一个”构造映射再取 <code>keys</code>”的差分程序，<strong>第一次跑它居然通过了</strong>，因为随机顺序碰巧和插入顺序相同；再跑几次它就失败了，而且每次失败时解释器那一边的顺序都不一样（三个键有 6 种排列，碰巧通过的概率我估计是六分之一，没有统计过）。这是一类特别讨厌的测试：偶尔通过。</p><p>五个不一致（引号、块作用域、嵌套 <code>for</code>、<code>len</code> 遮蔽、映射顺序）里，块作用域和映射顺序是两个后端对语言规范的<strong>不同选择</strong>，而我们根本没有写过这份规范。嵌套 <code>for</code> 和 <code>len</code> 遮蔽才是明确的 bug。引号则是纯粹的呈现差异。差分测试做到了它该做的：把这些分歧逼了出来。它没法替你决定谁对。</p><p>所以我不会说”两个后端行为一致”。它们已知一致的范围，恰好就是这五个差分程序、<code>pebble-bench</code> 里那三个基准程序，加上 <code>fib(20)</code> 这个命令行测试。</p></section>
<section><h2>Criterion：量速度<a href="#criterion量速度"><span>#</span></a></h2><p>现在回答开头的第一个问题：VM 到底快多少。</p><p>最朴素的办法是 <code>Instant::now()</code> 夹住一段代码，跑一遍，打印差值。刚才命令行的 <code>bench</code> 子命令就是这么做的，上面我们已经看到这个办法有多不可靠：同一个二进制，同一条命令，两次相差五成。原因有很多：CPU 频率在变，缓存是冷的，操作系统调度了别的进程。<strong>单次计时测的是噪声。</strong> 有人想过这个问题，写成了 Criterion：它先<strong>预热</strong>（warm up）几秒，然后收集一批样本，每个样本里运行足够多次迭代，最后用统计方法给出一个<strong>置信区间</strong>，并标出离群值。</p><p>先在 <code>crates/pebble-bench/Cargo.toml</code> 里，<code>[dev-dependencies]</code> 的后面、<code>[lints]</code> 之前，加上基准目标的声明：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[[</span><span>bench</span><span>]]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"backends"</span></div></div><div><div><div>3</div></div><div><span>harness</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>harness = false</code> 的意思是：不要 Rust 默认的测试框架，这个目标自己带 <code>main</code>。Criterion 的 <code>criterion_main!</code> 宏生成这个 <code>main</code>。（第 10 篇我们写过 <code>macro_rules!</code> 的 <code>chunk!</code>；<code>criterion_group!</code> 和 <code>criterion_main!</code> 是同一类宏，只是别人写的。）然后创建 <code>crates/pebble-bench/benches/backends.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Criterion benchmarks comparing the tree-walking interpreter with the</span></div></div><div><div><div>2</div></div><div><span>//! bytecode VM on three workloads.</span></div></div><div><div><div>3</div></div><div><span>//!</span></div></div><div><div><div>4</div></div><div><span>//! Every workload runs both backends in the same benchmark group, so the</span></div></div><div><div><div>5</div></div><div><span>//! comparison is visible side by side in the report.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>time</span><span>::</span></span><span>Duration</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>criterion</span><span>::{</span></span><span>Criterion</span><span>, criterion_group, criterion_main};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_bench</span><span>::{</span></span><span>COLLECTIONS</span><span>, </span><span>FIB</span><span>, </span><span>LOOP_SUM</span><span>, run_interp, run_vm};</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>fn</span><span> </span><span>backends</span><span>(</span><span>c</span><span>: &amp;</span><span>mut</span><span> </span><span>Criterion</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>programs</span><span><span> </span><span>=</span><span> [</span></span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>(</span><span>"fib"</span><span>, </span><span>FIB</span><span>),</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>(</span><span>"loop_sum"</span><span>, </span><span>LOOP_SUM</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>(</span><span>"collections"</span><span>, </span><span>COLLECTIONS</span><span>),</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>];</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>for</span><span> (</span><span>name</span><span>, </span><span>src</span><span>) </span><span>in</span><span> </span><span>programs</span><span> {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>group</span><span><span> </span><span>=</span><span> </span></span><span>c</span><span>.</span><span>benchmark_group</span><span>(</span><span>name</span><span>);</span></div></div><div><div><div>22</div></div><div><span>        </span><span>group</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>.</span><span>sample_size</span><span>(</span><span>20</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>.</span><span>measurement_time</span><span><span>(</span><span>Duration</span><span>::</span></span><span>from_secs</span><span>(</span><span>2</span><span>));</span></div></div><div><div><div>25</div></div><div><span>        </span><span>group</span><span>.</span><span>bench_function</span><span>(</span><span>"interp"</span><span><span>, </span><span>|</span></span><span>b</span><span><span>|</span><span> </span></span><span>b</span><span>.</span><span>iter</span><span><span>(</span><span>||</span><span> </span></span><span>run_interp</span><span>(</span><span>src</span><span>)));</span></div></div><div><div><div>26</div></div><div><span>        </span><span>group</span><span>.</span><span>bench_function</span><span>(</span><span>"vm"</span><span><span>, </span><span>|</span></span><span>b</span><span><span>|</span><span> </span></span><span>b</span><span>.</span><span>iter</span><span><span>(</span><span>||</span><span> </span></span><span>run_vm</span><span>(</span><span>src</span><span>)));</span></div></div><div><div><div>27</div></div><div><span>        </span><span>group</span><span>.</span><span>finish</span><span>();</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>criterion_group!</span><span>(</span><span>benches</span><span>, </span><span>backends</span><span>);</span></div></div><div><div><div>32</div></div><div><span>criterion_main!</span><span>(</span><span>benches</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>结构很短。<code>criterion_group!(benches, backends)</code> 把函数 <code>backends</code> 登记成一个叫 <code>benches</code> 的组，<code>criterion_main!(benches)</code> 生成 <code>main</code>，调用这个组。<code>backends</code> 里对每个程序建一个<strong>基准组</strong>，组里放两个基准：<code>"interp"</code> 和 <code>"vm"</code>，这样报告里同一个程序的两个结果挨在一起。<code>group.sample_size(20).measurement_time(Duration::from_secs(2))</code> 是一个链式调用的构造者风格（第 3 篇），把样本数从默认的 100 降到 20，把每个基准的测量时间设成 2 秒，在这台机器上这样整个基准套件不到一分钟就能跑完。<code>b.iter(|| run_interp(src))</code>：Criterion 会反复调用这个闭包，<code>|b| b.iter(|| ...)</code> 是一个闭包里套闭包，里层的闭包借用外层的 <code>src</code>（一个 <code>&amp;str</code>，<code>Copy</code>）。</p><p>注意一件事：<code>run_interp(src)</code> 和 <code>run_vm(src)</code> 每次都<strong>从源码字符串开始</strong>，包括词法分析、解析，VM 这边还有编译。我稍后量了这部分前端的耗时，不到总时间的百分之一，所以不影响结论，但这是你读基准数字之前该知道的。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>bench</span><span> </span><span>-p</span><span> </span><span>pebble-bench</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用的是完整运行，没有加 <code>--quick</code>。release 构建（带 <code>lto = "thin"</code>）花了 43 秒，之后基准本身大约 45 秒，总共 88 秒。机器是 AMD Ryzen 7 PRO 6850U（16 个线程），rustc 1.95.0。输出很长，我节选了开头、一个带警告的片段和最后的结果：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Finished `bench` profile [optimized] target(s) in 43.22s</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>Running benches/backends.rs (target/release/deps/backends-ec0e44154f3574a9)</span></div></div><div><div><div>3</div></div><div><span>...</span></div></div><div><div><div>4</div></div><div><span>Benchmarking fib/interp</span></div></div><div><div><div>5</div></div><div><span>Benchmarking fib/interp: Warming up for 3.0000 s</span></div></div><div><div><div>6</div></div><div><span>Benchmarking fib/interp: Collecting 20 samples in estimated 3.2567 s (420 iterations)</span></div></div><div><div><div>7</div></div><div><span>Benchmarking fib/interp: Analyzing</span></div></div><div><div><div>8</div></div><div><span>fib/interp              time:   [7.7474 ms 7.7636 ms 7.7845 ms]</span></div></div><div><div><div>9</div></div><div><span>Found 1 outliers among 20 measurements (5.00%)</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>1 (5.00%) high mild</span></div></div><div><div><div>11</div></div><div><span>Benchmarking fib/vm</span></div></div><div><div><div>12</div></div><div><span>Benchmarking fib/vm: Warming up for 3.0000 s</span></div></div><div><div><div>13</div></div><div><span>Benchmarking fib/vm: Collecting 20 samples in estimated 2.1523 s (420 iterations)</span></div></div><div><div><div>14</div></div><div><span>Benchmarking fib/vm: Analyzing</span></div></div><div><div><div>15</div></div><div><span>fib/vm                  time:   [4.9480 ms 5.0018 ms 5.0753 ms]</span></div></div><div><div><div>16</div></div><div><span>Found 4 outliers among 20 measurements (20.00%)</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>4 (20.00%) low mild</span></div></div><div><div><div>18</div></div><div><span>...</span></div></div><div><div><div>19</div></div><div><span>Benchmarking loop_sum/vm: Warming up for 3.0000 s</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>Warning: Unable to complete 20 samples in 2.0s. You may wish to increase target time to 3.8s, enable flat sampling, or reduce sample count to 10.</span></div></div><div><div><div>22</div></div><div><span>Benchmarking loop_sum/vm: Collecting 20 samples in estimated 3.8457 s (210 iterations)</span></div></div><div><div><div>23</div></div><div><span>Benchmarking loop_sum/vm: Analyzing</span></div></div><div><div><div>24</div></div><div><span>loop_sum/vm             time:   [17.887 ms 17.903 ms 17.926 ms]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读法：<code>time: [低 估计 高]</code>，中间那个是点估计，两头是置信区间的边界。区间很窄（<code>fib/interp</code> 是 7.7474 到 7.7845 ms，宽度不到百分之半），说明这次的噪声比我们前面手工计时小得多。<code>Found 4 outliers</code> 是 Criterion 在告诉你有几个样本明显偏离。<code>Warning: Unable to complete 20 samples in 2.0s</code> 是说 <code>loop_sum/vm</code> 每次迭代太慢，2 秒内收不齐 20 个样本，它自己把测量时间延长到了 3.8 秒。</p><p>六个结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fib/interp              time:   [7.7474 ms 7.7636 ms 7.7845 ms]</span></div></div><div><div><div>2</div></div><div><span>fib/vm                  time:   [4.9480 ms 5.0018 ms 5.0753 ms]</span></div></div><div><div><div>3</div></div><div><span>loop_sum/interp         time:   [33.011 ms 33.273 ms 33.596 ms]</span></div></div><div><div><div>4</div></div><div><span>loop_sum/vm             time:   [17.887 ms 17.903 ms 17.926 ms]</span></div></div><div><div><div>5</div></div><div><span>collections/interp      time:   [769.52 µs 779.77 µs 788.51 µs]</span></div></div><div><div><div>6</div></div><div><span>collections/vm          time:   [676.86 µs 680.29 µs 685.15 µs]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整理成一张表（取中间的点估计）：</p>

<table><thead><tr><th>程序</th><th>树遍历</th><th>VM</th><th>树遍历 / VM</th></tr></thead><tbody><tr><td><code>fib(20)</code></td><td>7.7636 ms</td><td>5.0018 ms</td><td>1.55</td></tr><tr><td><code>loop_sum</code>（10 万次循环）</td><td>33.273 ms</td><td>17.903 ms</td><td>1.86</td></tr><tr><td><code>collections</code>（1000 次 <code>push</code> 加 1000 次下标）</td><td>779.77 µs</td><td>680.29 µs</td><td>1.15</td></tr></tbody></table><p><strong>VM 更快，但是快 1.15 到 1.86 倍，不是传说中的”字节码 VM 比树遍历快一个数量级”。</strong> 换算成每次函数调用：<code>fib(20)</code> 一共 21,891 次调用，树遍历每次约 355 ns，VM 约 228 ns。一次调用里发生的事情不多：一个比较、两次减法、一次加法、两次递归调用的准备。228 纳秒，按 4 GHz 粗估是大约 900 个周期（我没有测实际频率）。这个数字<strong>慢得需要解释</strong>。</p><section><h3>为什么不是 10 倍<a href="#为什么不是-10-倍"><span>#</span></a></h3><p>字节码 VM 比树遍历快，靠的是两件事：省掉对树的递归遍历（指令是线性数组，不用在指针之间跳），以及指令分派是一个紧凑的循环。这两样我们都有。但在 Lua 或者 CPython 里，更大的一块收益来自<strong>编译期</strong>就把变量名解析成了槽位下标，运行时的读变量是一次数组索引。我们没有做这件事：第 10 篇我说过，指令里没有”第几个局部变量”，运行时按名字找，并且说这是第 12 篇要量的。现在量。</p><p>我先排除一个嫌疑：前端。<code>run_interp</code> 和 <code>run_vm</code> 每次从源码开始，会不会是解析和编译把 VM 的数字拖慢了？我写了一个临时的小程序，用 <code>Instant</code> 把各阶段分开计时（放在 <code>crates/pebble-bench/examples/runonly.rs</code>，量完就删，不进 tag，它不是 Criterion，所以绝对数字只作参考，比较的是各阶段的数量级）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>time</span><span>::</span></span><span>Instant</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>pebble_bench</span><span>::{</span></span><span>COLLECTIONS</span><span>, </span><span>FIB</span><span>, </span><span>LOOP_SUM</span><span>, compile_source};</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>pebble_interp</span><span>::</span></span><span>Interpreter</span><span>;</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>;</span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>Buffer</span><span>;</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>pebble_vm</span><span>::</span></span><span>Vm</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>time</span><span>(</span><span>label</span><span>: &amp;</span><span>str</span><span>, </span><span>iterations</span><span>: </span><span>u32</span><span>, </span><span>mut</span><span> </span><span>body</span><span>: </span><span>impl</span><span> </span><span>FnMut</span><span>()) {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>iterations</span><span> / </span><span>10</span><span> + </span><span>1</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>body</span><span>();</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>iterations</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>body</span><span>();</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>per</span><span><span> </span><span>=</span><span> </span></span><span>start</span><span>.</span><span>elapsed</span><span>().</span><span>as_secs_f64</span><span>() * </span><span>1000.0</span><span> / </span><span>f64</span><span>::</span><span>from</span><span>(</span><span>iterations</span><span>);</span></div></div><div><div><div>18</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{label:&lt;24} {per:&gt;9.4} ms"</span><span>);</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>for</span><span> (</span><span>name</span><span>, </span><span>src</span><span>, </span><span>iterations</span><span>) </span><span>in</span><span> [</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>(</span><span>"fib"</span><span>, </span><span>FIB</span><span>, </span><span>200</span><span>),</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>(</span><span>"loop_sum"</span><span>, </span><span>LOOP_SUM</span><span>, </span><span>50</span><span>),</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(</span><span>"collections"</span><span>, </span><span>COLLECTIONS</span><span>, </span><span>2000</span><span>),</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>] {</span></div></div><div><div><div>27</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>28</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"&lt;bench&gt;"</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span><span>pebble_parser</span><span>::</span></span><span>parse</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_source</span><span>(</span><span>src</span><span>);</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>        </span><span>time</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{name}: parse"</span><span>), </span><span>iterations</span><span><span>, </span><span>||</span><span> {</span></span></div></div><div><div><div>33</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span><span>pebble_parser</span><span>::</span></span><span>parse</span><span>(</span><span>id</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>35</div></div><div><span>        </span><span>time</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{name}: compile"</span><span>), </span><span>iterations</span><span><span>, </span><span>||</span><span> {</span></span></div></div><div><div><div>36</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span><span>pebble_bytecode</span><span>::</span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>38</div></div><div><span>        </span><span>time</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{name}: interp run"</span><span>), </span><span>iterations</span><span><span>, </span><span>||</span><span> {</span></span></div></div><div><div><div>39</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>interp</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>40</div></div><div><span>            </span><span>interp</span><span>.</span><span>interpret</span><span>(&amp;</span><span>parsed</span><span>.program).</span><span>unwrap</span><span>();</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>42</div></div><div><span>        </span><span>time</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{name}: vm run"</span><span>), </span><span>iterations</span><span><span>, </span><span>||</span><span> {</span></span></div></div><div><div><div>43</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>vm</span><span><span> </span><span>=</span><span> </span></span><span>Vm</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>44</div></div><div><span>            </span><span>vm</span><span>.</span><span>run</span><span>(&amp;</span><span>compiled</span><span>).</span><span>unwrap</span><span>();</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>46</div></div><div><span>        </span><span>println!</span><span>();</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--release</span><span> </span><span>-q</span><span> </span><span>-p</span><span> </span><span>pebble-bench</span><span> </span><span>--example</span><span> </span><span>runonly</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fib: parse                  0.0015 ms</span></div></div><div><div><div>2</div></div><div><span>fib: compile                0.0007 ms</span></div></div><div><div><div>3</div></div><div><span>fib: interp run             7.6040 ms</span></div></div><div><div><div>4</div></div><div><span>fib: vm run                 4.7703 ms</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>loop_sum: parse             0.0015 ms</span></div></div><div><div><div>7</div></div><div><span>loop_sum: compile           0.0006 ms</span></div></div><div><div><div>8</div></div><div><span>loop_sum: interp run       35.8668 ms</span></div></div><div><div><div>9</div></div><div><span>loop_sum: vm run           16.6299 ms</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>collections: parse          0.0032 ms</span></div></div><div><div><div>12</div></div><div><span>collections: compile        0.0011 ms</span></div></div><div><div><div>13</div></div><div><span>collections: interp run     0.7617 ms</span></div></div><div><div><div>14</div></div><div><span>collections: vm run         0.7698 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>fib</code> 的解析是 0.0015 ms、编译是 0.0007 ms，加起来约是运行时间的 0.05%。<code>collections</code> 最短，前端也只占 0.6%。所以前端不是答案：<strong>那 5 毫秒确实花在 VM 执行上。</strong> 顺带注意最后一行：<code>collections</code> 在这个手工计时里，VM（0.7698 ms）和树遍历（0.7617 ms）几乎一样快。</p></section><section><h3>四个嫌疑<a href="#四个嫌疑"><span>#</span></a></h3><p>我有四个怀疑对象。前三个我能给出数字，第四个只是猜测。</p><p><strong>嫌疑一：按名字找变量。</strong> 我往 VM 里临时塞了三个计数器（用 <code>AtomicUsize</code>，这是一个线程安全的计数器，第 13 篇细讲原子操作，这里当作”静态全局变量也能安全地改”的办法；<code>static mut</code> 要 <code>unsafe</code>，原子类型不要）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>sync</span><span>::</span><span>atomic</span><span>::{</span></span><span>AtomicUsize</span><span><span>, </span><span>Ordering</span><span>::</span></span><span>Relaxed</span><span>};</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>static</span><span> </span><span>LOOKUPS</span><span>: </span><span>AtomicUsize</span><span><span> </span><span>=</span><span> </span></span><span>AtomicUsize</span><span>::</span><span>new</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>static</span><span> </span><span>COMPARES</span><span>: </span><span>AtomicUsize</span><span><span> </span><span>=</span><span> </span></span><span>AtomicUsize</span><span>::</span><span>new</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>static</span><span> </span><span>ENVS</span><span>: </span><span>AtomicUsize</span><span><span> </span><span>=</span><span> </span></span><span>AtomicUsize</span><span>::</span><span>new</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>5</div></div><div><span>// lookup(): LOOKUPS.fetch_add(1, Relaxed);</span></div></div><div><div><div>6</div></div><div><span>// 扫描槽位的 find 闭包里: COMPARES.fetch_add(1, Relaxed);</span></div></div><div><div><div>7</div></div><div><span>// call() 分配新环境之前: ENVS.fetch_add(1, Relaxed);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后在一个临时的 <code>examples/counts.rs</code> 里把三个基准程序各跑一遍，打印计数器：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fib: lookups=76618 name_compares=339309 call_envs=21891 heap_live_after=406</span></div></div><div><div><div>2</div></div><div><span>loop_sum: lookups=400002 name_compares=4900025 call_envs=0 heap_live_after=12</span></div></div><div><div><div>3</div></div><div><span>collections: lookups=10003 name_compares=130042 call_envs=0 heap_live_after=13</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>loop_sum</code> 读了 400,002 次变量（十万次循环，每圈读 <code>i</code> 三次、<code>sum</code> 一次，再加上循环条件的最后一次检查和结尾的 <code>sum;</code>），扫描槽位时做了 4,900,025 次名字比较，<strong>平均每次读变量比较 12.25 次字符串</strong>。为什么这么多？因为全局环境是 <code>VmObject::Env</code>，它的槽位数组里<strong>前 11 项是内建函数</strong>（<code>with_output</code> 一开始塞进去的 <code>print</code>、<code>len</code>……），用户的 <code>i</code> 和 <code>sum</code> 排在第 12、13 位，每次读它们都得先把前面 11 个名字比一遍。这是我对数字的解释，我没有拆开统计每个名字各比较了多少次。树遍历的全局环境是 <code>HashMap</code>，读一次就是一次哈希加一次比较，没有这个问题。我在前面说”大多数环境只有几个名字，数组扫一遍比哈希快”，这句话对<strong>全局环境不成立</strong>。</p><p>加上我们前面指出的另一件事：<code>constant_name</code> 每次读变量都会克隆一个 <code>String</code>。<code>fib(20)</code> 一共 76,618 次 <code>lookup</code>，也就是至少 76,618 次小的堆分配，仅仅为了拿到一个名字。</p><p><strong>嫌疑二：每次调用分配一个环境。</strong> <code>call_envs=21891</code>：<code>fib(20)</code> 恰好是 21,891 次调用，每次调用在 GC 堆上分配一个 <code>Env</code> 对象，里面是一个 <code>Vec</code> 加一个 <code>String</code>（形参名）。第 9 篇的树遍历解释器也要为每次调用分配 <code>Rc&lt;RefCell&lt;Env&gt;&gt;</code>，所以这一项不是 VM 相对树遍历的劣势，但它也不是 VM 的优势，是两者共有的成本。</p><p><strong>嫌疑三：集合里的分配。</strong> 前面指给你看过 <code>index_value</code> 对列表做了 <code>items.clone()</code>。<code>collections</code> 程序里，读一次 <code>xs[j]</code> 就克隆一个 1000 个元素的 <code>Vec</code>，一千次读就是一百万次元素拷贝。我给这个函数临时加了一条快路径，在”克隆快照”那行注释之前：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span> </span><span>let</span><span> </span><span>VmObject</span><span>::</span><span>List</span><span>(</span><span>items</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.heap.</span><span>get</span><span>(</span><span>reference</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>position</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>resolve_index</span><span>(</span><span>items</span><span>.</span><span>len</span><span>(), </span><span>index</span><span>, </span><span>span</span><span>)?;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>items</span><span>[</span><span>position</span><span>]);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里 <code>items</code> 是对堆的<strong>共享</strong>借用，<code>resolve_index</code> 也只要 <code>&amp;self</code>，两者不冲突；而且我们只读，读出来的是 <code>Copy</code> 的 <code>VmValue</code>，不需要在借用结束之后再改堆。所以这一支用不着前面那个”先拷出来”的规矩。重新跑 <code>runonly</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>collections: parse          0.0032 ms</span></div></div><div><div><div>2</div></div><div><span>collections: compile        0.0011 ms</span></div></div><div><div><div>3</div></div><div><span>collections: interp run     0.7972 ms</span></div></div><div><div><div>4</div></div><div><span>collections: vm run         0.3771 ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>collections</code> 在 VM 上从 0.7698 ms 降到 <strong>0.3771 ms，快了一倍</strong>，树遍历那一行（0.76 到 0.80）在噪声之内，没有变化。<code>pebble-vm</code> 的 14 个测试仍然全部通过。<strong>这个改动我没有放进 tag</strong>：这篇文章讲的是 tag 上的 VM，所以我把它留成了一个”你自己可以做的练习”。借用检查器让你”先拷出来再改”，是安全的；但在你只读不改的路径上，把拷贝省掉是你的责任，编译器不会替你发现这一点。</p><p><strong>嫌疑四：分派开销。</strong> 每执行一条指令，<code>run</code> 都要做一次 <code>Rc::clone</code>（原型的引用计数加一，指令结束时减一）、一次 <code>chunk.code()[ip]</code> 的边界检查、一次 <code>chunk.span_at(ip)</code>、一次 <code>frames[frame_index]</code> 的边界检查，再加上 <code>match</code> 本身。我怀疑这些加起来在每条指令上要多花几个周期，但是我<strong>没有量</strong>。我也没有量安全的 Rust <code>enum</code> 分派和 C 里 <code>switch</code> 加计算跳转（computed goto）的差别。这是剩下的那部分差距的一个猜测，不是结论。</p><p>把四个嫌疑摆在一起，我的判断是：<strong>这个 VM 慢，原因几乎全在语义设计上，而不是循环写得不够好。</strong> 把名字解析成槽位下标，让 <code>Opcode::GetLocal(slot)</code> 变成一次数组索引，把全局内建函数放到另一个表里，是比任何分派优化都大的收益，据我所知这正是 Lua 和 CPython 的做法。这是第 12 篇的 VM 的延伸方向。我们没有走，因为那意味着要同时改编译器（第 10 篇）、VM（这一篇），还有闭包捕获的语义，而这个系列的目标是教 Rust，不是做出一个快的语言。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 <code>/tmp/pebble-rebuild-12</code>（你的是 <code>~/pebble</code>）里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-vm</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-bench</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span></div></div><div><div><div>4</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--workspace</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>5</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-vm</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_vm-32c6a9188a572458)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 14 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::arithmetic_matches_the_tree_walker ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::division_by_zero_is_an_error ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::control_flow_matches_the_tree_walker ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::arity_errors_are_reported ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::closures_capture_their_environment ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::garbage_collection_reclaims_cycles ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::print_reaches_the_output_sink ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::lists_maps_and_indexing ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::recursion_via_global_function ... ok</span></div></div><div><div><div>15</div></div><div><span>test tests::undefined_variables_are_reported ... ok</span></div></div><div><div><div>16</div></div><div><span>test tests::strings_concatenate_and_index ... ok</span></div></div><div><div><div>17</div></div><div><span>test tests::variables_and_assignment ... ok</span></div></div><div><div><div>18</div></div><div><span>test tests::while_loop_accumulates ... ok</span></div></div><div><div><div>19</div></div><div><span>test tests::recursion_limit_is_enforced ... ok</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>   </span></span><span>Doc-tests pebble_vm</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>running 0 tests</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bench</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_bench-045df968f730d13b)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 2 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::both_backends_agree ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::programs_produce_the_expected_values ... ok</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.58s</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>Running tests/differential.rs (target/debug/deps/differential-145949794168dc6a)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>running 5 tests</span></div></div><div><div><div>14</div></div><div><span>test lists_are_shared_by_reference_backends_agree ... ok</span></div></div><div><div><div>15</div></div><div><span>test strings_and_negative_indexing_backends_agree ... ok</span></div></div><div><div><div>16</div></div><div><span>test closures_keep_private_state_backends_agree ... ok</span></div></div><div><div><div>17</div></div><div><span>test loops_with_break_and_continue_backends_agree ... ok</span></div></div><div><div><div>18</div></div><div><span>test recursion_and_arithmetic_backends_agree ... ok</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>Doc-tests pebble_bench</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>running 0 tests</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-cli</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.06s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/main.rs (target/debug/deps/pebble-f848d73f4f46c5f3)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 3 tests</span></div></div><div><div><div>6</div></div><div><span>test repl::tests::arguments_are_kept_whole ... ok</span></div></div><div><div><div>7</div></div><div><span>test repl::tests::quit_has_a_short_form ... ok</span></div></div><div><div><div>8</div></div><div><span>test repl::tests::unknown_commands_are_rejected ... ok</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>Running tests/cli.rs (target/debug/deps/cli-8a92629c34332c03)</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>running 10 tests</span></div></div><div><div><div>15</div></div><div><span>test check_is_silent_on_a_valid_file ... ok</span></div></div><div><div><div>16</div></div><div><span>test parse_prints_the_syntax_tree ... ok</span></div></div><div><div><div>17</div></div><div><span>test quiet_suppresses_the_final_value ... ok</span></div></div><div><div><div>18</div></div><div><span>test run_reports_parse_errors_on_stderr ... ok</span></div></div><div><div><div>19</div></div><div><span>test check_reports_compile_errors ... ok</span></div></div><div><div><div>20</div></div><div><span>test tokens_prints_one_line_per_token ... ok</span></div></div><div><div><div>21</div></div><div><span>test unknown_subcommands_are_usage_errors ... ok</span></div></div><div><div><div>22</div></div><div><span>test run_evaluates_fibonacci ... ok</span></div></div><div><div><div>23</div></div><div><span>test dump_bytecode_prints_the_listing ... ok</span></div></div><div><div><div>24</div></div><div><span>test the_vm_and_the_interpreter_agree ... ok</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo clippy --workspace --all-targets -- -D warnings</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s</span></div></div><div><div><div>3</div></div><div><span>$ cargo fmt --all --check; echo "exit: $?"</span></div></div><div><div><div>4</div></div><div><span>exit: 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pebble-vm</code> 14 个测试，<code>pebble-bench</code> 2 个单元测试加 5 个差分测试，<code>pebble-cli</code> 3 个单元测试加 10 个集成测试，<code>pebble-macros</code> 仍然没有测试（它的”测试”是 <code>pebble-bench</code> 里这五个展开出来的 <code>#[test]</code>）。clippy 和 rustfmt 干净。</p><p>下面这些文件现在必须和 tag 逐字节相同：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-vm/Cargo.toml" target="_blank"><code>crates/pebble-vm/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-vm/src/lib.rs" target="_blank"><code>crates/pebble-vm/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bench/Cargo.toml" target="_blank"><code>crates/pebble-bench/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bench/src/lib.rs" target="_blank"><code>crates/pebble-bench/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bench/tests/differential.rs" target="_blank"><code>crates/pebble-bench/tests/differential.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bench/benches/backends.rs" target="_blank"><code>crates/pebble-bench/benches/backends.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-macros/Cargo.toml" target="_blank"><code>crates/pebble-macros/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-macros/src/lib.rs" target="_blank"><code>crates/pebble-macros/src/lib.rs</code></a></li>
</ul><p>此外，之前各篇的 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-lexer</code>、<code>pebble-ast</code>、<code>pebble-parser</code>、<code>pebble-value</code>、<code>pebble-interp</code>、<code>pebble-bytecode</code>、<code>pebble-gc</code>，以及 <code>examples/</code> 下的五个文件，以及 <code>crates/pebble-cli/src/sources.rs</code> 和 <code>crates/pebble-cli/src/repl.rs</code>，也都和 tag 相同。我对重建目录里的每一个都用 <code>diff</code> 比对过 tag：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>same  crates/pebble-vm/Cargo.toml</span></div></div><div><div><div>2</div></div><div><span>same  crates/pebble-vm/src/lib.rs</span></div></div><div><div><div>3</div></div><div><span>same  crates/pebble-bench/Cargo.toml</span></div></div><div><div><div>4</div></div><div><span>same  crates/pebble-bench/src/lib.rs</span></div></div><div><div><div>5</div></div><div><span>same  crates/pebble-bench/tests/differential.rs</span></div></div><div><div><div>6</div></div><div><span>same  crates/pebble-bench/benches/backends.rs</span></div></div><div><div><div>7</div></div><div><span>same  crates/pebble-macros/Cargo.toml</span></div></div><div><div><div>8</div></div><div><span>same  crates/pebble-macros/src/lib.rs</span></div></div><div><div><div>9</div></div><div><span>same  crates/pebble-cli/src/sources.rs</span></div></div><div><div><div>10</div></div><div><span>same  crates/pebble-cli/src/repl.rs</span></div></div><div><div><div>11</div></div><div><span>same  examples/closures.pebble</span></div></div><div><div><div>12</div></div><div><span>same  examples/collections.pebble</span></div></div><div><div><div>13</div></div><div><span>same  examples/errors.pebble</span></div></div><div><div><div>14</div></div><div><span>same  examples/fib.pebble</span></div></div><div><div><div>15</div></div><div><span>same  examples/hello.pebble</span></div></div><div><div><div>16</div></div><div><span>same  crates/pebble-span/Cargo.toml</span></div></div><div><div><div>17</div></div><div><span>same  crates/pebble-span/src/lib.rs</span></div></div><div><div><div>18</div></div><div><span>same  crates/pebble-diag/Cargo.toml</span></div></div><div><div><div>19</div></div><div><span>same  crates/pebble-diag/src/lib.rs</span></div></div><div><div><div>20</div></div><div><span>same  crates/pebble-lexer/Cargo.toml</span></div></div><div><div><div>21</div></div><div><span>same  crates/pebble-lexer/src/lib.rs</span></div></div><div><div><div>22</div></div><div><span>same  crates/pebble-ast/Cargo.toml</span></div></div><div><div><div>23</div></div><div><span>same  crates/pebble-ast/src/lib.rs</span></div></div><div><div><div>24</div></div><div><span>same  crates/pebble-parser/Cargo.toml</span></div></div><div><div><div>25</div></div><div><span>same  crates/pebble-parser/src/lib.rs</span></div></div><div><div><div>26</div></div><div><span>same  crates/pebble-value/Cargo.toml</span></div></div><div><div><div>27</div></div><div><span>same  crates/pebble-value/src/lib.rs</span></div></div><div><div><div>28</div></div><div><span>same  crates/pebble-interp/Cargo.toml</span></div></div><div><div><div>29</div></div><div><span>same  crates/pebble-interp/src/lib.rs</span></div></div><div><div><div>30</div></div><div><span>same  crates/pebble-bytecode/Cargo.toml</span></div></div><div><div><div>31</div></div><div><span>same  crates/pebble-bytecode/src/lib.rs</span></div></div><div><div><div>32</div></div><div><span>same  crates/pebble-gc/Cargo.toml</span></div></div><div><div><div>33</div></div><div><span>same  crates/pebble-gc/src/bump.rs</span></div></div><div><div><div>34</div></div><div><span>same  crates/pebble-gc/src/lib.rs</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>每一行都是 <code>same</code>，没有一行 <code>DIFF</code>。另外，<code>pebble-vm</code>、<code>pebble-macros</code>、<code>pebble-bench</code> 的 <code>src/lib.rs</code>，我还按文章里的顺序把每一段代码拼起来，再和 tag 比对，也是逐字节相同的（这一步是单独跑的，不在上面的输出里）。</p><p><strong>中间文件</strong>（还不是最终版）：</p><ul>
<li><code>crates/pebble-cli/</code> 下的全部八个文件（<code>Cargo.toml</code>、<code>src/</code> 下六个、<code>tests/cli.rs</code>）：第四版。相对第三版的改动就是上面的那几处：新增 <code>backend.rs</code>，<code>cli.rs</code> 多了 <code>--vm</code> 和 <code>bench</code>，<code>commands.rs</code> 的 <code>run</code> 重写并多了 <code>bench</code> 及四个泛型函数，<code>main.rs</code> 多一行 <code>mod backend;</code> 和一个 <code>Bench</code> 分支，<code>Cargo.toml</code> 多一个依赖，测试多一条。最终版多出来的东西：并行的 <code>check</code>（第 14 篇）、可选的 <code>repl</code> 特性（第 15 篇）。</li>
<li>根 <code>Cargo.toml</code>：现在列了 11 个内部 crate（多了 <code>pebble-gc</code> 和 <code>pebble-vm</code>），外部多了 <code>criterion</code> 和 <code>rand</code>，还多了两段 profile。最终版还有 <code>pebble-sys</code>、<code>pebble-nostd</code>、<code>pebble-net</code>、<code>pebble-cli</code>、<code>cc</code>、<code>tokio</code>（第 13 到 15 篇）。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）。<code>git clone</code> 到 <code>~/pebble-tag</code> 的话，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，用 <code>cd ~/pebble-tag/lab/rust-lab</code> 运行（也可以把这个目录拷到 <code>~/rust-lab</code>）。这一篇的实验都只在 <code>rust-lab</code> 里，没有侧 crate。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch12_dispatch.rs" target="_blank"><code>ch12_dispatch.rs</code></a></td><td><code>cargo run --quiet --bin ch12_dispatch</code></td><td>运行，首行 <code>fast -&gt; 42</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch12_gat_missing.rs" target="_blank"><code>ch12_gat_missing.rs</code></a></td><td><code>cargo build --bin ch12_gat_missing</code></td><td>编译失败：<code>error: missing lifetime in associated type</code>（没有 E 编码）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch12_gat.rs" target="_blank"><code>ch12_gat.rs</code></a></td><td><code>cargo run --quiet --bin ch12_gat</code></td><td>运行，首行 <code>borrowing:   6</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch12_object_safety.rs" target="_blank"><code>ch12_object_safety.rs</code></a></td><td><code>cargo build --bin ch12_object_safety</code></td><td>编译失败：E0038</td></tr></tbody></table></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 让 VM 的 <code>let</code> 有块级作用域。</strong>（讨论题）你会改哪几处？最容易漏掉什么？</p>
参考答案<p>根因是 <code>define</code> 总是写进 <code>current_env()</code>，也就是当前帧的那一个环境，而 VM 里没有任何东西会为一个块创建新环境。要让它和树遍历一致，自然的设计是两条新指令：<code>EnterScope</code> 分配一个以当前帧环境为父的新 <code>Env</code>，并把它设为当前帧的环境；<code>ExitScope</code> 把帧环境换回它的父。编译器在每个块的开头和结尾各发射一条。</p><p>麻烦在<strong>提前离开块</strong>的路径。<code>return</code> 没问题，因为整个帧都会被弹掉。但 <code>break</code> 和 <code>continue</code> 会从嵌套的块里直接跳出去，跳过了它们结尾的 <code>ExitScope</code>。所以编译器必须知道，从当前位置到目标循环之间隔着几层块，在跳转之前发射相应数量的 <code>ExitScope</code>（第 10 篇的 <code>LoopCtx</code> 需要多记一个”进入循环时的作用域深度”）。漏掉它，循环里 <code>break</code> 之后，帧的环境就停在一个内层的子环境上，后面所有的 <code>let</code> 都会写错地方，而且最先暴露的是别的某个看起来毫不相关的测试。</p><p>闭包不用特别处理：<code>MakeClosure</code> 本来就是捕获 <code>frames[frame_index].env</code>，这时候它会捕获当时最内层的块环境，和树遍历解释器一致。代价是性能：每个块、每圈循环体都多一次堆分配，<code>loop_sum</code> 这类程序大概率会变慢。</p><p>我没有把这个设计实现出来，上面是我读代码得出的推断，不是跑出来的结论。</p><p><strong>2. 把引号的分歧修掉。</strong>（只改 VM 的显示，让被删掉的第六个差分程序通过）</p>
参考答案<p>树遍历解释器的 <code>write_value</code> 有一个 <code>quoted</code> 参数：顶层字符串不带引号，嵌在列表或映射里的字符串用 <code>{:?}</code> 带引号。VM 的 <code>write_display</code> 没有这个参数。给它加上，顶层传 <code>false</code>，递归时传 <code>true</code>，字符串那一支按它选择（以下是我实际跑的补丁，改的是 <code>crates/pebble-vm/src/lib.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -1042,11 +1042,11 @@</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>pub fn display(&amp;self, value: VmValue) -&gt; String {</span></div></div><div><div><div>4</div></div><div><span><span>         </span></span><span>let mut out = String::new();</span></div></div><div><div><div>5</div></div><div><span>-        self.write_display(&amp;mut out, value, 0);</span></div></div><div><div><div>6</div></div><div><span>+        self.write_display(&amp;mut out, value, 0, false);</span></div></div><div><div><div>7</div></div><div><span><span>         </span></span><span>out</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>-    fn write_display(&amp;self, out: &amp;mut String, value: VmValue, depth: usize) {</span></div></div><div><div><div>11</div></div><div><span>+    fn write_display(&amp;self, out: &amp;mut String, value: VmValue, depth: usize, quoted: bool) {</span></div></div><div><div><div>12</div></div><div><span><span>         </span></span><span>const MAX_DEPTH: usize = 4;</span></div></div><div><div><div>13</div></div><div><span><span>         </span></span><span>match value {</span></div></div><div><div><div>14</div></div><div><span><span>             </span></span><span>VmValue::Nil =&gt; out.push_str("nil"),</span></div></div><div><div><div>15</div></div><div><span>@@ -1060,7 +1060,13 @@</span></div></div><div><div><div>16</div></div><div><span><span>                 </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>             </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>             </span></span><span>VmValue::Ref(reference) =&gt; match self.heap.get(reference) {</span></div></div><div><div><div>19</div></div><div><span>-                VmObject::Str(text) =&gt; out.push_str(text),</span></div></div><div><div><div>20</div></div><div><span>+                VmObject::Str(text) =&gt; {</span></div></div><div><div><div>21</div></div><div><span>+                    if quoted {</span></div></div><div><div><div>22</div></div><div><span>+                        out.push_str(&amp;format!("{text:?}"));</span></div></div><div><div><div>23</div></div><div><span>+                    } else {</span></div></div><div><div><div>24</div></div><div><span>+                        out.push_str(text);</span></div></div><div><div><div>25</div></div><div><span>+                    }</span></div></div><div><div><div>26</div></div><div><span>+                }</span></div></div><div><div><div>27</div></div><div><span><span>                 </span></span><span>VmObject::List(items) =&gt; {</span></div></div><div><div><div>28</div></div><div><span><span>                     </span></span><span>if depth &gt;= MAX_DEPTH {</span></div></div><div><div><div>29</div></div><div><span><span>                         </span></span><span>out.push_str("[...]");</span></div></div><div><div><div>30</div></div><div><span>@@ -1071,7 +1077,7 @@</span></div></div><div><div><div>31</div></div><div><span><span>                         </span></span><span>if index &gt; 0 {</span></div></div><div><div><div>32</div></div><div><span><span>                             </span></span><span>out.push_str(", ");</span></div></div><div><div><div>33</div></div><div><span><span>                         </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>-                        self.write_display(out, *item, depth + 1);</span></div></div><div><div><div>35</div></div><div><span>+                        self.write_display(out, *item, depth + 1, true);</span></div></div><div><div><div>36</div></div><div><span><span>                     </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>                     </span></span><span>out.push(']');</span></div></div><div><div><div>38</div></div><div><span><span>                 </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>@@ -1085,9 +1091,9 @@</span></div></div><div><div><div>40</div></div><div><span><span>                         </span></span><span>if index &gt; 0 {</span></div></div><div><div><div>41</div></div><div><span><span>                             </span></span><span>out.push_str(", ");</span></div></div><div><div><div>42</div></div><div><span><span>                         </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span>-                        self.write_display(out, *key, depth + 1);</span></div></div><div><div><div>44</div></div><div><span>+                        self.write_display(out, *key, depth + 1, true);</span></div></div><div><div><div>45</div></div><div><span><span>                         </span></span><span>out.push_str(": ");</span></div></div><div><div><div>46</div></div><div><span>-                        self.write_display(out, *item, depth + 1);</span></div></div><div><div><div>47</div></div><div><span>+                        self.write_display(out, *item, depth + 1, true);</span></div></div><div><div><div>48</div></div><div><span><span>                     </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span><span>                     </span></span><span>out.push('}');</span></div></div><div><div><div>50</div></div><div><span><span>                 </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>然后把第六个程序加回 <code>differential.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s</span></div></div><div><div><div>2</div></div><div><span><span>     </span></span><span>Running tests/differential.rs (/tmp/ch12-stage-target/debug/deps/differential-145949794168dc6a)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>running 6 tests</span></div></div><div><div><div>5</div></div><div><span>test lists_are_shared_by_reference_backends_agree ... ok</span></div></div><div><div><div>6</div></div><div><span>test closures_keep_private_state_backends_agree ... ok</span></div></div><div><div><div>7</div></div><div><span>test logical_operators_return_operands_backends_agree ... ok</span></div></div><div><div><div>8</div></div><div><span>test strings_and_negative_indexing_backends_agree ... ok</span></div></div><div><div><div>9</div></div><div><span>test loops_with_break_and_continue_backends_agree ... ok</span></div></div><div><div><div>10</div></div><div><span>test recursion_and_arithmetic_backends_agree ... ok</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>六个差分测试全部通过，<code>pebble-vm</code> 的 14 个测试也仍然通过。映射键顺序的那个分歧不会因此消失：它需要你先决定哪一边对（要么给树遍历解释器换一个保持插入顺序的映射，要么承认顺序是未指定的，让 <code>keys</code> 返回排好序的结果），这是语言设计的决定，不是修一行显示的事。做完请把补丁还原，因为你的 <code>pebble-vm</code> 应该和 tag 相同。</p><p><strong>3. 给 <code>run</code> 加一个 <code>--backend</code> 选项，用枚举代替 <code>--vm</code> 开关。</strong>（clap 的 <code>ValueEnum</code>）</p>
参考答案<p><code>clap</code> 的 <code>ValueEnum</code> 派生宏把一个枚举变成一个有限取值的命令行参数，变体名自动转成短横线风格（<code>TreeWalker</code> 变成 <code>tree-walker</code>），变体上的文档注释变成帮助文字。我实际改的是 <code>cli.rs</code> 和 <code>commands.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -8,3 +8,3 @@</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>-use clap::{Args, Parser, Subcommand};</span></div></div><div><div><div>4</div></div><div><span>+use clap::{Args, Parser, Subcommand, ValueEnum};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>@@ -48,5 +48,5 @@</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>pub(crate) file: PathBuf,</span></div></div><div><div><div>8</div></div><div><span>-    /// Execute on the bytecode VM instead of the tree-walking interpreter.</span></div></div><div><div><div>9</div></div><div><span>-    #[arg(long)]</span></div></div><div><div><div>10</div></div><div><span>-    pub(crate) vm: bool,</span></div></div><div><div><div>11</div></div><div><span>+    /// Which engine executes the program.</span></div></div><div><div><div>12</div></div><div><span>+    #[arg(long, value_enum, default_value_t = BackendChoice::TreeWalker)]</span></div></div><div><div><div>13</div></div><div><span>+    pub(crate) backend: BackendChoice,</span></div></div><div><div><div>14</div></div><div><span><span>     </span></span><span>/// Print the parsed syntax tree before running.</span></div></div><div><div><div>15</div></div><div><span>@@ -71 +71,10 @@</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>+</span></div></div><div><div><div>18</div></div><div><span>+/// The engines `pebble run` can use.</span></div></div><div><div><div>19</div></div><div><span>+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]</span></div></div><div><div><div>20</div></div><div><span>+pub(crate) enum BackendChoice {</span></div></div><div><div><div>21</div></div><div><span>+    /// The tree-walking interpreter.</span></div></div><div><div><div>22</div></div><div><span>+    TreeWalker,</span></div></div><div><div><div>23</div></div><div><span>+    /// The bytecode compiler plus the stack VM.</span></div></div><div><div><div>24</div></div><div><span>+    Vm,</span></div></div><div><div><div>25</div></div><div><span>+}</span></div></div><div><div><div>26</div></div><div><span>@@ -16,3 +16,3 @@</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>use crate::backend::{Backend, BytecodeVm, TreeWalker};</span></div></div><div><div><div>28</div></div><div><span>-use crate::cli::{BenchArgs, FileArgs, RunArgs};</span></div></div><div><div><div>29</div></div><div><span>+use crate::cli::{BackendChoice, BenchArgs, FileArgs, RunArgs};</span></div></div><div><div><div>30</div></div><div><span><span> </span></span><span>use crate::sources::{self, Source};</span></div></div><div><div><div>31</div></div><div><span>@@ -34,3 +34,3 @@</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>-    if args.vm {</span></div></div><div><div><div>34</div></div><div><span>+    if args.backend == BackendChoice::Vm {</span></div></div><div><div><div>35</div></div><div><span><span>         </span></span><span>let Some(prepared) = prepare_or_report::&lt;BytecodeVm&gt;(&amp;parsed.program, &amp;source) else {</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>PartialEq</code> 是 <code>args.backend == BackendChoice::Vm</code> 要用的。测试里的 <code>--vm</code> 要改成 <code>--backend vm</code>：我没改之前，<code>the_vm_and_the_interpreter_agree</code> 失败了，信息是 <code>error: unexpected argument '--vm' found</code>；改完之后 <code>pebble-cli</code> 的 3 个单元测试和 10 个集成测试全部通过。真实运行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s</span></div></div><div><div><div>3</div></div><div><span>$ pebble run -h</span></div></div><div><div><div>4</div></div><div><span>Execute a source file on the tree-walker or the bytecode VM</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Usage: pebble run [OPTIONS] &lt;FILE&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>Arguments:</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>&lt;FILE&gt;  The source file to execute</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Options:</span></div></div><div><div><div>12</div></div><div><span><span>      </span></span><span>--backend &lt;BACKEND&gt;  Which engine executes the program [default: tree-walker] [possible</span></div></div><div><div><div>13</div></div><div><span><span>                           </span></span><span>values: tree-walker, vm]</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>--ast                Print the parsed syntax tree before running</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>--dump-bytecode      Print the compiled bytecode disassembly before running</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>--quiet              Do not print the program's final value</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>-h, --help               Print help (see more with '--help')</span></div></div><div><div><div>18</div></div><div><span>$ pebble run --backend vm examples/fib.pebble</span></div></div><div><div><div>19</div></div><div><span>6765</span></div></div><div><div><div>20</div></div><div><span>$ pebble run --backend jit examples/fib.pebble</span></div></div><div><div><div>21</div></div><div><span>error: invalid value 'jit' for '--backend &lt;BACKEND&gt;'</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>[possible values: tree-walker, vm]</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>For more information, try '--help'.</span></div></div><div><div><div>25</div></div><div><span>(exit 2)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>填错取值时，clap 直接列出可选项并以 2 退出，这比手写一个 <code>String</code> 参数再自己匹配强得多。做完之后要还原，因为这个改动不在 tag 里。</p><hr /><p>两个后端都在了，差别也有了数字，而且大部分差距是我们自己的设计留下的。我最想做的下一个实验是顺着嫌疑一往下走：在编译期做一遍”名字到槽位”的静态分析，然后分别让树遍历解释器和 VM 吃它的结果。我的预测是两边都会快，VM 会快得更多，因为一条 <code>GetLocal(3)</code> 是一次数组索引，而一个树节点 <code>Var("i")</code> 要先找到那个字符串；但这是预测，不是数字，我没有做。</p><p>不过现在先把 Pebble 搁一搁。到目前为止，我们写的 Rust 几乎全是<strong>安全</strong>的，只有第 11 篇那个小小的 arena 例外。而真正的程序要和世界打交道：调用别人用 C 写的库，或者在没有操作系统的环境里运行。<a href="/posts/rust-by-pebble-13-ffi-nostd/">下一篇</a>我们越过这条边界：用 <code>pebble-sys</code> 从 Rust 调一个 C 写的 arena，让一个 Rust 闭包被 C 回调，再用 <code>pebble-nostd</code> 把整个求值器放进一个没有 <code>std</code> 的世界里，用我们自己的分配器。<code>unsafe</code> 会比第 11 篇多得多，我会尽量让每一行都有理由。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-11-gc-unsafe/</id>
      <title type="text">手写垃圾回收器：泛型容器、Cell 与第一次 unsafe</title>
      <published>2026-10-13T10:00:00.000Z</published>
      <updated>2026-10-13T10:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-11-gc-unsafe/"/>
      <summary type="text">第 9 篇我们亲手造出了 Rc 环泄漏，这一篇用一个泛型的标记清除回收器把它治好：索引句柄、Cell 内部可变性、显式工作表，再加一个用 unsafe 写成的 bump arena。</summary>
      <content type="html"><![CDATA[<p><a href="/posts/rust-by-pebble-09-interpreter-closures/">第 9 篇</a>结尾，我写了两个测试证明一件有点丢人的事：只要全局环境里存了任何一个函数，这个函数就抓着它定义时的环境，环境又抓着这个函数，<code>Rc</code> 的引用计数永远降不到零，整个全局环境在解释器销毁后依然躺在堆上。<code>Weak</code> 能治其中一种环，治不了”闭包存进它自己捕获的环境”这种一般形态。C++ 程序员对此毫不陌生，<code>shared_ptr</code> 的老毛病。治本的办法是不再数引用，改成从根出发走一遍，没走到的就是垃圾。这一篇我们从零写这个走一遍的东西。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇我们写完 <code>pebble-gc</code>：一个对对象类型泛型的标记清除（mark-sweep）回收器 <code>Heap&lt;O&gt;</code>，外加一个用 <code>unsafe</code> 写的 bump arena。涉及的 Rust 特性：泛型结构体与 <code>impl&lt;O: GcObject&gt;</code>、<code>&amp;mut dyn FnMut</code> 闭包 trait 对象、<code>Option::take</code>、<code>Cell&lt;bool&gt;</code> 内部可变性、显式工作表代替递归、外部 crate（<code>rand</code> 作为 dev-dependency）、<code>BTreeSet</code>/<code>VecDeque</code>/<code>HashSet</code>，以及 <code>unsafe</code>、<code>MaybeUninit</code>、裸指针、<code>ptr::copy_nonoverlapping</code>、<code>slice::from_raw_parts_mut</code>、<code>// SAFETY:</code> 注释。上一篇：<a href="/posts/rust-by-pebble-10-bytecode-macros/">字节码编译器</a>。这个 crate 不依赖任何别的 Pebble crate，所以做完它，<code>pebble-gc</code> 就和仓库里 <code>tutorial-v1</code> 标签上的版本逐字节一致。</p></div></div>
<section><h2>要解决什么问题<a href="#要解决什么问题"><span>#</span></a></h2><p>先把”跟踪式回收”说成一句话：<strong>从一组根（roots）出发，沿着对象之间的引用走一遍，走到的标记为活，没标记的全部回收。</strong> 想象仓库盘点：你从门口（根）出发，每走到一个货架就贴一张”已盘”的标签，再顺着货架上的指引单走向下一个货架；盘点结束，没贴标签的货架整排清走。环不是问题，贴了标签的货架不会再走第二次。</p><p>所以一个回收器要回答三个问题：</p><ol>
<li>对象存在哪里，怎么引用它？（句柄）</li>
<li>一个对象怎么告诉回收器”我引用了谁”?（<code>trace</code>）</li>
<li>谁来决定根是什么，什么时候回收？（嵌入方，也就是第 12 篇的 VM）</li>
</ol><p>这个 crate 的设计是：<code>Heap&lt;O&gt;</code> 对 <code>O</code> 泛型，<code>O</code> 实现 <code>GcObject</code> 来汇报自己的子引用；回收分两步，嵌入方先对每个根调用 <code>heap.mark(root)</code>，再调用 <code>heap.collect()</code> 清扫。回收器自己不知道什么是栈，什么是全局变量：根是嵌入方的事。</p><div><div><div><div><span><p>mark</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>根: 栈, 帧, 全局</p></span></div><div><span><p>标记阶段<br /><br />沿 trace 走遍可达对象</p></span></div><div><span><p>collect: 清扫<br /><br />未标记的槽位放回空闲表</p></span></div><div><span><p>空闲表<br /><br />alloc 复用</p></span></div><div><span><p>alloc</p></span></div>
</div><div><div><span><p>mark</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>根: 栈, 帧, 全局</p></span></div><div><span><p>标记阶段<br /><br />沿 trace 走遍可达对象</p></span></div><div><span><p>collect: 清扫<br /><br />未标记的槽位放回空闲表</p></span></div><div><span><p>空闲表<br /><br />alloc 复用</p></span></div><div><span><p>alloc</p></span></div>
</div></div></div></section>
<section><h2>建 crate<a href="#建-crate"><span>#</span></a></h2><p>在你的 workspace 根目录（第 1 篇建的那个）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-gc</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>根 <code>Cargo.toml</code> 里的 <code>members = ["crates/*"]</code> 会自动把它收进来。把 <code>crates/pebble-gc/Cargo.toml</code> 整个换成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-gc"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"A mark-sweep garbage collector written with raw pointers and unsafe."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>14</div></div><div><span>rand.workspace</span><span> = </span><span>true</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>17</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>[dev-dependencies]</code> 是只给测试、示例、基准用的依赖，不会链接进库本身。<code>rand.workspace = true</code> 的意思是版本号去根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 里找，所以给那一段加两行（其余行保持你已有的样子）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>workspace</span><span>.</span><span>dependencies</span><span>]</span></div></div><div><div><div>2</div></div><div><span>pebble-gc</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-gc"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>3</div></div><div><span>rand</span><span> = </span><span>"0.10"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pebble-gc</code> 那一行暂时没有人用，但 <code>tutorial-v1</code> 上的根 <code>Cargo.toml</code> 本来就列着它，第 12 篇的 VM 会通过它依赖这个 crate。</p><p>还要建一个空文件，后面会往里写 arena，先让 <code>pub mod bump;</code> 有地方可指：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>touch</span><span> </span><span>crates/pebble-gc/src/bump.rs</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>句柄：用下标，不用指针<a href="#句柄用下标不用指针"><span>#</span></a></h2><p>C 程序员的第一反应是：对象在堆上，引用就是指针。但在 Rust 里，“一个对象被很多地方引用，还会互相引用成环”恰好是借用检查器最不愿意看到的形状，用裸指针表达只能 <code>unsafe</code>。第 8 篇我们已经看过一个答案：<code>Rc&lt;RefCell&lt;..&gt;&gt;</code>，代价是引用计数和环。</p><p>这一篇用第三种办法，也是游戏引擎和 ECS 里的老手艺：<strong>对象放在一个 <code>Vec</code> 里，引用就是下标。</strong> 下标是个整数，可以随便复制，不涉及借用，不涉及生命周期，全程安全代码。</p><p>先把 <code>cargo new</code> 生成的 <code>src/lib.rs</code> 清空（里面有一个 <code>add</code> 函数和一个测试，我们不要）。</p><p>写 <code>crates/pebble-gc/src/lib.rs</code>。先从文件开头开始：crate 文档、一个 <code>use</code>、一个模块声明。这段文档比较长，因为它是整个 crate 的使用说明，而且里面的 <code># Example</code> 会被 <code>cargo test</code> 当成 doc test 编译运行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! A precise mark-sweep garbage collector plus a small bump arena for one-shot</span></div></div><div><div><div>2</div></div><div><span>//! allocations ([`bump`]).</span></div></div><div><div><div>3</div></div><div><span>//!</span></div></div><div><div><div>4</div></div><div><span>//! The collector is generic over the [`GcObject`] trait, so an embedder such as</span></div></div><div><div><div>5</div></div><div><span>//! the Pebble VM plugs in its own object enum and decides how each object</span></div></div><div><div><div>6</div></div><div><span>//! reports its child references. The heap owns a flat table of slots; an</span></div></div><div><div><div>7</div></div><div><span>//! embedder allocates with [`Heap::alloc`] and keeps the returned [`GcRef`]</span></div></div><div><div><div>8</div></div><div><span>//! handles in its own roots (value stack, globals, temporaries). Collection is</span></div></div><div><div><div>9</div></div><div><span>//! split into two embedder-driven phases:</span></div></div><div><div><div>10</div></div><div><span>//!</span></div></div><div><div><div>11</div></div><div><span>//! 1. Mark every root: `heap.mark(root)` for each handle the embedder still</span></div></div><div><div><div>12</div></div><div><span>//!    holds. Marking follows the children reported by [`GcObject::trace`]</span></div></div><div><div><div>13</div></div><div><span>//!    transitively.</span></div></div><div><div><div>14</div></div><div><span>//! 2. Sweep: `heap.collect()` returns every unmarked slot to the free list and</span></div></div><div><div><div>15</div></div><div><span>//!    clears the mark bits, ready for the next cycle.</span></div></div><div><div><div>16</div></div><div><span>//!</span></div></div><div><div><div>17</div></div><div><span>//! [`Heap::mark`] only needs `&amp;self`, so roots can be marked through a shared</span></div></div><div><div><div>18</div></div><div><span>//! reference; the mark bits live in [`Cell`]s inside the heap. There is no</span></div></div><div><div><div>19</div></div><div><span>//! global collector state and no `Drop`-based collection: reclamation happens</span></div></div><div><div><div>20</div></div><div><span>//! only when the embedder calls [`Heap::collect`].</span></div></div><div><div><div>21</div></div><div><span>//!</span></div></div><div><div><div>22</div></div><div><span>//! # Example</span></div></div><div><div><div>23</div></div><div><span>//!</span></div></div><div><div><div>24</div></div><div><span>//! ```</span></div></div><div><div><div>25</div></div><div><span>//! use pebble_gc::{GcObject, GcRef, Heap};</span></div></div><div><div><div>26</div></div><div><span>//!</span></div></div><div><div><div>27</div></div><div><span>//! /// A list-like object: it traces the references it holds.</span></div></div><div><div><div>28</div></div><div><span>//! struct Node(Vec&lt;GcRef&gt;);</span></div></div><div><div><div>29</div></div><div><span>//!</span></div></div><div><div><div>30</div></div><div><span>//! impl GcObject for Node {</span></div></div><div><div><div>31</div></div><div><span>//!     fn trace(&amp;self, mark: &amp;mut dyn FnMut(GcRef)) {</span></div></div><div><div><div>32</div></div><div><span>//!         for &amp;child in &amp;self.0 {</span></div></div><div><div><div>33</div></div><div><span>//!             mark(child);</span></div></div><div><div><div>34</div></div><div><span>//!         }</span></div></div><div><div><div>35</div></div><div><span>//!     }</span></div></div><div><div><div>36</div></div><div><span>//! }</span></div></div><div><div><div>37</div></div><div><span>//!</span></div></div><div><div><div>38</div></div><div><span>//! let mut heap: Heap&lt;Node&gt; = Heap::new();</span></div></div><div><div><div>39</div></div><div><span>//! let kept = heap.alloc(Node(vec![]));</span></div></div><div><div><div>40</div></div><div><span>//! let garbage = heap.alloc(Node(vec![]));</span></div></div><div><div><div>41</div></div><div><span>//!</span></div></div><div><div><div>42</div></div><div><span>//! heap.mark(kept);</span></div></div><div><div><div>43</div></div><div><span>//! heap.collect();</span></div></div><div><div><div>44</div></div><div><span>//!</span></div></div><div><div><div>45</div></div><div><span>//! assert_eq!(heap.live_count(), 1);</span></div></div><div><div><div>46</div></div><div><span>//! assert!(heap.is_alive(kept));</span></div></div><div><div><div>47</div></div><div><span>//! assert!(!heap.is_alive(garbage));</span></div></div><div><div><div>48</div></div><div><span>//! ```</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>cell</span><span>::</span></span><span>Cell</span><span>;</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>pub</span><span> </span><span>mod</span><span> bump;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>//!</code> 是给”外层条目”写的文档注释（这里外层是整个 crate），<code>///</code> 是给紧随其后的条目写的。写成 <code>[`Heap::alloc`]</code> 这样的方括号链接叫 intra-doc link，<code>cargo doc</code> 会把它解析成链接，写错了会报警告。<code>use core::cell::Cell;</code> 先放着，几节以后就用上。</p><p>接下来是句柄本身，把它追加在 <code>pub mod bump;</code> 后面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A handle to a live object inside a [`Heap`].</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// `GcRef` is a plain `u32` slot index and is `Copy`, so embedders can store</span></div></div><div><div><div>4</div></div><div><span>/// handles on their own stacks and in registers without touching the heap. A</span></div></div><div><div><div>5</div></div><div><span>/// handle only means something to the [`Heap`] that produced it, and it becomes</span></div></div><div><div><div>6</div></div><div><span>/// *dangling* once its slot is returned by [`Heap::free`] or reclaimed by</span></div></div><div><div><div>7</div></div><div><span>/// [`Heap::collect`]; [`Heap::is_alive`] tests for that without panicking.</span></div></div><div><div><div>8</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Hash</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>GcRef</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>GcRef</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>/// The underlying slot index of this handle.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>index</span><span>(</span><span>self</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span>.</span><span>0</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>/// Rebuilds a handle from a slot index.</span></div></div><div><div><div>18</div></div><div><span>    </span><span>///</span></div></div><div><div><div>19</div></div><div><span>    </span><span>/// The index must have come from [`GcRef::index`]; forging one for a slot</span></div></div><div><div><div>20</div></div><div><span>    </span><span>/// that is not live produces a dangling handle.</span></div></div><div><div><div>21</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>from_index</span><span>(</span><span>index</span><span>: </span><span>u32</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>Self</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这是第 1 篇学过的元组结构体 newtype，<code>Copy</code> 也是第 1 篇的”和 C 结构体一样按位复制”。字段 <code>u32</code> 没有 <code>pub</code>，所以外部代码造不出随便的句柄，只能通过 <code>from_index</code> 这个明说”你要对后果负责”的口子（测试和嵌入方需要它）。</p><p><code>u32</code> 而不是 <code>usize</code>，是因为 <code>Option&lt;GcRef&gt;</code> 之类的东西会因此更小，下标也不需要 64 位。我在实验里量过几个尺寸（<code>~/rust-lab/src/bin/ch11_sizes.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch11_sizes</span></div></div><div><div><div>2</div></div><div><span>GcRef                 = 4 bytes</span></div></div><div><div><div>3</div></div><div><span>*const u8             = 8 bytes</span></div></div><div><div><div>4</div></div><div><span>Object                = 48 bytes</span></div></div><div><div><div>5</div></div><div><span>Option&lt;Object&gt;        = 48 bytes</span></div></div><div><div><div>6</div></div><div><span>Cell&lt;bool&gt;            = 1 bytes</span></div></div><div><div><div>7</div></div><div><span>Option&lt;Box&lt;Object&gt;&gt;   = 8 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_sizes.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::size_of;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>#[allow(dead_code)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>GcRef</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#[allow(dead_code)]</span></div></div><div><div><div>8</div></div><div><span>enum</span><span> </span><span>Object</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>Str</span><span>(</span><span>String</span><span>),</span></div></div><div><div><div>10</div></div><div><span>    </span><span>List</span><span>(</span><span>Vec</span><span>&lt;</span><span>GcRef</span><span>&gt;),</span></div></div><div><div><div>11</div></div><div><span>    </span><span>Closure</span><span> { </span><span>name</span><span>: </span><span>String</span><span>, </span><span>captured</span><span>: </span><span>Vec</span><span>&lt;</span><span>GcRef</span><span>&gt; },</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"GcRef                 = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>GcRef</span><span>&gt;());</span></div></div><div><div><div>16</div></div><div><span>    </span><span>println!</span><span>(</span><span>"*const u8             = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;*</span><span>const</span><span> </span><span>u8</span><span>&gt;());</span></div></div><div><div><div>17</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Object                = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Object</span><span>&gt;());</span></div></div><div><div><div>18</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Option&lt;Object&gt;        = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Option</span><span>&lt;</span><span>Object</span><span>&gt;&gt;());</span></div></div><div><div><div>19</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Cell&lt;bool&gt;            = {} bytes"</span><span>, </span><span>size_of</span><span><span>::&lt;</span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>Cell</span><span>&lt;</span><span>bool</span><span>&gt;&gt;());</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Option&lt;Box&lt;Object&gt;&gt;   = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Option</span><span>&lt;</span><span>Box</span><span>&lt;</span><span>Object</span><span>&gt;&gt;&gt;());</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>句柄 4 字节，裸指针 8 字节。另外有一个值得记住的数字：<code>Option&lt;Object&gt;</code> 和 <code>Object</code> 一样大，都是 48 字节。<code>Option</code> 没有多出标签，因为 <code>Object</code> 自己的判别标签只用了 3 个取值，<code>None</code> 借用了一个没用到的取值（Rust 管这叫 niche 优化；我没有拆开布局确认到底借的是哪一个，这是我的推断）。后面我们的槽位是 <code>Vec&lt;Option&lt;O&gt;&gt;</code>，“这个槽位空着”这件事是免费的。</p><section><h3>下标句柄的代价<a href="#下标句柄的代价"><span>#</span></a></h3><p>句柄是安全的，不代表句柄永远指向对的东西。下标指向”第 k 个槽位”，槽位被释放后再被复用，旧句柄就悄悄指向了新对象，没有任何 UB，也没有任何 panic。我用真正的 <code>Heap</code>（等你写完这一章再跑）做了一个实验。它需要依赖你的 <code>pebble-gc</code>，所以不放在 <code>rust-lab</code> 里，而是单独建一个小 crate：<code>cargo new ~/lab-ch11-gc</code>，在它的 <code>Cargo.toml</code> 的 <code>[dependencies]</code> 下加一行 <code>pebble-gc = { path = "&lt;你的 workspace&gt;/crates/pebble-gc" }</code>，把下面的完整源码放进 <code>src/main.rs</code>（完整版在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch11-gc" target="_blank"><code>lab/lab-ch11-gc</code></a>，在那个目录里运行即可）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin lab-ch11-gc</span></div></div><div><div><div>2</div></div><div><span>alive after free: false</span></div></div><div><div><div>3</div></div><div><span>old == new: true</span></div></div><div><div><div>4</div></div><div><span>old now reads as "second"</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：src/main.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// An index handle can dangle without any memory unsafety: it just points at</span></div></div><div><div><div>2</div></div><div><span>// whatever lives in that slot now.</span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>pebble_gc</span><span>::{</span></span><span>GcObject</span><span>, </span><span>GcRef</span><span>, </span><span>Heap</span><span>};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>6</div></div><div><span>struct</span><span> </span><span>Name</span><span>(&amp;'</span><span>static</span><span> </span><span>str</span><span>);</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>GcObject</span><span> </span><span>for</span><span> </span><span>Name</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>_mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>)) {}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span>: </span><span>Heap</span><span>&lt;</span><span>Name</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Heap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>old</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span>(</span><span>Name</span><span>(</span><span>"first"</span><span>));</span></div></div><div><div><div>15</div></div><div><span>    </span><span>heap</span><span>.</span><span>free</span><span>(</span><span>old</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>println!</span><span>(</span><span>"alive after free: {}"</span><span>, </span><span>heap</span><span>.</span><span>is_alive</span><span>(</span><span>old</span><span>));</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>new</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span>(</span><span>Name</span><span>(</span><span>"second"</span><span>));</span></div></div><div><div><div>19</div></div><div><span>    </span><span>println!</span><span>(</span><span>"old == new: {}"</span><span>, </span><span>old</span><span><span> </span><span>==</span><span> </span></span><span>new</span><span>);</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"old now reads as {:?}"</span><span>, </span><span>heap</span><span>.</span><span>get</span><span>(</span><span>old</span><span>).</span><span>0</span><span>);</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>old</code> 释放之后 <code>is_alive</code> 说它死了，但新对象立刻复用了同一个槽位，<code>old == new</code> 为 <code>true</code>。这就是 C 里的 use-after-free 的”安全版本”：不会崩，但会读到别人的数据。防御办法是给句柄加一个代数（generation）计数，槽位每次复用就加一，句柄里记着自己出生时的代数。这是练习 1，我们这一版不做，因为 VM 只在指令边界上回收，栈和帧里的句柄全部是根，不会悬空。设计成”只要嵌入方守规矩就够用”是个明确的取舍，文档里也写了。</p></section></section>
<section><h2>对象怎么汇报自己的引用<a href="#对象怎么汇报自己的引用"><span>#</span></a></h2><p>回收器不知道 <code>O</code> 是什么，所以需要一个 trait 让对象自己说出引用了谁。接着写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A heap-managed object that can report its child references.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// The collector calls [`GcObject::trace`] once per slot while marking, passing</span></div></div><div><div><div>4</div></div><div><span>/// a callback that records one child reference. An implementation must report</span></div></div><div><div><div>5</div></div><div><span>/// every live [`GcRef`] it owns, and nothing else: a reference it fails to</span></div></div><div><div><div>6</div></div><div><span>/// report may be swept while still in use, and one it invents may keep garbage</span></div></div><div><div><div>7</div></div><div><span>/// alive forever.</span></div></div><div><div><div>8</div></div><div><span>pub</span><span> </span><span>trait</span><span> </span><span>GcObject</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// Reports each child reference to `mark`, so the collector can mark it.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>));</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>参数 <code>mark: &amp;mut dyn FnMut(GcRef)</code> 值得停一下。它读作”一个可变借用，指向某个实现了 <code>FnMut(GcRef)</code> 的闭包”。<code>dyn</code> 是第 8 篇讲过的 trait 对象：运行时通过虚表调用，和 C++ 的虚函数一个原理。为什么不写成泛型 <code>fn trace&lt;F: FnMut(GcRef)&gt;(&amp;self, mark: F)</code>？那也可以（带来的是每个闭包类型各编译一份），我选 <code>dyn</code> 是为了让 trait 保持简单、<code>trace</code> 只编译一份，代价是每汇报一个子引用多一次间接调用。我没有为它测过时间，很可能在这里无关紧要。</p><p><code>FnMut</code> 是第 9 篇讲过的”会修改自己捕获的状态的闭包”。待会儿 <code>mark</code> 传进来的闭包要往 <code>pending</code> 这个 <code>Vec</code> 里 push，所以必须是 <code>FnMut</code>。</p></section>
<section><h2>Heap：一个泛型结构体<a href="#heap一个泛型结构体"><span>#</span></a></h2><p>现在是主角。先看结构和 <code>new</code>（接着追加）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A mark-sweep heap over a flat table of slots, generic over the object type.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Freed slots are recycled: the slot table grows to the peak number of</span></div></div><div><div><div>4</div></div><div><span>/// simultaneously live objects, not to the total number of allocations ever</span></div></div><div><div><div>5</div></div><div><span>/// performed.</span></div></div><div><div><div>6</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>7</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Heap</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>slots</span><span>: </span><span>Vec</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>O</span><span>&gt;&gt;,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>free</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span>&gt;,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>marked</span><span>: </span><span>Vec</span><span>&lt;</span><span>Cell</span><span>&lt;</span><span>bool</span><span>&gt;&gt;,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>allocations_since_gc</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>impl</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>/// Creates an empty heap with no slots allocated.</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>slots</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>19</div></div><div><span>            </span><span>free</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>20</div></div><div><span>            </span><span>marked</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>21</div></div><div><span>            </span><span>allocations_since_gc</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这个 <code>impl</code> 块没有收尾，后面的方法都往里放，最后我们会补上闭合的 <code>}</code>。</p><p>四个字段：</p><ul>
<li><code>slots: Vec&lt;Option&lt;O&gt;&gt;</code>：槽位表。<code>Some(object)</code> 是活对象，<code>None</code> 是空槽。</li>
<li><code>free: Vec&lt;u32&gt;</code>：空槽的下标栈。<code>alloc</code> 先从这里弹，弹不出来才扩容。C 里叫 free list。</li>
<li><code>marked: Vec&lt;Cell&lt;bool&gt;&gt;</code>：每个槽位一个标记位。为什么是 <code>Cell&lt;bool&gt;</code> 不是 <code>bool</code>，下一节用编译器的报错说话。</li>
<li><code>allocations_since_gc</code>：自上次回收以来分配了多少次，嵌入方拿它决定什么时候该回收。</li>
</ul><p><code>Heap&lt;O: GcObject&gt;</code> 是<strong>泛型结构体</strong>：<code>O</code> 是类型参数，冒号后面是约束，“O 必须实现 <code>GcObject</code>”。对 C++ 程序员：这是 <code>template &lt;typename O&gt; requires GcObject&lt;O&gt; struct Heap</code>，只不过 Rust 的约束是 trait 而且在定义处就检查，而不是等实例化后在模板深处报一屏错误。和 C++ 模板一样，Rust 泛型是单态化的：<code>Heap&lt;Node&gt;</code> 和 <code>Heap&lt;VmObject&gt;</code> 在编译后是两份独立的机器码，没有运行时开销。</p><p><code>impl&lt;O: GcObject&gt; Heap&lt;O&gt;</code> 里的第一个 <code>&lt;O: GcObject&gt;</code> 是”为所有满足约束的 O 实现”，第二个 <code>Heap&lt;O&gt;</code> 是被实现的类型。约束需要在 <code>struct</code> 和 <code>impl</code> 两处各写一遍，这是 Rust 的老规矩。</p><p><code>#[derive(Debug)]</code> 在泛型结构体上会自动生成 <code>impl&lt;O: GcObject + Debug&gt; Debug for Heap&lt;O&gt;</code>：只有 <code>O</code> 自己能打印时，<code>Heap&lt;O&gt;</code> 才能打印。</p><section><h3>alloc、get、get_mut、free<a href="#allocgetget_mutfree"><span>#</span></a></h3><p>继续往 <code>impl</code> 里追加（缩进 4 个空格，放在 <code>new</code> 后面）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Allocates `object` and returns a handle to it.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// A slot on the free list is reused if one is available; otherwise the</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// slot table grows by one entry.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>///</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>7</div></div><div><span>    </span><span>///</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Panics if the heap would need more than `u32::MAX` slots.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>alloc</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>object</span><span>: </span><span>O</span><span>) -&gt; </span><span>GcRef</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span><span>.allocations_since_gc </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>index</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.free.</span><span>pop</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>self</span><span>.slots[</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>];</span></div></div><div><div><div>13</div></div><div><span>            </span><span>debug_assert!</span><span>(</span><span>slot</span><span>.</span><span>is_none</span><span>(), </span><span>"free list contained an occupied slot"</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>*</span><span>slot</span><span><span> </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>object</span><span>);</span></div></div><div><div><div>15</div></div><div><span>            </span><span>self</span><span>.marked[</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>].</span><span>set</span><span>(</span><span>false</span><span>);</span></div></div><div><div><div>16</div></div><div><span>            </span><span>GcRef</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>u32</span><span>::</span><span>try_from</span><span>(</span><span>self</span><span>.slots.</span><span>len</span><span>()).</span><span>expect</span><span>(</span><span>"heap outgrew its u32 slot index"</span><span>);</span></div></div><div><div><div>19</div></div><div><span>            </span><span>self</span><span>.slots.</span><span>push</span><span>(</span><span>Some</span><span>(</span><span>object</span><span>));</span></div></div><div><div><div>20</div></div><div><span>            </span><span>self</span><span>.marked.</span><span>push</span><span><span>(</span><span>Cell</span><span>::</span></span><span>new</span><span>(</span><span>false</span><span>));</span></div></div><div><div><div>21</div></div><div><span>            </span><span>GcRef</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个第 1 篇到第 8 篇的老朋友：<code>if let</code> 解开 <code>Option</code>；<code>debug_assert!</code> 只在 debug 构建检查；<code>u32::try_from(usize)</code> 是可能失败的转换，<code>expect</code> 失败时 panic 并带上消息（第 7 篇讲过 <code>unwrap</code>/<code>expect</code> 什么时候可以用：这里是”不可能发生，发生了就是 bug”，并且文档里有 <code># Panics</code> 一节写明）。<code>*slot = Some(object)</code> 把 <code>object</code> <strong>移动</strong>进槽位。<code>object</code> 从此归 <code>Heap</code> 所有，调用者拿到的只是一个 <code>GcRef</code>。</p><p>接下来读写：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Returns a shared reference to the object behind `r`.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>4</div></div><div><span>    </span><span>///</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// Panics if `r` is out of range or points to a slot that is currently</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// free, i.e. a dangling handle. Use [`Heap::is_alive`] to check first.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>get</span><span>(&amp;</span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) -&gt; &amp;</span><span>O</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span>.slots</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>.</span><span>get</span><span>(</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>.</span><span>and_then</span><span><span>(</span><span>|</span></span><span>slot</span><span><span>|</span><span> </span></span><span>slot</span><span>.</span><span>as_ref</span><span>())</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>unwrap_or_else</span><span><span>(</span><span>||</span><span> </span></span><span>panic!</span><span>(</span><span>"GcRef({}) does not point to a live object"</span><span>, </span><span>r</span><span>.</span><span>0</span><span>))</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>/// Returns a mutable reference to the object behind `r`.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>///</span></div></div><div><div><div>16</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>17</div></div><div><span>    </span><span>///</span></div></div><div><div><div>18</div></div><div><span>    </span><span>/// Panics under the same conditions as [`Heap::get`].</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>get_mut</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) -&gt; &amp;</span><span>mut</span><span> </span><span>O</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span>.slots</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>.</span><span>get_mut</span><span>(</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>.</span><span>and_then</span><span><span>(</span><span>|</span></span><span>slot</span><span><span>|</span><span> </span></span><span>slot</span><span>.</span><span>as_mut</span><span>())</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>.</span><span>unwrap_or_else</span><span><span>(</span><span>||</span><span> </span></span><span>panic!</span><span>(</span><span>"GcRef({}) does not point to a live object"</span><span>, </span><span>r</span><span>.</span><span>0</span><span>))</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>slots.get(i)</code> 返回 <code>Option&lt;&amp;Option&lt;O&gt;&gt;</code>（下标越界得 <code>None</code>），<code>and_then(|slot| slot.as_ref())</code> 把里层的 <code>&amp;Option&lt;O&gt;</code> 变成 <code>Option&lt;&amp;O&gt;</code> 并压平，<code>unwrap_or_else</code> 在 <code>None</code> 时才执行 panic 闭包（闭包是惰性的，<code>unwrap_or(panic!(..))</code> 会不管三七二十一先 panic）。</p><p>两个签名的借用关系是这一整套设计的核心：<code>get(&amp;self, r) -&gt; &amp;O</code>，返回的引用借用整个 <code>Heap</code>；<code>get_mut(&amp;mut self, ..) -&gt; &amp;mut O</code> 借用整个 <code>Heap</code> 的独占。第 2 篇说过”别名 XOR 可变”。落到这里就是：你拿着一个 <code>&amp;mut O</code> 的时候，不能再 <code>alloc</code>（<code>alloc</code> 可能让 <code>slots</code> 重新分配，<code>&amp;mut O</code> 就悬空了，这正是 C++ 里 <code>vector</code> push 之后旧指针失效的 bug），编译器直接拒绝。句柄 <code>GcRef</code> 因为是 <code>Copy</code> 整数，不受这个限制，所以对象之间互相引用时用句柄，需要读写内容时才临时借出引用。</p><p>再来 <code>free</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Returns the slot behind `r` to the free list.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Any mark bit on the slot is cleared, so the slot is immediately</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// reusable by [`Heap::alloc`].</span></div></div><div><div><div>5</div></div><div><span>    </span><span>///</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>7</div></div><div><span>    </span><span>///</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Panics if `r` is out of range or already free, which usually means the</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// embedder double-freed or used a dangling handle.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>free</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>12</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>13</div></div><div><span>            </span><span>self</span><span>.slots</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>.</span><span>get_mut</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>.</span><span>and_then</span><span><span>(</span><span>|</span></span><span>slot</span><span><span>|</span><span> </span></span><span>slot</span><span>.</span><span>take</span><span>())</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>.</span><span>is_some</span><span>(),</span></div></div><div><div><div>17</div></div><div><span>            </span><span>"GcRef({}) does not point to a live object"</span><span>,</span></div></div><div><div><div>18</div></div><div><span>            </span><span>r</span><span>.</span><span>0</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span>.marked[</span><span>index</span><span>].</span><span>set</span><span>(</span><span>false</span><span>);</span></div></div><div><div><div>21</div></div><div><span>        </span><span>self</span><span>.free.</span><span>push</span><span>(</span><span>r</span><span>.</span><span>0</span><span>);</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Option::take()</code> 是这一节的新朋友：它把 <code>Option</code> 里的值拿走，原位置留下 <code>None</code>，返回原值。这就是第 2 篇那个 <code>mem::take</code> 的 <code>Option</code> 特化版：不能直接把 <code>O</code> 从 <code>&amp;mut Option&lt;O&gt;</code> 里”移出来”（会留下一个被移走的洞，和 C++ 的 moved-from 对象不同，Rust 不允许有这种半死状态），<code>take</code> 用 <code>None</code> 填上洞。这里还顺手利用了 <code>take</code> 的返回值做断言：取出来是 <code>None</code> 说明槽位本来就是空的，即重复释放。被取出的 <code>O</code> 在语句结束时被丢弃（<code>Drop</code>），它拥有的 <code>String</code>、<code>Vec</code> 随之释放。</p><p>可以先 <code>cargo check -p pebble-gc</code> 看看到现在为止有没有拼写错误。（它会抱怨 <code>impl</code> 没闭合，因为我们还没写完；先把下面的内容追加完再检查也可以。）</p></section></section>
<section><h2>标记：为什么 mark 只要 &amp;self<a href="#标记为什么-mark-只要-self"><span>#</span></a></h2><p>现在是回收器的心脏。我先给你看我最初写的版本，也就是你可能会写的版本：递归，标记位用普通的 <code>Vec&lt;bool&gt;</code>。<code>~/rust-lab/src/bin/ch11_cell_naive.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>mark</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> </span><span>self</span><span>.marked[</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>] {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>    </span><span>self</span><span>.marked[</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span><span>] </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>self</span><span>.</span><span>get</span><span>(</span><span>r</span><span>).</span><span>trace</span><span>(&amp;</span><span>mut</span><span><span> </span><span>|</span></span><span>child</span><span><span>|</span><span> </span></span><span>self</span><span>.</span><span>mark</span><span>(</span><span>child</span><span>));</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch11_cell_naive</span></div></div><div><div><div>2</div></div><div><span>error[E0500]: closure requires unique access to `*self` but it is already borrowed</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch11_cell_naive.rs:24:32</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>24 |         self.get(r).trace(&amp;mut |child| self.mark(child));</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|         ----        -----      ^^^^^^^ ---- second borrow occurs due to use of `*self` in closure</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|         |           |          |</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|         |           |          closure construction occurs here</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|         |           first borrow later used by call</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|         borrow occurs here</span></div></div><div><div><div>11</div></div><div><span>For more information about this error, try `rustc --explain E0500`.</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_cell_naive.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// First attempt at `mark`: recursion, with the mark bits in a plain Vec&lt;bool&gt;.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>GcRef</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>trait</span><span> </span><span>GcObject</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>));</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>struct</span><span> </span><span>Heap</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>slots</span><span>: </span><span>Vec</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>O</span><span>&gt;&gt;,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>marked</span><span>: </span><span>Vec</span><span>&lt;</span><span>bool</span><span>&gt;,</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>impl</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>get</span><span>(&amp;</span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) -&gt; &amp;</span><span>O</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.slots[</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>].</span><span>as_ref</span><span>().</span><span>unwrap</span><span>()</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>fn</span><span> </span><span>mark</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.marked[</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>] {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>return</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>        </span><span>self</span><span>.marked[</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span><span>] </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>24</div></div><div><span>        </span><span>self</span><span>.</span><span>get</span><span>(</span><span>r</span><span>).</span><span>trace</span><span>(&amp;</span><span>mut</span><span><span> </span><span>|</span></span><span>child</span><span><span>|</span><span> </span></span><span>self</span><span>.</span><span>mark</span><span>(</span><span>child</span><span>));</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>struct</span><span> </span><span>Node</span><span>(</span><span>Vec</span><span>&lt;</span><span>GcRef</span><span>&gt;);</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>impl</span><span> </span><span>GcObject</span><span> </span><span>for</span><span> </span><span>Node</span><span> {</span></div></div><div><div><div>31</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>)) {</span></div></div><div><div><div>32</div></div><div><span>        </span><span>for</span><span> &amp;</span><span>child</span><span> </span><span>in</span><span> &amp;</span><span>self</span><span>.</span><span>0</span><span> {</span></div></div><div><div><div>33</div></div><div><span>            </span><span>mark</span><span>(</span><span>child</span><span>);</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>39</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span><span> </span><span>=</span><span> </span></span><span>Heap</span><span> { </span><span>slots</span><span>: </span><span>vec!</span><span>[</span><span>Some</span><span>(</span><span>Node</span><span>(</span><span>vec!</span><span>[]))], </span><span>marked</span><span>: </span><span>vec!</span><span>[</span><span>false</span><span>] };</span></div></div><div><div><div>40</div></div><div><span>    </span><span>heap</span><span>.</span><span>mark</span><span>(</span><span>GcRef</span><span>(</span><span>0</span><span>));</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（完整输出还有一行 <code>error: could not compile</code>，我省掉了。）它说的事情很直白：<code>self.get(r)</code> 借出了 <code>&amp;O</code>，这个借用要一直活到 <code>trace</code> 调用结束；而闭包里的 <code>self.mark(child)</code> 需要 <code>&amp;mut self</code>。你在读一个对象的同时要修改整个堆，这在 C++ 里也许会通过（然后在 <code>vector</code> 重新分配时爆炸），在 Rust 里直接拒绝。</p><p>办法一：不要递归。用一个局部的工作表（worklist）<code>pending: Vec&lt;GcRef&gt;</code>，闭包只往这个局部变量里 push，不碰 <code>self</code>。这样 <code>&amp;mut self</code> 版本其实<strong>能编译</strong>（<code>ch11_cell_roots.rs</code> 里的 <code>Heap::mark</code> 就是这样，它编译通过了）。所以 <code>Cell</code> 不是被”逼出来”的，这一点我想说清楚，免得你以为借用检查器非要它不可。</p><p>那为什么还用 <code>Cell</code>？因为问题出在调用者。<code>mark(&amp;mut self)</code> 要求调用者手里没有任何对堆的借用。这是个设计选择：让根的枚举可以一边持有对堆的共享借用一边调用 <code>mark</code>，比如”凡是被钉住的对象都是根”（<code>ch11_cell_roots.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> (</span><span>r</span><span>, </span><span>node</span><span>) </span><span>in</span><span> </span><span>heap</span><span>.</span><span>iter</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> </span><span>node</span><span>.pinned {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>heap</span><span>.</span><span>mark</span><span>(</span><span>r</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch11_cell_roots</span></div></div><div><div><div>2</div></div><div><span>error[E0502]: cannot borrow `heap` as mutable because it is also borrowed as immutable</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch11_cell_roots.rs:58:13</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>56 |     for (r, node) in heap.iter() {</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|                      -----------</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|                      |</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|                      immutable borrow occurs here</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|                      immutable borrow later used here</span></div></div><div><div><div>10</div></div><div><span>57 |         if node.pinned {</span></div></div><div><div><div>11</div></div><div><span>58 |             heap.mark(r);</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|             ^^^^^^^^^^^^ mutable borrow occurs here</span></div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0502`.</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_cell_roots.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Second attempt: no recursion (a worklist), mark bits still a plain Vec&lt;bool&gt;.</span></div></div><div><div><div>2</div></div><div><span>// Marking now compiles on its own. Trouble starts at the caller, which picks</span></div></div><div><div><div>3</div></div><div><span>// roots by looking at the heap it is about to mark.</span></div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>GcRef</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>trait</span><span> </span><span>GcObject</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>));</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>Heap</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>slots</span><span>: </span><span>Vec</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>O</span><span>&gt;&gt;,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>marked</span><span>: </span><span>Vec</span><span>&lt;</span><span>bool</span><span>&gt;,</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>impl</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>mark</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>root</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>pending</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>root</span><span>];</span></div></div><div><div><div>19</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>current</span><span><span>) </span><span>=</span><span> </span></span><span>pending</span><span>.</span><span>pop</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>current</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>21</div></div><div><span>            </span><span>let</span><span> </span><span>Some</span><span>(</span><span>object</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.slots[</span><span>index</span><span>].</span><span>as_ref</span><span>() </span><span>else</span><span> { </span><span>continue</span><span> };</span></div></div><div><div><div>22</div></div><div><span>            </span><span>if</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>replace</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.marked[</span><span>index</span><span>], </span><span>true</span><span>) {</span></div></div><div><div><div>23</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>            </span><span>object</span><span>.</span><span>trace</span><span>(&amp;</span><span>mut</span><span><span> </span><span>|</span></span><span>child</span><span><span>|</span><span> </span></span><span>pending</span><span>.</span><span>push</span><span>(</span><span>child</span><span>));</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>fn</span><span> </span><span>iter</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span><span> </span><span>=</span><span> (</span></span><span>GcRef</span><span>, &amp;</span><span>O</span><span>)&gt; {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>self</span><span>.slots</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>.</span><span>enumerate</span><span>()</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>.</span><span>filter_map</span><span><span>(</span><span>|</span><span>(</span></span><span>i</span><span>, </span><span>slot</span><span><span>)</span><span>|</span><span> </span></span><span>slot</span><span>.</span><span>as_ref</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>o</span><span><span>|</span><span> (</span></span><span>GcRef</span><span>(</span><span>i</span><span> </span><span>as</span><span> </span><span>u32</span><span>), </span><span>o</span><span>)))</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>struct</span><span> </span><span>Node</span><span> {</span></div></div><div><div><div>38</div></div><div><span>    </span><span>pinned</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>39</div></div><div><span>    </span><span>children</span><span>: </span><span>Vec</span><span>&lt;</span><span>GcRef</span><span>&gt;,</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>impl</span><span> </span><span>GcObject</span><span> </span><span>for</span><span> </span><span>Node</span><span> {</span></div></div><div><div><div>43</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>)) {</span></div></div><div><div><div>44</div></div><div><span>        </span><span>for</span><span> &amp;</span><span>child</span><span> </span><span>in</span><span> &amp;</span><span>self</span><span>.children {</span></div></div><div><div><div>45</div></div><div><span>            </span><span>mark</span><span>(</span><span>child</span><span>);</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>51</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span><span> </span><span>=</span><span> </span></span><span>Heap</span><span> {</span></div></div><div><div><div>52</div></div><div><span>        </span><span>slots</span><span>: </span><span>vec!</span><span>[</span><span>Some</span><span>(</span><span>Node</span><span> { </span><span>pinned</span><span>: </span><span>true</span><span>, </span><span>children</span><span>: </span><span>vec!</span><span>[] })],</span></div></div><div><div><div>53</div></div><div><span>        </span><span>marked</span><span>: </span><span>vec!</span><span>[</span><span>false</span><span>],</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>55</div></div><div><span>    </span><span>// Every pinned object is a root.</span></div></div><div><div><div>56</div></div><div><span>    </span><span>for</span><span> (</span><span>r</span><span>, </span><span>node</span><span>) </span><span>in</span><span> </span><span>heap</span><span>.</span><span>iter</span><span>() {</span></div></div><div><div><div>57</div></div><div><span>        </span><span>if</span><span> </span><span>node</span><span>.pinned {</span></div></div><div><div><div>58</div></div><div><span>            </span><span>heap</span><span>.</span><span>mark</span><span>(</span><span>r</span><span>);</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>61</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>heap.iter()</code> 持有堆的共享借用，而 <code>mark</code> 想要独占。可是”标记”在逻辑上并不修改堆里的对象，它只是翻转一个旁路的标记位。这正是<strong>内部可变性</strong>（interior mutability）的用武之地：把标记位放进 <code>Cell&lt;bool&gt;</code>，<code>mark</code> 就能以 <code>&amp;self</code> 签名工作。<code>ch11_cell.rs</code> 里换成 <code>Cell</code> 之后同样的调用者编译通过，运行结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch11_cell</span></div></div><div><div><div>2</div></div><div><span>[true, true, false]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_cell.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The fix: mark bits in `Cell&lt;bool&gt;`, so `mark` takes `&amp;self`.</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>Cell</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>GcRef</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>trait</span><span> </span><span>GcObject</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>));</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>Heap</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>slots</span><span>: </span><span>Vec</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>O</span><span>&gt;&gt;,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>marked</span><span>: </span><span>Vec</span><span>&lt;</span><span>Cell</span><span>&lt;</span><span>bool</span><span>&gt;&gt;,</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>impl</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>mark</span><span>(&amp;</span><span>self</span><span>, </span><span>root</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>pending</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>root</span><span>];</span></div></div><div><div><div>19</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>current</span><span><span>) </span><span>=</span><span> </span></span><span>pending</span><span>.</span><span>pop</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>current</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>21</div></div><div><span>            </span><span>let</span><span> </span><span>Some</span><span>(</span><span>object</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.slots[</span><span>index</span><span>].</span><span>as_ref</span><span>() </span><span>else</span><span> { </span><span>continue</span><span> };</span></div></div><div><div><div>22</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.marked[</span><span>index</span><span>].</span><span>replace</span><span>(</span><span>true</span><span>) {</span></div></div><div><div><div>23</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>            </span><span>object</span><span>.</span><span>trace</span><span>(&amp;</span><span>mut</span><span><span> </span><span>|</span></span><span>child</span><span><span>|</span><span> </span></span><span>pending</span><span>.</span><span>push</span><span>(</span><span>child</span><span>));</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>fn</span><span> </span><span>iter</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span><span> </span><span>=</span><span> (</span></span><span>GcRef</span><span>, &amp;</span><span>O</span><span>)&gt; {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>self</span><span>.slots</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>.</span><span>enumerate</span><span>()</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>.</span><span>filter_map</span><span><span>(</span><span>|</span><span>(</span></span><span>i</span><span>, </span><span>slot</span><span><span>)</span><span>|</span><span> </span></span><span>slot</span><span>.</span><span>as_ref</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>o</span><span><span>|</span><span> (</span></span><span>GcRef</span><span>(</span><span>i</span><span> </span><span>as</span><span> </span><span>u32</span><span>), </span><span>o</span><span>)))</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>struct</span><span> </span><span>Node</span><span> {</span></div></div><div><div><div>38</div></div><div><span>    </span><span>pinned</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>39</div></div><div><span>    </span><span>children</span><span>: </span><span>Vec</span><span>&lt;</span><span>GcRef</span><span>&gt;,</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>impl</span><span> </span><span>GcObject</span><span> </span><span>for</span><span> </span><span>Node</span><span> {</span></div></div><div><div><div>43</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>)) {</span></div></div><div><div><div>44</div></div><div><span>        </span><span>for</span><span> &amp;</span><span>child</span><span> </span><span>in</span><span> &amp;</span><span>self</span><span>.children {</span></div></div><div><div><div>45</div></div><div><span>            </span><span>mark</span><span>(</span><span>child</span><span>);</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>51</div></div><div><span>    </span><span>let</span><span> </span><span>heap</span><span><span> </span><span>=</span><span> </span></span><span>Heap</span><span> {</span></div></div><div><div><div>52</div></div><div><span>        </span><span>slots</span><span>: </span><span>vec!</span><span>[</span></div></div><div><div><div>53</div></div><div><span>            </span><span>Some</span><span>(</span><span>Node</span><span> { </span><span>pinned</span><span>: </span><span>true</span><span>, </span><span>children</span><span>: </span><span>vec!</span><span>[</span><span>GcRef</span><span>(</span><span>1</span><span>)] }),</span></div></div><div><div><div>54</div></div><div><span>            </span><span>Some</span><span>(</span><span>Node</span><span> { </span><span>pinned</span><span>: </span><span>false</span><span>, </span><span>children</span><span>: </span><span>vec!</span><span>[] }),</span></div></div><div><div><div>55</div></div><div><span>            </span><span>Some</span><span>(</span><span>Node</span><span> { </span><span>pinned</span><span>: </span><span>false</span><span>, </span><span>children</span><span>: </span><span>vec!</span><span>[] }),</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>],</span></div></div><div><div><div>57</div></div><div><span>        </span><span>marked</span><span>: </span><span>vec!</span><span>[</span><span>Cell</span><span>::</span><span>new</span><span>(</span><span>false</span><span>), </span><span>Cell</span><span>::</span><span>new</span><span>(</span><span>false</span><span>), </span><span>Cell</span><span>::</span><span>new</span><span>(</span><span>false</span><span>)],</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>59</div></div><div><span>    </span><span>for</span><span> (</span><span>r</span><span>, </span><span>node</span><span>) </span><span>in</span><span> </span><span>heap</span><span>.</span><span>iter</span><span>() {</span></div></div><div><div><div>60</div></div><div><span>        </span><span>if</span><span> </span><span>node</span><span>.pinned {</span></div></div><div><div><div>61</div></div><div><span>            </span><span>heap</span><span>.</span><span>mark</span><span>(</span><span>r</span><span>);</span></div></div><div><div><div>62</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>64</div></div><div><span>    </span><span>let</span><span> </span><span>marks</span><span>: </span><span>Vec</span><span>&lt;</span><span>bool</span><span><span>&gt; </span><span>=</span><span> </span></span><span>heap</span><span>.marked.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>Cell</span><span>::</span></span><span>get</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>65</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{marks:?}"</span><span>);</span></div></div><div><div><div>66</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>槽位 0 是被钉住的根，它引用槽位 1，所以 0 和 1 被标记，槽位 2 没有。</p><section><h3>Cell 是什么<a href="#cell-是什么"><span>#</span></a></h3><p><code>Cell&lt;T&gt;</code> 是 <code>RefCell</code> 的更简单的亲戚（<code>RefCell</code> 第 8 篇讲过）。区别：</p>

<table><thead><tr><th></th><th><code>RefCell&lt;T&gt;</code></th><th><code>Cell&lt;T&gt;</code></th></tr></thead><tbody><tr><td>怎么读写</td><td>借出 <code>Ref</code>/<code>RefMut</code> 引用</td><td>整个值拷进拷出，从不借出引用</td></tr><tr><td>运行时检查</td><td>有，借用冲突 panic</td><td>没有，也不需要</td></tr><tr><td>额外空间</td><td>一个借用计数</td><td>没有（上面量过：<code>Cell&lt;bool&gt;</code> 1 字节）</td></tr><tr><td>适用</td><td>任意 <code>T</code></td><td>小的、<code>Copy</code> 的值</td></tr></tbody></table><p>因为 <code>Cell</code> 从不把引用借给外人，不会出现”我读着你改了”的情形，所以不需要运行时检查。<code>replace(true)</code> 一次性写入新值并返回旧值，等价于 C 里的 test-and-set。C++ 对照：<code>mutable bool</code> 成员加上 <code>const</code> 方法，语言层面允许 <code>const</code> 方法修改它。Rust 的 <code>Cell</code> 是把这个后门做成一个明确的类型，而且和 <code>RefCell</code> 一样<strong>不是</strong> <code>Sync</code>（不能跨线程共享，第 14 篇讲）。</p></section><section><h3>显式工作表<a href="#显式工作表"><span>#</span></a></h3><p><code>mark</code> 的实现。接着追加：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Marks `root` and everything reachable from it, transitively.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Marking is idempotent: a slot that is already marked is not traced</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// again, so repeated roots and cycles terminate. The traversal keeps an</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// explicit worklist instead of recursing, so arbitrarily deep object</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// graphs cannot overflow the native stack.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>///</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>9</div></div><div><span>    </span><span>///</span></div></div><div><div><div>10</div></div><div><span>    </span><span>/// Panics if `root` is out of range. A handle to a freed slot is ignored,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// since it can no longer name a reachable object.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>mark</span><span>(&amp;</span><span>self</span><span>, </span><span>root</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>pending</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>root</span><span>];</span></div></div><div><div><div>14</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>current</span><span><span>) </span><span>=</span><span> </span></span><span>pending</span><span>.</span><span>pop</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>current</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>16</div></div><div><span>            </span><span>let</span><span> </span><span>Some</span><span>(</span><span>object</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.slots[</span><span>index</span><span>].</span><span>as_ref</span><span>() </span><span>else</span><span> {</span></div></div><div><div><div>17</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>19</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.marked[</span><span>index</span><span>].</span><span>replace</span><span>(</span><span>true</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>            </span><span>object</span><span>.</span><span>trace</span><span>(&amp;</span><span>mut</span><span><span> </span><span>|</span></span><span>child</span><span><span>|</span><span> </span></span><span>pending</span><span>.</span><span>push</span><span>(</span><span>child</span><span>));</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐行读：<code>while let Some(current) = pending.pop()</code> 是第 4 篇的老句式，工作表空了就结束。<code>let ... else { continue; }</code> 是第 3 篇的 let-else：槽位是空的（指向已释放对象的句柄）就跳过。<code>replace(true)</code> 返回的旧值如果是 <code>true</code> 就说明已经走过，跳过，这一句保证了每个对象最多被追踪一次，环于是不会造成死循环。最后 <code>object.trace(&amp;mut |child| pending.push(child))</code> 把闭包借给 <code>trace</code>，闭包只捕获局部的 <code>pending</code>，和 <code>self</code> 没有借用冲突。</p><p>用不用递归，还有一个独立于借用检查器的理由。我写了个最小的递归 <code>mark_recursive</code>（<code>ch11_deep_recursion.rs</code>），让它标记一个链表式的嵌套：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --quiet --bin ch11_deep_recursion</span></div></div><div><div><div>2</div></div><div><span>$ ./target/debug/ch11_deep_recursion 10000</span></div></div><div><div><div>3</div></div><div><span>marked 10001 objects at depth 10000</span></div></div><div><div><div>4</div></div><div><span>$ ./target/debug/ch11_deep_recursion 20000 test-thread</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>thread '&lt;unknown&gt;' (66495) has overflowed its stack</span></div></div><div><div><div>7</div></div><div><span>fatal runtime error: stack overflow, aborting</span></div></div><div><div><div>8</div></div><div><span>$ ./target/debug/ch11_deep_recursion 50000</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>thread 'main' (66502) has overflowed its stack</span></div></div><div><div><div>11</div></div><div><span>fatal runtime error: stack overflow, aborting</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_deep_recursion.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Why `mark` keeps a worklist: a recursive marker dies on a deep list.</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>Cell</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>GcRef</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>trait</span><span> </span><span>GcObject</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>));</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>Heap</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>slots</span><span>: </span><span>Vec</span><span>&lt;</span><span>Option</span><span>&lt;</span><span>O</span><span>&gt;&gt;,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>marked</span><span>: </span><span>Vec</span><span>&lt;</span><span>Cell</span><span>&lt;</span><span>bool</span><span>&gt;&gt;,</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>impl</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>mark_recursive</span><span>(&amp;</span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>19</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.marked[</span><span>index</span><span>].</span><span>replace</span><span>(</span><span>true</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>return</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>self</span><span>.slots[</span><span>index</span><span>].</span><span>as_ref</span><span>().</span><span>unwrap</span><span>().</span><span>trace</span><span>(&amp;</span><span>mut</span><span><span> </span><span>|</span></span><span>child</span><span><span>|</span><span> </span></span><span>self</span><span>.</span><span>mark_recursive</span><span>(</span><span>child</span><span>));</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>struct</span><span> </span><span>Node</span><span>(</span><span>Option</span><span>&lt;</span><span>GcRef</span><span>&gt;);</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>impl</span><span> </span><span>GcObject</span><span> </span><span>for</span><span> </span><span>Node</span><span> {</span></div></div><div><div><div>29</div></div><div><span>    </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>)) {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>child</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>0</span><span> {</span></div></div><div><div><div>31</div></div><div><span>            </span><span>mark</span><span>(</span><span>child</span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>fn</span><span> </span><span>run</span><span>() {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>let</span><span> </span><span>depth</span><span>: </span><span>u32</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>env</span><span>::</span></span><span>args</span><span>().</span><span>nth</span><span>(</span><span>1</span><span>).</span><span>map_or</span><span>(</span><span>10_000</span><span><span>, </span><span>|</span></span><span>s</span><span><span>|</span><span> </span></span><span>s</span><span>.</span><span>parse</span><span>().</span><span>unwrap</span><span>());</span></div></div><div><div><div>38</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>slots</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>Some</span><span>(</span><span>Node</span><span>(</span><span>None</span><span>))];</span></div></div><div><div><div>39</div></div><div><span>    </span><span>for</span><span> </span><span>i</span><span> </span><span>in</span><span> </span><span>1</span><span>..=</span><span>depth</span><span> {</span></div></div><div><div><div>40</div></div><div><span>        </span><span>slots</span><span>.</span><span>push</span><span>(</span><span>Some</span><span>(</span><span>Node</span><span>(</span><span>Some</span><span>(</span><span>GcRef</span><span>(</span><span>i</span><span> - </span><span>1</span><span>)))));</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span>    </span><span>let</span><span> </span><span>marked</span><span><span> </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>slots</span><span>.</span><span>len</span><span>()).</span><span>map</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> </span><span>Cell</span><span>::</span></span><span>new</span><span>(</span><span>false</span><span>)).</span><span>collect</span><span>();</span></div></div><div><div><div>43</div></div><div><span>    </span><span>let</span><span> </span><span>heap</span><span><span> </span><span>=</span><span> </span></span><span>Heap</span><span> { </span><span>slots</span><span>, </span><span>marked</span><span> };</span></div></div><div><div><div>44</div></div><div><span>    </span><span>heap</span><span>.</span><span>mark_recursive</span><span>(</span><span>GcRef</span><span>(</span><span>depth</span><span>));</span></div></div><div><div><div>45</div></div><div><span>    </span><span>println!</span><span>(</span><span>"marked {} objects at depth {depth}"</span><span>, </span><span>heap</span><span>.marked.</span><span>iter</span><span>().</span><span>filter</span><span><span>(</span><span>|</span></span><span>m</span><span><span>|</span><span> </span></span><span>m</span><span>.</span><span>get</span><span>()).</span><span>count</span><span>());</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>49</div></div><div><span>    </span><span>// `cargo test` runs each test on a thread with a 2 MiB stack; the main</span></div></div><div><div><div>50</div></div><div><span>    </span><span>// thread usually gets 8 MiB. Pick one with the second argument.</span></div></div><div><div><div>51</div></div><div><span>    </span><span>if</span><span><span> </span><span>std</span><span>::</span><span>env</span><span>::</span></span><span>args</span><span>().</span><span>nth</span><span>(</span><span>2</span><span>).</span><span>as_deref</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>"test-thread"</span><span>) {</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>std</span><span>::</span><span>thread</span><span>::</span><span>Builder</span><span>::</span><span>new</span><span>().</span><span>stack_size</span><span>(</span><span>2</span><span> * </span><span>1024</span><span> * </span><span>1024</span><span>).</span><span>spawn</span><span>(</span><span>run</span><span>).</span><span>unwrap</span><span>().</span><span>join</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>54</div></div><div><span>        </span><span>run</span><span>();</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>56</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>test-thread</code> 参数表示在一个 2 MiB 栈的线程里跑（<code>cargo test</code> 给每个测试线程的默认大小），否则在 8 MiB 的主线程里跑。debug 构建里 10,000 层居然没事，2 MiB 栈上 20,000 层就崩，8 MiB 的主线程 50,000 层崩。（崩的时候是 <code>abort</code>：进程直接被杀，不是 panic，<code>catch_unwind</code> 也拦不住。）Pebble 程序里的嵌套深度是用户可以控制的，所以回收器不能把”别人写的数据有多深”变成”我的栈有多深”。显式工作表把深度挪到堆上：<code>pending</code> 是一个 <code>Vec</code>，它能长到内存耗尽为止。</p><div><div><div></div><div>实话实说</div></div><div><p>仓库里的测试 <code>deeply_nested_lists_survive</code> 用的深度是 10,000。上面的实验说明，在 debug 构建里一个递归标记器其实也能撑过 10,000 层，所以这个测试单靠深度<strong>并不能</strong>把递归实现和工作表实现区分开。我没有去改它（它在标签里就是这样），但如果你要它真的守住这条回归，把循环里的 <code>10_000</code> 改成 <code>100_000</code>，断言里的 <code>10_001</code> 同步改成 <code>100_001</code>。</p></div></div></section></section>
<section><h2>清扫、查询，以及剩下的方法<a href="#清扫查询以及剩下的方法"><span>#</span></a></h2><p>标记之后，清扫：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Sweeps every slot that is not marked, then clears all mark bits.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// The embedder must have marked its roots (via [`Heap::mark`]) before</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// calling this. After the sweep the heap is ready for the next mark phase.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>collect</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> </span><span>index</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>self</span><span>.slots.</span><span>len</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.marked[</span><span>index</span><span>].</span><span>get</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>self</span><span>.marked[</span><span>index</span><span>].</span><span>set</span><span>(</span><span>false</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.slots[</span><span>index</span><span>].</span><span>is_some</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> </span></span><span>u32</span><span>::</span><span>try_from</span><span>(</span><span>index</span><span>).</span><span>expect</span><span>(</span><span>"heap outgrew its u32 slot index"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                </span><span>self</span><span>.</span><span>free</span><span>(</span><span>GcRef</span><span>(</span><span>slot</span><span>));</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span><span>.allocations_since_gc </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里是下标循环，不是 <code>for slot in &amp;mut self.slots</code>，因为循环体里要调用 <code>self.free(..)</code>，它需要 <code>&amp;mut self</code> 整个借用，和迭代器对 <code>self.slots</code> 的借用冲突。又是同一条规矩。标记过的槽位：清掉标记位留给下一轮；没标记且还占着的：<code>free</code> 回空闲表。<code>collect(&amp;mut self)</code> 要独占，这也是个有用的保证：清扫时没有任何人手里还拿着对堆的借用。</p><p>最后是几个查询方法，加上 <code>Default</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Whether `r` names a slot that currently holds an object.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Unlike [`Heap::get`], this never panics: an out-of-range or freed handle</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// simply reports `false`.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>is_alive</span><span>(&amp;</span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span>.slots</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>get</span><span>(</span><span>r</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>slot</span><span><span>|</span><span> </span></span><span>slot</span><span>.</span><span>is_some</span><span>())</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>/// Number of occupied slots, i.e. objects not yet swept.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>live_count</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span>.slots.</span><span>len</span><span>() - </span><span>self</span><span>.free.</span><span>len</span><span>()</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>/// Number of object slots the heap can hold without reallocating its</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// tables.</span></div></div><div><div><div>18</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>capacity</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span>.slots.</span><span>capacity</span><span>()</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>/// Estimated total bytes occupied by the live slots.</span></div></div><div><div><div>23</div></div><div><span>    </span><span>///</span></div></div><div><div><div>24</div></div><div><span>    </span><span>/// This is `live_count() * size_of::&lt;O&gt;()`: the inline size of each object</span></div></div><div><div><div>25</div></div><div><span>    </span><span>/// as stored in its slot. Memory a `GcObject` owns indirectly, such as a</span></div></div><div><div><div>26</div></div><div><span>    </span><span>/// `String` buffer or a `Vec`'s heap block, is not counted, so treat this</span></div></div><div><div><div>27</div></div><div><span>    </span><span>/// as an estimate for deciding when to collect rather than an exact</span></div></div><div><div><div>28</div></div><div><span>    </span><span>/// allocator figure.</span></div></div><div><div><div>29</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>bytes_allocated</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>self</span><span>.</span><span>live_count</span><span><span>() * </span><span>core</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>O</span><span>&gt;()</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>/// Number of allocations performed since the last [`Heap::collect`].</span></div></div><div><div><div>34</div></div><div><span>    </span><span>///</span></div></div><div><div><div>35</div></div><div><span>    </span><span>/// An embedder can use this to trigger collection after a fixed number of</span></div></div><div><div><div>36</div></div><div><span>    </span><span>/// allocations.</span></div></div><div><div><div>37</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>allocations_since_gc</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>38</div></div><div><span>        </span><span>self</span><span>.allocations_since_gc</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>impl</span><span>&lt;</span><span>O</span><span>: </span><span>GcObject</span><span>&gt; </span><span>Default</span><span> </span><span>for</span><span> </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>43</div></div><div><span>    </span><span>fn</span><span> </span><span>default</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>44</div></div><div><span>        </span><span>Self</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第一个 <code>}</code> 终于闭合了 <code>impl&lt;O: GcObject&gt; Heap&lt;O&gt;</code>。<code>is_alive</code>、<code>live_count</code>、<code>capacity</code>、<code>allocations_since_gc</code> 都是一两行的访问器，逻辑没有新东西；<code>bytes_allocated</code> 是 <code>live_count() * size_of::&lt;O&gt;()</code> 的估算，文档里写得很诚实：它不算 <code>String</code> 缓冲区这类间接内存。这五个方法你照抄即可，理由是它们既不涉及借用难点，也不涉及本篇的新概念，读文档注释就够了。</p><p><code>Default</code> 的实现是为了 clippy。工作区的 lint 设置了 <code>clippy::all = "warn"</code>，我把它删掉跑了一遍 <code>cargo clippy -p pebble-gc</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>warning: you should consider adding a `Default` implementation for `Heap&lt;O&gt;`</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>= note: `-W clippy::new-without-default` implied by `-W clippy::all`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（我没有抄全这条警告。第一行是 clippy 的标题，中间是它指出的 <code>new</code> 的源码片段。）规则的意思很实际：“有无参的 <code>new</code>，就应该有 <code>Default</code>”，这样 <code>#[derive(Default)]</code> 的外层结构体、<code>unwrap_or_default</code> 之类的泛型代码才能用上它。</p><p>到这里可以先检查一下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>check</span><span> </span><span>-p</span><span> </span><span>pebble-gc</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>测试：让一个随机图来审判<a href="#测试让一个随机图来审判"><span>#</span></a></h2><p>测试模块比实现更能说明回收器在做什么。我们要的”模拟对象”是个 enum，形状和第 12 篇 VM 里的对象相仿：字符串、列表、闭包。接在 <code>lib.rs</code> 末尾写模块头：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::{</span><span>GcObject</span><span>, </span><span>GcRef</span><span>, </span><span>Heap</span><span>};</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span><span> </span><span>rand</span><span>::</span><span>prelude</span><span>::*;</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>/// A concrete heap object for the tests, mirroring the shapes the Pebble</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// runtime will plug in: strings, lists and closures.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[derive(</span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>enum</span><span> </span><span>TestObject</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Str</span><span>(</span><span>String</span><span>),</span></div></div><div><div><div>11</div></div><div><span>        </span><span>List</span><span>(</span><span>Vec</span><span>&lt;</span><span>GcRef</span><span>&gt;),</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Closure</span><span> { </span><span>name</span><span>: </span><span>String</span><span>, </span><span>captured</span><span>: </span><span>Vec</span><span>&lt;</span><span>GcRef</span><span>&gt; },</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>impl</span><span> </span><span>GcObject</span><span> </span><span>for</span><span> </span><span>TestObject</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>fn</span><span> </span><span>trace</span><span>(&amp;</span><span>self</span><span>, </span><span>mark</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>GcRef</span><span>)) {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>18</div></div><div><span>                </span><span>TestObject</span><span>::</span><span>Str</span><span>(</span><span>_</span><span>) =&gt; {}</span></div></div><div><div><div>19</div></div><div><span>                </span><span>TestObject</span><span>::</span><span>List</span><span>(</span><span>items</span><span>) =&gt; {</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>for</span><span> &amp;</span><span>child</span><span> </span><span>in</span><span> </span><span>items</span><span> {</span></div></div><div><div><div>21</div></div><div><span>                        </span><span>mark</span><span>(</span><span>child</span><span>);</span></div></div><div><div><div>22</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>                </span><span>TestObject</span><span>::</span><span>Closure</span><span> { </span><span>captured</span><span>, .. } =&gt; {</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>for</span><span> &amp;</span><span>child</span><span> </span><span>in</span><span> </span><span>captured</span><span> {</span></div></div><div><div><div>26</div></div><div><span>                        </span><span>mark</span><span>(</span><span>child</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>fn</span><span> </span><span>list</span><span>(</span><span>heap</span><span>: &amp;</span><span>mut</span><span> </span><span>Heap</span><span>&lt;</span><span>TestObject</span><span>&gt;, </span><span>children</span><span>: &amp;[</span><span>GcRef</span><span>]) -&gt; </span><span>GcRef</span><span> {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>heap</span><span>.</span><span>alloc</span><span><span>(</span><span>TestObject</span><span>::</span></span><span>List</span><span>(</span><span>children</span><span>.</span><span>to_vec</span><span>()))</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>use rand::prelude::*;</code> 引入外部 crate <code>rand</code>。你在第 6、7 篇已经加过外部依赖，这次放在 <code>[dev-dependencies]</code> 里：只有测试编译它。<code>rand</code> 来自 crates.io（Rust 的官方包仓库，你在 C++ 里用 vcpkg/Conan 做的事）。第一次 <code>cargo test</code> 时 Cargo 会下载并编译它，并把解析出的确切版本写进 <code>Cargo.lock</code>。<code>GcObject</code> 的实现里，<code>match self</code> 的三个分支正是”这个对象引用了谁”：字符串没有子引用，列表和闭包把自己持有的句柄逐个汇报。<code>TestObject::Closure { captured, .. }</code> 里的 <code>..</code> 是”其余字段我不关心”。</p><p>接着要加进来的测试，分写和抄两种。<strong>抄</strong>的：从 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-gc/src/lib.rs" target="_blank"><code>crates/pebble-gc/src/lib.rs</code></a> 里把 <code>alloc_get_and_get_mut</code>、<code>freed_slots_are_reused</code>、<code>mark_traces_children_transitively</code>、<code>collect_without_roots_frees_everything</code> 四个测试，按这个顺序紧跟在 <code>list</code> 函数后面粘贴；再把 <code>is_alive_tracks_slots</code> 粘在后面说的 <code>unreachable_cycles_are_collected</code> 之后。它们各自只验证一个简单的断言（分配取回、槽位复用、传递标记、无根全清、<code>is_alive</code> 的行为），没有新的 Rust 概念，逐行读即可。位置和顺序也属于逐字节比对的内容。</p><p><strong>写</strong>的先来两个：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>deeply_nested_lists_survive</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span>: </span><span>Heap</span><span>&lt;</span><span>TestObject</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Heap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>current</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span><span>(</span><span>TestObject</span><span>::</span></span><span>Str</span><span>(</span><span>"bottom"</span><span>.</span><span>into</span><span>()));</span></div></div><div><div><div>5</div></div><div><span>        </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>10_000</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>current</span><span><span> </span><span>=</span><span> </span></span><span>list</span><span>(&amp;</span><span>mut</span><span> </span><span>heap</span><span>, &amp;[</span><span>current</span><span>]);</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>        </span><span>// The worklist marker must handle this depth without recursing.</span></div></div><div><div><div>10</div></div><div><span>        </span><span>heap</span><span>.</span><span>mark</span><span>(</span><span>current</span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>heap</span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>heap</span><span>.</span><span>live_count</span><span>(), </span><span>10_001</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>unreachable_cycles_are_collected</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span>: </span><span>Heap</span><span>&lt;</span><span>TestObject</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Heap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>list</span><span>(&amp;</span><span>mut</span><span> </span><span>heap</span><span>, &amp;[]);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>list</span><span>(&amp;</span><span>mut</span><span> </span><span>heap</span><span>, &amp;[</span><span>a</span><span>]);</span></div></div><div><div><div>21</div></div><div><span>        </span><span>// Close the cycle: a -&gt; b -&gt; a. Neither is rooted.</span></div></div><div><div><div>22</div></div><div><span>        </span><span>match</span><span> </span><span>heap</span><span>.</span><span>get_mut</span><span>(</span><span>a</span><span>) {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>TestObject</span><span>::</span><span>List</span><span>(</span><span>items</span><span>) =&gt; </span><span>items</span><span>.</span><span>push</span><span>(</span><span>b</span><span>),</span></div></div><div><div><div>24</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>panic!</span><span>(</span><span>"expected a list, got {other:?}"</span><span>),</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>        </span><span>heap</span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>heap</span><span>.</span><span>live_count</span><span>(), </span><span>0</span><span>, </span><span>"an unreachable cycle is swept"</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>heap</span><span>.</span><span>bytes_allocated</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>31</div></div><div><span>        </span><span>assert!</span><span><span>(</span><span>!</span></span><span>heap</span><span>.</span><span>is_alive</span><span>(</span><span>a</span><span>));</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第二个是整个 crate 存在的理由：<code>a -&gt; b -&gt; a</code>，谁也不在根里，<code>Rc</code> 会永远留着它们，这里 <code>live_count()</code> 变成 0。两个测试放在 <code>collect_without_roots_frees_everything</code> 之后，它们中间没有别的东西，然后是抄来的 <code>is_alive_tracks_slots</code>。</p><p>再写 <code>#[should_panic]</code> 测试，放在 <code>is_alive_tracks_slots</code> 之后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[should_panic(expected </span><span>=</span><span> </span><span>"does not point to a live object"</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>getting_a_freed_reference_panics</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span>: </span><span>Heap</span><span>&lt;</span><span>TestObject</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Heap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span><span>(</span><span>TestObject</span><span>::</span></span><span>Str</span><span>(</span><span>"gone"</span><span>.</span><span>into</span><span>()));</span></div></div><div><div><div>6</div></div><div><span>        </span><span>heap</span><span>.</span><span>free</span><span>(</span><span>r</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>get</span><span>(</span><span>r</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>一个不认识实现的裁判<a href="#一个不认识实现的裁判"><span>#</span></a></h3><p>最后是最有价值的测试：随机图 oracle。我们生成随机有向图（边可以向前也可以向后，所以环很常见），随机挑一些节点做根，让堆标记+清扫，然后和一个<strong>用另一种方法</strong>算出来的可达集合对比。“另一种方法”很重要：实现里用的是 <code>Vec</code> 当栈的深度优先，所以裁判改用 <code>VecDeque</code> 做广度优先，再用 <code>BTreeSet</code> 去重：这样两边不共享任何代码和遍历顺序，同时算错的概率很低。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>randomised_reachability_matches_sweep</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>use</span><span><span> </span><span>std</span><span>::</span><span>collections</span><span>::{</span></span><span>BTreeSet</span><span>, </span><span>VecDeque</span><span>};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>rng</span><span><span> </span><span>=</span><span> </span><span>rand</span><span>::</span></span><span>rng</span><span>();</span></div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>200</span><span> {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>rng</span><span>.</span><span>random_range</span><span>(</span><span>1</span><span>..</span><span>40</span><span>usize</span><span>);</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>            </span><span>// A random directed graph. Edges may point forward or backward, so</span></div></div><div><div><div>10</div></div><div><span>            </span><span>// cycles are common.</span></div></div><div><div><div>11</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>edges</span><span>: </span><span>Vec</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>usize</span><span><span>&gt;&gt; </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>with_capacity</span><span>(</span><span>n</span><span>);</span></div></div><div><div><div>12</div></div><div><span>            </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>n</span><span> {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>let</span><span> </span><span>degree</span><span><span> </span><span>=</span><span> </span></span><span>rng</span><span>.</span><span>random_range</span><span>(</span><span>0</span><span>..=</span><span>3</span><span>);</span></div></div><div><div><div>14</div></div><div><span>                </span><span>let</span><span> </span><span>mut</span><span> </span><span>children</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>with_capacity</span><span>(</span><span>degree</span><span>);</span></div></div><div><div><div>15</div></div><div><span>                </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>degree</span><span> {</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>children</span><span>.</span><span>push</span><span>(</span><span>rng</span><span>.</span><span>random_range</span><span>(</span><span>0</span><span>..</span><span>n</span><span>));</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>                </span><span>edges</span><span>.</span><span>push</span><span>(</span><span>children</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span>: </span><span>Heap</span><span>&lt;</span><span>TestObject</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Heap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>22</div></div><div><span>            </span><span>for</span><span> </span><span>children</span><span> </span><span>in</span><span> &amp;</span><span>edges</span><span> {</span></div></div><div><div><div>23</div></div><div><span>                </span><span>let</span><span> </span><span>refs</span><span>: </span><span>Vec</span><span>&lt;</span><span>GcRef</span><span><span>&gt; </span><span>=</span><span> </span></span><span>children</span></div></div><div><div><div>24</div></div><div><span><span>                    </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>25</div></div><div><span><span>                    </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span><span>&amp;</span></span><span>child</span><span><span>|</span><span> </span><span>GcRef</span><span>::</span></span><span>from_index</span><span>(</span><span>child</span><span> </span><span>as</span><span> </span><span>u32</span><span>))</span></div></div><div><div><div>26</div></div><div><span><span>                    </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>27</div></div><div><span>                </span><span>heap</span><span>.</span><span>alloc</span><span><span>(</span><span>TestObject</span><span>::</span></span><span>List</span><span>(</span><span>refs</span><span>));</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>            </span><span>let</span><span> </span><span>roots</span><span>: </span><span>Vec</span><span>&lt;</span><span>usize</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>n</span><span>).</span><span>filter</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> </span></span><span>rng</span><span>.</span><span>random_bool</span><span>(</span><span>0.3</span><span>)).</span><span>collect</span><span>();</span></div></div><div><div><div>31</div></div><div><span>            </span><span>for</span><span> &amp;</span><span>root</span><span> </span><span>in</span><span> &amp;</span><span>roots</span><span> {</span></div></div><div><div><div>32</div></div><div><span>                </span><span>heap</span><span>.</span><span>mark</span><span><span>(</span><span>GcRef</span><span>::</span></span><span>from_index</span><span>(</span><span>root</span><span> </span><span>as</span><span> </span><span>u32</span><span>));</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>            </span><span>// Expected survivors: everything reachable from the roots.</span></div></div><div><div><div>36</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>reachable</span><span><span> </span><span>=</span><span> </span></span><span>BTreeSet</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>37</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>queue</span><span>: </span><span>VecDeque</span><span>&lt;</span><span>usize</span><span><span>&gt; </span><span>=</span><span> </span></span><span>roots</span><span>.</span><span>iter</span><span>().</span><span>copied</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>38</div></div><div><span>            </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>node</span><span><span>) </span><span>=</span><span> </span></span><span>queue</span><span>.</span><span>pop_front</span><span>() {</span></div></div><div><div><div>39</div></div><div><span>                </span><span>if</span><span> </span><span>reachable</span><span>.</span><span>insert</span><span>(</span><span>node</span><span>) {</span></div></div><div><div><div>40</div></div><div><span>                    </span><span>queue</span><span>.</span><span>extend</span><span>(</span><span>edges</span><span>[</span><span>node</span><span>].</span><span>iter</span><span>().</span><span>copied</span><span>());</span></div></div><div><div><div>41</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>            </span><span>heap</span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>            </span><span>let</span><span> </span><span>live</span><span>: </span><span>Vec</span><span>&lt;</span><span>usize</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>n</span><span>).</span><span>filter</span><span><span>(</span><span>|</span><span>&amp;</span></span><span>i</span><span><span>|</span><span> </span></span><span>heap</span><span>.slots[</span><span>i</span><span>].</span><span>is_some</span><span>()).</span><span>collect</span><span>();</span></div></div><div><div><div>47</div></div><div><span>            </span><span>let</span><span> </span><span>expected</span><span>: </span><span>Vec</span><span>&lt;</span><span>usize</span><span><span>&gt; </span><span>=</span><span> </span></span><span>reachable</span><span>.</span><span>into_iter</span><span>().</span><span>collect</span><span>(); </span><span>// already sorted</span></div></div><div><div><div>48</div></div><div><span>            </span><span>assert_eq!</span><span>(</span><span>live</span><span>, </span><span>expected</span><span>, </span><span>"the sweep kept the wrong objects"</span><span>);</span></div></div><div><div><div>49</div></div><div><span>            </span><span>assert_eq!</span><span>(</span><span>heap</span><span>.</span><span>live_count</span><span>(), </span><span>expected</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>50</div></div><div><span>            </span><span>for</span><span> &amp;</span><span>i</span><span> </span><span>in</span><span> &amp;</span><span>expected</span><span> {</span></div></div><div><div><div>51</div></div><div><span>                </span><span>assert!</span><span>(</span><span>heap</span><span>.</span><span>is_alive</span><span><span>(</span><span>GcRef</span><span>::</span></span><span>from_index</span><span>(</span><span>i</span><span> </span><span>as</span><span> </span><span>u32</span><span>)));</span></div></div><div><div><div>52</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>最后那个 <code>}</code> 闭合 <code>mod tests</code>。几个细节：</p><ul>
<li><code>heap.slots[i]</code> 是私有字段，但测试模块是 crate 内部的子模块，可以访问。第 2 篇讲过的”私有只到模块边界”在这里体现成：单元测试住在被测代码里面，可以看内部；集成测试（<code>tests/</code> 目录）只能用公开 API。</li>
<li><code>reachable.insert(node)</code> 返回 <code>bool</code>：是不是新插入。这让”去重 + 判断是否第一次到达”变成一次调用，和 <code>Cell::replace</code> 异曲同工。</li>
<li><code>BTreeSet</code> 对迭代顺序有保证（有序），<code>into_iter().collect()</code> 得到的就是排好序的 <code>Vec</code>。我在实验里并排试了三个同类容器（<code>ch11_collections.rs</code>）：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch11_collections</span></div></div><div><div><div>2</div></div><div><span>insert 5: hash new? true  tree new? true</span></div></div><div><div><div>3</div></div><div><span>insert 1: hash new? true  tree new? true</span></div></div><div><div><div>4</div></div><div><span>insert 5: hash new? false  tree new? false</span></div></div><div><div><div>5</div></div><div><span>insert 3: hash new? true  tree new? true</span></div></div><div><div><div>6</div></div><div><span>insert 1: hash new? false  tree new? false</span></div></div><div><div><div>7</div></div><div><span>tree (sorted): {1, 3, 5}</span></div></div><div><div><div>8</div></div><div><span>hash (sorted by us): [1, 3, 5]</span></div></div><div><div><div>9</div></div><div><span>pop_front = Some(0), rest = [1, 2]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_collections.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>collections</span><span>::{</span></span><span>BTreeSet</span><span>, </span><span>HashSet</span><span>, </span><span>VecDeque</span><span>};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>items</span><span><span> </span><span>=</span><span> [</span></span><span>5</span><span>, </span><span>1</span><span>, </span><span>5</span><span>, </span><span>3</span><span>, </span><span>1</span><span>];</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>hash</span><span>: </span><span>HashSet</span><span>&lt;</span><span>i32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>HashSet</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>tree</span><span>: </span><span>BTreeSet</span><span>&lt;</span><span>i32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>BTreeSet</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>items</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>println!</span><span>(</span><span>"insert {item}: hash new? {}  tree new? {}"</span><span>, </span><span>hash</span><span>.</span><span>insert</span><span>(</span><span>item</span><span>), </span><span>tree</span><span>.</span><span>insert</span><span>(</span><span>item</span><span>));</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"tree (sorted): {:?}"</span><span>, </span><span>tree</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>from_hash</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> </span></span><span>hash</span><span>.</span><span>into_iter</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>13</div></div><div><span>    </span><span>from_hash</span><span>.</span><span>sort</span><span>();</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"hash (sorted by us): {from_hash:?}"</span><span>);</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>queue</span><span>: </span><span>VecDeque</span><span>&lt;</span><span>i32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>VecDeque</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>17</div></div><div><span>    </span><span>queue</span><span>.</span><span>push_back</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>18</div></div><div><span>    </span><span>queue</span><span>.</span><span>push_back</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>queue</span><span>.</span><span>push_front</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>front</span><span><span> </span><span>=</span><span> </span></span><span>queue</span><span>.</span><span>pop_front</span><span>();</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"pop_front = {front:?}, rest = {queue:?}"</span><span>);</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>HashSet</code>、<code>BTreeSet</code> 的 <code>insert</code> 返回值语义相同；区别是前者无序（对应 <code>std::unordered_set</code>）、后者有序（对应 <code>std::set</code>，红黑树换成了 B 树）；<code>VecDeque</code> 对应 <code>std::deque</code>，<code>push_front</code>/<code>pop_front</code> 都是 O(1)。我们这里选 <code>BTreeSet</code> 是因为想要排好序的结果直接 <code>assert_eq!</code>。（我写第一版时用的是 <code>Vec&lt;bool&gt;</code> 再配 <code>Vec</code> 当栈，后来换成这一版。语义一样，但现在裁判和实现不再用同一种遍历，更像裁判。）</p><p>一个我没处理的小事：<code>rand::rng()</code> 没有固定种子，所以这个测试每次跑的图都不一样。好处是跑得越多覆盖越广，坏处是如果哪天它失败了，你没法重放同一张图。我认为在这个规模下可以接受；要重放的话该换成带种子的随机数生成器，这是一个很自然的练习。</p></section></section>
<section><h2>第一个检查点：安全的部分<a href="#第一个检查点安全的部分"><span>#</span></a></h2><p><code>bump.rs</code> 现在还是空的，所以先跑一次，看安全代码这一半：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-gc</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>running 9 tests</span></div></div><div><div><div>4</div></div><div><span>test tests::alloc_get_and_get_mut ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::collect_without_roots_frees_everything ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::getting_a_freed_reference_panics - should panic ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::freed_slots_are_reused ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::mark_traces_children_transitively ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::is_alive_tracks_slots ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::unreachable_cycles_are_collected ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::deeply_nested_lists_survive ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::randomised_reachability_matches_sweep ... ok</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（后面还跟着 crate 文档里那个 doc test，<code>1 passed</code>；最终完整输出在文末的检查点里。）到这里 <code>pebble-gc</code> 的 <code>Heap</code> 用了 0 行 <code>unsafe</code>。第 12 篇的 VM 会拿它存放所有对象。</p></section>
<section><h2>unsafe：第一次签字担保<a href="#unsafe第一次签字担保"><span>#</span></a></h2><p>文件里还剩一个模块 <code>bump</code>。它是一个”一次性分配”的 arena（bump allocator）：预先要一大块字节，分配就是把一个偏移量往前推；不能单个释放，只能整个 <code>reset</code>。在编译器和 Web 服务器里，“请求一来分配一堆小东西，请求结束全部丢掉”的场景特别适合这种分配器。实话：Pebble 里目前没有任何地方用到它。VM 的对象都在 <code>Heap&lt;O&gt;</code> 里。<code>BumpArena</code> 在这个仓库里的存在，是为了有一个足够小、能把 <code>unsafe</code> 讲清楚的真实例子。</p><p>先说一件重要的事：<strong>这个 arena 完全可以用安全代码写</strong>。我把它写出来了（<code>ch11_bump_safe.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>alloc_bytes</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>bytes</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; &amp;</span><span>mut</span><span> [</span><span>u8</span><span>] {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.offset;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>start</span><span> + </span><span>bytes</span><span>.</span><span>len</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>self</span><span><span>.offset </span><span>=</span><span> </span></span><span>end</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>self</span><span>.buffer[</span><span>start</span><span>..</span><span>end</span><span>];</span></div></div><div><div><div>6</div></div><div><span>    </span><span>slot</span><span>.</span><span>copy_from_slice</span><span>(</span><span>bytes</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>slot</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>alloc_str</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>s</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>mut</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>bytes</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>alloc_bytes</span><span>(</span><span>s</span><span>.</span><span>as_bytes</span><span>());</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>std</span><span>::</span><span>str</span><span>::</span><span>from_utf8_mut</span><span>(</span><span>bytes</span><span>).</span><span>expect</span><span>(</span><span>"copied from a &amp;str"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch11_bump_safe</span></div></div><div><div><div>2</div></div><div><span>PEB</span></div></div><div><div><div>3</div></div><div><span>ble</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_bump_safe.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The same arena without unsafe, to see what unsafe buys.</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>SafeArena</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>buffer</span><span>: </span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>offset</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>SafeArena</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>with_capacity</span><span>(</span><span>bytes</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Self</span><span> { </span><span>buffer</span><span>: </span><span>vec!</span><span>[</span><span>0</span><span>; </span><span>bytes</span><span>], </span><span>offset</span><span>: </span><span>0</span><span> }</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>alloc_bytes</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>bytes</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; &amp;</span><span>mut</span><span> [</span><span>u8</span><span>] {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.offset;</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>start</span><span> + </span><span>bytes</span><span>.</span><span>len</span><span>();</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span><span>.offset </span><span>=</span><span> </span></span><span>end</span><span>;</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>self</span><span>.buffer[</span><span>start</span><span>..</span><span>end</span><span>];</span></div></div><div><div><div>17</div></div><div><span>        </span><span>slot</span><span>.</span><span>copy_from_slice</span><span>(</span><span>bytes</span><span>);</span></div></div><div><div><div>18</div></div><div><span>        </span><span>slot</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>fn</span><span> </span><span>alloc_str</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>s</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>mut</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>bytes</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>alloc_bytes</span><span>(</span><span>s</span><span>.</span><span>as_bytes</span><span>());</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>std</span><span>::</span><span>str</span><span>::</span><span>from_utf8_mut</span><span>(</span><span>bytes</span><span>).</span><span>expect</span><span>(</span><span>"copied from a &amp;str"</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>28</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>SafeArena</span><span>::</span><span>with_capacity</span><span>(</span><span>16</span><span>);</span></div></div><div><div><div>29</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_str</span><span>(</span><span>"peb"</span><span>);</span></div></div><div><div><div>30</div></div><div><span>    </span><span>a</span><span>.</span><span>make_ascii_uppercase</span><span>();</span></div></div><div><div><div>31</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{a}"</span><span>);</span></div></div><div><div><div>32</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_str</span><span>(</span><span>"ble"</span><span>);</span></div></div><div><div><div>33</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{b}"</span><span>);</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它用 <code>Vec&lt;u8&gt;</code> 当缓冲区，下标自带越界检查，<code>from_utf8_mut</code> 带校验。它比 <code>unsafe</code> 版本短，还更安全。那 <code>unsafe</code> 版本换来了什么？只有两件小事：缓冲区在语义上不需要先初始化（<code>MaybeUninit</code>），<code>alloc_str</code> 不用再扫一遍字节做 UTF-8 校验（明明刚从 <code>&amp;str</code> 拷出来）。我没有为它们量过时间；在这个规模下大概可以忽略。所以说实话，这是教学用的例子，不是性能优化。真实世界里一个 <code>unsafe</code> 块值不值得写，标准是你量过的数字。</p><p>安全版本还替我们验证了一件事，这件事在 <code>unsafe</code> 版本里同样成立：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"one"</span><span>);</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"two"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"{a:?} {b:?}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch11_arena_alias</span></div></div><div><div><div>2</div></div><div><span>error[E0499]: cannot borrow `arena` as mutable more than once at a time</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch11_arena_alias.rs:22:13</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>21 |     let a = arena.alloc_bytes(b"one");</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|             ----- first mutable borrow occurs here</span></div></div><div><div><div>7</div></div><div><span>22 |     let b = arena.alloc_bytes(b"two");</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|             ^^^^^ second mutable borrow occurs here</span></div></div><div><div><div>9</div></div><div><span>23 |     println!("{a:?} {b:?}");</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|                - first borrow later used here</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_arena_alias.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The arena's promise, tested against the borrow checker: a second allocation</span></div></div><div><div><div>2</div></div><div><span>// while the first reference is alive must not compile.</span></div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>SafeArena</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>buffer</span><span>: </span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>offset</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>SafeArena</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>alloc_bytes</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>bytes</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; &amp;</span><span>mut</span><span> [</span><span>u8</span><span>] {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.offset;</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>start</span><span> + </span><span>bytes</span><span>.</span><span>len</span><span>();</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span><span>.offset </span><span>=</span><span> </span></span><span>end</span><span>;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>self</span><span>.buffer[</span><span>start</span><span>..</span><span>end</span><span>];</span></div></div><div><div><div>14</div></div><div><span>        </span><span>slot</span><span>.</span><span>copy_from_slice</span><span>(</span><span>bytes</span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>slot</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>SafeArena</span><span> { </span><span>buffer</span><span>: </span><span>vec!</span><span>[</span><span>0</span><span>; </span><span>16</span><span>], </span><span>offset</span><span>: </span><span>0</span><span> };</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"one"</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"two"</span><span>);</span></div></div><div><div><div>23</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{a:?} {b:?}"</span><span>);</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>alloc_bytes(&amp;mut self, ..) -&gt; &amp;mut [u8]</code>：返回的引用的生命周期被绑定到 <code>&amp;mut self</code> 的借用上（第 4 篇讲的省略规则：输出引用的生命周期取自 <code>&amp;mut self</code>）。只要 <code>a</code> 还活着，就不能再分配，更不能 <code>reset</code>。C++ 里一个 arena 返回 <code>char*</code>，调用者就算在 <code>reset</code> 之后还拿着旧指针，编译器也不会吭声。Rust 里，<strong>函数签名本身就是安全论证的一部分</strong>。接下来的 <code>unsafe</code> 实现换了内脏，但签名一个字没改，所以这个保证原样继承。</p><section><h3>unsafe 到底解锁了什么<a href="#unsafe-到底解锁了什么"><span>#</span></a></h3><p><code>unsafe</code> 不关掉借用检查器，也不关掉类型检查。它只解锁五件事，其余规则一切照旧：</p><ol>
<li>解引用裸指针</li>
<li>调用 <code>unsafe fn</code></li>
<li>读写 <code>static mut</code></li>
<li>实现 <code>unsafe trait</code></li>
<li>读取 <code>union</code> 的字段</li>
</ol><p>我把五件事各写了一行，放在一个能编译的文件里（<code>ch11_unsafe_five.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch11_unsafe_five</span></div></div><div><div><div>2</div></div><div><span>deref: 7</span></div></div><div><div><div>3</div></div><div><span>call: 7</span></div></div><div><div><div>4</div></div><div><span>static mut: 1</span></div></div><div><div><div>5</div></div><div><span>union: 0x3f800000</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_unsafe_five.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The five things `unsafe` unlocks, one line each.</span></div></div><div><div><div>2</div></div><div><span>static</span><span> </span><span>mut</span><span> </span><span>COUNTER</span><span>: </span><span>u32</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>union</span><span> </span><span>IntOrFloat</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>i</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>f</span><span>: </span><span>f32</span><span>,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>unsafe</span><span> </span><span>trait</span><span> </span><span>Zeroable</span><span> {}</span></div></div><div><div><div>10</div></div><div><span>unsafe</span><span> </span><span>impl</span><span> </span><span>Zeroable</span><span> </span><span>for</span><span> </span><span>u32</span><span> {}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>unsafe</span><span> </span><span>fn</span><span> </span><span>first_byte</span><span>(</span><span>p</span><span>: *</span><span>const</span><span> </span><span>u8</span><span>) -&gt; </span><span>u8</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>unsafe</span><span> { *</span><span>p</span><span> }</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>fn</span><span> </span><span>needs_zeroable</span><span>&lt;</span><span>T</span><span>: </span><span>Zeroable</span><span>&gt;() {}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>needs_zeroable</span><span>::&lt;</span><span>u32</span><span>&gt;();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>bytes</span><span><span> </span><span>=</span><span> [</span></span><span>7</span><span>u8</span><span>, </span><span>8</span><span>, </span><span>9</span><span>];</span></div></div><div><div><div>21</div></div><div><span>    </span><span>unsafe</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>// 1. dereference a raw pointer</span></div></div><div><div><div>23</div></div><div><span>        </span><span>println!</span><span>(</span><span>"deref: {}"</span><span>, *</span><span>bytes</span><span>.</span><span>as_ptr</span><span>());</span></div></div><div><div><div>24</div></div><div><span>        </span><span>// 2. call an unsafe function</span></div></div><div><div><div>25</div></div><div><span>        </span><span>println!</span><span>(</span><span>"call: {}"</span><span>, </span><span>first_byte</span><span>(</span><span>bytes</span><span>.</span><span>as_ptr</span><span>()));</span></div></div><div><div><div>26</div></div><div><span>        </span><span>// 3. read or write a mutable static</span></div></div><div><div><div>27</div></div><div><span>        </span><span>COUNTER</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>28</div></div><div><span>        </span><span>println!</span><span>(</span><span>"static mut: {}"</span><span>, *(&amp;</span><span>raw</span><span> </span><span>const</span><span> </span><span>COUNTER</span><span>));</span></div></div><div><div><div>29</div></div><div><span>        </span><span>// 4. (unsafe trait: see the `unsafe impl` above)</span></div></div><div><div><div>30</div></div><div><span>        </span><span>// 5. read a union field</span></div></div><div><div><div>31</div></div><div><span>        </span><span>let</span><span> </span><span>u</span><span><span> </span><span>=</span><span> </span></span><span>IntOrFloat</span><span> { </span><span>f</span><span>: </span><span>1.0</span><span> };</span></div></div><div><div><div>32</div></div><div><span>        </span><span>println!</span><span>(</span><span>"union: {:#x}"</span><span>, </span><span>u</span><span>.i);</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第四件事（<code>unsafe impl Zeroable for u32</code>）没有输出：它只是一句”我保证这个类型满足 trait 文档里的额外条件”。标准库里最有名的两个 <code>unsafe trait</code> 是 <code>Send</code> 和 <code>Sync</code>，第 13、14 篇会真的用到。<code>static mut</code> 和 <code>union</code> 留到第 13 篇，Pebble 本身不会用它们，第 13 篇会说明为什么。</p><p>为了证明第一条不是空话，我写了一个最短的反例（<code>ch11_unsafe_basic.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>x</span><span><span> </span><span>=</span><span> </span></span><span>42</span><span>u32</span><span>;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>p</span><span><span> </span><span>=</span><span> &amp;</span></span><span>x</span><span> </span><span>as</span><span> *</span><span>const</span><span> </span><span>u32</span><span>;</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, *</span><span>p</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch11_unsafe_basic</span></div></div><div><div><div>2</div></div><div><span>error[E0133]: dereference of raw pointer is unsafe and requires unsafe block</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch11_unsafe_basic.rs:4:20</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>4 |     println!("{}", *p);</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                    ^^ dereference of raw pointer</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_unsafe_basic.rs<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>x</span><span><span> </span><span>=</span><span> </span></span><span>42</span><span>u32</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>p</span><span><span> </span><span>=</span><span> &amp;</span></span><span>x</span><span> </span><span>as</span><span> *</span><span>const</span><span> </span><span>u32</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, *</span><span>p</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意：<strong>创建</strong>裸指针（<code>&amp;x as *const u32</code>）是安全的，<strong>解引用</strong>才不安全。<code>unsafe { *p }</code> 之后输出 <code>42</code>（<code>ch11_unsafe_basic_ok.rs</code>）。C 里没有这条分界：所有指针随时可以解引用。</p></section><section><h3>UB：C 程序员熟悉的那个词<a href="#ubc-程序员熟悉的那个词"><span>#</span></a></h3><p>Rust 的”安全代码不会引发 UB”是这门语言最重要的承诺。UB（undefined behavior，未定义行为）和 C 里是同一个意思：有符号溢出、越界读写、读未初始化内存、违反严格别名规则、解引用空指针，这些在 C 里”编译通过，运行时爱怎样怎样”。Rust 的安全子集里它们要么编译不过，要么在运行时 panic 或有定义地回绕（第 1 篇的溢出：debug 构建 panic，release 构建回绕）。进了 <code>unsafe</code> 块，你就是在对编译器说：“这一块里的 UB 由我负责排除。“如果你错了，后果和 C 里一样，没有 panic，没有报错，可能”偶尔能跑”。</p><p>所以 <code>unsafe</code> 的工程纪律是：<strong>块要小；每个块前面写一条 <code>// SAFETY:</code> 注释，论证为什么这里不会 UB；把 unsafe 包在一个安全的函数签名后面，让调用者无从犯错。</strong> 我们马上就会看到这三点。</p></section><section><h3>MaybeUninit 与裸指针<a href="#maybeuninit-与裸指针"><span>#</span></a></h3><p>arena 的缓冲区是 <code>Vec&lt;MaybeUninit&lt;u8&gt;&gt;</code>。<code>MaybeUninit&lt;T&gt;</code> 是”可能还没初始化的 <code>T</code>”：它不允许你直接读，必须先 <code>write</code> 再 <code>assume_init</code>，<code>assume_init</code> 是 <code>unsafe</code> 的，因为”这里确实已经初始化了”是你的担保。实验 <code>ch11_maybeuninit.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch11_maybeuninit</span></div></div><div><div><div>2</div></div><div><span>size_of::&lt;u8&gt;()               = 1</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;MaybeUninit&lt;u8&gt;&gt;()  = 1</span></div></div><div><div><div>4</div></div><div><span>size_of::&lt;MaybeUninit&lt;u64&gt;&gt;() = 8</span></div></div><div><div><div>5</div></div><div><span>Ok("ok")</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_maybeuninit.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::{size_of, </span></span><span>MaybeUninit</span><span>};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;u8&gt;()               = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>u8</span><span>&gt;());</span></div></div><div><div><div>5</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;MaybeUninit&lt;u8&gt;&gt;()  = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>MaybeUninit</span><span>&lt;</span><span>u8</span><span>&gt;&gt;());</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;MaybeUninit&lt;u64&gt;&gt;() = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>MaybeUninit</span><span>&lt;</span><span>u64</span><span>&gt;&gt;());</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>// A buffer that does not claim to contain valid bytes yet.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>buffer</span><span>: </span><span>Vec</span><span>&lt;</span><span>MaybeUninit</span><span>&lt;</span><span>u8</span><span><span>&gt;&gt; </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>MaybeUninit</span><span>::</span><span>uninit</span><span>(); </span><span>4</span><span>];</span></div></div><div><div><div>10</div></div><div><span>    </span><span>buffer</span><span>[</span><span>0</span><span>].</span><span>write</span><span>(</span><span>b'o'</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>buffer</span><span>[</span><span>1</span><span>].</span><span>write</span><span>(</span><span>b'k'</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>// SAFETY: indices 0 and 1 were initialised on the two lines above.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>ok</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> { [</span><span>buffer</span><span>[</span><span>0</span><span>].</span><span>assume_init</span><span>(), </span><span>buffer</span><span>[</span><span>1</span><span>].</span><span>assume_init</span><span>()] };</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span><span>, </span><span>std</span><span>::</span></span><span>str</span><span>::</span><span>from_utf8</span><span>(&amp;</span><span>ok</span><span>));</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>大小和内部类型一样，没有额外的”是否已初始化”标记，编译器只是不再假设里面有合法的值。为什么这个很重要：在 Rust 里，哪怕只是”读一个未初始化的 <code>u8</code> 变量”也是 UB，不能因为”反正是字节”就放过；据我所知 C 对 <code>unsigned char</code> 有特殊照顾，Rust 没有这个例外。所以”手里有一块还没写过的字节缓冲区”这个概念，在 Rust 里必须用 <code>MaybeUninit</code> 来表达。</p><p>裸指针 <code>*const T</code>/<code>*mut T</code> 和 C 指针最像：可以为空、可以悬空、没有生命周期。同一个实验目录里的 <code>ch11_raw_ptr.rs</code> 做了 arena 里用到的全部动作：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch11_raw_ptr</span></div></div><div><div><div>2</div></div><div><span>null? false  null_mut? true</span></div></div><div><div><div>3</div></div><div><span>[80, 69, 66, 66, 76, 69]</span></div></div><div><div><div>4</div></div><div><span>PEBBLE</span></div></div><div><div><div>5</div></div><div><span>[0, 80, 69, 66, 66, 76, 69, 0]</span></div></div><div><div><div>6</div></div><div><span>checked: Err(Utf8Error { valid_up_to: 0, error_len: Some(1) })</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
完整源码：ch11_raw_ptr.rs<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::{ptr, slice};</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>storage</span><span><span> </span><span>=</span><span> [</span></span><span>0</span><span>u8</span><span>; </span><span>8</span><span>];</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>base</span><span>: *</span><span>mut</span><span> </span><span>u8</span><span><span> </span><span>=</span><span> </span></span><span>storage</span><span>.</span><span>as_mut_ptr</span><span>();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"null? {}  null_mut? {}"</span><span>, </span><span>base</span><span>.</span><span>is_null</span><span><span>(), </span><span>ptr</span><span>::</span></span><span>null_mut</span><span>::&lt;</span><span>u8</span><span>&gt;().</span><span>is_null</span><span>());</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>b"pebble"</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// SAFETY: both ranges are in bounds (6 &lt;= 8) and do not overlap.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>view</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>ptr</span><span>::</span><span>copy_nonoverlapping</span><span>(</span><span>source</span><span>.</span><span>as_ptr</span><span>(), </span><span>base</span><span>.</span><span>add</span><span>(</span><span>1</span><span>), </span><span>source</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>slice</span><span>::</span><span>from_raw_parts_mut</span><span>(</span><span>base</span><span>.</span><span>add</span><span>(</span><span>1</span><span>), </span><span>source</span><span>.</span><span>len</span><span>())</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>14</div></div><div><span>    </span><span>view</span><span>.</span><span>make_ascii_uppercase</span><span>();</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{view:?}"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span><span>, </span><span>std</span><span>::</span></span><span>str</span><span>::</span><span>from_utf8</span><span>(</span><span>view</span><span>).</span><span>unwrap</span><span>());</span></div></div><div><div><div>17</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{storage:?}"</span><span>);</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>// `str::from_utf8` checks; the unchecked version trusts you.</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>bad</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>0xff</span><span>u8</span><span>, </span><span>0xfe</span><span>];</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"checked: {:?}"</span><span><span>, </span><span>std</span><span>::</span></span><span>str</span><span>::</span><span>from_utf8</span><span>(&amp;</span><span>bad</span><span>));</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对应的核心代码（完整文件里还有输出语句）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>storage</span><span><span> </span><span>=</span><span> [</span></span><span>0</span><span>u8</span><span>; </span><span>8</span><span>];</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>base</span><span>: *</span><span>mut</span><span> </span><span>u8</span><span><span> </span><span>=</span><span> </span></span><span>storage</span><span>.</span><span>as_mut_ptr</span><span>();</span></div></div><div><div><div>3</div></div><div><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>b"pebble"</span><span>;</span></div></div><div><div><div>4</div></div><div><span>let</span><span> </span><span>view</span><span><span> </span><span>=</span><span> </span></span><span>unsafe</span><span> {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ptr</span><span>::</span><span>copy_nonoverlapping</span><span>(</span><span>source</span><span>.</span><span>as_ptr</span><span>(), </span><span>base</span><span>.</span><span>add</span><span>(</span><span>1</span><span>), </span><span>source</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>slice</span><span>::</span><span>from_raw_parts_mut</span><span>(</span><span>base</span><span>.</span><span>add</span><span>(</span><span>1</span><span>), </span><span>source</span><span>.</span><span>len</span><span>())</span></div></div><div><div><div>7</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个函数：</p><ul>
<li><code>base.add(1)</code>：指针算术，和 C 的 <code>p + 1</code> 一样按元素大小前进。它本身是 <code>unsafe</code> 的，因为越过了分配的边界就是 UB。</li>
<li><code>ptr::copy_nonoverlapping(src, dst, n)</code>：就是 <code>memcpy</code>，名字里写明”不重叠”这个前提；有重叠要用 <code>ptr::copy</code>（<code>memmove</code>）。前提不成立是 UB。</li>
<li><code>slice::from_raw_parts_mut(ptr, len)</code>：把”指针加长度”变回一个带借用语义的 <code>&amp;mut [T]</code>。这是裸指针世界回到安全世界的出口，从这里开始借用检查器重新接管。</li>
</ul><p>输出验证了”贴着数组 1 号位置写了 6 个字节，首尾两个槽位保持 0”。最后一行是 <code>str::from_utf8</code> 对非法字节的反应：返回 <code>Err</code> 而不是 UB。它的 <code>unchecked</code> 兄弟 <code>from_utf8_unchecked_mut</code> 跳过这个检查，你必须自己保证字节合法，这正是 <code>alloc_str</code> 里的那一行。</p></section><section><h3>写 bump.rs<a href="#写-bumprs"><span>#</span></a></h3><p>现在我们写 <code>crates/pebble-gc/src/bump.rs</code>。文件开头是模块文档和几个 <code>use</code>（文档里的 <code># Alignment</code> 一节说明这个 arena 只适合对齐为 1 的数据）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! A tiny bump (arena) allocator built on raw pointers.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! The arena owns one flat byte buffer and hands out slices from the front by</span></div></div><div><div><div>4</div></div><div><span>//! advancing an offset. Individual allocations can never be freed; the whole</span></div></div><div><div><div>5</div></div><div><span>//! arena is rewound at once with [`BumpArena::reset`]. In exchange, allocation</span></div></div><div><div><div>6</div></div><div><span>//! is a bounds check, a pointer bump and a `memcpy`, and deallocation is free.</span></div></div><div><div><div>7</div></div><div><span>//!</span></div></div><div><div><div>8</div></div><div><span>//! # Alignment</span></div></div><div><div><div>9</div></div><div><span>//!</span></div></div><div><div><div>10</div></div><div><span>//! Every allocation starts at the arena's current offset, which is only</span></div></div><div><div><div>11</div></div><div><span>//! advanced by whole bytes. The effective alignment is therefore</span></div></div><div><div><div>12</div></div><div><span>//! `align_of::&lt;u8&gt;()`, i.e. 1, which is exactly what byte strings and</span></div></div><div><div><div>13</div></div><div><span>//! consequently `str` need. This arena is not suitable for values with a</span></div></div><div><div><div>14</div></div><div><span>//! stricter alignment without adding an alignment-aware bump.</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>mem</span><span>::</span></span><span>MaybeUninit</span><span>;</span></div></div><div><div><div>17</div></div><div><span>use</span><span><span> </span><span>core</span><span>::ptr;</span></span></div></div><div><div><div>18</div></div><div><span>use</span><span><span> </span><span>core</span><span>::slice;</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>结构体和构造函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A bump arena: a fixed-size byte buffer handed out from the front.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Returned references borrow the arena mutably, so the borrow checker</span></div></div><div><div><div>4</div></div><div><span>/// guarantees that a second allocation cannot invalidate an outstanding</span></div></div><div><div><div>5</div></div><div><span>/// reference and that [`BumpArena::reset`] cannot run while one is alive.</span></div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>BumpArena</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>buffer</span><span>: </span><span>Vec</span><span>&lt;</span><span>MaybeUninit</span><span>&lt;</span><span>u8</span><span>&gt;&gt;,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>offset</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>BumpArena</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>/// Creates an arena owning `bytes` of storage.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_capacity</span><span>(</span><span>bytes</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>buffer</span><span>: </span><span>vec!</span><span>[</span><span>MaybeUninit</span><span>::</span><span>uninit</span><span>(); </span><span>bytes</span><span>],</span></div></div><div><div><div>16</div></div><div><span>            </span><span>offset</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>vec![MaybeUninit::uninit(); bytes]</code> 能用是因为 <code>MaybeUninit&lt;u8&gt;</code> 是 <code>Copy</code>，<code>vec!</code> 需要克隆元素。我没有检查生成的汇编里它是不是真的省掉了清零，所以不替它声称性能优势；这里用 <code>MaybeUninit</code> 的原因是语义上的：这块内存里不存在合法的 <code>u8</code>。</p><p>字符串的版本只是字节版本的一层包装：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Copies `s` into the arena and returns the copy.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>4</div></div><div><span>    </span><span>///</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// Panics if the arena does not have enough free space left.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>alloc_str</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>s</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>mut</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>bytes</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>alloc_bytes</span><span>(</span><span>s</span><span>.</span><span>as_bytes</span><span>());</span></div></div><div><div><div>8</div></div><div><span>        </span><span>// SAFETY: `alloc_bytes` copied the exact bytes of `s`, which is valid</span></div></div><div><div><div>9</div></div><div><span>        </span><span>// UTF-8, so the returned byte slice is still valid UTF-8.</span></div></div><div><div><div>10</div></div><div><span>        </span><span>unsafe</span><span><span> { </span><span>core</span><span>::</span></span><span>str</span><span>::</span><span>from_utf8_unchecked_mut</span><span>(</span><span>bytes</span><span>) }</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>// SAFETY:</code> 注释的格式是社区约定：每个 <code>unsafe</code> 块前一条，论证”为什么这里满足前提”。注释写不出来的时候，通常说明这个 <code>unsafe</code> 不该存在。</p><p>核心函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Copies `bytes` into the arena and returns the copy.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>4</div></div><div><span>    </span><span>///</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// Panics if the arena does not have enough free space left.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>alloc_bytes</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>bytes</span><span>: &amp;[</span><span>u8</span><span>]) -&gt; &amp;</span><span>mut</span><span> [</span><span>u8</span><span>] {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.offset;</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>start</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>.</span><span>checked_add</span><span>(</span><span>bytes</span><span>.</span><span>len</span><span>())</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>.</span><span>expect</span><span>(</span><span>"bump arena size overflowed usize"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>12</div></div><div><span>            </span><span>end</span><span><span> </span><span>&lt;=</span><span> </span></span><span>self</span><span>.buffer.</span><span>len</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>"bump arena capacity of {} bytes exceeded"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>            </span><span>self</span><span>.buffer.</span><span>len</span><span>()</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span><span>.offset </span><span>=</span><span> </span></span><span>end</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>        </span><span>// SAFETY: `start..end` was just checked to be inside `self.buffer`, and</span></div></div><div><div><div>19</div></div><div><span>        </span><span>// `MaybeUninit&lt;u8&gt;` has the same layout and alignment (1) as `u8`, so</span></div></div><div><div><div>20</div></div><div><span>        </span><span>// the cast keeps the pointer properly aligned and in bounds.</span></div></div><div><div><div>21</div></div><div><span>        </span><span>// `copy_nonoverlapping` is sound because `bytes` is an external slice</span></div></div><div><div><div>22</div></div><div><span>        </span><span>// that cannot alias the arena. The returned range is not handed out</span></div></div><div><div><div>23</div></div><div><span>        </span><span>// again: `offset` only moves forward, and `reset` needs `&amp;mut self`, so</span></div></div><div><div><div>24</div></div><div><span>        </span><span>// it cannot run while the returned reference is alive.</span></div></div><div><div><div>25</div></div><div><span>        </span><span>unsafe</span><span> {</span></div></div><div><div><div>26</div></div><div><span>            </span><span>let</span><span> </span><span>base</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.buffer.</span><span>as_mut_ptr</span><span>().</span><span>cast</span><span>::&lt;</span><span>u8</span><span>&gt;();</span></div></div><div><div><div>27</div></div><div><span>            </span><span>let</span><span> </span><span>destination</span><span><span> </span><span>=</span><span> </span></span><span>base</span><span>.</span><span>add</span><span>(</span><span>start</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>ptr</span><span>::</span><span>copy_nonoverlapping</span><span>(</span><span>bytes</span><span>.</span><span>as_ptr</span><span>(), </span><span>destination</span><span>, </span><span>bytes</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>slice</span><span>::</span><span>from_raw_parts_mut</span><span>(</span><span>destination</span><span>, </span><span>bytes</span><span>.</span><span>len</span><span>())</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>把这个 SAFETY 注释当成清单逐条看：</p><ol>
<li><strong>在界内</strong>：<code>end &lt;= self.buffer.len()</code> 的 <code>assert!</code> 在 <code>unsafe</code> 之前，所以 <code>base.add(start)</code> 和写入 <code>bytes.len()</code> 个字节都落在缓冲区里。<code>checked_add</code> 防止 <code>start + len</code> 绕回（第 1 篇的溢出）。</li>
<li><strong>对齐与布局</strong>：<code>MaybeUninit&lt;u8&gt;</code> 和 <code>u8</code> 布局相同，<code>cast::&lt;u8&gt;()</code> 只是改指针的类型标签。</li>
<li><strong>不重叠</strong>：<code>bytes</code> 是外部传进来的切片，而 arena 的缓冲区是我们独占的 <code>Vec</code>，两者不可能是同一块内存。</li>
<li><strong>不重复发出</strong>：<code>offset</code> 只前进，所以这一段字节不会第二次发出去；<code>reset</code> 需要 <code>&amp;mut self</code>，而返回的引用借用着 <code>self</code>，所以它们不能同时存在。</li>
</ol><p>这四条里最重要的是第四条，它不靠”小心”，靠签名和借用检查器。</p><p>这里还有一件有意思的事：<code>assert!</code> 是真的 <code>assert!</code>，不是 <code>debug_assert!</code>。它在 release 构建里也在。理由写在练习 3 里：去掉这个检查就是缓冲区溢出。</p><p>三个小访问器和 <code>reset</code>，闭合 <code>impl</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Number of bytes handed out so far.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>used</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.offset</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>/// Total number of bytes the arena owns.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>capacity</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span>.buffer.</span><span>len</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>/// Rewinds the arena, freeing every allocation at once.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>///</span></div></div><div><div><div>13</div></div><div><span>    </span><span>/// The bytes are not zeroed, and previously returned references cannot be</span></div></div><div><div><div>14</div></div><div><span>    </span><span>/// used afterwards because `reset` borrows the arena mutably.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>reset</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span><span>.offset </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>reset</code> 是安全函数，没有 <code>unsafe</code>：它只是把偏移量设回 0，内存还在，没有 UB。唯一的危险是”旧引用还被使用”，这由 <code>&amp;mut self</code> 排除。</p><p><code>unsafe fn</code> 的写法这里没出现，因为我们把所有 <code>unsafe</code> 都封进了安全函数里。但工作区的 <code>Cargo.toml</code> 里有一条 lint：<code>unsafe_op_in_unsafe_fn = "warn"</code>。它管的是另一种情形：在一个 <code>unsafe fn</code> <strong>里面</strong>直接做不安全操作而不套 <code>unsafe</code> 块。我写了个最小例子（<code>ch11_unsafe_fn_lint.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>unsafe</span><span> </span><span>fn</span><span> </span><span>first_byte</span><span>(</span><span>p</span><span>: *</span><span>const</span><span> </span><span>u8</span><span>) -&gt; </span><span>u8</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>*</span><span>p</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch11_unsafe_fn_lint</span></div></div><div><div><div>2</div></div><div><span>warning[E0133]: dereference of raw pointer is unsafe and requires unsafe block</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch11_unsafe_fn_lint.rs:2:5</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>2 |     *p</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|     ^^ dereference of raw pointer</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior</span></div></div><div><div><div>9</div></div><div><span>note: an unsafe function restricts its caller, but its body is safe by default</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch11_unsafe_fn_lint.rs:1:1</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>1 | unsafe fn first_byte(p: *const u8) -&gt; u8 {</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>14</div></div><div><span>...</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>= note: `#[warn(unsafe_op_in_unsafe_fn)]` (part of `#[warn(rust_2024_compatibility)]`) on by default</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（2024 edition 里这条 lint 本来就是 warn，工作区的 <code>unsafe_op_in_unsafe_fn = "warn"</code> 那一行只是把它写明。）</p>
完整源码：ch11_unsafe_fn_lint.rs<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>unsafe</span><span> </span><span>fn</span><span> </span><span>first_byte</span><span>(</span><span>p</span><span>: *</span><span>const</span><span> </span><span>u8</span><span>) -&gt; </span><span>u8</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>*</span><span>p</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>bytes</span><span><span> </span><span>=</span><span> [</span></span><span>7</span><span>u8</span><span>, </span><span>8</span><span>, </span><span>9</span><span>];</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>unsafe</span><span> { </span><span>first_byte</span><span>(</span><span>bytes</span><span>.</span><span>as_ptr</span><span>()) });</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>unsafe fn</code> 的 <code>unsafe</code> 是对<strong>调用者</strong>的警告（“你调用我要满足前提”），函数体里想做不安全操作仍然需要自己的 <code>unsafe</code> 块、自己的 SAFETY 论证。老版本的 Rust 里整个函数体默认是 unsafe 的，2024 edition 把它改了。第 13 篇的 FFI 声明和 <code>unsafe extern "C"</code> 块会大量用到这个。</p><p>最后是 bump 的四个测试，全部写出来（每个都很短）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::</span><span>BumpArena</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>allocates_str_and_bytes</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>BumpArena</span><span>::</span><span>with_capacity</span><span>(</span><span>64</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>9</div></div><div><span>            </span><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_str</span><span>(</span><span>"hello"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>            </span><span>assert_eq!</span><span>(&amp;*</span><span>text</span><span>, </span><span>"hello"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>            </span><span>text</span><span>.</span><span>make_ascii_uppercase</span><span>();</span></div></div><div><div><div>12</div></div><div><span>            </span><span>assert_eq!</span><span>(&amp;*</span><span>text</span><span>, </span><span>"HELLO"</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>15</div></div><div><span>            </span><span>let</span><span> </span><span>bytes</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(&amp;[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]);</span></div></div><div><div><div>16</div></div><div><span>            </span><span>assert_eq!</span><span>(</span><span>bytes</span><span>, &amp;[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]);</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>arena</span><span>.</span><span>used</span><span>(), </span><span>8</span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>arena</span><span>.</span><span>capacity</span><span>(), </span><span>64</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>24</div></div><div><span>    </span><span>fn</span><span> </span><span>reset_reuses_storage</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>BumpArena</span><span>::</span><span>with_capacity</span><span>(</span><span>16</span><span>);</span></div></div><div><div><div>26</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"0123456789"</span><span>).</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>27</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>arena</span><span>.</span><span>used</span><span>(), </span><span>10</span><span>);</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>        </span><span>arena</span><span>.</span><span>reset</span><span>();</span></div></div><div><div><div>30</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>arena</span><span>.</span><span>used</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>        </span><span>let</span><span> </span><span>again</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"abcdefghij"</span><span>);</span></div></div><div><div><div>33</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>again</span><span>, </span><span>b"abcdefghij"</span><span>);</span></div></div><div><div><div>34</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>again</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>, </span><span>start</span><span>, </span><span>"storage is reused verbatim"</span><span>);</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>38</div></div><div><span>    </span><span>fn</span><span> </span><span>allocations_do_not_overlap</span><span>() {</span></div></div><div><div><div>39</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>BumpArena</span><span>::</span><span>with_capacity</span><span>(</span><span>32</span><span>);</span></div></div><div><div><div>40</div></div><div><span>        </span><span>let</span><span> (</span><span>first_start</span><span>, </span><span>first_len</span><span><span>) </span><span>=</span><span> {</span></span></div></div><div><div><div>41</div></div><div><span>            </span><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"aaaa"</span><span>);</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>(</span><span>first</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>, </span><span>first</span><span>.</span><span>len</span><span>())</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>44</div></div><div><span>        </span><span>let</span><span> (</span><span>second_start</span><span>, </span><span>second_len</span><span><span>) </span><span>=</span><span> {</span></span></div></div><div><div><div>45</div></div><div><span>            </span><span>let</span><span> </span><span>second</span><span><span> </span><span>=</span><span> </span></span><span>arena</span><span>.</span><span>alloc_str</span><span>(</span><span>"bbbbbb"</span><span>);</span></div></div><div><div><div>46</div></div><div><span><span>            </span></span><span>(</span><span>second</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>, </span><span>second</span><span>.</span><span>len</span><span>())</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>        </span><span>let</span><span> </span><span>first_end</span><span><span> </span><span>=</span><span> </span></span><span>first_start</span><span> + </span><span>first_len</span><span>;</span></div></div><div><div><div>50</div></div><div><span>        </span><span>let</span><span> </span><span>second_end</span><span><span> </span><span>=</span><span> </span></span><span>second_start</span><span> + </span><span>second_len</span><span>;</span></div></div><div><div><div>51</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>52</div></div><div><span>            </span><span>first_end</span><span><span> </span><span>&lt;=</span><span> </span></span><span>second_start</span><span><span> </span><span>||</span><span> </span></span><span>second_end</span><span><span> </span><span>&lt;=</span><span> </span></span><span>first_start</span><span>,</span></div></div><div><div><div>53</div></div><div><span>            </span><span>"allocations overlap: {first_start}..{first_end} vs {second_start}..{second_end}"</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>#[should_panic(expected </span><span>=</span><span> </span><span>"capacity"</span><span>)]</span></div></div><div><div><div>59</div></div><div><span>    </span><span>fn</span><span> </span><span>over_capacity_panics</span><span>() {</span></div></div><div><div><div>60</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>arena</span><span><span> </span><span>=</span><span> </span></span><span>BumpArena</span><span>::</span><span>with_capacity</span><span>(</span><span>4</span><span>);</span></div></div><div><div><div>61</div></div><div><span>        </span><span>arena</span><span>.</span><span>alloc_bytes</span><span>(</span><span>b"12345"</span><span>);</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意 <code>allocates_str_and_bytes</code> 里那两对花括号：它们把 <code>text</code> 和 <code>bytes</code> 的借用范围画出来，读起来一目了然。严格说它们不是必需的：我把花括号去掉试过（放在一个临时测试里），照样编译通过、测试通过，因为第 2 篇讲的 NLL 让借用在最后一次使用处就结束，不必等到作用域末尾。花括号只是风格。</p></section><section><h3>Miri 和这个 arena<a href="#miri-和这个-arena"><span>#</span></a></h3><p>对写 <code>unsafe</code> 的人，正经的下一步是用 Miri（Rust 的 MIR 解释器）跑测试：它能在解释执行时检查越界、未初始化读、别名违规，是测 <code>unsafe</code> 的标准工具。<strong>我这台机器上没有装 Miri，所以我没有跑过它</strong>，上面的 SAFETY 论证是我读出来的，不是工具验出来的。你的环境里如果装了，<code>cargo +nightly miri test -p pebble-gc</code> 是一个值得做的练习；如果它报错，我愿意相信它而不是相信我的注释。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在你的 workspace 根目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-gc</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是我在自己的重建目录里的真实输出（省略编译行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 13 tests</span></div></div><div><div><div>2</div></div><div><span>test bump::tests::over_capacity_panics - should panic ... ok</span></div></div><div><div><div>3</div></div><div><span>test bump::tests::reset_reuses_storage ... ok</span></div></div><div><div><div>4</div></div><div><span>test bump::tests::allocates_str_and_bytes ... ok</span></div></div><div><div><div>5</div></div><div><span>test bump::tests::allocations_do_not_overlap ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::alloc_get_and_get_mut ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::collect_without_roots_frees_everything ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::freed_slots_are_reused ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::getting_a_freed_reference_panics - should panic ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::is_alive_tracks_slots ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::mark_traces_children_transitively ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::unreachable_cycles_are_collected ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::deeply_nested_lists_survive ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::randomised_reachability_matches_sweep ... ok</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>Doc-tests pebble_gc</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>running 1 test</span></div></div><div><div><div>21</div></div><div><span>test crates/pebble-gc/src/lib.rs - (line 24) ... ok</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>13 个单元测试加 1 个 doc test（来自 crate 文档里的 <code># Example</code>）。测试名字顺序不由我们决定（每次运行线程调度不同），你的顺序可能和我的不同，通过数量一致即可。再跑 <code>cargo clippy -p pebble-gc --all-targets -- -D warnings</code> 与 <code>cargo fmt --all --check</code>，我这边都没有输出任何警告。</p><p>完成了以下这些文件，它们应该和标签 <code>tutorial-v1</code> 逐字节相同：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-gc/Cargo.toml" target="_blank"><code>crates/pebble-gc/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-gc/src/lib.rs" target="_blank"><code>crates/pebble-gc/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-gc/src/bump.rs" target="_blank"><code>crates/pebble-gc/src/bump.rs</code></a></li>
</ul><p>比对方式（<code>~/pebble-tag</code> 是 tag 的本地克隆，在你的 workspace 根目录运行）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> </span><span>f</span><span> </span><span>in</span><span> </span><span>Cargo.toml</span><span> </span><span>src/lib.rs</span><span> </span><span>src/bump.rs</span><span>; </span><span>do</span></div></div><div><div><div>2</div></div><div><span>  </span><span>diff</span><span> </span><span>&lt;(</span><span>git</span><span> </span><span>-C</span><span> ~/pebble-tag show tutorial-v1:crates/pebble-gc/</span><span>$f</span><span>)</span><span> </span><span>crates/pebble-gc/</span><span>$f</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>"</span><span>$f</span><span> ok"</span></div></div><div><div><div>3</div></div><div><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有中间状态文件：这一篇的 crate 是一次写完的。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）。<code>git clone</code> 到 <code>~/pebble-tag</code> 的话，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，用 <code>cd ~/pebble-tag/lab/rust-lab</code> 运行（也可以把这个目录拷到 <code>~/rust-lab</code>）。<code>lab-ch11-gc</code> 是独立的小 crate，在 <code>lab/lab-ch11-gc/</code> 里，用路径依赖指向仓库里的 <code>crates/pebble-gc</code>，命令已写出完整目录。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_sizes.rs" target="_blank"><code>ch11_sizes.rs</code></a></td><td><code>cargo run --quiet --bin ch11_sizes</code></td><td>运行，首行 <code>GcRef                 = 4 bytes</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch11-gc/src/main.rs" target="_blank"><code>main.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch11-gc &amp;&amp; cargo run --quiet --bin lab-ch11-gc</code></td><td>运行，首行 <code>alive after free: false</code>（后面是 <code>old == new: true</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_cell_naive.rs" target="_blank"><code>ch11_cell_naive.rs</code></a></td><td><code>cargo build --bin ch11_cell_naive</code></td><td>编译失败：E0500</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_cell_roots.rs" target="_blank"><code>ch11_cell_roots.rs</code></a></td><td><code>cargo build --bin ch11_cell_roots</code></td><td>编译失败：E0502</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_cell.rs" target="_blank"><code>ch11_cell.rs</code></a></td><td><code>cargo run --quiet --bin ch11_cell</code></td><td>运行，输出 <code>[true, true, false]</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_deep_recursion.rs" target="_blank"><code>ch11_deep_recursion.rs</code></a></td><td><code>cargo run --quiet --bin ch11_deep_recursion</code></td><td>不带参数运行，输出 <code>marked 10001 objects at depth 10000</code>（文章里 20000 层、50000 层的栈溢出要传参数，见上文）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_collections.rs" target="_blank"><code>ch11_collections.rs</code></a></td><td><code>cargo run --quiet --bin ch11_collections</code></td><td>运行，首行 <code>insert 5: hash new? true  tree new? true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_bump_safe.rs" target="_blank"><code>ch11_bump_safe.rs</code></a></td><td><code>cargo run --quiet --bin ch11_bump_safe</code></td><td>运行，首行 <code>PEB</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_arena_alias.rs" target="_blank"><code>ch11_arena_alias.rs</code></a></td><td><code>cargo build --bin ch11_arena_alias</code></td><td>编译失败：E0499</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_unsafe_five.rs" target="_blank"><code>ch11_unsafe_five.rs</code></a></td><td><code>cargo run --quiet --bin ch11_unsafe_five</code></td><td>运行，首行 <code>deref: 7</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_unsafe_basic.rs" target="_blank"><code>ch11_unsafe_basic.rs</code></a></td><td><code>cargo build --bin ch11_unsafe_basic</code></td><td>编译失败：E0133</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_unsafe_basic_ok.rs" target="_blank"><code>ch11_unsafe_basic_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch11_unsafe_basic_ok</code></td><td>运行，输出 <code>42</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_maybeuninit.rs" target="_blank"><code>ch11_maybeuninit.rs</code></a></td><td><code>cargo run --quiet --bin ch11_maybeuninit</code></td><td>运行，首行 <code>size_of::&lt;u8&gt;()               = 1</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_raw_ptr.rs" target="_blank"><code>ch11_raw_ptr.rs</code></a></td><td><code>cargo run --quiet --bin ch11_raw_ptr</code></td><td>运行，首行 <code>null? false  null_mut? true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch11_unsafe_fn_lint.rs" target="_blank"><code>ch11_unsafe_fn_lint.rs</code></a></td><td><code>cargo run --quiet --bin ch11_unsafe_fn_lint</code></td><td>运行，输出 <code>7</code>，编译时有警告 <code>warning[E0133]: dereference of raw pointer is unsafe and requires unsafe block</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch11-gc/src/bin/generation.rs" target="_blank"><code>generation.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch11-gc &amp;&amp; cargo run --quiet --bin generation</code></td><td>运行（练习 1），首行 <code>old == new: false</code></td></tr></tbody></table></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>练习 1（讨论）</strong>：给 <code>GcRef</code> 加一个代数计数，让上面实验里的”旧句柄读到新对象”变成可检测的。</p>
参考答案<p>把 <code>GcRef</code> 从 <code>u32</code> 改成两个字段 <code>index</code>、<code>generation</code>，每个槽位也记一个代数，<code>free</code> 时代数加一，<code>get</code> 时先比较代数。我在实验里写了一个最小版本（和上面的 <code>lab-ch11-gc</code> 同一个小 crate，放在 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch11-gc/src/bin/generation.rs" target="_blank"><code>src/bin/generation.rs</code></a>，不依赖 <code>pebble-gc</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Exercise 1: a generation counter makes stale handles detectable.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>GcRef</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>index</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>generation</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>struct</span><span> </span><span>Slot</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>generation</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>object</span><span>: </span><span>Option</span><span>&lt;</span><span>O</span><span>&gt;,</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>struct</span><span> </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>slots</span><span>: </span><span>Vec</span><span>&lt;</span><span>Slot</span><span>&lt;</span><span>O</span><span>&gt;&gt;,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>free</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span>&gt;,</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>impl</span><span>&lt;</span><span>O</span><span>&gt; </span><span>Heap</span><span>&lt;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Self</span><span> { </span><span>slots</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(), </span><span>free</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>() }</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>fn</span><span> </span><span>alloc</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>object</span><span>: </span><span>O</span><span>) -&gt; </span><span>GcRef</span><span> {</span></div></div><div><div><div>24</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>index</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.free.</span><span>pop</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>self</span><span>.slots[</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>];</span></div></div><div><div><div>26</div></div><div><span>            </span><span>slot</span><span><span>.object </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>object</span><span>);</span></div></div><div><div><div>27</div></div><div><span>            </span><span>GcRef</span><span> { </span><span>index</span><span>, </span><span>generation</span><span>: </span><span>slot</span><span>.generation }</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>29</div></div><div><span>            </span><span>self</span><span>.slots.</span><span>push</span><span>(</span><span>Slot</span><span> { </span><span>generation</span><span>: </span><span>0</span><span>, </span><span>object</span><span>: </span><span>Some</span><span>(</span><span>object</span><span>) });</span></div></div><div><div><div>30</div></div><div><span>            </span><span>GcRef</span><span> { </span><span>index</span><span>: </span><span>self</span><span>.slots.</span><span>len</span><span>() </span><span>as</span><span> </span><span>u32</span><span> - </span><span>1</span><span>, </span><span>generation</span><span>: </span><span>0</span><span> }</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span>fn</span><span> </span><span>free</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) {</span></div></div><div><div><div>35</div></div><div><span>        </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>self</span><span>.slots[</span><span>r</span><span>.index </span><span>as</span><span> </span><span>usize</span><span>];</span></div></div><div><div><div>36</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>slot</span><span>.generation, </span><span>r</span><span>.generation, </span><span>"stale handle"</span><span>);</span></div></div><div><div><div>37</div></div><div><span>        </span><span>slot</span><span><span>.object </span><span>=</span><span> </span></span><span>None</span><span>;</span></div></div><div><div><div>38</div></div><div><span>        </span><span>slot</span><span><span>.generation </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>39</div></div><div><span>        </span><span>self</span><span>.free.</span><span>push</span><span>(</span><span>r</span><span>.index);</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span>fn</span><span> </span><span>get</span><span>(&amp;</span><span>self</span><span>, </span><span>r</span><span>: </span><span>GcRef</span><span>) -&gt; </span><span>Option</span><span>&lt;&amp;</span><span>O</span><span>&gt; {</span></div></div><div><div><div>43</div></div><div><span>        </span><span>let</span><span> </span><span>slot</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.slots.</span><span>get</span><span>(</span><span>r</span><span>.index </span><span>as</span><span> </span><span>usize</span><span>)?;</span></div></div><div><div><div>44</div></div><div><span>        </span><span>if</span><span> </span><span>slot</span><span><span>.generation </span><span>==</span><span> </span></span><span>r</span><span>.generation { </span><span>slot</span><span>.object.</span><span>as_ref</span><span>() } </span><span>else</span><span> { </span><span>None</span><span> }</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>49</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>heap</span><span><span> </span><span>=</span><span> </span></span><span>Heap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>50</div></div><div><span>    </span><span>let</span><span> </span><span>old</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span>(</span><span>"first"</span><span>);</span></div></div><div><div><div>51</div></div><div><span>    </span><span>heap</span><span>.</span><span>free</span><span>(</span><span>old</span><span>);</span></div></div><div><div><div>52</div></div><div><span>    </span><span>let</span><span> </span><span>new</span><span><span> </span><span>=</span><span> </span></span><span>heap</span><span>.</span><span>alloc</span><span>(</span><span>"second"</span><span>);</span></div></div><div><div><div>53</div></div><div><span>    </span><span>println!</span><span>(</span><span>"old == new: {}"</span><span>, </span><span>old</span><span><span> </span><span>==</span><span> </span></span><span>new</span><span>);</span></div></div><div><div><div>54</div></div><div><span>    </span><span>println!</span><span>(</span><span>"old reads as {:?}"</span><span>, </span><span>heap</span><span>.</span><span>get</span><span>(</span><span>old</span><span>));</span></div></div><div><div><div>55</div></div><div><span>    </span><span>println!</span><span>(</span><span>"new reads as {:?}"</span><span>, </span><span>heap</span><span>.</span><span>get</span><span>(</span><span>new</span><span>));</span></div></div><div><div><div>56</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;GcRef&gt;() = {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>GcRef</span><span>&gt;());</span></div></div><div><div><div>57</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>alloc</code> 复用空槽时把槽位的当前代数写进新句柄；<code>get</code> 里 <code>if slot.generation == r.generation { slot.object.as_ref() } else { None }</code>。运行结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin generation</span></div></div><div><div><div>2</div></div><div><span>old == new: false</span></div></div><div><div><div>3</div></div><div><span>old reads as None</span></div></div><div><div><div>4</div></div><div><span>new reads as Some("second")</span></div></div><div><div><div>5</div></div><div><span>size_of::&lt;GcRef&gt;() = 8</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>旧句柄不再等于新句柄，读它得到 <code>None</code>。代价也在输出里：句柄从 4 字节变成 8 字节。我们没这么做，是因为 VM 保证所有句柄都在栈或帧里被当作根，不会悬空；但如果将来要把句柄交给用户代码（比如暴露成 Pebble 里的弱引用），代数计数就是正确的做法。</p><p><strong>练习 2</strong>：解释为什么 <code>mark</code> 永远不递归，并用实验数字支持你的回答。</p>
参考答案<p>递归标记每深一层就多一个栈帧，而被标记的数据结构的深度由 Pebble 程序决定，不由回收器决定。上面的 <code>ch11_deep_recursion</code> 实验里，递归标记器在 2 MiB 栈的线程上 20,000 层就崩了（<code>has overflowed its stack</code>，进程被 abort），在 8 MiB 的主线程上 50,000 层崩；而工作表 <code>Vec&lt;GcRef&gt;</code> 住在堆上，上限是内存。附带的还有一个借用检查器的理由：递归版本要在闭包里调用 <code>self.mark</code>，和 <code>self.get(r)</code> 的借用冲突（上面的 E0500）。要注意的是，这个 crate 里的 <code>deeply_nested_lists_survive</code> 用的是 10,000 层，实验表明这个深度在 debug 构建里递归实现也能过，所以这条测试对”不递归”这件事的约束其实很弱，要加强它应该把循环里的 <code>10_000</code> 调到 <code>100_000</code> 以上，断言里的 <code>10_001</code> 同步改。</p><p><strong>练习 3</strong>：如果 <code>alloc_bytes</code> 里去掉那条 <code>assert!(end &lt;= self.buffer.len(), ..)</code>，UB 具体在哪里发生？</p>
参考答案<p><code>self.offset = end</code> 之后进入 <code>unsafe</code> 块：<code>base.add(start)</code> 可以指到缓冲区之外（<code>add</code> 的安全前提就是结果必须落在同一个分配里，所以仅仅计算出越界指针在严格意义上就已经违反了前提），然后 <code>ptr::copy_nonoverlapping</code> 往 <code>Vec</code> 的分配之外写 <code>bytes.len()</code> 个字节，这就是堆缓冲区溢出，和 C 里 <code>memcpy</code> 越界写一样，可能覆盖相邻分配的数据、破坏分配器的元数据，也可能碰巧”没事”。<code>from_raw_parts_mut</code> 紧接着又造出一个指向越界内存的 <code>&amp;mut [u8]</code>。这段 SAFETY 注释里的第一条论证就是”<code>start..end</code> 刚刚检查过在缓冲区内”，删掉 <code>assert!</code> 后注释就变成了谎话，这是 SAFETY 注释的价值：它让谎话有迹可查。我没有跑这个越界版本：UB 的行为不可预期，不适合拿来当”演示输出”。</p><hr /><p>到这里，Pebble 有了两套可以并存的内存管理：树遍历解释器靠 <code>Rc&lt;RefCell&lt;Env&gt;&gt;</code>，会泄漏那种环；<code>pebble-gc</code> 靠下标句柄和标记清除，环不是问题。但 <code>pebble-gc</code> 目前只是一个没人调用的库。下一篇我们写栈式虚拟机，让它的 <code>VmObject</code> 实现 <code>GcObject</code>，让栈、调用帧和全局表充当根，把这个回收器接到一台真的机器上，同时回答第 9 篇留下的那个问题：一个函数把自己存进自己的环境之后，到底会不会被回收。我已经看过那个测试量出来的数字（2 个对象），但先别急着知道它是怎么来的。下一篇是<a href="/posts/rust-by-pebble-12-vm-backends/">栈式虚拟机：GC 根、两个后端与 Criterion</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-10-bytecode-macros/</id>
      <title type="text">字节码编译器：macro_rules!、回填和常量池</title>
      <published>2026-10-13T09:00:00.000Z</published>
      <updated>2026-10-13T09:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-10-bytecode-macros/"/>
      <summary type="text">把 AST 压扁成一串线性指令：用函数式过程宏 opcode_set! 生成指令枚举，用 macro_rules! 写 chunk!，用 HashMap 的 entry API 给名字去重，用回填补全跳转目标，再亲手弄坏 for 循环的压栈顺序看测试漏掉了什么。</summary>
      <content type="html"><![CDATA[<p><code>1 + 2 * 3;</code> 在 AST 里是五个节点，三层嵌套。下面是我们这一篇写出来的编译器对它的看法（<code>1.pebble</code> 里只有这一行），真实输出：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode 1.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 1</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 2</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>2 3</span></div></div><div><div><div>6</div></div><div><span>== code ==</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>0 Const 0</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>1 Const 1</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>2 Const 2</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>3 Mul</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>4 Add</span></div></div><div><div><div>12</div></div><div><span>7</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>树不见了。剩下一张常量表，和五条按顺序排好的指令。最后那个 <code>7</code> 是第 9 篇的树遍历解释器跑出来的（<code>run</code> 在打印完指令清单之后照旧用它执行），这一篇的编译器本身还不会执行任何东西，那是第 12 篇虚拟机的活。我们这一篇只做一件事：把”一棵要递归下去的树”变成”一张从头读到尾的清单”，并且写一个能把这张清单打印出来的反汇编器，让你亲眼看到它。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇我们新建 <code>pebble-bytecode</code>（字节码编译器），在 <code>pebble-macros</code> 里加上第二个宏 <code>opcode_set!</code>，并把命令行升级到第三版：<code>pebble run --dump-bytecode</code>，<code>pebble check</code> 顺带编译。用到的 Rust 特性：函数式过程宏、<code>macro_rules!</code>（片段、重复、<code>$crate</code>、卫生性）、带载荷的 <code>Copy</code> 枚举、<code>HashMap</code> 的 <code>entry</code> API 与闭包的分字段捕获、<code>ref</code> 绑定、let 链（edition 2024）、<code>split_last</code> 加 <code>let else</code>、<code>Rc&lt;FunctionProto&gt;</code>，以及 doc test。结束时 <code>cargo test -p pebble-bytecode</code> 是 14 个测试加 1 个 doc test 全部通过，命令行是 3 个单元测试加 9 个集成测试，<code>crates/pebble-bytecode/</code> 下的两个文件与 tag 逐字节相同。上一篇是 <a href="/posts/rust-by-pebble-09-interpreter-closures/">树遍历解释器：闭包、控制流与一个内存泄漏</a>。</p></div></div>
<section><h2>为什么要把树压扁<a href="#为什么要把树压扁"><span>#</span></a></h2><p>第 9 篇的结尾我列了三个”一眼看出会贵”的地方：变量查找沿着环境链走、每次调用分配一个 <code>Env</code>、每次执行 <code>fn</code> 语句复制一遍函数体。字节码本身不会魔法般地解决前两个（这一篇的变量依旧按名字查，每次调用依旧要一个环境，第 12 篇会老老实实量出来），但它至少改变三件事：</p><ul>
<li>执行变成<strong>一个循环</strong>：取一条指令，执行，取下一条。树遍历解释器每走一层嵌套就多一层 Rust 的函数调用栈，所以第 9 篇才需要 <code>with_call_depth</code> 和 <code>thread::Builder::stack_size</code>；循环不吃调用栈。</li>
<li>函数体只编译一次。编译产物用 <code>Rc</code> 共享，运行 <code>fn</code> 语句不再复制 AST。</li>
<li>程序变成<strong>一张可以打印出来的表</strong>。调试一个编译器时，能看到它吐了什么，比什么都值钱（上面那段输出就是证据）。</li>
</ul><p>我选的是<strong>栈机</strong>：每条指令从一个操作数栈上取参数，把结果压回栈上。另一个流派是寄存器机（Lua 5 用的），指令里写明参数在哪个寄存器，指令更少、更快，编译器更难写。栈机的编译器几乎是 AST 的后序遍历，写起来直白。（我没有拿 Pebble 的两种设计做对比，这个取舍是照着教科书做的，不是量出来的。）</p><p>后序遍历是什么意思？先编译左边，再编译右边，最后输出运算符。<code>1 + 2 * 3</code> 变成 <code>1 2 3 * +</code>，这是逆波兰表达式，HP 的老计算器就是这么用的。我手工推演一遍栈的变化（第 12 篇的虚拟机会让机器替我做这件事）：</p>

<table><thead><tr><th>指令</th><th>栈（左边是栈底）</th></tr></thead><tbody><tr><td><code>Const 0</code></td><td><code>[1]</code></td></tr><tr><td><code>Const 1</code></td><td><code>[1, 2]</code></td></tr><tr><td><code>Const 2</code></td><td><code>[1, 2, 3]</code></td></tr><tr><td><code>Mul</code></td><td><code>[1, 6]</code></td></tr><tr><td><code>Add</code></td><td><code>[7]</code></td></tr></tbody></table><p>每条运算指令弹出自己的参数、压回结果。整个程序跑完，栈上留下的就是它的值。</p></section>
<section><h2>第一步：给指令写个名字表<a href="#第一步给指令写个名字表"><span>#</span></a></h2><p>先定义指令集。一条 Pebble 指令是一个枚举：大多数指令不带参数，少数带一个 <code>u32</code>（常量表的下标，或者一个跳转目标）。我先试着手写一个只有六条指令的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>Opcode</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Const</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Nil</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Pop</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Add</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Mul</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Return</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>Opcode</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>name</span><span>(</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>_</span><span>) =&gt; </span><span>"Const"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>Nil</span><span> =&gt; </span><span>"Nil"</span><span>,</span></div></div><div><div><div>16</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>Pop</span><span> =&gt; </span><span>"Pop"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>Add</span><span> =&gt; </span><span>"Add"</span><span>,</span></div></div><div><div><div>18</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>Mul</span><span> =&gt; </span><span>"Mul"</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>name()</code> 是给反汇编器用的：打印 <code>Mul</code> 而不是 <code>Opcode::Mul</code>（<code>Debug</code> 派生出来的格式会连载荷一起打印，<code>Const(0)</code> 而不是我们想要的 <code>Const 0</code>）。我故意漏了 <code>Return</code>，这是实验室里的 <code>ch10_name_table.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch10_name_table</span></div></div><div><div><div>2</div></div><div><span>error[E0004]: non-exhaustive patterns: `Opcode::Return` not covered</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch10_name_table.rs:13:15</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>13 |         match self {</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|               ^^^^ pattern `Opcode::Return` not covered</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>8 |     Return,</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|     ------ not covered</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div><div><div><div>12</div></div><div><span>For more information about this error, try `rustc --explain E0004`.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器抓住了遗漏，这是第 3 篇学过的穷尽匹配。所以问题<strong>不是</strong>安全，是重复：每条指令要写在枚举里、写在 <code>name()</code> 里，完整版有 32 条，还要加上 <code>derive</code> 列表和文档。每次加一条指令（本篇的练习就要你加一条）得改两个地方，而且这两个地方的内容 90% 是同一个标识符。</p><p>这是一个”从一份声明生成两份代码”的活，宏的本职工作。第 6 篇我们用 derive 宏给 AST 生成 <code>impl Spanned</code>，那是<strong>派生宏</strong>：它挂在一个类型上，生成的代码<strong>附加</strong>在类型后面。这一次我们要写一个<strong>函数式过程宏</strong>：调用起来像 <code>opcode_set! { ... }</code>，它吃掉大括号里的所有 token，输出<strong>替换</strong>掉整个调用。区别在于输出：派生宏的输出是”追加”，枚举本身还在；函数式宏什么都不保留，所以我们得自己把枚举重新输出一遍。</p><div><div><div></div><div>其实 macro_rules! 也行</div></div><div><p>这个任务用 <code>macro_rules!</code> 也写得出来。我没有真的写那个版本，所以只能说我的判断：要处理文档注释（它们是 <code>#[doc = "..."]</code> 属性）、可选的元组载荷，规则会很绕。这里我把它当作函数式过程宏的练习，下面 <code>chunk!</code> 才是 <code>macro_rules!</code> 的主场。</p></div></div><section><h3>写 opcode_set<a href="#写-opcode_set"><span>#</span></a></h3><p>在 <code>crates/pebble-macros/src/lib.rs</code> 里先把 <code>use syn::...</code> 那一行改成多引入一个 <code>ItemEnum</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>syn</span><span>::{</span></span><span>Data</span><span>, </span><span>DeriveInput</span><span>, </span><span>Fields</span><span>, </span><span>ItemEnum</span><span>, parse_macro_input};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>ItemEnum</code> 是 <code>syn</code> 对”一个 <code>enum</code> 定义”的解析结果，第 6 篇的 <code>DeriveInput</code> 的近亲。然后在文件末尾加入入口函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[proc_macro]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>opcode_set</span><span>(</span><span>input</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>item</span><span><span> </span><span>=</span><span> </span></span><span>parse_macro_input!</span><span>(</span><span>input</span><span> </span><span>as</span><span> </span><span>ItemEnum</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>match</span><span> </span><span>expand_opcodes</span><span>(&amp;</span><span>item</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Ok</span><span>(</span><span>tokens</span><span>) =&gt; </span><span>tokens</span><span>.</span><span>into</span><span>(),</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; </span><span>error</span><span>.</span><span>to_compile_error</span><span>().</span><span>into</span><span>(),</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>和第 6 篇的 <code>derive_spanned</code> 比，差别只有属性：<code>#[proc_macro]</code> 而不是 <code>#[proc_macro_derive(Spanned)]</code>。函数名就是宏名，调用时写 <code>opcode_set!</code>。<code>parse_macro_input!(input as ItemEnum)</code> 把 token 流解析成 <code>ItemEnum</code>，解析失败时它自己 <code>return</code> 一个编译错误，所以后面不用处理那种情况。真实的失败长这样：我写了个 <code>pub struct Tiny { pc: u32 }</code> 塞给它（实验 <code>ch10_opcode_set_struct.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: expected `enum`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch10_opcode_set_struct.rs:4:9</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>4 |     pub struct Tiny {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|         ^^^^^^</span></div></div><div><div><div>6</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>错误指向用户写的那个 <code>struct</code>，没有任何宏内部的东西漏出来。</p><p>现在是 <code>expand_opcodes</code>，分三小段。第一段取出 <code>ItemEnum</code> 的各个部分：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>expand_opcodes</span><span>(</span><span>item</span><span>: &amp;</span><span>ItemEnum</span><span><span>) -&gt; </span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>TokenStream2</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>attrs</span><span><span> </span><span>=</span><span> &amp;</span></span><span>item</span><span>.attrs;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>vis</span><span><span> </span><span>=</span><span> &amp;</span></span><span>item</span><span>.vis;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>item</span><span>.ident;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>variants</span><span><span> </span><span>=</span><span> &amp;</span></span><span>item</span><span>.variants;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>attrs</code> 是枚举上的属性。注意文档注释 <code>/// One Pebble instruction.</code> 在编译器眼里就是属性 <code>#[doc = " One Pebble instruction."]</code>，它会出现在这里。<code>vis</code> 是可见性（<code>pub</code>），<code>ident</code> 是名字，<code>variants</code> 是变体列表（一个带逗号的 <code>Punctuated</code> 序列）。</p><p>第二段为每个变体生成 <code>name()</code> 里的一个 <code>match</code> 分支：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>let</span><span> </span><span>arms</span><span><span> </span><span>=</span><span> </span></span><span>variants</span><span>.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>variant</span><span><span>|</span><span> {</span></span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>variant_name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>variant</span><span>.ident;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>if</span><span> </span><span>variant</span><span>.fields.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>quote!</span><span><span>(#</span><span>name</span><span>::#</span></span><span>variant_name</span><span> =&gt; </span><span>stringify!</span><span>(#</span><span>variant_name</span><span>))</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>quote!</span><span><span>(#</span><span>name</span><span>::#</span></span><span>variant_name</span><span>(..) =&gt; </span><span>stringify!</span><span>(#</span><span>variant_name</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>无载荷的变体生成 <code>Opcode::Nil =&gt; "Nil"</code>，带载荷的生成 <code>Opcode::Const(..) =&gt; "Const"</code>（<code>(..)</code> 是”不管里面有什么”的模式）。<code>stringify!</code> 是标准库的宏：把传给它的 token 原样变成字符串字面量，<code>stringify!(Nil)</code> 就是 <code>"Nil"</code>。这是整个宏里唯一需要”把标识符变成字符串”的地方，也是手写 <code>name()</code> 时我最容易拼错的地方。<code>arms</code> 是一个惰性的迭代器，这里还没有产生任何东西，下一段会把它交给 <code>quote!</code>。</p><p>第三段拼出最终输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>Ok</span><span>(</span><span>quote!</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Hash</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>#(#</span><span>attrs</span><span>)*</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>#</span><span>vis</span><span> </span><span>enum</span><span> #</span><span>name</span><span> {</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>#</span><span>variants</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>        </span><span>impl</span><span> #</span><span>name</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>/// The opcode's name, for disassembly and tracing.</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>#[must_use]</span></div></div><div><div><div>11</div></div><div><span>            </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>name</span><span>(</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>13</div></div><div><span><span>                    </span></span><span>#(#</span><span>arms</span><span>),*</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>#(#attrs)*</code> 把每个属性原样重放一遍，用户的文档注释因此保留。<code>#variants</code> 直接把整个 <code>Punctuated</code> 列表（连逗号一起）插进去。<code>#(#arms),*</code> 是 <code>quote!</code> 的重复语法：对 <code>arms</code> 迭代，每个元素之间用逗号分隔。整个宏做的事比 derive 还少：没有泛型要处理，没有字段要查。</p><p>宏自己加了一份 <code>#[derive(...)]</code>。<code>Clone, Copy</code> 因为指令要在 <code>match self.code[at]</code> 里直接复制出来（下面会遇到），<code>PartialEq, Debug</code> 因为测试要 <code>assert_eq!</code> 它们。<code>const fn</code> 是第 1 篇的老朋友：<code>name()</code> 能在编译期求值。</p><p>整个宏文件末尾现在多了 <code>opcode_set</code> 和 <code>expand_opcodes</code> 两个函数。顶部的模块文档也要更新，把”目前一种宏”改成两种：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! compile time, in the compiler's process, and speak in tokens. So far two</span></div></div><div><div><div>2</div></div><div><span>//! flavours live here:</span></div></div><div><div><div>3</div></div><div><span>//!</span></div></div><div><div><div>4</div></div><div><span>//! * [`macro@Spanned`] - a **derive** macro that writes the boring</span></div></div><div><div><div>5</div></div><div><span>//!   `impl pebble_span::Spanned` for AST nodes.</span></div></div><div><div><div>6</div></div><div><span>//! * [`opcode_set`] - a **function-like** macro that turns a declarative list of</span></div></div><div><div><div>7</div></div><div><span>//!   opcodes into an enum plus a `name()` table.</span></div></div><div><div><div>8</div></div><div><span>//!</span></div></div><div><div><div>9</div></div><div><span>//! (A third, **declarative** kind, `macro_rules!`, lives next to the code</span></div></div><div><div><div>10</div></div><div><span>//! that uses it in `pebble-bytecode`.)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用实验室验证它：新建一个依赖 <code>pebble-macros</code> 的小 crate（路径依赖），写 <code>src/bin/ch10_opcode_set.rs</code>。完整版在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch10-opcode" target="_blank"><code>lab/lab-ch10-opcode</code></a>，在那个目录里运行即可：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_macros</span><span>::opcode_set;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>opcode_set!</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>enum</span><span> </span><span>Tiny</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Const</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Nil</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>Jump</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> </span><span>op</span><span> </span><span>in</span><span> [</span><span>Tiny</span><span>::</span><span>Const</span><span>(</span><span>7</span><span>), </span><span>Tiny</span><span>::</span><span>Nil</span><span>, </span><span>Tiny</span><span>::</span><span>Jump</span><span>(</span><span>3</span><span>)] {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{:?} is called {}"</span><span>, </span><span>op</span><span>, </span><span>op</span><span>.</span><span>name</span><span>());</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch10_opcode_set</span></div></div><div><div><div>2</div></div><div><span>Const(7) is called Const</span></div></div><div><div><div>3</div></div><div><span>Nil is called Nil</span></div></div><div><div><div>4</div></div><div><span>Jump(3) is called Jump</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>{:?}</code> 来自宏加的 <code>Debug</code> 派生，<code>name()</code> 来自宏生成的 <code>impl</code>。</p></section></section>
<section><h2>第二步：pebble-bytecode，最小的一版<a href="#第二步pebble-bytecode最小的一版"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-bytecode</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先在根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 里，<code>pebble-interp</code> 那一行后面加上它自己：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-bytecode</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-bytecode"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>crates/pebble-bytecode/Cargo.toml</code> 的完整内容如下，和 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bytecode/Cargo.toml" target="_blank">tag 里的文件</a> 一致：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-bytecode"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Bytecode chunks, opcodes, and the Pebble compiler front-to-back end."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div><span>pebble-ast.workspace</span><span> = </span><span>true</span></div></div><div><div><div>15</div></div><div><span>pebble-macros.workspace</span><span> = </span><span>true</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>18</div></div><div><span>pebble-parser.workspace</span><span> = </span><span>true</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>21</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四个正常依赖：<code>span</code>（每条指令记下它来自源码的哪一段）、<code>diag</code>（编译期的错误，比如 <code>break</code> 在循环外面）、<code>ast</code>（输入）、<code>macros</code>（刚写的 <code>opcode_set!</code>）。<code>pebble-parser</code> 和上一篇的 <code>pebble-interp</code> 一样，只在测试里把字符串变成 AST，放在 <code>[dev-dependencies]</code>。</p><p>下面是 <code>crates/pebble-bytecode/src/lib.rs</code> 的第一版。我要的最小东西：六条指令，一种常量，一个 <code>Chunk</code>，一个只会编译整数加乘的 <code>Compiler</code>。先是文件头和指令集：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div><span>//! The Pebble bytecode compiler: AST in, an instruction stream out.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::{</span></span><span>BinaryOp</span><span>, </span><span>Expr</span><span>, </span><span>Program</span><span>, </span><span>Spanned</span><span>, </span><span>Stmt</span><span>};</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>pebble_macros</span><span>::opcode_set;</span></span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>opcode_set!</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// One Pebble instruction.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>enum</span><span> </span><span>Opcode</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>/// Push `constants[operand]`.</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Const</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>13</div></div><div><span>        </span><span>/// Push `nil`.</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Nil</span><span>,</span></div></div><div><div><div>15</div></div><div><span>        </span><span>/// Discard the top of the stack.</span></div></div><div><div><div>16</div></div><div><span>        </span><span>Pop</span><span>,</span></div></div><div><div><div>17</div></div><div><span>        </span><span>/// `a + b`</span></div></div><div><div><div>18</div></div><div><span>        </span><span>Add</span><span>,</span></div></div><div><div><div>19</div></div><div><span>        </span><span>/// `a * b`</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Mul</span><span>,</span></div></div><div><div><div>21</div></div><div><span>        </span><span>/// Return the top of the stack.</span></div></div><div><div><div>22</div></div><div><span>        </span><span>Return</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>#![forbid(unsafe_code)]</code> 是第 2 篇的老规矩。这个 crate 里没有一行 <code>unsafe</code>，以后也不会有。</p><p>那个宏调用展开成什么样？稳定版编译器不接受 <code>-Zunpretty</code>，第 6 篇说过 <code>RUSTC_BOOTSTRAP=1</code> 可以让它接受，<strong>只用来观察</strong>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>RUSTC_BOOTSTRAP</span><span>=</span><span>1</span><span> </span><span>cargo</span><span> </span><span>rustc</span><span> </span><span>-p</span><span> </span><span>pebble-bytecode</span><span> </span><span>--lib</span><span> </span><span>--</span><span> </span><span>-Zunpretty=expanded</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[doc = " One Pebble instruction."]</span></div></div><div><div><div>2</div></div><div><span>pub enum Opcode {</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>#[doc = " Push `constants[operand]`."]</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>Const(u32),</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>#[doc = " Push `nil`."]</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>Nil,</span></div></div><div><div><div>9</div></div><div><span>...</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div><div><div><div>12</div></div><div><span>impl Opcode {</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>#[doc = r" The opcode's name, for disassembly and tracing."]</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>pub const fn name(self) -&gt; &amp;'static str {</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>match self {</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>Opcode::Const(..) =&gt; "Const",</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>Opcode::Nil =&gt; "Nil",</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>Opcode::Pop =&gt; "Pop",</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>Opcode::Add =&gt; "Add",</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>Opcode::Mul =&gt; "Mul",</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>Opcode::Return =&gt; "Return",</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（中间省略的是 <code>Clone</code>、<code>Copy</code>、<code>PartialEq</code>、<code>Eq</code>、<code>Hash</code>、<code>Debug</code> 这些派生出来的 <code>impl</code>，加起来六十来行，一行都不用我们自己写。）<code>name()</code> 和上面手写的版本一模一样，这次一个都不会漏。</p><section><h3>带载荷的 Copy 枚举有多大<a href="#带载荷的-copy-枚举有多大"><span>#</span></a></h3><p><code>Opcode</code> 是 <code>Copy</code> 的，里面还装着一个 <code>u32</code>。它占多少字节？实验室里 <code>ch10_sizes.rs</code>，同样形状的小枚举，对照一个 C 程序员会写的结构体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch10_sizes</span></div></div><div><div><div>2</div></div><div><span>enum Op        = 8 bytes</span></div></div><div><div><div>3</div></div><div><span>Option&lt;Op&gt;     = 8 bytes</span></div></div><div><div><div>4</div></div><div><span>repr(C) struct = 8 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>8 字节：一个字节的判别式（“现在是哪一条指令”），三个字节的填充，四个字节的 <code>u32</code>。C 里写 <code>struct { uint8_t op; uint32_t arg; }</code> 得到的也是 8。<code>std::variant</code> 做的是同一件事，只是 Rust 的枚举是语言内置的，<code>match</code> 能检查穷尽性。（<code>Option&lt;Op&gt;</code> 同样是 8 字节：判别式有没用到的取值，<code>None</code> 借用其中一个，这叫 niche 优化。）</p><p>一个真正的字节码格式会把操作数紧紧压在指令后面：1 字节操作码加 4 字节操作数 = 5 字节。Pebble 没这么做：用枚举换来了 <code>match</code> 的便利和 100% 安全的代码，代价是每条指令 8 字节。本篇稍后的 <code>operand_bytes()</code> 会把”压缩后应该是多少”算出来，让这个代价看得见。</p></section><section><h3>常量、Chunk 和编译器<a href="#常量chunk-和编译器"><span>#</span></a></h3><p>接下来是常量、<code>Chunk</code> 和一个编译器。常量先只支持整数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A compile-time constant.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Constant</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Chunk</code> 是编译的产物：一串指令，一张常量表，和一个与指令一一对应的 <code>Vec&lt;Span&gt;</code>（出错时能指回源码）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A linear sequence of instructions plus its constant pool.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Default</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Chunk</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>code</span><span>: </span><span>Vec</span><span>&lt;</span><span>Opcode</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>constants</span><span>: </span><span>Vec</span><span>&lt;</span><span>Constant</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>spans</span><span>: </span><span>Vec</span><span>&lt;</span><span>Span</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>impl</span><span> </span><span>Chunk</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>/// Appends an instruction and returns its index.</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>emit</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>op</span><span>: </span><span>Opcode</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span>.code.</span><span>push</span><span>(</span><span>op</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span>.spans.</span><span>push</span><span>(</span><span>span</span><span>);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span>.code.</span><span>len</span><span>() - </span><span>1</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>/// Adds a constant and returns its index.</span></div></div><div><div><div>18</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>add_constant</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>constant</span><span>: </span><span>Constant</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span>.constants.</span><span>push</span><span>(</span><span>constant</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>(</span><span>self</span><span>.constants.</span><span>len</span><span>() - </span><span>1</span><span>) </span><span>as</span><span> </span><span>u32</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>emit</code> 返回新指令的下标，下面的”回填”一节要靠这个返回值。字段是私有的，外面只能通过方法看：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The instructions.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>code</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;[</span><span>Opcode</span><span>] {</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.code</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>/// The constant pool.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>constants</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;[</span><span>Constant</span><span>] {</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.constants</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>返回 <code>&amp;[Opcode]</code> 而不是 <code>&amp;Vec&lt;Opcode&gt;</code>：调用方只需要读，不需要知道背后是不是 <code>Vec</code>（第 2 篇的切片）。</p><p>编译器的入口：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Compiles a parsed program into one chunk.</span></div></div><div><div><div>2</div></div><div><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>compile</span><span>(</span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>Chunk</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>compiler</span><span><span> </span><span>=</span><span> </span></span><span>Compiler</span><span>::</span><span>default</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>compiler</span><span>.</span><span>compile_body</span><span>(&amp;</span><span>program</span><span>.stmts, </span><span>false</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>compiler</span><span>.chunk</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#[derive(</span><span>Default</span><span>)]</span></div></div><div><div><div>10</div></div><div><span>struct</span><span> </span><span>Compiler</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>chunk</span><span>: </span><span>Chunk</span><span>,</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Compiler</code> 现在只有一个字段，<code>#[derive(Default)]</code> 给它一个”全空”的构造。后面它会长出更多字段，到时候我们会把 <code>Default</code> 换成手写的 <code>new()</code>。</p><p><code>compile_body</code> 编译一串语句。这里有两个约定要记住：程序<strong>最后一条表达式语句的值就是整个程序的值</strong>，所以它的结果不弹出，留在栈上；函数体里这个值还要用 <code>Return</code> 返回出去。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Compiler</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>emit</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>op</span><span>: </span><span>Opcode</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.chunk.</span><span>emit</span><span>(</span><span>op</span><span>, </span><span>span</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>compile_body</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>stmts</span><span>: &amp;[</span><span>Stmt</span><span>], </span><span>in_function</span><span>: </span><span>bool</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>stmts</span><span>.</span><span>first</span><span>().</span><span>map_or</span><span><span>(</span><span>Span</span><span>::</span></span><span>DUMMY</span><span><span>, </span><span>Spanned</span><span>::</span></span><span>span</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>((</span><span>last</span><span>, </span><span>init</span><span><span>)) </span><span>=</span><span> </span></span><span>stmts</span><span>.</span><span>split_last</span><span>() </span><span>else</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Nil</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>10</div></div><div><span>            </span><span>if</span><span> </span><span>in_function</span><span> {</span></div></div><div><div><div>11</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Return</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>            </span><span>return</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>split_last</code> 把切片拆成”最后一个”加”前面所有”，返回 <code>Option&lt;(&amp;T, &amp;[T])&gt;</code>：空切片返回 <code>None</code>。<code>let ... else</code>（第 3 篇）处理空程序：压一个 <code>nil</code> 当作它的值。<code>map_or(Span::DUMMY, Spanned::span)</code> 里的 <code>Spanned::span</code> 是把 trait 方法当函数传：<code>first()</code> 给出 <code>Option&lt;&amp;Stmt&gt;</code>，<code>map_or</code> 对里面的值调用 <code>Spanned::span</code>，没有语句时用 <code>Span::DUMMY</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>init</span><span> {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>self</span><span>.</span><span>compile_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Stmt</span><span>::</span><span>Expr</span><span> { </span><span>expr</span><span><span>, .. } </span><span>=</span><span> </span></span><span>last</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>expr</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>self</span><span>.</span><span>compile_stmt</span><span>(</span><span>last</span><span>);</span></div></div><div><div><div>8</div></div><div><span>            </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Nil</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> </span><span>in_function</span><span> {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Return</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前面的语句各自编译。最后一条：如果是表达式语句，直接编译表达式，值留在栈上；否则（比如 <code>let</code>）正常编译，然后压一个 <code>nil</code> 当作值。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>compile_stmt</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>stmt</span><span>: &amp;</span><span>Stmt</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>stmt</span><span>.</span><span>span</span><span>();</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>stmt</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Expr</span><span> { </span><span>expr</span><span>, .. } =&gt; {</span></div></div><div><div><div>5</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>expr</span><span>);</span></div></div><div><div><div>6</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Pop</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>todo!</span><span>(</span><span>"statement"</span><span>),</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>表达式语句的值没人要，所以后面跟一条 <code>Pop</code>。其余语句暂时 <code>todo!()</code>：和第 7 篇写解析器时一样，先让一条路径跑通，别的以后补。最后是表达式：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>compile_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>expr</span><span>.</span><span>span</span><span>();</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>expr</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Int</span><span> { </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>5</div></div><div><span>                </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>add_constant</span><span><span>(</span><span>Constant</span><span>::</span></span><span>Int</span><span>(*</span><span>value</span><span>));</span></div></div><div><div><div>6</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Const</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Binary</span><span> { </span><span>op</span><span>, </span><span>lhs</span><span>, </span><span>rhs</span><span>, .. } =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>lhs</span><span>);</span></div></div><div><div><div>10</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>rhs</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span>(</span></div></div><div><div><div>12</div></div><div><span>                    </span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>13</div></div><div><span><span>                        </span></span><span>BinaryOp</span><span>::</span><span>Add</span><span><span> =&gt; </span><span>Opcode</span><span>::</span></span><span>Add</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>                        </span></span><span>BinaryOp</span><span>::</span><span>Mul</span><span><span> =&gt; </span><span>Opcode</span><span>::</span></span><span>Mul</span><span>,</span></div></div><div><div><div>15</div></div><div><span>                        </span><span>_</span><span> =&gt; </span><span>todo!</span><span>(</span><span>"operator"</span><span>),</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>},</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>span</span><span>,</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>todo!</span><span>(</span><span>"expression"</span><span>),</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>整个编译器的精髓就在 <code>Binary</code> 这个分支里：<strong>先编译左边，再编译右边，最后输出运算符。</strong> 左右两边各自会产生”把自己的值留在栈顶”的指令序列，运算符指令从栈顶取两个值。这个递归的后序遍历，就是我们写了五篇文章的树变成清单的全过程。</p><p>测试放在文件末尾。它是整个 crate 里最严格的一种：不是在文本清单里找子串，而是直接比较指令序列：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_parser</span><span>::parse;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>compile_src</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Chunk</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>assert!</span><span>(</span><span>result</span><span>.</span><span>is_ok</span><span>(), </span><span>"parse errors: {:?}"</span><span>, </span><span>result</span><span>.diagnostics);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>compile</span><span>(&amp;</span><span>result</span><span>.program)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>compiles_arithmetic</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>chunk</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"1 + 2 * 3;"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>17</div></div><div><span>            </span><span>chunk</span><span>.</span><span>code</span><span>(),</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>[</span></div></div><div><div><div>19</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>0</span><span>),</span></div></div><div><div><div>20</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>1</span><span>),</span></div></div><div><div><div>21</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>2</span><span>),</span></div></div><div><div><div>22</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Mul</span><span>,</span></div></div><div><div><div>23</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Add</span><span>,</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>]</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>chunk.code()</code> 是 <code>&amp;[Opcode]</code>，右边是 <code>[Opcode; 5]</code>，标准库给”切片引用和数组”实现了 <code>PartialEq</code>，所以直接 <code>assert_eq!</code>。这一行能写，是因为 <code>opcode_set!</code> 加了 <code>PartialEq</code> 和 <code>Debug</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bytecode</span></div></div><div><div><div>2</div></div><div><span>running 1 test</span></div></div><div><div><div>3</div></div><div><span>test tests::compiles_arithmetic ... ok</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一版就跑通了：五条指令正是开头那一段输出里的那五条。</p></section></section>
<section><h2>第三步：包装成 Compiled，让命令行能看见它<a href="#第三步包装成-compiled让命令行能看见它"><span>#</span></a></h2><p><code>compile</code> 现在返回一个裸 <code>Chunk</code>。但真正的程序有函数：每个函数体是它自己的 <code>Chunk</code>，所以顶层的程序也该是”一个叫 <code>&lt;main&gt;</code> 的函数”。另外编译期可能出错（<code>break</code> 写在循环外面），不能 <code>panic</code>，要和解析器一样返回诊断。我们把返回类型改成这样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A compiled function body.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>FunctionProto</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// `Some` for named declarations, `None` for anonymous closures.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>name</span><span>: </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// Parameter names, in order.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>params</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// The function's instruction stream.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>pub</span><span> </span><span>chunk</span><span>: </span><span>Chunk</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>FunctionProto</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>/// A display name for diagnostics.</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>display_name</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span>.name.</span><span>as_deref</span><span>().</span><span>unwrap_or</span><span>(</span><span>"&lt;anonymous&gt;"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>proto</code> 是 prototype 的缩写：函数的”原型”，编译产物里不变的那一部分（名字、参数名、指令）。第 12 篇的运行期闭包会是”一个 <code>proto</code> 加上它捕获的环境”，和第 9 篇树遍历解释器里的闭包一模一样。<code>name.as_deref()</code> 把 <code>Option&lt;String&gt;</code> 借成 <code>Option&lt;&amp;str&gt;</code>，<code>unwrap_or</code> 给匿名函数一个默认名字。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The result of compiling a program.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Compiled</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// The top-level function. Its `chunk` is the program.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>main</span><span>: </span><span>Rc</span><span>&lt;</span><span>FunctionProto</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>Compiled</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// The top-level instruction stream.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>chunk</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>Chunk</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.main.chunk</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为什么是 <code>Rc&lt;FunctionProto&gt;</code>？第 8 篇学过 <code>Rc</code>：多个所有者共享同一份数据，引用计数。以后 VM 里每个由这个函数创建的闭包都要指着同一份原型，复制整个 <code>Chunk</code> 既慢又没意义。需要 <code>use std::rc::Rc;</code>，下面的 <code>CompileResult</code> 还要 <code>use pebble_diag::Diagnostic;</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Compilation output: the program plus any diagnostics.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>CompileResult</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>compiled</span><span>: </span><span>Compiled</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>diagnostics</span><span>: </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>CompileResult</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Whether compilation produced no diagnostics.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>is_ok</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span>.diagnostics.</span><span>is_empty</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>和第 7 篇解析器的 <code>ParseResult</code> 同一个设计：不论有没有错，都交出一个结果，加上诊断列表。<code>compile</code> 本身变成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[must_use]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>compile</span><span>(</span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>CompileResult</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>compiler</span><span><span> </span><span>=</span><span> </span></span><span>Compiler</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>compiler</span><span>.</span><span>compile_body</span><span>(&amp;</span><span>program</span><span>.stmts, </span><span>false</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>CompileResult</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>compiled</span><span>: </span><span>Compiled</span><span> {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>main</span><span>: </span><span>Rc</span><span>::</span><span>new</span><span>(</span><span>FunctionProto</span><span> {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>name</span><span>: </span><span>Some</span><span>(</span><span>"&lt;main&gt;"</span><span>.</span><span>to_string</span><span>()),</span></div></div><div><div><div>9</div></div><div><span>                </span><span>params</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>                </span><span>chunk</span><span>: </span><span>compiler</span><span>.chunk,</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}),</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>13</div></div><div><span>        </span><span>diagnostics</span><span>: </span><span>compiler</span><span>.diagnostics,</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Compiler</code> 多一个字段，<code>Default</code> 派生换成手写的 <code>new()</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Compiler</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>chunk</span><span>: </span><span>Chunk</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>diagnostics</span><span>: </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>impl</span><span> </span><span>Compiler</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>chunk</span><span>: </span><span>Chunk</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>diagnostics</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Chunk</code> 也要有一个 <code>new()</code>，它只是 <code>Self::default()</code> 的包装：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// An empty chunk.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Self</span><span>::</span><span>default</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>#[must_use]</code> 的理由和第 1 篇一样：忘记接收返回值一定是个 bug。再补上 <code>Chunk</code> 的另外几个读取器，它们是整个 crate 对外的只读接口：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The source span associated with instruction `index`.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>span_at</span><span>(&amp;</span><span>self</span><span>, </span><span>index</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.spans.</span><span>get</span><span>(</span><span>index</span><span>).</span><span>copied</span><span>().</span><span>unwrap_or</span><span><span>(</span><span>Span</span><span>::</span></span><span>DUMMY</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>/// Number of instructions.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>len</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span>.code.</span><span>len</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>/// Whether the chunk is empty.</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>is_empty</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.code.</span><span>is_empty</span><span>()</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>get(index)</code> 返回 <code>Option&lt;&amp;Span&gt;</code>，<code>copied()</code> 把里面的引用变成值（<code>Span</code> 是 <code>Copy</code>，第 1 篇），<code>unwrap_or</code> 给越界的情况一个 <code>DUMMY</code>。<code>is_empty</code> 是 clippy 的要求：有 <code>len</code> 就得有 <code>is_empty</code>。</p><section><h3>反汇编器<a href="#反汇编器"><span>#</span></a></h3><p>现在写让一切可见的函数。它放在 <code>impl Chunk</code> 里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// A human-readable listing, `rustc`-style.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>disassemble</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>out</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>out</span><span>.</span><span>push_str</span><span>(</span><span>"== constants ==</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> (</span><span>index</span><span>, </span><span>constant</span><span>) </span><span>in</span><span> </span><span>self</span><span>.constants.</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span><span>"  {index:&gt;4} {constant:?}</span><span>\n</span><span>"</span><span>));</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>        </span><span>out</span><span>.</span><span>push_str</span><span>(</span><span>"== code ==</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>for</span><span> (</span><span>index</span><span>, </span><span>op</span><span>) </span><span>in</span><span> </span><span>self</span><span>.code.</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>let</span><span> </span><span>operand</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>index</span><span>) =&gt; </span><span>format!</span><span>(</span><span>" {index}"</span><span>),</span></div></div><div><div><div>13</div></div><div><span>                </span><span>_</span><span> =&gt; </span><span>String</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>15</div></div><div><span>            </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span><span>"  {index:&gt;4} {}{operand}</span><span>\n</span><span>"</span><span>, </span><span>op</span><span>.</span><span>name</span><span>()));</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>        </span><span>out</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>{index:&gt;4}</code> 是第 3 篇的格式迷你语言：右对齐、宽度 4。<code>op.name()</code> 来自宏，<code>operand</code> 是带载荷指令后面的数字。这个函数后面会长大（跳转、嵌套函数），但骨架就是这样。</p></section><section><h3>测试跟着改<a href="#测试跟着改"><span>#</span></a></h3><p><code>compile</code> 返回的类型变了，第一版的测试要重写。辅助函数现在返回 <code>Compiled</code>，并且要求诊断列表为空：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>compile_src</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Compiled</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>3</div></div><div><span>        </span><span>assert!</span><span>(</span><span>result</span><span>.</span><span>is_ok</span><span>(), </span><span>"parse errors: {:?}"</span><span>, </span><span>result</span><span>.diagnostics);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile</span><span>(&amp;</span><span>result</span><span>.program);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>6</div></div><div><span>            </span><span>compiled</span><span>.</span><span>is_ok</span><span>(),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>"compile errors: {:?}"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>            </span><span>compiled</span><span>.diagnostics</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>compiled</span><span>.compiled</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试本身我换了一种写法：不再比较整个指令序列，而是在反汇编出来的清单里找子串。三个基础测试：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>compiles_arithmetic</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"1 + 2 * 3;"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>listing</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"Const"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"Mul"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"Add"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>fn</span><span> </span><span>non_trailing_expression_statements_are_popped</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"1 + 2; 3;"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>listing</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>();</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"Pop"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>trailing_expression_is_the_program_value</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"1; 2;"</span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>// The last expression is not popped, so the VM can return it.</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>code</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span><span>.</span><span>chunk</span><span>().</span><span>code</span><span>();</span></div></div><div><div><div>22</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>code</span><span>.</span><span>last</span><span>(), </span><span>Some</span><span>(&amp;</span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>1</span><span>)));</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第三个守着前面讲的那个约定：最后一条表达式的值不弹出，所以最后一条指令是 <code>Const 1</code>，不是 <code>Pop</code>。子串测试对”不相干的指令变了”很耐受（我加一条新指令，不会让 <code>compiles_arithmetic</code> 红），代价是它们看不见<strong>顺序</strong>。这个代价会在 for 循环那一节兑现：我会故意弄坏一行，看这些测试漏掉了什么。</p></section><section><h3>命令行第三版，第一半：<code>--dump-bytecode</code><a href="#命令行第三版第一半--dump-bytecode"><span>#</span></a></h3><p>让命令行能调用它。三处小改动。<code>crates/pebble-cli/Cargo.toml</code> 的依赖里，<code>pebble-interp</code> 后面加一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-bytecode.workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>crates/pebble-cli/src/cli.rs</code> 的 <code>RunArgs</code>，在 <code>ast</code> 字段后面加一个开关：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Print the compiled bytecode disassembly before running.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[arg(long)]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>dump_bytecode</span><span>: </span><span>bool</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>clap 的魔法和第 7 篇一样：字段名 <code>dump_bytecode</code> 变成 <code>--dump-bytecode</code>，文档注释变成帮助文字。<code>crates/pebble-cli/src/commands.rs</code> 的 <code>run</code>，在 <code>args.ast</code> 那个 <code>if</code> 后面加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>if</span><span> </span><span>args</span><span>.dump_bytecode {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>3</div></div><div><span>        </span><span>if</span><span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>compiled</span><span>.diagnostics, &amp;</span><span>source</span><span>.map) {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>        </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>compiled</span><span>.compiled.</span><span>chunk</span><span>().</span><span>disassemble</span><span>());</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>别忘了文件头的 <code>use pebble_bytecode::compile;</code>。这是命令行的第一次编译：解析之后、解释之前，插一段”编译并打印清单”。编译出错就停下，和解析出错的处理方式一样。解释器仍然是 <code>Interpreter</code>，这一版命令行还没有第二个后端。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode 1.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 Int(1)</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 Int(2)</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>2 Int(3)</span></div></div><div><div><div>6</div></div><div><span>== code ==</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>0 Const 0</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>1 Const 1</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>2 Const 2</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>3 Mul</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>4 Add</span></div></div><div><div><div>12</div></div><div><span>7</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>能用。但 <code>Int(1)</code> 太吵了：我想看到的是常量的值，不是它的类型标签。这个等一下改，先试一下我们还不支持的东西：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode examples/fib.pebble</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>thread 'main' (123062) panicked at crates/pebble-bytecode/src/lib.rs:225:18:</span></div></div><div><div><div>4</div></div><div><span>not yet implemented: statement</span></div></div><div><div><div>5</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这就是第 7 篇那个”先写 <code>todo!()</code>，让它自己告诉你下一步写什么”。<code>fib.pebble</code> 的第一条语句是 <code>fn</code>，我们的编译器对它说”没实现”。接下来一节一节把洞补上。每补一节，测试都会通过，我们也可以用 <code>--dump-bytecode</code> 随时看结果。</p></section></section>
<section><h2>第四步：常量池<a href="#第四步常量池"><span>#</span></a></h2><p><code>Const 0</code> 里的 <code>0</code> 是什么？是<strong>常量池</strong>的下标。字面量 <code>3</code> 不直接写在指令里，而是放进 <code>Chunk</code> 的一张表，指令里只写它的位置。C 程序员见过这个设计：ELF 文件的 <code>.rodata</code> 段和字符串表就是同一个想法，代码里只放地址，数据放在另一个地方。好处有两个：每条指令保持固定大小（字符串字面量可以任意长），同一个字面量在整张表里只存一份。</p><p>我们先把 <code>Constant</code> 补全。它要能放 <code>nil</code>、布尔、整数、浮点、字符串，以及一个”嵌套的函数体”（下面写函数的时候用）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A compile-time constant.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Constant</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Nil</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Bool</span><span>(</span><span>bool</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Float</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Str</span><span>(</span><span>String</span><span>),</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// A nested function or closure body.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>Function</span><span>(</span><span>Rc</span><span>&lt;</span><span>FunctionProto</span><span>&gt;),</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意<strong>这次没有 <code>#[derive(Debug)]</code></strong>，我要手写。理由刚才你看到了：派生版本打印 <code>Int(1)</code>，我想要 <code>1</code>；字符串应该带引号打印，这样整数 <code>1</code> 和字符串 <code>"1"</code> 在清单里是两个不同的东西；函数常量不该把整个 <code>Chunk</code> 打印出来，一个 <code>&lt;function fib&gt;</code> 就够了。手写 <code>Debug</code> 是第 3 篇学过的 <code>fmt::Formatter</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>Constant</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Nil</span><span> =&gt; </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"nil"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Bool</span><span>(</span><span>value</span><span>) =&gt; </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{value}"</span><span>),</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Int</span><span>(</span><span>value</span><span>) =&gt; </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{value}"</span><span>),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Float</span><span>(</span><span>value</span><span>) =&gt; </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{value}"</span><span>),</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Str</span><span>(</span><span>value</span><span>) =&gt; </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{value:?}"</span><span>),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Constant</span><span>::</span><span>Function</span><span>(</span><span>proto</span><span>) =&gt; </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"&lt;function {}&gt;"</span><span>, </span><span>proto</span><span>.</span><span>display_name</span><span>()),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>{value:?}</code> 对 <code>String</code> 用的是 <code>String</code> 自己的 <code>Debug</code>，它会加引号并转义。需要 <code>use std::fmt;</code>。</p><section><h3>去重：为什么 Constant 没有 PartialEq<a href="#去重为什么-constant-没有-partialeq"><span>#</span></a></h3><p>常量池的卖点是去重：编译 <code>1 + 1 + 1</code> 时，<code>1</code> 只该存一份。判断”这两个常量是同一个吗”最顺手的做法是给 <code>Constant</code> 派生 <code>PartialEq</code>。我没有这么做，因为浮点数。实验室里 <code>ch10_float_bits.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch10_float_bits</span></div></div><div><div><div>2</div></div><div><span>0.0 == -0.0        -&gt; true</span></div></div><div><div><div>3</div></div><div><span>NaN == NaN         -&gt; false</span></div></div><div><div><div>4</div></div><div><span>bits equal 0/-0    -&gt; false</span></div></div><div><div><div>5</div></div><div><span>bits equal NaN/NaN -&gt; true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>f64</code> 的 <code>==</code> 遵循 IEEE 754：<code>0.0 == -0.0</code> 是真，<code>NaN == NaN</code> 是假。作为数学相等这没问题，作为”这两个常量能不能共用一格”就错了：把 <code>-0.0</code> 折叠进 <code>0.0</code> 会让 <code>1.0 / -0.0</code> 悄悄变成 <code>+inf</code>；而 <code>NaN</code> 永远不等于自己，意味着永远不去重，白占位置（现在的解析器不会产生这样的字面量，这是为以后常量折叠留的保险）。我们要的是<strong>逐比特相同</strong>，所以手写一个自己的比较，不叫 <code>==</code>，叫 <code>same</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Constant</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>same</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Constant</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> (</span><span>self</span><span>, </span><span>other</span><span>) {</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>(</span><span>Constant</span><span>::</span><span>Nil</span><span>, </span><span>Constant</span><span>::</span><span>Nil</span><span>) =&gt; </span><span>true</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>(</span><span>Constant</span><span>::</span><span>Bool</span><span>(</span><span>a</span><span>), </span><span>Constant</span><span>::</span><span>Bool</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>(</span><span>Constant</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Constant</span><span>::</span><span>Int</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>(</span><span>Constant</span><span>::</span><span>Float</span><span>(</span><span>a</span><span>), </span><span>Constant</span><span>::</span><span>Float</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span>.</span><span>to_bits</span><span><span>() </span><span>==</span><span> </span></span><span>b</span><span>.</span><span>to_bits</span><span>(),</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>(</span><span>Constant</span><span>::</span><span>Str</span><span>(</span><span>a</span><span>), </span><span>Constant</span><span>::</span><span>Str</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>// Function prototypes are never deduplicated.</span></div></div><div><div><div>10</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>false</span><span>,</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>(self, other)</code> 是个元组，<code>match</code> 同时拆开两边（第 3 篇）。最后的 <code>_ =&gt; false</code> 同时管着两件事：不同种类的常量当然不相同，函数原型永远不去重（两个函数即使字节一模一样，运行起来也是两个不同的闭包）。<code>to_bits()</code> 把 <code>f64</code> 看成它的 64 个比特，整数比较。</p><p>接着往 <code>impl Chunk</code> 里加 <code>intern</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Adds a scalar constant, reusing an identical one already in the pool.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Function prototypes are never deduplicated.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>intern</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>constant</span><span>: </span><span>Constant</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>if</span><span><span> </span><span>!</span></span><span>matches!</span><span>(</span><span>constant</span><span>, </span><span>Constant</span><span>::</span><span>Function</span><span>(</span><span>_</span><span>))</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>&amp;&amp;</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>position</span><span><span>) </span><span>=</span><span> </span></span><span>self</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>.constants</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>.</span><span>position</span><span><span>(</span><span>|</span></span><span>existing</span><span><span>|</span><span> </span></span><span>existing</span><span>.</span><span>same</span><span>(&amp;</span><span>constant</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>11</div></div><div><span>            </span><span>return</span><span> </span><span>position</span><span> </span><span>as</span><span> </span><span>u32</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span>.</span><span>add_constant</span><span>(</span><span>constant</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有一个新语法：<code>if A &amp;&amp; let Some(x) = B { ... }</code>，<strong>let 链</strong>（let chains）。条件里可以把布尔表达式和模式匹配用 <code>&amp;&amp;</code> 串起来，任何一环失败就整体失败，成功时 <code>position</code> 在块里可用。这是 2024 版的特性（Rust 1.88 起稳定，所以第 1 篇的工作区 <code>rust-version</code> 是 <code>1.88</code>）。没有它，你得写嵌套的 <code>if</code> 加 <code>if let</code>，或者先算出 <code>Option</code> 再拿去 <code>match</code>。<code>position</code> 返回第一个满足闭包的元素的下标。</p><p><code>pub(crate)</code> 是第 7 篇的可见性：只有这个 crate 里的代码能调它。外面的人想往常量池塞东西，走 <code>add_constant</code>（不去重），以免外部代码误以为 <code>intern</code> 对函数原型也去重。</p></section><section><h3>补上字面量<a href="#补上字面量"><span>#</span></a></h3><p>现在补上 <code>Nil</code>、<code>True</code>、<code>False</code> 两条指令和字面量的编译。在 <code>opcode_set!</code> 里，<code>Nil</code> 后面加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>/// Push `true`.</span></div></div><div><div><div>2</div></div><div><span>        </span><span>True</span><span>,</span></div></div><div><div><div>3</div></div><div><span>        </span><span>/// Push `false`.</span></div></div><div><div><div>4</div></div><div><span>        </span><span>False</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>compile_expr</code> 的 <code>match</code> 里，<code>Expr::Int</code> 那个分支换成用 <code>intern</code>，并补上其他字面量：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Nil</span><span> { .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Nil</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Bool</span><span> { </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>5</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span>(</span><span>if</span><span> *</span><span>value</span><span><span> { </span><span>Opcode</span><span>::</span></span><span>True</span><span> } </span><span>else</span><span><span> { </span><span>Opcode</span><span>::</span></span><span>False</span><span> }, </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Int</span><span> { </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>intern</span><span><span>(</span><span>Constant</span><span>::</span></span><span>Int</span><span>(*</span><span>value</span><span>));</span></div></div><div><div><div>9</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Const</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Float</code> 和 <code>Str</code> 的写法和 <code>Int</code> 一模一样，只换 <code>Constant::Float(*value)</code> 和 <code>Constant::Str(value.clone())</code>。<code>value.clone()</code> 是因为 AST 拥有这个 <code>String</code>，我们只借着 AST，常量池要自己的一份。</p><p>布尔和 <code>nil</code> 没有进常量池：它们各自只有一两个取值，一条无操作数的指令就够了，不值得绕一个下标。</p><p>测试：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>constants_are_deduplicated</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"1 + 1 + 1; </span><span>\"</span><span>x</span><span>\"</span><span>; </span><span>\"</span><span>x</span><span>\"</span><span>;"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>ints</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>chunk</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>constants</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>filter</span><span><span>(</span><span>|</span></span><span>c</span><span><span>|</span><span> </span></span><span>matches!</span><span>(</span><span>c</span><span><span>, </span><span>Constant</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)))</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>.</span><span>count</span><span>();</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>strings</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>chunk</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>.</span><span>constants</span><span>()</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>.</span><span>filter</span><span><span>(</span><span>|</span></span><span>c</span><span><span>|</span><span> </span></span><span>matches!</span><span>(</span><span>c</span><span><span>, </span><span>Constant</span><span>::</span></span><span>Str</span><span>(</span><span>s</span><span>) </span><span>if</span><span> </span><span>s</span><span><span> </span><span>==</span><span> </span></span><span>"x"</span><span>))</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>.</span><span>count</span><span>();</span></div></div><div><div><div>16</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>ints</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>strings</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>matches!(c, Constant::Str(s) if s == "x")</code> 是第 7 篇学的 <code>matches!</code>，这次带了守卫：只有字符串内容是 <code>"x"</code> 才算数。</p></section></section>
<section><h2>第五步：名字<a href="#第五步名字"><span>#</span></a></h2><p>变量怎么编译？<code>let x = 1;</code> 要”把名字 <code>x</code> 绑定到值 <code>1</code>”，<code>x</code> 要”找到这个名字的值”，<code>x = 2</code> 要”改已有的绑定”。我们引入三条指令，操作数都是<strong>名字在常量池里的下标</strong>（名字是字符串常量）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>/// Define `constants[operand]` in the current frame.</span></div></div><div><div><div>2</div></div><div><span>        </span><span>DefineName</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>3</div></div><div><span>        </span><span>/// Push the value of `constants[operand]`.</span></div></div><div><div><div>4</div></div><div><span>        </span><span>GetName</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>/// Assign to the existing binding `constants[operand]`, leaving the</span></div></div><div><div><div>6</div></div><div><span>        </span><span>/// value on the stack.</span></div></div><div><div><div>7</div></div><div><span>        </span><span>SetName</span><span>(</span><span>u32</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把它们加到 <code>opcode_set!</code> 里 <code>Pop</code> 的后面。注意是<strong>按名字</strong>：指令里没有”第几个局部变量”，运行时靠名字去环境里找，和第 9 篇的树遍历解释器一样。这让闭包简单到极点（闭包捕获整个环境，找名字沿着环境链往上），代价是速度：每次读变量都是一次哈希查找。生产级的编译器会在编译期把名字解析成槽位下标，再为闭包单独处理”上值”，Lua 就是这么做的。这是第 12 篇里要量一量的东西，现在先接受它。</p><p>三个新指令要用名字的常量下标，所以需要一个辅助函数：<code>name_const(name)</code>，给我一个名字的常量下标，没有就加进去。第一版很自然：我已经有 <code>intern</code>，名字不过是字符串常量，对吧？但每次变量引用都要线性扫描一遍常量池，我想加一张哈希表缓存”名字到下标”。这就是 <code>HashMap</code> 的 <code>entry</code> API 登场的地方：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>collections</span><span>::</span></span><span>HashMap</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>Compiler</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>chunk</span><span>: </span><span>Chunk</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>names</span><span>: </span><span>HashMap</span><span>&lt;</span><span>String</span><span>, </span><span>u32</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>diagnostics</span><span>: </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>new()</code> 里也要加 <code>names: HashMap::new()</code>。那么 <code>name_const</code>，我的第一稿是这么写的：<code>intern</code> 放在 <code>Compiler</code> 上（因为 <code>compile_expr</code> 到处都要用它），<code>name_const</code> 调用它（你不用真的敲这个版本，下面的输出是我敲出来抓的）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The constant-pool index of `name`, interned on first use.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>name_const</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>*</span><span>self</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>.names</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>entry</span><span>(</span><span>name</span><span>.</span><span>to_owned</span><span>())</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>or_insert_with</span><span><span>(</span><span>||</span><span> </span></span><span>self</span><span>.</span><span>intern</span><span><span>(</span><span>Constant</span><span>::</span></span><span>Str</span><span>(</span><span>name</span><span>.</span><span>to_owned</span><span>())))</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>entry(key)</code> 返回一个 <code>Entry</code>，表示”这个键对应的槽位，可能有值，可能没有”；<code>or_insert_with(f)</code> 说”有就用现成的，没有就调 <code>f()</code> 生成一个放进去”，两种情况都返回指向槽位里值的 <code>&amp;mut u32</code>，开头那个 <code>*</code> 把它读成 <code>u32</code>。C++ 里最接近的是 <code>unordered_map::try_emplace</code>，或者 <code>m[key]</code> 的默认构造再赋值，都是只哈希一次完成”查找或插入”。如果不用 <code>entry</code>，你得先 <code>get</code> 一次、没有再 <code>insert</code> 一次，哈希两次。</p><p>那一版编译不过。真实输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0500]: closure requires unique access to `*self` but it is already borrowed</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-bytecode/src/lib.rs:262:29</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>259 |           *self</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|  __________-</span></div></div><div><div><div>6</div></div><div><span>260 | |             .names</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>| |__________________- borrow occurs here</span></div></div><div><div><div>8</div></div><div><span>261 |               .entry(name.to_owned())</span></div></div><div><div><div>9</div></div><div><span>262 |               .or_insert_with(|| self.intern(Constant::Str(name.to_owned())))</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|                -------------- ^^ ---- second borrow occurs due to use of `*self` in closure</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|                |              |</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>|                |              closure construction occurs here</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>|                first borrow later used by call</span></div></div><div><div><div>14</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读法是：<code>self.names.entry(..)</code> 对 <code>self.names</code> 做了一次可变借用，这个借用在 <code>or_insert_with</code> 返回之前一直活着。而闭包 <code>|| self.intern(..)</code> 要对整个 <code>*self</code> 做可变借用，因为 <code>intern</code> 是个 <code>&amp;mut self</code> 方法，它<strong>可能</strong>改任何字段，包括正被借用的 <code>names</code>。两个可变借用重叠，第 2 篇的”别名和可变不能同时存在”。换成 C++，没有任何东西拦你写出结构相同的代码，要是换成 <code>vector</code> 或开放寻址的哈希表，<code>intern</code> 恰好触发了扩容，手里那个指向槽位的引用就悬空了，未定义行为；Rust 在编译期就说不。</p><p>实验室里 <code>ch10_capture.rs</code> 把这个问题缩到最小（一个 <code>Vec&lt;String&gt;</code> 做池子，一个 <code>HashMap</code> 做名字表），编译器给出同样的 E0500：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0500]: closure requires unique access to `*self` but it is already borrowed</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch10_capture.rs:18:29</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>15 |           *self</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|  __________-</span></div></div><div><div><div>6</div></div><div><span>16 | |             .names</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>| |__________________- borrow occurs here</span></div></div><div><div><div>8</div></div><div><span>17 |               .entry(name.to_owned())</span></div></div><div><div><div>9</div></div><div><span>18 |               .or_insert_with(|| self.add(name))</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|                -------------- ^^ ---- second borrow occurs due to use of `*self` in closure</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|                |              |</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|                |              closure construction occurs here</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|                first borrow later used by call</span></div></div><div><div><div>14</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>出路：别让闭包碰整个 <code>self</code>，只碰它真正需要的<strong>字段</strong>。从 2021 版开始，闭包是<strong>按字段捕获</strong>的：如果闭包里只写了 <code>self.pool.push(..)</code>，它只借 <code>self.pool</code>，不借 <code>self.names</code>，两个借用互不重叠，编译器放行。实验室里 <code>ch10_capture_ok.rs</code> 把方法体直接内联进闭包：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch10_capture_ok</span></div></div><div><div><div>2</div></div><div><span>0 1 0</span></div></div><div><div><div>3</div></div><div><span>pool = ["x", "y"]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>x</code> 第二次查询得到 <code>0</code>，没有再往池子里加一份。真实代码里不想把整个 <code>intern</code> 内联，于是把 <code>intern</code> 挪到 <code>Chunk</code> 上（我们上一节已经这么写了）：它只借 <code>self.chunk</code>，<code>name_const</code> 变成这样，和上面的失败版本只差 <code>self.chunk.intern</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The constant-pool index of `name`, interned on first use.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>name_const</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>*</span><span>self</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>.names</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>entry</span><span>(</span><span>name</span><span>.</span><span>to_owned</span><span>())</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>or_insert_with</span><span><span>(</span><span>||</span><span> </span></span><span>self</span><span>.chunk.</span><span>intern</span><span><span>(</span><span>Constant</span><span>::</span></span><span>Str</span><span>(</span><span>name</span><span>.</span><span>to_owned</span><span>())))</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>闭包只用 <code>self.chunk</code>，<code>entry</code> 只用 <code>self.names</code>，不同字段，编译通过。这也是为什么 <code>intern</code> 在 <code>Chunk</code> 上而不在 <code>Compiler</code> 上：不是美学，是借用检查器逼的。</p><p>这张哈希表值得吗？我在一个临时测试里量了一下（release 构建，这台机器）：20000 个互不相同的变量，每个 <code>let</code> 一次再引用一次。有 <code>names</code> 表的版本编译用 386 到 395 ms，去掉表、直接 <code>self.chunk.intern(..)</code> 的版本 731 到 753 ms，大约两倍。只有两倍的原因：每个<strong>新</strong>名字第一次出现时仍然要在池子里线性扫一遍，哈希表只省了重复引用的那一次。整个编译在”不同常量个数”上依旧是平方级的。真正的修法是让常量池自己用哈希去重，我没做。（这台机器上的数字，一次性的临时测试，不是基准，别太当真；要点是 <code>entry</code> 让”查或插”只哈希一次。）</p><section><h3>编译 let、变量和赋值<a href="#编译-let变量和赋值"><span>#</span></a></h3><p><code>compile_stmt</code> 里加 <code>let</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Let</span><span> { </span><span>name</span><span>, </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>3</div></div><div><span>                </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(&amp;</span><span>name</span><span>.name);</span></div></div><div><div><div>4</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>DefineName</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先编译右边（值留在栈顶），再 <code>DefineName</code> 把栈顶弹出，绑定到名字。<code>compile_expr</code> 里加变量读取和赋值（对索引赋值留到后面）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Var</span><span> { </span><span>name</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(&amp;</span><span>name</span><span>.name);</span></div></div><div><div><div>3</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>GetName</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>赋值是 <code>Expr::Assign</code>，目标是个变量时的分支如下。下标赋值和非法目标先留一个 <code>todo!()</code>，第九步再补：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Assign</span><span> { </span><span>target</span><span>, </span><span>value</span><span>, .. } =&gt; </span><span>match</span><span> </span><span>target</span><span>.</span><span>as_ref</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>Expr</span><span>::</span><span>Var</span><span> { </span><span>name</span><span>, .. } =&gt; {</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(&amp;</span><span>name</span><span>.name);</span></div></div><div><div><div>5</div></div><div><span>                    </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>SetName</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>                </span><span>_</span><span> =&gt; </span><span>todo!</span><span>(</span><span>"assignment target"</span><span>),</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>赋值 <code>x = value</code> 也是先编译右边，然后 <code>SetName</code>。注意注释里的细节：<code>SetName</code> <strong>把值留在栈上</strong>，因为赋值在 Pebble 里是个表达式（<code>a = b = 3</code> 合法）。表达式语句会跟一条 <code>Pop</code> 把它扔掉。看 <code>names.pebble</code>（<code>let x = 1; x = x + 2; x;</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode names.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 1</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 "x"</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>2 2</span></div></div><div><div><div>6</div></div><div><span>== code ==</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>0 Const 0</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>1 DefineName 1</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>2 GetName 1</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>3 Const 2</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>4 Add</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>5 SetName 1</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>6 Pop</span></div></div><div><div><div>14</div></div><div><span><span>     </span></span><span>7 GetName 1</span></div></div><div><div><div>15</div></div><div><span>3</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个常量：整数 <code>1</code>，字符串 <code>"x"</code>（引号告诉你它是名字，不是数字，这就是手写 <code>Debug</code> 的回报），整数 <code>2</code>。<code>x</code> 的下标 <code>1</code> 在 <code>DefineName</code>、<code>GetName</code>、<code>SetName</code> 里都是同一个，因为 <code>names</code> 缓存把它们折叠了。第 5 条是 <code>SetName</code>，第 6 条 <code>Pop</code> 把赋值表达式的值扔掉，因为这是一条语句。最后一条 <code>GetName 1</code> 没有 <code>Pop</code>：它是整个程序的值，解释器打印出 <code>3</code>。</p></section></section>
<section><h2>第六步：补全运算符<a href="#第六步补全运算符"><span>#</span></a></h2><p><code>1 + 2 * 3</code> 里只有加和乘。其他运算符：减、除、取余、取负、逻辑非、六个比较。在 <code>opcode_set!</code> 里把 <code>Add</code>、<code>Mul</code> 的位置换成完整的一组：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>/// `a + b`</span></div></div><div><div><div>2</div></div><div><span>        </span><span>Add</span><span>,</span></div></div><div><div><div>3</div></div><div><span>        </span><span>/// `a - b`</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Sub</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>/// `a * b`</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Mul</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>/// `a / b`</span></div></div><div><div><div>8</div></div><div><span>        </span><span>Div</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>/// `a % b`</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Rem</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>/// `-a`</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Neg</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>/// `not a`</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Not</span><span>,</span></div></div><div><div><div>15</div></div><div><span>        </span><span>/// `a == b`</span></div></div><div><div><div>16</div></div><div><span>        </span><span>Equal</span><span>,</span></div></div><div><div><div>17</div></div><div><span>        </span><span>/// `a != b`</span></div></div><div><div><div>18</div></div><div><span>        </span><span>NotEqual</span><span>,</span></div></div><div><div><div>19</div></div><div><span>        </span><span>/// `a &lt; b`</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Less</span><span>,</span></div></div><div><div><div>21</div></div><div><span>        </span><span>/// `a &lt;= b`</span></div></div><div><div><div>22</div></div><div><span>        </span><span>LessEqual</span><span>,</span></div></div><div><div><div>23</div></div><div><span>        </span><span>/// `a &gt; b`</span></div></div><div><div><div>24</div></div><div><span>        </span><span>Greater</span><span>,</span></div></div><div><div><div>25</div></div><div><span>        </span><span>/// `a &gt;= b`</span></div></div><div><div><div>26</div></div><div><span>        </span><span>GreaterEqual</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器侧：<code>BinaryOp</code> 到 <code>Opcode</code> 的映射是一个独立的 <code>const fn</code>，放在 <code>impl Compiler</code> 外面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>fn</span><span> </span><span>arithmetic_opcode</span><span>(</span><span>op</span><span>: </span><span>BinaryOp</span><span>) -&gt; </span><span>Opcode</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Add</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Add</span><span>,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Sub</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Sub</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Mul</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Mul</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Div</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Div</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Rem</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Rem</span><span>,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Eq</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Equal</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>NotEq</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>NotEqual</span><span>,</span></div></div><div><div><div>10</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Lt</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Less</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>LtEq</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>LessEqual</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Gt</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>Greater</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>GtEq</span><span> =&gt; </span><span>Opcode</span><span>::</span><span>GreaterEqual</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>没有 <code>_</code> 通配分支。这是故意的：以后有人在 AST 的 <code>BinaryOp</code> 里加一个 <code>Pow</code>，这里会立刻 E0004 提醒他。<code>BinaryOp</code> 是 <code>Copy</code> 的（第 5 篇），所以按值传。<code>compile_expr</code> 里原来的 <code>Binary</code> 分支和新增的 <code>Unary</code> 分支变成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Unary</span><span> { </span><span>op</span><span>, </span><span>operand</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>operand</span><span>);</span></div></div><div><div><div>3</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span>(</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>5</div></div><div><span><span>                        </span></span><span>UnaryOp</span><span>::</span><span>Neg</span><span><span> =&gt; </span><span>Opcode</span><span>::</span></span><span>Neg</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>                        </span></span><span>UnaryOp</span><span>::</span><span>Not</span><span><span> =&gt; </span><span>Opcode</span><span>::</span></span><span>Not</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>                    </span></span><span>},</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>span</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Binary</span><span> { </span><span>op</span><span>, </span><span>lhs</span><span>, </span><span>rhs</span><span>, .. } =&gt; {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>lhs</span><span>);</span></div></div><div><div><div>13</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>rhs</span><span>);</span></div></div><div><div><div>14</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span>(</span><span>arithmetic_opcode</span><span>(*</span><span>op</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>use pebble_ast::{BinaryOp, Expr, LogicalOp, Program, Spanned, Stmt, UnaryOp};</code> 这一行同时把后面要用的 <code>LogicalOp</code> 也带上。</p></section>
<section><h2>第七步：跳转和回填<a href="#第七步跳转和回填"><span>#</span></a></h2><p><code>if cond { A } else { B }</code> 怎么变成一串线性指令？</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>&lt;cond&gt;</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>JumpIfFalse -&gt; ELSE</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;A&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>Jump -&gt; END</span></div></div><div><div><div>5</div></div><div><span>ELSE:</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>&lt;B&gt;</span></div></div><div><div><div>7</div></div><div><span>END:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>JumpIfFalse</code> 要写”目标是 <code>ELSE</code> 的下标”，但编译到这条指令时我们还没编译 <code>A</code>，不知道 <code>ELSE</code> 在哪。C 程序员熟悉这个问题：汇编器遇到向前的 <code>goto</code> 标签时，也不知道标签的地址。解法有两种：两遍扫描（第一遍记下所有标签，第二遍填）；或者<strong>回填</strong>（backpatching）：先发一条目标留空的跳转，记下它的位置，等目标确定了再回来改。像写一封信里”详见第 __ 页”，先空着，写到那一页的时候再翻回去补。</p><p>跳转指令加入 <code>opcode_set!</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>/// Jump to an absolute instruction index.</span></div></div><div><div><div>2</div></div><div><span>        </span><span>Jump</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>3</div></div><div><span>        </span><span>/// Pop, then jump if the value was falsy.</span></div></div><div><div><div>4</div></div><div><span>        </span><span>JumpIfFalse</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>/// Peek: if falsy, jump keeping the value; otherwise pop and continue.</span></div></div><div><div><div>6</div></div><div><span>        </span><span>JumpIfFalseKeep</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>7</div></div><div><span>        </span><span>/// Peek: if truthy, jump keeping the value; otherwise pop and continue.</span></div></div><div><div><div>8</div></div><div><span>        </span><span>JumpIfTrueKeep</span><span>(</span><span>u32</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>后两条（<code>Keep</code> 版）是给 <code>and</code>/<code>or</code> 用的，下面讲。目标是<strong>绝对的指令下标</strong>。<code>impl Chunk</code> 里加两个方法：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Appends a placeholder that will be patched once its target is known.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>placeholder</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>op</span><span>: </span><span>Opcode</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span>(</span><span>op</span><span>, </span><span>span</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>/// Points a previously emitted jump at `target`.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>///</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// Panics if `at` is not a jump instruction; that would be a compiler bug.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>patch_jump</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>at</span><span>: </span><span>usize</span><span>, </span><span>target</span><span>: </span><span>usize</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>target</span><span><span> </span><span>=</span><span> </span></span><span>target</span><span> </span><span>as</span><span> </span><span>u32</span><span>;</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span>.code[</span><span>at</span><span><span>] </span><span>=</span><span> </span></span><span>match</span><span> </span><span>self</span><span>.code[</span><span>at</span><span>] {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>Jump</span><span>(</span><span>_</span><span>) =&gt; </span><span>Opcode</span><span>::</span><span>Jump</span><span>(</span><span>target</span><span>),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>JumpIfFalse</span><span>(</span><span>_</span><span>) =&gt; </span><span>Opcode</span><span>::</span><span>JumpIfFalse</span><span>(</span><span>target</span><span>),</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>JumpIfFalseKeep</span><span>(</span><span>_</span><span>) =&gt; </span><span>Opcode</span><span>::</span><span>JumpIfFalseKeep</span><span>(</span><span>target</span><span>),</span></div></div><div><div><div>16</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>JumpIfTrueKeep</span><span>(</span><span>_</span><span>) =&gt; </span><span>Opcode</span><span>::</span><span>JumpIfTrueKeep</span><span>(</span><span>target</span><span>),</span></div></div><div><div><div>17</div></div><div><span>            </span><span>ref</span><span> </span><span>other</span><span> =&gt; </span><span>panic!</span><span>(</span><span>"cannot patch {other:?} as a jump"</span><span>),</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>placeholder</code> 只是 <code>emit</code> 的别名，让调用处的意图更清楚（“这条的目标是空的，后面要补”）。典型的用法：<code>let at = chunk.placeholder(Opcode::Jump(0), span);</code>，操作数 <code>0</code> 是占位符，没有意义；返回的 <code>at</code> 是这条指令的下标，之后交给 <code>patch_jump(at, target)</code>。</p><p><code>patch_jump</code> 里有两个值得停下来的东西。第一，<code>match self.code[at]</code> 匹配的是一个<strong>位置</strong>（<code>Vec</code> 的一个槽位）而不是一个值。第二，最后一个分支里的 <code>ref other</code>：第 9 篇只提过一句”默认绑定模式让你不用写 <code>ref</code>”，现在第一次真的写出来。通常 <code>other =&gt; ...</code> 会把被匹配的值<strong>移动</strong>进 <code>other</code>；加了 <code>ref</code> 就改成<strong>借用</strong>。看实验室 <code>ch10_ref_binding.rs</code>，它用一个不是 <code>Copy</code> 的枚举：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0507]: cannot move out of index of `Vec&lt;Token&gt;`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch10_ref_binding.rs:11:22</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>11 |     code[at] = match code[at] {</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                      ^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span>12 |         Token::Jump(_) =&gt; Token::Jump(7),</span></div></div><div><div><div>7</div></div><div><span>13 |         other =&gt; panic!("cannot patch {other:?} as a jump"),</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|         -----</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|         |</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|         data moved here</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|         move occurs because `other` has type `Token`, which does not implement the `Copy` trait</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>help: consider borrowing here</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>11 |     code[at] = match &amp;code[at] {</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>|                      +</span></div></div><div><div><div>17</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>你不能从 <code>Vec</code> 里把一个元素”拿走”，留下一个空洞（C++ 里这叫 moved-from，对象还在，状态不明；Rust 干脆不允许）。<code>ref other</code> 就是”我只想看一眼”。换成 <code>ref other</code> 之后（<code>ch10_ref_binding_ok.rs</code>）输出 <code>[Jump(7), Name("x")]</code>。</p><p>但对 <code>Opcode</code> 呢？它是 <code>Copy</code> 的，我试了：把 <code>ref</code> 去掉，<code>cargo build</code> 和 <code>cargo clippy -- -D warnings</code> 都照样通过，<code>other</code> 只是复制了一份。所以这里的 <code>ref</code> 不是必须的，是为了让读代码的人一眼看出”这个分支不消耗它”，以及万一哪天 <code>Opcode</code> 不再是 <code>Copy</code> 也不会突然炸。我保留了它。</p><p><code>target as u32</code> 是第 1 篇的 <code>as</code>：如果程序超过 40 亿条指令会悄悄截断，我选择不理会，这个编译器的输入是人写的脚本。</p><section><h3>if<a href="#if"><span>#</span></a></h3><p>回到 <code>Compiler</code>。先加一个小工具 <code>here</code>，“下一条指令会放在哪个下标”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>here</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>self</span><span>.chunk.</span><span>len</span><span>()</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>compile_stmt</code> 里的 <code>if</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>If</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>cond</span><span>,</span></div></div><div><div><div>3</div></div><div><span>                </span><span>then_branch</span><span>,</span></div></div><div><div><div>4</div></div><div><span>                </span><span>else_branch</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>..</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>} =&gt; {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>cond</span><span>);</span></div></div><div><div><div>8</div></div><div><span>                </span><span>let</span><span> </span><span>jump_else</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>placeholder</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>JumpIfFalse</span><span>(</span><span>0</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>9</div></div><div><span>                </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>then_branch</span><span> {</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>                </span><span>let</span><span> </span><span>jump_end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>placeholder</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Jump</span><span>(</span><span>0</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>13</div></div><div><span>                </span><span>let</span><span> </span><span>else_target</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>14</div></div><div><span>                </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>jump_else</span><span>, </span><span>else_target</span><span>);</span></div></div><div><div><div>15</div></div><div><span>                </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>else_branch</span><span> {</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>                </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>19</div></div><div><span>                </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>jump_end</span><span>, </span><span>end</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对着前面的模板读一遍：发条件；发一条目标待定的 <code>JumpIfFalse</code>，记下它的下标；编译 <code>then</code>；发一条目标待定的 <code>Jump</code>（跳过 <code>else</code>）；<strong>此刻</strong>的 <code>here()</code> 就是 <code>ELSE</code> 的位置，回头补上第一条；编译 <code>else</code>；<code>here()</code> 又是 <code>END</code>，补上第二条。真实输出，<code>if true { 1; } else { 2; }</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode ifelse.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 1</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 2</span></div></div><div><div><div>5</div></div><div><span>== code ==</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>0 True</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>1 JumpIfFalse -&gt; 5</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>2 Const 0</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>3 Pop</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>4 Jump -&gt; 7</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>5 Const 1</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>6 Pop</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>7 Nil</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>条件为假时第 1 条跳到 5（<code>else</code> 的 <code>Const 1</code>），为真时顺着走完 2、3，第 4 条 <code>Jump -&gt; 7</code> 跳过 <code>else</code>。7 是整个程序的末尾：<code>if</code> 是语句，最后一条语句不是表达式，所以编译器按约定补了一条 <code>Nil</code> 当作程序的值。<code>-&gt;</code> 后面这些数字，没有一个是我写的，都是回填填的。disassemble 里要给跳转指令一个 <code>-&gt;</code> 的显示格式，把它加进上面 <code>match op</code> 的分支里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Jump</span><span>(</span><span>target</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfFalse</span><span>(</span><span>target</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfFalseKeep</span><span>(</span><span>target</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfTrueKeep</span><span>(</span><span>target</span><span>) =&gt; </span><span>format!</span><span>(</span><span>" -&gt; {target}"</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里用了<strong>或模式</strong>：四个变体共享同一个分支，它们都有一个叫 <code>target</code> 的 <code>u32</code> 载荷。</p></section><section><h3>while<a href="#while"><span>#</span></a></h3><p>循环不一样：回跳的目标（循环开头）在编译循环体之前就已经知道了，是<strong>向后</strong>跳，不需要回填；但循环<strong>出口</strong>的目标（循环结束处）是向前的，要回填。而且循环体里可能有 <code>break</code> 和 <code>continue</code>，它们也要跳到这两个位置，但它们可能嵌在 <code>if</code> 的深处，编译到它们的时候我们不在 <code>while</code> 的那段代码里。解决办法是一个小栈，记着”当前有哪些循环，每个循环里有哪些待补的 <code>break</code>/<code>continue</code>”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Bookkeeping for one enclosing loop, so `break`/`continue` can be patched.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Default</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>LoopCtx</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>continue_jumps</span><span>: </span><span>Vec</span><span>&lt;</span><span>usize</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>break_jumps</span><span>: </span><span>Vec</span><span>&lt;</span><span>usize</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Compiler</code> 多一个字段 <code>loops: Vec&lt;LoopCtx&gt;</code>（<code>new()</code> 里 <code>loops: Vec::new()</code>）。<code>while</code> 的编译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>While</span><span> { </span><span>cond</span><span>, </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>loop_start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>3</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>cond</span><span>);</span></div></div><div><div><div>4</div></div><div><span>                </span><span>let</span><span> </span><span>exit</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>placeholder</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>JumpIfFalse</span><span>(</span><span>0</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>5</div></div><div><span>                </span><span>self</span><span>.loops.</span><span>push</span><span><span>(</span><span>LoopCtx</span><span>::</span></span><span>default</span><span>());</span></div></div><div><div><div>6</div></div><div><span>                </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>body</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>                </span><span>let</span><span> </span><span>ctx</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.loops.</span><span>pop</span><span>().</span><span>expect</span><span>(</span><span>"loop context"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Jump</span><span>(</span><span>loop_start</span><span> </span><span>as</span><span> </span><span>u32</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>12</div></div><div><span>                </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>exit</span><span>, </span><span>end</span><span>);</span></div></div><div><div><div>13</div></div><div><span>                </span><span>for</span><span> </span><span>jump</span><span> </span><span>in</span><span> </span><span>ctx</span><span>.break_jumps {</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>jump</span><span>, </span><span>end</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>                </span><span>for</span><span> </span><span>jump</span><span> </span><span>in</span><span> </span><span>ctx</span><span>.continue_jumps {</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>jump</span><span>, </span><span>loop_start</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>进入循环体前往栈里压一个 <code>LoopCtx</code>；编译体的时候，遇到 <code>break</code>/<code>continue</code> 的代码会把它们的占位跳转记在栈顶那个 <code>LoopCtx</code> 里；体编译完，弹出这个 <code>LoopCtx</code>，循环尾发回跳 <code>Jump(loop_start)</code>，然后把所有记下来的 <code>break</code> 补到 <code>end</code>、<code>continue</code> 补回 <code>loop_start</code>。<code>expect("loop context")</code> 是第 7 篇讲的”这里 <code>None</code> 就是我的 bug”：刚刚才 <code>push</code> 过，必然非空。真实输出，<code>let i = 0; while i &lt; 3 { i = i + 1; }</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode loop.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 0</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 "i"</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>2 3</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>3 1</span></div></div><div><div><div>7</div></div><div><span>== code ==</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>0 Const 0</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>1 DefineName 1</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>2 GetName 1</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>3 Const 2</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>4 Less</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>5 JumpIfFalse -&gt; 12</span></div></div><div><div><div>14</div></div><div><span><span>     </span></span><span>6 GetName 1</span></div></div><div><div><div>15</div></div><div><span><span>     </span></span><span>7 Const 3</span></div></div><div><div><div>16</div></div><div><span><span>     </span></span><span>8 Add</span></div></div><div><div><div>17</div></div><div><span><span>     </span></span><span>9 SetName 1</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>10 Pop</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>11 Jump -&gt; 2</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>12 Nil</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第 11 条 <code>Jump -&gt; 2</code> 就是回边：向后，直接指向循环开头的条件检查。第 5 条 <code>JumpIfFalse -&gt; 12</code> 是回填的出口。</p><p><code>break</code> 和 <code>continue</code> 本身：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Break</span><span> { .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>jump</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>placeholder</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Jump</span><span>(</span><span>0</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>3</div></div><div><span>                </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>ctx</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.loops.</span><span>last_mut</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>ctx</span><span>.break_jumps.</span><span>push</span><span>(</span><span>jump</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>6</div></div><div><span>                    </span><span>self</span><span>.diagnostics.</span><span>push</span><span>(</span></div></div><div><div><div>7</div></div><div><span><span>                        </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"`break` outside a loop"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>                            </span></span><span>.</span><span>with_code</span><span>(</span><span>"E3001"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>                            </span></span><span>.</span><span>with_span</span><span>(</span><span>span</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>                    </span></span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>发一条占位 <code>Jump</code>，然后看栈顶有没有循环：有，记下来；没有，就是用户写错了，往 <code>self.diagnostics</code> 里推一条错误。<code>last_mut()</code> 返回 <code>Option&lt;&amp;mut LoopCtx&gt;</code>，和第 3 篇的 <code>Diagnostic</code> 构建器（<code>error</code>、<code>with_code</code>、<code>with_span</code>）接上了。<code>Stmt::Continue</code> 完全对称：改成 <code>continue_jumps</code>，消息 <code> `continue` outside a loop</code>，错误码 <code>E3002</code>。</p><p>看一个把两者都用上的程序，<code>while true { if true { break; } continue; }</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode brk.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span>== code ==</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>0 True</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>1 JumpIfFalse -&gt; 8</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>2 True</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>3 JumpIfFalse -&gt; 6</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>4 Jump -&gt; 8</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>5 Jump -&gt; 6</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>6 Jump -&gt; 0</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>7 Jump -&gt; 0</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>8 Nil</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>逐条读：0、1 是 <code>while true</code> 的条件和出口（出口是 8）；2、3 是内层 <code>if true</code> 的条件和”不进 then 就跳到 6”；4 是 <code>break</code>（目标 8，循环出口）；5 是 <code>if</code> 的 then 分支末尾”跳过 else”，没有 <code>else</code>，所以目标 6，下一条；6 是 <code>continue</code>（目标 0，循环开头）；7 是循环尾的回边。</p></section><section><h3>and 和 or：短路，但保留值<a href="#and-和-or短路但保留值"><span>#</span></a></h3><p><code>a or 5</code> 在 Pebble 里（和 Lua、Python 一样）返回<strong>操作数</strong>，不是布尔：<code>nil or 5</code> 是 <code>5</code>，<code>3 or 5</code> 是 <code>3</code>。这要求跳转指令在跳的时候<strong>不弹出</strong>栈顶的值，所以才有了 <code>JumpIfTrueKeep</code> 和 <code>JumpIfFalseKeep</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Logical</span><span> { </span><span>op</span><span>, </span><span>lhs</span><span>, </span><span>rhs</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>lhs</span><span>);</span></div></div><div><div><div>3</div></div><div><span>                </span><span>let</span><span> </span><span>keep</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>LogicalOp</span><span>::</span><span>And</span><span> =&gt; </span><span>self</span><span>.chunk.</span><span>placeholder</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>JumpIfFalseKeep</span><span>(</span><span>0</span><span>), </span><span>span</span><span>),</span></div></div><div><div><div>5</div></div><div><span>                    </span><span>LogicalOp</span><span>::</span><span>Or</span><span> =&gt; </span><span>self</span><span>.chunk.</span><span>placeholder</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>JumpIfTrueKeep</span><span>(</span><span>0</span><span>), </span><span>span</span><span>),</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>};</span></div></div><div><div><div>7</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>rhs</span><span>);</span></div></div><div><div><div>8</div></div><div><span>                </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>9</div></div><div><span>                </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>keep</span><span>, </span><span>end</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>规则：<code>Keep</code> 版的跳转<strong>看一眼</strong>栈顶（不弹出）。<code>or</code>：真值就跳走，值留着当整个表达式的结果；假值就弹掉继续算右边。<code>and</code> 反过来。右边算完，栈顶就是结果。<code>let a = nil; a or 5;</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode logic.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 "a"</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 5</span></div></div><div><div><div>5</div></div><div><span>== code ==</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>0 Nil</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>1 DefineName 0</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>2 GetName 0</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>3 JumpIfTrueKeep -&gt; 5</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>4 Const 1</span></div></div><div><div><div>11</div></div><div><span>5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>a</code> 是 <code>nil</code>，是假值，第 3 条不跳，弹掉它，第 4 条压 <code>5</code>，程序的值是 <code>5</code>。（跳转目标 5 恰好等于指令总数：一个”跳到末尾之后”的下标，意思是”执行完了”。第 12 篇的 VM 主循环在指令下标走到末尾时结束，所以这是合法的。）</p><p>加上这些测试，覆盖 <code>if</code>、<code>while</code>、<code>break</code>、逻辑运算：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>if_compiles_to_a_patched_forward_jump</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"if true { 1; } else { 2; }"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>listing</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"JumpIfFalse -&gt;"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"Jump -&gt;"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>logical_operators_short_circuit_with_keeping_jumps</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"a and b; a or b;"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>listing</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>();</span></div></div><div><div><div>13</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"JumpIfFalseKeep"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"JumpIfTrueKeep"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>还差三个测试。<code>break</code> 写在循环外面要得到诊断，这个我们自己写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>break_outside_a_loop_is_a_diagnostic</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"break;"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile</span><span>(&amp;</span><span>result</span><span>.program);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>6</div></div><div><span>            </span><span>compiled</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>.diagnostics</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>.</span><span>any</span><span><span>(</span><span>|</span></span><span>d</span><span><span>|</span><span> </span></span><span>d</span><span>.code.</span><span>as_deref</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>"E3001"</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里故意只 <code>parse</code> 不断言解析成功：<code>break;</code> 在语法上合法，错在语义，是编译器的活。<code>d.code</code> 是 <code>Option&lt;String&gt;</code>，<code>as_deref()</code> 借成 <code>Option&lt;&amp;str&gt;</code>，才能和 <code>Some("E3001")</code> 比较。</p><p>另外两个，<code>while_back_edge_points_backwards</code>（回边的目标比自己小）和 <code>break_and_continue_are_patched</code>（直接比较指令序列里的下标），和上面的清单是同一个思路，直接从 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bytecode/src/lib.rs" target="_blank">tag 里的测试</a> 复制，从 <code>fn while_back_edge_points_backwards</code> 到 <code>fn break_and_continue_are_patched</code>。到这里的第一次完整检查：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bytecode</span></div></div><div><div><div>2</div></div><div><span>running 9 tests</span></div></div><div><div><div>3</div></div><div><span>test tests::constants_are_deduplicated ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::break_and_continue_are_patched ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::break_outside_a_loop_is_a_diagnostic ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::if_compiles_to_a_patched_forward_jump ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::logical_operators_short_circuit_with_keeping_jumps ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::compiles_arithmetic ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::non_trailing_expression_statements_are_popped ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::trailing_expression_is_the_program_value ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::while_back_edge_points_backwards ... ok</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>第八步：函数<a href="#第八步函数"><span>#</span></a></h2><p>函数是编译器里最有意思的一块，因为它让”一个程序”变成”一组程序”。每个函数体是它自己的指令序列，有自己的常量池，外层函数的常量池里放着它。运行时有两件事要发生：把这个函数变成一个<strong>值</strong>（闭包，捕获当前环境），以及<strong>调用</strong>它。对应三条指令，其中 <code>Return</code> 我们早就加过了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>/// Build a closure from the function prototype `constants[operand]`,</span></div></div><div><div><div>2</div></div><div><span>        </span><span>/// capturing the current frame.</span></div></div><div><div><div>3</div></div><div><span>        </span><span>MakeClosure</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>4</div></div><div><span>        </span><span>/// Call the callee sitting below `operand` arguments.</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Call</span><span>(</span><span>u32</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>MakeClosure</code> 的操作数指向常量池里一个 <code>Constant::Function</code>；<code>Call(n)</code> 的约定是：栈上依次是被调用的东西和它的 n 个参数，<strong>被调用者在参数下面</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>栈：[ ..., callee, arg1, arg2 ]      Call 2</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>之后：[ ..., result ]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先看怎么编译一个函数体：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>compile_function</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>3</div></div><div><span>        </span><span>name</span><span>: </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>params</span><span><span>: &amp;[</span><span>pebble_ast</span><span>::</span></span><span>Ident</span><span>],</span></div></div><div><div><div>5</div></div><div><span>        </span><span>body</span><span>: &amp;[</span><span>Stmt</span><span>],</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sub</span><span><span> </span><span>=</span><span> </span></span><span>Compiler</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>8</div></div><div><span>        </span><span>sub</span><span>.</span><span>compile_body</span><span>(</span><span>body</span><span>, </span><span>true</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>self</span><span>.diagnostics.</span><span>extend</span><span>(</span><span>sub</span><span>.diagnostics);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>proto</span><span><span> </span><span>=</span><span> </span></span><span>FunctionProto</span><span> {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>name</span><span>,</span></div></div><div><div><div>12</div></div><div><span>            </span><span>params</span><span>: </span><span>params</span><span>.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>param</span><span><span>|</span><span> </span></span><span>param</span><span>.name.</span><span>clone</span><span>()).</span><span>collect</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>chunk</span><span>: </span><span>sub</span><span>.chunk,</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span>.chunk.</span><span>add_constant</span><span><span>(</span><span>Constant</span><span>::</span></span><span>Function</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span>(</span><span>proto</span><span>)))</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这个函数值得逐行读。<code>Compiler::new()</code> 造一个<strong>全新的</strong>子编译器：新的 <code>Chunk</code>、新的 <code>loops</code>、新的 <code>names</code> 缓存。<code>names</code> 必须是新的，因为它缓存的是”名字在<strong>当前</strong> <code>Chunk</code> 的常量池里的下标”，换了一个 <code>Chunk</code> 下标就全不对了。这也是名字缓存放在 <code>Compiler</code> 里、而不是做成全局的原因。</p><p><code>compile_body(body, true)</code> 里的 <code>true</code> 就是前面那个 <code>in_function</code> 参数：函数体最后一条表达式的值要用 <code>Return</code> 返回出去；最后一条不是表达式（或者函数体是空的）时，补一个 <code>Nil</code> 再 <code>Return</code>。</p><p>然后是 Rust 里一个小而重要的东西：<code>self.diagnostics.extend(sub.diagnostics)</code> 把 <code>sub.diagnostics</code> 这个字段<strong>移出</strong>了 <code>sub</code>，下一行又用 <code>sub.chunk</code> 把另一个字段也移走。只移走结构体的一部分，叫<strong>部分移动</strong>，只要这个结构体没有实现 <code>Drop</code>（没有自定义析构函数）就是允许的：编译器清楚地知道哪些字段还在，哪些已经走了。C++ 里你得小心地对每个成员 <code>std::move</code>，而且移动后的成员仍然是个”有效但未指定”的对象，Rust 里被移走的字段<strong>根本不能再碰</strong>。</p><p>最后，函数原型进常量池用的是 <code>add_constant</code>，不是 <code>intern</code>：函数永远不去重（前面讲过）。返回值是它的常量下标，调用者拿去发 <code>MakeClosure</code>。<code>Rc::new(proto)</code> 把原型装进 <code>Rc</code>，以后 VM 里每个闭包持有的都是同一份。</p><p>函数声明、匿名函数和调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Fn</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>name</span><span>, </span><span>params</span><span>, </span><span>body</span><span>, ..</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>} =&gt; {</span></div></div><div><div><div>4</div></div><div><span>                </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>compile_function</span><span>(</span><span>Some</span><span>(</span><span>name</span><span>.name.</span><span>clone</span><span>()), </span><span>params</span><span>, </span><span>body</span><span>);</span></div></div><div><div><div>5</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>MakeClosure</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span>                </span><span>let</span><span> </span><span>name_index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(&amp;</span><span>name</span><span>.name);</span></div></div><div><div><div>7</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>DefineName</span><span>(</span><span>name_index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>fn fib(n) {...}</code> 就是”做一个闭包，再给它起个名字”，和 <code>let fib = fn(n) {...};</code> 几乎一样，区别只是原型里多记了一个名字 <code>fib</code>（<code>display_name</code> 会用它）。这一段放进 <code>compile_stmt</code>。<code>return</code> 语句也放进去：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Return</span><span> { </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>match</span><span> </span><span>value</span><span> {</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>Some</span><span>(</span><span>expr</span><span>) =&gt; </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>expr</span><span>),</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>None</span><span> =&gt; {</span></div></div><div><div><div>5</div></div><div><span>                        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Nil</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Return</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>compile_expr</code> 里的两个分支：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Call</span><span> { </span><span>callee</span><span>, </span><span>args</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>callee</span><span>);</span></div></div><div><div><div>3</div></div><div><span>                </span><span>for</span><span> </span><span>arg</span><span> </span><span>in</span><span> </span><span>args</span><span> {</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>arg</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Call</span><span>(</span><span>args</span><span>.</span><span>len</span><span>() </span><span>as</span><span> </span><span>u32</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Fn</span><span> { </span><span>params</span><span>, </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>compile_function</span><span>(</span><span>None</span><span>, </span><span>params</span><span>, </span><span>body</span><span>);</span></div></div><div><div><div>10</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>MakeClosure</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先编译被调用者，再依次编译每个参数，然后 <code>Call(参数个数)</code>。记住这个顺序，下一节 <code>for</code> 循环里它会咬人。</p><section><h3>反汇编器学会递归<a href="#反汇编器学会递归"><span>#</span></a></h3><p>函数的指令在它自己的 <code>Chunk</code> 里，反汇编器要能钻进去。<code>disassemble</code> 变成对一个带缩进参数的私有函数的调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>disassemble</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>out</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.</span><span>disassemble_into</span><span>(&amp;</span><span>mut</span><span> </span><span>out</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>out</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>disassemble_into</code> 就是之前的循环，加上缩进，加上三组新的操作数格式（<code>MakeClosure</code> 和 <code>Const</code> 一样是常量下标；<code>Call</code> 的操作数是个数，<code>BuildList</code>、<code>BuildMap</code> 要到第九步才有，到时再把它们并进这一支），以及遇到 <code>MakeClosure</code> 时递归：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>disassemble_into</span><span>(&amp;</span><span>self</span><span>, </span><span>out</span><span>: &amp;</span><span>mut</span><span> </span><span>String</span><span>, </span><span>indent</span><span>: </span><span>usize</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>pad</span><span><span> </span><span>=</span><span> </span></span><span>"  "</span><span>.</span><span>repeat</span><span>(</span><span>indent</span><span>);</span></div></div><div><div><div>3</div></div><div><span>        </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{pad}== constants ==</span><span>\n</span><span>"</span><span>));</span></div></div><div><div><div>4</div></div><div><span>        </span><span>for</span><span> (</span><span>index</span><span>, </span><span>constant</span><span>) </span><span>in</span><span> </span><span>self</span><span>.constants.</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{pad}  {index:&gt;4} {constant:?}</span><span>\n</span><span>"</span><span>));</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>        </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{pad}== code ==</span><span>\n</span><span>"</span><span>));</span></div></div><div><div><div>8</div></div><div><span>        </span><span>for</span><span> (</span><span>index</span><span>, </span><span>op</span><span>) </span><span>in</span><span> </span><span>self</span><span>.code.</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>let</span><span> </span><span>operand</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>DefineName</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>GetName</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>SetName</span><span>(</span><span>index</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>MakeClosure</span><span>(</span><span>index</span><span>) =&gt; </span><span>format!</span><span>(</span><span>" {index}"</span><span>),</span></div></div><div><div><div>15</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Jump</span><span>(</span><span>target</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfFalse</span><span>(</span><span>target</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfFalseKeep</span><span>(</span><span>target</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfTrueKeep</span><span>(</span><span>target</span><span>) =&gt; </span><span>format!</span><span>(</span><span>" -&gt; {target}"</span><span>),</span></div></div><div><div><div>19</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Call</span><span>(</span><span>count</span><span>) =&gt; {</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>format!</span><span>(</span><span>" {count}"</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>                </span><span>_</span><span> =&gt; </span><span>String</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>24</div></div><div><span>            </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span><span>"{pad}  {index:&gt;4} {}{operand}</span><span>\n</span><span>"</span><span>, </span><span>op</span><span>.</span><span>name</span><span>()));</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（上面是一个超过 15 行的块，主题会把它折叠，没关系，整段就是”之前的循环加三组分支”。）循环体最后一段是递归：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Opcode</span><span>::</span><span>MakeClosure</span><span>(</span><span>constant</span><span><span>) </span><span>=</span><span> </span></span><span>op</span></div></div><div><div><div>2</div></div><div><span><span>                </span></span><span>&amp;&amp;</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>Constant</span><span>::</span><span>Function</span><span>(</span><span>proto</span><span><span>)) </span><span>=</span><span> </span></span><span>self</span><span>.constants.</span><span>get</span><span>(*</span><span>constant</span><span> </span><span>as</span><span> </span><span>usize</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>{</span></div></div><div><div><div>4</div></div><div><span>                </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span></div></div><div><div><div>5</div></div><div><span>                    </span><span>"{pad}      -- function {} --</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>                    </span><span>proto</span><span>.</span><span>display_name</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>));</span></div></div><div><div><div>8</div></div><div><span>                </span><span>proto</span><span>.chunk.</span><span>disassemble_into</span><span>(</span><span>out</span><span>, </span><span>indent</span><span> + </span><span>3</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>又一个 let 链：<code>op</code> 是 <code>MakeClosure</code> <strong>并且</strong>它指向的常量确实是个函数，才进去递归。<code>out</code> 是一个 <code>&amp;mut String</code>，一路传下去，每一层往同一个 <code>String</code> 里追加，没有任何拷贝。<code>indent + 3</code> 让子清单缩进三级。</p><p>用它看 <code>examples/fib.pebble</code>。这是整篇文章里最长的一份真实输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --quiet --dump-bytecode examples/fib.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 &lt;function fib&gt;</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 "fib"</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>2 "print"</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>3 20</span></div></div><div><div><div>7</div></div><div><span>== code ==</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>0 MakeClosure 0</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>-- function fib --</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>== constants ==</span></div></div><div><div><div>11</div></div><div><span><span>           </span></span><span>0 "n"</span></div></div><div><div><div>12</div></div><div><span><span>           </span></span><span>1 2</span></div></div><div><div><div>13</div></div><div><span><span>           </span></span><span>2 "fib"</span></div></div><div><div><div>14</div></div><div><span><span>           </span></span><span>3 1</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>== code ==</span></div></div><div><div><div>16</div></div><div><span><span>           </span></span><span>0 GetName 0</span></div></div><div><div><div>17</div></div><div><span><span>           </span></span><span>1 Const 1</span></div></div><div><div><div>18</div></div><div><span><span>           </span></span><span>2 Less</span></div></div><div><div><div>19</div></div><div><span><span>           </span></span><span>3 JumpIfFalse -&gt; 7</span></div></div><div><div><div>20</div></div><div><span><span>           </span></span><span>4 GetName 0</span></div></div><div><div><div>21</div></div><div><span><span>           </span></span><span>5 Return</span></div></div><div><div><div>22</div></div><div><span><span>           </span></span><span>6 Jump -&gt; 7</span></div></div><div><div><div>23</div></div><div><span><span>           </span></span><span>7 GetName 2</span></div></div><div><div><div>24</div></div><div><span><span>           </span></span><span>8 GetName 0</span></div></div><div><div><div>25</div></div><div><span><span>           </span></span><span>9 Const 3</span></div></div><div><div><div>26</div></div><div><span><span>          </span></span><span>10 Sub</span></div></div><div><div><div>27</div></div><div><span><span>          </span></span><span>11 Call 1</span></div></div><div><div><div>28</div></div><div><span><span>          </span></span><span>12 GetName 2</span></div></div><div><div><div>29</div></div><div><span><span>          </span></span><span>13 GetName 0</span></div></div><div><div><div>30</div></div><div><span><span>          </span></span><span>14 Const 1</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>15 Sub</span></div></div><div><div><div>32</div></div><div><span><span>          </span></span><span>16 Call 1</span></div></div><div><div><div>33</div></div><div><span><span>          </span></span><span>17 Add</span></div></div><div><div><div>34</div></div><div><span><span>          </span></span><span>18 Return</span></div></div><div><div><div>35</div></div><div><span><span>          </span></span><span>19 Nil</span></div></div><div><div><div>36</div></div><div><span><span>          </span></span><span>20 Return</span></div></div><div><div><div>37</div></div><div><span><span>     </span></span><span>1 DefineName 1</span></div></div><div><div><div>38</div></div><div><span><span>     </span></span><span>2 GetName 2</span></div></div><div><div><div>39</div></div><div><span><span>     </span></span><span>3 GetName 1</span></div></div><div><div><div>40</div></div><div><span><span>     </span></span><span>4 Const 3</span></div></div><div><div><div>41</div></div><div><span><span>     </span></span><span>5 Call 1</span></div></div><div><div><div>42</div></div><div><span><span>     </span></span><span>6 Call 1</span></div></div><div><div><div>43</div></div><div><span>6765</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>从外往里读。顶层只有七条指令：做出 <code>fib</code> 闭包（0），起名（1），然后 <code>print(fib(20))</code>：取 <code>print</code>（2），取 <code>fib</code>（3），常量 <code>20</code>（4），<code>Call 1</code> 调用 <code>fib</code>（5），栈上现在是 <code>[print, 6765]</code>，再 <code>Call 1</code> 调用 <code>print</code>（6）。这就是”被调用者在参数下面”：<code>print</code> 先压栈，后面整个 <code>fib(20)</code> 的结果充当它的参数。最后的 <code>6765</code> 是 <code>print</code> 打出来的，不是程序的值（<code>print</code> 返回 <code>nil</code>）。</p><p>里面那个函数体 21 条。<code>GetName 0</code> 是 <code>n</code>（<code>"n"</code> 是函数自己的常量 0）；<code>Const 1</code> 是 <code>2</code>；<code>Less</code>；<code>JumpIfFalse -&gt; 7</code>；<code>n &lt; 2</code> 成立就 <code>GetName 0; Return</code>；接着第 6 条 <code>Jump -&gt; 7</code>，是 <code>if</code> 为真分支末尾那个”跳过 else”，但 <code>Return</code> 已经走了，<strong>永远执行不到</strong>，这是 <code>if</code> 的固定模板留下的死代码，编译器没有去优化它。7 到 18 是 <code>fib(n - 1) + fib(n - 2)</code>，注意这里的 <code>fib</code> 要用 <code>GetName 2</code> 去环境里按名字找，递归调用也是这样；最后 19、20 是 <code>compile_body</code> 在”最后一条语句不是表达式”时补的 <code>Nil; Return</code>，这里最后一条是 <code>return</code> 语句，所以这两条同样永远执行不到。</p><div><div><div></div><div>常量池是每个函数一份</div></div><div><p>两层各有自己的常量池：外层池里有 <code>"fib"</code>、<code>"print"</code> 和整数 <code>20</code>，里层池里有 <code>"n"</code>、<code>2</code>、<code>"fib"</code> 和 <code>1</code>。<code>"fib"</code> 在两个池里各存了一份，下标还不一样（外层是 1，里层是 2）。这正是前面说的”<code>names</code> 缓存必须每个 <code>Compiler</code> 一份”。</p></div></div></section></section>
<section><h2>第九步：列表、映射、下标，以及 for<a href="#第九步列表映射下标以及-for"><span>#</span></a></h2><p>四条指令，对应第 8 篇的列表和映射：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>/// Pop `operand` items into a list.</span></div></div><div><div><div>2</div></div><div><span>        </span><span>BuildList</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>3</div></div><div><span>        </span><span>/// Pop `operand` key/value pairs into a map.</span></div></div><div><div><div>4</div></div><div><span>        </span><span>BuildMap</span><span>(</span><span>u32</span><span>),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>/// `container[key]`</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Index</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>/// `container[key] = value`, leaving the value on the stack.</span></div></div><div><div><div>8</div></div><div><span>        </span><span>SetIndex</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把它们加进 <code>opcode_set!</code> 之后，回到第八步的 <code>disassemble_into</code>，把 <code>Opcode::Call(count) =&gt; {</code> 一行改成三路或模式（<code>BuildList</code> 和 <code>BuildMap</code> 的操作数也是个数）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>                </span><span>Opcode</span><span>::</span><span>Call</span><span>(</span><span>count</span><span><span>) </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>BuildList</span><span>(</span><span>count</span><span><span>) </span><span>|</span><span> </span></span><span>Opcode</span><span>::</span><span>BuildMap</span><span>(</span><span>count</span><span>) =&gt; {</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>List</span><span> { </span><span>items</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>items</span><span> {</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>item</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>BuildList</span><span>(</span><span>items</span><span>.</span><span>len</span><span>() </span><span>as</span><span> </span><span>u32</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Map</span><span> { </span><span>entries</span><span>, .. } =&gt; {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>for</span><span> (</span><span>key</span><span>, </span><span>value</span><span>) </span><span>in</span><span> </span><span>entries</span><span> {</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>key</span><span>);</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>BuildMap</span><span>(</span><span>entries</span><span>.</span><span>len</span><span>() </span><span>as</span><span> </span><span>u32</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Index</span><span> { </span><span>object</span><span>, </span><span>index</span><span>, .. } =&gt; {</span></div></div><div><div><div>15</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>object</span><span>);</span></div></div><div><div><div>16</div></div><div><span>                </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>index</span><span>);</span></div></div><div><div><div>17</div></div><div><span>                </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Index</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>和 <code>Call</code> 一个套路：先把所有元素压栈，最后一条指令说”栈顶这 n 个东西做成一个列表”。赋值分支现在可以补完整了：把第五步那个带 <code>todo!("assignment target")</code> 的 <code>Expr::Assign</code> 分支整个换成下面的版本。对下标赋值多出一路，目标既不是变量也不是下标时给出一个诊断（<code>E3003</code>），把右边照样编译出来，让栈保持平衡：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Assign</span><span> { </span><span>target</span><span>, </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>match</span><span> </span><span>target</span><span>.</span><span>as_ref</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>Expr</span><span>::</span><span>Var</span><span> { </span><span>name</span><span>, .. } =&gt; {</span></div></div><div><div><div>4</div></div><div><span>                        </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>5</div></div><div><span>                        </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(&amp;</span><span>name</span><span>.name);</span></div></div><div><div><div>6</div></div><div><span>                        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>SetName</span><span>(</span><span>index</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>Expr</span><span>::</span><span>Index</span><span> { </span><span>object</span><span>, </span><span>index</span><span>, .. } =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                        </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>object</span><span>);</span></div></div><div><div><div>10</div></div><div><span>                        </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>index</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                        </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>12</div></div><div><span>                        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>SetIndex</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>other</span><span> =&gt; {</span></div></div><div><div><div>15</div></div><div><span>                        </span><span>self</span><span>.diagnostics.</span><span>push</span><span>(</span></div></div><div><div><div>16</div></div><div><span><span>                            </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"invalid assignment target"</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>                                </span></span><span>.</span><span>with_code</span><span>(</span><span>"E3003"</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>                                </span></span><span>.</span><span>with_span</span><span>(</span><span>other</span><span>.</span><span>span</span><span>()),</span></div></div><div><div><div>19</div></div><div><span><span>                        </span></span><span>);</span></div></div><div><div><div>20</div></div><div><span>                        </span><span>// Emit the value anyway so the stack stays balanced.</span></div></div><div><div><div>21</div></div><div><span>                        </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>22</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>target.as_ref()</code> 是 <code>Box&lt;Expr&gt;</code> 到 <code>&amp;Expr</code> 的转换（第 5 篇的 <code>Box</code>）。<code>let xs = [1, 2]; xs[0] = 9; xs;</code> 的清单：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode lists.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 1</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 2</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>2 "xs"</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>3 0</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>4 9</span></div></div><div><div><div>8</div></div><div><span>== code ==</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>0 Const 0</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>1 Const 1</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>2 BuildList 2</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>3 DefineName 2</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>4 GetName 2</span></div></div><div><div><div>14</div></div><div><span><span>     </span></span><span>5 Const 3</span></div></div><div><div><div>15</div></div><div><span><span>     </span></span><span>6 Const 4</span></div></div><div><div><div>16</div></div><div><span><span>     </span></span><span>7 SetIndex</span></div></div><div><div><div>17</div></div><div><span><span>     </span></span><span>8 Pop</span></div></div><div><div><div>18</div></div><div><span><span>     </span></span><span>9 GetName 2</span></div></div><div><div><div>19</div></div><div><span>[9, 2]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意 <code>SetIndex</code> 前面压了三样东西：容器、下标、值。它改的是<strong>同一个列表</strong>（第 8 篇，列表是 <code>Rc&lt;RefCell&lt;..&gt;&gt;</code>，共享引用），所以最后读 <code>xs</code> 看到 <code>[9, 2]</code>。</p><section><h3>for 循环：脱糖<a href="#for-循环脱糖"><span>#</span></a></h3><p>AST 里有 <code>for x in iter { body }</code>，字节码里没有对应的指令。我选择在编译期把它<strong>脱糖</strong>（desugar）成已有的东西：一个计数器循环。Pebble 里等价的写法是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let #iter = &lt;iter&gt;;</span></div></div><div><div><div>2</div></div><div><span>let #idx = 0;</span></div></div><div><div><div>3</div></div><div><span>while #idx &lt; len(#iter) {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>let x = #iter[#idx];</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>&lt;body&gt;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>#idx = #idx + 1;</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>#iter</code> 和 <code>#idx</code> 是我在这里写的伪名字，真实代码里它们是 <code>" #iter"</code> 和 <code>" #idx"</code>，<strong>带一个前导空格</strong>：Pebble 的标识符里不可能有空格，所以它们不会和用户的变量撞名。<code>continue</code> 要跳到”自增”那一段而不是循环顶（否则会死循环），所以 <code>compile_for</code> 要自己管 <code>LoopCtx</code>。下面是完整的 <code>compile_for</code>，分段放进 <code>impl Compiler</code>。准备工作：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Desugars `for x in iter { body }` into a hidden counter loop.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>compile_for</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>var</span><span><span>: &amp;</span><span>pebble_ast</span><span>::</span></span><span>Ident</span><span>, </span><span>iter</span><span>: &amp;</span><span>Expr</span><span>, </span><span>body</span><span>: &amp;[</span><span>Stmt</span><span>], </span><span>span</span><span>: </span><span>Span</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>// Names that cannot collide with user identifiers (they contain a</span></div></div><div><div><div>4</div></div><div><span>        </span><span>// space), used for the hidden iterator and index.</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>iter_name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(</span><span>" #iter"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>index_name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(</span><span>" #idx"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>zero</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>intern</span><span><span>(</span><span>Constant</span><span>::</span></span><span>Int</span><span>(</span><span>0</span><span>));</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>one</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>intern</span><span><span>(</span><span>Constant</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>len_name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(</span><span>"len"</span><span>);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span>.</span><span>compile_expr</span><span>(</span><span>iter</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>DefineName</span><span>(</span><span>iter_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Const</span><span>(</span><span>zero</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>DefineName</span><span>(</span><span>index_name</span><span>), </span><span>span</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>循环头：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>let</span><span> </span><span>loop_start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>2</div></div><div><span>        </span><span>// `#idx &lt; len(#iter)`: callee first, then its argument.</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>GetName</span><span>(</span><span>index_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>GetName</span><span>(</span><span>len_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>GetName</span><span>(</span><span>iter_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Call</span><span>(</span><span>1</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Less</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>exit</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.chunk.</span><span>placeholder</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>JumpIfFalse</span><span>(</span><span>0</span><span>), </span><span>span</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>循环体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>self</span><span>.loops.</span><span>push</span><span><span>(</span><span>LoopCtx</span><span>::</span></span><span>default</span><span>());</span></div></div><div><div><div>2</div></div><div><span>        </span><span>// `let x = #iter[#idx];`</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>GetName</span><span>(</span><span>iter_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>GetName</span><span>(</span><span>index_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Index</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>var_name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>name_const</span><span>(&amp;</span><span>var</span><span>.name);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>DefineName</span><span>(</span><span>var_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>body</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>self</span><span>.</span><span>compile_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>自增、回边和回填：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>let</span><span> </span><span>increment</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>2</div></div><div><span>        </span><span>// `#idx = #idx + 1`</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>GetName</span><span>(</span><span>index_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Const</span><span>(</span><span>one</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Add</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>SetName</span><span>(</span><span>index_name</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Pop</span><span>, </span><span>span</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Jump</span><span>(</span><span>loop_start</span><span> </span><span>as</span><span> </span><span>u32</span><span>), </span><span>span</span><span>);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>here</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>exit</span><span>, </span><span>end</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>ctx</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.loops.</span><span>pop</span><span>().</span><span>expect</span><span>(</span><span>"loop context"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>for</span><span> </span><span>jump</span><span> </span><span>in</span><span> </span><span>ctx</span><span>.break_jumps {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>jump</span><span>, </span><span>end</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>        </span><span>for</span><span> </span><span>jump</span><span> </span><span>in</span><span> </span><span>ctx</span><span>.continue_jumps {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>self</span><span>.chunk.</span><span>patch_jump</span><span>(</span><span>jump</span><span>, </span><span>increment</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>最后在 <code>compile_stmt</code> 里加分支，把 <code>Stmt::For</code> 交给它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>For</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>var</span><span>, </span><span>iter</span><span>, </span><span>body</span><span>, ..</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>} =&gt; </span><span>self</span><span>.</span><span>compile_for</span><span>(</span><span>var</span><span>, </span><span>iter</span><span>, </span><span>body</span><span>, </span><span>span</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现在 <code>compile_stmt</code> 和 <code>compile_expr</code> 的两个 <code>match</code> 都穷尽了 <code>Stmt</code> 和 <code>Expr</code> 的每个变体，把 <code>_ =&gt; todo!("statement")</code> 和 <code>_ =&gt; todo!("expression")</code> 两个兜底分支删掉（留着它们是不可达分支，会有警告，也和 tag 对不上）。</p><p><code>range(3)</code> 上的真实清单（<code>for i in range(3) { print(i); }</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --dump-bytecode for.pebble</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>0 " #iter"</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>1 " #idx"</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>2 0</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>3 1</span></div></div><div><div><div>7</div></div><div><span><span>     </span></span><span>4 "len"</span></div></div><div><div><div>8</div></div><div><span><span>     </span></span><span>5 "range"</span></div></div><div><div><div>9</div></div><div><span><span>     </span></span><span>6 3</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>7 "i"</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>8 "print"</span></div></div><div><div><div>12</div></div><div><span>== code ==</span></div></div><div><div><div>13</div></div><div><span><span>     </span></span><span>0 GetName 5</span></div></div><div><div><div>14</div></div><div><span><span>     </span></span><span>1 Const 6</span></div></div><div><div><div>15</div></div><div><span><span>     </span></span><span>2 Call 1</span></div></div><div><div><div>16</div></div><div><span><span>     </span></span><span>3 DefineName 0</span></div></div><div><div><div>17</div></div><div><span><span>     </span></span><span>4 Const 2</span></div></div><div><div><div>18</div></div><div><span><span>     </span></span><span>5 DefineName 1</span></div></div><div><div><div>19</div></div><div><span><span>     </span></span><span>6 GetName 1</span></div></div><div><div><div>20</div></div><div><span><span>     </span></span><span>7 GetName 4</span></div></div><div><div><div>21</div></div><div><span><span>     </span></span><span>8 GetName 0</span></div></div><div><div><div>22</div></div><div><span><span>     </span></span><span>9 Call 1</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>10 Less</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>11 JumpIfFalse -&gt; 26</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>12 GetName 0</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>13 GetName 1</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>14 Index</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>15 DefineName 7</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>16 GetName 8</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>17 GetName 7</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>18 Call 1</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>19 Pop</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>20 GetName 1</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>21 Const 3</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>22 Add</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>23 SetName 1</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>24 Pop</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>25 Jump -&gt; 6</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>26 Nil</span></div></div><div><div><div>40</div></div><div><span>0</span></div></div><div><div><div>41</div></div><div><span>1</span></div></div><div><div><div>42</div></div><div><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读它：0 到 2 是 <code>range(3)</code>（被调用者 <code>range</code> 在先，参数 <code>3</code> 在后，<code>Call 1</code>），结果存进隐藏变量 <code>" #iter"</code>（3），计数器 <code>" #idx"</code> 置 0（4、5）。循环头从 6 开始：<code>#idx</code>、<code>len</code>、<code>#iter</code>，<code>Call 1</code> 算出 <code>len(#iter)</code>，<code>Less</code> 比较，不成立就跳到 26（11）。12 到 15 是 <code>let i = #iter[#idx];</code>，16 到 19 是 <code>print(i);</code>，20 到 24 是自增（<code>SetName</code> 之后那条 <code>Pop</code> 把赋值表达式的值扔掉），25 是回边，指向 6，也就是循环头。注意常量表里的 <code>" #iter"</code> 和 <code>" #idx"</code> 带着引号和前导空格，一眼就看得出它们是名字，而且是用户写不出来的名字。</p><p>头上那五条指令（6 到 10）有一个顺序，代码注释里用大白话写着：“callee first, then its argument”。这个顺序是整个 <code>for</code> 里最脆弱的一处。</p></section><section><h3>弄坏它，看测试漏掉什么<a href="#弄坏它看测试漏掉什么"><span>#</span></a></h3><p>我想知道这个顺序要是写反了会怎样，于是在一个临时的 worktree 里（<code>git worktree add /tmp/pebble-repro-10 tutorial-v1 --detach</code>，和 tag 一模一样的代码，用完即删）把 <code>compile_for</code> 里这两行对调：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>self.emit(Opcode::GetName(index_name), span);</span></div></div><div><div><div>2</div></div><div><span>self.emit(Opcode::GetName(len_name), span);</span></div></div><div><div><div>3</div></div><div><span>self.emit(Opcode::GetName(iter_name), span);</span></div></div><div><div><div>4</div></div><div><span>self.emit(Opcode::GetName(len_name), span);</span></div></div><div><div><div>5</div></div><div><span>self.emit(Opcode::Call(1), span);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现在栈上是 <code>[#idx, #iter, len]</code>，<code>Call 1</code> 会把”参数下面的那个东西”当被调用者，也就是 <code>#iter</code>，一个列表。用第 12 篇才会正式介绍的 <code>--vm</code> 开关跑（tag 里已经有了，这里只借来一用），真实输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --vm for.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E2001]: list is not callable</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; for.pebble:1:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>1 | for i in range(3) {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>“list is not callable”。信息是对的，一眼就能推出”它把列表当函数调用了”，而这只有在我知道前面讲的栈布局的前提下才有用。然后是重点：同一份被弄坏的代码，我把测试跑了一遍。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bytecode</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>4</div></div><div><span>...</span></div></div><div><div><div>5</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>6</div></div><div><span>$ cargo test -p pebble-vm</span></div></div><div><div><div>7</div></div><div><span>...</span></div></div><div><div><div>8</div></div><div><span>test tests::control_flow_matches_the_tree_walker ... FAILED</span></div></div><div><div><div>9</div></div><div><span>...</span></div></div><div><div><div>10</div></div><div><span>test result: FAILED. 13 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>我们这个 crate 的 14 个测试一个都没红。</strong> <code>for_loop_desugars_to_indexing</code> 只检查清单里有没有 <code>BuildList</code>、<code>Index</code>、<code>Less</code> 这三个子串，它们当然都在，只是顺序错了。抓到它的是还没写的 VM 里的一个测试 <code>control_flow_matches_the_tree_walker</code>，它的失败信息是（节选）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>called `Result::unwrap()` on an `Err` value: "error[E2001]: list is not callable\n --&gt; &lt;test&gt;:3:13\n  |\n3 |             for i in range(10) {\n  |             ^^^^^^^^^^^^^^^^^^^^\n  |\n"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是前面”子串测试看不见顺序”的兑现。要堵这个洞，更严格的做法是像第一版 <code>compiles_arithmetic</code> 那样直接比较指令序列。我没有在仓库里这么做（练习里你可以试试），我留下的是现状：这个 crate 的单元测试只能证明”编译器吐出了这些种类的指令”，证明”这些指令顺序正确”要靠执行它们的测试。这也是为什么第 12 篇要写一套<strong>差分测试</strong>：同一个程序在两个后端跑，结果必须一致。</p></section><section><h3>两个我自己撞上的毛病<a href="#两个我自己撞上的毛病"><span>#</span></a></h3><p>写这一篇的时候，我又用 <code>--vm</code> 和树遍历解释器对着跑了几个 <code>for</code>，发现了两个真实的毛病。两个都在 tag 上，我没有去修，因为这篇文章的工作是解释这个编译器，不是改它。</p><p>第一个，嵌套的 <code>for</code>。隐藏变量名是固定的 <code>" #iter"</code> 和 <code>" #idx"</code>，两层循环用<strong>同一对名字</strong>，<code>DefineName</code> 又都定义在同一个环境里，里层循环会覆盖外层的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for i in [1, 2] {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>for j in [10, 20] {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>print(i * j);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --vm nested.pebble</span></div></div><div><div><div>2</div></div><div><span>10</span></div></div><div><div><div>3</div></div><div><span>20</span></div></div><div><div><div>4</div></div><div><span>$ pebble run nested.pebble</span></div></div><div><div><div>5</div></div><div><span>10</span></div></div><div><div><div>6</div></div><div><span>20</span></div></div><div><div><div>7</div></div><div><span>20</span></div></div><div><div><div>8</div></div><div><span>40</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>VM（吃的是我们这一篇的字节码）只打印了两行。里层循环跑完时，<code>" #iter"</code> 已经是里层的 <code>[10, 20]</code>，<code>" #idx"</code> 是 <code>2</code>，外层先自增（2 变成 3），回到循环头检查 <code>3 &lt; len([10, 20])</code>，不成立，退出。树遍历解释器每个 <code>for</code> 有自己的作用域，没这个问题。</p><p>第二个：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let len = 3;</span></div></div><div><div><div>2</div></div><div><span>for x in [10, 20] {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>print(x);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run --vm shadow.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E2001]: int is not callable</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; shadow.pebble:2:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>2 | for x in [10, 20] {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>$ pebble run shadow.pebble</span></div></div><div><div><div>9</div></div><div><span>10</span></div></div><div><div><div>10</div></div><div><span>20</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>for</code> 的脱糖调用的是名叫 <code>len</code> 的<strong>全局变量</strong>，用户把它盖掉了，循环就坏了。这两个毛病的修法都明摆着：加一条专门的迭代指令，或者给隐藏名字加上嵌套深度。但两者都是这类”脱糖在字节码层面生成了用户能碰到的东西”的问题，说明这个设计的代价：没有在语言层面引入新概念，换来的是编译器偷偷占用了名字空间。第 12 篇会拿它们当差分测试的素材。</p></section></section>
<section><h2>补全：测试、Debug 和 operand_bytes<a href="#补全测试debug-和-operand_bytes"><span>#</span></a></h2><p>函数和闭包的两个测试放在一起，它们守着清单里的 <code>MakeClosure</code> 和嵌套函数的标题：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>functions_become_constants_and_closures</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"fn add(a, b) { return a + b; } add(1, 2);"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>listing</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"MakeClosure"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"-- function add --"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"Return"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>fn</span><span> </span><span>closures_capture_implicitly_by_name</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile_src</span><span>(</span><span>"let f = fn(x) { return x; };"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>listing</span><span><span> </span><span>=</span><span> </span></span><span>compiled</span><span>.</span><span>chunk</span><span>().</span><span>disassemble</span><span>();</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert!</span><span>(</span><span>listing</span><span>.</span><span>contains</span><span>(</span><span>"-- function &lt;anonymous&gt; --"</span><span>), </span><span>"{listing}"</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>for_loop_desugars_to_indexing</code> 前面讲过它只查三个子串，从 tag 里照抄。</p><p><code>FunctionProto</code> 和 <code>Chunk</code> 的手写 <code>Debug</code> 也从 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bytecode/src/lib.rs" target="_blank">tag 里</a> 抄（<code>impl fmt::Debug for FunctionProto</code> 和 <code>impl fmt::Debug for Chunk</code>）。它们都用 <code>f.debug_struct("Name").field(..).finish()</code>，只打印”有几条指令、几个常量”，不把整个指令序列倒出来。派生版本会把每条指令和每个 <code>Span</code> 都打印出来，一个 <code>assert_eq!</code> 失败就是几屏输出。这两个也照抄，不然最后的 <code>diff</code> 不干净（没有它们代码照样能编译，它们只在你打印调试输出时才起作用）。</p><p><code>Chunk::push</code> 也要抄，下面的 <code>chunk!</code> 要用它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Appends an instruction with no source span.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Useful when building chunks programmatically; the VM only uses the span</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// to attribute a runtime error, so a placeholder is fine.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>push</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>op</span><span>: </span><span>Opcode</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span>(</span><span>op</span><span><span>, </span><span>Span</span><span>::</span></span><span>DUMMY</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是给”手工拼一个 <code>Chunk</code>”用的：没有源码，所以 span 是 <code>Span::DUMMY</code>（第 1 篇）。下面的宏要用它。</p><section><h3>operand_bytes：把 8 字节的代价算出来<a href="#operand_bytes把-8-字节的代价算出来"><span>#</span></a></h3><p>前面说过 <code>Opcode</code> 占 8 字节，一个紧凑的编码只要 5。仓库里有一个小函数把”带操作数的指令会占多少字节的操作数”算出来，让这笔账有地方可看：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Opcode</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Number of bytes of inline operand this instruction would carry in a</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// packed encoding. Pebble stores operands in the enum, but a real bytecode</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// format flattens them into the instruction stream, so this is here to make</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// that cost visible.</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>operand_bytes</span><span>(</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>DefineName</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>GetName</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>SetName</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>MakeClosure</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>Jump</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfFalse</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfFalseKeep</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>JumpIfTrueKeep</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>Call</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>BuildList</span><span>(</span><span>_</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Opcode</span><span>::</span><span>BuildMap</span><span>(</span><span>_</span><span>) =&gt; </span><span>4</span><span>,</span></div></div><div><div><div>21</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>0</span><span>,</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>十二个带载荷的变体用或模式合成一个分支，<code>_</code> 管剩下的二十个。这个 <code>_</code> 通配有代价：以后新加一个带操作数的变体，忘了把它写进来，编译器<strong>不会</strong>提醒你（前面 <code>arithmetic_opcode</code> 没有通配分支，刚好相反）。我没有一个干净的办法既保留”其余全是 0”又让编译器检查，这是个取舍：这个函数只是个说明，不是正确性的一部分，所以我选了短的写法。它也是 <code>const fn</code>，<code>self</code> 按值传（<code>Opcode</code> 是 <code>Copy</code> 的）。测试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>operand_bytes_accounts_for_operands</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>0</span><span>).</span><span>operand_bytes</span><span>(), </span><span>4</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>Opcode</span><span>::</span><span>Add</span><span>.</span><span>operand_bytes</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>Opcode</span><span>::</span><span>Add</span><span>.</span><span>name</span><span>(), </span><span>"Add"</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一行顺带用 <code>name()</code> 检查了宏生成的东西。</p></section></section>
<section><h2>macro_rules!：chunk!<a href="#macro_ruleschunk"><span>#</span></a></h2><p>测试里，我要手工拼一个 <code>Chunk</code>：<code>Nil</code>、<code>Const 0</code>、<code>Return</code>。不用宏的话是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>chunk</span><span><span> </span><span>=</span><span> </span></span><span>Chunk</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>2</div></div><div><span>chunk</span><span>.</span><span>push</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>3</div></div><div><span>chunk</span><span>.</span><span>push</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Const</span><span>(</span><span>0</span><span>));</span></div></div><div><div><div>4</div></div><div><span>chunk</span><span>.</span><span>push</span><span><span>(</span><span>Opcode</span><span>::</span></span><span>Return</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>重复的、模式固定的东西，又是宏的活。这次用 Rust 的第二套宏系统，<strong>声明式宏</strong> <code>macro_rules!</code>，不需要新 crate，不需要 <code>syn</code>，写在 <code>pebble-bytecode</code> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[macro_export]</span></div></div><div><div><div>2</div></div><div><span>macro_rules!</span><span> </span><span>chunk</span><span> {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>($($</span><span>op</span><span>:</span><span>expr</span><span>),* $(,)?) =&gt; {{</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>chunk</span><span><span> </span><span>=</span><span> $</span></span><span>crate</span><span>::</span><span>Chunk</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>$(</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>$</span><span>crate</span><span>::</span><span>Chunk</span><span>::</span><span>push</span><span>(&amp;</span><span>mut</span><span> </span><span>chunk</span><span>, $</span><span>op</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>)*</span></div></div><div><div><div>8</div></div><div><span>        </span><span>chunk</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}};</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用它：<code>chunk![Opcode::Nil, Opcode::Const(0), Opcode::Return]</code> 就是上面那四行。</p><p><code>macro_rules!</code> 的结构是一串”模式 =&gt; 模板”的规则，调用时编译器依次尝试，第一条匹配上的规则生效。这里只有一条规则，逐段读：</p><ul>
<li><code>$op:expr</code> 是一个<strong>捕获</strong>：<code>$op</code> 是名字，<code>expr</code> 是<strong>片段说明符</strong>，意思是”这里要有一个完整的表达式”。常见的还有 <code>ident</code>（标识符）、<code>ty</code>（类型）、<code>tt</code>（单个 token 树，最灵活）、<code>pat</code>（模式）。</li>
<li><code>$( ... ),*</code> 是<strong>重复</strong>：括号里的模式重复零次或多次，用逗号分隔。</li>
<li><code>$(,)?</code> 是<strong>可选的尾逗号</strong>：<code>?</code> 表示零次或一次。</li>
</ul><p>模板里 <code>$( ... )*</code> 的对应关系：每匹配到一个 <code>$op</code>，把括号里的内容展开一次，<code>$op</code> 换成那个表达式。所以三个操作码就展开三行 <code>push</code>。</p><p>外面为什么是双层大括号 <code>{{ ... }}</code>？第一层是宏规则本身的分隔符（外层的大括号你也可以写成圆括号或方括号，规则的语法要求有一对），第二层大括号是模板里的<strong>块表达式</strong>：<code>{ let mut chunk = ...; ...; chunk }</code> 求值为最后那个 <code>chunk</code>。要是只写一层，展开出来是一串语句，没法当表达式用，<code>let c = chunk![...]</code> 就不合法了。</p><section><h3>为什么 <code>expr</code> 比 C 的宏强<a href="#为什么-expr-比-c-的宏强"><span>#</span></a></h3><p>C 程序员对宏的恐惧是有历史原因的。<code>#define CHUNK_PUSH(c, op) ...</code> 做的是<strong>文本替换</strong>：<code>MAX(a++, b)</code> 会把 <code>a++</code> 复制进展开结果两次，你得给每个参数加括号才能防止优先级出错。<code>macro_rules!</code> 不一样：<code>$op:expr</code> 先由编译器把调用处的 token 解析成一个完整的表达式，再原样放进模板，优先级和副作用都不会变：<code>chunk![Opcode::Const(1 + 2)]</code> 里，<code>$op</code> 绑定的是 <code>Opcode::Const(1 + 2)</code> 这一整个表达式，不会被拆开。</p></section><section><h3>卫生性<a href="#卫生性"><span>#</span></a></h3><p>另一件事：模板里的 <code>let mut chunk</code> 和调用者可能也有一个叫 <code>chunk</code> 的变量。不会冲突。实验室里 <code>ch10_hygiene.rs</code>，一个宏在内部定义 <code>x</code>，调用处想用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>macro_rules!</span><span> </span><span>make_x</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>() =&gt; {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>x</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>make_x!</span><span>();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{x}"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0425]: cannot find value `x` in this scope</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch10_hygiene.rs:9:16</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>9 |     println!("{x}");</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                ^ not found in this scope</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>help: an identifier with the same name is defined here, but is not accessible due to macro hygiene</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch10_hygiene.rs:3:13</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>3 |         let x = 1;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|             ^</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div><div><div><div>13</div></div><div><span>8 |     make_x!();</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|     --------- in this macro invocation</span></div></div><div><div><div>15</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器甚至解释了原因：宏里的 <code>x</code> 和调用处的 <code>x</code> 是<strong>两个不同的变量</strong>，哪怕名字一样。这叫<strong>卫生性</strong>（hygiene）。反过来也成立：<code>ch10_hygiene_ok.rs</code> 里，宏内部有 <code>let x = $e; x + x</code>，调用者也有自己的 <code>x</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch10_hygiene_ok</span></div></div><div><div><div>2</div></div><div><span>22</span></div></div><div><div><div>3</div></div><div><span>10</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>double!(x + 1)</code> 里调用者的 <code>x</code> 是 10，宏里的 <code>x</code> 是 11，结果 22；调用者的 <code>x</code> 还是 10。C 的 <code>#define</code> 做不到这点：展开后两个 <code>x</code> 就是同一个变量。过程宏没有这种卫生性：<code>quote!</code> 生成的 token 会原样落在调用处。所以第 6 篇里要写绝对路径 <code>::pebble_span::Spanned</code>，不依赖调用处有没有 <code>use</code>，也不怕调用处恰好有同名的东西，这是过程宏里避免名字冲突的办法。</p></section><section><h3>尾逗号<a href="#尾逗号"><span>#</span></a></h3><p><code>$(,)?</code> 看起来像装饰，其实不是。实验室里 <code>ch10_trailing_comma.rs</code> 把它去掉：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>macro_rules!</span><span> </span><span>list</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>($($</span><span>item</span><span>:</span><span>expr</span><span>),*) =&gt; {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>vec!</span><span>[$($</span><span>item</span><span>),*]</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调用 <code>list![1, 2, 3,]</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: unexpected end of macro invocation</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch10_trailing_comma.rs:8:27</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>1 | macro_rules! list {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>| ----------------- when calling this macro</span></div></div><div><div><div>6</div></div><div><span>...</span></div></div><div><div><div>7</div></div><div><span>8 |     let v = list![1, 2, 3,];</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|                           ^ missing tokens in macro arguments</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>note: while trying to match meta-variable `$item:expr`</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch10_trailing_comma.rs:2:8</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>2 |     ($($item:expr),*) =&gt; {</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|        ^^^^^^^^^^</span></div></div><div><div><div>15</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>最后一个逗号之后，模式期待下一个 <code>$item:expr</code>，没有了。加上 <code>$(,)?</code> 就行（<code>ch10_trailing_comma_ok.rs</code>，同时试了空列表）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch10_trailing_comma_ok</span></div></div><div><div><div>2</div></div><div><span>[1, 2, 3] []</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>vec![1, 2, 3,]</code> 能写尾逗号，标准库的 <code>vec!</code> 就是这么做的。</p></section><section><h3><code>$crate</code> 和 <code>#[macro_export]</code><a href="#crate-和-macro_export"><span>#</span></a></h3><p>模板里写的是 <code>$crate::Chunk::new()</code>，不是 <code>Chunk::new()</code>。<code>$crate</code> 展开成”<strong>定义这个宏的 crate</strong>”的路径。如果写成 <code>Chunk::new()</code>，在另一个 crate 里调用 <code>chunk!</code>，调用处得恰好 <code>use</code> 了 <code>Chunk</code> 才行；用 <code>$crate</code> 就和调用处的 <code>use</code> 无关。（其实我用 <code>chunk.push($op)</code> 的方法调用写法也成立，用完全限定的 <code>$crate::Chunk::push(&amp;mut chunk, $op)</code> 是我给”必须在任意作用域里都能工作的宏”养成的习惯，不是这里非用不可。）</p><p><code>#[macro_export]</code> 让宏出现在 <strong>crate 的根上</strong>，调用者写 <code>use pebble_bytecode::chunk;</code> 或者 <code>pebble_bytecode::chunk![...]</code>，不管它在源文件的哪个模块里。没有它，宏只在<strong>定义之后</strong>的同一个模块（和子模块）里可见，<code>macro_rules!</code> 是按文本顺序生效的，这是它和普通函数最不一样的地方。</p></section><section><h3>文档测试<a href="#文档测试"><span>#</span></a></h3><p>宏上面那段 <code>///</code> 文档里有一个代码块，就是文档测试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Builds a [`Chunk`] from a list of opcodes.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// ```</span></div></div><div><div><div>4</div></div><div><span>/// use pebble_bytecode::{chunk, Opcode};</span></div></div><div><div><div>5</div></div><div><span>///</span></div></div><div><div><div>6</div></div><div><span>/// let chunk = chunk![Opcode::Nil, Opcode::True, Opcode::Return];</span></div></div><div><div><div>7</div></div><div><span>/// assert_eq!(chunk.len(), 3);</span></div></div><div><div><div>8</div></div><div><span>/// ```</span></div></div><div><div><div>9</div></div><div><span>#[macro_export]</span></div></div><div><div><div>10</div></div><div><span>macro_rules!</span><span> </span><span>chunk</span><span> {</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo test</code> 会把文档里的 <code>```</code> 代码块提取出来，每个包进一个 <code>fn main</code>，作为<strong>一个独立的、依赖这个 crate 的外部程序</strong>编译并运行。所以它要写 <code>use pebble_bytecode::{chunk, Opcode};</code>：它和别的 crate 看到的世界一样。这有两层意义：文档里的示例永远不会过期（一旦 API 变了，测试就红），以及它是对公共 API 的一次真实检验，尤其对宏：宏在别的 crate 里能不能用，<code>$crate</code> 有没有写对，只有 doc test 能看见。输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bytecode --doc</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Doc-tests pebble_bytecode</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>running 1 test</span></div></div><div><div><div>5</div></div><div><span>test crates/pebble-bytecode/src/lib.rs - chunk (line 353) ... ok</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>all doctests ran in 0.50s; merged doctests compilation took 0.50s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>单元测试里也有一个：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>chunk_macro_builds_a_chunk</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>chunk</span><span><span> </span><span>=</span><span> </span></span><span>chunk!</span><span>[</span><span>Opcode</span><span>::</span><span>Nil</span><span>, </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>0</span><span>), </span><span>Opcode</span><span>::</span><span>Return</span><span>];</span></div></div><div><div><div>4</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>chunk</span><span>.</span><span>len</span><span>(), </span><span>3</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>chunk</span><span>.</span><span>code</span><span>()[</span><span>0</span><span>], </span><span>Opcode</span><span>::</span><span>Nil</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>chunk</span><span>.</span><span>code</span><span>()[</span><span>1</span><span>], </span><span>Opcode</span><span>::</span><span>Const</span><span>(</span><span>0</span><span>));</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>有三处要和 tag 对齐，<code>diff</code> 才会干净。第一，<code>opcode_set!</code> 里指令的<strong>顺序</strong>，最终是这样（每条的文档注释就是前面各节里写的那一句）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Const, Nil, True, False, Pop, DefineName, GetName, SetName, MakeClosure,</span></div></div><div><div><div>2</div></div><div><span>Add, Sub, Mul, Div, Rem, Neg, Not, Equal, NotEqual, Less, LessEqual,</span></div></div><div><div><div>3</div></div><div><span>Greater, GreaterEqual, Jump, JumpIfFalse, JumpIfFalseKeep, JumpIfTrueKeep,</span></div></div><div><div><div>4</div></div><div><span>Call, Return, BuildList, BuildMap, Index, SetIndex</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二，文件里条目的<strong>排列</strong>。从上到下：<code>opcode_set!</code>、<code>impl Opcode</code>（<code>operand_bytes</code>）、<code>Constant</code> 和它的两个 <code>impl</code>（<code>same</code>、<code>Debug</code>）、<code>FunctionProto</code> 和它的两个 <code>impl</code>、<code>Chunk</code>、<code>impl Chunk</code>、<code>Chunk</code> 的 <code>Debug</code>、<code>chunk!</code> 宏、<code>Compiled</code>、<code>CompileResult</code>、<code>compile</code>、<code>LoopCtx</code>、<code>Compiler</code>、<code>arithmetic_opcode</code>，最后是测试。<code>impl Chunk</code> 里的方法：<code>new</code>、<code>emit</code>、<code>push</code>、<code>placeholder</code>、<code>patch_jump</code>、<code>add_constant</code>、<code>intern</code>、<code>code</code>、<code>constants</code>、<code>span_at</code>、<code>len</code>、<code>is_empty</code>、<code>disassemble</code>、<code>disassemble_into</code>。<code>impl Compiler</code> 里：<code>new</code>、<code>here</code>、<code>emit</code>、<code>name_const</code>、<code>compile_body</code>、<code>compile_stmt</code>、<code>compile_for</code>、<code>compile_expr</code>、<code>compile_function</code>。第三，注释：文件头的模块文档、<code>Opcode</code> 的文档注释、<code>compile_body</code> 的文档注释和 <code>// --- statements ---</code> 这样的分节线，我前面写得很简略，tag 里的更长一些。注释不影响行为，直接用 tag 里的文字。</p><p>照着上面补齐、排好以后，<code>crates/pebble-bytecode/src/lib.rs</code> 就是 tag 里那个文件。对一下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> </span><span>f</span><span> </span><span>in</span><span> </span><span>crates/pebble-bytecode/Cargo.toml</span><span> </span><span>crates/pebble-bytecode/src/lib.rs</span><span>; </span><span>do</span></div></div><div><div><div>2</div></div><div><span>  </span><span>diff</span><span> </span><span>&lt;(</span><span>git</span><span> </span><span>-C</span><span> ~/pebble-tag show tutorial-v1:</span><span>$f</span><span>)</span><span> </span><span>$f</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>"same  </span><span>$f</span><span>"</span></div></div><div><div><div>3</div></div><div><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>same  crates/pebble-bytecode/Cargo.toml</span></div></div><div><div><div>2</div></div><div><span>same  crates/pebble-bytecode/src/lib.rs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bytecode</span></div></div><div><div><div>2</div></div><div><span>running 14 tests</span></div></div><div><div><div>3</div></div><div><span>test tests::break_outside_a_loop_is_a_diagnostic ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::chunk_macro_builds_a_chunk ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::break_and_continue_are_patched ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::closures_capture_implicitly_by_name ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::constants_are_deduplicated ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::if_compiles_to_a_patched_forward_jump ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::for_loop_desugars_to_indexing ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::compiles_arithmetic ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::trailing_expression_is_the_program_value ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::functions_become_constants_and_closures ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::while_back_edge_points_backwards ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::operand_bytes_accounts_for_operands ... ok</span></div></div><div><div><div>15</div></div><div><span>test tests::non_trailing_expression_statements_are_popped ... ok</span></div></div><div><div><div>16</div></div><div><span>test tests::logical_operators_short_circuit_with_keeping_jumps ... ok</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>19</div></div><div><span>...</span></div></div><div><div><div>20</div></div><div><span>running 1 test</span></div></div><div><div><div>21</div></div><div><span>test crates/pebble-bytecode/src/lib.rs - chunk (line 353) ... ok</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>
<section><h2>命令行第三版，第二半：check 也编译<a href="#命令行第三版第二半check-也编译"><span>#</span></a></h2><p><code>pebble check</code> 的使命是”报告问题，别的什么也不做”。以前它只解析；现在编译器也能报诊断了（<code>break</code> 在循环外面，非法的赋值目标），所以让它也编译一遍。<code>crates/pebble-cli/src/commands.rs</code> 里的 <code>check</code> 变成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// `pebble check`: lex, parse, and compile a file, reporting diagnostics and</span></div></div><div><div><div>2</div></div><div><span>/// nothing else.</span></div></div><div><div><div>3</div></div><div><span>///</span></div></div><div><div><div>4</div></div><div><span>/// # Errors</span></div></div><div><div><div>5</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>6</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>check</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>compiled</span><span><span> </span><span>=</span><span> </span></span><span>compile</span><span>(&amp;</span><span>parsed</span><span>.program);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>failed</span><span><span> </span><span>|=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>compiled</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>failed |= ...</code> 是”或等于”，布尔值的复合赋值。注意两次 <code>emit</code> 都会执行：不能写 <code>a || b</code>，那会在 <code>a</code> 为真时短路，漏掉编译器的诊断。我不管解析有没有错都继续编译，这样一次 <code>check</code> 能把两个阶段的问题都报告出来；解析器已经做了错误恢复（第 7 篇），拿到的 AST 是可编译的。试一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble check badbreak.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E3001]: `break` outside a loop</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; badbreak.pebble:2:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>2 | break;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>| ^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>$ pebble check examples/errors.pebble</span></div></div><div><div><div>9</div></div><div><span>error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>--&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>badbreak.pebble</code> 是 <code>let n = 3;</code> 加一行 <code>break;</code>。）<code>errors.pebble</code> 的输出里只有解析器那一条，编译器没有在它残缺的 AST 上凭空编出新的错误。<code>pebble run --dump-bytecode badbreak.pebble</code> 对同一个文件给同一条诊断，退出码是 1，不会往下执行。</p><p><code>sources.rs</code> 的模块文档里那句话也改一个词：<code>the lexer and parser</code> 改成 <code>the lexer/parser/compiler</code>（和 tag 一致，这就是这个文件和 tag 完全相同的原因，它从这一版起不会再变了）。</p><p>两个新的集成测试，写进 <code>crates/pebble-cli/tests/cli.rs</code> 的末尾。第一个检查 <code>--dump-bytecode</code> 真的打印了清单（含嵌套的函数标题）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>dump_bytecode_prints_the_listing</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"run"</span><span>, </span><span>"--quiet"</span><span>, </span><span>"--dump-bytecode"</span><span>, </span><span>"examples/fib.pebble"</span><span>])</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>.</span><span>success</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>.</span><span>stdout</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>predicate</span><span>::</span><span>str</span><span>::</span><span>contains</span><span>(</span><span>"== code =="</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>.</span><span>and</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"-- function fib --"</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>.</span><span>and</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"MakeClosure"</span><span>)),</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二个需要一个有编译错误的文件。我不想往仓库里再加一个示例文件，所以让测试自己在临时目录里造一个，用完删掉：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>check_reports_compile_errors</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>path</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>env</span><span>::</span></span><span>temp_dir</span><span>().</span><span>join</span><span>(</span><span>format!</span><span>(</span><span>"pebble-break-{}.pebble"</span><span><span>, </span><span>std</span><span>::</span><span>process</span><span>::</span></span><span>id</span><span>()));</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>std</span><span>::</span><span>fs</span><span>::</span><span>write</span><span>(&amp;</span><span>path</span><span>, </span><span>"break;</span><span>\n</span><span>"</span><span>).</span><span>expect</span><span>(</span><span>"the temp file is writable"</span><span>);</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>.</span><span>arg</span><span>(</span><span>"check"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>.</span><span>arg</span><span>(&amp;</span><span>path</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>.</span><span>failure</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>.</span><span>stderr</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"`break` outside a loop"</span><span>));</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>std</span><span>::</span><span>fs</span><span>::</span><span>remove_file</span><span>(&amp;</span><span>path</span><span>).</span><span>expect</span><span>(</span><span>"the temp file is removable"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文件名里带进程号，免得两个并行的测试进程互相踩。<code>fs::write</code>、<code>fs::remove_file</code> 都返回 <code>Result</code>，在测试里直接 <code>expect</code>（第 7 篇：测试里 <code>panic</code> 是合适的）。<code>.arg(&amp;path)</code>：<code>Path</code> 实现了 <code>AsRef&lt;OsStr&gt;</code>，所以能直接当命令行参数传。</p></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 <code>/tmp/pebble-rebuild-10</code>（你的是 <code>~/pebble</code>）里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-bytecode</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--workspace</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>4</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-bytecode</span></div></div><div><div><div>2</div></div><div><span>running 14 tests</span></div></div><div><div><div>3</div></div><div><span>...</span></div></div><div><div><div>4</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>5</div></div><div><span>...</span></div></div><div><div><div>6</div></div><div><span>running 1 test</span></div></div><div><div><div>7</div></div><div><span>test crates/pebble-bytecode/src/lib.rs - chunk (line 353) ... ok</span></div></div><div><div><div>8</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>9</div></div><div><span>$ cargo test -p pebble-cli</span></div></div><div><div><div>10</div></div><div><span>running 3 tests</span></div></div><div><div><div>11</div></div><div><span>test repl::tests::arguments_are_kept_whole ... ok</span></div></div><div><div><div>12</div></div><div><span>test repl::tests::quit_has_a_short_form ... ok</span></div></div><div><div><div>13</div></div><div><span>test repl::tests::unknown_commands_are_rejected ... ok</span></div></div><div><div><div>14</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>15</div></div><div><span>running 9 tests</span></div></div><div><div><div>16</div></div><div><span>test parse_prints_the_syntax_tree ... ok</span></div></div><div><div><div>17</div></div><div><span>test quiet_suppresses_the_final_value ... ok</span></div></div><div><div><div>18</div></div><div><span>test check_reports_compile_errors ... ok</span></div></div><div><div><div>19</div></div><div><span>test tokens_prints_one_line_per_token ... ok</span></div></div><div><div><div>20</div></div><div><span>test check_is_silent_on_a_valid_file ... ok</span></div></div><div><div><div>21</div></div><div><span>test run_reports_parse_errors_on_stderr ... ok</span></div></div><div><div><div>22</div></div><div><span>test unknown_subcommands_are_usage_errors ... ok</span></div></div><div><div><div>23</div></div><div><span>test run_evaluates_fibonacci ... ok</span></div></div><div><div><div>24</div></div><div><span>test dump_bytecode_prints_the_listing ... ok</span></div></div><div><div><div>25</div></div><div><span>test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s</span></div></div><div><div><div>26</div></div><div><span>$ cargo clippy --workspace --all-targets -- -D warnings</span></div></div><div><div><div>27</div></div><div><span>...</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>Checking pebble-bytecode v0.1.0 (/tmp/pebble-rebuild-10/crates/pebble-bytecode)</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>Checking pebble-cli v0.1.0 (/tmp/pebble-rebuild-10/crates/pebble-cli)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s</span></div></div><div><div><div>31</div></div><div><span>$ cargo fmt --all --check; echo "exit: $?"</span></div></div><div><div><div>32</div></div><div><span>exit: 0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pebble-bytecode</code> 14 个测试加 1 个文档测试，<code>pebble-cli</code> 3 个单元测试加 9 个集成测试，<code>pebble-macros</code> 没有测试（<code>cargo test -p pebble-macros</code> 是 <code>running 0 tests</code>：过程宏 crate 没法在自己里面调用自己的宏，它的测试是 <code>pebble-ast</code> 里的 <code>derive_spanned_*</code> 和这个 crate 的编译本身）。clippy 和 rustfmt 干净。</p><p>下面这两个文件现在必须和 tag 逐字节相同：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bytecode/Cargo.toml" target="_blank"><code>crates/pebble-bytecode/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-bytecode/src/lib.rs" target="_blank"><code>crates/pebble-bytecode/src/lib.rs</code></a></li>
</ul><p><code>crates/pebble-cli/src/sources.rs</code> 也已经和 tag 相同（<code>repl.rs</code> 还有两处文档注释和 tag 不同，第 9 篇说过，行为一样），<code>examples/</code> 下的五个文件、更早的 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-lexer</code>、<code>pebble-ast</code>、<code>pebble-parser</code>、<code>pebble-value</code>、<code>pebble-interp</code> 也都是（我对着 tag 逐个 <code>diff</code> 过）。</p><p><strong>中间文件</strong>（还不是最终版，后面的篇章会继续改）：</p><ul>
<li><code>crates/pebble-macros/Cargo.toml</code> 和 <code>crates/pebble-macros/src/lib.rs</code>：现在有 <code>#[derive(Spanned)]</code> 和 <code>opcode_set!</code> 两个宏。tag 里的版本多了第 12 篇的 <code>#[differential]</code>，以及对应的文档、一个 <code>[dev-dependencies]</code>，<code>Cargo.toml</code> 的 <code>description</code> 也不同。<code>derive_spanned</code>、<code>expand_spanned</code>、<code>span_member</code>、<code>opcode_set</code>、<code>expand_opcodes</code> 与 tag 逐字相同。</li>
<li><code>crates/pebble-cli/</code> 下的全部七个文件：第三版。这一版相对第二版的改动只有这些：<code>Cargo.toml</code> 多一个依赖，<code>cli.rs</code> 多一个 <code>--dump-bytecode</code>，<code>commands.rs</code> 的 <code>run</code> 和 <code>check</code>，<code>sources.rs</code> 一个词，<code>tests/cli.rs</code> 多两个测试。最终版多出来的东西：<code>Backend</code> trait、<code>--vm</code> 和 <code>bench</code>（第 12 篇）、并行的 <code>check</code>（第 14 篇）、可选的 <code>repl</code> 特性（第 15 篇）。</li>
<li>根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code>：现在列了 9 个内部 crate 加 <code>anyhow</code>、<code>clap</code>、<code>proc-macro2</code>、<code>quote</code>、<code>rustyline</code>、<code>syn</code>。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）。<code>git clone</code> 到 <code>~/pebble-tag</code> 的话，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，用 <code>cd ~/pebble-tag/lab/rust-lab</code> 运行（也可以把这个目录拷到 <code>~/rust-lab</code>）。表中 <code>lab-ch10-opcode</code> 的两行在侧 crate <code>lab/lab-ch10-opcode/</code> 里（依赖仓库里的 <code>pebble-macros</code>），命令已写出完整目录。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_name_table.rs" target="_blank"><code>ch10_name_table.rs</code></a></td><td><code>cargo build --bin ch10_name_table</code></td><td>编译失败：E0004</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch10-opcode/src/bin/ch10_opcode_set_struct.rs" target="_blank"><code>ch10_opcode_set_struct.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch10-opcode &amp;&amp; cargo build --bin ch10_opcode_set_struct</code></td><td>编译失败：<code>error: expected `enum` </code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch10-opcode/src/bin/ch10_opcode_set.rs" target="_blank"><code>ch10_opcode_set.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch10-opcode &amp;&amp; cargo run --quiet --bin ch10_opcode_set</code></td><td>运行，首行 <code>Const(7) is called Const</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_sizes.rs" target="_blank"><code>ch10_sizes.rs</code></a></td><td><code>cargo run --quiet --bin ch10_sizes</code></td><td>运行，首行 <code>enum Op        = 8 bytes</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_float_bits.rs" target="_blank"><code>ch10_float_bits.rs</code></a></td><td><code>cargo run --quiet --bin ch10_float_bits</code></td><td>运行，首行 <code>0.0 == -0.0        -&gt; true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_capture.rs" target="_blank"><code>ch10_capture.rs</code></a></td><td><code>cargo build --bin ch10_capture</code></td><td>编译失败：E0500</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_capture_ok.rs" target="_blank"><code>ch10_capture_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch10_capture_ok</code></td><td>运行，输出 <code>0 1 0</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_ref_binding.rs" target="_blank"><code>ch10_ref_binding.rs</code></a></td><td><code>cargo build --bin ch10_ref_binding</code></td><td>编译失败：E0507</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_ref_binding_ok.rs" target="_blank"><code>ch10_ref_binding_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch10_ref_binding_ok</code></td><td>运行，输出 <code>[Jump(7), Name("x")]</code>（另有两条 <code>field `0` is never read</code> 警告）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_hygiene.rs" target="_blank"><code>ch10_hygiene.rs</code></a></td><td><code>cargo build --bin ch10_hygiene</code></td><td>编译失败：E0425</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_hygiene_ok.rs" target="_blank"><code>ch10_hygiene_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch10_hygiene_ok</code></td><td>运行，输出 <code>22</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_trailing_comma.rs" target="_blank"><code>ch10_trailing_comma.rs</code></a></td><td><code>cargo build --bin ch10_trailing_comma</code></td><td>编译失败：<code>error: unexpected end of macro invocation</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch10_trailing_comma_ok.rs" target="_blank"><code>ch10_trailing_comma_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch10_trailing_comma_ok</code></td><td>运行，输出 <code>[1, 2, 3] []</code></td></tr></tbody></table></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 加一条 <code>Dup</code> 指令</strong>（复制栈顶）。需要改哪些地方，编译器会在哪里提醒你？</p>
参考答案<p>在 <code>opcode_set!</code> 里加两行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>/// Duplicate the top of the stack.</span></div></div><div><div><div>2</div></div><div><span>        </span><span>Dup</span><span>,</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只此而已。<code>name()</code>、<code>Debug</code>、<code>PartialEq</code>、<code>Copy</code> 都由宏重新生成；<code>operand_bytes</code> 的 <code>_ =&gt; 0</code> 和反汇编里的 <code>_ =&gt; String::new()</code> 恰好都不需要改。我在临时 worktree 里加了它和一个测试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>dup_is_listed_without_an_operand</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>chunk</span><span><span> </span><span>=</span><span> </span></span><span>chunk!</span><span>[</span><span>Opcode</span><span>::</span><span>Nil</span><span>, </span><span>Opcode</span><span>::</span><span>Dup</span><span>, </span><span>Opcode</span><span>::</span><span>Return</span><span>];</span></div></div><div><div><div>4</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>Opcode</span><span>::</span><span>Dup</span><span>.</span><span>name</span><span>(), </span><span>"Dup"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>Opcode</span><span>::</span><span>Dup</span><span>.</span><span>operand_bytes</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert!</span><span>(</span><span>chunk</span><span>.</span><span>disassemble</span><span>().</span><span>contains</span><span>(</span><span>"   1 Dup</span><span>\n</span><span>"</span><span>), </span><span>"{}"</span><span>, </span><span>chunk</span><span>.</span><span>disassemble</span><span>());</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo test -p pebble-bytecode</code> 是 14 个测试加 1 个 doc test 全部通过。但编译器的提醒在<strong>别处</strong>：这个 crate 里没有任何地方对 <code>Opcode</code> 做穷尽匹配，所以它不叫；第 12 篇的 VM 有，<code>cargo build -p pebble-vm</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0004]: non-exhaustive patterns: `Opcode::Dup` not covered</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-vm/src/lib.rs:226:19</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>226 |             match instruction {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                   ^^^^^^^^^^^ pattern `Opcode::Dup` not covered</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>...</span></div></div><div><div><div>8</div></div><div><span>help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>380 ~                 },</span></div></div><div><div><div>11</div></div><div><span>381 +                 Opcode::Dup =&gt; todo!()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这就是穷尽匹配的价值：新指令加进来，没有一个执行它的地方能被悄悄漏掉。（我这里没有写编译器里<strong>发射</strong> <code>Dup</code> 的代码，因为目前的 Pebble 语法里没有哪个构造需要它；它是个为以后的优化预备的指令。）</p><p><strong>2. 用 <code>chunk!</code> 造一个三条指令的 chunk：<code>True</code>、一个目标为 0 的 <code>JumpIfFalse</code>、<code>Return</code>。</strong> 把跳转指向最后一条并打印清单。然后让 <code>patch_jump</code> 去补第 0 条，看会发生什么。</p>
参考答案<p>写成一个临时测试，用 <code>cargo test -p pebble-bytecode scratch -- --nocapture</code> 跑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>scratch</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>chunk</span><span><span> </span><span>=</span><span> </span></span><span>chunk!</span><span>[</span><span>Opcode</span><span>::</span><span>True</span><span>, </span><span>Opcode</span><span>::</span><span>JumpIfFalse</span><span>(</span><span>0</span><span>), </span><span>Opcode</span><span>::</span><span>Return</span><span>];</span></div></div><div><div><div>4</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>chunk</span><span>.</span><span>disassemble</span><span>());</span></div></div><div><div><div>5</div></div><div><span>        </span><span>chunk</span><span>.</span><span>patch_jump</span><span>(</span><span>1</span><span>, </span><span>2</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>chunk</span><span>.</span><span>disassemble</span><span>());</span></div></div><div><div><div>7</div></div><div><span>        </span><span>chunk</span><span>.</span><span>patch_jump</span><span>(</span><span>0</span><span>, </span><span>2</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 1 test</span></div></div><div><div><div>2</div></div><div><span>== constants ==</span></div></div><div><div><div>3</div></div><div><span>== code ==</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>0 True</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>1 JumpIfFalse -&gt; 0</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>2 Return</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>== constants ==</span></div></div><div><div><div>9</div></div><div><span>== code ==</span></div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>0 True</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>1 JumpIfFalse -&gt; 2</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>2 Return</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>thread 'tests::scratch' (135274) panicked at crates/pebble-bytecode/src/lib.rs:238:26:</span></div></div><div><div><div>16</div></div><div><span>cannot patch True as a jump</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第一份清单里的 <code>-&gt; 0</code> 是占位符；<code>patch_jump(1, 2)</code> 之后变成 <code>-&gt; 2</code>。第 0 条是 <code>True</code>，不是跳转，<code>patch_jump</code> 的最后一个分支（带 <code>ref other</code> 的那个）触发 <code>panic!</code>，信息里用 <code>{other:?}</code> 打印出了那条指令，也就是宏给我们派生的 <code>Debug</code>。文档里的 <code># Panics</code> 一节就是写给这个情况的：这是编译器自己的 bug，不是用户的错，所以 <code>panic</code> 而不是返回 <code>Result</code>。</p><p><strong>3. 为什么跳转目标要回填，而不是在发射指令时直接算出来？有没有不用回填的办法？</strong>（讨论题）</p>
参考答案<p>因为一条向前的跳转，它的目标取决于<strong>还没发射的指令有多少条</strong>：<code>if</code> 的 <code>then</code> 分支有几条指令，要编译完才知道。要在发射时算出来，你得在编译之前就知道每段代码的大小，这要求要么先做一遍”只数指令”的预扫描（代价是整个编译器要写两份，一份真的发射，一份只计数，两份必须永远一致），要么把代码先编译成一棵中间树、算好大小再展开。回填的代价是每条向前的跳转要记一个下标，回头改一次，非常便宜：<code>Vec</code> 里改一个槽位，O(1)。</p><p>之所以在 Pebble 里回填特别简单，是因为每条指令是固定大小的 8 字节枚举，把 <code>Jump(0)</code> 换成 <code>Jump(17)</code> 不会改变任何后面指令的位置。如果你换成变长编码（操作数按需用 1、2 或 4 字节），回填时目标可能放不进你预留的字节数，真正的汇编器要为此做”分支松弛”，反复调整直到所有跳转的宽度稳定。（这是我从汇编器那里知道的做法，Pebble 里没用到，也没有试过。）</p><p>另一个不用回填的办法是<strong>用标签</strong>：编译时把跳转目标记成一个符号 <code>Label(7)</code>，全部编译完再统一把标签换成下标。这本质上还是两步（发射，再解析），只是把”回头改”集中到了最后。</p><p>我们现在有一份清单，却没有人执行它。真正执行它的虚拟机，要在一个自己管理的堆上给列表、映射和闭包分配对象，还要知道哪些对象还活着：第 9 篇我们已经看到 <code>Rc</code> 在这里会让我们漏内存。所以在写那个虚拟机之前，<a href="/posts/rust-by-pebble-11-gc-unsafe/">下一篇</a>我们先做它需要的零件：一个手写的垃圾回收器，用索引代替指针，用一个工作队列代替递归，在中途遇到我们的第一个 <code>unsafe</code>。字节码已经等在那里了，现在它只需要一个人来读它。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-09-interpreter-closures/</id>
      <title type="text">树遍历解释器：闭包、控制流与一个内存泄漏</title>
      <published>2026-10-13T08:00:00.000Z</published>
      <updated>2026-10-13T08:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-09-interpreter-closures/"/>
      <summary type="text">让 AST 真正跑起来：用 Flow 枚举把 return、break、continue 变成返回值，用 Rc&lt;RefCell&lt;Env&gt;&gt; 实现闭包，用 FnOnce 守住递归深度，再写两个测试证明 fn fact 会让全局环境永远不被释放，并看清 Weak 为什么救不了它。</summary>
      <content type="html"><![CDATA[<p>这一篇里有两个测试，断言的是相反的事。一个说：跑完 <code>let x = 1 + 2;</code> 之后，解释器的全局环境被释放了。另一个说：跑完 <code>fn fact(n) { ... } fact(5);</code> 之后，全局环境<strong>没有</strong>被释放，而且这是我们故意写下来的预期。第二个测试的名字叫 <code>top_level_functions_keep_globals_alive</code>，它是我给 <code>Rc</code> 写过的最不光彩的测试：我花了两篇的时间让所有权系统保证内存安全，然后用它证明我的解释器会泄漏。</p>
<p>在那之前我们得先让 Pebble 跑起来。第 7 篇我们把源码变成了 AST，第 8 篇把运行时的值、环境和内建函数备好了。这一篇把它们接在一起：一个几百行的树遍历解释器，加上命令行里的 <code>run</code> 和 <code>repl</code>。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇我们写出 <code>pebble-interp</code>（Pebble 的第一个执行后端），并把命令行升级到第二版：新增 <code>pebble run</code> 和 <code>pebble repl</code>。用到的 Rust 特性：用枚举把控制流当数据返回、闭包的三种捕获方式和 <code>Fn</code>/<code>FnMut</code>/<code>FnOnce</code>、<code>move</code>、<code>Weak</code>、<code>std::thread::Builder</code>、<code>collect::&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()</code>、泛型辅助函数，以及为自己的类型实现标准库的 <code>FromStr</code>。结束时 <code>cargo test -p pebble-interp</code> 是 16 个测试全部通过（包括两个 <code>Weak</code> 泄漏测试），命令行是 3 个单元测试加 7 个集成测试，<code>crates/pebble-interp/</code> 下的两个文件与 tag 逐字节相同。上一篇是 <a href="/posts/rust-by-pebble-08-rc-refcell-values/">共享与可变：Rc、RefCell 和运行时的值</a>，<code>Value</code>、<code>Env</code>、<code>Function</code>、<code>RuntimeError</code> 都是在那里写的。</p></div></div>
<section><h2>要执行的东西和它的形状<a href="#要执行的东西和它的形状"><span>#</span></a></h2><p>先说清楚解释器要干什么。输入是一个 <code>Program</code>，也就是 <code>Vec&lt;Stmt&gt;</code>；输出是一个 <code>Value</code>，或者一个 <code>RuntimeError</code>。两种东西要处理：</p><ul>
<li><strong>表达式</strong>（<code>Expr</code>）：求值，得到一个 <code>Value</code>。<code>1 + 2</code> 得到 <code>Int(3)</code>。</li>
<li><strong>语句</strong>（<code>Stmt</code>）：执行。<code>let</code> 往环境里加一个名字，<code>if</code> 选一个分支，<code>while</code> 反复执行一个块。语句本身不产生值（<code>Expr</code> 语句除外），但它们有”怎么结束的”这件事。</li>
</ul><p>“怎么结束的”是整个解释器最有意思的地方。看这个 Pebble 程序，真实运行一遍（本篇前面出现的 <code>pebble run</code> 输出，要等最后的第二版命令行写好才能自己复现）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn find_first_even(xs) {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>for x in xs {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>if x % 2 == 0 {</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>return x;</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>return nil;</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div><span>print(find_first_even([1, 3, 6, 8]));</span></div></div><div><div><div>10</div></div><div><span>print(find_first_even([1, 3]));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run flow.pebble</span></div></div><div><div><div>2</div></div><div><span>6</span></div></div><div><div><div>3</div></div><div><span>nil</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>return x;</code> 写在 <code>if</code> 里面，<code>if</code> 在 <code>for</code> 里面，<code>for</code> 在函数体里面。执行到 <code>return</code> 的那一刻，要一口气跳出三层。C 程序员遇到这种事有三个选择：<code>goto</code>、<code>longjmp</code>，或者在每一层都检查一个”是否该退出”的标志。C++ 程序员可能想到异常，但用异常实现 <code>return</code> 是个臭名昭著的坏主意：慢，而且让读代码的人以为程序出错了。</p><p>Rust 的答案是第四个：<strong>把”怎么结束的”做成返回值</strong>。</p></section>
<section><h2>Flow：控制流是数据<a href="#flow控制流是数据"><span>#</span></a></h2><p><code>execute_stmt</code> 执行一条语句，返回 <code>Result&lt;Flow, RuntimeError&gt;</code>。外层 <code>Result</code> 是第 7 篇学的那一层：<code>Err</code> 表示运行时出错了，用 <code>?</code> 一路往上传。内层 <code>Flow</code> 是新东西，描述语句正常执行时是<strong>怎么结束的</strong>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// How a statement finished.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Most statements produce a value and fall through (`Value`); the other three</span></div></div><div><div><div>4</div></div><div><span>/// variants unwind the walker.</span></div></div><div><div><div>5</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Flow</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// Completed normally, producing this value.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Value</span><span>(</span><span>Value</span><span>),</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// A `return` escaped.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Return</span><span>(</span><span>Value</span><span>),</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// A `break` escaped.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>Break</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>/// A `continue` escaped.</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Continue</span><span>,</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>impl</span><span> </span><span>Flow</span><span> {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>is_normal</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>matches!</span><span>(</span><span>self</span><span>, </span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>_</span><span>))</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>四种结束方式：</p><ul>
<li><code>Value(v)</code>：顺顺当当执行完，产生值 <code>v</code>（绝大多数语句产生 <code>nil</code>，<code>Expr</code> 语句产生表达式的值）。</li>
<li><code>Return(v)</code>：撞上了 <code>return v;</code>。</li>
<li><code>Break</code> 和 <code>Continue</code>：撞上了同名语句。</li>
</ul><p>每个会包含其它语句的节点（块、<code>if</code>、<code>while</code>、<code>for</code>、函数体）在执行完子语句后只做一件事：看 <code>Flow</code> 是不是 <code>Value</code>。是，继续往下；不是，原样往上递，直到有人认领。<code>while</code> 认领 <code>Break</code> 和 <code>Continue</code>，函数调用认领 <code>Return</code>，其余全部放行。</p><p><code>is_normal</code> 里的 <code>matches!</code> 是第 7 篇见过的宏：<code>matches!(self, Flow::Value(_))</code> 展开成一个返回 <code>bool</code> 的 <code>match</code>。注意 <code>Flow</code> 的 <code>Value</code> 变体和 <code>pebble_value::Value</code> 类型同名，这在 Rust 里没问题：变体总是通过 <code>Flow::Value</code> 这样带前缀的路径访问。</p><p>这和 C++ 里 <code>std::variant&lt;Value, Return, Break, Continue&gt;</code> 的做法很像，区别是 Rust 的枚举是语言的一部分，<code>match</code> 会检查你有没有处理所有情况。第 3 篇我们看过漏掉一个分支时的 E0004，这一篇里它每天都会救我一次。</p></section>
<section><h2>新建 pebble-interp<a href="#新建-pebble-interp"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-interp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先在根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 里，<code>pebble-value</code> 那一行后面加上它自己：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-interp</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-interp"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>crates/pebble-interp/Cargo.toml</code> 的完整内容如下，和 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/Cargo.toml" target="_blank">tag 里的文件</a> 一致：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-interp"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"A tree-walking interpreter for Pebble."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div><span>pebble-ast.workspace</span><span> = </span><span>true</span></div></div><div><div><div>15</div></div><div><span>pebble-value.workspace</span><span> = </span><span>true</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>18</div></div><div><span>pebble-parser.workspace</span><span> = </span><span>true</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>21</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pebble-parser</code> 只放在 <code>[dev-dependencies]</code>：解释器本身不需要解析器，只有测试要把字符串变成 AST。<code>cargo</code> 会把 dev 依赖只用在 <code>cargo test</code> 里，不会泄漏到使用 <code>pebble-interp</code> 的其他 crate。</p></section>
<section><h2>第一版：表达式、变量和块<a href="#第一版表达式变量和块"><span>#</span></a></h2><p>接下来我们分四步写 <code>crates/pebble-interp/src/lib.rs</code>。每一步结束都有一个可以跑的检查，不会让你写几百行才知道对不对。</p><p>第一步只做算术、变量、赋值和语句序列。把文件写成下面这样，我分几块给出，按顺序拼起来就是一个文件。先是文件头：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::{</span></span><span>BinaryOp</span><span>, </span><span>Expr</span><span>, </span><span>LogicalOp</span><span>, </span><span>Program</span><span>, </span><span>Spanned</span><span>, </span><span>Stmt</span><span>, </span><span>UnaryOp</span><span>};</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::{</span></span><span>Env</span><span>, </span><span>RuntimeError</span><span>, </span><span>Value</span><span>, compare, div, mul, negate, rem, sub};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>#![forbid(unsafe_code)]</code> 是第 2 篇写过的 crate 属性：这整个 crate 里一行 <code>unsafe</code> 都不允许。<code>use</code> 列表里的东西我们一路走一路用上；现在列出来的都是第一步用得到的。</p><p>然后是上一节的 <code>Flow</code>，原样写进去（包括 <code>is_normal</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// How a statement finished.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Most statements produce a value and fall through (`Value`); the other three</span></div></div><div><div><div>4</div></div><div><span>/// variants unwind the walker.</span></div></div><div><div><div>5</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Flow</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// Completed normally, producing this value.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Value</span><span>(</span><span>Value</span><span>),</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// A `return` escaped.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Return</span><span>(</span><span>Value</span><span>),</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// A `break` escaped.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>Break</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>/// A `continue` escaped.</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Continue</span><span>,</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>impl</span><span> </span><span>Flow</span><span> {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>is_normal</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>matches!</span><span>(</span><span>self</span><span>, </span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>_</span><span>))</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>接着是解释器本身。第一版只有一个字段，全局环境：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The tree-walking interpreter.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Interpreter</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>globals</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>impl</span><span> </span><span>Default</span><span> </span><span>for</span><span> </span><span>Interpreter</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>default</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>Self</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>impl</span><span> </span><span>Interpreter</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>/// Creates an interpreter whose global scope holds the builtins.</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>globals</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Env</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>18</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>globals</span><span>.</span><span>borrow_mut</span><span>();</span></div></div><div><div><div>19</div></div><div><span>            </span><span>for</span><span> </span><span>function</span><span> </span><span>in</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>BUILTINS</span><span> {</span></div></div><div><div><div>20</div></div><div><span>                </span><span>scope</span><span>.</span><span>define</span><span>(</span><span>function</span><span><span>.name, </span><span>Value</span><span>::</span></span><span>Native</span><span>(</span><span>function</span><span>));</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>        </span><span>Self</span><span> { </span><span>globals</span><span> }</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>/// The global scope.</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>28</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>globals</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt; {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>self</span><span>.globals)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Rc&lt;RefCell&lt;Env&gt;&gt;</code> 是第 8 篇的主角：<code>Rc</code> 让多个所有者共享，<code>RefCell</code> 让共享的东西还能改。<code>new</code> 里先建一个空的 <code>Env</code>，然后把 <code>BUILTINS</code> 表里的 11 个内建函数全部 <code>define</code> 进去。<code>for function in pebble_value::BUILTINS</code> 里的 <code>function</code> 是 <code>&amp;'static NativeFunction</code>，直接放进 <code>Value::Native</code> 就行（第 8 篇的 <code>static</code> 表）。</p><p><code>globals()</code> 返回 <code>Rc::clone(&amp;self.globals)</code>：克隆的是指针，引用计数加一，不是整个环境。这个方法在后面的泄漏测试里会发挥主要作用。</p><p><code>interpret</code> 是入口（从这里起，缩进四格的代码都写在同一个 <code>impl Interpreter</code> 块里，直到我说它结束）。程序本身就是一个顶层块：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Evaluates a whole program, returning the value of the last statement.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>interpret</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>globals</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>self</span><span>.globals);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span>.</span><span>execute_block</span><span>(&amp;</span><span>program</span><span>.stmts, </span><span>globals</span><span>)? {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>value</span><span><span>) </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>value</span><span>),</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Flow</span><span>::</span><span>Break</span><span><span> </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Continue</span><span> =&gt; </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>Span</span><span>::</span><span>DUMMY</span><span>,</span></div></div><div><div><div>8</div></div><div><span>                </span><span>"`break` or `continue` used outside a loop"</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>)),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>顶层的 <code>Break</code> 或 <code>Continue</code> 没有任何循环可以认领，这是用户的程序错误，所以变成一个 <code>RuntimeError</code>（<code>Span::DUMMY</code> 是第 1 篇的占位 span）。<code>Flow::Value(value) | Flow::Return(value)</code> 是一个或模式：顶层的 <code>return</code> 也当成程序的结果。</p><p><code>execute_block</code> 就是上面说的”检查 <code>Flow</code>”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- statements -------------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>execute_block</span><span>(</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>stmts</span><span>: &amp;[</span><span>Stmt</span><span>],</span></div></div><div><div><div>6</div></div><div><span>        </span><span>env</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Flow</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>last</span><span><span> </span><span>=</span><span> </span></span><span>Flow</span><span>::</span><span>Value</span><span><span>(</span><span>Value</span><span>::</span></span><span>Nil</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>stmts</span><span> {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>last</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>execute_stmt</span><span>(</span><span>stmt</span><span>, &amp;</span><span>env</span><span>)?;</span></div></div><div><div><div>11</div></div><div><span>            </span><span>if</span><span><span> </span><span>!</span></span><span>last</span><span>.</span><span>is_normal</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>last</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Ok</span><span>(</span><span>last</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>每条语句执行完，如果不是正常结束就立刻返回，整个块的值是最后一条语句的值。<code>self.execute_stmt(stmt, &amp;env)?</code> 这个 <code>?</code> 处理的是 <code>Err</code> 那一层。</p><p>然后是 <code>execute_stmt</code>。第一步只有 <code>let</code> 和表达式语句，其余分支先用 <code>todo!()</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>execute_stmt</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>stmt</span><span>: &amp;</span><span>Stmt</span><span>, </span><span>env</span><span>: &amp;</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;) -&gt; </span><span>Result</span><span>&lt;</span><span>Flow</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>match</span><span> </span><span>stmt</span><span> {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Let</span><span> { </span><span>name</span><span>, </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>4</div></div><div><span>                </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>value</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>5</div></div><div><span>                </span><span>env</span><span>.</span><span>borrow_mut</span><span>().</span><span>define</span><span>(</span><span>name</span><span>.name.</span><span>clone</span><span>(), </span><span>value</span><span>);</span></div></div><div><div><div>6</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Value</span><span><span>(</span><span>Value</span><span>::</span></span><span>Nil</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Expr</span><span> { </span><span>expr</span><span>, .. } =&gt; </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>self</span><span>.</span><span>eval</span><span>(</span><span>expr</span><span>, </span><span>env</span><span>)?)),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>todo!</span><span>(</span><span>"the other statements come in the next sections"</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Stmt::Let { name, value, .. }</code> 里的 <code>..</code> 表示”其他字段我不关心”。<code>match stmt</code> 的 <code>stmt</code> 是 <code>&amp;Stmt</code>，所以模式里绑定出来的 <code>name</code>、<code>value</code> 都是引用；这就是 Rust 的”默认绑定模式”，你不用写 <code>ref</code>。<code>env.borrow_mut().define(...)</code> 是 <code>RefCell</code> 的运行时借用：临时拿一个可变引用，用完（这一行结束）就还。</p><p>求值是 <code>eval</code>，它是整个文件里最长的函数，但每个分支都很短。先是字面量和变量：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- expressions ------------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>eval</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>, </span><span>env</span><span>: &amp;</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>match</span><span> </span><span>expr</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Nil</span><span> { .. } =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Nil</span><span>),</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Bool</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Bool</span><span>(*</span><span>value</span><span>)),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Int</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(*</span><span>value</span><span>)),</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Float</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(*</span><span>value</span><span>)),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Str</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>string</span><span>(</span><span>value</span><span>.</span><span>clone</span><span>())),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Var</span><span> { </span><span>name</span><span>, </span><span>span</span><span> } =&gt; </span><span>env</span><span>.</span><span>borrow</span><span>().</span><span>get</span><span>(&amp;</span><span>name</span><span>.name).</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>RuntimeError</span><span>::</span><span>new</span><span>(*</span><span>span</span><span>, </span><span>format!</span><span>(</span><span>"undefined variable `{}`"</span><span>, </span><span>name</span><span>.name))</span></div></div><div><div><div>12</div></div><div><span><span>                    </span></span><span>.</span><span>with_note</span><span>(</span><span>"declare it first with `let`"</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Expr::Var</code> 分支值得看一眼：<code>env.borrow().get(&amp;name.name)</code> 返回 <code>Option&lt;Value&gt;</code>，<code>ok_or_else</code> 把 <code>None</code> 变成 <code>Err(RuntimeError)</code>。用 <code>ok_or_else</code> 而不是 <code>ok_or</code> 是因为错误信息要 <code>format!</code>，不到出错那一刻不应该白白分配一个 <code>String</code>；闭包只在 <code>None</code> 时才会执行。<code>.with_note(...)</code> 是第 8 篇的构建器写法，给错误挂一条提示。</p><p>接着是一元、二元和逻辑运算：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Unary</span><span> { </span><span>op</span><span>, </span><span>operand</span><span>, </span><span>span</span><span> } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>operand</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>3</div></div><div><span>                </span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>UnaryOp</span><span>::</span><span>Neg</span><span> =&gt; </span><span>negate</span><span>(&amp;</span><span>value</span><span>, *</span><span>span</span><span>),</span></div></div><div><div><div>5</div></div><div><span>                    </span><span>UnaryOp</span><span>::</span><span>Not</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Bool</span><span><span>(</span><span>!</span></span><span>value</span><span>.</span><span>is_truthy</span><span>())),</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Binary</span><span> { </span><span>op</span><span>, </span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span> } =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                </span><span>let</span><span> </span><span>lhs</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>lhs</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>10</div></div><div><span>                </span><span>let</span><span> </span><span>rhs</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>rhs</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>11</div></div><div><span>                </span><span>eval_binary</span><span>(*</span><span>op</span><span>, &amp;</span><span>lhs</span><span>, &amp;</span><span>rhs</span><span>, *</span><span>span</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Logical</span><span> { </span><span>op</span><span>, </span><span>lhs</span><span>, </span><span>rhs</span><span>, .. } =&gt; {</span></div></div><div><div><div>14</div></div><div><span>                </span><span>let</span><span> </span><span>lhs</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>lhs</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>15</div></div><div><span>                </span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>LogicalOp</span><span>::</span><span>And</span><span> =&gt; {</span></div></div><div><div><div>17</div></div><div><span>                        </span><span>if</span><span> </span><span>lhs</span><span>.</span><span>is_truthy</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>                            </span><span>self</span><span>.</span><span>eval</span><span>(</span><span>rhs</span><span>, </span><span>env</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>                        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>20</div></div><div><span>                            </span><span>Ok</span><span>(</span><span>lhs</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>                    </span><span>LogicalOp</span><span>::</span><span>Or</span><span> =&gt; {</span></div></div><div><div><div>24</div></div><div><span>                        </span><span>if</span><span> </span><span>lhs</span><span>.</span><span>is_truthy</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>                            </span><span>Ok</span><span>(</span><span>lhs</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>                        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>27</div></div><div><span>                            </span><span>self</span><span>.</span><span>eval</span><span>(</span><span>rhs</span><span>, </span><span>env</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Unary</code> 里 <code>negate(&amp;value, *span)</code>：<code>negate</code> 是第 8 篇写的函数，要 <code>&amp;Value</code> 和一个 <code>Span</code>，出错时用这个 span 报错位置。<code>Binary</code> 先求左再求右，然后交给 <code>eval_binary</code>（下面）。<code>Logical</code> 不能这么做：<code>and</code>/<code>or</code> 要<strong>短路</strong>，右边只有在需要时才求值，所以它是自己的分支。注意它返回的是<strong>操作数本身</strong>（<code>Ok(lhs)</code> 或 <code>self.eval(rhs, env)</code>），而不是 <code>bool</code>。<code>nil or 3</code> 是 <code>3</code>，这和 Python、JavaScript 一样。</p><p>最后是赋值，还有一个 <code>_ =&gt; todo!()</code> 暂时兜住后面要写的所有分支：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Assign</span><span> { </span><span>target</span><span>, </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>new_value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>value</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>3</div></div><div><span>                </span><span>match</span><span> </span><span>target</span><span>.</span><span>as_ref</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>Expr</span><span>::</span><span>Var</span><span> { </span><span>name</span><span>, </span><span>span</span><span> } =&gt; {</span></div></div><div><div><div>5</div></div><div><span>                        </span><span>if</span><span> </span><span>env</span><span>.</span><span>borrow_mut</span><span>().</span><span>assign</span><span>(&amp;</span><span>name</span><span>.name, </span><span>new_value</span><span>.</span><span>clone</span><span>()) {</span></div></div><div><div><div>6</div></div><div><span>                            </span><span>Ok</span><span>(</span><span>new_value</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>                        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>8</div></div><div><span>                            </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>9</div></div><div><span><span>                                </span></span><span>*</span><span>span</span><span>,</span></div></div><div><div><div>10</div></div><div><span>                                </span><span>format!</span><span>(</span><span>"cannot assign to undefined variable `{}`"</span><span>, </span><span>name</span><span>.name),</span></div></div><div><div><div>11</div></div><div><span><span>                            </span></span><span>))</span></div></div><div><div><div>12</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>other</span><span> =&gt; </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span><span>other</span><span>.</span><span>span</span><span>(), </span><span>"invalid assignment target"</span><span>)),</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>todo!</span><span>(</span><span>"calls, closures and collections come later"</span><span>),</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>赋值分支先求右边，再看左边是什么。第一步只支持变量；列表下标的赋值留到第四步，现在碰到它会落进 <code>other</code> 分支，报一个”invalid assignment target”。<code>env.borrow_mut().assign(...)</code> 返回 <code>bool</code>：找得到名字就是 <code>true</code>。</p><p><code>impl Interpreter { ... }</code> 到这里结束，所以紧接着在文件里写一个 <code>}</code> 收尾，然后写 <code>eval_binary</code>。它是个独立函数，不需要 <code>self</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>eval_binary</span><span>(</span><span>op</span><span>: </span><span>BinaryOp</span><span>, </span><span>lhs</span><span>: &amp;</span><span>Value</span><span>, </span><span>rhs</span><span>: &amp;</span><span>Value</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>use</span><span><span> </span><span>std</span><span>::</span><span>cmp</span><span>::</span></span><span>Ordering</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Ok</span><span>(</span><span>match</span><span> </span><span>op</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Add</span><span><span> =&gt; </span><span>pebble_value</span><span>::</span></span><span>add</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span>)?,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Sub</span><span> =&gt; </span><span>sub</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span>)?,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Mul</span><span> =&gt; </span><span>mul</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span>)?,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Div</span><span> =&gt; </span><span>div</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span>)?,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Rem</span><span> =&gt; </span><span>rem</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span>)?,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Eq</span><span> =&gt; </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>lhs</span><span>.</span><span>equals</span><span>(</span><span>rhs</span><span>)),</span></div></div><div><div><div>10</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>NotEq</span><span> =&gt; </span><span>Value</span><span>::</span><span>Bool</span><span><span>(</span><span>!</span></span><span>lhs</span><span>.</span><span>equals</span><span>(</span><span>rhs</span><span>)),</span></div></div><div><div><div>11</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Lt</span><span> =&gt; </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>compare</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span><span>)? </span><span>==</span><span> </span><span>Ordering</span><span>::</span></span><span>Less</span><span>),</span></div></div><div><div><div>12</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>LtEq</span><span> =&gt; </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>compare</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span><span>)? </span><span>!=</span><span> </span><span>Ordering</span><span>::</span></span><span>Greater</span><span>),</span></div></div><div><div><div>13</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>Gt</span><span> =&gt; </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>compare</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span><span>)? </span><span>==</span><span> </span><span>Ordering</span><span>::</span></span><span>Greater</span><span>),</span></div></div><div><div><div>14</div></div><div><span>        </span><span>BinaryOp</span><span>::</span><span>GtEq</span><span> =&gt; </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>compare</span><span>(</span><span>lhs</span><span>, </span><span>rhs</span><span>, </span><span>span</span><span><span>)? </span><span>!=</span><span> </span><span>Ordering</span><span>::</span></span><span>Less</span><span>),</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Ok(match op { ... })</code> 是个小技巧：每个分支都产生一个 <code>Value</code>，其中一些带 <code>?</code>，最后统一包进 <code>Ok</code>，比每个分支各写一个 <code>Ok(...)</code> 干净。<code>use std::cmp::Ordering;</code> 写在函数内部：这个名字只有这里用，不污染文件顶部。比较用的是第 8 篇的 <code>compare</code>（它返回 <code>Ordering</code>，而不是实现 <code>PartialOrd</code>）。</p><p>最后给这一步写一个小测试模块，放在文件末尾：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_parser</span><span>::parse;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>assert!</span><span>(</span><span>result</span><span>.</span><span>is_ok</span><span>(), </span><span>"parse errors: {:?}"</span><span>, </span><span>result</span><span>.diagnostics);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>result</span><span>.program)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>evaluates_arithmetic_with_precedence</span><span>() {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>"1 + 2 * 3;"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>7</span><span>));</span></div></div><div><div><div>17</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>"(1 + 2) * 3;"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>9</span><span>));</span></div></div><div><div><div>18</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>"7 % 3;"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>22</div></div><div><span>    </span><span>fn</span><span> </span><span>variables_and_assignment</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>"let x = 1; x = x + 41; x;"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>42</span><span>));</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>use super::*;</code> 把父模块的东西都带进来（第 1 篇）。<code>run</code> 辅助函数做三件事：把字符串解析成 AST、断言没有解析错误、交给解释器。注意 <code>pebble_parser</code> 是 dev 依赖，只有测试里才能用。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-interp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 2 tests</span></div></div><div><div><div>2</div></div><div><span>test tests::variables_and_assignment ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::evaluates_arithmetic_with_precedence ... ok</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个测试通过。这时候 <code>1 + 2 * 3;</code> 已经能得到 <code>7</code> 了：优先级是第 7 篇的解析器管的，解释器只是走树。</p></section>
<section><h2>第二步：块、<code>if</code>、<code>while</code>、<code>for</code><a href="#第二步块ifwhilefor"><span>#</span></a></h2><p>现在给 <code>execute_stmt</code> 加上控制流。先写一个辅助函数 <code>run_scoped_block</code>，放在 <code>execute_block</code> 后面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Runs a nested block in a fresh child scope, normalising its value to</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// `nil`.</span></div></div><div><div><div>3</div></div><div><span>    </span><span>///</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Only expression statements carry a value; a control-flow statement such</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// as `if` produces `nil`. The bytecode compiler makes the same choice, so</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// the two backends agree on what a program evaluates to.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>run_scoped_block</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>stmts</span><span>: &amp;[</span><span>Stmt</span><span>],</span></div></div><div><div><div>10</div></div><div><span>        </span><span>env</span><span>: &amp;</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;,</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Flow</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(</span><span>env</span><span>));</span></div></div><div><div><div>13</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span>.</span><span>execute_block</span><span>(</span><span>stmts</span><span>, </span><span>scope</span><span>)? {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>_</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Value</span><span><span>(</span><span>Value</span><span>::</span></span><span>Nil</span><span>)),</span></div></div><div><div><div>15</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>Ok</span><span>(</span><span>other</span><span>),</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它做两件事。第一，<strong>开一个子作用域</strong>：<code>Env::child(Rc::clone(env))</code> 创建一个新的 <code>Env</code>，它的父指针指向当前环境，所以块里 <code>let</code> 的名字在块外不可见，但块里能读到外面的名字。第二，<strong>把值归一成 <code>nil</code></strong>：<code>if</code> 这样的控制流语句不产生值，只有表达式语句有值，所以一个块里的最后一条语句哪怕是 <code>x;</code>，整个 <code>if</code> 也要得到 <code>nil</code>。后面字节码编译器做同样的选择，这样两个后端对”程序的值是什么”的回答一致。</p><p>这个子作用域有一个后果，我现在先说明，<a href="/posts/rust-by-pebble-12-vm-backends/">第 12 篇</a>才会回头：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let x = 1;</span></div></div><div><div><div>2</div></div><div><span>if true {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>let x = 2;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div><span>print(x);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run blk.pebble</span></div></div><div><div><div>2</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>树遍历解释器给每个块一个子 <code>Env</code>，里面的 <code>let x = 2;</code> 只改了子作用域里的 <code>x</code>，外面的还是 <code>1</code>。第 12 篇的 VM 每次函数调用只有一个环境，同一个程序会打印 <code>2</code>。<strong>两个后端并不处处一致</strong>，我们会在那里把这个差异摆出来。</p><p>现在补齐 <code>execute_stmt</code> 里的控制流分支。下面四段都加在 <code>Stmt::Expr</code> 分支之后、兜底的 <code>_ =&gt; todo!(...)</code> 之前（<code>Fn</code> 分支要到第三步才写，所以兜底先留着）。<code>if</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>If</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>cond</span><span>,</span></div></div><div><div><div>3</div></div><div><span>                </span><span>then_branch</span><span>,</span></div></div><div><div><div>4</div></div><div><span>                </span><span>else_branch</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>..</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>} =&gt; {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>if</span><span> </span><span>self</span><span>.</span><span>eval</span><span>(</span><span>cond</span><span>, </span><span>env</span><span>)?.</span><span>is_truthy</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>self</span><span>.</span><span>run_scoped_block</span><span>(</span><span>then_branch</span><span>, </span><span>env</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>else_branch</span><span>.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Value</span><span><span>(</span><span>Value</span><span>::</span></span><span>Nil</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>12</div></div><div><span>                    </span><span>self</span><span>.</span><span>run_scoped_block</span><span>(</span><span>else_branch</span><span>, </span><span>env</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>while</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>While</span><span> { </span><span>cond</span><span>, </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>while</span><span> </span><span>self</span><span>.</span><span>eval</span><span>(</span><span>cond</span><span>, </span><span>env</span><span>)?.</span><span>is_truthy</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>let</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(</span><span>env</span><span>));</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>match</span><span> </span><span>self</span><span>.</span><span>execute_block</span><span>(</span><span>body</span><span>, </span><span>scope</span><span>)? {</span></div></div><div><div><div>5</div></div><div><span>                        </span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span>) =&gt; </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span>)),</span></div></div><div><div><div>6</div></div><div><span>                        </span><span>Flow</span><span>::</span><span>Break</span><span> =&gt; </span><span>break</span><span>,</span></div></div><div><div><div>7</div></div><div><span>                        </span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>_</span><span><span>) </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Continue</span><span> =&gt; {}</span></div></div><div><div><div>8</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Value</span><span><span>(</span><span>Value</span><span>::</span></span><span>Nil</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每次循环迭代都开一个新的子作用域。<code>match self.execute_block(body, scope)?</code> 对 <code>Flow</code> 做分类：<code>Return</code> 原样往上传，<code>Break</code> 退出这个 <code>while</code> 然后正常结束，其余两种（<code>Value</code> 和 <code>Continue</code>）什么也不做，进入下一次迭代。<code>Flow::Value(_) | Flow::Continue =&gt; {}</code> 里的 <code>{}</code> 是空块，也就是”什么都不做”。</p><p><code>for</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>For</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>var</span><span>, </span><span>iter</span><span>, </span><span>body</span><span>, ..</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>} =&gt; {</span></div></div><div><div><div>4</div></div><div><span>                </span><span>let</span><span> </span><span>iterable</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>iter</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>5</div></div><div><span>                </span><span>let</span><span> </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>items</span><span><span>) </span><span>=</span><span> &amp;</span></span><span>iterable</span><span> </span><span>else</span><span> {</span></div></div><div><div><div>6</div></div><div><span>                    </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>7</div></div><div><span>                        </span><span>iterable_span</span><span>(</span><span>iter</span><span>),</span></div></div><div><div><div>8</div></div><div><span>                        </span><span>format!</span><span>(</span><span>"`for` needs a list, got {}"</span><span>, </span><span>iterable</span><span>.</span><span>type_name</span><span>()),</span></div></div><div><div><div>9</div></div><div><span><span>                    </span></span><span>));</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>};</span></div></div><div><div><div>11</div></div><div><span>                </span><span>let</span><span> </span><span>snapshot</span><span><span> </span><span>=</span><span> </span></span><span>items</span><span>.</span><span>borrow</span><span>().</span><span>clone</span><span>();</span></div></div><div><div><div>12</div></div><div><span>                </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>snapshot</span><span> {</span></div></div><div><div><div>13</div></div><div><span>                    </span><span>let</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(</span><span>env</span><span>));</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>scope</span><span>.</span><span>borrow_mut</span><span>().</span><span>define</span><span>(</span><span>var</span><span>.name.</span><span>clone</span><span>(), </span><span>item</span><span>);</span></div></div><div><div><div>15</div></div><div><span>                    </span><span>match</span><span> </span><span>self</span><span>.</span><span>execute_block</span><span>(</span><span>body</span><span>, </span><span>scope</span><span>)? {</span></div></div><div><div><div>16</div></div><div><span>                        </span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span>) =&gt; </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span>)),</span></div></div><div><div><div>17</div></div><div><span>                        </span><span>Flow</span><span>::</span><span>Break</span><span> =&gt; </span><span>break</span><span>,</span></div></div><div><div><div>18</div></div><div><span>                        </span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>_</span><span><span>) </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Continue</span><span> =&gt; {}</span></div></div><div><div><div>19</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Value</span><span><span>(</span><span>Value</span><span>::</span></span><span>Nil</span><span>))</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里有三件事要说。第一，<code>let Value::List(items) = &amp;iterable else { ... };</code> 是第 3 篇的 <code>let ... else</code>：只要不是列表就走 <code>else</code> 里的 <code>return Err(...)</code>。第二，<code>iterable_span(iter)</code> 是个只有一行的辅助函数，写在文件后面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>iterable_span</span><span>(</span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>expr</span><span>.</span><span>span</span><span>()</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它的作用就是让错误位置指向 <code>for</code> 后面那个表达式。<code>span()</code> 方法来自 <code>pebble_ast::Spanned</code>，这个 trait 是第 6 篇用 derive 宏生成的。第三，也是最重要的：<code>let snapshot = items.borrow().clone();</code>。这一行为什么要先复制一份，我们到第四步再看，到时候有真实的 panic 可看。</p><p>最后是 <code>return</code>、<code>break</code>、<code>continue</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Return</span><span> { </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>value</span><span> {</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>Some</span><span>(</span><span>expr</span><span>) =&gt; </span><span>self</span><span>.</span><span>eval</span><span>(</span><span>expr</span><span>, </span><span>env</span><span>)?,</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>None</span><span> =&gt; </span><span>Value</span><span>::</span><span>Nil</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>};</span></div></div><div><div><div>6</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Break</span><span> { .. } =&gt; </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Break</span><span>),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Continue</span><span> { .. } =&gt; </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Continue</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它们都很短：<code>return</code> 求值（没有表达式就是 <code>nil</code>），然后包进 <code>Flow::Return</code>。<code>break</code> 和 <code>continue</code> 不携带数据。</p><p>此时 <code>execute_stmt</code> 里只剩 <code>Stmt::Fn</code> 还没有分支。兜底的消息可以顺手改成 “functions come in the next section”，这只是给自己看的提示。</p><p>加一个测试，放在 <code>tests</code> 模块里 <code>variables_and_assignment</code> 后面。它用的是 <code>while</code>，不需要函数调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>while_loop_accumulates</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>"let i = 0; let s = 0; while i &lt; 5 { s = s + i; i = i + 1; } s;"</span><span>;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>src</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>10</span><span>));</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>还没有函数的时候，我们得到了循环、条件和块。三个测试通过。</p></section>
<section><h2>第三步：函数和闭包<a href="#第三步函数和闭包"><span>#</span></a></h2><p>解释器里最难的一个词其实是”闭包”，但我想先绕开它，先看 Rust 自己的闭包是怎么回事，因为我们马上要写一个会接收闭包的函数。</p><section><h3>Rust 的闭包<a href="#rust-的闭包"><span>#</span></a></h3><p>C++ 程序员已经认识了 lambda：<code>[=]</code>、<code>[&amp;]</code>、<code>[x = std::move(x)]</code>，以及 <code>std::function</code> 这个类型擦除的容器。Rust 的闭包是同一个东西的另一种设计：</p><ul>
<li><strong>捕获方式不是你写的，是编译器从函数体里读出来的。</strong> 闭包里只读一个变量，就按共享引用 <code>&amp;T</code> 捕获；修改它，就按可变引用 <code>&amp;mut T</code> 捕获；把它移走（比如返回它），就按值捕获，也就是移动。</li>
<li>每个闭包有自己<strong>独一无二的匿名类型</strong>，所以闭包通常通过泛型传递（<code>impl Fn(..)</code>），编译时单态化，没有 <code>std::function</code> 的虚调用开销。</li>
<li>闭包按它<strong>怎么使用捕获的东西</strong>实现三个 trait 之一或几个：<code>Fn</code>（只读，可以调用任意多次）、<code>FnMut</code>（会修改，需要 <code>&amp;mut</code> 才能调用，可以调用多次）、<code>FnOnce</code>（会消耗捕获物，最多调用一次）。它们是包含关系：每个 <code>Fn</code> 都是 <code>FnMut</code>，每个 <code>FnMut</code> 都是 <code>FnOnce</code>。所以如果你的函数只调用传入的闭包<strong>一次</strong>，参数就该声明为 <code>FnOnce</code>，这是对调用者要求最少的一个。</li>
</ul><p>在实验室里把四种情况都跑一遍。文件是 <code>~/rust-lab/src/bin/ch09_closures.rs</code>：</p><div><div><div><figure><figcaption><span>~/rust-lab/src/bin/ch09_closures.rs</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>make_counter</span><span>() -&gt; </span><span>impl</span><span> </span><span>FnMut</span><span>() -&gt; </span><span>i32</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>n</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>n</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>// 1. A closure that only reads what it captured: Fn.</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>greeting</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"hello"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>shout</span><span><span> </span><span>=</span><span> </span><span>||</span><span> </span></span><span>greeting</span><span>.</span><span>to_uppercase</span><span>();</span></div></div><div><div><div>13</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{} {}"</span><span>, </span><span>shout</span><span>(), </span><span>shout</span><span>());</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"greeting is still ours: {greeting}"</span><span>);</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>// 2. A closure that mutates what it captured: FnMut.</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>count</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>bump</span><span><span> </span><span>=</span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>19</div></div><div><span>        </span><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>20</div></div><div><span>        </span><span>count</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>22</div></div><div><span>    </span><span>bump</span><span>();</span></div></div><div><div><div>23</div></div><div><span>    </span><span>bump</span><span>();</span></div></div><div><div><div>24</div></div><div><span>    </span><span>println!</span><span>(</span><span>"count after two bumps: {count}"</span><span>);</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>// 3. `move` copies the captured value into the closure.</span></div></div><div><div><div>27</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>10</span><span>;</span></div></div><div><div><div>28</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>owned</span><span><span> </span><span>=</span><span> </span></span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>29</div></div><div><span>        </span><span>n</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>30</div></div><div><span>        </span><span>n</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>32</div></div><div><span>    </span><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>owned</span><span>();</span></div></div><div><div><div>33</div></div><div><span>    </span><span>let</span><span> </span><span>second</span><span><span> </span><span>=</span><span> </span></span><span>owned</span><span>();</span></div></div><div><div><div>34</div></div><div><span>    </span><span>println!</span><span>(</span><span>"owned() = {first}, then {second}"</span><span>);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>println!</span><span>(</span><span>"the outer n is untouched: {n}"</span><span>);</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>    </span><span>// 4. A function can hand out a closure if the closure owns its state.</span></div></div><div><div><div>38</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>counter</span><span><span> </span><span>=</span><span> </span></span><span>make_counter</span><span>();</span></div></div><div><div><div>39</div></div><div><span>    </span><span>println!</span><span>(</span><span>"counter: {} {} {}"</span><span>, </span><span>counter</span><span>(), </span><span>counter</span><span>(), </span><span>counter</span><span>());</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch09_closures</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>HELLO HELLO</span></div></div><div><div><div>2</div></div><div><span>greeting is still ours: hello</span></div></div><div><div><div>3</div></div><div><span>count after two bumps: 2</span></div></div><div><div><div>4</div></div><div><span>owned() = 11, then 12</span></div></div><div><div><div>5</div></div><div><span>the outer n is untouched: 10</span></div></div><div><div><div>6</div></div><div><span>counter: 1 2 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一种是 <code>Fn</code>：闭包只读 <code>greeting</code>，调用两次都没问题，调用完 <code>greeting</code> 还是我们的。第二种是 <code>FnMut</code>：闭包里 <code>count += 1</code>，所以 <code>bump</code> 必须声明成 <code>let mut</code>，调用完外面的 <code>count</code> 变成 2，因为闭包拿的是 <code>&amp;mut count</code>。第三种是 <code>move</code>：<code>move ||</code> 强迫闭包拿走（对 <code>i32</code> 来说是复制）所有它用到的变量，所以闭包里加了两次，外面的 <code>n</code> 还是 10。第四种是返回闭包：<code>make_counter</code> 里的 <code>n</code> 是局部变量，函数一返回就没了，所以闭包必须 <code>move</code> 把它带走。</p><p>不 <code>move</code> 会怎样？去掉 <code>move</code>（文件 <code>~/rust-lab/src/bin/ch09_closure_escape.rs</code>）：</p><div><figure><figcaption><span>~/rust-lab/src/bin/ch09_closure_escape.rs</span></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>make_counter</span><span>() -&gt; </span><span>impl</span><span> </span><span>FnMut</span><span>() -&gt; </span><span>i32</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>||</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>n</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>n</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>counter</span><span><span> </span><span>=</span><span> </span></span><span>make_counter</span><span>();</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{} {}"</span><span>, </span><span>counter</span><span>(), </span><span>counter</span><span>());</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch09_closure_escape</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0373]: closure may outlive the current function, but it borrows `n`, which is owned by the current function</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch09_closure_escape.rs:4:5</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>4 |     || {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|     ^^ may outlive borrowed value `n`</span></div></div><div><div><div>8</div></div><div><span>5 |         n += 1;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|         - `n` is borrowed here</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>note: closure is returned here</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch09_closure_escape.rs:4:5</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>4 | /     || {</span></div></div><div><div><div>15</div></div><div><span>5 | |         n += 1;</span></div></div><div><div><div>16</div></div><div><span>6 | |         n</span></div></div><div><div><div>17</div></div><div><span>7 | |     }</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>| |_____^</span></div></div><div><div><div>19</div></div><div><span>help: to force the closure to take ownership of `n` (and any other referenced variables), use the `move` keyword</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>21</div></div><div><span>4 |     move || {</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>|     ++++</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>For more information about this error, try `rustc --explain E0373`.</span></div></div><div><div><div>25</div></div><div><span>error: could not compile `rust-lab` (bin "ch09_closure_escape") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器连修法都给了。这就是 C++ 里 <code>[&amp;]</code> 捕获了局部变量然后把 lambda 返回出去的情形：在 C++ 里是悬垂引用，运行时才爆炸（或者悄悄给你错误的数）；在 Rust 里是编译错误。</p><p><code>FnOnce</code> 的”最多一次”是编译器强制的。<code>~/rust-lab/src/bin/ch09_fnonce_twice.rs</code>：</p><div><figure><figcaption><span>~/rust-lab/src/bin/ch09_fnonce_twice.rs</span></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"pebble"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>consume</span><span><span> </span><span>=</span><span> </span></span><span>move</span><span><span> </span><span>||</span><span> </span></span><span>name</span><span>; </span><span>// returns the captured String: it can only run once</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>consume</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>second</span><span><span> </span><span>=</span><span> </span></span><span>consume</span><span>();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{first} {second}"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch09_fnonce_twice</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0382]: use of moved value: `consume`</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch09_fnonce_twice.rs:6:18</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>5 |     let first = consume();</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                 --------- `consume` moved due to this call</span></div></div><div><div><div>8</div></div><div><span>6 |     let second = consume();</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|                  ^^^^^^^ value used here after move</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>note: closure cannot be invoked more than once because it moves the variable `name` out of its environment</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch09_fnonce_twice.rs:4:27</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>4 |     let consume = move || name; // returns the captured String: it can only run once</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|                           ^^^^</span></div></div><div><div><div>16</div></div><div><span>note: this value implements `FnOnce`, which causes it to be moved when called</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch09_fnonce_twice.rs:5:17</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>19</div></div><div><span>5 |     let first = consume();</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>|                 ^^^^^^^</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>For more information about this error, try `rustc --explain E0382`.</span></div></div><div><div><div>23</div></div><div><span>error: could not compile `rust-lab` (bin "ch09_fnonce_twice") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>consume</code> 把捕获的 <code>name</code> 当作返回值移出去了，所以它只能是 <code>FnOnce</code>；调用它会<strong>消耗这个闭包本身</strong>。这和 C++ 里”moved-from 的东西能不能再用，要靠你自己记住”完全不同。</p></section><section><h3>Pebble 的闭包<a href="#pebble-的闭包"><span>#</span></a></h3><p>现在说 Pebble 自己的闭包。Pebble 的函数值在第 8 篇已经定义好了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Function</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>name</span><span>: </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>params</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>env</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这就是一个<strong>手工做出来的闭包对象</strong>：参数名、函数体（代码），加上一个 <code>env</code>（捕获物）。编译器为 Rust 闭包生成的结构体本质上也是这个形状，不同之处在于捕获的方式。Rust 闭包按变量一个个捕获，<code>move</code> 之后每个闭包有自己的一份拷贝；Pebble 闭包捕获的是<strong>整条环境链的一个共享指针</strong>。所以 Pebble 的 <code>make_counter</code> 里 <code>count = count + 1</code> 修改的是所有闭包共享的同一个 <code>Env</code>，外面能看到，这和 Rust lab 里第三种情况（外面的 <code>n</code> 纹丝不动）正好相反。</p></section><section><h3>写出来<a href="#写出来"><span>#</span></a></h3><p>先把解释器的结构改成最终形态。<code>Interpreter</code> 需要三个新字段：输出槽 <code>out</code>（<code>print</code> 往哪写，第 8 篇的 <code>dyn Output</code>）、当前调用深度 <code>depth</code>、最大深度 <code>max_depth</code>。把 <code>use</code> 块更新为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::{</span></span><span>BinaryOp</span><span>, </span><span>Expr</span><span>, </span><span>LogicalOp</span><span>, </span><span>Program</span><span>, </span><span>Spanned</span><span>, </span><span>Stmt</span><span>, </span><span>UnaryOp</span><span>};</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::{</span></span></div></div><div><div><div>9</div></div><div><span>    </span><span>Env</span><span>, </span><span>Function</span><span>, </span><span>Output</span><span>, </span><span>RuntimeError</span><span>, </span><span>Stdout</span><span>, </span><span>Value</span><span>, call_native, compare, div, mul, negate,</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>rem, sub,</span></div></div><div><div><div>11</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结构体和构造函数换成下面这样（<code>new</code> 变成一行，真正的工作移到 <code>with_output</code>；这个拆分是为了测试能注入一个 <code>Buffer</code> 来捕获输出）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The tree-walking interpreter.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Interpreter</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>globals</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>out</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>depth</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>max_depth</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>impl</span><span> </span><span>Default</span><span> </span><span>for</span><span> </span><span>Interpreter</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>default</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Self</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>impl</span><span> </span><span>Interpreter</span><span> {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>/// Creates an interpreter that prints to standard output.</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>18</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>Self</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>Stdout</span><span>))</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>/// Creates an interpreter with a custom output sink.</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>24</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_output</span><span>(</span><span>out</span><span>: </span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>globals</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Env</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>27</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>globals</span><span>.</span><span>borrow_mut</span><span>();</span></div></div><div><div><div>28</div></div><div><span>            </span><span>for</span><span> </span><span>function</span><span> </span><span>in</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>BUILTINS</span><span> {</span></div></div><div><div><div>29</div></div><div><span>                </span><span>scope</span><span>.</span><span>define</span><span>(</span><span>function</span><span><span>.name, </span><span>Value</span><span>::</span></span><span>Native</span><span>(</span><span>function</span><span>));</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>33</div></div><div><span>            </span><span>globals</span><span>,</span></div></div><div><div><div>34</div></div><div><span>            </span><span>out</span><span>,</span></div></div><div><div><div>35</div></div><div><span>            </span><span>depth</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>36</div></div><div><span>            </span><span>// Kept well below what the host stack can take, so a runaway</span></div></div><div><div><div>37</div></div><div><span>            </span><span>// recursion produces a clean Pebble error instead of a SIGSEGV.</span></div></div><div><div><div>38</div></div><div><span>            </span><span>// Bump it with `set_max_depth` if you need deeper recursion.</span></div></div><div><div><div>39</div></div><div><span>            </span><span>max_depth</span><span>: </span><span>128</span><span>,</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>max_depth: 128</code> 的来历在后面”递归深度”一节有一个实验。接下来是 getter 和一个让 Rust 代码往全局环境里塞值的 <code>define_global</code>（这一块包含第一步已经写过的 <code>globals()</code>，整块替换就行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The global scope.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>globals</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>self</span><span>.globals)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>/// The current recursion limit.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>max_depth</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span>.max_depth</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>/// Sets the recursion limit.</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>set_max_depth</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>depth</span><span>: </span><span>usize</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span><span>.max_depth </span><span>=</span><span> </span></span><span>depth</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>/// Defines a value in the global scope from Rust.</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>define_global</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>value</span><span>: </span><span>Value</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span>.globals.</span><span>borrow_mut</span><span>().</span><span>define</span><span>(</span><span>name</span><span>, </span><span>value</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>define_global(&amp;mut self, name: impl Into&lt;String&gt;, value: Value)</code> 用的是第 2 篇的 <code>impl Into&lt;String&gt;</code>：调用者传 <code>&amp;str</code> 或 <code>String</code> 都行。</p><p><code>interpret</code> 多一行 <code>self.depth = 0;</code>（每次从头开始），后面再写一个对外暴露的 <code>call</code>，让 Rust 调用者能调用 Pebble 函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Evaluates a whole program, returning the value of the last statement.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>interpret</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span><span>.depth </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>globals</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>self</span><span>.globals);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span>.</span><span>execute_block</span><span>(&amp;</span><span>program</span><span>.stmts, </span><span>globals</span><span>)? {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>value</span><span><span>) </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>value</span><span>),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Flow</span><span>::</span><span>Break</span><span><span> </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Continue</span><span> =&gt; </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>Span</span><span>::</span><span>DUMMY</span><span>,</span></div></div><div><div><div>9</div></div><div><span>                </span><span>"`break` or `continue` used outside a loop"</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>)),</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>/// Calls a function value with the given arguments.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>call</span><span>(</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>17</div></div><div><span>        </span><span>callee</span><span>: </span><span>Value</span><span>,</span></div></div><div><div><div>18</div></div><div><span>        </span><span>args</span><span>: </span><span>Vec</span><span>&lt;</span><span>Value</span><span>&gt;,</span></div></div><div><div><div>19</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>self</span><span>.</span><span>call_value</span><span>(</span><span>callee</span><span>, </span><span>args</span><span>, </span><span>span</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>函数声明</strong>是 <code>execute_stmt</code> 里的新分支。现在所有语句都有分支了，把兜底的 <code>_ =&gt; todo!(...)</code> 删掉，把它放在 <code>Stmt::Let</code> 和 <code>Stmt::Expr</code> 之间（顺序和 tag 一致）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Fn</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>name</span><span>, </span><span>params</span><span>, </span><span>body</span><span>, ..</span></div></div><div><div><div>3</div></div><div><span><span>            </span></span><span>} =&gt; {</span></div></div><div><div><div>4</div></div><div><span>                </span><span>let</span><span> </span><span>function</span><span><span> </span><span>=</span><span> </span></span><span>Function</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>5</div></div><div><span>                    </span><span>Some</span><span>(</span><span>name</span><span>.name.</span><span>clone</span><span>()),</span></div></div><div><div><div>6</div></div><div><span>                    </span><span>params</span><span>.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>p</span><span><span>|</span><span> </span></span><span>p</span><span>.name.</span><span>clone</span><span>()).</span><span>collect</span><span>(),</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>body</span><span>.</span><span>clone</span><span>(),</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>Rc</span><span>::</span><span>clone</span><span>(</span><span>env</span><span>),</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>10</div></div><div><span>                </span><span>env</span><span>.</span><span>borrow_mut</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>                    </span></span><span>.</span><span>define</span><span>(</span><span>name</span><span>.name.</span><span>clone</span><span><span>(), </span><span>Value</span><span>::</span></span><span>Function</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span>(</span><span>function</span><span>)));</span></div></div><div><div><div>12</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Flow</span><span>::</span><span>Value</span><span><span>(</span><span>Value</span><span>::</span></span><span>Nil</span><span>))</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有两个值得讲的细节。第一，<code>Function::new(...)</code> 的第四个参数是 <code>Rc::clone(env)</code>：<strong>函数在声明的那一刻把当时的环境指针记下来</strong>，这就是闭包的”闭”。第二，<code>body.clone()</code> 是整个函数体的深拷贝（<code>Vec&lt;Stmt&gt;</code>）：每次执行到一个 <code>fn</code> 语句，都会复制一遍它的 AST。我没有改这件事，因为它不影响正确性，只影响性能；要是想改，把 <code>body</code> 换成 <code>Rc&lt;[Stmt]&gt;</code> 是最自然的做法，这留给你。</p><p>匿名函数 <code>fn(x) { ... }</code> 是表达式，没有名字，其余完全一样。下面这个分支和后面的 <code>Call</code> 分支都加在 <code>eval</code> 里 <code>Assign</code> 之后、<code>_ =&gt; todo!(...)</code> 之前，<code>Call</code> 在前，<code>Fn</code> 在后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Fn</span><span> { </span><span>params</span><span>, </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>function</span><span><span> </span><span>=</span><span> </span></span><span>Function</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>3</div></div><div><span>                    </span><span>None</span><span>,</span></div></div><div><div><div>4</div></div><div><span>                    </span><span>params</span><span>.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>p</span><span><span>|</span><span> </span></span><span>p</span><span>.name.</span><span>clone</span><span>()).</span><span>collect</span><span>(),</span></div></div><div><div><div>5</div></div><div><span>                    </span><span>body</span><span>.</span><span>clone</span><span>(),</span></div></div><div><div><div>6</div></div><div><span><span>                    </span></span><span>Rc</span><span>::</span><span>clone</span><span>(</span><span>env</span><span>),</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>);</span></div></div><div><div><div>8</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Function</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span>(</span><span>function</span><span>)))</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>调用</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Call</span><span> { </span><span>callee</span><span>, </span><span>args</span><span>, </span><span>span</span><span> } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>callee</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>callee</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>3</div></div><div><span>                </span><span>let</span><span> </span><span>args</span><span><span> </span><span>=</span><span> </span></span><span>args</span></div></div><div><div><div>4</div></div><div><span><span>                    </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>                    </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>arg</span><span><span>|</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>arg</span><span>, </span><span>env</span><span>))</span></div></div><div><div><div>6</div></div><div><span><span>                    </span></span><span>.</span><span>collect</span><span>::&lt;</span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;, </span><span>_</span><span>&gt;&gt;()?;</span></div></div><div><div><div>7</div></div><div><span>                </span><span>self</span><span>.</span><span>call_value</span><span>(</span><span>callee</span><span>, </span><span>args</span><span>, *</span><span>span</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>args.iter().map(|arg| self.eval(arg, env)).collect::&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()?</code> 是这个文件里最值得停下来看的一行 Rust。<code>map</code> 对每个实参调用 <code>eval</code>，得到的是一串 <code>Result&lt;Value, RuntimeError&gt;</code>；<code>collect</code> 要把它们收成一个 <code>Vec</code>。标准库为 <code>Result</code> 实现了”收集”：如果每个元素都是 <code>Ok</code>，就得到 <code>Ok(Vec&lt;Value&gt;)</code>；只要有一个是 <code>Err</code>，收集在遇到它的那一刻停下来，整个结果是那个 <code>Err</code>。turbofish <code>::&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;</code> 告诉 <code>collect</code> 目标类型，<code>_</code> 让编译器自己推出元素类型。结尾的 <code>?</code> 把 <code>Err</code> 往上传。用 C++ 写，你会写一个循环，每次 <code>if (!r) return r.error()</code>。</p><p>我们还要写三个方法：<code>call_value</code> 处理调用，<code>run_function</code> 执行函数体，<code>with_call_depth</code> 守住递归深度。它们放在 <code>eval</code> 之后，用一行 <code>// --- calls ---</code> 注释隔开，仍然在同一个 <code>impl</code> 块里（<code>with_call_depth</code> 之后才是收尾的 <code>}</code>）。调用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- calls ------------------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>call_value</span><span>(</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>callee</span><span>: </span><span>Value</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>args</span><span>: </span><span>Vec</span><span>&lt;</span><span>Value</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>match</span><span> </span><span>callee</span><span> {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>Value</span><span>::</span><span>Native</span><span>(</span><span>function</span><span>) =&gt; {</span></div></div><div><div><div>11</div></div><div><span>                </span><span>let</span><span> </span><span>out</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> *</span><span>self</span><span>.out;</span></div></div><div><div><div>12</div></div><div><span>                </span><span>call_native</span><span>(</span><span>function</span><span>, </span><span>out</span><span>, </span><span>span</span><span>, &amp;</span><span>args</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Value</span><span>::</span><span>Function</span><span>(</span><span>function</span><span>) =&gt; {</span></div></div><div><div><div>15</div></div><div><span>                </span><span>if</span><span> </span><span>args</span><span>.</span><span>len</span><span><span>() </span><span>!=</span><span> </span></span><span>function</span><span>.params.</span><span>len</span><span>() {</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>17</div></div><div><span>                        </span><span>span</span><span>,</span></div></div><div><div><div>18</div></div><div><span>                        </span><span>format!</span><span>(</span></div></div><div><div><div>19</div></div><div><span>                            </span><span>"{} expects {} argument(s), got {}"</span><span>,</span></div></div><div><div><div>20</div></div><div><span>                            </span><span>function</span><span>.</span><span>display_name</span><span>(),</span></div></div><div><div><div>21</div></div><div><span>                            </span><span>function</span><span>.params.</span><span>len</span><span>(),</span></div></div><div><div><div>22</div></div><div><span>                            </span><span>args</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>23</div></div><div><span><span>                        </span></span><span>),</span></div></div><div><div><div>24</div></div><div><span><span>                    </span></span><span>));</span></div></div><div><div><div>25</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>                </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>with_call_depth</span><span>(</span><span>span</span><span><span>, </span><span>|</span></span><span>this</span><span><span>|</span><span> </span></span><span>this</span><span>.</span><span>run_function</span><span>(&amp;</span><span>function</span><span>, </span><span>args</span><span>));</span></div></div><div><div><div>27</div></div><div><span>                </span><span>result</span><span>.</span><span>map_err</span><span><span>(</span><span>|</span></span><span>error</span><span><span>|</span><span> </span></span><span>error</span><span>.</span><span>with_frame</span><span>(</span><span>function</span><span>.</span><span>display_name</span><span>(), </span><span>span</span><span>))</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>30</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>31</div></div><div><span>                </span><span>format!</span><span>(</span><span>"{} is not callable"</span><span>, </span><span>other</span><span>.</span><span>type_name</span><span>()),</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>)),</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>match callee</code> 直接按值匹配，<code>callee</code> 在这里被消耗。两条主要路径：</p><ul>
<li><code>Value::Native(function)</code>：内建函数。<code>&amp;mut *self.out</code> 先解引用 <code>Box&lt;dyn Output&gt;</code> 得到 <code>dyn Output</code>，再取可变借用，变成 <code>&amp;mut dyn Output</code>，交给 <code>call_native</code>（第 8 篇）。</li>
<li><code>Value::Function(function)</code>：用户函数。先检查参数个数（报错信息里 <code>display_name()</code> 对匿名函数给 <code>&lt;anonymous&gt;</code>），然后把真正的调用交给 <code>with_call_depth</code>，出错时用 <code>map_err</code> 追加一层调用栈（<code>with_frame</code>），所以报错信息能列出整条 <code>in inner ... in outer ...</code>。</li>
</ul><p>最后一个分支 <code>other =&gt; Err(...)</code> 处理”调用一个不是函数的东西”，比如 <code>let x = 1; x();</code>。</p><p>函数体由 <code>run_function</code> 执行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>run_function</span><span>(</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>3</div></div><div><span>        </span><span>function</span><span>: &amp;</span><span>Rc</span><span>&lt;</span><span>Function</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>args</span><span>: </span><span>Vec</span><span>&lt;</span><span>Value</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>env</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>function</span><span>.env));</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>8</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>env</span><span>.</span><span>borrow_mut</span><span>();</span></div></div><div><div><div>9</div></div><div><span>            </span><span>for</span><span> (</span><span>param</span><span>, </span><span>arg</span><span>) </span><span>in</span><span> </span><span>function</span><span>.params.</span><span>iter</span><span>().</span><span>zip</span><span>(</span><span>args</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>scope</span><span>.</span><span>define</span><span>(</span><span>param</span><span>.</span><span>clone</span><span>(), </span><span>arg</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span>.</span><span>execute_block</span><span>(&amp;</span><span>function</span><span>.body, </span><span>env</span><span>)? {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Flow</span><span>::</span><span>Return</span><span>(</span><span>value</span><span><span>) </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Value</span><span>(</span><span>value</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>value</span><span>),</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Flow</span><span>::</span><span>Break</span><span><span> </span><span>|</span><span> </span></span><span>Flow</span><span>::</span><span>Continue</span><span> =&gt; </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>16</div></div><div><span>                </span><span>function</span><span>.body.</span><span>first</span><span>().</span><span>map_or</span><span><span>(</span><span>Span</span><span>::</span></span><span>DUMMY</span><span><span>, </span><span>Spanned</span><span>::</span></span><span>span</span><span>),</span></div></div><div><div><div>17</div></div><div><span>                </span><span>"`break` or `continue` escaped a function body"</span><span>,</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>)),</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三步。<strong>第一，建一个新环境，它的父是函数捕获的环境</strong>（<code>function.env</code>，不是调用者的环境，这是词法作用域和动态作用域的区别）。<strong>第二，把形参和实参配对定义进去。</strong> <code>function.params.iter().zip(args)</code> 把两个迭代器拉成一串二元组；<code>args</code> 是 <code>Vec&lt;Value&gt;</code>，所以 <code>zip(args)</code> 消耗它，每个 <code>arg</code> 是 <code>Value</code>（不是引用）。<strong>第三，执行函数体</strong>，<code>Flow::Return(value) | Flow::Value(value)</code> 把两种正常结束都当作返回值，<code>Break</code> 或 <code>Continue</code> 溢出到函数边界是用户程序的错误。<code>function.body.first().map_or(Span::DUMMY, Spanned::span)</code> 里 <code>Spanned::span</code> 是把一个 trait 方法当作普通函数值传给 <code>map_or</code>：空函数体没有第一条语句，就用占位 span。</p></section><section><h3>with_call_depth：为什么是 FnOnce<a href="#with_call_depth为什么是-fnonce"><span>#</span></a></h3><p>现在是 <code>with_call_depth</code>，最后一块：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Runs `body` one call level deeper, failing first if that would exceed the limit.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>with_call_depth</span><span>&lt;</span><span>T</span><span>&gt;(</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>body</span><span>: </span><span>impl</span><span> </span><span>FnOnce</span><span>(&amp;</span><span>mut</span><span> </span><span>Self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>T</span><span>, </span><span>RuntimeError</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>T</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.depth </span><span>&gt;=</span><span> </span></span><span>self</span><span>.max_depth {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>9</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>10</div></div><div><span>                </span><span>format!</span><span>(</span><span>"call stack overflow (recursion limit {})"</span><span>, </span><span>self</span><span>.max_depth),</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>));</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span><span>.depth </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>body</span><span>(</span><span>self</span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span><span>.depth </span><span>-=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>16</div></div><div><span>        </span><span>result</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它做的事是一个”括号”：先检查深度，加一，运行 <code>body</code>，减一。这种”前置、中间、后置”在 C++ 里通常用 RAII 守卫对象，一个构造时加一、析构时减一的小类。这里我选了闭包：调用者传一段代码，它在加一减一之间运行。好处是<strong>不可能忘了减一</strong>，也不可能在中间插入一个 <code>?</code> 提前返回而漏掉后置。</p><p>参数写的是 <code>impl FnOnce(&amp;mut Self) -&gt; Result&lt;T, RuntimeError&gt;</code>。为什么是 <code>FnOnce</code>？回头看调用点：<code>|this| this.run_function(&amp;function, args)</code>。闭包体里 <code>args</code> 被<strong>移动</strong>进了 <code>run_function</code>，所以这个闭包只能被调用一次，它实现的是 <code>FnOnce</code>，不是 <code>Fn</code>，也不是 <code>FnMut</code>。如果我把参数的 trait 写成 <code>Fn</code>，编译器就会反对（这是真实输出，来自我们当前这个文件）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0507]: cannot move out of `args`, a captured variable in an `Fn` closure</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-interp/src/lib.rs:343:93</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>323 |         args: Vec&lt;Value&gt;,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|         ----  ---------- move occurs because `args` has type `Vec&lt;Value&gt;`, which does not implement the `Copy` trait</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|         |</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|         captured outer variable</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div><div><div><div>9</div></div><div><span>343 |                 let result = self.with_call_depth(span, |this| this.run_function(&amp;function, args));</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|                                                         ------                              ^^^^ `args` is moved here</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|                                                         |</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>|                                                         captured by this `Fn` closure</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consume them only once</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-interp/src/lib.rs:378:20</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>17</div></div><div><span>378 |         body: impl Fn(&amp;mut Self) -&gt; Result&lt;T, RuntimeError&gt;,</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>|                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>19</div></div><div><span>help: consider cloning the value if the performance cost is acceptable</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>21</div></div><div><span>343 |                 let result = self.with_call_depth(span, |this| this.run_function(&amp;function, args.clone()));</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>|                                                                                                 ++++++++</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>For more information about this error, try `rustc --explain E0507`.</span></div></div><div><div><div>25</div></div><div><span>error: could not compile `pebble-interp` (lib) due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（行号是我当前这个文件里的，你的可能不同。）</p><p>编译器连”该用 <code>FnOnce</code>”都告诉了你。</p><p>另一个问题：闭包为什么要接收 <code>this: &amp;mut Self</code>，而不是直接用外面的 <code>self</code>？试试把 <code>|this| this.run_function(...)</code> 改成 <code>|_this| self.run_function(...)</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0501]: cannot borrow `*self` as mutable because previous closure requires unique access</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-interp/src/lib.rs:343:30</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>343 |                 let result = self.with_call_depth(span, |_this| self.run_function(&amp;function, args));</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                              ^^^^^---------------^^^^^^^-------^----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|                              |    |                     |       |</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|                              |    |                     |       first borrow occurs due to use of `*self` in closure</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>|                              |    |                     closure construction occurs here</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|                              |    first borrow later used by call</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|                              second borrow occurs here</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>error[E0500]: closure requires unique access to `*self` but it is already borrowed</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-interp/src/lib.rs:343:57</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>343 |                 let result = self.with_call_depth(span, |_this| self.run_function(&amp;function, args));</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>|                              ---- ---------------       ^^^^^^^ ---- second borrow occurs due to use of `*self` in closure</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>|                              |    |                     |</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>|                              |    |                     closure construction occurs here</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|                              |    first borrow later used by call</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>|                              borrow occurs here</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>Some errors have detailed explanations: E0500, E0501.</span></div></div><div><div><div>23</div></div><div><span>For more information about an error, try `rustc --explain E0500`.</span></div></div><div><div><div>24</div></div><div><span>error: could not compile `pebble-interp` (lib) due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>self.with_call_depth(...)</code> 已经可变借用了 <code>*self</code>，闭包里再用 <code>self</code> 就是第二个可变借用，违反第 2 篇的”可变引用独占”。解决办法是 <code>with_call_depth</code> 自己把 <code>self</code> 借给闭包：调用它的时候，它拿着 <code>&amp;mut self</code>，在 <code>body(self)</code> 里把它<strong>转交</strong>给闭包，这样同一时刻只有一个人持有。这个签名里的 <code>&amp;mut Self</code> 参数就是这么来的。</p><p>我在实验室里把同一个问题缩成十行，文件是 <code>~/rust-lab/src/bin/ch09_fn_bound.rs</code>：</p><div><figure><figcaption><span>~/rust-lab/src/bin/ch09_fn_bound.rs</span></figcaption><pre><code><div><div><div>1</div></div><div><span>// A cut-down `with_call_depth`: `body` is declared `Fn`, but we move `args` into it.</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>with_depth</span><span>&lt;</span><span>T</span><span>&gt;(</span><span>body</span><span>: </span><span>impl</span><span> </span><span>Fn</span><span>() -&gt; </span><span>T</span><span>) -&gt; </span><span>T</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>body</span><span>()</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>args</span><span>: </span><span>Vec</span><span>&lt;</span><span>i64</span><span><span>&gt; </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>];</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>total</span><span><span> </span><span>=</span><span> </span></span><span>with_depth</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>consumed</span><span><span> </span><span>=</span><span> </span></span><span>args</span><span>; </span><span>// moves `args` out of the closure's captured state</span></div></div><div><div><div>10</div></div><div><span>        </span><span>consumed</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{total}"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch09_fn_bound</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0507]: cannot move out of `args`, a captured variable in an `Fn` closure</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch09_fn_bound.rs:10:24</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>8 |     let args: Vec&lt;i64&gt; = vec![1, 2, 3];</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|         ---- captured outer variable</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>9 |     let total = with_depth(move || {</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|                            ------- captured by this `Fn` closure</span></div></div><div><div><div>10</div></div><div><span>10 |         let consumed = args; // moves `args` out of the closure's captured state</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|                        ^^^^ move occurs because `args` has type `Vec&lt;i64&gt;`, which does not implement the `Copy` trait</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consume them only once</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch09_fn_bound.rs:3:29</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>3 | fn with_depth&lt;T&gt;(body: impl Fn() -&gt; T) -&gt; T {</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>|                             ^^^^^^^^^</span></div></div><div><div><div>18</div></div><div><span>help: consider borrowing here</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>20</div></div><div><span>10 |         let consumed = &amp;args; // moves `args` out of the closure's captured state</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>|                        +</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>For more information about this error, try `rustc --explain E0507`.</span></div></div><div><div><div>24</div></div><div><span>error: could not compile `rust-lab` (bin "ch09_fn_bound") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>两个测试<a href="#两个测试"><span>#</span></a></h3><p>先把 <code>tests</code> 模块的头换成最终形态，它多了一个 <code>run_with_output</code>，用来观察 <code>print</code> 写出了什么（<code>Shared</code> 是一个临时定义在函数里的类型，包着一个 <code>Rc&lt;RefCell&lt;Buffer&gt;&gt;</code>；你已经在第 8 篇见过 <code>Buffer</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_parser</span><span>::parse;</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>Buffer</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>run</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert!</span><span>(</span><span>result</span><span>.</span><span>is_ok</span><span>(), </span><span>"parse errors: {:?}"</span><span>, </span><span>result</span><span>.diagnostics);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>result</span><span>.program)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>run_with_output</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; (</span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt;, </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>struct</span><span> </span><span>Shared</span><span>(</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Buffer</span><span>&gt;&gt;);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>impl</span><span> </span><span>Output</span><span> </span><span>for</span><span> </span><span>Shared</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>19</div></div><div><span>                </span><span>self</span><span>.</span><span>0.</span><span>borrow_mut</span><span>().</span><span>write_line</span><span>(</span><span>line</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>shared</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>23</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>Shared</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>shared</span><span>))));</span></div></div><div><div><div>24</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>result</span><span>.program);</span></div></div><div><div><div>26</div></div><div><span>        </span><span>let</span><span> </span><span>lines</span><span><span> </span><span>=</span><span> </span></span><span>shared</span><span>.</span><span>borrow</span><span>().</span><span>lines</span><span>().</span><span>to_vec</span><span>();</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>(</span><span>value</span><span>, </span><span>lines</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>然后写两个测试。闭包：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>closures_capture_their_environment</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>"</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>fn make_counter() {</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>let n = 0;</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>return fn() { n = n + 1; return n; };</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>let c = make_counter();</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>c(); c(); c();</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>"</span><span>;</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>src</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>3</span><span>));</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make_counter</code> 返回一个匿名函数，匿名函数的 <code>env</code> 就是 <code>make_counter</code> 这次调用的环境，<code>n</code> 住在里面。<code>let c = make_counter();</code> 之后三次调用 <code>c()</code>，每次都改那个共享的 <code>n</code>，最后一次得到 <code>3</code>。这就是 <code>examples/closures.pebble</code> 的核心。</p><p>递归：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>recursion_works</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>"</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>fn fact(n) {</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>if n &lt; 2 { return 1; }</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>return n * fact(n - 1);</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>fact(10);</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>"</span><span>;</span></div></div><div><div><div>10</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>src</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>3_628_800</span><span>));</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>你手里现在有 5 个测试。tag 的测试模块里还有 7 个同一类的测试（下面按名字列出，不是整段的行范围，因为那个范围里还有后面几步才写的测试）：<code>control_flow_loops</code>、<code>print_reaches_the_output_sink</code>、<code>runtime_errors_are_reported_with_a_trace</code>、<code>recursive_overflow_is_caught</code>、<code>arity_errors_are_reported</code>、<code>calling_a_non_function_is_an_error</code>、<code>type_errors_propagate_from_operators</code>。它们没有新的 Rust 概念（<code>recursive_overflow_is_caught</code> 下一节单独讲），你可以现在就从 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/src/lib.rs#L533-L726" target="_blank">tag</a> 复制进来，也可以留到最后一次性换成 tag 的版本。我是现在就复制了，得到的是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-interp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 12 tests</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>递归深度：一个实验<a href="#递归深度一个实验"><span>#</span></a></h3><p>解释器里每一层 Pebble 调用都对应好几层 Rust 调用：<code>eval</code> → <code>call_value</code> → <code>with_call_depth</code> → 闭包 → <code>run_function</code> → <code>execute_block</code> → <code>execute_stmt</code> → <code>eval</code> ……，所有这些都在<strong>真正的 C 栈</strong>上。Pebble 程序写一个无限递归，不拦着它，Rust 会把整个进程的栈耗光，然后进程被信号杀死。<code>max_depth</code> 就是在那之前自己报错。</p><p>我最初把限制设成 512，因为听上去合理。做了一个实验：在仓库的一个临时 worktree 里把 <code>max_depth</code> 改成 512，在<strong>默认大小的测试线程</strong>上运行同样的无限递归：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>overflow_on_the_default_test_stack</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>run</span><span>(</span><span>"fn loop() { return loop(); } loop();"</span><span>).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>assert!</span><span>(</span><span>error</span><span>.</span><span>message</span><span>().</span><span>contains</span><span>(</span><span>"stack overflow"</span><span>), </span><span>"{error}"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-interp overflow_on_the_default</span></div></div><div><div><div>2</div></div><div><span>running 1 test</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>thread 'tests::overflow_on_the_default_test_stack' (118017) has overflowed its stack</span></div></div><div><div><div>5</div></div><div><span>fatal runtime error: stack overflow, aborting</span></div></div><div><div><div>6</div></div><div><span>error: test failed, to rerun pass `-p pebble-interp --lib`</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>Caused by:</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>process didn't exit successfully: `/tmp/target-repro-09/debug/deps/pebble_interp-73b1f9eb2ef807c8 overflow_on_the_default` (signal: 6, SIGABRT: process abort signal)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个测试进程被 <code>SIGABRT</code> 带走了，我们没有看到 Pebble 的任何错误信息，连断言都没来得及跑。同样的测试把限制换回 128：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test tests::overflow_on_the_default_test_stack ... ok</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我又试了 192、256、320、384、448，全部栈溢出。所以在这台机器上、debug 构建下，测试线程默认的 2 MiB 栈能撑过 128 层、撑不过 192 层，换算下来每层 Pebble 调用大约吃 11 到 16 KiB 栈（这是我用两端界限估的，不是测出来的精确值）。命令行的主线程是 8 MiB，所以同样的 512 在 <code>pebble run</code> 里没问题（1024 就不行了，主线程也会栈溢出），这也是限制值可以按平台调整的原因，<code>set_max_depth</code> 就是给它留的。</p><p>仓库里的测试 <code>recursive_overflow_is_caught</code> 没有用默认线程，而是自己开了一个 64 MiB 的线程：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>recursive_overflow_is_caught</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>// Reaching the recursion limit needs far more host stack than a default</span></div></div><div><div><div>4</div></div><div><span>        </span><span>// test thread has, and `Value` is not `Send`, so the assertion runs</span></div></div><div><div><div>5</div></div><div><span>        </span><span>// inside a dedicated large-stack thread.</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>std</span><span>::</span><span>thread</span><span>::</span><span>Builder</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>stack_size</span><span>(</span><span>64</span><span> * </span><span>1024</span><span> * </span><span>1024</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>spawn</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>9</div></div><div><span>                </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>"fn loop() { return loop(); } loop();"</span><span>;</span></div></div><div><div><div>10</div></div><div><span>                </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span><span>Interpreter</span><span>::</span></span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>11</div></div><div><span>                </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>12</div></div><div><span>                </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>result</span><span>.program).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>13</div></div><div><span>                </span><span>assert!</span><span>(</span><span>error</span><span>.</span><span>message</span><span>().</span><span>contains</span><span>(</span><span>"stack overflow"</span><span>), </span><span>"{error}"</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>})</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>.</span><span>expect</span><span>(</span><span>"spawn overflow thread"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>.</span><span>join</span><span>()</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>.</span><span>expect</span><span>(</span><span>"overflow test panicked"</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>std::thread::Builder</code> 是 <code>thread::spawn</code> 的”可配置版”，能设置栈大小和线程名。<code>.spawn(closure)</code> 返回 <code>io::Result&lt;JoinHandle&lt;T&gt;&gt;</code>（线程创建可能失败，所以是 <code>Result</code>），<code>.expect(...)</code> 解包，<code>.join()</code> 等线程结束并取回返回值，<code>.expect(...)</code> 再解一次。<strong>整个测试在线程里面</strong>，因为 <code>Value</code> 是 <code>!Send</code>（里面有 <code>Rc</code>，第 14 篇细讲 <code>Send</code>）：不能在外面建好解释器再送进线程，所以解释器是在闭包里面创建的。至于为什么给 64 MiB：测试里的注释说”需要远多于默认测试线程的栈”，但按上面的实验，在这台机器的 debug 构建下，限制是 128 的时候默认栈就够了，所以这 64 MiB 是留的余量，不是必需；以后谁把限制调高，这个测试不会因为栈不够而误报。</p><p>至此：</p><p>在第二版命令行里（马上就写），这个无限递归程序 <code>forever.pebble</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn forever(n) {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>return forever(n + 1);</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div><span>forever(0);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>给出的是一条干净的 Pebble 错误：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run forever.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E2001]: call stack overflow (recursion limit 128)</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; forever.pebble:2:12</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>2 |     return forever(n + 1);</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|            ^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>in `forever` at forever.pebble:2:12</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>in `forever` at forever.pebble:2:12</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>in `forever` at forever.pebble:2:12</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>...</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>in `forever` at forever.pebble:2:12</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>in `forever` at forever.pebble:4:1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调用栈一共 129 帧（文件是 135 行），我截掉了中间：128 帧是 <code>forever</code> 自己在第 2 行的递归调用，最后一帧（<code>at ...:4:1</code>）是顶层的 <code>forever(0);</code>。</p></section></section>
<section><h2>第四步：列表、映射和下标<a href="#第四步列表映射和下标"><span>#</span></a></h2><p>最后补上集合。先更新文件头：多了 <code>HashMap</code> 和 <code>MapKey</code>，再加一行 <code>#![allow(...)]</code>。后者的来历：map 字面量建的是 <code>HashMap&lt;MapKey, Value&gt;</code>，而 <code>MapKey</code> 里有 <code>Rc&lt;RefCell&lt;..&gt;&gt;</code>，clippy 会抗议（第 8 篇见过同一条）。不加它，<code>cargo clippy</code> 给出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>warning: mutable key type</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-interp/src/lib.rs:259:17</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>259 |                 let mut map = HashMap::with_capacity(entries.len());</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>= note: ... because it contains `MapKey`, which has interior mutability</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>= note: ... because it contains `Value`, which has interior mutability</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>= note: ... because it contains `Rc&lt;RefCell&lt;Vec&lt;Value&gt;&gt;&gt;`, which has interior mutability</span></div></div><div><div><div>10</div></div><div><span>...</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>= note: `-W clippy::mutable-key-type` implied by `-W clippy::all`</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>= help: to override `-W clippy::all` add `#[allow(clippy::mutable_key_type)]`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>理由和第 8 篇的一样：<code>MapKey</code> 的 <code>Hash</code> 看的是值本身，而列表、map 这类带内部可变性的键按指针身份哈希，所以不会在键被插入之后悄悄改变哈希值。新的文件头是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div><span>#![allow(clippy</span><span>::</span><span>mutable_key_type)]</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>collections</span><span>::</span></span><span>HashMap</span><span>;</span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::{</span></span><span>BinaryOp</span><span>, </span><span>Expr</span><span>, </span><span>LogicalOp</span><span>, </span><span>Program</span><span>, </span><span>Spanned</span><span>, </span><span>Stmt</span><span>, </span><span>UnaryOp</span><span>};</span></div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::{</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>Env</span><span>, </span><span>Function</span><span>, </span><span>MapKey</span><span>, </span><span>Output</span><span>, </span><span>RuntimeError</span><span>, </span><span>Stdout</span><span>, </span><span>Value</span><span>, call_native, compare, div, mul,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>negate, rem, sub,</span></div></div><div><div><div>13</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后给 <code>eval</code> 补四个分支。字面量，放在 <code>Var</code> 之后：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>List</span><span> { </span><span>items</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>let</span><span> </span><span>values</span><span><span> </span><span>=</span><span> </span></span><span>items</span></div></div><div><div><div>3</div></div><div><span><span>                    </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>                    </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>item</span><span><span>|</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>item</span><span>, </span><span>env</span><span>))</span></div></div><div><div><div>5</div></div><div><span><span>                    </span></span><span>.</span><span>collect</span><span>::&lt;</span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;, </span><span>_</span><span>&gt;&gt;()?;</span></div></div><div><div><div>6</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>list</span><span>(</span><span>values</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Map</span><span> { </span><span>entries</span><span>, .. } =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>HashMap</span><span>::</span><span>with_capacity</span><span>(</span><span>entries</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>10</div></div><div><span>                </span><span>for</span><span> (</span><span>key</span><span>, </span><span>value</span><span>) </span><span>in</span><span> </span><span>entries</span><span> {</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>let</span><span> </span><span>key</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>key</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>12</div></div><div><span>                    </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>value</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>13</div></div><div><span>                    </span><span>map</span><span>.</span><span>insert</span><span>(</span><span>MapKey</span><span>(</span><span>key</span><span>), </span><span>value</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Map</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>map</span><span>))))</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里又出现了 <code>collect::&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()?</code>。Map 字面量用的是 <code>HashMap::with_capacity</code> 预分配，<code>MapKey(key)</code> 是第 8 篇的包装类型。注意 <code>Value::Map(Rc::new(RefCell::new(map)))</code>：新建的 map 从一开始就是共享的。</p><p>赋值的下标分支，放在 <code>Expr::Var</code> 目标分支后面、<code>other</code> 兜底前面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>                    </span><span>Expr</span><span>::</span><span>Index</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                        </span><span>object</span><span>,</span></div></div><div><div><div>3</div></div><div><span>                        </span><span>index</span><span>,</span></div></div><div><div><div>4</div></div><div><span>                        </span><span>span</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>                    </span></span><span>} =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                        </span><span>let</span><span> </span><span>container</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>object</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>7</div></div><div><span>                        </span><span>let</span><span> </span><span>key</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>index</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>8</div></div><div><span>                        </span><span>assign_index</span><span>(&amp;</span><span>container</span><span>, &amp;</span><span>key</span><span>, </span><span>new_value</span><span>.</span><span>clone</span><span>(), *</span><span>span</span><span>)?;</span></div></div><div><div><div>9</div></div><div><span>                        </span><span>Ok</span><span>(</span><span>new_value</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>                    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读下标，放在 <code>Call</code> 和 <code>Fn</code> 分支之间：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Index</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>object</span><span>,</span></div></div><div><div><div>3</div></div><div><span>                </span><span>index</span><span>,</span></div></div><div><div><div>4</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>} =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>let</span><span> </span><span>container</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>object</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>7</div></div><div><span>                </span><span>let</span><span> </span><span>key</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>eval</span><span>(</span><span>index</span><span>, </span><span>env</span><span>)?;</span></div></div><div><div><div>8</div></div><div><span>                </span><span>index_value</span><span>(&amp;</span><span>container</span><span>, &amp;</span><span>key</span><span>, *</span><span>span</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现在 <code>eval</code> 里不再需要 <code>_ =&gt; todo!()</code> 了，把它删掉。需要三个辅助函数，放在 <code>eval_binary</code> 后面、<code>iterable_span</code> 前面，都是普通函数，不需要 <code>self</code>。读和写的实现：</p><ul>
<li><code>index_value</code>：<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/src/lib.rs#L458-L479" target="_blank">tag 的第 458 到 479 行</a>，按 <code>Value</code> 的种类分三路：列表、字符串、映射。字符串下标要先 <code>chars().collect()</code>，因为第 2 篇说过 UTF-8 字符串不能按字节偏移当下标。</li>
<li><code>assign_index</code>：<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/src/lib.rs#L481-L502" target="_blank">第 481 到 502 行</a>，列表和映射可以写，字符串不行（报”cannot assign into string”）。</li>
</ul><p>这两个函数没有新的 Rust 概念，复制过去就行，这里省得你读。第三个辅助函数 <code>resolve_index</code> 值得写，因为它是个泛型函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Turns an index value into a `usize`, supporting negative indices.</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>resolve_index</span><span>&lt;</span><span>T</span><span>&gt;(</span><span>items</span><span>: &amp;[</span><span>T</span><span>], </span><span>key</span><span>: &amp;</span><span>Value</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>usize</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>raw</span><span><span>) </span><span>=</span><span> </span></span><span>key</span><span> </span><span>else</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>5</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>6</div></div><div><span>            </span><span>format!</span><span>(</span><span>"list index must be an int, got {}"</span><span>, </span><span>key</span><span>.</span><span>type_name</span><span>()),</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>));</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>len</span><span><span> </span><span>=</span><span> </span></span><span>items</span><span>.</span><span>len</span><span>() </span><span>as</span><span> </span><span>i64</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> *</span><span>raw</span><span><span> </span><span>&lt;</span><span> </span></span><span>0</span><span> { </span><span>len</span><span> + </span><span>raw</span><span> } </span><span>else</span><span> { *</span><span>raw</span><span> };</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> </span><span>index</span><span><span> </span><span>&lt;</span><span> </span></span><span>0</span><span><span> </span><span>||</span><span> </span></span><span>index</span><span><span> </span><span>&gt;=</span><span> </span></span><span>len</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>13</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>14</div></div><div><span>            </span><span>format!</span><span>(</span><span>"index {raw} is out of range for length {len}"</span><span>),</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>));</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>    </span><span>Ok</span><span>(</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>)</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>列表的元素是 <code>Value</code>，字符串的”元素”是 <code>char</code>，但把一个 Pebble 整数变成合法的 <code>usize</code> 下标，规则完全一样：必须是整数，负数从末尾数，越界报错。所以我们不关心元素是什么，只需要长度，签名写成 <code>items: &amp;[T]</code>，对任何 <code>T</code> 都成立。调用点：<code>resolve_index(&amp;items.borrow(), key, span)</code>（<code>&amp;Vec&lt;Value&gt;</code> 自动转成 <code>&amp;[Value]</code>，第 2 篇说的切片）和 <code>resolve_index(&amp;chars, key, span)</code>（<code>&amp;Vec&lt;char&gt;</code>）。编译器会为 <code>Value</code> 和 <code>char</code> 各生成一份专门的代码，也就是第 5 篇的单态化。</p><p>这里的 <code>len as i64</code> 和 <code>index as usize</code> 是 <code>as</code> 强转：长度几乎不可能超过 <code>i64::MAX</code>，而 <code>index</code> 在前面已经检查过落在 <code>0..len</code> 之间，所以转成 <code>usize</code> 一定不会丢信息。</p><p>补上测试，放在 <code>recursion_works</code> 后面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>lists_maps_and_indexing</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>4</div></div><div><span>            </span><span>run</span><span>(</span><span>"let xs = [10, 20, 30]; xs[1];"</span><span>).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>20</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>// Negative indices count from the end.</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>9</div></div><div><span>            </span><span>run</span><span>(</span><span>"let xs = [10, 20, 30]; xs[-1];"</span><span>).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>30</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>13</div></div><div><span>            </span><span>run</span><span>(</span><span>"let m = #{ </span><span>\"</span><span>a</span><span>\"</span><span>: 1 }; m[</span><span>\"</span><span>a</span><span>\"</span><span>];"</span><span>).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>17</div></div><div><span>            </span><span>run</span><span>(</span><span>"let xs = [1]; xs[0] = 5; xs[0];"</span><span>).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>18</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>5</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>23</div></div><div><span>    </span><span>fn</span><span> </span><span>string_concatenation_and_indexing</span><span>() {</span></div></div><div><div><div>24</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>"</span><span>\"</span><span>foo</span><span>\"</span><span> + </span><span>\"</span><span>bar</span><span>\"</span><span>;"</span><span>).</span><span>unwrap</span><span>().</span><span>to_string</span><span>(), </span><span>"foobar"</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>run</span><span>(</span><span>"</span><span>\"</span><span>abc</span><span>\"</span><span>[2];"</span><span>).</span><span>unwrap</span><span>().</span><span>to_string</span><span>(), </span><span>"c"</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>for 为什么先克隆<a href="#for-为什么先克隆"><span>#</span></a></h3><p>现在可以回答之前留下的问题：<code>for</code> 为什么先 <code>items.borrow().clone()</code> 一份快照。看这个 Pebble 程序，一边遍历列表，一边往同一个列表里推新元素：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let xs = [1, 2, 3];</span></div></div><div><div><div>2</div></div><div><span>for x in xs {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>push(xs, x * 10);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div><span>print(xs);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run grow.pebble</span></div></div><div><div><div>2</div></div><div><span>[1, 2, 3, 10, 20, 30]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>遍历用的是快照 <code>[1, 2, 3]</code>，所以只会跑三次，<code>push</code> 往真正的列表里加了三个，最后是 6 个元素，不会死循环。如果不拷贝呢？我在 worktree 里把这两行改成”拿着借用守卫直接遍历”（<code>let guard = items.borrow(); for item in guard.iter().cloned() { ... }</code>）。它能通过编译，Rust 不会拦你，因为 <code>RefCell</code> 把借用检查推迟到了运行时：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run grow.pebble</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>thread 'main' (118031) panicked at crates/pebble-value/src/lib.rs:912:11:</span></div></div><div><div><div>4</div></div><div><span>RefCell already borrowed</span></div></div><div><div><div>5</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>guard</code> 是 <code>Ref&lt;'_, Vec&lt;Value&gt;&gt;</code>，活到循环结束；循环体里 <code>push</code> 想要 <code>borrow_mut()</code>，此时有一个 <code>Ref</code> 还活着，<code>RefCell</code> 发现了冲突，当场 panic。这是第 8 篇 <code>ch08_refcell_panic</code> 那个实验的真实版本。</p><p>我在实验室里把它缩成最小的复现，文件是 <code>~/rust-lab/src/bin/ch09_iter_mutate.rs</code>：</p><div><div><div><figure><figcaption><span>~/rust-lab/src/bin/ch09_iter_mutate.rs</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>list</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]));</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>// The snapshot way: clone the Vec, loop over the copy, mutate the original.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>snapshot</span><span><span> </span><span>=</span><span> </span></span><span>list</span><span>.</span><span>borrow</span><span>().</span><span>clone</span><span>();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> </span><span>x</span><span> </span><span>in</span><span> </span><span>snapshot</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>list</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span>(</span><span>x</span><span> * </span><span>10</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"with a snapshot: {:?}"</span><span>, </span><span>list</span><span>.</span><span>borrow</span><span>());</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>// The tempting way: loop over the live borrow while the body mutates.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>list</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]));</span></div></div><div><div><div>16</div></div><div><span>    </span><span>for</span><span> </span><span>x</span><span> </span><span>in</span><span> </span><span>list</span><span>.</span><span>borrow</span><span>().</span><span>iter</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>list</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span>(</span><span>x</span><span> * </span><span>10</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>    </span><span>println!</span><span>(</span><span>"unreachable: {:?}"</span><span>, </span><span>list</span><span>.</span><span>borrow</span><span>());</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch09_iter_mutate</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>with a snapshot: [1, 2, 3, 10, 20, 30]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>thread 'main' (117827) panicked at src/bin/ch09_iter_mutate.rs:18:14:</span></div></div><div><div><div>4</div></div><div><span>RefCell already borrowed</span></div></div><div><div><div>5</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一部分是快照：克隆一份 <code>Vec</code>，在副本上遍历，原件随便改。第二部分是”活借用”：编译通过，运行时 panic，位置在 <code>borrow_mut()</code>。C++ 里这就是”一边遍历 <code>std::vector</code> 一边 <code>push_back</code>”，迭代器失效，是未定义行为，可能崩溃，也可能悄悄给你错误的数据。Rust + <code>RefCell</code> 把它变成一个确定的、可复现的 panic；而我们的解释器再往前走一步，用快照让这类程序<strong>有定义</strong>。克隆一个 <code>Vec&lt;Value&gt;</code> 每个元素各 <code>clone</code> 一次（<code>Rc</code> 变体只是计数加一，整数、浮点这些直接复制），不是深拷贝，所以快照的代价是一次 <code>Vec</code> 分配加 N 次廉价的 <code>clone</code>。</p></section></section>
<section><h2>那个泄漏<a href="#那个泄漏"><span>#</span></a></h2><p>终于到这里了。看这个程序：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn fact(n) {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>if n &lt; 2 { return 1; }</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>return n * fact(n - 1);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div><span>fact(5);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>跑完之后，解释器里那个全局 <code>Env</code> 会发生什么？</p><p><code>fact</code> 被定义在全局环境里：全局的 <code>values</code> 哈希表里有一项 <code>"fact" -&gt; Value::Function(Rc&lt;Function&gt;)</code>。而这个 <code>Function</code> 的 <code>env</code> 字段，是它声明那一刻捕获的环境，也就是全局环境本身（<code>Rc::clone(env)</code>）。所以：</p><div><div><div><div><span><p>values[fact]</p></span></div><div><span><p>env</p></span></div><div><span><p>全局 Env</p></span></div><div><span><p>Function fact</p></span></div>
</div><div><div><span><p>values[fact]</p></span></div><div><span><p>env</p></span></div><div><span><p>全局 Env</p></span></div><div><span><p>Function fact</p></span></div>
</div></div></div><p>全局环境拥有 <code>fact</code>，<code>fact</code> 拥有全局环境。<code>Rc</code> 的引用计数是一个简单的整数：全局 <code>Env</code> 的计数至少是 2（解释器的 <code>globals</code> 字段一个，<code>fact.env</code> 一个）。解释器被丢弃时，<code>globals</code> 字段释放，计数从 2 降到 1，<strong>不是 0</strong>，<code>Env</code> 永远不会被释放。</p><p><strong>不止递归函数：任何存放在它自己捕获的环境里的函数都会造成这个环。</strong> <code>fn f() {}</code> 也行，<code>let f = fn() { return 1; };</code> 也行。回忆第 8 篇里 <code>Rc</code> 和 C++ <code>shared_ptr</code> 的对照：<code>shared_ptr</code> 也有一模一样的问题，所以 C++ 里有 <code>weak_ptr</code>。Rust 的对应物是 <code>Weak&lt;T&gt;</code>：它指向 <code>Rc</code> 管理的数据，但<strong>不增加强引用计数</strong>；想用的时候调用 <code>upgrade()</code>，数据还在就得到 <code>Some(Rc&lt;T&gt;)</code>，已经被释放就得到 <code>None</code>。</p><p>先在实验室里把这个环的整个故事演一遍，用最小的结构体表示”作用域”和”函数”。文件是 <code>~/rust-lab/src/bin/ch09_cycle.rs</code>：</p><div><div><div><figure><figcaption><span>~/rust-lab/src/bin/ch09_cycle.rs</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::{</span></span><span>Rc</span><span>, </span><span>Weak</span><span>};</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>struct</span><span> </span><span>Scope</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>name</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>// A "function" stored in the scope that it captured.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>function</span><span>: </span><span>Option</span><span>&lt;</span><span>Rc</span><span>&lt;</span><span>Function</span><span>&gt;&gt;,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>struct</span><span> </span><span>Function</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>// Never read: the field exists only to keep the scope alive.</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#[allow(dead_code)]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>env</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Scope</span><span>&gt;&gt;,</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>impl</span><span> </span><span>Drop</span><span> </span><span>for</span><span> </span><span>Scope</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>drop</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>println!</span><span>(</span><span>"  dropping scope `{}`"</span><span>, </span><span>self</span><span>.name);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>println!</span><span>(</span><span>"-- a scope without functions"</span><span>);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>plain</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>Scope</span><span> { </span><span>name</span><span>: </span><span>"plain"</span><span>, </span><span>function</span><span>: </span><span>None</span><span> }));</span></div></div><div><div><div>25</div></div><div><span>    </span><span>let</span><span> </span><span>weak_plain</span><span>: </span><span>Weak</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Scope</span><span><span>&gt;&gt; </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>downgrade</span><span>(&amp;</span><span>plain</span><span>);</span></div></div><div><div><div>26</div></div><div><span>    </span><span>drop</span><span>(</span><span>plain</span><span>);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  alive after drop: {}"</span><span>, </span><span>weak_plain</span><span>.</span><span>upgrade</span><span>().</span><span>is_some</span><span>());</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>println!</span><span>(</span><span>"-- a scope that stores a function capturing itself"</span><span>);</span></div></div><div><div><div>30</div></div><div><span>    </span><span>let</span><span> </span><span>globals</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>Scope</span><span> { </span><span>name</span><span>: </span><span>"globals"</span><span>, </span><span>function</span><span>: </span><span>None</span><span> }));</span></div></div><div><div><div>31</div></div><div><span>    </span><span>let</span><span> </span><span>function</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span>(</span><span>Function</span><span> { </span><span>env</span><span><span>: </span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>globals</span><span>) });</span></div></div><div><div><div>32</div></div><div><span>    </span><span>globals</span><span>.</span><span>borrow_mut</span><span><span>().function </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>function</span><span>);</span></div></div><div><div><div>33</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  strong count: {}"</span><span>, </span><span>Rc</span><span>::</span><span>strong_count</span><span>(&amp;</span><span>globals</span><span>));</span></div></div><div><div><div>34</div></div><div><span>    </span><span>let</span><span> </span><span>weak_globals</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>downgrade</span><span>(&amp;</span><span>globals</span><span>);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>drop</span><span>(</span><span>globals</span><span>);</span></div></div><div><div><div>36</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  alive after drop: {}"</span><span>, </span><span>weak_globals</span><span>.</span><span>upgrade</span><span>().</span><span>is_some</span><span>());</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch09_cycle</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- a scope without functions</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>dropping scope `plain`</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>alive after drop: false</span></div></div><div><div><div>4</div></div><div><span>-- a scope that stores a function capturing itself</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>strong count: 2</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>alive after drop: true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对比一目了然：没有函数的作用域在 <code>drop(plain)</code> 后马上执行了 <code>Drop</code>（打印了 <code>dropping scope plain</code>），<code>Weak::upgrade</code> 得到 <code>None</code>；存了一个捕获自己的函数的作用域，<code>strong count</code> 是 2，<code>drop(globals)</code> 之后，<code>Weak::upgrade</code> 还是 <code>Some</code>，而且 <code>Drop</code> 根本没被调用，没有那行 <code>dropping scope globals</code>。</p><p>我们要把这个事实写进测试。两个测试都放在 <code>tests</code> 模块末尾，并在模块开头的 <code>use</code> 里加一行 <code>use std::rc::Weak;</code>（<code>use pebble_span::SourceId;</code> 和 <code>use pebble_value::Buffer;</code> 你在前几步已经有了）。Pebble 的测试用 <code>Weak</code> 作”看门狗”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Any function value stored in the environment it captured forms an `Rc`</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// cycle (not only recursive ones): the globals own `fact`, and `fact`</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// owns the globals it closed over.</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>top_level_functions_keep_globals_alive</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>src</span><span><span> </span><span>=</span><span> </span></span><span>"fn fact(n) { if n &lt; 2 { return 1; } return n * fact(n - 1); } fact(5);"</span><span>;</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>result</span><span>.program).</span><span>unwrap</span><span>();</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>globals</span><span>: </span><span>Weak</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span><span>&gt;&gt; </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>downgrade</span><span>(&amp;</span><span>interpreter</span><span>.</span><span>globals</span><span>());</span></div></div><div><div><div>11</div></div><div><span>        </span><span>drop</span><span>(</span><span>interpreter</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>13</div></div><div><span>            </span><span>globals</span><span>.</span><span>upgrade</span><span>().</span><span>is_some</span><span>(),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>"globals hold `fact`, `fact` holds globals: the cycle leaks"</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Rc::downgrade(&amp;interpreter.globals())</code> 对 <code>globals()</code> 返回的临时 <code>Rc</code> 做了降级，得到一个不计数的 <code>Weak</code>；这个临时 <code>Rc</code> 在这一行结束时被释放，计数回到原来的值。<code>drop(interpreter)</code> 释放解释器。断言在这之后：<code>globals.upgrade().is_some()</code>，环境<strong>还活着</strong>。这个测试的断言信息（“the cycle leaks”）是它存在的意义：它不是在测一个 bug 的修复，是在<strong>记录一个已知的限制</strong>。</p><p>它的反面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>globals_without_functions_are_freed</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>with_output</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Buffer</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"let x = 1 + 2; x;"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>result</span><span>.program).</span><span>unwrap</span><span>();</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>globals</span><span>: </span><span>Weak</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span><span>&gt;&gt; </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>downgrade</span><span>(&amp;</span><span>interpreter</span><span>.</span><span>globals</span><span>());</span></div></div><div><div><div>7</div></div><div><span>        </span><span>drop</span><span>(</span><span>interpreter</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert!</span><span>(</span><span>globals</span><span>.</span><span>upgrade</span><span>().</span><span>is_none</span><span>());</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有函数时，<code>globals</code> 的计数只有一个，<code>drop(interpreter)</code> 后降到 0，<code>upgrade()</code> 得到 <code>None</code>。这个测试保证第一个测试不是因为别的原因通过的：同样的测试夹具，换一个程序，结果相反。</p><section><h3>为什么不用 Weak 修掉它<a href="#为什么不用-weak-修掉它"><span>#</span></a></h3><p>很自然的想法是：把 <code>Function.env</code> 改成 <code>Weak&lt;RefCell&lt;Env&gt;&gt;</code>，环就断了。我在实验室里把这个想法最小地做了一遍，文件是 <code>~/rust-lab/src/bin/ch09_weak_env.rs</code>：</p><div><div><div><figure><figcaption><span>~/rust-lab/src/bin/ch09_weak_env.rs</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::{</span></span><span>Rc</span><span>, </span><span>Weak</span><span>};</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>struct</span><span> </span><span>Scope</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>count</span><span>: </span><span>i64</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>// A closure that only holds a Weak pointer to the scope it captured.</span></div></div><div><div><div>9</div></div><div><span>struct</span><span> </span><span>WeakClosure</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>env</span><span>: </span><span>Weak</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Scope</span><span>&gt;&gt;,</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>impl</span><span> </span><span>WeakClosure</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>call</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>i64</span><span>&gt; {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>env</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.env.</span><span>upgrade</span><span>()?;</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>env</span><span>.</span><span>borrow_mut</span><span>();</span></div></div><div><div><div>17</div></div><div><span>        </span><span>scope</span><span><span>.count </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>18</div></div><div><span>        </span><span>Some</span><span>(</span><span>scope</span><span>.count)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>// Like `make_counter()` in Pebble: the call's scope is a local that nobody else owns.</span></div></div><div><div><div>23</div></div><div><span>fn</span><span> </span><span>make_counter</span><span>() -&gt; </span><span>WeakClosure</span><span> {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>scope</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>Scope</span><span> { </span><span>count</span><span>: </span><span>0</span><span> }));</span></div></div><div><div><div>25</div></div><div><span>    </span><span>WeakClosure</span><span> { </span><span>env</span><span>: </span><span>Rc</span><span>::</span><span>downgrade</span><span>(&amp;</span><span>scope</span><span>) }</span></div></div><div><div><div>26</div></div><div><span>} </span><span>// `scope` is dropped here, the only strong pointer is gone</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>29</div></div><div><span>    </span><span>let</span><span> </span><span>next</span><span><span> </span><span>=</span><span> </span></span><span>make_counter</span><span>();</span></div></div><div><div><div>30</div></div><div><span>    </span><span>println!</span><span>(</span><span>"first call:  {:?}"</span><span>, </span><span>next</span><span>.</span><span>call</span><span>());</span></div></div><div><div><div>31</div></div><div><span>    </span><span>println!</span><span>(</span><span>"second call: {:?}"</span><span>, </span><span>next</span><span>.</span><span>call</span><span>());</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch09_weak_env</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>first call:  None</span></div></div><div><div><div>2</div></div><div><span>second call: None</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>make_counter</code> 里的 <code>scope</code> 是调用时创建的环境，它<strong>只有这个局部变量一个强引用</strong>。闭包只拿 <code>Weak</code>，所以函数一返回，<code>scope</code> 被释放，闭包再调用时 <code>upgrade()</code> 总是 <code>None</code>：这个计数器永远数不到 1。闭包必须是环境的<strong>所有者</strong>之一，闭包才能把 <code>count</code> 带出去活着。这就是 <code>examples/closures.pebble</code> 的意义，也是 Pebble 闭包的本质。</p><p>所以问题没有局部解法：环是<strong>真的</strong>有，因为全局函数确实需要全局环境，全局环境确实需要找到全局函数。引用计数天生无法回收环；<code>Weak</code> 只适合”那条边本来就是反向的”的场合，比如父指针（子拥有，父不拥有）。Pebble 的另一个后端不再借用 <code>Rc</code>，而是自己写一个<strong>追踪式垃圾回收器</strong>，从根出发标记可达对象，剩下的清扫，环不再是问题。那是<a href="/posts/rust-by-pebble-11-gc-unsafe/">第 11 篇</a>和<a href="/posts/rust-by-pebble-12-vm-backends/">第 12 篇</a>。第 12 篇里有个测试会真的造出一个不可达的闭包环，再证明回收器把它释放。</p><p>在这之前我选择接受。短命的 CLI 进程退出时操作系统收回所有内存；真正持续运行的使用场景是 REPL，那里每次调用里声明的内层函数（<code>Env</code> 和函数互相引用）会泄漏一点点，对一个学习项目来说可以容忍。<strong>把限制写成一个会失败的测试</strong>，比把它写进注释好：哪天有人修好了它，这个测试会红，提醒他更新断言和文档。</p></section></section>
<section><h2>补全文件<a href="#补全文件"><span>#</span></a></h2><p>四步都写完了。现在做最后一件事：让文件和 tag 逐字节一致。需要补的东西有三块。</p><p><strong>文件头</strong>：把文件开头到第一行 <code>use</code> 之前的部分换成 tag 里的 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/src/lib.rs#L1-L15" target="_blank">第 1 到 15 行</a>：<code>#![forbid(unsafe_code)]</code>，那行 <code>#![allow(clippy::mutable_key_type)]</code> 加上两行说明理由的注释，以及 crate 级文档注释。这段文档里有一个英文破折号，我的博客不用这个符号，所以不在文章里贴它，请直接从链接复制。<code>use pebble_value::{...}</code> 那一行要补上 <code>builtin</code>（放在 <code>Value</code> 之后，<code>call_native</code> 之前），因为下面的 <code>builtin_value</code> 要用它，其余的 <code>use</code> 不用动。</p><p><strong><code>builtin_value</code></strong>：一个给嵌入使用者的小函数，放在 <code>iterable_span</code> 之后、<code>mod tests</code> 之前：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Convenience: looks up a builtin by name, for embedding tests and tools.</span></div></div><div><div><div>2</div></div><div><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>builtin_value</span><span>(</span><span>name</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Value</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>builtin</span><span>(</span><span>name</span><span>).</span><span>map</span><span><span>(</span><span>Value</span><span>::</span></span><span>Native</span><span>)</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>测试</strong>：把整个 <code>#[cfg(test)] mod tests { ... }</code> 换成 tag 里的 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/src/lib.rs#L533-L726" target="_blank">第 533 到 726 行</a>。里面是按固定次序排的 16 个测试，次序是：<code>evaluates_arithmetic_with_precedence</code>、<code>variables_and_assignment</code>、<code>closures_capture_their_environment</code>、<code>recursion_works</code>、<code>lists_maps_and_indexing</code>、<code>string_concatenation_and_indexing</code>、<code>control_flow_loops</code>、<code>while_loop_accumulates</code>、<code>print_reaches_the_output_sink</code>、<code>runtime_errors_are_reported_with_a_trace</code>、<code>recursive_overflow_is_caught</code>、<code>arity_errors_are_reported</code>、<code>calling_a_non_function_is_an_error</code>、<code>type_errors_propagate_from_operators</code>，最后是我们写的两个泄漏测试。测试头里多了 <code>use std::rc::Weak;</code>。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-interp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 16 tests</span></div></div><div><div><div>2</div></div><div><span>test tests::calling_a_non_function_is_an_error ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::arity_errors_are_reported ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::closures_capture_their_environment ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::control_flow_loops ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::evaluates_arithmetic_with_precedence ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::globals_without_functions_are_freed ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::print_reaches_the_output_sink ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::lists_maps_and_indexing ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::runtime_errors_are_reported_with_a_trace ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::recursion_works ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::string_concatenation_and_indexing ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::type_errors_propagate_from_operators ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::variables_and_assignment ... ok</span></div></div><div><div><div>15</div></div><div><span>test tests::top_level_functions_keep_globals_alive ... ok</span></div></div><div><div><div>16</div></div><div><span>test tests::while_loop_accumulates ... ok</span></div></div><div><div><div>17</div></div><div><span>test tests::recursive_overflow_is_caught ... ok</span></div></div><div><div><div>18</div></div><div><span>test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>19</div></div><div><span>running 0 tests</span></div></div><div><div><div>20</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>16 个，两个 <code>Weak</code> 测试在列。</p></section>
<section><h2>命令行第二版：run 和 repl<a href="#命令行第二版run-和-repl"><span>#</span></a></h2><p>现在把解释器接到 <code>pebble</code> 命令行上。第 7 篇的版本有 <code>tokens</code>、<code>parse</code>、<code>check</code>；这一版加上 <code>run</code>（执行文件）和 <code>repl</code>（交互）。改动涉及全部六个文件外加一个新文件 <code>repl.rs</code>，我把每一个都完整列出来。</p><p>先在根 <code>Cargo.toml</code> 的外部依赖里加一行 <code>rustyline</code>，它是一个行编辑库，REPL 用它读输入（上下箭头、历史记录）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>rustyline</span><span> = </span><span>"18"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>放在 <code>quote</code> 之后、<code>syn</code> 之前，保持字母序。</p><p><code>crates/pebble-cli/Cargo.toml</code>：新增三个依赖，<code>pebble-value</code>、<code>pebble-interp</code>、<code>rustyline</code>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-cli"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"The pebble command-line driver: run, compile, disassemble, repl."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[[</span><span>bin</span><span>]]</span></div></div><div><div><div>12</div></div><div><span>name</span><span> = </span><span>"pebble"</span></div></div><div><div><div>13</div></div><div><span>path</span><span> = </span><span>"src/main.rs"</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>16</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>17</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>18</div></div><div><span>pebble-lexer.workspace</span><span> = </span><span>true</span></div></div><div><div><div>19</div></div><div><span>pebble-ast.workspace</span><span> = </span><span>true</span></div></div><div><div><div>20</div></div><div><span>pebble-parser.workspace</span><span> = </span><span>true</span></div></div><div><div><div>21</div></div><div><span>pebble-value.workspace</span><span> = </span><span>true</span></div></div><div><div><div>22</div></div><div><span>pebble-interp.workspace</span><span> = </span><span>true</span></div></div><div><div><div>23</div></div><div><span>anyhow.workspace</span><span> = </span><span>true</span></div></div><div><div><div>24</div></div><div><span>clap.workspace</span><span> = </span><span>true</span></div></div><div><div><div>25</div></div><div><span>rustyline.workspace</span><span> = </span><span>true</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>28</div></div><div><span>assert_cmd</span><span> = </span><span>"2"</span></div></div><div><div><div>29</div></div><div><span>predicates</span><span> = </span><span>"3"</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>32</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>src/cli.rs</code> 多了两个子命令和一组参数。<code>Run(RunArgs)</code> 里的 <code>quiet</code> 是为了让脚本能只看 <code>print</code> 的输出，不看最后那个值：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! The command-line surface, declared with clap's derive API.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Everything here is data: the actual work lives in [`crate::commands`] and</span></div></div><div><div><div>4</div></div><div><span>//! [`crate::repl`]. Keeping the shape of the interface in one small file makes</span></div></div><div><div><div>5</div></div><div><span>//! it easy to see at a glance what the driver can do.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>path</span><span>::</span></span><span>PathBuf</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>clap</span><span>::{</span></span><span>Args</span><span>, </span><span>Parser</span><span>, </span><span>Subcommand</span><span>};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>/// Pebble: a small scripting language and toolchain.</span></div></div><div><div><div>12</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Parser</span><span>)]</span></div></div><div><div><div>13</div></div><div><span>#[command(name </span><span>=</span><span> </span><span>"pebble"</span><span>, version)]</span></div></div><div><div><div>14</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>Cli</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>/// The action to perform.</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>#[command(subcommand)]</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>command</span><span>: </span><span>Command</span><span>,</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>/// Every top-level action the driver understands.</span></div></div><div><div><div>21</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Subcommand</span><span>)]</span></div></div><div><div><div>22</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>enum</span><span> </span><span>Command</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>/// Execute a source file with the tree-walking interpreter.</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Run</span><span>(</span><span>RunArgs</span><span>),</span></div></div><div><div><div>25</div></div><div><span>    </span><span>/// Start an interactive read-eval-print loop.</span></div></div><div><div><div>26</div></div><div><span>    </span><span>Repl</span><span>,</span></div></div><div><div><div>27</div></div><div><span>    </span><span>/// Print the token stream of a source file.</span></div></div><div><div><div>28</div></div><div><span>    </span><span>Tokens</span><span>(</span><span>FileArgs</span><span>),</span></div></div><div><div><div>29</div></div><div><span>    </span><span>/// Print the parsed abstract syntax tree of a source file.</span></div></div><div><div><div>30</div></div><div><span>    </span><span>Parse</span><span>(</span><span>FileArgs</span><span>),</span></div></div><div><div><div>31</div></div><div><span>    </span><span>/// Lex and parse a source file without running it.</span></div></div><div><div><div>32</div></div><div><span>    </span><span>Check</span><span>(</span><span>FileArgs</span><span>),</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>/// A subcommand whose only input is one source file.</span></div></div><div><div><div>36</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Args</span><span>)]</span></div></div><div><div><div>37</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>FileArgs</span><span> {</span></div></div><div><div><div>38</div></div><div><span>    </span><span>/// The source file to read.</span></div></div><div><div><div>39</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>file</span><span>: </span><span>PathBuf</span><span>,</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>/// Arguments to `pebble run`.</span></div></div><div><div><div>43</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Args</span><span>)]</span></div></div><div><div><div>44</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>RunArgs</span><span> {</span></div></div><div><div><div>45</div></div><div><span>    </span><span>/// The source file to execute.</span></div></div><div><div><div>46</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>file</span><span>: </span><span>PathBuf</span><span>,</span></div></div><div><div><div>47</div></div><div><span>    </span><span>/// Print the parsed syntax tree before running.</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>#[arg(long)]</span></div></div><div><div><div>49</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>ast</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>50</div></div><div><span>    </span><span>/// Do not print the program's final value.</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>#[arg(long)]</span></div></div><div><div><div>52</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>quiet</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>53</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>src/sources.rs</code>：这个文件多了三样东西：<code>Source::from_text</code>（REPL 要把一行文本当成一个”文件”）、<code>render_runtime</code>、<code>emit_runtime</code>。运行时错误除了自己的诊断，还带着调用栈，所以这里要给每一帧补上”在哪个文件第几行第几列”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Loading source text and turning diagnostics into printable text.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Every subcommand works the same way: read a file into a [`SourceMap`], hand</span></div></div><div><div><div>4</div></div><div><span>//! the text and its id to the lexer and parser, then render whatever came</span></div></div><div><div><div>5</div></div><div><span>//! back against that same map so the carets land on the right characters.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::fs;</span></span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>path</span><span>::</span></span><span>Path</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>anyhow</span><span>::{</span></span><span>Context</span><span>, </span><span>Result</span><span>};</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::{</span></span><span>Diagnostic</span><span>, </span><span>Severity</span><span>};</span></div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceId</span><span>, </span><span>SourceMap</span><span>};</span></div></div><div><div><div>13</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>RuntimeError</span><span>;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>/// One registered source file plus the map that owns it.</span></div></div><div><div><div>16</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>Source</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// Maps spans back to the text they came from.</span></div></div><div><div><div>18</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>map</span><span>: </span><span>SourceMap</span><span>,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>/// The id the text was registered under.</span></div></div><div><div><div>20</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>id</span><span>: </span><span>SourceId</span><span>,</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>impl</span><span> </span><span>Source</span><span> {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>/// Reads `path` and registers its text in a fresh map.</span></div></div><div><div><div>25</div></div><div><span>    </span><span>///</span></div></div><div><div><div>26</div></div><div><span>    </span><span>/// # Errors</span></div></div><div><div><div>27</div></div><div><span>    </span><span>/// Returns an error if the file cannot be read as UTF-8 text.</span></div></div><div><div><div>28</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>load</span><span>(</span><span>path</span><span>: &amp;</span><span>Path</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>&gt; {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span></span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>fs</span><span>::</span><span>read_to_string</span><span>(</span><span>path</span><span>).</span><span>with_context</span><span><span>(</span><span>||</span><span> </span></span><span>format!</span><span>(</span><span>"cannot read {}"</span><span>, </span><span>path</span><span>.</span><span>display</span><span>()))?;</span></div></div><div><div><div>31</div></div><div><span>        </span><span>Ok</span><span>(</span><span>Self</span><span>::</span><span>from_text</span><span>(</span><span>path</span><span>.</span><span>display</span><span>().</span><span>to_string</span><span>(), </span><span>text</span><span>))</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>    </span><span>/// Registers `text` under `name` in a fresh map.</span></div></div><div><div><div>35</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>from_text</span><span>(</span><span>name</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>text</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>37</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>map</span><span>.</span><span>add</span><span>(</span><span>name</span><span>, </span><span>text</span><span>);</span></div></div><div><div><div>38</div></div><div><span>        </span><span>Self</span><span> { </span><span>map</span><span>, </span><span>id</span><span> }</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>    </span><span>/// The registered text.</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>43</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>text</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>44</div></div><div><span>        </span><span>self</span><span>.map[</span><span>self</span><span>.id].</span><span>text</span><span>()</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>/// Whether any diagnostic is an error (as opposed to a warning).</span></div></div><div><div><div>49</div></div><div><span>#[must_use]</span></div></div><div><div><div>50</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>has_errors</span><span>(</span><span>diagnostics</span><span>: &amp;[</span><span>Diagnostic</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>51</div></div><div><span>    </span><span>diagnostics</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>.</span><span>any</span><span><span>(</span><span>|</span></span><span>diagnostic</span><span><span>|</span><span> </span></span><span>diagnostic</span><span><span>.severity </span><span>==</span><span> </span><span>Severity</span><span>::</span></span><span>Error</span><span>)</span></div></div><div><div><div>54</div></div><div><span>}</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>/// Renders every diagnostic, one block after another.</span></div></div><div><div><div>57</div></div><div><span>#[must_use]</span></div></div><div><div><div>58</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>render</span><span>(</span><span>diagnostics</span><span>: &amp;[</span><span>Diagnostic</span><span>], </span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>59</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>out</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>60</div></div><div><span>    </span><span>for</span><span> </span><span>diagnostic</span><span> </span><span>in</span><span> </span><span>diagnostics</span><span> {</span></div></div><div><div><div>61</div></div><div><span>        </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>diagnostic</span><span>.</span><span>render</span><span>(</span><span>sources</span><span>));</span></div></div><div><div><div>62</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span>    </span><span>out</span></div></div><div><div><div>64</div></div><div><span>}</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div><span>/// Renders a runtime error, followed by its stack trace.</span></div></div><div><div><div>67</div></div><div><span>#[must_use]</span></div></div><div><div><div>68</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>render_runtime</span><span>(</span><span>error</span><span>: &amp;</span><span>RuntimeError</span><span>, </span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>69</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>out</span><span><span> </span><span>=</span><span> </span></span><span>error</span><span>.diagnostic.</span><span>render</span><span>(</span><span>sources</span><span>);</span></div></div><div><div><div>70</div></div><div><span>    </span><span>for</span><span> </span><span>frame</span><span> </span><span>in</span><span> &amp;</span><span>error</span><span>.trace {</span></div></div><div><div><div>71</div></div><div><span>        </span><span>match</span><span> </span><span>sources</span><span>.</span><span>get</span><span>(</span><span>frame</span><span>.span.source) {</span></div></div><div><div><div>72</div></div><div><span>            </span><span>Some</span><span>(</span><span>file</span><span>) =&gt; {</span></div></div><div><div><div>73</div></div><div><span>                </span><span>let</span><span> </span><span>position</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>position</span><span>(</span><span>frame</span><span>.span.start);</span></div></div><div><div><div>74</div></div><div><span>                </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span></div></div><div><div><div>75</div></div><div><span>                    </span><span>"  in `{}` at {}:{}:{}</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>76</div></div><div><span>                    </span><span>frame</span><span>.function,</span></div></div><div><div><div>77</div></div><div><span>                    </span><span>file</span><span>.</span><span>name</span><span>(),</span></div></div><div><div><div>78</div></div><div><span>                    </span><span>position</span><span>.line,</span></div></div><div><div><div>79</div></div><div><span>                    </span><span>position</span><span>.column</span></div></div><div><div><div>80</div></div><div><span><span>                </span></span><span>));</span></div></div><div><div><div>81</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>82</div></div><div><span>            </span><span>None</span><span> =&gt; </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>format!</span><span>(</span><span>"  in `{}`</span><span>\n</span><span>"</span><span>, </span><span>frame</span><span>.function)),</span></div></div><div><div><div>83</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>85</div></div><div><span>    </span><span>out</span></div></div><div><div><div>86</div></div><div><span>}</span></div></div><div><div><div>87</div></div><div>
</div></div><div><div><div>88</div></div><div><span>/// Prints every diagnostic to stderr; returns `true` if any of them is an error.</span></div></div><div><div><div>89</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>emit</span><span>(</span><span>diagnostics</span><span>: &amp;[</span><span>Diagnostic</span><span>], </span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>90</div></div><div><span>    </span><span>eprint!</span><span>(</span><span>"{}"</span><span>, </span><span>render</span><span>(</span><span>diagnostics</span><span>, </span><span>sources</span><span>));</span></div></div><div><div><div>91</div></div><div><span>    </span><span>has_errors</span><span>(</span><span>diagnostics</span><span>)</span></div></div><div><div><div>92</div></div><div><span>}</span></div></div><div><div><div>93</div></div><div>
</div></div><div><div><div>94</div></div><div><span>/// Prints a runtime error and its stack trace to stderr.</span></div></div><div><div><div>95</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>emit_runtime</span><span>(</span><span>error</span><span>: &amp;</span><span>RuntimeError</span><span>, </span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>) {</span></div></div><div><div><div>96</div></div><div><span>    </span><span>eprint!</span><span>(</span><span>"{}"</span><span>, </span><span>render_runtime</span><span>(</span><span>error</span><span>, </span><span>sources</span><span>));</span></div></div><div><div><div>97</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>render_runtime</code> 里 <code>sources.get(frame.span.source)</code> 返回 <code>Option</code>（第 2 篇的 <code>get</code> 对比 <code>[]</code>）：有就打印位置，没有就只打印函数名。<code>format!</code> 的格式里 <code>{}</code> 的数量要和参数个数对得上，编译器会检查。</p><p><code>src/commands.rs</code>：多了 <code>run</code>。它先解析，有语法错误就打印并退出（不会运行半个程序）；<code>--ast</code> 时打印语法树；然后建解释器，执行，成功时如果不是 <code>--quiet</code> 并且值不是 <code>nil</code> 就打印：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! The work behind each subcommand.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Every function returns the process exit code it wants: `0` on success and</span></div></div><div><div><div>4</div></div><div><span>//! `1` when the input produced diagnostics or failed at runtime. Only host</span></div></div><div><div><div>5</div></div><div><span>//! problems (an unreadable file, say) escape as an [`anyhow::Error`].</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>anyhow</span><span>::</span></span><span>Result</span><span>;</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::pretty;</span></span></div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>pebble_interp</span><span>::</span></span><span>Interpreter</span><span>;</span></div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>pebble_lexer</span><span>::tokenize;</span></span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_parser</span><span>::parse </span></span><span>as</span><span> parse_program;</span></div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>pebble_value</span><span>::</span></span><span>Value</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>cli</span><span>::{</span></span><span>FileArgs</span><span>, </span><span>RunArgs</span><span>};</span></div></div><div><div><div>15</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>sources</span><span>::{</span></span><span>self</span><span>, </span><span>Source</span><span>};</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>/// `pebble run`: parse a file, then execute it with the tree-walking interpreter.</span></div></div><div><div><div>18</div></div><div><span>///</span></div></div><div><div><div>19</div></div><div><span>/// # Errors</span></div></div><div><div><div>20</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>21</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>run</span><span>(</span><span>args</span><span>: &amp;</span><span>RunArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>24</div></div><div><span>    </span><span>if</span><span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map) {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span>if</span><span> </span><span>args</span><span>.ast {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>pretty</span><span>(&amp;</span><span>parsed</span><span>.program));</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>33</div></div><div><span>    </span><span>match</span><span> </span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>parsed</span><span>.program) {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>Ok</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>35</div></div><div><span>            </span><span>if</span><span><span> </span><span>!</span></span><span>args</span><span><span>.quiet </span><span>&amp;&amp;</span><span> </span><span>!</span></span><span>matches!</span><span>(</span><span>value</span><span>, </span><span>Value</span><span>::</span><span>Nil</span><span>) {</span></div></div><div><div><div>36</div></div><div><span>                </span><span>println!</span><span>(</span><span>"{value}"</span><span>);</span></div></div><div><div><div>37</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>            </span><span>Ok</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; {</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>sources</span><span>::</span><span>emit_runtime</span><span>(&amp;</span><span>error</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>42</div></div><div><span>            </span><span>Ok</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>/// `pebble tokens`: one `line:col kind` line per token.</span></div></div><div><div><div>48</div></div><div><span>///</span></div></div><div><div><div>49</div></div><div><span>/// # Errors</span></div></div><div><div><div>50</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>51</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>tokens</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>52</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>53</div></div><div><span>    </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>54</div></div><div><span>    </span><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>diagnostics</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>    </span><span>let</span><span> </span><span>file</span><span><span> </span><span>=</span><span> &amp;</span></span><span>source</span><span>.map[</span><span>source</span><span>.id];</span></div></div><div><div><div>57</div></div><div><span>    </span><span>for</span><span> </span><span>token</span><span> </span><span>in</span><span> &amp;</span><span>tokens</span><span> {</span></div></div><div><div><div>58</div></div><div><span>        </span><span>let</span><span> </span><span>position</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>position</span><span>(</span><span>token</span><span>.span.start);</span></div></div><div><div><div>59</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{}:{} {}"</span><span>, </span><span>position</span><span>.line, </span><span>position</span><span>.column, </span><span>token</span><span>.kind);</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>61</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>62</div></div><div><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>/// `pebble parse`: print the recovered syntax tree.</span></div></div><div><div><div>65</div></div><div><span>///</span></div></div><div><div><div>66</div></div><div><span>/// # Errors</span></div></div><div><div><div>67</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>68</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>parse</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>69</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>70</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>71</div></div><div><span>    </span><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>72</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>pretty</span><span>(&amp;</span><span>parsed</span><span>.program));</span></div></div><div><div><div>73</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>74</div></div><div><span>}</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span>/// `pebble check`: lex and parse a file, reporting diagnostics and nothing else.</span></div></div><div><div><div>77</div></div><div><span>///</span></div></div><div><div><div>78</div></div><div><span>/// # Errors</span></div></div><div><div><div>79</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>80</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>check</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>81</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>82</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>83</div></div><div><span>    </span><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>84</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>85</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>src/main.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! The `pebble` command-line driver.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! This binary is the front door to the toolchain: it wires the lexer, the</span></div></div><div><div><div>4</div></div><div><span>//! parser, the interpreter, and the diagnostics renderer together behind a</span></div></div><div><div><div>5</div></div><div><span>//! handful of subcommands.</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! Exit codes follow the usual convention: `0` for success, `1` when the input</span></div></div><div><div><div>8</div></div><div><span>//! produced diagnostics or failed at runtime, and `2` (clap's default) for a</span></div></div><div><div><div>9</div></div><div><span>//! usage error.</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>mod</span><span> cli;</span></div></div><div><div><div>14</div></div><div><span>mod</span><span> commands;</span></div></div><div><div><div>15</div></div><div><span>mod</span><span> repl;</span></div></div><div><div><div>16</div></div><div><span>mod</span><span> sources;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>process</span><span>::</span></span><span>ExitCode</span><span>;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>use</span><span><span> </span><span>clap</span><span>::</span></span><span>Parser</span><span>;</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>cli</span><span>::{</span></span><span>Cli</span><span>, </span><span>Command</span><span>};</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>fn</span><span> </span><span>main</span><span>() -&gt; </span><span>ExitCode</span><span> {</span></div></div><div><div><div>25</div></div><div><span>    </span><span>let</span><span> </span><span>cli</span><span><span> </span><span>=</span><span> </span></span><span>Cli</span><span>::</span><span>parse</span><span>();</span></div></div><div><div><div>26</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>cli</span><span>.command {</span></div></div><div><div><div>27</div></div><div><span>        </span><span>Command</span><span>::</span><span>Run</span><span>(</span><span>args</span><span><span>) =&gt; </span><span>commands</span><span>::</span></span><span>run</span><span>(&amp;</span><span>args</span><span>),</span></div></div><div><div><div>28</div></div><div><span>        </span><span>Command</span><span>::</span><span>Repl</span><span><span> =&gt; </span><span>repl</span><span>::</span></span><span>run</span><span>(),</span></div></div><div><div><div>29</div></div><div><span>        </span><span>Command</span><span>::</span><span>Tokens</span><span>(</span><span>args</span><span><span>) =&gt; </span><span>commands</span><span>::</span></span><span>tokens</span><span>(&amp;</span><span>args</span><span>),</span></div></div><div><div><div>30</div></div><div><span>        </span><span>Command</span><span>::</span><span>Parse</span><span>(</span><span>args</span><span><span>) =&gt; </span><span>commands</span><span>::</span></span><span>parse</span><span>(&amp;</span><span>args</span><span>),</span></div></div><div><div><div>31</div></div><div><span>        </span><span>Command</span><span>::</span><span>Check</span><span>(</span><span>args</span><span><span>) =&gt; </span><span>commands</span><span>::</span></span><span>check</span><span>(&amp;</span><span>args</span><span>),</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>33</div></div><div><span>    </span><span>match</span><span> </span><span>result</span><span> {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>Ok</span><span>(</span><span>code</span><span>) =&gt; </span><span>ExitCode</span><span>::</span><span>from</span><span>(</span><span>code</span><span>),</span></div></div><div><div><div>35</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; {</span></div></div><div><div><div>36</div></div><div><span>            </span><span>eprintln!</span><span>(</span><span>"pebble: {error:#}"</span><span>);</span></div></div><div><div><div>37</div></div><div><span>            </span><span>ExitCode</span><span>::</span><span>from</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>repl.rs 和 FromStr<a href="#replrs-和-fromstr"><span>#</span></a></h3><p>REPL 是一个循环：读一行，判断是命令还是代码，执行，打印。<strong>以 <code>:</code> 开头的是元命令</strong>（<code>:help</code>、<code>:quit</code>、<code>:tokens &lt;代码&gt;</code>、<code>:ast &lt;代码&gt;</code>）。</p><p>处理元命令，最粗暴的写法是在循环里写一串 <code>if line == ":quit"</code>。我们换一种：把元命令做成一个枚举，再为它实现标准库的 <code>FromStr</code>。<code>FromStr</code> 是 <code>str::parse</code> 背后的 trait：当你写 <code>"42".parse::&lt;i32&gt;()</code>，其实调用的是 <code>i32::from_str("42")</code>。第 8 篇我们是<strong>用</strong>它，这里是第一次<strong>实现</strong>它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>trait</span><span> </span><span>FromStr</span><span>: </span><span>Sized</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Err</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>from_str</span><span>(</span><span>s</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>, </span><span>Self</span><span>::</span><span>Err</span><span>&gt;;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>type Err</code> 是一个<strong>关联类型</strong>（第 4 篇的 <code>Iterator::Item</code> 是同一类东西）：每个实现者自己决定解析失败时返回什么错误类型。我们用 <code>String</code>，因为只需要一段给用户看的话。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! The interactive read-eval-print loop.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! The interpreter is created once, so definitions persist from line to line,</span></div></div><div><div><div>4</div></div><div><span>//! but every line gets its own [`pebble_span::SourceMap`]: a line is parsed and</span></div></div><div><div><div>5</div></div><div><span>//! rendered in isolation, which keeps offsets small and diagnostics exact.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>str</span><span>::</span></span><span>FromStr</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>anyhow</span><span>::{</span></span><span>Result</span><span>, anyhow};</span></div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::{</span></span><span>Program</span><span>, </span><span>Spanned</span><span> </span><span>as</span><span> _, </span><span>Stmt</span><span>, pretty};</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_interp</span><span>::</span></span><span>Interpreter</span><span>;</span></div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>pebble_lexer</span><span>::tokenize;</span></span></div></div><div><div><div>13</div></div><div><span>use</span><span><span> </span><span>pebble_parser</span><span>::{parse, parse_expression};</span></span></div></div><div><div><div>14</div></div><div><span>use</span><span><span> </span><span>rustyline</span><span>::</span></span><span>DefaultEditor</span><span>;</span></div></div><div><div><div>15</div></div><div><span>use</span><span><span> </span><span>rustyline</span><span>::</span><span>error</span><span>::</span></span><span>ReadlineError</span><span>;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>sources</span><span>::{</span></span><span>self</span><span>, </span><span>Source</span><span>};</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>/// The text shown before every input line.</span></div></div><div><div><div>20</div></div><div><span>const</span><span> </span><span>PROMPT</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"pebble&gt; "</span><span>;</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>/// Runs the REPL until the user quits or sends end-of-file.</span></div></div><div><div><div>23</div></div><div><span>///</span></div></div><div><div><div>24</div></div><div><span>/// # Errors</span></div></div><div><div><div>25</div></div><div><span>/// Returns an error if the line editor cannot be initialised.</span></div></div><div><div><div>26</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>run</span><span>() -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>editor</span><span><span> </span><span>=</span></span></div></div><div><div><div>28</div></div><div><span>        </span><span>DefaultEditor</span><span>::</span><span>new</span><span>().</span><span>map_err</span><span><span>(</span><span>|</span></span><span>error</span><span><span>|</span><span> </span></span><span>anyhow!</span><span>(</span><span>"cannot start the line editor: {error}"</span><span>))?;</span></div></div><div><div><div>29</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>interpreter</span><span><span> </span><span>=</span><span> </span></span><span>Interpreter</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Pebble REPL. Type :help for commands, :quit to exit."</span><span>);</span></div></div><div><div><div>32</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>33</div></div><div><span>        </span><span>match</span><span> </span><span>editor</span><span>.</span><span>readline</span><span>(</span><span>PROMPT</span><span>) {</span></div></div><div><div><div>34</div></div><div><span>            </span><span>Ok</span><span>(</span><span>line</span><span>) =&gt; {</span></div></div><div><div><div>35</div></div><div><span>                </span><span>let</span><span> </span><span>trimmed</span><span><span> </span><span>=</span><span> </span></span><span>line</span><span>.</span><span>trim</span><span>();</span></div></div><div><div><div>36</div></div><div><span>                </span><span>if</span><span> </span><span>trimmed</span><span>.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>37</div></div><div><span>                    </span><span>continue</span><span>;</span></div></div><div><div><div>38</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>                </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>editor</span><span>.</span><span>add_history_entry</span><span>(</span><span>line</span><span>.</span><span>as_str</span><span>());</span></div></div><div><div><div>40</div></div><div><span>                </span><span>if</span><span> </span><span>trimmed</span><span>.</span><span>starts_with</span><span>(</span><span>':'</span><span>) {</span></div></div><div><div><div>41</div></div><div><span>                    </span><span>match</span><span> </span><span>trimmed</span><span>.</span><span>parse</span><span>::&lt;</span><span>MetaCommand</span><span>&gt;() {</span></div></div><div><div><div>42</div></div><div><span>                        </span><span>Ok</span><span>(</span><span>MetaCommand</span><span>::</span><span>Quit</span><span>) =&gt; </span><span>break</span><span>,</span></div></div><div><div><div>43</div></div><div><span>                        </span><span>Ok</span><span>(</span><span>MetaCommand</span><span>::</span><span>Help</span><span>) =&gt; </span><span>print_help</span><span>(),</span></div></div><div><div><div>44</div></div><div><span>                        </span><span>Ok</span><span>(</span><span>MetaCommand</span><span>::</span><span>Tokens</span><span>(</span><span>code</span><span>)) =&gt; </span><span>show_tokens</span><span>(&amp;</span><span>code</span><span>),</span></div></div><div><div><div>45</div></div><div><span>                        </span><span>Ok</span><span>(</span><span>MetaCommand</span><span>::</span><span>Ast</span><span>(</span><span>code</span><span>)) =&gt; </span><span>show_ast</span><span>(&amp;</span><span>code</span><span>),</span></div></div><div><div><div>46</div></div><div><span>                        </span><span>Err</span><span>(</span><span>message</span><span>) =&gt; </span><span>eprintln!</span><span>(</span><span>"pebble: {message}"</span><span>),</span></div></div><div><div><div>47</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>49</div></div><div><span>                    </span><span>eval_line</span><span>(</span><span>trimmed</span><span>, &amp;</span><span>mut</span><span> </span><span>interpreter</span><span>);</span></div></div><div><div><div>50</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>52</div></div><div><span>            </span><span>// Ctrl-C and Ctrl-D both leave the session cleanly.</span></div></div><div><div><div>53</div></div><div><span>            </span><span>Err</span><span>(</span><span>ReadlineError</span><span>::</span><span>Interrupted</span><span><span> </span><span>|</span><span> </span></span><span>ReadlineError</span><span>::</span><span>Eof</span><span>) =&gt; </span><span>break</span><span>,</span></div></div><div><div><div>54</div></div><div><span>            </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; {</span></div></div><div><div><div>55</div></div><div><span>                </span><span>eprintln!</span><span>(</span><span>"pebble: input error: {error}"</span><span>);</span></div></div><div><div><div>56</div></div><div><span>                </span><span>break</span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>60</div></div><div><span>    </span><span>println!</span><span>();</span></div></div><div><div><div>61</div></div><div><span>    </span><span>Ok</span><span>(</span><span>0</span><span>)</span></div></div><div><div><div>62</div></div><div><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>/// A `:`-prefixed REPL command.</span></div></div><div><div><div>65</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>)]</span></div></div><div><div><div>66</div></div><div><span>enum</span><span> </span><span>MetaCommand</span><span> {</span></div></div><div><div><div>67</div></div><div><span>    </span><span>/// Leave the REPL.</span></div></div><div><div><div>68</div></div><div><span>    </span><span>Quit</span><span>,</span></div></div><div><div><div>69</div></div><div><span>    </span><span>/// Show the command list.</span></div></div><div><div><div>70</div></div><div><span>    </span><span>Help</span><span>,</span></div></div><div><div><div>71</div></div><div><span>    </span><span>/// Print the token stream of the argument.</span></div></div><div><div><div>72</div></div><div><span>    </span><span>Tokens</span><span>(</span><span>String</span><span>),</span></div></div><div><div><div>73</div></div><div><span>    </span><span>/// Print the syntax tree of the argument.</span></div></div><div><div><div>74</div></div><div><span>    </span><span>Ast</span><span>(</span><span>String</span><span>),</span></div></div><div><div><div>75</div></div><div><span>}</span></div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span>impl</span><span> </span><span>FromStr</span><span> </span><span>for</span><span> </span><span>MetaCommand</span><span> {</span></div></div><div><div><div>78</div></div><div><span>    </span><span>type</span><span> </span><span>Err</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>;</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>    </span><span>fn</span><span> </span><span>from_str</span><span>(</span><span>line</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>, </span><span>Self</span><span>::</span><span>Err</span><span>&gt; {</span></div></div><div><div><div>81</div></div><div><span>        </span><span>let</span><span> </span><span>Some</span><span>(</span><span>rest</span><span><span>) </span><span>=</span><span> </span></span><span>line</span><span>.</span><span>strip_prefix</span><span>(</span><span>':'</span><span>) </span><span>else</span><span> {</span></div></div><div><div><div>82</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>format!</span><span>(</span><span>"`{line}` is not a command"</span><span>));</span></div></div><div><div><div>83</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>84</div></div><div><span>        </span><span>let</span><span> (</span><span>name</span><span>, </span><span>argument</span><span><span>) </span><span>=</span><span> </span></span><span>match</span><span> </span><span>rest</span><span>.</span><span>split_once</span><span><span>(</span><span>char</span><span>::</span></span><span>is_whitespace</span><span>) {</span></div></div><div><div><div>85</div></div><div><span>            </span><span>Some</span><span>((</span><span>name</span><span>, </span><span>argument</span><span>)) =&gt; (</span><span>name</span><span>, </span><span>argument</span><span>.</span><span>trim</span><span>()),</span></div></div><div><div><div>86</div></div><div><span>            </span><span>None</span><span> =&gt; (</span><span>rest</span><span>, </span><span>""</span><span>),</span></div></div><div><div><div>87</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>88</div></div><div><span>        </span><span>match</span><span> </span><span>name</span><span> {</span></div></div><div><div><div>89</div></div><div><span>            </span><span>"quit"</span><span><span> </span><span>|</span><span> </span></span><span>"q"</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Self</span><span>::</span><span>Quit</span><span>),</span></div></div><div><div><div>90</div></div><div><span>            </span><span>"help"</span><span><span> </span><span>|</span><span> </span></span><span>"h"</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Self</span><span>::</span><span>Help</span><span>),</span></div></div><div><div><div>91</div></div><div><span>            </span><span>"tokens"</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Self</span><span>::</span><span>Tokens</span><span>(</span><span>argument</span><span>.</span><span>to_owned</span><span>())),</span></div></div><div><div><div>92</div></div><div><span>            </span><span>"ast"</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Self</span><span>::</span><span>Ast</span><span>(</span><span>argument</span><span>.</span><span>to_owned</span><span>())),</span></div></div><div><div><div>93</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>Err</span><span>(</span><span>format!</span><span>(</span><span>"unknown command `:{other}`; try :help"</span><span>)),</span></div></div><div><div><div>94</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>95</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>96</div></div><div><span>}</span></div></div><div><div><div>97</div></div><div>
</div></div><div><div><div>98</div></div><div><span>/// Prints the list of meta commands.</span></div></div><div><div><div>99</div></div><div><span>fn</span><span> </span><span>print_help</span><span>() {</span></div></div><div><div><div>100</div></div><div><span>    </span><span>println!</span><span>(</span><span>"commands:"</span><span>);</span></div></div><div><div><div>101</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  :help, :h        show this help"</span><span>);</span></div></div><div><div><div>102</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  :quit, :q        leave the REPL"</span><span>);</span></div></div><div><div><div>103</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  :tokens &lt;code&gt;   print the token stream for &lt;code&gt;"</span><span>);</span></div></div><div><div><div>104</div></div><div><span>    </span><span>println!</span><span>(</span><span>"  :ast &lt;code&gt;      print the parsed syntax tree for &lt;code&gt;"</span><span>);</span></div></div><div><div><div>105</div></div><div><span>}</span></div></div><div><div><div>106</div></div><div>
</div></div><div><div><div>107</div></div><div><span>/// `:tokens`: lex a snippet and print one `line:col kind` line per token.</span></div></div><div><div><div>108</div></div><div><span>fn</span><span> </span><span>show_tokens</span><span>(</span><span>code</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>109</div></div><div><span>    </span><span>if</span><span> </span><span>code</span><span>.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>110</div></div><div><span>        </span><span>eprintln!</span><span>(</span><span>"usage: :tokens &lt;code&gt;"</span><span>);</span></div></div><div><div><div>111</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>112</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>113</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>from_text</span><span>(</span><span>"&lt;repl&gt;"</span><span>, </span><span>code</span><span>);</span></div></div><div><div><div>114</div></div><div><span>    </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>115</div></div><div><span><span>    </span></span><span>sources</span><span>::</span><span>emit</span><span>(&amp;</span><span>diagnostics</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>116</div></div><div>
</div></div><div><div><div>117</div></div><div><span>    </span><span>let</span><span> </span><span>file</span><span><span> </span><span>=</span><span> &amp;</span></span><span>source</span><span>.map[</span><span>source</span><span>.id];</span></div></div><div><div><div>118</div></div><div><span>    </span><span>for</span><span> </span><span>token</span><span> </span><span>in</span><span> &amp;</span><span>tokens</span><span> {</span></div></div><div><div><div>119</div></div><div><span>        </span><span>let</span><span> </span><span>position</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>position</span><span>(</span><span>token</span><span>.span.start);</span></div></div><div><div><div>120</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{}:{} {}"</span><span>, </span><span>position</span><span>.line, </span><span>position</span><span>.column, </span><span>token</span><span>.kind);</span></div></div><div><div><div>121</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>122</div></div><div><span>}</span></div></div><div><div><div>123</div></div><div>
</div></div><div><div><div>124</div></div><div><span>/// `:ast`: parse a snippet and print its syntax tree.</span></div></div><div><div><div>125</div></div><div><span>fn</span><span> </span><span>show_ast</span><span>(</span><span>code</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>126</div></div><div><span>    </span><span>if</span><span> </span><span>code</span><span>.</span><span>is_empty</span><span>() {</span></div></div><div><div><div>127</div></div><div><span>        </span><span>eprintln!</span><span>(</span><span>"usage: :ast &lt;code&gt;"</span><span>);</span></div></div><div><div><div>128</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>129</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>130</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>from_text</span><span>(</span><span>"&lt;repl&gt;"</span><span>, </span><span>code</span><span>);</span></div></div><div><div><div>131</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>132</div></div><div><span><span>    </span></span><span>sources</span><span>::</span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>133</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>pretty</span><span>(&amp;</span><span>parsed</span><span>.program));</span></div></div><div><div><div>134</div></div><div><span>}</span></div></div><div><div><div>135</div></div><div>
</div></div><div><div><div>136</div></div><div><span>/// Evaluates one input line: an expression if it parses as one, a program</span></div></div><div><div><div>137</div></div><div><span>/// otherwise.</span></div></div><div><div><div>138</div></div><div><span>fn</span><span> </span><span>eval_line</span><span>(</span><span>line</span><span>: &amp;</span><span>str</span><span>, </span><span>interpreter</span><span>: &amp;</span><span>mut</span><span> </span><span>Interpreter</span><span>) {</span></div></div><div><div><div>139</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>from_text</span><span>(</span><span>"&lt;repl&gt;"</span><span>, </span><span>line</span><span>);</span></div></div><div><div><div>140</div></div><div>
</div></div><div><div><div>141</div></div><div><span>    </span><span>let</span><span> (</span><span>expression</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>parse_expression</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>142</div></div><div><span>    </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>expression</span><span><span>) </span><span>=</span><span> </span></span><span>expression</span><span> {</span></div></div><div><div><div>143</div></div><div><span><span>        </span></span><span>sources</span><span>::</span><span>emit</span><span>(&amp;</span><span>diagnostics</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>144</div></div><div><span>        </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>expression</span><span>.</span><span>span</span><span>();</span></div></div><div><div><div>145</div></div><div><span>        </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>Program</span><span>::</span><span>new</span><span>(</span><span>vec!</span><span><span>[</span><span>Stmt</span><span>::</span></span><span>Expr</span><span> {</span></div></div><div><div><div>146</div></div><div><span>            </span><span>expr</span><span>: </span><span>expression</span><span>,</span></div></div><div><div><div>147</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>148</div></div><div><span><span>        </span></span><span>}]);</span></div></div><div><div><div>149</div></div><div><span>        </span><span>match</span><span> </span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>program</span><span>) {</span></div></div><div><div><div>150</div></div><div><span>            </span><span>Ok</span><span>(</span><span>value</span><span>) =&gt; </span><span>println!</span><span>(</span><span>"{value}"</span><span>),</span></div></div><div><div><div>151</div></div><div><span>            </span><span>Err</span><span>(</span><span>error</span><span><span>) =&gt; </span><span>sources</span><span>::</span></span><span>emit_runtime</span><span>(&amp;</span><span>error</span><span>, &amp;</span><span>source</span><span>.map),</span></div></div><div><div><div>152</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>153</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>154</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>155</div></div><div>
</div></div><div><div><div>156</div></div><div><span>    </span><span>// Not an expression: treat the line as a small program and execute it.</span></div></div><div><div><div>157</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>158</div></div><div><span>    </span><span>if</span><span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map) {</span></div></div><div><div><div>159</div></div><div><span>        </span><span>return</span><span>;</span></div></div><div><div><div>160</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>161</div></div><div><span>    </span><span>if</span><span> </span><span>let</span><span> </span><span>Err</span><span>(</span><span>error</span><span><span>) </span><span>=</span><span> </span></span><span>interpreter</span><span>.</span><span>interpret</span><span>(&amp;</span><span>parsed</span><span>.program) {</span></div></div><div><div><div>162</div></div><div><span><span>        </span></span><span>sources</span><span>::</span><span>emit_runtime</span><span>(&amp;</span><span>error</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>163</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>164</div></div><div><span>}</span></div></div><div><div><div>165</div></div><div>
</div></div><div><div><div>166</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>167</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>168</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>169</div></div><div>
</div></div><div><div><div>170</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>171</div></div><div><span>    </span><span>fn</span><span> </span><span>quit_has_a_short_form</span><span>() {</span></div></div><div><div><div>172</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>":q"</span><span>.</span><span>parse</span><span>(), </span><span>Ok</span><span>(</span><span>MetaCommand</span><span>::</span><span>Quit</span><span>));</span></div></div><div><div><div>173</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>174</div></div><div>
</div></div><div><div><div>175</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>176</div></div><div><span>    </span><span>fn</span><span> </span><span>arguments_are_kept_whole</span><span>() {</span></div></div><div><div><div>177</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>178</div></div><div><span>            </span><span>":tokens 1 + 2"</span><span>.</span><span>parse</span><span>(),</span></div></div><div><div><div>179</div></div><div><span>            </span><span>Ok</span><span>(</span><span>MetaCommand</span><span>::</span><span>Tokens</span><span>(</span><span>"1 + 2"</span><span>.</span><span>to_owned</span><span>()))</span></div></div><div><div><div>180</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>181</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>182</div></div><div>
</div></div><div><div><div>183</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>184</div></div><div><span>    </span><span>fn</span><span> </span><span>unknown_commands_are_rejected</span><span>() {</span></div></div><div><div><div>185</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>186</div></div><div><span>            </span><span>":nope"</span><span>.</span><span>parse</span><span>::&lt;</span><span>MetaCommand</span><span>&gt;(),</span></div></div><div><div><div>187</div></div><div><span>            </span><span>Err</span><span>(</span><span>"unknown command `:nope`; try :help"</span><span>.</span><span>to_owned</span><span>())</span></div></div><div><div><div>188</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>189</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>190</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（这个文件里有两行文档注释，tag 里用了英文破折号，我这里换成了冒号；它们只是注释，你写成什么标点都行。）</p><p>几个值得看的地方：</p><ul>
<li><code>line.strip_prefix(':')</code> 返回 <code>Option&lt;&amp;str&gt;</code>，有前缀就去掉前缀返回剩下的，没有就是 <code>None</code>。配合 <code>let Some(rest) = ... else { return Err(...) };</code>，“不是命令”的情况一行搞定。</li>
<li><code>rest.split_once(char::is_whitespace)</code> 在第一个空白处把字符串切成两半。<code>char::is_whitespace</code> 是一个函数项，作为<strong>模式</strong>传给 <code>split_once</code>：标准库里凡是接受”模式”的方法，都能接受字符、字符串、字符切片或者 <code>FnMut(char) -&gt; bool</code>。</li>
<li><code>match name { "quit" | "q" =&gt; ... }</code> 字符串上的或模式。<code>other =&gt; Err(...)</code> 捕获所有剩下的名字，并在错误信息里用上它。</li>
<li>在主循环里，<code>trimmed.parse::&lt;MetaCommand&gt;()</code> 返回 <code>Result&lt;MetaCommand, String&gt;</code>，一个 <code>match</code> 就分发完了：<code>Ok(MetaCommand::Quit) =&gt; break</code>，<code>Err(message) =&gt; eprintln!("pebble: {message}")</code>。</li>
<li><code>Err(ReadlineError::Interrupted | ReadlineError::Eof) =&gt; break</code> 是嵌套的或模式：Ctrl-C 和 Ctrl-D 都安静地退出。</li>
<li><code>let _ = editor.add_history_entry(line.as_str());</code> 里 <code>let _ =</code> 明确丢弃一个 <code>Result</code>，因为历史记录失败不值得中断会话。（<code>Result</code> 是 <code>#[must_use]</code> 的，直接丢弃编译器会警告。）</li>
<li><code>eval_line</code> 先试着把输入当<strong>表达式</strong>解析（<code>parse_expression</code>），成功就把它包进一个只含一条 <code>Stmt::Expr</code> 的 <code>Program</code>，求值并打印结果；否则当作一整个程序解析执行。所以你输入 <code>1 + 2</code> 直接得到 <code>3</code>，输入 <code>let x = 5;</code> 什么也不打印。</li>
<li>解释器在 <code>run</code> 里<strong>只建一次</strong>：<code>let mut interpreter = Interpreter::new();</code>，所以 <code>let x = 5;</code> 之后下一行还能用 <code>x</code>。</li>
</ul><p>三个单元测试是为 <code>FromStr</code> 写的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>quit_has_a_short_form</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>":q"</span><span>.</span><span>parse</span><span>(), </span><span>Ok</span><span>(</span><span>MetaCommand</span><span>::</span><span>Quit</span><span>));</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>    </span><span>// ... arguments_are_kept_whole, unknown_commands_are_rejected</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>assert_eq!(":q".parse(), Ok(MetaCommand::Quit))</code> 里不用写 turbofish：右边的类型让编译器推出 <code>parse::&lt;MetaCommand&gt;</code>。能这样比较是因为 <code>MetaCommand</code> 和 <code>String</code> 都实现了 <code>PartialEq</code>，所以 <code>Result&lt;MetaCommand, String&gt;</code> 也是。完整测试在上面那个文件里。</p></section><section><h3>集成测试<a href="#集成测试"><span>#</span></a></h3><p><code>tests/cli.rs</code> 在第 7 篇的四个测试之外多了三个：<code>run_evaluates_fibonacci</code>、<code>run_reports_parse_errors_on_stderr</code>、<code>quiet_suppresses_the_final_value</code>。完整文件：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! End-to-end tests for the `pebble` binary.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! The tests drive the real executable against the example programs in the</span></div></div><div><div><div>4</div></div><div><span>//! workspace's `examples/` directory, which is why every command runs with the</span></div></div><div><div><div>5</div></div><div><span>//! workspace root as its working directory.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>path</span><span>::{</span></span><span>Path</span><span>, </span><span>PathBuf</span><span>};</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>process</span><span>::</span></span><span>Output</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>assert_cmd</span><span>::</span></span><span>Command</span><span>;</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>predicates</span><span>::</span><span>prelude</span><span>::*;</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>/// The workspace root, two levels above this crate.</span></div></div><div><div><div>14</div></div><div><span>fn</span><span> </span><span>workspace_root</span><span>() -&gt; </span><span>PathBuf</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Path</span><span>::</span><span>new</span><span>(</span><span>env!</span><span>(</span><span>"CARGO_MANIFEST_DIR"</span><span>)).</span><span>join</span><span>(</span><span>"../.."</span><span>)</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>/// A `pebble` invocation rooted at the workspace, so `examples/...` resolves.</span></div></div><div><div><div>19</div></div><div><span>fn</span><span> </span><span>pebble</span><span>() -&gt; </span><span>Command</span><span> {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>command</span><span><span> </span><span>=</span><span> </span></span><span>Command</span><span>::</span><span>cargo_bin</span><span>(</span><span>"pebble"</span><span>).</span><span>expect</span><span>(</span><span>"the pebble binary is built"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>command</span><span>.</span><span>current_dir</span><span>(</span><span>workspace_root</span><span>());</span></div></div><div><div><div>22</div></div><div><span>    </span><span>command</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>/// Runs the binary with `args` and captures its output.</span></div></div><div><div><div>26</div></div><div><span>fn</span><span> </span><span>pebble_output</span><span>(</span><span>args</span><span>: &amp;[&amp;</span><span>str</span><span>]) -&gt; </span><span>Output</span><span> {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pebble</span><span>().</span><span>args</span><span>(</span><span>args</span><span>).</span><span>output</span><span>().</span><span>expect</span><span>(</span><span>"pebble runs"</span><span>)</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>#[test]</span></div></div><div><div><div>31</div></div><div><span>fn</span><span> </span><span>run_evaluates_fibonacci</span><span>() {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"run"</span><span>, </span><span>"examples/fib.pebble"</span><span>])</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>.</span><span>success</span><span>()</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>.</span><span>stdout</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"6765"</span><span>));</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>#[test]</span></div></div><div><div><div>40</div></div><div><span>fn</span><span> </span><span>run_reports_parse_errors_on_stderr</span><span>() {</span></div></div><div><div><div>41</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"run"</span><span>, </span><span>"examples/errors.pebble"</span><span>])</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>.</span><span>failure</span><span>()</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>.</span><span>stderr</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"error"</span><span>));</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>#[test]</span></div></div><div><div><div>49</div></div><div><span>fn</span><span> </span><span>parse_prints_the_syntax_tree</span><span>() {</span></div></div><div><div><div>50</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"parse"</span><span>, </span><span>"examples/closures.pebble"</span><span>])</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>53</div></div><div><span><span>        </span></span><span>.</span><span>success</span><span>()</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>.</span><span>stdout</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"fn"</span><span>));</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>#[test]</span></div></div><div><div><div>58</div></div><div><span>fn</span><span> </span><span>tokens_prints_one_line_per_token</span><span>() {</span></div></div><div><div><div>59</div></div><div><span>    </span><span>let</span><span> </span><span>output</span><span><span> </span><span>=</span><span> </span></span><span>pebble_output</span><span>(&amp;[</span><span>"tokens"</span><span>, </span><span>"examples/hello.pebble"</span><span>]);</span></div></div><div><div><div>60</div></div><div><span>    </span><span>assert!</span><span>(</span><span>output</span><span>.status.</span><span>success</span><span>(), </span><span>"tokens failed: {output:?}"</span><span>);</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span>    </span><span>let</span><span> </span><span>stdout</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from_utf8</span><span>(</span><span>output</span><span>.stdout).</span><span>expect</span><span>(</span><span>"stdout is UTF-8"</span><span>);</span></div></div><div><div><div>63</div></div><div><span>    </span><span>let</span><span> </span><span>lines</span><span>: </span><span>Vec</span><span>&lt;&amp;</span><span>str</span><span><span>&gt; </span><span>=</span><span> </span></span><span>stdout</span><span>.</span><span>lines</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>64</div></div><div><span>    </span><span>assert!</span><span><span>(</span><span>!</span></span><span>lines</span><span>.</span><span>is_empty</span><span>(), </span><span>"expected at least one token line"</span><span>);</span></div></div><div><div><div>65</div></div><div><span>    </span><span>assert!</span><span>(</span></div></div><div><div><div>66</div></div><div><span>        </span><span>lines</span><span>[</span><span>0</span><span>].</span><span>contains</span><span>(</span><span>':'</span><span><span>) </span><span>&amp;&amp;</span><span> </span></span><span>lines</span><span>[</span><span>0</span><span>].</span><span>contains</span><span>(</span><span>' '</span><span>),</span></div></div><div><div><div>67</div></div><div><span>        </span><span>"expected `line:col kind`, got {first:?}"</span><span>,</span></div></div><div><div><div>68</div></div><div><span>        </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>lines</span><span>[</span><span>0</span><span>]</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>70</div></div><div><span>}</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>#[test]</span></div></div><div><div><div>73</div></div><div><span>fn</span><span> </span><span>quiet_suppresses_the_final_value</span><span>() {</span></div></div><div><div><div>74</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>75</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"run"</span><span>, </span><span>"--quiet"</span><span>, </span><span>"examples/hello.pebble"</span><span>])</span></div></div><div><div><div>76</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>77</div></div><div><span><span>        </span></span><span>.</span><span>success</span><span>()</span></div></div><div><div><div>78</div></div><div><span><span>        </span></span><span>.</span><span>stdout</span><span>(</span></div></div><div><div><div>79</div></div><div><span><span>            </span></span><span>predicate</span><span>::</span><span>str</span><span>::</span><span>contains</span><span>(</span><span>"hello, pebble!"</span><span>).</span><span>and</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"nil"</span><span>).</span><span>not</span><span>()),</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>81</div></div><div><span>}</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span>#[test]</span></div></div><div><div><div>84</div></div><div><span>fn</span><span> </span><span>check_is_silent_on_a_valid_file</span><span>() {</span></div></div><div><div><div>85</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>86</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"check"</span><span>, </span><span>"examples/fib.pebble"</span><span>])</span></div></div><div><div><div>87</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>88</div></div><div><span><span>        </span></span><span>.</span><span>success</span><span>()</span></div></div><div><div><div>89</div></div><div><span><span>        </span></span><span>.</span><span>stdout</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>is_empty</span><span>())</span></div></div><div><div><div>90</div></div><div><span><span>        </span></span><span>.</span><span>stderr</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>is_empty</span><span>());</span></div></div><div><div><div>91</div></div><div><span>}</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span>#[test]</span></div></div><div><div><div>94</div></div><div><span>fn</span><span> </span><span>unknown_subcommands_are_usage_errors</span><span>() {</span></div></div><div><div><div>95</div></div><div><span>    </span><span>pebble</span><span>().</span><span>arg</span><span>(</span><span>"frobnicate"</span><span>).</span><span>assert</span><span>().</span><span>code</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>96</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>真实运行<a href="#真实运行"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span></div></div><div><div><div>2</div></div><div><span>./target/debug/pebble</span><span> </span><span>run</span><span> </span><span>examples/closures.pebble</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run examples/closures.pebble</span></div></div><div><div><div>2</div></div><div><span>1</span></div></div><div><div><div>3</div></div><div><span>2</span></div></div><div><div><div>4</div></div><div><span>3</span></div></div><div><div><div>5</div></div><div><span>$ pebble run examples/collections.pebble</span></div></div><div><div><div>6</div></div><div><span>[1, 4, 9, 16, 25]</span></div></div><div><div><div>7</div></div><div><span>fr -&gt; Paris</span></div></div><div><div><div>8</div></div><div><span>jp -&gt; Tokyo</span></div></div><div><div><div>9</div></div><div><span>de -&gt; Berlin</span></div></div><div><div><div>10</div></div><div><span>3</span></div></div><div><div><div>11</div></div><div><span>$ pebble run examples/fib.pebble</span></div></div><div><div><div>12</div></div><div><span>6765</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>closures.pebble</code> 的三行 <code>1 2 3</code> 就是 <code>make_counter</code> 那个闭包在三次调用里记住了状态。<code>collections.pebble</code> 里 map 遍历写成固定顺序的 <code>["fr", "jp", "de"]</code>，因为 <code>HashMap</code> 的内部顺序不保证，这是示例文件里一个故意的设计。</p><p>语法错误在运行之前就被拦下，退出码是 1：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run examples/errors.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运行时错误会带着调用栈。一个有两层调用的程序，文件是 <code>trace.pebble</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn inner(x) {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>return x + missing;</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div><span>fn outer(x) {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>return inner(x) * 2;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>outer(1);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ pebble run trace.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E2001]: undefined variable `missing`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; trace.pebble:2:16</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>2 |     return x + missing;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                ^^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= note: declare it first with `let`</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>in `inner` at trace.pebble:5:12</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>in `outer` at trace.pebble:7:1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每个 <code>in ... at</code> 是调用发生的位置，从最里面往外：<code>inner</code> 是在 <code>5:12</code> 被调用的（<code>outer</code> 的函数体里），<code>outer</code> 是在 <code>7:1</code> 被调用的（顶层）。这就是 <code>with_frame</code> 一层一层往上加的结果。</p><p><code>pebble repl</code> 的一次会话（我把它输入重定向进去，所以提示符和输出交织在一起）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Pebble REPL. Type :help for commands, :quit to exit.</span></div></div><div><div><div>2</div></div><div><span>pebble&gt; pebble&gt; 10</span></div></div><div><div><div>3</div></div><div><span>pebble&gt; expr</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>binary +</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>int 1</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>int 2</span></div></div><div><div><div>7</div></div><div><span>pebble&gt; pebble&gt; 7</span></div></div><div><div><div>8</div></div><div><span>pebble&gt; 1:1 1</span></div></div><div><div><div>9</div></div><div><span>1:3 `+`</span></div></div><div><div><div>10</div></div><div><span>1:5 2</span></div></div><div><div><div>11</div></div><div><span>1:6 end of file</span></div></div><div><div><div>12</div></div><div><span>pebble&gt; pebble: unknown command `:nope`; try :help</span></div></div><div><div><div>13</div></div><div><span>pebble&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>let x = 5;</code> 没有输出，<code>x * 2</code> 得到 <code>10</code>，<code>:ast 1 + 2;</code> 打印语法树，<code>fn f() { return 7; }</code> 没有输出，<code>f()</code> 得到 <code>7</code>，<code>:tokens 1 + 2</code> 逐个打印 token，<code>:nope</code> 报我们在 <code>FromStr</code> 里写的那句”unknown command”。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在工作区根目录里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-interp</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--workspace</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>4</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-interp</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>4</div></div><div><span>$ cargo test -p pebble-cli</span></div></div><div><div><div>5</div></div><div><span>...</span></div></div><div><div><div>6</div></div><div><span>running 3 tests</span></div></div><div><div><div>7</div></div><div><span>test repl::tests::arguments_are_kept_whole ... ok</span></div></div><div><div><div>8</div></div><div><span>test repl::tests::quit_has_a_short_form ... ok</span></div></div><div><div><div>9</div></div><div><span>test repl::tests::unknown_commands_are_rejected ... ok</span></div></div><div><div><div>10</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div><div><div><div>12</div></div><div><span>running 7 tests</span></div></div><div><div><div>13</div></div><div><span>test unknown_subcommands_are_usage_errors ... ok</span></div></div><div><div><div>14</div></div><div><span>test run_reports_parse_errors_on_stderr ... ok</span></div></div><div><div><div>15</div></div><div><span>test check_is_silent_on_a_valid_file ... ok</span></div></div><div><div><div>16</div></div><div><span>test quiet_suppresses_the_final_value ... ok</span></div></div><div><div><div>17</div></div><div><span>test parse_prints_the_syntax_tree ... ok</span></div></div><div><div><div>18</div></div><div><span>test tokens_prints_one_line_per_token ... ok</span></div></div><div><div><div>19</div></div><div><span>test run_evaluates_fibonacci ... ok</span></div></div><div><div><div>20</div></div><div><span>test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s</span></div></div><div><div><div>21</div></div><div><span>$ cargo clippy --workspace --all-targets -- -D warnings</span></div></div><div><div><div>22</div></div><div><span>...</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>Checking pebble-cli v0.1.0 (/tmp/pebble-rebuild-09/crates/pebble-cli)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s</span></div></div><div><div><div>25</div></div><div><span>$ cargo fmt --all --check; echo "exit: $?"</span></div></div><div><div><div>26</div></div><div><span>exit: 0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pebble-interp</code> 的 16 个测试，<code>pebble-cli</code> 的 3 个单元测试加 7 个集成测试，clippy 和 rustfmt 都干净。</p><p>下面这两个文件现在必须和 tag 逐字节相同：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/Cargo.toml" target="_blank"><code>crates/pebble-interp/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-interp/src/lib.rs" target="_blank"><code>crates/pebble-interp/src/lib.rs</code></a></li>
</ul><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>for</span><span> </span><span>f</span><span> </span><span>in</span><span> </span><span>crates/pebble-interp/Cargo.toml</span><span> </span><span>crates/pebble-interp/src/lib.rs</span><span>; </span><span>do</span></div></div><div><div><div>2</div></div><div><span>  </span><span>diff</span><span> </span><span>&lt;(</span><span>git</span><span> </span><span>-C</span><span> ~/pebble-tag show tutorial-v1:</span><span>$f</span><span>)</span><span> </span><span>$f</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>"same  </span><span>$f</span><span>"</span></div></div><div><div><div>3</div></div><div><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>same  crates/pebble-interp/Cargo.toml</span></div></div><div><div><div>2</div></div><div><span>same  crates/pebble-interp/src/lib.rs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>examples/</code> 下的五个 <code>.pebble</code> 文件也和 tag 相同，它们在第 7 篇已经创建。</p><p><strong>中间文件</strong>（还不是最终版，后面的篇章会继续改）：</p><ul>
<li><code>crates/pebble-cli/</code> 下的全部七个文件：<code>Cargo.toml</code>、<code>src/main.rs</code>、<code>src/cli.rs</code>、<code>src/commands.rs</code>、<code>src/sources.rs</code>、<code>src/repl.rs</code>、<code>tests/cli.rs</code>。最终版本多出来的东西：字节码和反汇编（第 10 篇）、两个后端的 <code>Backend</code> trait、<code>--vm</code> 和 <code>bench</code>（第 12 篇）、并行的 <code>check</code>（第 14 篇）、可选的 <code>repl</code> 特性（第 15 篇）。其中 <code>src/repl.rs</code> 已经和 tag 一致（除了上面说的两行注释的标点），第 15 篇它只多出一个特性开关。</li>
<li>根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code>：现在列了 8 个内部 crate 加 <code>anyhow</code>、<code>clap</code>、<code>proc-macro2</code>、<code>quote</code>、<code>rustyline</code>、<code>syn</code>。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）；如果你把仓库克隆到了 <code>~/pebble-tag</code>，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，先 <code>cd ~/pebble-tag/lab/rust-lab</code> 再运行下表的命令（也可以把这个目录复制成 <code>~/rust-lab</code>）。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch09_closures.rs" target="_blank"><code>ch09_closures.rs</code></a></td><td><code>cargo run --quiet --bin ch09_closures</code></td><td>运行，首行 <code>HELLO HELLO</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch09_closure_escape.rs" target="_blank"><code>ch09_closure_escape.rs</code></a></td><td><code>cargo build --bin ch09_closure_escape</code></td><td>编译失败：E0373</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch09_fnonce_twice.rs" target="_blank"><code>ch09_fnonce_twice.rs</code></a></td><td><code>cargo build --bin ch09_fnonce_twice</code></td><td>编译失败：E0382</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch09_fn_bound.rs" target="_blank"><code>ch09_fn_bound.rs</code></a></td><td><code>cargo build --bin ch09_fn_bound</code></td><td>编译失败：E0507</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch09_iter_mutate.rs" target="_blank"><code>ch09_iter_mutate.rs</code></a></td><td><code>cargo run --quiet --bin ch09_iter_mutate</code></td><td>先打印 <code>with a snapshot: [1, 2, 3, 10, 20, 30]</code>，随后 <code>RefCell already borrowed</code> panic，退出码 101</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch09_cycle.rs" target="_blank"><code>ch09_cycle.rs</code></a></td><td><code>cargo run --quiet --bin ch09_cycle</code></td><td>运行，首行 <code>-- a scope without functions</code>；不带函数的作用域 <code>alive after drop: false</code>，存了函数的那个 <code>alive after drop: true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch09_weak_env.rs" target="_blank"><code>ch09_weak_env.rs</code></a></td><td><code>cargo run --quiet --bin ch09_weak_env</code></td><td>运行，首行 <code>first call:  None</code></td></tr></tbody></table></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 加一个 <code>:env</code> 元命令。</strong> 它应该列出全局环境里用户定义的名字。需要改哪些地方，会遇到什么障碍？（讨论题）</p>
参考答案<p><code>MetaCommand</code> 加一个 <code>Env</code> 变体，<code>from_str</code> 里加 <code>"env" =&gt; Ok(Self::Env)</code>，循环里加一个 <code>Ok(MetaCommand::Env) =&gt; show_env(&amp;interpreter)</code> 分支。障碍在 <code>pebble-value</code>：<code>Env</code> 的 <code>values</code> 字段是私有的，只有 <code>get</code>、<code>contains_local</code>、<code>local_len</code> 这些方法，没有办法<strong>枚举名字</strong>。你要给 <code>Env</code> 加一个方法，比如 <code>pub fn names(&amp;self) -&gt; impl Iterator&lt;Item = &amp;str&gt;</code>（第 8 篇的 <code>-&gt; impl Iterator</code>），然后 REPL 里 <code>interpreter.globals().borrow().names()</code>。第二个障碍是内建函数也在全局环境里，不过滤的话 11 个 <code>print</code>、<code>len</code> 会淹没用户定义的名字，可以对每个名字 <code>get</code> 出来，跳过 <code>Value::Native</code>。由于 <code>HashMap</code> 的迭代顺序不确定，<code>:env</code> 的输出最好先排序。</p><p><strong>2. <code>fn f() {}</code> 单独一行会泄漏吗？</strong> 先预测，再解释。顺便猜一下下面六个程序里哪些让全局环境泄漏（每个程序跑完、丢掉解释器之后，检查全局环境是否还活着）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn f() {}</span></div></div><div><div><div>2</div></div><div><span>let f = fn() { return 1; }; f();</span></div></div><div><div><div>3</div></div><div><span>(fn() { return 1; })();</span></div></div><div><div><div>4</div></div><div><span>let x = [1, 2]; for i in x { print(i); }</span></div></div><div><div><div>5</div></div><div><span>fn make() { return fn() { return 1; }; } let g = make();</span></div></div><div><div><div>6</div></div><div><span>let a = []; push(a, a);</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
参考答案<p>会泄漏：函数声明本身就把 <code>f</code> 放进了全局环境，<code>f</code> 的 <code>env</code> 又是全局环境，环和有没有调用、函数体里写什么无关。我用一个临时测试验证过，每个程序跑完、丢掉解释器之后，检查全局环境是否还活着（<code>Weak::upgrade().is_some()</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn f() {}                                                    leaks: true</span></div></div><div><div><div>2</div></div><div><span>let f = fn() { return 1; }; f();                             leaks: true</span></div></div><div><div><div>3</div></div><div><span>(fn() { return 1; })();                                      leaks: false</span></div></div><div><div><div>4</div></div><div><span>let x = [1, 2]; for i in x { print(i); }                     leaks: false</span></div></div><div><div><div>5</div></div><div><span>fn make() { return fn() { return 1; }; } let g = make();     leaks: true</span></div></div><div><div><div>6</div></div><div><span>let a = []; push(a, a);                                      leaks: false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第三行没有泄漏：匿名函数立刻被调用，调用完这个临时的函数值就被丢掉了，它从来没有放进任何环境里，所以没有环。第六行也显示 <code>false</code>，但别被骗了：<code>push(a, a)</code> 让列表包含它自己，这是另一个 <code>Rc</code> 环（<code>Rc&lt;RefCell&lt;Vec&lt;Value&gt;&gt;&gt;</code> 里装着指向自己的 <code>Rc</code>），泄漏的是那个列表，而全局环境本身没泄漏，我的检查只看全局环境。</p><p><strong>3. 让 <code>while</code> 支持 <code>break 值;</code>，整个 <code>while</code> 就变成一个有值的表达式。</strong> 需要改哪些层？Rust 自己为什么只允许 <code>loop</code> 这么做？（讨论题）</p>
参考答案<p>改动是纵向的：解析器要接受 <code>break</code> 后面跟一个可选表达式；AST 的 <code>Stmt::Break</code> 要带一个 <code>Option&lt;Expr&gt;</code>；<code>Flow::Break</code> 要带一个 <code>Option&lt;Value&gt;</code>（它现在是一个没有数据的变体）；解释器里 <code>Stmt::While</code> 分支遇到 <code>Break(v)</code> 要返回 <code>Flow::Value(v)</code> 而不是 <code>nil</code>；字节码编译器和 VM 也要跟上（第 10、12 篇）。最麻烦的是语法：<code>while</code> 是语句，不是表达式，要让它”有值”，要么把 <code>while</code> 提升成表达式，要么只让它的值出现在 <code>let x = while ... ;</code> 这样的位置。</p><p>Rust 把 <code>break 值</code> 限制在 <code>loop</code> 上，原因是 <code>loop</code> 只能通过 <code>break</code> 退出，每条出口都带着值，类型是确定的；<code>while cond { ... }</code> 还能因为条件变成 <code>false</code> 而自然结束，那条路径没有值可给，只能是 <code>()</code>，于是 <code>break 5</code> 的类型和”自然结束”的类型对不上。第 1 篇的 <code>loop { break value }</code> 就是这个设计。</p><p>解释器能跑了，闭包、递归、列表、映射都在，它也泄漏。还有几处我一眼就看出会贵的地方，只是现在还没量过：每次变量查找都要沿着 <code>HashMap&lt;String, Value&gt;</code> 的链走，每次函数调用分配一个 <code>Env</code>，每次执行 <code>fn</code> 语句复制一遍函数体。<a href="/posts/rust-by-pebble-10-bytecode-macros/">下一篇</a>我们换一种做法：先把 AST 编译成一串线性的字节码指令，然后用一个循环去跑它。那时候你会看到 <code>macro_rules!</code>、回填，以及 <code>HashMap</code> 的 <code>entry</code> API 怎么让编译器给我们的标识符做去重。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-08-rc-refcell-values/</id>
      <title type="text">共享与可变：Rc、RefCell 和运行时的值</title>
      <published>2026-10-13T07:00:00.000Z</published>
      <updated>2026-10-13T07:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-08-rc-refcell-values/"/>
      <summary type="text">let b = a; push(b, 1); a 必须显示 [1]。我们依次撞上移动、独占借用和 Rc 的墙，最后用 Rc&lt;RefCell&lt;..&gt;&gt; 搭出 pebble-value：值、可哈希的 Map 键、运行时错误、环境链和内建函数表，顺带学会 trait 对象、函数指针、Hash/Eq 契约。</summary>
      <content type="html"><![CDATA[<p>Pebble 里有一个我最先想写成测试的程序，只有四行：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let a = [];</span></div></div><div><div><div>2</div></div><div><span>let b = a;</span></div></div><div><div><div>3</div></div><div><span>push(b, 1);</span></div></div><div><div><div>4</div></div><div><span>a;</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>最后一行必须得到 <code>[1]</code>。<code>a</code> 和 <code>b</code> 是同一个列表的两个名字，通过 <code>b</code> 改，<code>a</code> 就能看到。这在 Python 和 JavaScript 里是常识，没人会为此停下来想。在 Rust 里，这四行里的每一步都会撞上编译器：<code>let b = a</code> 是移动，<code>push</code> 要独占借用，两个名字想同时拥有一块数据，所有权模型直接说不。这一篇我们就是要绕过这堵墙，并且在绕的过程中把 <code>Rc</code>、<code>RefCell</code>、<code>HashMap</code>、trait 对象和函数指针一次学完。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇我们从零写出 <code>pebble-value</code>，也就是 Pebble 运行时的对象模型：<code>Value</code> 枚举、可以做 Map 键的 <code>MapKey</code>、带调用栈的 <code>RuntimeError</code>、嵌套作用域 <code>Env</code>、内建函数表。你会用到的 Rust 特性：<code>Rc</code>、<code>Rc&lt;str&gt;</code>、<code>RefCell</code>、<code>HashMap</code> 与 <code>Hash</code>/<code>Eq</code> 契约、手写 <code>PartialEq</code>、<code>From</code>、函数指针、trait 对象 <code>dyn Output</code>、<code>static</code> 表、切片模式、<code>-&gt; impl Iterator</code>。结束时 <code>cargo test -p pebble-value</code> 必须是 11 个测试全部通过，并且 <code>crates/pebble-value/</code> 下的两个文件与 tag 逐字节相同。上一篇是 <a href="/posts/rust-by-pebble-07-parser-errors-cli/">Result、? 与递归下降：解析器和第一个命令行</a>，这一篇不依赖命令行，只依赖前面写好的 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-ast</code>。</p></div></div>
<section><h2>先撞三面墙<a href="#先撞三面墙"><span>#</span></a></h2><p>我们把 Pebble 的列表先翻译成 Rust 里最自然的东西：<code>Vec&lt;i64&gt;</code>。第一次尝试，直接照字面写（<code>~/rust-lab/src/bin/ch08_shared_list_move.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span>: </span><span>Vec</span><span>&lt;</span><span>i64</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>b</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>b</span><span>.</span><span>push</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>a</span><span>);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>let b = a;</code> 在 Rust 里不是”让 b 也指向同一个列表”，而是把 <code>Vec</code> 的所有权搬给 <code>b</code>。第 2 篇讲过，这叫移动。编译器的反馈：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch08_shared_list_move</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0382]: borrow of moved value: `a`</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch08_shared_list_move.rs:6:22</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>2 |     let a: Vec&lt;i64&gt; = Vec::new();</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|         - move occurs because `a` has type `Vec&lt;i64&gt;`, which does not implement the `Copy` trait</span></div></div><div><div><div>8</div></div><div><span>3 |     let b = a;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|             - value moved here</span></div></div><div><div><div>10</div></div><div><span>...</span></div></div><div><div><div>11</div></div><div><span>6 |     println!("{:?}", a);</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|                      ^ value borrowed here after move</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>help: consider cloning the value if the performance cost is acceptable</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span>3 |     let b = a.clone();</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|              ++++++++</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>For more information about this error, try `rustc --explain E0382`.</span></div></div><div><div><div>20</div></div><div><span>error: could not compile `rust-lab` (bin "ch08_shared_list_move") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C++ 程序员在这里的第一反应是”那我用引用”。好，第二次尝试，用两个可变引用（<code>~/rust-lab/src/bin/ch08_shared_list_mut.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>a</span><span>: </span><span>Vec</span><span>&lt;</span><span>i64</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>a</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>c</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>a</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>b</span><span>.</span><span>push</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>c</span><span>.</span><span>push</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>a</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch08_shared_list_mut</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0499]: cannot borrow `a` as mutable more than once at a time</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch08_shared_list_mut.rs:4:13</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>3 |     let b = &amp;mut a;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|             ------ first mutable borrow occurs here</span></div></div><div><div><div>8</div></div><div><span>4 |     let c = &amp;mut a;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|             ^^^^^^ second mutable borrow occurs here</span></div></div><div><div><div>10</div></div><div><span>5 |     b.push(1);</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|     - first borrow later used here</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0499`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `rust-lab` (bin "ch08_shared_list_mut") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是第 2 篇的”别名与可变不能同时存在”，原样撞上。借用解决不了问题，因为借用有生命周期：<code>b</code> 和 <code>c</code> 都得在某个作用域里结束。而 Pebble 的两个变量可以活得一样久，谁先死谁后死由程序决定。我们需要的是<strong>共同所有权</strong>：两个所有者，最后一个离开时才释放。</p><p>C++ 里这个东西叫 <code>std::shared_ptr</code>。Rust 标准库里对应的是 <code>Rc&lt;T&gt;</code>（reference counted）。第三次尝试（<code>~/rust-lab/src/bin/ch08_shared_list_rc.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span>: </span><span>Rc</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>i64</span><span><span>&gt;&gt; </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>Vec</span><span>::</span></span><span>new</span><span>());</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>a</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>b</span><span>.</span><span>push</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>a</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch08_shared_list_rc</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0596]: cannot borrow data in an `Rc` as mutable</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch08_shared_list_rc.rs:6:5</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>6 |     b.push(1);</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|     ^ cannot borrow as mutable</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc&lt;Vec&lt;i64&gt;&gt;`</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>For more information about this error, try `rustc --explain E0596`.</span></div></div><div><div><div>12</div></div><div><span>error: could not compile `rust-lab` (bin "ch08_shared_list_rc") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Rc::clone(&amp;a)</code> 不复制列表，只把引用计数加一，然后返回一个新的 <code>Rc</code>，指向同一块堆内存。这一步成功了。失败的是 <code>b.push(1)</code>：<code>Rc&lt;T&gt;</code> 只给你 <code>&amp;T</code>，从不给 <code>&amp;mut T</code>。原因很直接：如果两个所有者都能拿到 <code>&amp;mut T</code>，那就把刚才的 E0499 从编译期搬到了运行期，并且没人检查。</p><p><code>shared_ptr&lt;vector&lt;int&gt;&gt;</code> 在 C++ 里允许你随便写，迭代到一半被另一个别名 <code>push_back</code> 导致迭代器失效，那是你自己的事（UB）。Rust 的选择更保守：共享的所有权默认只读，要想写，你得显式地走一个”带检查的后门”。这个后门叫 <code>RefCell&lt;T&gt;</code>。</p><p><code>~/rust-lab/src/bin/ch08_shared_list.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>i64</span><span><span>&gt;&gt;&gt; </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Vec</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>a</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"strong count: {}"</span><span>, </span><span>Rc</span><span>::</span><span>strong_count</span><span>(&amp;</span><span>a</span><span>));</span></div></div><div><div><div>8</div></div><div><span>    </span><span>b</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>a</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"a = {:?}"</span><span>, </span><span>a</span><span>.</span><span>borrow</span><span>());</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"b = {:?}"</span><span>, </span><span>b</span><span>.</span><span>borrow</span><span>());</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"same allocation: {}"</span><span>, </span><span>Rc</span><span>::</span><span>ptr_eq</span><span>(&amp;</span><span>a</span><span>, &amp;</span><span>b</span><span>));</span></div></div><div><div><div>13</div></div><div><span>    </span><span>drop</span><span>(</span><span>b</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"strong count after drop(b): {}"</span><span>, </span><span>Rc</span><span>::</span><span>strong_count</span><span>(&amp;</span><span>a</span><span>));</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_shared_list</span></div></div><div><div><div>2</div></div><div><span>strong count: 2</span></div></div><div><div><div>3</div></div><div><span>a = [1, 2]</span></div></div><div><div><div>4</div></div><div><span>b = [1, 2]</span></div></div><div><div><div>5</div></div><div><span>same allocation: true</span></div></div><div><div><div>6</div></div><div><span>strong count after drop(b): 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>类型 <code>Rc&lt;RefCell&lt;Vec&lt;i64&gt;&gt;&gt;</code> 读起来像俄罗斯套娃，但每一层只做一件事：</p><ul>
<li><code>Vec&lt;i64&gt;</code>：数据本身。</li>
<li><code>RefCell&lt;..&gt;</code>：在数据旁边放一个借用计数，<code>borrow()</code> 返回只读守卫 <code>Ref</code>，<code>borrow_mut()</code> 返回独占守卫 <code>RefMut</code>。</li>
<li><code>Rc&lt;..&gt;</code>：在外面再加一个引用计数，让多个所有者共享。</li>
</ul><p><code>RefCell</code> 的规则和第 2 篇的编译期规则完全一样：多个只读，或者一个独占，二选一。区别只是检查的时间。编译器没法知道运行时谁会来，所以它在”进门时”翻一下牌子（那个计数）：牌子显示有人在读，你想写，就当场 panic。</p><p><code>~/rust-lab/src/bin/ch08_refcell_panic.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>cell</span><span><span> </span><span>=</span><span> </span></span><span>RefCell</span><span>::</span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>reader</span><span><span> </span><span>=</span><span> </span></span><span>cell</span><span>.</span><span>borrow</span><span>();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"try_borrow_mut while reading: {:?}"</span><span>, </span><span>cell</span><span>.</span><span>try_borrow_mut</span><span>().</span><span>is_err</span><span>());</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"first = {}"</span><span>, </span><span>reader</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>cell</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span>(</span><span>4</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>println!</span><span>(</span><span>"unreachable"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_refcell_panic</span></div></div><div><div><div>2</div></div><div><span>try_borrow_mut while reading: true</span></div></div><div><div><div>3</div></div><div><span>first = 1</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>thread 'main' (89899) panicked at src/bin/ch08_refcell_panic.rs:8:10:</span></div></div><div><div><div>6</div></div><div><span>RefCell already borrowed</span></div></div><div><div><div>7</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行 <code>true</code> 是 <code>try_borrow_mut()</code>：它不 panic，而是返回 <code>Result</code>，让你自己决定怎么办。第二次 <code>borrow_mut()</code> 没有这个礼貌，直接 panic，<code>reader</code> 这个 <code>Ref</code> 守卫还活着，所以计数没归零。这个 panic 不是内存不安全，是 Rust 在说”你的程序逻辑违反了别名规则”。第 9 篇里解释器在遍历列表的同时修改它时，会再撞上它一次。</p><p>还有两点要提前交代，免得你在后面的代码里被吓到：</p><ul>
<li><code>Rc</code> 的计数是<strong>非原子</strong>的，所以它不能跨线程（C++ 的 <code>shared_ptr</code> 计数永远是原子的，哪怕你用不上）。Rust 会在类型层面禁止你把 <code>Rc</code> 送到另一个线程，这是第 14 篇的内容。</li>
<li><code>RefCell</code> 里的计数在 clippy 的报错里会露面一次（<code>Cell&lt;isize&gt;</code>）。<code>Cell</code> 是第 11 篇的主角，这里先当成”能在 <code>&amp;self</code> 下修改的整数”。</li>
</ul></section>
<section><h2>新建 pebble-value<a href="#新建-pebble-value"><span>#</span></a></h2><p>我们已经有了 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-lexer</code>、<code>pebble-ast</code>、<code>pebble-macros</code>、<code>pebble-parser</code> 六个 crate。按第 1 篇的步骤新建第七个：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-value</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把它登记到根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 里，放在 <code>pebble-parser</code> 那一行后面（这一行必须和 tag 里的一样，因为到了第 9 篇，解释器要通过 <code>workspace = true</code> 引用它）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-value</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-value"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>crates/pebble-value/Cargo.toml</code> 的完整内容如下，和 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-value/Cargo.toml" target="_blank">tag 里的文件</a> 一致：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-value"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Runtime values for the Pebble tree-walking interpreter."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div><span>pebble-ast.workspace</span><span> = </span><span>true</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>17</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它依赖三个我们自己写的 crate：<code>pebble-span</code> 提供 <code>Span</code>，<code>pebble-diag</code> 提供 <code>Diagnostic</code>，<code>pebble-ast</code> 提供 <code>Stmt</code>（函数值要保存它的函数体，而函数体就是一串语句）。注意依赖方向：<code>value</code> 依赖 <code>ast</code>，所以 <code>ast</code> 永远不知道运行时是什么。</p></section>
<section><h2>Value：第一版<a href="#value第一版"><span>#</span></a></h2><p><code>Value</code> 是 Pebble 里每个变量、每个表达式结果的类型。我们按文件顺序写，但分五轮（Value、Map、运算符、Env、内建函数，各占一节）：先写能编译的最小版本，每一轮加一类东西，每一轮都跑测试。第一轮只有不需要哈希、不需要环境、不需要函数的六种值。</p><p><code>crates/pebble-value/src/lib.rs</code> 的第一版开头如下。先把 <code>cargo new --lib</code> 生成的 <code>src/lib.rs</code>（一个示例函数加一个测试）整个清空，再写属性和 <code>use</code>。文件头的完整注释放到最后一步再补；下面这组 <code>use</code> 是 tag 里的完整版本，一次写全，省得后面每一轮回来补。<code>HashMap</code>、<code>Hash</code>/<code>Hasher</code>、<code>Span</code>、<code>Diagnostic</code> 要到后面的轮次才会用到，那之前编译器会报 “unused import” 警告，不用管：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>collections</span><span>::</span></span><span>HashMap</span><span>;</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>std</span><span>::fmt;</span></span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>hash</span><span>::{</span></span><span>Hash</span><span>, </span><span>Hasher</span><span>};</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::</span></span><span>Diagnostic</span><span>;</span></div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后是枚举本身：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A Pebble runtime value.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Heap-allocated variants share through [`Rc`]; the mutable ones wrap their</span></div></div><div><div><div>4</div></div><div><span>/// contents in [`RefCell`] so mutation is checked at run time rather than at</span></div></div><div><div><div>5</div></div><div><span>/// compile time.</span></div></div><div><div><div>6</div></div><div><span>#[derive(</span><span>Clone</span><span>)]</span></div></div><div><div><div>7</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// `nil`</span></div></div><div><div><div>9</div></div><div><span>    </span><span>Nil</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>/// `true` / `false`</span></div></div><div><div><div>11</div></div><div><span>    </span><span>Bool</span><span>(</span><span>bool</span><span>),</span></div></div><div><div><div>12</div></div><div><span>    </span><span>/// A 64-bit signed integer.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>14</div></div><div><span>    </span><span>/// A 64-bit float.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Float</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>16</div></div><div><span>    </span><span>/// An immutable, shared string.</span></div></div><div><div><div>17</div></div><div><span>    </span><span>Str</span><span>(</span><span>Rc</span><span>&lt;</span><span>str</span><span>&gt;),</span></div></div><div><div><div>18</div></div><div><span>    </span><span>/// A mutable, shared list.</span></div></div><div><div><div>19</div></div><div><span>    </span><span>List</span><span>(</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>Value</span><span>&gt;&gt;&gt;),</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个没见过的写法：</p><ul>
<li><code>Rc&lt;str&gt;</code> 是不可变的共享字符串。<code>str</code> 是第 2 篇的”字符串切片”本体（没有长度信息的动态大小类型），<code>Rc&lt;str&gt;</code> 把字符数据和引用计数放在同一块堆内存里。对比 <code>Rc&lt;String&gt;</code>：那会有两层间接（先到 <code>String</code> 的三个字，再到它的缓冲区）。</li>
<li><code>Rc&lt;RefCell&lt;Vec&lt;Value&gt;&gt;&gt;</code> 就是我们上面实验里的套娃，元素类型换成了 <code>Value</code> 自己。<code>Value</code> 里面有 <code>Vec&lt;Value&gt;</code>，没有无限大小问题，因为 <code>Vec</code> 把元素放在堆上，自身只占三个字（指针、容量、长度）。第 5 篇讲过，递归类型需要一层间接，<code>Vec</code> 和 <code>Rc</code> 都算。</li>
<li><code>#[derive(Clone)]</code> 但没有 <code>Copy</code>。克隆一个 <code>Value::List</code> 只是把引用计数加一，不会复制列表。对 C++ 程序员来说，这等于 <code>shared_ptr</code> 的拷贝构造，而不是 <code>vector</code> 的。</li>
</ul><p>我在实验里量过这几个类型的大小（<code>~/rust-lab/src/bin/ch08_rc_str.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::size_of;</span></span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>owned</span><span>: </span><span>String</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"pebble"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>shared</span><span>: </span><span>Rc</span><span>&lt;</span><span>str</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>from</span><span>(</span><span>owned</span><span>.</span><span>as_str</span><span>());</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>other</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>clone</span><span>(&amp;</span><span>shared</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;String&gt;():   {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>String</span><span>&gt;());</span></div></div><div><div><div>9</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;Rc&lt;str&gt;&gt;():  {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Rc</span><span>&lt;</span><span>str</span><span>&gt;&gt;());</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;Rc&lt;String&gt;&gt;(): {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Rc</span><span>&lt;</span><span>String</span><span>&gt;&gt;());</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"len = {}, same = {}, count = {}"</span><span>, </span><span>shared</span><span>.</span><span>len</span><span>(), </span><span>Rc</span><span>::</span><span>ptr_eq</span><span>(&amp;</span><span>shared</span><span>, &amp;</span><span>other</span><span>), </span><span>Rc</span><span>::</span><span>strong_count</span><span>(&amp;</span><span>shared</span><span>));</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>upper</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> &amp;</span></span><span>shared</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>upper</span><span>.</span><span>to_uppercase</span><span>());</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_rc_str</span></div></div><div><div><div>2</div></div><div><span>size_of::&lt;String&gt;():   24</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;Rc&lt;str&gt;&gt;():  16</span></div></div><div><div><div>4</div></div><div><span>size_of::&lt;Rc&lt;String&gt;&gt;(): 8</span></div></div><div><div><div>5</div></div><div><span>len = 6, same = true, count = 2</span></div></div><div><div><div>6</div></div><div><span>PEBBLE</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Rc&lt;str&gt;</code> 是 16 字节（指针加长度，叫胖指针），<code>Rc&lt;String&gt;</code> 是 8 字节但要多跳一次。Pebble 的字符串是创建后不再修改的，所以我们选了前者：<code>Value::Str</code> 克隆起来是 O(1)，<code>len</code> 不需要跳转。在 tag 的 worktree 里加一个临时测试，量出整个 <code>Value</code> 是 24 字节：16 字节给最大的成员 <code>Rc&lt;str&gt;</code>，剩下 8 字节是判别标记（连同对齐填充）。第 2 篇说过 <code>enum</code> 在 Rust 里是带标签的联合体，这就是账单。</p><p>接下来是 <code>impl Value</code>。每个方法都很短：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Builds a string value.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>string</span><span>(</span><span>text</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>Rc</span><span>&lt;</span><span>str</span><span>&gt;&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>text</span><span>.</span><span>into</span><span>())</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>/// Builds a list value.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>list</span><span>(</span><span>items</span><span>: </span><span>Vec</span><span>&lt;</span><span>Value</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Value</span><span>::</span><span>List</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>items</span><span>)))</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>/// The value's type name, as `type()` reports it.</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>type_name</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>Value</span><span>::</span><span>Nil</span><span> =&gt; </span><span>"nil"</span><span>,</span></div></div><div><div><div>19</div></div><div><span>            </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>_</span><span>) =&gt; </span><span>"bool"</span><span>,</span></div></div><div><div><div>20</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>_</span><span>) =&gt; </span><span>"int"</span><span>,</span></div></div><div><div><div>21</div></div><div><span>            </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>_</span><span>) =&gt; </span><span>"float"</span><span>,</span></div></div><div><div><div>22</div></div><div><span>            </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>_</span><span>) =&gt; </span><span>"string"</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>_</span><span>) =&gt; </span><span>"list"</span><span>,</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>    </span><span>/// Truthiness: only `nil` and `false` are falsy.</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>29</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>is_truthy</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>!</span><span>matches!</span><span>(</span><span>self</span><span>, </span><span>Value</span><span>::</span><span>Nil</span><span><span> </span><span>|</span><span> </span></span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>false</span><span>))</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>/// The language `==` operator.</span></div></div><div><div><div>34</div></div><div><span>    </span><span>///</span></div></div><div><div><div>35</div></div><div><span>    </span><span>/// Numbers compare across `int`/`float`; strings compare by contents;</span></div></div><div><div><div>36</div></div><div><span>    </span><span>/// everything else (lists, maps, functions) compares by identity, so two</span></div></div><div><div><div>37</div></div><div><span>    </span><span>/// equal-looking lists are *not* `==` unless they are the same list.</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>39</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>equals</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Value</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>40</div></div><div><span>        </span><span>match</span><span> (</span><span>self</span><span>, </span><span>other</span><span>) {</span></div></div><div><div><div>41</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Nil</span><span>, </span><span>Value</span><span>::</span><span>Nil</span><span>) =&gt; </span><span>true</span><span>,</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>,</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>,</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>b</span><span><span>)) </span><span>|</span><span> (</span></span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>b</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>)) =&gt; {</span></div></div><div><div><div>45</div></div><div><span><span>                </span></span><span>(*</span><span>a</span><span> </span><span>as</span><span> </span><span>f64</span><span><span>) </span><span>==</span><span> *</span></span><span>b</span></div></div><div><div><div>46</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>,</span></div></div><div><div><div>48</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>,</span></div></div><div><div><div>49</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>List</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Rc</span><span>::</span><span>ptr_eq</span><span>(</span><span>a</span><span>, </span><span>b</span><span>),</span></div></div><div><div><div>50</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>false</span><span>,</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>53</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐个看：</p><ul>
<li><code>string(text: impl Into&lt;Rc&lt;str&gt;&gt;)</code> 和第 2 篇的 <code>SourceMap::add</code> 是同一个写法：参数可以是任何能转成 <code>Rc&lt;str&gt;</code> 的东西，<code>&amp;str</code>、<code>String</code>、已经有的 <code>Rc&lt;str&gt;</code> 都行。</li>
<li><code>type_name</code> 和 <code>is_truthy</code> 是 <code>const fn</code>，因为它们只是读判别标记。<code>matches!</code> 是第 7 篇见过的宏，<code>!matches!(self, Value::Nil | Value::Bool(false))</code> 读作”既不是 nil，也不是 false”。</li>
<li><code>equals</code> 是 Pebble 语言里的 <code>==</code>。它的 <code>(Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a))</code> 是一个或模式：两个分支绑定相同名字、相同类型，所以右边的表达式可以共用。<code>*a as f64</code> 的 <code>*</code> 是因为 <code>a</code> 是 <code>&amp;i64</code>（模式匹配 <code>&amp;self</code> 时按引用绑定），<code>as</code> 是第 1 篇的显式转换。</li>
<li>列表用 <code>Rc::ptr_eq</code> 比较，也就是比地址。如果这里写成 <code>a == b</code>，<code>Rc&lt;T&gt;</code> 的 <code>==</code> 会去比较 <code>T</code> 的<strong>内容</strong>，那样两个内容相同的列表就相等了。Pebble 的语义是”同一个列表才相等”，所以必须显式要求身份比较。字符串那行 <code>a == b</code> 比的是内容，这正是我们要的。</li>
</ul><p>标准库的 <code>==</code> 是 <code>PartialEq</code> trait。我们让它转发给 <code>equals</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>PartialEq</span><span> </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>eq</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.</span><span>equals</span><span>(</span><span>other</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为什么不 <code>#[derive(PartialEq)]</code>？因为派生会逐字段递归，列表就按内容比较了，和 <code>equals</code> 的语义对不上。为什么只是 <code>PartialEq</code> 而不是 <code>Eq</code>？因为 <code>Value::Float(f64::NAN) == Value::Float(f64::NAN)</code> 是 <code>false</code>，<code>Eq</code> 要求自反（<code>a == a</code> 永远成立），NaN 不满足。这个区别马上会在 Map 的键上咬我们。</p><p>再加几个 <code>From</code>，让 <code>1.into()</code> 和 <code>"x".into()</code> 都能变成 <code>Value</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;</span><span>i64</span><span>&gt; </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>value</span><span>: </span><span>i64</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>value</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;</span><span>f64</span><span>&gt; </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>value</span><span>: </span><span>f64</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>value</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;</span><span>bool</span><span>&gt; </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>value</span><span>: </span><span>bool</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>value</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;&amp;</span><span>str</span><span>&gt; </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>value</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>Value</span><span>::</span><span>Str</span><span><span>(</span><span>Rc</span><span>::</span></span><span>from</span><span>(</span><span>value</span><span>))</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;</span><span>String</span><span>&gt; </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>26</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>value</span><span>: </span><span>String</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>27</div></div><div><span>        </span><span>Value</span><span>::</span><span>Str</span><span><span>(</span><span>Rc</span><span>::</span></span><span>from</span><span>(</span><span>value</span><span>.</span><span>as_str</span><span>()))</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>From&lt;String&gt;</code> 里 <code>Rc::from(value.as_str())</code> 会把字符复制一遍：<code>Rc&lt;str&gt;</code> 的引用计数和字符数据必须在同一块内存里，<code>String</code> 的缓冲区没法原地改造。这是 O(n)，但每个字符串字面量只付一次。</p><p>最后是打印。Pebble 里 <code>print([1, "a"])</code> 应该显示 <code>[1, "a"]</code>：顶层字符串不带引号，嵌套在列表里的带引号。所以一个递归函数需要知道自己是不是在”嵌套里”，还要防止自己包含自己的列表把栈打爆：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Renders a value, guarding against cycles in self-referential lists.</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>write_value</span><span>(</span></div></div><div><div><div>3</div></div><div><span>    </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>value</span><span>: &amp;</span><span>Value</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>depth</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>quoted</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>const</span><span> </span><span>MAX_DEPTH</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>4</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>match</span><span> </span><span>value</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Value</span><span>::</span><span>Nil</span><span> =&gt; </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"nil"</span><span>),</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>inner</span><span>) =&gt; </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{inner}"</span><span>),</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>inner</span><span>) =&gt; </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{inner}"</span><span>),</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>inner</span><span>) =&gt; {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>if</span><span> </span><span>inner</span><span>.</span><span>is_finite</span><span><span>() </span><span>&amp;&amp;</span><span> </span></span><span>inner</span><span>.</span><span>fract</span><span><span>() </span><span>==</span><span> </span></span><span>0.0</span><span> {</span></div></div><div><div><div>15</div></div><div><span>                </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{inner:.1}"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>17</div></div><div><span>                </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{inner}"</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>inner</span><span>) =&gt; {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>if</span><span> </span><span>quoted</span><span> {</span></div></div><div><div><div>22</div></div><div><span>                </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{inner:?}"</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>24</div></div><div><span>                </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>inner</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>        </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>items</span><span>) =&gt; {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>if</span><span> </span><span>depth</span><span><span> </span><span>&gt;=</span><span> </span></span><span>MAX_DEPTH</span><span> {</span></div></div><div><div><div>29</div></div><div><span>                </span><span>return</span><span> </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"[...]"</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>            </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"["</span><span>)?;</span></div></div><div><div><div>32</div></div><div><span>            </span><span>for</span><span> (</span><span>index</span><span>, </span><span>item</span><span>) </span><span>in</span><span> </span><span>items</span><span>.</span><span>borrow</span><span>().</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>33</div></div><div><span>                </span><span>if</span><span> </span><span>index</span><span><span> </span><span>&gt;</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>34</div></div><div><span>                    </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>", "</span><span>)?;</span></div></div><div><div><div>35</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>                </span><span>write_value</span><span>(</span><span>f</span><span>, </span><span>item</span><span>, </span><span>depth</span><span> + </span><span>1</span><span>, </span><span>true</span><span>)?;</span></div></div><div><div><div>37</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>            </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"]"</span><span>)</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>write_value</span><span>(</span><span>f</span><span>, </span><span>self</span><span>, </span><span>0</span><span>, </span><span>false</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>write_value</span><span>(</span><span>f</span><span>, </span><span>self</span><span>, </span><span>0</span><span>, </span><span>true</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>write_value</code> 里有几个格式化小技巧，第 3 篇的格式语法都在：<code>{inner:?}</code> 是 <code>Debug</code> 格式，对 <code>Rc&lt;str&gt;</code> 会带引号并转义；<code>{inner:.1}</code> 是一位小数，所以整数值的浮点数会显示成 <code>2.0</code> 而不是 <code>2</code>。<code>Display</code> 和 <code>Debug</code> 共用一个函数，只有 <code>quoted</code> 不同，这样 <code>print</code> 和 <code>{:?}</code> 的行为不会走样。</p><p>注意 <code>items.borrow()</code> 在整个 <code>for</code> 循环期间都持有守卫。这没问题，因为递归里全是只读的 <code>borrow()</code>，多个读者可以共存。但它引出一个问题：如果列表包含它自己呢？我们先来写那四行的 Rust 版本，再顺便试一下。</p><p>在 <code>lib.rs</code> 末尾加一个<strong>临时</strong>测试（跑完就删掉，后面不再出现）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> scratch {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>clones_share_one_list</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span>[]);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span>.</span><span>clone</span><span>();</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>items</span><span><span>) </span><span>=</span><span> &amp;</span></span><span>b</span><span> {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>items</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span><span>(</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>println!</span><span>(</span><span>"a = {a}"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>println!</span><span>(</span><span>"b = {b}"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>println!</span><span>(</span><span>"a == b: {}"</span><span>, </span><span>a</span><span>.</span><span>equals</span><span>(&amp;</span><span>b</span><span>));</span></div></div><div><div><div>15</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>items</span><span><span>) </span><span>=</span><span> &amp;</span></span><span>a</span><span> {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>items</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span>(</span><span>a</span><span>.</span><span>clone</span><span>());</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>        </span><span>println!</span><span>(</span><span>"cyclic = {a}"</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value clones_share -- --nocapture</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_value-8a12146cf753ccc2)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 1 test</span></div></div><div><div><div>6</div></div><div><span>a = [1]</span></div></div><div><div><div>7</div></div><div><span>b = [1]</span></div></div><div><div><div>8</div></div><div><span>a == b: true</span></div></div><div><div><div>9</div></div><div><span>cyclic = [1, [1, [1, [1, [...]]]]]</span></div></div><div><div><div>10</div></div><div><span>test scratch::clones_share_one_list ... ok</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>a</code> 和 <code>b</code> 是同一个列表，<code>b</code> 里 push 的 1，在 <code>a</code> 里看得见，开头那个四行程序的核心行为我们已经有了。<code>a == b</code> 是 <code>true</code>，因为它们是同一个 <code>Rc</code>。最后一行 <code>cyclic</code> 是把 <code>a</code> 放进它自己：列表里套着列表，一路套下去，<code>write_value</code> 在深度 4 处停下，打印 <code>[...]</code>。如果没有 <code>MAX_DEPTH</code>，这就是一个栈溢出。</p><div><div><div></div><div>这里藏着一个内存泄漏</div></div><div><p><code>a</code> 里放了一个指向 <code>a</code> 自己的 <code>Rc</code>，引用计数永远不会归零。这个列表会一直活到进程结束。Rc 的引用计数没有办法回收环，这是它和垃圾回收器（第 11 篇）最本质的区别。这个问题会在第 9 篇再次出现，而且那次不是故意的。</p></div></div><p>删掉临时测试，换上第一轮真正的测试，它是前面那个断言的正式版：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 在 lib.rs 末尾</span></div></div><div><div><div>2</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>3</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>list_identity_not_structure_for_equality</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)]);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)]);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>assert!</span><span><span>(</span><span>!</span></span><span>a</span><span>.</span><span>equals</span><span>(&amp;</span><span>b</span><span>));</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert!</span><span>(</span><span>a</span><span>.</span><span>equals</span><span>(&amp;</span><span>a</span><span>.</span><span>clone</span><span>()));</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.02s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_value-8a12146cf753ccc2)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 1 test</span></div></div><div><div><div>6</div></div><div><span>test tests::list_identity_not_structure_for_equality ... ok</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>Doc-tests pebble_value</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>running 0 tests</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个测试通过。两个不同列表即使内容相同也不 <code>equals</code>，同一个列表的克隆 <code>equals</code>，这是 <code>Rc::ptr_eq</code> 的行为。</p></section>
<section><h2>Map，和 Hash/Eq 的契约<a href="#map和-hasheq-的契约"><span>#</span></a></h2><p>Pebble 有映射 <code>#{"a": 1}</code>，键可以是任何值。Rust 里对应的类型是 <code>HashMap&lt;K, V&gt;</code>，要求 <code>K: Eq + Hash</code>。C++ 的 <code>unordered_map</code> 同样需要 <code>operator==</code> 和 <code>std::hash</code>，只是 C++ 在你忘了的时候给你一屏模板错误，而且契约本身（相等的值必须有相同的哈希）从来不被检查。</p><p>最省事的做法是在 <code>Value</code> 上 <code>#[derive(Hash, Eq)]</code>。我们先试一下，一个更小的例子（<code>~/rust-lab/src/bin/ch08_derive_hash.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Hash</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>Key</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Float</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>Key</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>Key</span><span>::</span><span>Float</span><span>(</span><span>1.0</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch08_derive_hash</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0277]: the trait bound `f64: Hash` is not satisfied</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch08_derive_hash.rs:4:11</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>1 | #[derive(Hash, PartialEq, Eq)]</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|          ---- in this derive macro expansion</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div><div><div><div>9</div></div><div><span>4 |     Float(f64),</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|           ^^^ the trait `Hash` is not implemented for `f64`</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>= help: the following other types implement trait `Hash`:</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>i128</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>i16</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>i32</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>i64</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>i8</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>isize</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>u128</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>u16</span></div></div><div><div><div>21</div></div><div><span><span>          </span></span><span>and 4 others</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>error[E0277]: the trait bound `f64: Eq` is not satisfied</span></div></div><div><div><div>24</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch08_derive_hash.rs:4:11</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>26</div></div><div><span>1 | #[derive(Hash, PartialEq, Eq)]</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>|                           -- in this derive macro expansion</span></div></div><div><div><div>28</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>f64</code> 既不是 <code>Hash</code> 也不是 <code>Eq</code>。这不是标准库偷懒，是浮点数真的不满足契约：<code>NaN != NaN</code>，<code>Eq</code> 要求自反；<code>0.0 == -0.0</code> 但位模式不同，哈希要么违反”相等则哈希相同”，要么违反”位模式不同则哈希不同”，二选一。</p><p>我们的解法有两步。</p><p><strong>第一步：用一个新类型包住 <code>Value</code>。</strong> 为什么不直接给 <code>Value</code> 实现 <code>Eq</code> 和 <code>Hash</code>？因为 <code>Value</code> 已经是 <code>PartialEq</code> 了，它的 <code>==</code> 是语言里的 <code>==</code>（<code>NaN != NaN</code>）。键需要的是另一种相等：自反的、<code>NaN</code> 等于 <code>NaN</code> 的。一个类型不能有两种 <code>==</code>，所以做两个类型。这个模式叫 newtype（第 1 篇的元组结构体 <code>SourceId</code> 同款）。</p><p><strong>第二步：两个私有方法定义”键相等”和”键哈希”，让它们互相一致。</strong> 先加 Map 变体（放在 <code>List</code> 后面）和构造函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// A mutable, shared map.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Map</span><span>(</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>HashMap</span><span>&lt;</span><span>MapKey</span><span>, </span><span>Value</span><span>&gt;&gt;&gt;),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Builds an empty map value.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>map</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Value</span><span>::</span><span>Map</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>HashMap</span><span>::</span></span><span>new</span><span>())))</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>type_name</code> 加一行 <code>Value::Map(_) =&gt; "map",</code>，<code>equals</code> 里加一行 <code>(Value::Map(a), Value::Map(b)) =&gt; Rc::ptr_eq(a, b),</code>，<code>write_value</code> 在 <code>List</code> 分支后加 <code>Map</code> 分支：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>Value</span><span>::</span><span>Map</span><span>(</span><span>entries</span><span>) =&gt; {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>if</span><span> </span><span>depth</span><span><span> </span><span>&gt;=</span><span> </span></span><span>MAX_DEPTH</span><span> {</span></div></div><div><div><div>3</div></div><div><span>                </span><span>return</span><span> </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"#{...}"</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>            </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"#{"</span><span>)?;</span></div></div><div><div><div>6</div></div><div><span>            </span><span>for</span><span> (</span><span>index</span><span>, (</span><span>key</span><span>, </span><span>item</span><span>)) </span><span>in</span><span> </span><span>entries</span><span>.</span><span>borrow</span><span>().</span><span>iter</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>if</span><span> </span><span>index</span><span><span> </span><span>&gt;</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>", "</span><span>)?;</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>                </span><span>write_value</span><span>(</span><span>f</span><span>, &amp;</span><span>key</span><span>.</span><span>0</span><span>, </span><span>depth</span><span> + </span><span>1</span><span>, </span><span>true</span><span>)?;</span></div></div><div><div><div>11</div></div><div><span>                </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>": "</span><span>)?;</span></div></div><div><div><div>12</div></div><div><span>                </span><span>write_value</span><span>(</span><span>f</span><span>, </span><span>item</span><span>, </span><span>depth</span><span> + </span><span>1</span><span>, </span><span>true</span><span>)?;</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>            </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"}"</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>key_eq</code> 和 <code>key_hash</code> 放在 <code>impl Value</code> 里，紧接在 <code>equals</code> 的后面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Equality used for map keys.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Differs from [`Value::equals`] in one place: floats compare by bits, so</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// `NaN` is a usable, reflexive key. That is what `Eq` requires and what a</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// `HashMap` needs.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>key_eq</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Value</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>match</span><span> (</span><span>self</span><span>, </span><span>other</span><span>) {</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span>.</span><span>to_bits</span><span><span>() </span><span>==</span><span> </span></span><span>b</span><span>.</span><span>to_bits</span><span>(),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>self</span><span>.</span><span>equals</span><span>(</span><span>other</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Hashing consistent with [`Value::key_eq`].</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>key_hash</span><span>&lt;</span><span>H</span><span>: </span><span>Hasher</span><span>&gt;(&amp;</span><span>self</span><span>, </span><span>state</span><span>: &amp;</span><span>mut</span><span> </span><span>H</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Value</span><span>::</span><span>Nil</span><span> =&gt; </span><span>0</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Value</span><span>::</span><span>Bool</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>1</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>7</div></div><div><span>                </span><span>value</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>2</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                </span><span>value</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>14</div></div><div><span>                </span><span>3</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>15</div></div><div><span>                </span><span>value</span><span>.</span><span>to_bits</span><span>().</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>            </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>18</div></div><div><span>                </span><span>4</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>19</div></div><div><span>                </span><span>value</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>            </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>22</div></div><div><span>                </span><span>5</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>23</div></div><div><span>                </span><span>Rc</span><span>::</span><span>as_ptr</span><span>(</span><span>value</span><span>).</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>            </span><span>Value</span><span>::</span><span>Map</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>26</div></div><div><span>                </span><span>6</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>27</div></div><div><span>                </span><span>Rc</span><span>::</span><span>as_ptr</span><span>(</span><span>value</span><span>).</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个值得停下来看的地方（有一处例外这里先不说，练习 2 会让你找到它）：</p><ul>
<li>浮点数按位比较、按位哈希：<code>a.to_bits() == b.to_bits()</code>。<code>NaN</code> 的位模式是确定的，所以 <code>NaN</code> 现在是个自反的键。<code>0.0</code> 和 <code>-0.0</code> 位模式不同，所以是两个不同的键，这和 <code>==</code> 不一致，但对键而言是可以接受的。</li>
<li>每个变体先哈希一个小整数（<code>0u8</code>、<code>1u8</code>……）作为判别标记，再哈希内容。不加的话，<code>Int(1)</code> 和 <code>Bool(true)</code> 可能哈希相同。冲突不违反契约，但会让桶变得拥挤。</li>
<li>列表、Map 用 <code>Rc::as_ptr(value).hash(state)</code>：哈希<strong>地址</strong>，不是内容。原因和 <code>equals</code> 一致：键相等是”同一个对象”，哈希必须和它对齐。如果哈希内容，那么往作为键的列表里 <code>push</code> 一个元素，它的哈希就变了，<code>HashMap</code> 再也找不到它。这不是推理，我写了一个实验验证（<code>~/rust-lab/src/bin/ch08_mutable_key.rs</code>）：</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>cell</span><span>::</span></span><span>RefCell</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>collections</span><span>::</span></span><span>HashMap</span><span>;</span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>hash</span><span>::{</span></span><span>Hash</span><span>, </span><span>Hasher</span><span>};</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>struct</span><span> </span><span>ByContents</span><span>(</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>i64</span><span>&gt;&gt;&gt;);</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>PartialEq</span><span> </span><span>for</span><span> </span><span>ByContents</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>eq</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>*</span><span>self</span><span>.</span><span>0.</span><span>borrow</span><span><span>() </span><span>==</span><span> *</span></span><span>other</span><span>.</span><span>0.</span><span>borrow</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>impl</span><span> </span><span>Eq</span><span> </span><span>for</span><span> </span><span>ByContents</span><span> {}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>impl</span><span> </span><span>Hash</span><span> </span><span>for</span><span> </span><span>ByContents</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>hash</span><span>&lt;</span><span>H</span><span>: </span><span>Hasher</span><span>&gt;(&amp;</span><span>self</span><span>, </span><span>state</span><span>: &amp;</span><span>mut</span><span> </span><span>H</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>self</span><span>.</span><span>0.</span><span>borrow</span><span>().</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>list</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>]));</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>HashMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>25</div></div><div><span>    </span><span>map</span><span>.</span><span>insert</span><span>(</span><span>ByContents</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>list</span><span>)), </span><span>"one"</span><span>);</span></div></div><div><div><div>26</div></div><div><span>    </span><span>println!</span><span>(</span><span>"before push: {:?}"</span><span>, </span><span>map</span><span>.</span><span>get</span><span>(&amp;</span><span>ByContents</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>list</span><span>))));</span></div></div><div><div><div>27</div></div><div><span>    </span><span>list</span><span>.</span><span>borrow_mut</span><span>().</span><span>push</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>println!</span><span>(</span><span>"after push:  {:?}"</span><span>, </span><span>map</span><span>.</span><span>get</span><span>(&amp;</span><span>ByContents</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>list</span><span>))));</span></div></div><div><div><div>29</div></div><div><span>    </span><span>println!</span><span>(</span><span>"entries: {}"</span><span>, </span><span>map</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_mutable_key</span></div></div><div><div><div>2</div></div><div><span>before push: Some("one")</span></div></div><div><div><div>3</div></div><div><span>after push:  None</span></div></div><div><div><div>4</div></div><div><span>entries: 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>push</code> 之后同一个列表对象查不到自己了，而且那个条目留在表里无法删除，<code>len</code> 还是 1。这是”可变键”的经典事故。C++ 里 <code>unordered_set&lt;vector&lt;int&gt;*&gt;</code> 之类的写法不会出这个事，因为你哈希的是指针本身。我们的 <code>Rc::as_ptr</code> 做的就是同样的事。</p><p>然后是包装类型本身：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A map key: a [`Value`] with a reflexive `Eq`/`Hash` implementation.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>MapKey</span><span>(</span><span>pub</span><span> </span><span>Value</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>impl</span><span> </span><span>MapKey</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Borrows the underlying value.</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>8</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>value</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>Value</span><span> {</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.</span><span>0</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>impl</span><span> </span><span>PartialEq</span><span> </span><span>for</span><span> </span><span>MapKey</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>eq</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span>.</span><span>0.</span><span>key_eq</span><span>(&amp;</span><span>other</span><span>.</span><span>0</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>impl</span><span> </span><span>Eq</span><span> </span><span>for</span><span> </span><span>MapKey</span><span> {}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>impl</span><span> </span><span>Hash</span><span> </span><span>for</span><span> </span><span>MapKey</span><span> {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>fn</span><span> </span><span>hash</span><span>&lt;</span><span>H</span><span>: </span><span>Hasher</span><span>&gt;(&amp;</span><span>self</span><span>, </span><span>state</span><span>: &amp;</span><span>mut</span><span> </span><span>H</span><span>) {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>self</span><span>.</span><span>0.</span><span>key_hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>impl</span><span> </span><span>From</span><span>&lt;</span><span>Value</span><span>&gt; </span><span>for</span><span> </span><span>MapKey</span><span> {</span></div></div><div><div><div>28</div></div><div><span>    </span><span>fn</span><span> </span><span>from</span><span>(</span><span>value</span><span>: </span><span>Value</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>MapKey</span><span>(</span><span>value</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pub struct MapKey(pub Value)</code> 是元组结构体。<code>impl Eq for MapKey {}</code> 是一个空的”标记 trait”实现：<code>Eq</code> 没有方法，它只是向编译器声明”我保证 <code>PartialEq</code> 是自反的”。这是个承诺而不是代码，编译器无法验证它，但因为 <code>key_eq</code> 里浮点数按位比较，我们能自己验证。</p><p>如果漏掉这一行，错误来得很直接：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-value v0.1.0 (/tmp/pebble-rebuild-08/crates/pebble-value)</span></div></div><div><div><div>3</div></div><div><span>error[E0277]: the trait bound `MapKey: Eq` is not satisfied</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-value/src/lib.rs:293:13</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>293 |         map.insert(MapKey(Value::Float(f64::NAN)), Value::Int(1));</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|             ^^^^^^ unsatisfied trait bound</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>help: the trait `Eq` is not implemented for `MapKey`</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-value/src/lib.rs:248:1</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>248 | pub struct MapKey(pub Value);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>| ^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>14</div></div><div><span>note: required by a bound in `std::collections::HashMap::&lt;K, V, S, A&gt;::insert`</span></div></div><div><div><div>15</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>测试放在 <code>list_identity_not_structure_for_equality</code> 后面（第一轮写的那个）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>map_keys_survive_nan_and_lists</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>HashMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>map</span><span>.</span><span>insert</span><span>(</span><span>MapKey</span><span><span>(</span><span>Value</span><span>::</span></span><span>Float</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span><span>)), </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>5</div></div><div><span>        </span><span>map</span><span>.</span><span>insert</span><span>(</span><span>MapKey</span><span><span>(</span><span>Value</span><span>::</span></span><span>list</span><span>(</span><span>vec!</span><span><span>[])), </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>));</span></div></div><div><div><div>6</div></div><div><span>        </span><span>// A fresh NaN key must find the stored entry thanks to bitwise hashing.</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>8</div></div><div><span>            </span><span>map</span><span>.</span><span>get</span><span>(&amp;</span><span>MapKey</span><span><span>(</span><span>Value</span><span>::</span></span><span>Float</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span>))),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Some</span><span>(&amp;</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>))</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>map</span><span>.</span><span>len</span><span>(), </span><span>2</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个测试一口气验证三件事：<code>NaN</code> 作键能找回自己，空列表作键没问题，两个键共存。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_value-8a12146cf753ccc2)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 2 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::list_identity_not_structure_for_equality ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::map_keys_survive_nan_and_lists ... ok</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>Doc-tests pebble_value</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>running 0 tests</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>clippy 的一次抗议<a href="#clippy-的一次抗议"><span>#</span></a></h3><p>仓库的 CI 对所有 target 跑 <code>cargo clippy -- -D warnings</code>，我们的 workspace 把 <code>clippy::all</code> 设成 warn。在这个阶段跑一下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo clippy -p pebble-value --all-targets -- -D warnings</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Checking pebble-value v0.1.0 (/tmp/pebble-rebuild-08/crates/pebble-value)</span></div></div><div><div><div>3</div></div><div><span>error: mutable key type</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-value/src/lib.rs:293:9</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>293 |         let mut map = HashMap::new();</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>= note: ... because it contains `MapKey`, which has interior mutability</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>= note: ... because it contains `Value`, which has interior mutability</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>= note: ... because it contains `Rc&lt;RefCell&lt;Vec&lt;Value&gt;&gt;&gt;`, which has interior mutability</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>= note: ... because it contains `RefCell&lt;Vec&lt;Value&gt;&gt;`, which has interior mutability</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>= note: ... because it contains `Cell&lt;isize&gt;`, which has interior mutability</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>= note: ... because it contains `UnsafeCell&lt;isize&gt;`, which has interior mutability</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#mutable_key_type</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>= note: `-D clippy::mutable-key-type` implied by `-D warnings`</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>= help: to override `-D warnings` add `#[allow(clippy::mutable_key_type)]`</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>error: could not compile `pebble-value` (lib test) due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>clippy 的 <code>mutable_key_type</code> 检查认为：键的类型里有内部可变性（那一串 <code>because it contains</code> 一路追到了 <code>RefCell</code> 里的计数器 <code>Cell&lt;isize&gt;</code>），所以键的哈希可能会在表里悄悄变化。这个规则在一般情况下完全正确，上面的实验就是活生生的例子。但我们的 <code>MapKey</code> 偏偏做对了：可变的东西哈希地址，其余的变体本身不可变。所以这条 lint 在这里是误报，应该关掉，并且写清楚为什么。</p><p>我在 tag 的 worktree 里把这行 <code>allow</code> 删掉试过：<code>cargo clippy -p pebble-value</code> 对库代码没有意见，只有 <code>--all-targets</code> 时测试里的 <code>HashMap::new()</code> 触发它。所以更窄的做法是把 <code>#[allow]</code> 只放在那个测试函数上。tag 选择的是整个 crate 一次关掉，我们照抄，好让文件逐字节一致。无论哪种，规则是：<code>#[allow]</code> 必须带一句”为什么”。tag 里的这段在文件最顶部，等一下补文件头时会一起写进去：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Maps are keyed by `MapKey`, which wraps a `Value`. Clippy warns about</span></div></div><div><div><div>2</div></div><div><span>// interior-mutable key types, but our hashing is stable by construction:</span></div></div><div><div><div>3</div></div><div><span>// mutable values (lists, maps, closures) hash by pointer identity, and the</span></div></div><div><div><div>4</div></div><div><span>// scalar variants are immutable. See `Value::key_hash`.</span></div></div><div><div><div>5</div></div><div><span>#![allow(clippy</span><span>::</span><span>mutable_key_type)]</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>运算符：add、numeric 和 compare<a href="#运算符addnumeric-和-compare"><span>#</span></a></h2><p>Pebble 的 <code>+</code> 有三种含义：数字相加、字符串拼接、列表拼接。其他运算符只处理数字。我们先写 <code>add</code>，因为它包含所有套路：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// `lhs + rhs` with number/string/list overloading.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>add</span><span>(</span><span>lhs</span><span>: &amp;</span><span>Value</span><span>, </span><span>rhs</span><span>: &amp;</span><span>Value</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>match</span><span> (</span><span>lhs</span><span>, </span><span>rhs</span><span>) {</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>checked_add</span><span>(*</span><span>b</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span><span>(</span><span>Value</span><span>::</span></span><span>Int</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> </span></span><span>overflow</span><span>(</span><span>span</span><span>)),</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(*</span><span>a</span><span> </span><span>as</span><span> </span><span>f64</span><span> + </span><span>b</span><span>)),</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>a</span><span> + *</span><span>b</span><span> </span><span>as</span><span> </span><span>f64</span><span>)),</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>a</span><span> + </span><span>b</span><span>)),</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>b</span><span>)) =&gt; {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>text</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>with_capacity</span><span>(</span><span>a</span><span>.</span><span>len</span><span>() + </span><span>b</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>13</div></div><div><span>            </span><span>text</span><span>.</span><span>push_str</span><span>(</span><span>a</span><span>);</span></div></div><div><div><div>14</div></div><div><span>            </span><span>text</span><span>.</span><span>push_str</span><span>(</span><span>b</span><span>);</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>string</span><span>(</span><span>text</span><span>))</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>List</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>List</span><span>(</span><span>b</span><span>)) =&gt; {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>items</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span>.</span><span>borrow</span><span>().</span><span>clone</span><span>();</span></div></div><div><div><div>19</div></div><div><span>            </span><span>items</span><span>.</span><span>extend</span><span>(</span><span>b</span><span>.</span><span>borrow</span><span>().</span><span>iter</span><span>().</span><span>cloned</span><span>());</span></div></div><div><div><div>20</div></div><div><span>            </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>list</span><span>(</span><span>items</span><span>))</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>Err</span><span>(</span><span>TypeError</span><span>::</span><span>binary</span><span>(</span><span>"+"</span><span>, </span><span>lhs</span><span>, </span><span>rhs</span><span>).</span><span>into_error</span><span>(</span><span>span</span><span>)),</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读这个函数你会碰到几个 Rust 惯用写法：</p><ul>
<li><code>match (lhs, rhs)</code>：把两个值打包成元组再匹配，C++ 里要写两层 <code>switch</code> 或者 <code>std::visit</code> 配 <code>overloaded</code>。</li>
<li><code>a.checked_add(*b)</code> 返回 <code>Option&lt;i64&gt;</code>：溢出时是 <code>None</code>。第 1 篇说过 debug 构建里溢出会 panic、release 里回绕，两种都不是我们想要的，<code>checked_*</code> 让溢出成为普通的错误值。<code>.map(Value::Int)</code> 里 <code>Value::Int</code> 本身就是一个函数 <code>fn(i64) -&gt; Value</code>，可以直接传给 <code>map</code>。<code>.ok_or_else(|| overflow(span))</code> 把 <code>None</code> 变成 <code>Err</code>，闭包只在需要时才执行。</li>
<li>字符串拼接先 <code>String::with_capacity(a.len() + b.len())</code>，一次分配，然后 <code>push_str</code>；<code>a</code> 是 <code>&amp;Rc&lt;str&gt;</code>，<code>push_str</code> 期望 <code>&amp;str</code>，编译器自动解引用（<code>Rc&lt;str&gt;</code> → <code>str</code>）。</li>
<li>列表拼接 <code>a.borrow().clone()</code>：<code>borrow()</code> 返回只读守卫，守卫在这条语句结束时释放；<code>clone()</code> 克隆的是 <code>Vec&lt;Value&gt;</code>，每个元素只是 <code>Rc</code> 计数加一。</li>
</ul><p>下面是它依赖的几个辅助项。先是 <code>overflow</code>、<code>is_number</code>、<code>as_f64</code>，三个小函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>is_number</span><span>(</span><span>value</span><span>: &amp;</span><span>Value</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>matches!</span><span>(</span><span>value</span><span>, </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>_</span><span><span>) </span><span>|</span><span> </span></span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>_</span><span>))</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>as_f64</span><span>(</span><span>value</span><span>: &amp;</span><span>Value</span><span>) -&gt; </span><span>f64</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>match</span><span> </span><span>value</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>inner</span><span>) =&gt; *</span><span>inner</span><span> </span><span>as</span><span> </span><span>f64</span><span>,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>inner</span><span>) =&gt; *</span><span>inner</span><span>,</span></div></div><div><div><div>9</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>f64</span><span>::</span><span>NAN</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>overflow</span><span>(</span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>RuntimeError</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span><span>span</span><span>, </span><span>"integer overflow"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再是错误类型。<code>add</code> 的最后一行 <code>TypeError::binary("+", lhs, rhs).into_error(span)</code> 用到了一个小结构体，它存下两个操作数的类型名，再一次性生成带注释的错误：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Describes a type mismatch, with a note suggesting a conversion.</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>TypeError</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>op</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>lhs</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>rhs</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>TypeError</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>binary</span><span>(</span><span>op</span><span>: &amp;</span><span>str</span><span>, </span><span>lhs</span><span>: &amp;</span><span>Value</span><span>, </span><span>rhs</span><span>: &amp;</span><span>Value</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>op</span><span>: </span><span>op</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>12</div></div><div><span>            </span><span>lhs</span><span>: </span><span>lhs</span><span>.</span><span>type_name</span><span>().</span><span>to_string</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>rhs</span><span>: </span><span>rhs</span><span>.</span><span>type_name</span><span>().</span><span>to_string</span><span>(),</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>into_error</span><span>(</span><span>self</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>RuntimeError</span><span> {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>19</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>20</div></div><div><span>            </span><span>format!</span><span>(</span></div></div><div><div><div>21</div></div><div><span>                </span><span>"cannot apply `{}` to {} and {}"</span><span>,</span></div></div><div><div><div>22</div></div><div><span>                </span><span>self</span><span>.op, </span><span>self</span><span>.lhs, </span><span>self</span><span>.rhs</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>),</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>.</span><span>with_note</span><span>(</span><span>"operands of arithmetic operators must both be numbers"</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>into_error(self, ...)</code> 取走 <code>self</code>：生成错误之后 <code>TypeError</code> 就没用了，所有权转移让这件事在类型上明确。<code>.with_note(..)</code> 是第 3 篇 <code>Diagnostic</code> 的构建器风格，我们马上在 <code>RuntimeError</code> 里实现它。</p><section><h3>RuntimeError 里的 Box<a href="#runtimeerror-里的-box"><span>#</span></a></h3><p>运行时错误是一份 <code>Diagnostic</code>（第 3 篇）加一条调用栈。前者是我们已经写好的东西，后者是一个小结构体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// One frame of a runtime stack trace.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>TraceFrame</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>function</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A runtime error: a diagnostic plus the call stack that produced it.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>RuntimeError</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>diagnostic</span><span>: </span><span>Box</span><span>&lt;</span><span>Diagnostic</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>trace</span><span>: </span><span>Vec</span><span>&lt;</span><span>TraceFrame</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>RuntimeError</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// Creates an error at `span`.</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>span</span><span>: </span><span>Span</span><span>, </span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>diagnostic</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>message</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>                    </span></span><span>.</span><span>with_code</span><span>(</span><span>"E2001"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>.</span><span>with_span</span><span>(</span><span>span</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>),</span></div></div><div><div><div>18</div></div><div><span>            </span><span>trace</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>/// Attaches a note to the diagnostic.</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>24</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_note</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>note</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>diagnostic</span><span><span> </span><span>=</span><span> *</span></span><span>self</span><span>.diagnostic;</span></div></div><div><div><div>26</div></div><div><span>        </span><span>self</span><span><span>.diagnostic </span><span>=</span><span> </span></span><span>Box</span><span>::</span><span>new</span><span>(</span><span>diagnostic</span><span>.</span><span>with_note</span><span>(</span><span>note</span><span>));</span></div></div><div><div><div>27</div></div><div><span>        </span><span>self</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>    </span><span>/// Pushes a stack frame.</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>32</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_frame</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>function</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>33</div></div><div><span>        </span><span>self</span><span>.trace.</span><span>push</span><span>(</span><span>TraceFrame</span><span> {</span></div></div><div><div><div>34</div></div><div><span>            </span><span>function</span><span>: </span><span>function</span><span>.</span><span>into</span><span>(),</span></div></div><div><div><div>35</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>37</div></div><div><span>        </span><span>self</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span>/// The message text.</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>42</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>message</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.diagnostic.message</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>RuntimeError</span><span> {</span></div></div><div><div><div>48</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>49</div></div><div><span>        </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{}"</span><span>, &amp;*</span><span>self</span><span>.diagnostic)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>impl</span><span><span> </span><span>std</span><span>::</span><span>error</span><span>::</span></span><span>Error</span><span> </span><span>for</span><span> </span><span>RuntimeError</span><span> {}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>字段 <code>diagnostic: Box&lt;Diagnostic&gt;</code> 值得解释，因为它是我实际踩到的一个 clippy 报警。<code>Box&lt;T&gt;</code> 是堆上的独占指针，C++ 对应 <code>unique_ptr&lt;T&gt;</code>（第 5 篇）。为什么要把一个 <code>Diagnostic</code> 装进盒子？</p><p><code>Result&lt;Value, RuntimeError&gt;</code> 是几乎每个运算函数的返回类型。Rust 会把 <code>Result</code> 的大小定为两个变体里较大者，也就是说每一次成功的 <code>Ok(Value)</code> 也得为最大的 <code>Err</code> 腾出空间。我在 tag 的 worktree 里做了这个实验：加一个临时测试量大小，然后把 <code>Box&lt;Diagnostic&gt;</code> 改成 <code>Diagnostic</code>（只改字段类型和三处构造点，没有别的变化），再跑 clippy。</p><p>先是装盒子的真实版本，在最终代码上量出的大小。量大小的临时测试放在文件末尾，量完就删：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> scratch {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>sizes</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>println!</span><span>(</span><span>"size_of::&lt;Value&gt;() = {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>Value</span><span>&gt;());</span></div></div><div><div><div>8</div></div><div><span>        </span><span>println!</span><span>(</span><span>"size_of::&lt;RuntimeError&gt;() = {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>RuntimeError</span><span>&gt;());</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value sizes -- --nocapture</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_value-8a12146cf753ccc2)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 1 test</span></div></div><div><div><div>6</div></div><div><span>size_of::&lt;Value&gt;() = 24</span></div></div><div><div><div>7</div></div><div><span>size_of::&lt;RuntimeError&gt;() = 32</span></div></div><div><div><div>8</div></div><div><span>test scratch::sizes ... ok</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 11 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Diagnostic</code> 本身在我的 worktree 里量是 104 字节（严重程度、错误码、消息、标签、注记，一堆 <code>String</code> 和 <code>Vec</code>）；装进盒子后 <code>RuntimeError</code> 只有 32 字节：8 字节指针加 <code>Vec&lt;TraceFrame&gt;</code> 的 24 字节。去掉盒子以后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size_of::&lt;Diagnostic&gt;() = 104</span></div></div><div><div><div>2</div></div><div><span>size_of::&lt;RuntimeError&gt;() = 128</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;Result&lt;Value, RuntimeError&gt;&gt;() = 128</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Result&lt;Value, RuntimeError&gt;</code> 从 32 字节涨到 128 字节，是 4 倍，而 <code>Value</code> 本身只有 24 字节。每个返回 <code>Ok(Value::Int(3))</code> 的 <code>add</code> 都要返回一个 128 字节的 <code>Result</code>。clippy 立刻抗议了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: the `Err`-variant returned from this function is very large</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-value/src/lib.rs:329:53</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>329 | pub fn add(lhs: &amp;Value, rhs: &amp;Value, span: Span) -&gt; Result&lt;Value, RuntimeError&gt; {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the `Err`-variant is at least 128 bytes</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>= help: try reducing the size of `RuntimeError`, for example by boxing large elements or replacing it with `Box&lt;RuntimeError&gt;`</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#result_large_err</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>= note: `-D clippy::result-large-err` implied by `-D warnings`</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>= help: to override `-D warnings` add `#[allow(clippy::result_large_err)]`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个 <code>pebble-value</code> 里一共 22 处函数报这个错（<code>add</code>、<code>sub</code>、每个 <code>builtin_*</code>……），一个字段的改动，22 个警报。这就是”罕见路径付出常见路径的代价”：错误很少发生，而 Rust 没有异常，返回值得为它留位置。C++ 的 <code>std::expected&lt;Value, Error&gt;</code> 有完全相同的问题。解决办法是让错误路径多一次堆分配（出错时才付），常见路径只付 8 字节指针。这个故事的数字来自两次真实运行，两个版本我都保留了输出。</p><p><code>with_note</code> 里还有一个只有 <code>Box</code> 才有的写法：<code>let diagnostic = *self.diagnostic;</code>。对 <code>Box&lt;T&gt;</code> 的解引用是<strong>移动出来</strong>（<code>*</code> 取走盒子里的值并释放盒子），这是普通引用做不到的。然后重新 <code>Box::new</code> 包回去。</p><p><code>impl std::error::Error for RuntimeError {}</code> 又是一个空 impl。它的超 trait 要求（<code>Debug</code> + <code>Display</code>）我们都满足了（第 3 篇），所以这行本身没有方法，却让 <code>RuntimeError</code> 可以放进 <code>anyhow::Error</code>，或者用 <code>?</code> 转换。</p></section><section><h3>其余运算符<a href="#其余运算符"><span>#</span></a></h3><p><code>sub</code>、<code>mul</code>、<code>div</code>、<code>rem</code>、<code>negate</code> 都是 <code>add</code> 的变种，从 tag 复制：<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-value/src/lib.rs#L353-L404" target="_blank">crates/pebble-value/src/lib.rs 第 353 到 404 行</a>，放在 <code>add</code> 和 <code>compare</code> 之间。可以跳过细读的理由：<code>sub</code>/<code>mul</code> 就是调用 <code>numeric</code>；<code>div</code>/<code>rem</code> 比它们多一步，对整数零除数返回错误而不是 panic；<code>negate</code> 用 <code>checked_neg</code>，因为 <code>i64::MIN</code> 取负会溢出。你会在练习里用到 <code>compare</code>，所以我们写它：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// `&lt;`, `&lt;=`, `&gt;`, `&gt;=` for numbers and strings.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>compare</span><span>(</span><span>lhs</span><span>: &amp;</span><span>Value</span><span>, </span><span>rhs</span><span>: &amp;</span><span>Value</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>core</span><span>::</span><span>cmp</span><span>::</span><span>Ordering</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>match</span><span> (</span><span>lhs</span><span>, </span><span>rhs</span><span>) {</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Ok</span><span>(</span><span>a</span><span>.</span><span>cmp</span><span>(</span><span>b</span><span>)),</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Ok</span><span>(</span><span>a</span><span>.</span><span>cmp</span><span>(</span><span>b</span><span>)),</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>(</span><span>a</span><span>, </span><span>b</span><span>) </span><span>if</span><span> </span><span>is_number</span><span>(</span><span>a</span><span><span>) </span><span>&amp;&amp;</span><span> </span></span><span>is_number</span><span>(</span><span>b</span><span>) =&gt; {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>as_f64</span><span>(</span><span>a</span><span>);</span></div></div><div><div><div>8</div></div><div><span>            </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>as_f64</span><span>(</span><span>b</span><span>);</span></div></div><div><div><div>9</div></div><div><span>            </span><span>a</span><span>.</span><span>partial_cmp</span><span>(&amp;</span><span>b</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>.</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> </span><span>RuntimeError</span><span>::</span></span><span>new</span><span>(</span><span>span</span><span>, </span><span>"cannot compare with NaN"</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>13</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>14</div></div><div><span>            </span><span>format!</span><span>(</span><span>"cannot compare {} and {}"</span><span>, </span><span>lhs</span><span>.</span><span>type_name</span><span>(), </span><span>rhs</span><span>.</span><span>type_name</span><span>()),</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>)),</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>core::cmp::Ordering</code> 是 <code>Less</code>/<code>Equal</code>/<code>Greater</code> 三值枚举。<code>(a, b) if is_number(a) &amp;&amp; is_number(b)</code> 是<strong>带守卫的分支</strong>（第 4 篇）。<code>a.partial_cmp(&amp;b)</code> 对 <code>f64</code> 返回 <code>Option&lt;Ordering&gt;</code>：碰到 NaN 是 <code>None</code>，<code>.ok_or_else(..)</code> 把它变成”cannot compare with NaN”的错误。</p><p><code>numeric</code> 是所有数字运算共用的骨架，它的最后两个参数是<strong>函数指针</strong>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>numeric</span><span>(</span></div></div><div><div><div>2</div></div><div><span>    </span><span>lhs</span><span>: &amp;</span><span>Value</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>rhs</span><span>: &amp;</span><span>Value</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>op</span><span>: &amp;</span><span>str</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>int_op</span><span>: </span><span>fn</span><span>(</span><span>i64</span><span>, </span><span>i64</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>i64</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>float_op</span><span>: </span><span>fn</span><span>(</span><span>f64</span><span>, </span><span>f64</span><span>) -&gt; </span><span>f64</span><span>,</span></div></div><div><div><div>8</div></div><div><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>match</span><span> (</span><span>lhs</span><span>, </span><span>rhs</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>b</span><span>)) =&gt; {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>int_op</span><span>(*</span><span>a</span><span>, *</span><span>b</span><span>).</span><span>map</span><span><span>(</span><span>Value</span><span>::</span></span><span>Int</span><span>).</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> </span></span><span>overflow</span><span>(</span><span>span</span><span>))</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>(</span><span>a</span><span>, </span><span>b</span><span>) </span><span>if</span><span> </span><span>is_number</span><span>(</span><span>a</span><span><span>) </span><span>&amp;&amp;</span><span> </span></span><span>is_number</span><span>(</span><span>b</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>float_op</span><span>(</span><span>as_f64</span><span>(</span><span>a</span><span>), </span><span>as_f64</span><span>(</span><span>b</span><span>)))),</span></div></div><div><div><div>14</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>Err</span><span>(</span><span>TypeError</span><span>::</span><span>binary</span><span>(</span><span>op</span><span>, </span><span>lhs</span><span>, </span><span>rhs</span><span>).</span><span>into_error</span><span>(</span><span>span</span><span>)),</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>int_op: fn(i64, i64) -&gt; Option&lt;i64&gt;</code> 的 <code>fn</code> 是小写的，不是 <code>Fn</code>。它是<strong>函数指针类型</strong>，就是 C 的 <code>int (*)(int, int)</code>。<code>sub</code> 里传的 <code>i64::checked_sub</code> 是一个函数路径，<code>|a, b| a - b</code> 是一个不捕获任何变量的闭包，两者都能自动转成函数指针。这是个实验（<code>~/rust-lab/src/bin/ch08_fn_ptr.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>double</span><span>(</span><span>x</span><span>: </span><span>i64</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>x</span><span> * </span><span>2</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>table</span><span>: [(&amp;</span><span>str</span><span>, </span><span>fn</span><span>(</span><span>i64</span><span>) -&gt; </span><span>i64</span><span>); </span><span>2</span><span><span>] </span><span>=</span><span> [(</span></span><span>"double"</span><span>, </span><span>double</span><span>), (</span><span>"negate"</span><span><span>, </span><span>|</span></span><span>x</span><span><span>|</span><span> -</span></span><span>x</span><span>)];</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> (</span><span>name</span><span>, </span><span>f</span><span>) </span><span>in</span><span> </span><span>table</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{name}(21) = {}"</span><span>, </span><span>f</span><span>(</span><span>21</span><span>));</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of fn pointer: {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>fn</span><span>(</span><span>i64</span><span>) -&gt; </span><span>i64</span><span>&gt;());</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>offset</span><span><span> </span><span>=</span><span> </span></span><span>10</span><span>;</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>add_offset</span><span><span> </span><span>=</span><span> </span></span><span>move</span><span><span> </span><span>|</span></span><span>x</span><span>: </span><span>i64</span><span><span>|</span><span> </span></span><span>x</span><span> + </span><span>offset</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of capturing closure: {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of_val</span><span>(&amp;</span><span>add_offset</span><span>));</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"add_offset(1) = {}"</span><span>, </span><span>add_offset</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_fn_ptr</span></div></div><div><div><div>2</div></div><div><span>double(21) = 42</span></div></div><div><div><div>3</div></div><div><span>negate(21) = -21</span></div></div><div><div><div>4</div></div><div><span>size_of fn pointer: 8</span></div></div><div><div><div>5</div></div><div><span>size_of capturing closure: 8</span></div></div><div><div><div>6</div></div><div><span>add_offset(1) = 11</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>函数指针是 8 字节，只存地址。那个捕获了 <code>offset</code> 的闭包也是 8 字节，但原因完全不同：闭包是一个匿名结构体，里面装着它捕获的值（这里一个 <code>i64</code>），它没有”地址”这个概念，调用路径由类型决定（静态分派）。如果我们想把它当函数指针用（<code>~/rust-lab/src/bin/ch08_fn_ptr_capture.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>offset</span><span><span> </span><span>=</span><span> </span></span><span>10</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>add_offset</span><span><span> </span><span>=</span><span> </span></span><span>move</span><span><span> </span><span>|</span></span><span>x</span><span>: </span><span>i64</span><span><span>|</span><span> </span></span><span>x</span><span> + </span><span>offset</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>pointer</span><span>: </span><span>fn</span><span>(</span><span>i64</span><span>) -&gt; </span><span>i64</span><span><span> </span><span>=</span><span> </span></span><span>add_offset</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>pointer</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch08_fn_ptr_capture</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch08_fn_ptr_capture.rs:4:35</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>3 |     let add_offset = move |x: i64| x + offset;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                      ------------- the found closure</span></div></div><div><div><div>8</div></div><div><span>4 |     let pointer: fn(i64) -&gt; i64 = add_offset;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|                  --------------   ^^^^^^^^^^ expected fn pointer, found closure</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|                  |</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|                  expected due to this</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>= note: expected fn pointer `fn(i64) -&gt; i64`</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>found closure `{closure@src/bin/ch08_fn_ptr_capture.rs:3:22: 3:35}`</span></div></div><div><div><div>15</div></div><div><span>note: closures can only be coerced to `fn` types if they do not capture any variables</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch08_fn_ptr_capture.rs:3:40</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>18</div></div><div><span>3 |     let add_offset = move |x: i64| x + offset;</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>|                                        ^^^^^^ `offset` captured here</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>For more information about this error, try `rustc --explain E0308`.</span></div></div><div><div><div>22</div></div><div><span>error: could not compile `rust-lab` (bin "ch08_fn_ptr_capture") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器说得很直白：只有<strong>不捕获变量</strong>的闭包才能转成 <code>fn</code>。这就是为什么 <code>numeric</code> 的参数是 <code>fn(..)</code> 而不是泛型 <code>impl Fn(..)</code>：这里我们不需要捕获，函数指针让 <code>numeric</code> 只编译一份，调用时间接跳转，对这种一次只做一个运算的函数来说，简单就够了。第 9 篇会看到相反的情形：解释器里的 Pebble 闭包需要捕获环境，那时才轮到 <code>Fn</code>、<code>FnMut</code>、<code>FnOnce</code>。</p></section><section><h3>为什么不实现 Add 和 PartialOrd<a href="#为什么不实现-add-和-partialord"><span>#</span></a></h3><p>你一定想问：标准库有 <code>std::ops::Add</code>，为什么 <code>add</code> 是个普通函数，而不是 <code>impl Add for Value</code>？第 2 篇我们用 <code>Index</code> 重载了 <code>map[id]</code>，这里为什么不同？我把它写出来试了一遍（<code>~/rust-lab/src/bin/ch08_add_value.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>ops</span><span>::</span></span><span>Add</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>enum</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Str</span><span>(</span><span>Rc</span><span>&lt;</span><span>str</span><span>&gt;),</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>impl</span><span> </span><span>Add</span><span> </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>String</span><span>&gt;;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>fn</span><span> </span><span>add</span><span>(</span><span>self</span><span>, </span><span>rhs</span><span>: </span><span>Value</span><span>) -&gt; </span><span>Self</span><span>::</span><span>Output</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>match</span><span> (</span><span>self</span><span>, </span><span>rhs</span><span>) {</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>a</span><span> + </span><span>b</span><span>)),</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>(</span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>b</span><span>)) =&gt; </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>format!</span><span>(</span><span>"{a}{b}"</span><span>).</span><span>into</span><span>())),</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>(</span><span>a</span><span>, </span><span>b</span><span>) =&gt; </span><span>Err</span><span>(</span><span>format!</span><span>(</span><span>"cannot add {a:?} and {b:?}"</span><span>)),</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>25</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>a</span><span>.</span><span>clone</span><span>() + </span><span>b</span><span>.</span><span>clone</span><span>());</span></div></div><div><div><div>26</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>a</span><span>.</span><span>clone</span><span>() + </span><span>Value</span><span>::</span><span>Str</span><span>(</span><span>"x"</span><span>.</span><span>into</span><span>()));</span></div></div><div><div><div>27</div></div><div><span>    </span><span>let</span><span> </span><span>sum</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span> + </span><span>b</span><span>;</span></div></div><div><div><div>28</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>sum</span><span>);</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_add_value</span></div></div><div><div><div>2</div></div><div><span>Ok(Int(3))</span></div></div><div><div><div>3</div></div><div><span>Err("cannot add Int(1) and Str(\"x\")")</span></div></div><div><div><div>4</div></div><div><span>Ok(Int(3))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>能跑，<code>Output = Result&lt;Value, String&gt;</code> 也合法（<code>Add</code> 的返回类型是一个关联类型，你想返回什么都行）。但有三个问题：</p><ol>
<li>错误里没有位置。<code>Add::add(self, rhs)</code> 的签名固定，没法多传一个 <code>Span</code>。我们的诊断（第 3 篇）靠它画出插入符。</li>
<li><code>add</code> 按值取走两个操作数，<code>a + b</code> 之后 <code>a</code> 就没了。看这个最小例子的报错（<code>~/rust-lab/src/bin/ch08_add_value_moved.rs</code>）：</li>
</ol><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>ops</span><span>::</span></span><span>Add</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>struct</span><span> </span><span>Value</span><span>(</span><span>i64</span><span>);</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>impl</span><span> </span><span>Add</span><span> </span><span>for</span><span> </span><span>Value</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>add</span><span>(</span><span>self</span><span>, </span><span>rhs</span><span>: </span><span>Value</span><span>) -&gt; </span><span>Value</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Value</span><span>(</span><span>self</span><span>.</span><span>0</span><span> + </span><span>rhs</span><span>.</span><span>0</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>sum</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span> + </span><span>b</span><span>;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?} {:?}"</span><span>, </span><span>sum</span><span>, </span><span>a</span><span>);</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch08_add_value_moved</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0382]: borrow of moved value: `a`</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch08_add_value_moved.rs:18:32</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>15 |     let a = Value(1);</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|         - move occurs because `a` has type `Value`, which does not implement the `Copy` trait</span></div></div><div><div><div>8</div></div><div><span>16 |     let b = Value(2);</span></div></div><div><div><div>9</div></div><div><span>17 |     let sum = a + b;</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|               ----- `a` moved due to usage in operator</span></div></div><div><div><div>11</div></div><div><span>18 |     println!("{:?} {:?}", sum, a);</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|                                ^ value borrowed here after move</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>note: if `Value` implemented `Clone`, you could clone the value</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch08_add_value_moved.rs:4:1</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>4 | struct Value(i64);</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>| ^^^^^^^^^^^^ consider implementing `Clone` for this type</span></div></div><div><div><div>19</div></div><div><span>...</span></div></div><div><div><div>20</div></div><div><span>17 |     let sum = a + b;</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>|               - you could clone this value</span></div></div><div><div><div>22</div></div><div><span>note: calling this operator moves the left-hand side</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/ops/arith.rs:92:11</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>For more information about this error, try `rustc --explain E0382`.</span></div></div><div><div><div>26</div></div><div><span>error: could not compile `rust-lab` (bin "ch08_add_value_moved") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器甚至专门标出 <code>calling this operator moves the left-hand side</code>。<code>Value</code> 实现了 <code>Clone</code> 的话可以先 <code>clone()</code>，或者给 <code>&amp;Value</code> 实现 <code>Add</code>，但这些都是为了迁就一个签名而做的绕路。</p><ol>
<li><code>a + b</code> 掩盖了”这可能失败”。在解释器里，每个运算符都可能报运行时错误，显式的 <code>add(&amp;a, &amp;b, span)?</code> 让问号写在眼前。</li>
</ol><p>所以规则是：<strong>运算符重载适合”结果总是有意义”的类型</strong>（向量、矩阵、<code>Duration</code>、第 2 篇的 <code>Offset</code>），不适合会失败、需要额外上下文的语言运行时。</p><p><code>PartialOrd</code> 同理，而且它的问题更微妙。<code>compare</code> 为什么不是 <code>impl PartialOrd for Value</code>？</p><p><code>~/rust-lab/src/bin/ch08_partial_ord.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[derive(</span><span>PartialEq</span><span>, </span><span>PartialOrd</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>enum</span><span> </span><span>Naive</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Float</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>10</div></div><div><span>struct</span><span> </span><span>List</span><span>(</span><span>Rc</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>i64</span><span>&gt;&gt;);</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>impl</span><span> </span><span>PartialEq</span><span> </span><span>for</span><span> </span><span>List</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>fn</span><span> </span><span>eq</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Rc</span><span>::</span><span>ptr_eq</span><span>(&amp;</span><span>self</span><span>.</span><span>0</span><span>, &amp;</span><span>other</span><span>.</span><span>0</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>impl</span><span> </span><span>PartialOrd</span><span> </span><span>for</span><span> </span><span>List</span><span> {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>fn</span><span> </span><span>partial_cmp</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>std</span><span>::</span><span>cmp</span><span>::</span><span>Ordering</span><span>&gt; {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span>.</span><span>0.</span><span>partial_cmp</span><span>(&amp;</span><span>other</span><span>.</span><span>0</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Int(2) &lt; Float(0.5): {}"</span><span>, </span><span>Naive</span><span>::</span><span>Int</span><span>(</span><span>2</span><span>) &lt; </span><span>Naive</span><span>::</span><span>Float</span><span>(</span><span>0.5</span><span>));</span></div></div><div><div><div>26</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Float(NaN) &lt; Float(1.0): {}"</span><span>, </span><span>Naive</span><span>::</span><span>Float</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span>) &lt; </span><span>Naive</span><span>::</span><span>Float</span><span>(</span><span>1.0</span><span>));</span></div></div><div><div><div>27</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Float(NaN) == Float(NaN): {}"</span><span>, </span><span>Naive</span><span>::</span><span>Float</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span><span>) </span><span>==</span><span> </span></span><span>Naive</span><span>::</span><span>Float</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span>));</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>List</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>]));</span></div></div><div><div><div>30</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>List</span><span><span>(</span><span>Rc</span><span>::</span></span><span>new</span><span>(</span><span>vec!</span><span>[</span><span>1</span><span>]));</span></div></div><div><div><div>31</div></div><div><span>    </span><span>println!</span><span>(</span><span>"a == b: {}"</span><span>, </span><span>a</span><span><span> </span><span>==</span><span> </span></span><span>b</span><span>);</span></div></div><div><div><div>32</div></div><div><span>    </span><span>println!</span><span>(</span><span>"a.partial_cmp(&amp;b): {:?}"</span><span>, </span><span>a</span><span>.</span><span>partial_cmp</span><span>(&amp;</span><span>b</span><span>));</span></div></div><div><div><div>33</div></div><div><span>    </span><span>println!</span><span>(</span><span>"a &lt;= b &amp;&amp; a &gt;= b: {}"</span><span>, </span><span>a</span><span><span> </span><span>&lt;=</span><span> </span></span><span>b</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>a</span><span><span> </span><span>&gt;=</span><span> </span></span><span>b</span><span>);</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_partial_ord</span></div></div><div><div><div>2</div></div><div><span>Int(2) &lt; Float(0.5): true</span></div></div><div><div><div>3</div></div><div><span>Float(NaN) &lt; Float(1.0): false</span></div></div><div><div><div>4</div></div><div><span>Float(NaN) == Float(NaN): false</span></div></div><div><div><div>5</div></div><div><span>a == b: false</span></div></div><div><div><div>6</div></div><div><span>a.partial_cmp(&amp;b): Some(Equal)</span></div></div><div><div><div>7</div></div><div><span>a &lt;= b &amp;&amp; a &gt;= b: true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个坑一起爆出来：</p><ul>
<li><code>#[derive(PartialOrd)]</code> 对枚举先比变体顺序，再比内容。<code>Int(2) &lt; Float(0.5)</code> 是 <code>true</code>，因为 <code>Int</code> 声明在 <code>Float</code> 前面。Pebble 里 <code>2 &lt; 0.5</code> 当然是假的。</li>
<li>第二个类型模拟 <code>Value::List</code>：<code>==</code> 比较身份，<code>partial_cmp</code> 比较内容，于是两个内容相同的不同列表，<code>a == b</code> 是 <code>false</code>，<code>a.partial_cmp(&amp;b)</code> 是 <code>Some(Equal)</code>，<code>a &lt;= b &amp;&amp; a &gt;= b</code> 是 <code>true</code>。<code>PartialOrd</code> 的文档要求 <code>partial_cmp</code> 返回 <code>Some(Equal)</code> 当且仅当 <code>==</code> 成立。这个契约被打破以后，<code>sort</code>、<code>BTreeMap</code> 之类依赖它的标准库代码会得到不可预料的结果。</li>
</ul><p>我们的做法是：把比较做成一个可以失败的函数，返回 <code>Result&lt;Ordering, RuntimeError&gt;</code>，让列表、Map、函数这些”没有顺序”的类型直接报错。</p></section><section><h3>第三轮的测试<a href="#第三轮的测试"><span>#</span></a></h3><p>把 <code>span()</code> 辅助函数和下面这些测试放进 <code>tests</code> 模块，位置在 <code>list_identity_not_structure_for_equality</code> <strong>前面</strong>（<code>use super::*;</code> 的下面）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>span</span><span>() -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>Span</span><span>::</span><span>DUMMY</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>numbers_add_across_variants</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>8</div></div><div><span>            </span><span>add</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>), </span><span>span</span><span>()).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>12</div></div><div><span>            </span><span>add</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Float</span><span>(</span><span>0.5</span><span>), </span><span>span</span><span>()).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>1.5</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>integer_overflow_is_reported_not_wrapped</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>add</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span><span>(</span><span>i64</span><span>::</span></span><span>MAX</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>), </span><span>span</span><span>()).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>20</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>error</span><span>.</span><span>message</span><span>(), </span><span>"integer overflow"</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>24</div></div><div><span>    </span><span>fn</span><span> </span><span>strings_and_lists_overload_plus</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>hello</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>string</span><span>(</span><span>"he"</span><span>);</span></div></div><div><div><div>26</div></div><div><span>        </span><span>let</span><span> </span><span>llo</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>string</span><span>(</span><span>"llo"</span><span>);</span></div></div><div><div><div>27</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>add</span><span>(&amp;</span><span>hello</span><span>, &amp;</span><span>llo</span><span>, </span><span>span</span><span>()).</span><span>unwrap</span><span>().</span><span>to_string</span><span>(), </span><span>"hello"</span><span>);</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)]);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>)]);</span></div></div><div><div><div>31</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>add</span><span>(&amp;</span><span>a</span><span>, &amp;</span><span>b</span><span>, </span><span>span</span><span>()).</span><span>unwrap</span><span>().</span><span>to_string</span><span>(), </span><span>"[1, 2]"</span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>35</div></div><div><span>    </span><span>fn</span><span> </span><span>type_errors_are_clear</span><span>() {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>sub</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>string</span><span>(</span><span>"a"</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>), </span><span>span</span><span>()).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>37</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>38</div></div><div><span>            </span><span>error</span></div></div><div><div><div>39</div></div><div><span><span>                </span></span><span>.</span><span>message</span><span>()</span></div></div><div><div><div>40</div></div><div><span><span>                </span></span><span>.</span><span>contains</span><span>(</span><span>"cannot apply `-` to string and int"</span><span>)</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>45</div></div><div><span>    </span><span>fn</span><span> </span><span>division_by_zero_is_an_error</span><span>() {</span></div></div><div><div><div>46</div></div><div><span>        </span><span>assert!</span><span>(</span><span>div</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>0</span><span>), </span><span>span</span><span>()).</span><span>is_err</span><span>());</span></div></div><div><div><div>47</div></div><div><span>        </span><span>// Floating point division by zero is IEEE, not an error.</span></div></div><div><div><div>48</div></div><div><span>        </span><span>assert!</span><span>(</span><span>div</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Float</span><span>(</span><span>1.0</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>0</span><span>), </span><span>span</span><span>()).</span><span>is_ok</span><span>());</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_value-8a12146cf753ccc2)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 7 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::division_by_zero_is_an_error ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::list_identity_not_structure_for_equality ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::type_errors_are_clear ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::integer_overflow_is_reported_not_wrapped ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::numbers_add_across_variants ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::map_keys_survive_nan_and_lists ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::strings_and_lists_overload_plus ... ok</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>Doc-tests pebble_value</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>running 0 tests</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>七个测试。<code>integer_overflow_is_reported_not_wrapped</code> 是 <code>checked_add</code> 的账单：<code>i64::MAX + 1</code> 变成一条错误；<code>division_by_zero_is_an_error</code> 里整数除零是错误，浮点除零按 IEEE 给出无穷大，两者故意不同。</p></section></section>
<section><h2>Env：一串嵌套的作用域<a href="#env一串嵌套的作用域"><span>#</span></a></h2><p>变量名到值的映射在哪里？每个作用域一张表，作用域之间用链连起来：查找时先找当前表，找不到就找外层。闭包会捕获它定义时所在的作用域，所以作用域也必须被共享，同样是 <code>Rc&lt;RefCell&lt;..&gt;&gt;</code>，这次的套娃是 <code>Rc&lt;RefCell&lt;Env&gt;&gt;</code>。</p><p>先给 <code>Value</code> 加一个函数变体，因为 <code>Env</code> 之后马上要和它互相引用。把 <code>use pebble_ast::Stmt;</code> 加到文件头（放在 <code>use pebble_diag::Diagnostic;</code> 前面），然后在 <code>Map</code> 变体后加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// A user-defined function or closure.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Function</span><span>(</span><span>Rc</span><span>&lt;</span><span>Function</span><span>&gt;),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>type_name</code> 加 <code>Value::Function(_) =&gt; "function",</code>；<code>equals</code> 加 <code>(Value::Function(a), Value::Function(b)) =&gt; Rc::ptr_eq(a, b),</code>；<code>key_hash</code> 加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Value</span><span>::</span><span>Function</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>7</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>3</div></div><div><span>                </span><span>Rc</span><span>::</span><span>as_ptr</span><span>(</span><span>value</span><span>).</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>write_value</code> 加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>Value</span><span>::</span><span>Function</span><span>(</span><span>function</span><span>) =&gt; {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>write!</span><span>(</span></div></div><div><div><div>3</div></div><div><span>                </span><span>f</span><span>,</span></div></div><div><div><div>4</div></div><div><span>                </span><span>"&lt;fn {}&gt;"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>                </span><span>function</span><span>.name.</span><span>as_deref</span><span>().</span><span>unwrap_or</span><span>(</span><span>"anonymous"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Function</code> 结构体放在 <code>Env</code> 之后（文件里 <code>Environments</code> 一节之后是 <code>Functions</code> 一节）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A user-defined function or closure.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Function</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// `Some` for `fn name(..)`, `None` for an anonymous closure.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>name</span><span>: </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// Parameter names, in order.</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>params</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// The body statements.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>pub</span><span> </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// The environment the function captured when it was created.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>env</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;,</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>env: Rc&lt;RefCell&lt;Env&gt;&gt;</code> 是函数”记住自己出生地”的方式：创建 <code>Function</code> 时把当时的作用域塞进去。<code>new</code>、<code>arity</code>、<code>display_name</code> 和手写的 <code>Debug</code> 是纯样板，从 tag 复制：<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-value/src/lib.rs#L650-L688" target="_blank">第 650 到 688 行</a>，紧跟在结构体后面。为什么 <code>Debug</code> 要手写：<code>Function</code> 里有 <code>env</code>，<code>env</code> 里的表又可能放着这个函数，<code>#[derive(Debug)]</code> 会无限递归下去。手写版只打印名字、参数和函数体的长度。</p><p>现在写 <code>Env</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A lexical scope: a set of names plus a link to the enclosing scope.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Env</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>values</span><span>: </span><span>HashMap</span><span>&lt;</span><span>String</span><span>, </span><span>Value</span><span>&gt;,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>parent</span><span>: </span><span>Option</span><span>&lt;</span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;&gt;,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>Env</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// An empty global scope.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>values</span><span>: </span><span>HashMap</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>parent</span><span>: </span><span>None</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>/// A scope nested inside `parent`.</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>child</span><span>(</span><span>parent</span><span>: </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt;) -&gt; </span><span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>Env</span><span>&gt;&gt; {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span>(</span><span>Self</span><span> {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>values</span><span><span>: </span><span>HashMap</span><span>::</span></span><span>new</span><span>(),</span></div></div><div><div><div>22</div></div><div><span>            </span><span>parent</span><span>: </span><span>Some</span><span>(</span><span>parent</span><span>),</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>}))</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>    </span><span>/// Binds `name` in this scope, shadowing any outer binding.</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>define</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>value</span><span>: </span><span>Value</span><span>) {</span></div></div><div><div><div>28</div></div><div><span>        </span><span>self</span><span>.values.</span><span>insert</span><span>(</span><span>name</span><span>.</span><span>into</span><span>(), </span><span>value</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span>/// Looks up `name`, walking outward through parents.</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>33</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>get</span><span>(&amp;</span><span>self</span><span>, </span><span>name</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Value</span><span>&gt; {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>value</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.values.</span><span>get</span><span>(</span><span>name</span><span>) {</span></div></div><div><div><div>35</div></div><div><span>            </span><span>return</span><span> </span><span>Some</span><span>(</span><span>value</span><span>.</span><span>clone</span><span>());</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span>        </span><span>self</span><span>.parent.</span><span>as_ref</span><span>()?.</span><span>borrow</span><span>().</span><span>get</span><span>(</span><span>name</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span>/// Assigns to an existing binding, walking outward. Returns whether it was</span></div></div><div><div><div>41</div></div><div><span>    </span><span>/// found; [`false`] means the name is undefined.</span></div></div><div><div><div>42</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>assign</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: &amp;</span><span>str</span><span>, </span><span>value</span><span>: </span><span>Value</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>43</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>slot</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.values.</span><span>get_mut</span><span>(</span><span>name</span><span>) {</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>*</span><span>slot</span><span><span> </span><span>=</span><span> </span></span><span>value</span><span>;</span></div></div><div><div><div>45</div></div><div><span>            </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span>        </span><span>match</span><span> &amp;</span><span>self</span><span>.parent {</span></div></div><div><div><div>48</div></div><div><span>            </span><span>Some</span><span>(</span><span>parent</span><span>) =&gt; </span><span>parent</span><span>.</span><span>borrow_mut</span><span>().</span><span>assign</span><span>(</span><span>name</span><span>, </span><span>value</span><span>),</span></div></div><div><div><div>49</div></div><div><span>            </span><span>None</span><span> =&gt; </span><span>false</span><span>,</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>    </span><span>/// Whether this exact scope (not its parents) defines `name`.</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>55</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>contains_local</span><span>(&amp;</span><span>self</span><span>, </span><span>name</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>56</div></div><div><span>        </span><span>self</span><span>.values.</span><span>contains_key</span><span>(</span><span>name</span><span>)</span></div></div><div><div><div>57</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>    </span><span>/// Number of names defined in this scope.</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>61</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>local_len</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>62</div></div><div><span>        </span><span>self</span><span>.values.</span><span>len</span><span>()</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>64</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读这段代码，你至少要注意下面这些点：</p><ul>
<li><code>values: HashMap&lt;String, Value&gt;</code> 的键是 <code>String</code>，但 <code>get(&amp;self, name: &amp;str)</code> 接受 <code>&amp;str</code>。<code>HashMap&lt;K, V&gt;::get&lt;Q&gt;(&amp;self, k: &amp;Q)</code> 要求 <code>K: Borrow&lt;Q&gt;</code>：<code>String</code> 实现了 <code>Borrow&lt;str&gt;</code>，所以拿 <code>&amp;str</code> 就能查。这不是魔法，只是标准库里的一个 trait。C++ 的异构查找（<code>unordered_map::find</code> 接受 <code>string_view</code>）花了很多年才在 C++20 里进来，还要你自己给容器配透明哈希。实验（<code>~/rust-lab/src/bin/ch08_borrow.rs</code>）：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>collections</span><span>::</span></span><span>HashMap</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>env</span><span>: </span><span>HashMap</span><span>&lt;</span><span>String</span><span>, </span><span>i64</span><span><span>&gt; </span><span>=</span><span> </span></span><span>HashMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>env</span><span>.</span><span>insert</span><span>(</span><span>"x"</span><span>.</span><span>to_string</span><span>(), </span><span>1</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"x"</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>env</span><span>.</span><span>get</span><span>(</span><span>name</span><span>));</span></div></div><div><div><div>8</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>env</span><span>.</span><span>get</span><span>(&amp;</span><span>"y"</span><span>.</span><span>to_string</span><span>()));</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_borrow</span></div></div><div><div><div>2</div></div><div><span>Some(1)</span></div></div><div><div><div>3</div></div><div><span>None</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>类似地，<code>define(&amp;mut self, name: impl Into&lt;String&gt;, ..)</code> 里用 <code>Into</code>，是因为调用者有时给 <code>&amp;str</code>，有时给 <code>String</code>。与它相对的另一个转换 trait 是 <code>AsRef</code>：<code>fn shout(text: impl AsRef&lt;str&gt;)</code> 表示”我只想借用它的字符串视图”，不需要拥有。<code>Into</code> 消耗，<code>AsRef</code> 借用（<code>~/rust-lab/src/bin/ch08_asref.rs</code>）：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>shout</span><span>(</span><span>text</span><span>: </span><span>impl</span><span> </span><span>AsRef</span><span>&lt;</span><span>str</span><span>&gt;) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>text</span><span>.</span><span>as_ref</span><span>().</span><span>to_uppercase</span><span>()</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>shout</span><span>(</span><span>"pebble"</span><span>));</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>shout</span><span><span>(</span><span>String</span><span>::</span></span><span>from</span><span>(</span><span>"pebble"</span><span>)));</span></div></div><div><div><div>8</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>shout</span><span><span>(</span><span>std</span><span>::</span><span>rc</span><span>::</span><span>Rc</span><span>::&lt;</span></span><span>str</span><span>&gt;::</span><span>from</span><span>(</span><span>"pebble"</span><span>)));</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_asref</span></div></div><div><div><div>2</div></div><div><span>PEBBLE</span></div></div><div><div><div>3</div></div><div><span>PEBBLE</span></div></div><div><div><div>4</div></div><div><span>PEBBLE</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>get</code> 返回 <code>Option&lt;Value&gt;</code>（克隆出来的），而不是引用。因为要穿过 <code>RefCell</code> 的守卫：<code>Ref</code> 守卫在函数返回时就释放了，返回指向里面的引用会悬空，编译器不会让你这么写。克隆一个 <code>Value</code> 只是计数加一，便宜。</li>
<li><code>self.parent.as_ref()?.borrow().get(name)</code>：<code>as_ref()</code> 把 <code>&amp;Option&lt;Rc&lt;..&gt;&gt;</code> 变成 <code>Option&lt;&amp;Rc&lt;..&gt;&gt;</code>，问号在 <code>None</code> 时让整个函数返回 <code>None</code>（第 2 篇的 <code>?</code> on <code>Option</code>），<code>borrow()</code> 取父作用域的只读守卫，递归地 <code>get</code>。</li>
<li><code>assign</code> 里用 <code>borrow_mut()</code> 沿链往上改。每个作用域是一个<strong>独立的</strong> <code>RefCell</code>，所以子作用域持有 <code>&amp;mut self</code> 的时候，再去 <code>borrow_mut</code> 父作用域不会冲突。如果有人把作用域链做成环，这里就是 <code>already borrowed</code> 的 panic 现场。</li>
<li><code>child</code> 是关联函数，不是方法，参数 <code>parent</code> 是一个 <code>Rc</code>（所有权转入）；调用者想继续用，先 <code>Rc::clone</code>。</li>
</ul><p><code>contains_local</code>、<code>local_len</code> 是两个只读小方法，<code>Default</code>（直接 <code>new()</code>）和手写的 <code>Debug</code>（理由和 <code>Function</code> 一样）一并写上，完整代码就是上面的 <code>impl Env</code> 加这一段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Default</span><span> </span><span>for</span><span> </span><span>Env</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>default</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>Self</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>Env</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>f</span><span>.</span><span>debug_struct</span><span>(</span><span>"Env"</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>.</span><span>field</span><span>(</span><span>"locals"</span><span>, &amp;</span><span>self</span><span>.values.</span><span>len</span><span>())</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>field</span><span>(</span><span>"has_parent"</span><span>, &amp;</span><span>self</span><span>.parent.</span><span>is_some</span><span>())</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>.</span><span>finish</span><span>()</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试放在 <code>map_keys_survive_nan_and_lists</code> 后面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>environments_shadow_and_assign_outward</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>global</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Env</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>4</div></div><div><span>        </span><span>global</span><span>.</span><span>borrow_mut</span><span>().</span><span>define</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>inner</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>global</span><span>));</span></div></div><div><div><div>6</div></div><div><span>        </span><span>inner</span><span>.</span><span>borrow_mut</span><span>().</span><span>define</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>));</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>inner</span><span>.</span><span>borrow</span><span>().</span><span>get</span><span>(</span><span>"x"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>2</span><span>));</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert!</span><span>(</span><span>inner</span><span>.</span><span>borrow_mut</span><span>().</span><span>assign</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>3</span><span>)));</span></div></div><div><div><div>9</div></div><div><span>        </span><span>// Assignment inside the child updates the child's own binding.</span></div></div><div><div><div>10</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>inner</span><span>.</span><span>borrow</span><span>().</span><span>get</span><span>(</span><span>"x"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>3</span><span>));</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>other</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>global</span><span>));</span></div></div><div><div><div>13</div></div><div><span>        </span><span>assert!</span><span>(</span><span>other</span><span>.</span><span>borrow_mut</span><span>().</span><span>assign</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>9</span><span>)));</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>global</span><span>.</span><span>borrow</span><span>().</span><span>get</span><span>(</span><span>"x"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>9</span><span>));</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个作用域都定义了 <code>x</code> 时，子作用域的是 2（遮蔽），赋值只改自己最近的那个绑定；另一个子作用域给 <code>x</code> 赋值时，沿链找到全局的 <code>x</code>，全局变成 9。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_value-8a12146cf753ccc2)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 8 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::division_by_zero_is_an_error ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::environments_shadow_and_assign_outward ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::integer_overflow_is_reported_not_wrapped ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::numbers_add_across_variants ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::list_identity_not_structure_for_equality ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::map_keys_survive_nan_and_lists ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::type_errors_are_clear ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::strings_and_lists_overload_plus ... ok</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>Doc-tests pebble_value</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>running 0 tests</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>内建函数：Output、函数指针表和 dyn<a href="#内建函数output函数指针表和-dyn"><span>#</span></a></h2><p>最后一块是 <code>print</code>、<code>len</code>、<code>range</code> 这些内建函数。它们要解决三个问题：</p><ol>
<li>怎么放进一张表，让解释器和 VM 都能按名字查？</li>
<li><code>print</code> 要写输出，但测试里我们想捕获输出而不是真的打印。</li>
<li>参数个数怎么检查？</li>
</ol><section><h3>Output：trait 对象<a href="#outputtrait-对象"><span>#</span></a></h3><p>第二个问题的答案是让 <code>print</code> 不直接调用 <code>println!</code>，而是通过一个抽象的”输出”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Where a native function writes its output.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Making this a trait means tests can capture output instead of printing to</span></div></div><div><div><div>4</div></div><div><span>/// the terminal, and it is the reason the interpreter never calls `println!`.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>trait</span><span> </span><span>Output</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Writes one line.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>/// Writes to standard output.</span></div></div><div><div><div>11</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Default</span><span>)]</span></div></div><div><div><div>12</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Stdout</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>impl</span><span> </span><span>Output</span><span> </span><span>for</span><span> </span><span>Stdout</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{line}"</span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>/// Collects output in memory; convenient for tests.</span></div></div><div><div><div>21</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Default</span><span>)]</span></div></div><div><div><div>22</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Buffer</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>lines</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>impl</span><span> </span><span>Buffer</span><span> {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>/// A fresh buffer.</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>29</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>Self</span><span>::</span><span>default</span><span>()</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>/// The captured lines.</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>35</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>lines</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;[</span><span>String</span><span>] {</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.lines</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span>/// The captured output as a single newline-joined string.</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>41</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>text</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>42</div></div><div><span>        </span><span>self</span><span>.lines.</span><span>join</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span>}</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>impl</span><span> </span><span>Output</span><span> </span><span>for</span><span> </span><span>Buffer</span><span> {</span></div></div><div><div><div>47</div></div><div><span>    </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>48</div></div><div><span>        </span><span>self</span><span>.lines.</span><span>push</span><span>(</span><span>line</span><span>.</span><span>to_string</span><span>());</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pub trait Output</code> 里只有一个方法。<code>Stdout</code> 把一行写到标准输出，<code>Buffer</code> 把一行存在内存里。<code>#[derive(Debug, Default)]</code> 里的 <code>Default</code> 对 <code>Stdout</code>（单元结构体）和 <code>Buffer</code>（<code>Vec</code> 默认为空）都能自动生成。<code>lines(&amp;self) -&gt; &amp;[String]</code> 返回的是切片借用，不是 <code>Vec</code>；<code>self.lines.join("\n")</code> 是切片的方法。</p><p>重点是 <code>print</code> 怎么拿到”某个 <code>Output</code>”。两种办法：</p><ul>
<li><strong>泛型</strong>：<code>fn print&lt;O: Output&gt;(out: &amp;mut O, ..)</code>。每种 <code>O</code> 编译一份，静态分派，没有间接调用，和 C++ 模板一样。代价是 <code>print</code> 要放进一张表，而表里的每一项必须有相同的类型，泛型函数没法统一。</li>
<li><strong>trait 对象</strong>：<code>&amp;mut dyn Output</code>。一个运行时的”胖指针”：数据指针加一张函数表（vtable）指针，调用 <code>write_line</code> 是一次间接调用，和 C++ 的 virtual 函数调用是同样的机制，只是 vtable 指针不放在对象里，而是放在引用里。</li>
</ul><p><code>~/rust-lab/src/bin/ch08_dyn_output.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>trait</span><span> </span><span>Output</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>);</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>Stdout</span><span>;</span></div></div><div><div><div>6</div></div><div><span>struct</span><span> </span><span>Buffer</span><span>(</span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;);</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>Output</span><span> </span><span>for</span><span> </span><span>Stdout</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{line}"</span><span>);</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>impl</span><span> </span><span>Output</span><span> </span><span>for</span><span> </span><span>Buffer</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>write_line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>line</span><span>: &amp;</span><span>str</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.</span><span>0.</span><span>push</span><span>(</span><span>line</span><span>.</span><span>to_string</span><span>());</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>fn</span><span> </span><span>greet</span><span>(</span><span>out</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Output</span><span>) {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>out</span><span>.</span><span>write_line</span><span>(</span><span>"hello"</span><span>);</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>buffer</span><span><span> </span><span>=</span><span> </span></span><span>Buffer</span><span><span>(</span><span>Vec</span><span>::</span></span><span>new</span><span>());</span></div></div><div><div><div>26</div></div><div><span>    </span><span>greet</span><span>(&amp;</span><span>mut</span><span> </span><span>Stdout</span><span>);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>greet</span><span>(&amp;</span><span>mut</span><span> </span><span>buffer</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>println!</span><span>(</span><span>"captured: {:?}"</span><span>, </span><span>buffer</span><span>.</span><span>0</span><span>);</span></div></div><div><div><div>29</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of &amp;Buffer:     {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;&amp;</span><span>Buffer</span><span>&gt;());</span></div></div><div><div><div>30</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of &amp;dyn Output: {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;&amp;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;());</span></div></div><div><div><div>31</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of Box&lt;dyn Output&gt;: {}"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Output</span><span>&gt;&gt;());</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_dyn_output</span></div></div><div><div><div>2</div></div><div><span>hello</span></div></div><div><div><div>3</div></div><div><span>captured: ["hello"]</span></div></div><div><div><div>4</div></div><div><span>size_of &amp;Buffer:     8</span></div></div><div><div><div>5</div></div><div><span>size_of &amp;dyn Output: 16</span></div></div><div><div><div>6</div></div><div><span>size_of Box&lt;dyn Output&gt;: 16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>&amp;Buffer</code> 是 8 字节（普通指针），<code>&amp;dyn Output</code> 是 16 字节（数据指针加 vtable 指针）。<code>Box&lt;dyn Output&gt;</code> 也是 16 字节，所以 Rust 里不需要在每个对象里藏一个 vptr，代价是引用变成了胖指针。<code>dyn Trait</code> 只能用于”对象安全”的 trait（没有泛型方法、不返回 <code>Self</code> 之类），<code>Output</code> 满足，不满足的情形第 3 篇的 <code>DiagnosticSink</code> 已经撞过（E0038），第 12 篇会再撞一次。</p><p>回到我们的目标：内建函数的签名统一成 <code>fn(&amp;mut dyn Output, Span, &amp;[Value]) -&gt; Result&lt;Value, RuntimeError&gt;</code>，于是 <code>print</code> 可以和 <code>len</code> 放在同一张表里，而测试可以传 <code>&amp;mut Buffer</code>。这也是为什么解释器从不直接调用 <code>println!</code>。</p></section><section><h3>NativeFunction：函数指针放进表<a href="#nativefunction函数指针放进表"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// How many arguments a native function accepts.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Arity</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Exactly this many.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Exact</span><span>(</span><span>usize</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// At least this many.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>AtLeast</span><span>(</span><span>usize</span><span>),</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Any number.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>Any</span><span>,</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>impl</span><span> </span><span>Arity</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>/// Whether `count` arguments are acceptable.</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>accepts</span><span>(</span><span>self</span><span>, </span><span>count</span><span>: </span><span>usize</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>Arity</span><span>::</span><span>Exact</span><span>(</span><span>n</span><span>) =&gt; </span><span>count</span><span><span> </span><span>==</span><span> </span></span><span>n</span><span>,</span></div></div><div><div><div>18</div></div><div><span>            </span><span>Arity</span><span>::</span><span>AtLeast</span><span>(</span><span>n</span><span>) =&gt; </span><span>count</span><span><span> </span><span>&gt;=</span><span> </span></span><span>n</span><span>,</span></div></div><div><div><div>19</div></div><div><span>            </span><span>Arity</span><span>::</span><span>Any</span><span> =&gt; </span><span>true</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>/// A human description used in arity errors.</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>25</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>describe</span><span>(</span><span>self</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>27</div></div><div><span>            </span><span>Arity</span><span>::</span><span>Exact</span><span>(</span><span>n</span><span>) =&gt; </span><span>format!</span><span>(</span><span>"exactly {n}"</span><span>),</span></div></div><div><div><div>28</div></div><div><span>            </span><span>Arity</span><span>::</span><span>AtLeast</span><span>(</span><span>n</span><span>) =&gt; </span><span>format!</span><span>(</span><span>"at least {n}"</span><span>),</span></div></div><div><div><div>29</div></div><div><span>            </span><span>Arity</span><span>::</span><span>Any</span><span> =&gt; </span><span>"any number of"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Arity</code> 是个小枚举，<code>accepts</code> 是 <code>const fn</code>（第 1 篇），<code>describe</code> 生成错误消息文字。<code>#[derive(Copy, Clone, Debug, PartialEq, Eq)]</code> 里 <code>Copy</code> 成立，因为所有变体只有一个 <code>usize</code>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The signature every builtin shares.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>type</span><span> </span><span>NativeImpl</span><span><span> </span><span>=</span><span> </span></span><span>fn</span><span>(&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Output</span><span>, </span><span>Span</span><span>, &amp;[</span><span>Value</span><span>]) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt;;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>/// A builtin function.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>NativeFunction</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Name as seen from Pebble.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>name</span><span>: &amp;'</span><span>static</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Accepted argument count.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>arity</span><span>: </span><span>Arity</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>/// The implementation.</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>func</span><span>: </span><span>NativeImpl</span><span>,</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>NativeFunction</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"&lt;native {}&gt;"</span><span>, </span><span>self</span><span>.name)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>type NativeImpl = fn(...)</code> 是一个类型别名。读它的参数：<code>&amp;mut dyn Output</code>、<code>&amp;[Value]</code> 都带着引用，但你没看到任何生命周期。第 4 篇讲过省略规则：在函数指针类型里，每个省略的生命周期都是<strong>独立的、对所有生命周期成立</strong>的参数，也就是说它其实是这个东西（<code>~/rust-lab/src/bin/ch08_hrtb.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[allow(dead_code)]</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Span</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>type</span><span> </span><span>Native</span><span><span> </span><span>=</span><span> </span></span><span>fn</span><span>(&amp;</span><span>mut</span><span> </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>Span</span><span>, &amp;[</span><span>i64</span><span>]) -&gt; </span><span>i64</span><span>;</span></div></div><div><div><div>5</div></div><div><span>type</span><span> </span><span>Spelled</span><span><span> </span><span>=</span><span> </span></span><span>for</span><span>&lt;'</span><span>a</span><span>, '</span><span>b</span><span>&gt; </span><span>fn</span><span>(&amp;'</span><span>a</span><span> </span><span>mut</span><span> </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>Span</span><span>, &amp;'</span><span>b</span><span> [</span><span>i64</span><span>]) -&gt; </span><span>i64</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>count</span><span>(</span><span>_</span><span>: &amp;</span><span>mut</span><span> </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>_</span><span>: </span><span>Span</span><span>, </span><span>args</span><span>: &amp;[</span><span>i64</span><span>]) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>args</span><span>.</span><span>len</span><span>() </span><span>as</span><span> </span><span>i64</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span>: </span><span>Native</span><span><span> </span><span>=</span><span> </span></span><span>count</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span>: </span><span>Spelled</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span>;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>log</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>b</span><span>(&amp;</span><span>mut</span><span> </span><span>log</span><span>, </span><span>Span</span><span>(</span><span>0</span><span>), &amp;[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]));</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_hrtb</span></div></div><div><div><div>2</div></div><div><span>3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>for&lt;'a, 'b&gt; fn(&amp;'a mut Vec&lt;String&gt;, Span, &amp;'b [i64]) -&gt; i64</code> 读作”对于<strong>任何</strong>生命周期 <code>'a</code> 和 <code>'b</code>，一个接受 <code>&amp;'a</code> 和 <code>&amp;'b</code> 的函数”，这叫高阶生命周期约束（higher-ranked trait bound，HRTB），写 <code>for&lt;..&gt;</code> 的语法是它。省略写法和展开写法是同一个类型，上面 <code>let b: Spelled = a;</code> 能编译就是证据。现在知道这个名字就够了，你不需要自己写它。</p><p><code>NativeFunction</code> 的手写 <code>Debug</code> 只打印名字，因为函数指针没有有用的 <code>Debug</code> 输出。下面是它的调用方法：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>NativeFunction</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// The builtin's name.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>name</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.name</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>call</span><span>(</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>,</span></div></div><div><div><div>10</div></div><div><span>        </span><span>out</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Output</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>args</span><span>: &amp;[</span><span>Value</span><span>],</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>if</span><span><span> </span><span>!</span></span><span>self</span><span>.arity.</span><span>accepts</span><span>(</span><span>args</span><span>.</span><span>len</span><span>()) {</span></div></div><div><div><div>15</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span>(</span><span>RuntimeError</span><span>::</span><span>new</span><span>(</span></div></div><div><div><div>16</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>17</div></div><div><span>                </span><span>format!</span><span>(</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>"{} expects {} arguments, got {}"</span><span>,</span></div></div><div><div><div>19</div></div><div><span>                    </span><span>self</span><span>.name,</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>self</span><span>.arity.</span><span>describe</span><span>(),</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>args</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>),</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>));</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>(</span><span>self</span><span>.func)(</span><span>out</span><span>, </span><span>span</span><span>, </span><span>args</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>fn call(&amp;self, ..)</code> 没有 <code>pub</code>，它是模块私有的；外部通过后面的 <code>call_native</code> 调用。<code>(self.func)(out, span, args)</code> 的括号是必需的，不然编译器会以为你在调用一个叫 <code>func</code> 的方法，C++ 里也有相似的语法 <code>(obj.*pmf)(args)</code>。<code>self.arity.describe()</code> 在这里和 <code>accepts</code> 一样用到 <code>Copy</code>：<code>Arity</code> 是 <code>Copy</code>，所以 <code>self.arity.accepts(..)</code> 不会移走字段。</p></section><section><h3>range：切片模式<a href="#range切片模式"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>builtin_range</span><span>(</span><span>_</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Output</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>, </span><span>args</span><span>: &amp;[</span><span>Value</span><span>]) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> (</span><span>start</span><span>, </span><span>end</span><span><span>) </span><span>=</span><span> </span></span><span>match</span><span> </span><span>args</span><span> {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>[</span><span>end</span><span>] =&gt; (</span><span>0</span><span>, </span><span>int_arg</span><span>(</span><span>end</span><span>, </span><span>span</span><span>)?),</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>[</span><span>start</span><span>, </span><span>end</span><span>] =&gt; (</span><span>int_arg</span><span>(</span><span>start</span><span>, </span><span>span</span><span>)?, </span><span>int_arg</span><span>(</span><span>end</span><span>, </span><span>span</span><span>)?),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>unreachable!</span><span>(</span><span>"arity checked by caller"</span><span>),</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>items</span><span>: </span><span>Vec</span><span>&lt;</span><span>Value</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>start</span><span>..</span><span>end</span><span>).</span><span>map</span><span><span>(</span><span>Value</span><span>::</span></span><span>Int</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Ok</span><span>(</span><span>Value</span><span>::</span><span>list</span><span>(</span><span>items</span><span>))</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>range(5)</code> 给 <code>[0, 1, 2, 3, 4]</code>，<code>range(2, 5)</code> 给 <code>[2, 3, 4]</code>。<code>args</code> 是 <code>&amp;[Value]</code>，<code>match args { [end] =&gt; .., [start, end] =&gt; .. }</code> 是<strong>切片模式</strong>：按长度和内容同时匹配，<code>[end]</code> 只匹配长度为 1 的切片并绑定唯一的元素。C 里这要写 <code>if (argc == 1) {..} else if (argc == 2) {..}</code>，一旦漏了边界就是越界读；这里漏了就是编译错误（穷尽性检查）。</p><p>兜底分支 <code>_ =&gt; unreachable!("arity checked by caller")</code> 的措辞说”调用者已经检查过个数”，但 <code>range</code> 在表里的声明是 <code>Arity::AtLeast(1)</code>，“至少一个”包含三个。我在 tag 的 worktree 里加了个临时测试，用三个参数调用它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>thread 'scratch::range_three_args' (85959) panicked at crates/pebble-value/src/lib.rs:899:14:</span></div></div><div><div><div>2</div></div><div><span>internal error: entered unreachable code: arity checked by caller</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以 <code>range(1, 2, 3)</code> 会 panic，而不是报一条漂亮的 arity 错误。这是 tag 里一个真实的小毛刺：声明（<code>AtLeast(1)</code>）比实现（只处理 1 个或 2 个）宽。我没有改它，因为这一篇的目标是让你的文件和 tag 一致，但现在你知道 <code>unreachable!</code> 是一个承诺，写下它就要保证没有路径能抵达。给 <code>Arity</code> 加一个 <code>Between(usize, usize)</code> 变体是自然的修法。</p><p><code>(start..end).map(Value::Int).collect()</code> 里，<code>start..end</code> 本身就是一个迭代器（半开区间 <code>Range&lt;i64&gt;</code>），<code>.map(Value::Int)</code> 再次把变体当函数用，<code>.collect()</code> 把它们收集成 <code>Vec&lt;Value&gt;</code>（类型由左边 <code>let items: Vec&lt;Value&gt;</code> 标注推断）。</p></section><section><h3>builtin_names：-&gt; impl Iterator<a href="#builtin_names--impl-iterator"><span>#</span></a></h3><p>表里的名字要供给 VM 使用，我们暴露一个函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The names of every builtin, in table order.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>builtin_names</span><span>() -&gt; </span><span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span><span> </span><span>=</span><span> &amp;'</span></span><span>static</span><span> </span><span>str</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>BUILTINS</span><span>.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>function</span><span><span>|</span><span> </span></span><span>function</span><span>.name)</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-&gt; impl Iterator&lt;Item = &amp;'static str&gt;</code> 的意思是”返回某个迭代器，我不告诉你它的具体类型，只保证它产出 <code>&amp;'static str</code>”。具体类型是 <code>Map&lt;slice::Iter&lt;NativeFunction&gt;, 一个闭包类型&gt;</code>，这种类型是写不出来的（闭包类型没有名字），所以 <code>impl Trait</code> 是唯一的出路。C++ 里这就是 <code>auto</code> 返回类型，但 Rust 的 <code>impl Trait</code> 在签名里就承诺了接口，调用者看得到约束。</p><p><code>static BUILTINS: &amp;[NativeFunction]</code> 是整张表。<code>static</code> 是整个程序生命期的全局，地址固定，所以我们才能把 <code>&amp;'static NativeFunction</code> 放进 <code>Value::Native</code>，不需要 <code>Rc</code>。表里 11 项长得完全一样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The builtin functions, in one place.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>static</span><span> </span><span>BUILTINS</span><span>: &amp;[</span><span>NativeFunction</span><span><span>] </span><span>=</span><span> &amp;[</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>NativeFunction</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>name</span><span>: </span><span>"print"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>arity</span><span>: </span><span>Arity</span><span>::</span><span>Any</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>func</span><span>: </span><span>builtin_print</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>8</div></div><div><span>    </span><span>NativeFunction</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>name</span><span>: </span><span>"len"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>        </span><span>arity</span><span>: </span><span>Arity</span><span>::</span><span>Exact</span><span>(</span><span>1</span><span>),</span></div></div><div><div><div>11</div></div><div><span>        </span><span>func</span><span>: </span><span>builtin_len</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>剩下 9 项，以及 <code>builtin_print</code>、<code>builtin_len</code>、<code>builtin_type</code>、<code>builtin_str</code>、<code>builtin_int</code>、<code>builtin_float</code>、<code>builtin_push</code>、<code>builtin_keys</code>、<code>builtin_clock</code>、<code>builtin_assert</code>、<code>int_arg</code> 都从 tag 复制：<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-value/src/lib.rs#L825-L1039" target="_blank">第 825 到 1039 行</a>（这一段里有几项你已经写过：<code>builtin_range</code>、<code>builtin_names</code>、<code>builtin</code>、<code>call_native</code>，复制时跳过它们；<code>BUILTINS</code> 表你只写了前两项，用 tag 里完整的 11 项表替换它）。可以放心不细读的理由：它们就是 <code>match args[0]</code> 加一个错误分支，没有新的语言特性。值得扫一眼的有三个：<code>builtin_push</code> 里的 <code>let Value::List(items) = &amp;args[0] else { .. }</code> 是第 3 篇的 <code>let else</code>；<code>builtin_assert</code> 把消息写进 <code>out</code>，所以失败信息出现在被捕获的输出里；<code>builtin_int</code> 里的 <code>text.trim().parse::&lt;i64&gt;()</code> 是标准库的 <code>FromStr</code>，下面会讲它。</p><p>最后是 <code>builtin</code> 和 <code>call_native</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Looks up a builtin by name.</span></div></div><div><div><div>2</div></div><div><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>builtin</span><span>(</span><span>name</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Option</span><span>&lt;&amp;'</span><span>static</span><span> </span><span>NativeFunction</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>BUILTINS</span><span>.</span><span>iter</span><span>().</span><span>find</span><span><span>(</span><span>|</span></span><span>function</span><span><span>|</span><span> </span></span><span>function</span><span><span>.name </span><span>==</span><span> </span></span><span>name</span><span>)</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Calls a native function with a runtime arity check.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>call_native</span><span>(</span></div></div><div><div><div>3</div></div><div><span>    </span><span>function</span><span>: &amp;'</span><span>static</span><span> </span><span>NativeFunction</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>out</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Output</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>args</span><span>: &amp;[</span><span>Value</span><span>],</span></div></div><div><div><div>7</div></div><div><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>function</span><span>.</span><span>call</span><span>(</span><span>out</span><span>, </span><span>span</span><span>, </span><span>args</span><span>)</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Option&lt;&amp;'static NativeFunction&gt;</code>：查表的结果是”要么没有，要么一个永远有效的借用”。</p></section><section><h3>Native 变体<a href="#native-变体"><span>#</span></a></h3><p>终于可以补上 <code>Value</code> 的最后一个变体了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// A builtin implemented in Rust.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Native</span><span>(&amp;'</span><span>static</span><span> </span><span>NativeFunction</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>&amp;'static NativeFunction</code> 是一个 <code>Copy</code> 的引用。把它加进来，需要四处同步：<code>type_name</code> 的函数分支改成或模式 <code>Value::Function(_) | Value::Native(_) =&gt; "function",</code>；<code>equals</code> 加 <code>(Value::Native(a), Value::Native(b)) =&gt; core::ptr::eq(*a, *b),</code>（指针相等，因为函数表是静态的，同一个内建函数只有一份）；<code>key_hash</code> 加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>Value</span><span>::</span><span>Native</span><span>(</span><span>value</span><span>) =&gt; {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>8</span><span>u8</span><span>.</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>                </span></span><span>(*</span><span>value</span><span> </span><span>as</span><span> *</span><span>const</span><span> </span><span>NativeFunction</span><span>).</span><span>hash</span><span>(</span><span>state</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>write_value</code> 加 <code>Value::Native(function) =&gt; write!(f, "&lt;native fn {}&gt;", function.name),</code>。<code>(*value as *const NativeFunction).hash(state)</code> 把引用转成裸指针再哈希，和 <code>Rc::as_ptr</code> 是同一个意图，我们会在第 11 篇正式讲裸指针。</p></section><section><h3>转换 trait：TryFrom 和 FromStr<a href="#转换-traittryfrom-和-fromstr"><span>#</span></a></h3><p><code>builtin_int</code> 里有两种”可能失败的转换”，标准库为它们各留了一个 trait：</p><ul>
<li><code>TryFrom&lt;T&gt;</code>：类型到类型，失败时返回 <code>Self::Error</code>。<code>u32::try_from(-1i64)</code> 是 <code>Err</code>。</li>
<li><code>FromStr</code>：字符串到类型，<code>str::parse::&lt;T&gt;()</code> 的实现基础，<code>"42".parse::&lt;i64&gt;()</code> 就是 <code>i64::from_str("42")</code>。</li>
</ul><p>两种我们都没有给自己的类型实现过，所以做个实验（<code>~/rust-lab/src/bin/ch08_try_from.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>convert</span><span>::</span></span><span>TryFrom</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[allow(dead_code)]</span></div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>Index</span><span>(</span><span>u32</span><span>);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>TryFrom</span><span>&lt;</span><span>i64</span><span>&gt; </span><span>for</span><span> </span><span>Index</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>type</span><span> </span><span>Error</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>try_from</span><span>(</span><span>value</span><span>: </span><span>i64</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>, </span><span>Self</span><span>::</span><span>Error</span><span>&gt; {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>u32</span><span>::</span><span>try_from</span><span>(</span><span>value</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span>(</span><span>Index</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>.</span><span>map_err</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> </span></span><span>format!</span><span>(</span><span>"{value} is not a valid index"</span><span>))</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>Index</span><span>::</span><span>try_from</span><span>(</span><span>7</span><span>));</span></div></div><div><div><div>19</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>Index</span><span>::</span><span>try_from</span><span>(-</span><span>1</span><span>));</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>converted</span><span>: </span><span>Result</span><span>&lt;</span><span>Index</span><span>, </span><span>_</span><span><span>&gt; </span><span>=</span><span> </span></span><span>5_000_000_000</span><span>i64</span><span>.</span><span>try_into</span><span>();</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>converted</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>"42"</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;());</span></div></div><div><div><div>23</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>"4x2"</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;());</span></div></div><div><div><div>24</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>u8</span><span>::</span><span>try_from</span><span>(</span><span>300</span><span>i32</span><span>));</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch08_try_from</span></div></div><div><div><div>2</div></div><div><span>Ok(Index(7))</span></div></div><div><div><div>3</div></div><div><span>Err("-1 is not a valid index")</span></div></div><div><div><div>4</div></div><div><span>Err("5000000000 is not a valid index")</span></div></div><div><div><div>5</div></div><div><span>Ok(42)</span></div></div><div><div><div>6</div></div><div><span>Err(ParseIntError { kind: InvalidDigit })</span></div></div><div><div><div>7</div></div><div><span>Err(TryFromIntError(()))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>impl TryFrom&lt;i64&gt; for Index</code> 之后，<code>5_000_000_000i64.try_into()</code> 白送了：<code>TryInto</code> 是 <code>TryFrom</code> 的反向，标准库给所有 <code>TryFrom</code> 一个全覆盖实现（第 3 篇的 blanket impl）。<code>ParseIntError { kind: InvalidDigit }</code> 是 <code>parse</code> 的错误类型，<code>TryFromIntError(())</code> 是 <code>try_from</code> 的。到第 9 篇，我们会给自己的类型实现 <code>FromStr</code>，让 REPL 的元命令 <code>":quit"</code> 用 <code>.parse::&lt;MetaCommand&gt;()</code> 解析。</p></section></section>
<section><h2>补上文件头<a href="#补上文件头"><span>#</span></a></h2><p>所有部分都就位了。现在把文件最顶部那几行换成 tag 里的版本：<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-value/src/lib.rs#L1-L31" target="_blank">第 1 到 31 行</a>，一共三块：<code>#![forbid(unsafe_code)]</code>，上面那段带理由的 <code>#![allow(clippy::mutable_key_type)]</code>，以及 crate 级文档注释和完整的 <code>use</code> 列表。这段文档里有个英文破折号，我的博客不用这个符号，所以不在文章里贴它，直接从链接里复制。</p><p>文档注释里有两处值得说明：<code>//!</code> 是 crate 级文档，写给 <code>cargo doc</code> 看；[<code>Rc</code>] 这种方括号是 rustdoc 的”内部链接”，文档构建会检查它们指向的东西存在。第 15 篇我们会让 <code>RUSTDOCFLAGS="-D warnings"</code> 把失效的链接变成构建失败。</p><div><div><div></div><div>这段文档提前说了一个故事</div></div><div><p>文档里写着 <code>Weak</code> 在这里<strong>故意没用</strong>：闭包用强 <code>Rc</code> 捕获定义它的环境，所以一个存在它自己捕获的环境里的递归函数会形成引用环，进而泄漏。这就是第 9 篇要用 <code>Weak</code> 实验证明的内容，也是 VM 后端选择垃圾回收的原因。</p></div></div><p>最后补上剩下的测试。把它们放在 <code>tests</code> 模块里，次序必须和 tag 一致，这样文件才能逐字节相同：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>span</span><span>() -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Span</span><span>::</span><span>DUMMY</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>numbers_add_across_variants</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>12</div></div><div><span>            </span><span>add</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>), </span><span>span</span><span>()).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>3</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>16</div></div><div><span>            </span><span>add</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Float</span><span>(</span><span>0.5</span><span>), </span><span>span</span><span>()).</span><span>unwrap</span><span>(),</span></div></div><div><div><div>17</div></div><div><span>            </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>1.5</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>22</div></div><div><span>    </span><span>fn</span><span> </span><span>integer_overflow_is_reported_not_wrapped</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>        </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>add</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span><span>(</span><span>i64</span><span>::</span></span><span>MAX</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>), </span><span>span</span><span>()).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>24</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>error</span><span>.</span><span>message</span><span>(), </span><span>"integer overflow"</span><span>);</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>28</div></div><div><span>    </span><span>fn</span><span> </span><span>strings_and_lists_overload_plus</span><span>() {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>hello</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>string</span><span>(</span><span>"he"</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>llo</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>string</span><span>(</span><span>"llo"</span><span>);</span></div></div><div><div><div>31</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>add</span><span>(&amp;</span><span>hello</span><span>, &amp;</span><span>llo</span><span>, </span><span>span</span><span>()).</span><span>unwrap</span><span>().</span><span>to_string</span><span>(), </span><span>"hello"</span><span>);</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)]);</span></div></div><div><div><div>34</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>)]);</span></div></div><div><div><div>35</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>add</span><span>(&amp;</span><span>a</span><span>, &amp;</span><span>b</span><span>, </span><span>span</span><span>()).</span><span>unwrap</span><span>().</span><span>to_string</span><span>(), </span><span>"[1, 2]"</span><span>);</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>39</div></div><div><span>    </span><span>fn</span><span> </span><span>type_errors_are_clear</span><span>() {</span></div></div><div><div><div>40</div></div><div><span>        </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>sub</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>string</span><span>(</span><span>"a"</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>), </span><span>span</span><span>()).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>41</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>42</div></div><div><span>            </span><span>error</span></div></div><div><div><div>43</div></div><div><span><span>                </span></span><span>.</span><span>message</span><span>()</span></div></div><div><div><div>44</div></div><div><span><span>                </span></span><span>.</span><span>contains</span><span>(</span><span>"cannot apply `-` to string and int"</span><span>)</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>49</div></div><div><span>    </span><span>fn</span><span> </span><span>division_by_zero_is_an_error</span><span>() {</span></div></div><div><div><div>50</div></div><div><span>        </span><span>assert!</span><span>(</span><span>div</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>0</span><span>), </span><span>span</span><span>()).</span><span>is_err</span><span>());</span></div></div><div><div><div>51</div></div><div><span>        </span><span>// Floating point division by zero is IEEE, not an error.</span></div></div><div><div><div>52</div></div><div><span>        </span><span>assert!</span><span>(</span><span>div</span><span><span>(&amp;</span><span>Value</span><span>::</span></span><span>Float</span><span>(</span><span>1.0</span><span><span>), &amp;</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>0</span><span>), </span><span>span</span><span>()).</span><span>is_ok</span><span>());</span></div></div><div><div><div>53</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>56</div></div><div><span>    </span><span>fn</span><span> </span><span>list_identity_not_structure_for_equality</span><span>() {</span></div></div><div><div><div>57</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)]);</span></div></div><div><div><div>58</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Value</span><span>::</span><span>list</span><span>(</span><span>vec!</span><span><span>[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)]);</span></div></div><div><div><div>59</div></div><div><span>        </span><span>assert!</span><span><span>(</span><span>!</span></span><span>a</span><span>.</span><span>equals</span><span>(&amp;</span><span>b</span><span>));</span></div></div><div><div><div>60</div></div><div><span>        </span><span>assert!</span><span>(</span><span>a</span><span>.</span><span>equals</span><span>(&amp;</span><span>a</span><span>.</span><span>clone</span><span>()));</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>64</div></div><div><span>    </span><span>fn</span><span> </span><span>map_keys_survive_nan_and_lists</span><span>() {</span></div></div><div><div><div>65</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>HashMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>66</div></div><div><span>        </span><span>map</span><span>.</span><span>insert</span><span>(</span><span>MapKey</span><span><span>(</span><span>Value</span><span>::</span></span><span>Float</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span><span>)), </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>67</div></div><div><span>        </span><span>map</span><span>.</span><span>insert</span><span>(</span><span>MapKey</span><span><span>(</span><span>Value</span><span>::</span></span><span>list</span><span>(</span><span>vec!</span><span><span>[])), </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>));</span></div></div><div><div><div>68</div></div><div><span>        </span><span>// A fresh NaN key must find the stored entry thanks to bitwise hashing.</span></div></div><div><div><div>69</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>70</div></div><div><span>            </span><span>map</span><span>.</span><span>get</span><span>(&amp;</span><span>MapKey</span><span><span>(</span><span>Value</span><span>::</span></span><span>Float</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span>))),</span></div></div><div><div><div>71</div></div><div><span>            </span><span>Some</span><span>(&amp;</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>))</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>73</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>map</span><span>.</span><span>len</span><span>(), </span><span>2</span><span>);</span></div></div><div><div><div>74</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>77</div></div><div><span>    </span><span>fn</span><span> </span><span>environments_shadow_and_assign_outward</span><span>() {</span></div></div><div><div><div>78</div></div><div><span>        </span><span>let</span><span> </span><span>global</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Env</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>79</div></div><div><span>        </span><span>global</span><span>.</span><span>borrow_mut</span><span>().</span><span>define</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>80</div></div><div><span>        </span><span>let</span><span> </span><span>inner</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>global</span><span>));</span></div></div><div><div><div>81</div></div><div><span>        </span><span>inner</span><span>.</span><span>borrow_mut</span><span>().</span><span>define</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>));</span></div></div><div><div><div>82</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>inner</span><span>.</span><span>borrow</span><span>().</span><span>get</span><span>(</span><span>"x"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>2</span><span>));</span></div></div><div><div><div>83</div></div><div><span>        </span><span>assert!</span><span>(</span><span>inner</span><span>.</span><span>borrow_mut</span><span>().</span><span>assign</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>3</span><span>)));</span></div></div><div><div><div>84</div></div><div><span>        </span><span>// Assignment inside the child updates the child's own binding.</span></div></div><div><div><div>85</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>inner</span><span>.</span><span>borrow</span><span>().</span><span>get</span><span>(</span><span>"x"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>3</span><span>));</span></div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>        </span><span>let</span><span> </span><span>other</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>global</span><span>));</span></div></div><div><div><div>88</div></div><div><span>        </span><span>assert!</span><span>(</span><span>other</span><span>.</span><span>borrow_mut</span><span>().</span><span>assign</span><span>(</span><span>"x"</span><span><span>, </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>9</span><span>)));</span></div></div><div><div><div>89</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>global</span><span>.</span><span>borrow</span><span>().</span><span>get</span><span>(</span><span>"x"</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>9</span><span>));</span></div></div><div><div><div>90</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>91</div></div><div>
</div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>93</div></div><div><span>    </span><span>fn</span><span> </span><span>builtin_names_follow_the_table</span><span>() {</span></div></div><div><div><div>94</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>95</div></div><div><span>            </span><span>builtin_names</span><span>().</span><span>collect</span><span>::&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;(),</span></div></div><div><div><div>96</div></div><div><span><span>            </span></span><span>[</span></div></div><div><div><div>97</div></div><div><span>                </span><span>"print"</span><span>, </span><span>"len"</span><span>, </span><span>"type"</span><span>, </span><span>"str"</span><span>, </span><span>"int"</span><span>, </span><span>"float"</span><span>, </span><span>"range"</span><span>, </span><span>"push"</span><span>, </span><span>"keys"</span><span>, </span><span>"clock"</span><span>,</span></div></div><div><div><div>98</div></div><div><span>                </span><span>"assert"</span></div></div><div><div><div>99</div></div><div><span><span>            </span></span><span>]</span></div></div><div><div><div>100</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>101</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>104</div></div><div><span>    </span><span>fn</span><span> </span><span>builtins_are_looked_up_and_arity_checked</span><span>() {</span></div></div><div><div><div>105</div></div><div><span>        </span><span>let</span><span> </span><span>print</span><span><span> </span><span>=</span><span> </span></span><span>builtin</span><span>(</span><span>"print"</span><span>).</span><span>unwrap</span><span>();</span></div></div><div><div><div>106</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>buffer</span><span><span> </span><span>=</span><span> </span></span><span>Buffer</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>107</div></div><div><span>        </span><span>call_native</span><span>(</span><span>print</span><span>, &amp;</span><span>mut</span><span> </span><span>buffer</span><span>, </span><span>span</span><span><span>(), &amp;[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span>)]).</span><span>unwrap</span><span>();</span></div></div><div><div><div>108</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>buffer</span><span>.</span><span>text</span><span>(), </span><span>"1 2"</span><span>);</span></div></div><div><div><div>109</div></div><div>
</div></div><div><div><div>110</div></div><div><span>        </span><span>let</span><span> </span><span>len</span><span><span> </span><span>=</span><span> </span></span><span>builtin</span><span>(</span><span>"len"</span><span>).</span><span>unwrap</span><span>();</span></div></div><div><div><div>111</div></div><div><span>        </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>call_native</span><span>(</span><span>len</span><span>, &amp;</span><span>mut</span><span> </span><span>buffer</span><span>, </span><span>span</span><span>(), &amp;[]).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>112</div></div><div><span>        </span><span>assert!</span><span>(</span><span>error</span><span>.</span><span>message</span><span>().</span><span>contains</span><span>(</span><span>"expects exactly 1"</span><span>));</span></div></div><div><div><div>113</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>114</div></div><div>
</div></div><div><div><div>115</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>116</div></div><div><span>    </span><span>fn</span><span> </span><span>range_and_push_work</span><span>() {</span></div></div><div><div><div>117</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>buffer</span><span><span> </span><span>=</span><span> </span></span><span>Buffer</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>118</div></div><div><span>        </span><span>let</span><span> </span><span>range</span><span><span> </span><span>=</span><span> </span></span><span>builtin</span><span>(</span><span>"range"</span><span>).</span><span>unwrap</span><span>();</span></div></div><div><div><div>119</div></div><div><span>        </span><span>let</span><span> </span><span>list</span><span><span> </span><span>=</span><span> </span></span><span>call_native</span><span>(</span><span>range</span><span>, &amp;</span><span>mut</span><span> </span><span>buffer</span><span>, </span><span>span</span><span><span>(), &amp;[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>3</span><span>)]).</span><span>unwrap</span><span>();</span></div></div><div><div><div>120</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>list</span><span>.</span><span>to_string</span><span>(), </span><span>"[0, 1, 2]"</span><span>);</span></div></div><div><div><div>121</div></div><div>
</div></div><div><div><div>122</div></div><div><span>        </span><span>let</span><span> </span><span>push</span><span><span> </span><span>=</span><span> </span></span><span>builtin</span><span>(</span><span>"push"</span><span>).</span><span>unwrap</span><span>();</span></div></div><div><div><div>123</div></div><div><span>        </span><span>call_native</span><span>(</span><span>push</span><span>, &amp;</span><span>mut</span><span> </span><span>buffer</span><span>, </span><span>span</span><span>(), &amp;[</span><span>list</span><span>.</span><span>clone</span><span><span>(), </span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>9</span><span>)]).</span><span>unwrap</span><span>();</span></div></div><div><div><div>124</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>list</span><span>.</span><span>to_string</span><span>(), </span><span>"[0, 1, 2, 9]"</span><span>);</span></div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>126</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-value</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_value-8a12146cf753ccc2)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 11 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::builtin_names_follow_the_table ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::builtins_are_looked_up_and_arity_checked ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::division_by_zero_is_an_error ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::integer_overflow_is_reported_not_wrapped ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::list_identity_not_structure_for_equality ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::environments_shadow_and_assign_outward ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::map_keys_survive_nan_and_lists ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::numbers_add_across_variants ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::range_and_push_work ... ok</span></div></div><div><div><div>15</div></div><div><span>test tests::strings_and_lists_overload_plus ... ok</span></div></div><div><div><div>16</div></div><div><span>test tests::type_errors_are_clear ... ok</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>Doc-tests pebble_value</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>running 0 tests</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>十一个测试全部通过。<code>builtin_names_follow_the_table</code> 同时钉住了 <code>builtin_names</code> 的返回顺序和上面的 11 项表；<code>range_and_push_work</code> 通过 <code>call_native</code> 调用 <code>push</code>，再通过原来的 <code>list</code> 变量观察到改动，这是本篇开头那四行 Pebble 程序在 Rust 里的缩影。</p></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在工作区根目录里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-value</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--workspace</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo clippy --workspace --all-targets -- -D warnings</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div><div><div><div>3</div></div><div><span>$ cargo fmt --all --check; echo "exit: $?"</span></div></div><div><div><div>4</div></div><div><span>exit: 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo test -p pebble-value</code> 的预期输出就是上面最后一次：<code>11 passed</code>。</p><p>下面这两个文件现在必须和 tag 逐字节相同：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-value/Cargo.toml" target="_blank"><code>crates/pebble-value/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-value/src/lib.rs" target="_blank"><code>crates/pebble-value/src/lib.rs</code></a></li>
</ul><p>检查方法（在你的工作区根目录里，需要在有 tag 的仓库里取原文件）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>&lt;(</span><span>git</span><span> </span><span>-C</span><span> ~/pebble-tag show tutorial-v1:crates/pebble-value/src/lib.rs)</span><span> </span><span>crates/pebble-value/src/lib.rs</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>same</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>章节之间用三行横线注释隔开，格式如下（六节的标题依次是 <code>Values</code>、<code>Operators</code>、<code>Errors</code>、<code>Environments</code>、<code>Functions</code>、<code>Native functions</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// ---------------------------------------------------------------------------</span></div></div><div><div><div>2</div></div><div><span>// Values</span></div></div><div><div><div>3</div></div><div><span>// ---------------------------------------------------------------------------</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文件的顺序是：文件头；<code>Values</code>（<code>Value</code>、<code>impl Value</code>、<code>PartialEq</code>、<code>From</code>、<code>write_value</code>、<code>Display</code>、<code>Debug</code>、<code>MapKey</code> 及其 trait）；<code>Operators</code>（<code>add</code>、<code>sub</code>、<code>mul</code>、<code>div</code>、<code>rem</code>、<code>negate</code>、<code>compare</code>、<code>is_number</code>、<code>as_f64</code>、<code>numeric</code>、<code>overflow</code>、<code>TypeError</code>）；<code>Errors</code>（<code>TraceFrame</code>、<code>RuntimeError</code>）；<code>Environments</code>（<code>Env</code>）；<code>Functions</code>（<code>Function</code>）；<code>Native functions</code>（<code>Arity</code>、<code>Output</code>、<code>Stdout</code>、<code>Buffer</code>、<code>NativeImpl</code>、<code>NativeFunction</code>、各个 <code>builtin_*</code>、<code>BUILTINS</code>、<code>builtin</code>、<code>builtin_names</code>、<code>call_native</code>）；<code>tests</code>。章节之间那种 <code>// ----</code> 横线注释，从 tag 里抄。</p><p>没有需要列出的中间文件：这一篇的所有 crate 都是终稿，根 <code>Cargo.toml</code> 里新增的是 <code>pebble-value</code> 这一行（它要到第 15 篇才算最终版）。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）；如果你把仓库克隆到了 <code>~/pebble-tag</code>，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，先 <code>cd ~/pebble-tag/lab/rust-lab</code> 再运行下表的命令（也可以把这个目录复制成 <code>~/rust-lab</code>）。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_shared_list_move.rs" target="_blank"><code>ch08_shared_list_move.rs</code></a></td><td><code>cargo build --bin ch08_shared_list_move</code></td><td>编译失败：E0382</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_shared_list_mut.rs" target="_blank"><code>ch08_shared_list_mut.rs</code></a></td><td><code>cargo build --bin ch08_shared_list_mut</code></td><td>编译失败：E0499</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_shared_list_rc.rs" target="_blank"><code>ch08_shared_list_rc.rs</code></a></td><td><code>cargo build --bin ch08_shared_list_rc</code></td><td>编译失败：E0596</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_shared_list.rs" target="_blank"><code>ch08_shared_list.rs</code></a></td><td><code>cargo run --quiet --bin ch08_shared_list</code></td><td>运行，首行 <code>strong count: 2</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_refcell_panic.rs" target="_blank"><code>ch08_refcell_panic.rs</code></a></td><td><code>cargo run --quiet --bin ch08_refcell_panic</code></td><td>先打印 <code>try_borrow_mut while reading: true</code>，随后 <code>RefCell already borrowed</code> panic，退出码 101</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_rc_str.rs" target="_blank"><code>ch08_rc_str.rs</code></a></td><td><code>cargo run --quiet --bin ch08_rc_str</code></td><td>运行，首行 <code>size_of::&lt;String&gt;():   24</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_derive_hash.rs" target="_blank"><code>ch08_derive_hash.rs</code></a></td><td><code>cargo build --bin ch08_derive_hash</code></td><td>编译失败：E0277</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_mutable_key.rs" target="_blank"><code>ch08_mutable_key.rs</code></a></td><td><code>cargo run --quiet --bin ch08_mutable_key</code></td><td>运行，首行 <code>before push: Some("one")</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_fn_ptr.rs" target="_blank"><code>ch08_fn_ptr.rs</code></a></td><td><code>cargo run --quiet --bin ch08_fn_ptr</code></td><td>运行，首行 <code>double(21) = 42</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_fn_ptr_capture.rs" target="_blank"><code>ch08_fn_ptr_capture.rs</code></a></td><td><code>cargo build --bin ch08_fn_ptr_capture</code></td><td>编译失败：E0308</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_add_value.rs" target="_blank"><code>ch08_add_value.rs</code></a></td><td><code>cargo run --quiet --bin ch08_add_value</code></td><td>运行，首行 <code>Ok(Int(3))</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_add_value_moved.rs" target="_blank"><code>ch08_add_value_moved.rs</code></a></td><td><code>cargo build --bin ch08_add_value_moved</code></td><td>编译失败：E0382</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_partial_ord.rs" target="_blank"><code>ch08_partial_ord.rs</code></a></td><td><code>cargo run --quiet --bin ch08_partial_ord</code></td><td>运行，首行 <code>Int(2) &lt; Float(0.5): true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_borrow.rs" target="_blank"><code>ch08_borrow.rs</code></a></td><td><code>cargo run --quiet --bin ch08_borrow</code></td><td>运行，首行 <code>Some(1)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_asref.rs" target="_blank"><code>ch08_asref.rs</code></a></td><td><code>cargo run --quiet --bin ch08_asref</code></td><td>运行，首行 <code>PEBBLE</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_dyn_output.rs" target="_blank"><code>ch08_dyn_output.rs</code></a></td><td><code>cargo run --quiet --bin ch08_dyn_output</code></td><td>运行，首行 <code>hello</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_hrtb.rs" target="_blank"><code>ch08_hrtb.rs</code></a></td><td><code>cargo run --quiet --bin ch08_hrtb</code></td><td>运行，首行 <code>3</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch08_try_from.rs" target="_blank"><code>ch08_try_from.rs</code></a></td><td><code>cargo run --quiet --bin ch08_try_from</code></td><td>运行，首行 <code>Ok(Index(7))</code></td></tr></tbody></table></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 加一个 <code>min</code> 内建函数。</strong> 它接受至少一个参数，返回最小的那个；类型不可比较时报错。</p>
参考答案<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>builtin_min</span><span>(</span><span>_</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Output</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>, </span><span>args</span><span>: &amp;[</span><span>Value</span><span>]) -&gt; </span><span>Result</span><span>&lt;</span><span>Value</span><span>, </span><span>RuntimeError</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>best</span><span><span> </span><span>=</span><span> &amp;</span></span><span>args</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>3</div></div><div><span>    </span><span>for</span><span> </span><span>candidate</span><span> </span><span>in</span><span> &amp;</span><span>args</span><span>[</span><span>1</span><span>..] {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> </span><span>compare</span><span>(</span><span>candidate</span><span>, </span><span>best</span><span>, </span><span>span</span><span>)?.</span><span>is_lt</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>best</span><span><span> </span><span>=</span><span> </span></span><span>candidate</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>    </span><span>Ok</span><span>(</span><span>best</span><span>.</span><span>clone</span><span>())</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 <code>BUILTINS</code> 末尾加一项 <code>NativeFunction { name: "min", arity: Arity::AtLeast(1), func: builtin_min }</code>。<code>compare</code> 已经处理了 int/float 混合和 NaN，<code>Ordering::is_lt()</code> 是 <code>== Less</code> 的简写。注意 <code>builtin_names_follow_the_table</code> 会失败，因为它钉死了名字表，要在期望数组末尾加上 <code>"min"</code>。我把它连同下面这个测试一起跑过：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>min_picks_the_smallest</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>buffer</span><span><span> </span><span>=</span><span> </span></span><span>Buffer</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>min</span><span><span> </span><span>=</span><span> </span></span><span>builtin</span><span>(</span><span>"min"</span><span>).</span><span>unwrap</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>args</span><span><span> </span><span>=</span><span> [</span></span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>3</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>1.5</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>2</span><span>)];</span></div></div><div><div><div>6</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>call_native</span><span>(</span><span>min</span><span>, &amp;</span><span>mut</span><span> </span><span>buffer</span><span>, </span><span>span</span><span>(), &amp;</span><span>args</span><span>).</span><span>unwrap</span><span>(), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>1.5</span><span>));</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>call_native</span><span>(</span><span>min</span><span>, &amp;</span><span>mut</span><span> </span><span>buffer</span><span>, </span><span>span</span><span><span>(), &amp;[</span><span>Value</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span><span>), </span><span>Value</span><span>::</span></span><span>string</span><span>(</span><span>"x"</span><span>)]).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>error</span><span>.</span><span>message</span><span>(), </span><span>"cannot compare string and int"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 tag 的 worktree 里 13 个测试通过（原来的 11 个加上它和练习 3 的测试）。</p><p><strong>2. 找出 <code>key_eq</code> 和 <code>key_hash</code> 违反契约的地方。</strong> 提示：<code>Int(1)</code> 和 <code>Float(1.0)</code>。</p>
参考答案<p><code>key_eq</code> 对其他情形一律回落到 <code>self.equals(other)</code>，而 <code>equals</code> 认为 <code>Int(1)</code> 和 <code>Float(1.0)</code> 相等；<code>key_hash</code> 却给 <code>Int</code> 哈希标记 <code>2u8</code>、给 <code>Float</code> 哈希标记 <code>3u8</code>，所以哈希不同。契约要求”相等则哈希相同”，这里被打破了。我在 tag 的 worktree 里用临时测试证实：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>key_eq: true</span></div></div><div><div><div>2</div></div><div><span>len = 2</span></div></div><div><div><div>3</div></div><div><span>get(1) = Some(10)</span></div></div><div><div><div>4</div></div><div><span>get(1.0) = Some(20)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>MapKey(Int(1)) == MapKey(Float(1.0))</code> 说相等，但同一个 <code>HashMap</code> 里两个键共存。这个表现取决于哈希碰撞的运气：恰好落进同一个桶时，行为就取决于实现细节。修复是让键相等也区分这两个变体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>key_eq</span><span>(&amp;</span><span>self</span><span>, </span><span>other</span><span>: &amp;</span><span>Value</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>match</span><span> (</span><span>self</span><span>, </span><span>other</span><span>) {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>a</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>b</span><span>)) =&gt; </span><span>a</span><span>.</span><span>to_bits</span><span><span>() </span><span>==</span><span> </span></span><span>b</span><span>.</span><span>to_bits</span><span>(),</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>(</span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>_</span><span>), </span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>_</span><span><span>)) </span><span>|</span><span> (</span></span><span>Value</span><span>::</span><span>Float</span><span>(</span><span>_</span><span>), </span><span>Value</span><span>::</span><span>Int</span><span>(</span><span>_</span><span>)) =&gt; </span><span>false</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>self</span><span>.</span><span>equals</span><span>(</span><span>other</span><span>),</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>加了这一行以后，11 个原有测试照样通过，<code>key_eq</code> 对这对键返回 <code>false</code>。这个毛刺在 tag 里是存在的，我选择在这里指出来、不去改 tag：本篇的目标是让你的文件和 tag 一致，而你现在已经知道该怎么读它。</p><p><strong>3. 写 <code>Env::depth()</code>：这个作用域和全局作用域之间隔着几层。</strong></p>
参考答案<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// How many scopes lie between this one and the global scope.</span></div></div><div><div><div>2</div></div><div><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>depth</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>match</span><span> &amp;</span><span>self</span><span>.parent {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Some</span><span>(</span><span>parent</span><span>) =&gt; </span><span>1</span><span> + </span><span>parent</span><span>.</span><span>borrow</span><span>().</span><span>depth</span><span>(),</span></div></div><div><div><div>6</div></div><div><span>        </span><span>None</span><span> =&gt; </span><span>0</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>递归地沿着 <code>parent</code> 走，每层加一。<code>parent.borrow()</code> 是只读守卫，递归里全是只读借用，不会冲突。我用这个测试验证过：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>env_depth_counts_parents</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>global</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span><span>(</span><span>RefCell</span><span>::</span></span><span>new</span><span><span>(</span><span>Env</span><span>::</span></span><span>new</span><span>()));</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>inner</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>global</span><span>));</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>innermost</span><span><span> </span><span>=</span><span> </span></span><span>Env</span><span>::</span><span>child</span><span><span>(</span><span>Rc</span><span>::</span></span><span>clone</span><span>(&amp;</span><span>inner</span><span>));</span></div></div><div><div><div>6</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>global</span><span>.</span><span>borrow</span><span>().</span><span>depth</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>innermost</span><span>.</span><span>borrow</span><span>().</span><span>depth</span><span>(), </span><span>2</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下一篇我们终于让这些值动起来：解释器会把 <code>Function</code> 和 <code>Env</code> 接在一起，用闭包实现控制流。而那条一直埋着的引用环，会在一个看起来人畜无害的 <code>fn fact</code> 上咬出第一个真正的内存泄漏。我们会用 <code>Weak</code> 把它抓个现行。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-07-parser-errors-cli/</id>
      <title type="text">Result、? 与递归下降：解析器和第一个命令行</title>
      <published>2026-10-13T06:00:00.000Z</published>
      <updated>2026-10-13T06:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-07-parser-errors-cli/"/>
      <summary type="text">这一篇我们写 pebble-parser：用 Result 和 ? 把错误当数据，用递归下降加优先级爬升把 token 变成 AST，再用 panic 恢复让一个文件的三处错误一次报全。然后做出第一个能跑的 pebble 命令行：clap、anyhow、模块和集成测试。</summary>
      <content type="html"><![CDATA[<p>下面这个三行的文件里有两处错误：第一行 <code>let</code> 后面忘了变量名，第二行末尾忘了分号。大多数我用过的玩具解析器会在第一个错误处停下，让你改完再跑，再看到第二个。Pebble 的解析器一次报完两处，然后把第三行的 <code>let z = 3;</code> 完整地解析出来，一个字都不丢：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let = ;</span></div></div><div><div><div>2</div></div><div><span>let y = 2</span></div></div><div><div><div>3</div></div><div><span>let z = 3;</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- parse broken.pebble</span></div></div><div><div><div>2</div></div><div><span>error[E1001]: expected identifier, found `=`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; broken.pebble:1:5</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>1 | let = ;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|     ^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= note: `let` needs a name</span></div></div><div><div><div>9</div></div><div><span>error[E1001]: expected `;`, found `let`</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>--&gt; broken.pebble:3:1</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>3 | let z = 3;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>| ^^^</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>= note: statements end with `;`</span></div></div><div><div><div>16</div></div><div><span>let z</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>int 3</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<p>这篇的目标就是把上面这个输出做出来。我们要写完 <code>pebble-parser</code>（13 个测试），再搭出 <code>pebble</code> 命令行的第一个版本，它有 <code>tokens</code>、<code>parse</code>、<code>check</code> 三个子命令。顺路要学的 Rust 东西不少：<code>Result</code> 和 <code>?</code>、<code>panic!</code> 与 <code>unwrap</code> 的分寸、<code>todo!()</code>、<code>matches!</code>、<code>mem::discriminant</code>、一个叫 two-phase borrow 的借用细节、clap 的 derive、<code>anyhow</code>、文件模块和 <code>pub(crate)</code>、集成测试。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>做完这一篇，你的 <code>~/pebble</code> 里会有 <code>pebble-parser</code>（完成，和 <code>tutorial-v1</code> 标签逐字节一致）、<code>pebble-cli</code> 的第一个版本（中间状态，后面几篇会逐步长成最终版）、以及 <code>examples/</code> 下的五个 <code>.pebble</code> 示例文件。检查命令是 <code>cargo test -p pebble-parser -p pebble-cli</code>。</p><p>上一篇是 <a href="/posts/rust-by-pebble-06-proc-macros/">过程宏</a>：<code>pebble-ast</code> 已经用 <code>#[derive(Spanned)]</code> 写完了。这一篇的解析器就是 AST 的第一个真正的用户。</p></div></div>
<section><h2>先把输入和输出钉死<a href="#先把输入和输出钉死"><span>#</span></a></h2><p>解析器的函数签名很朴素：token 进去，<code>Program</code> 出来。但 Pebble 的前端有个设计原则叫 <strong>报告所有，打印一个都不要</strong>（第 3 篇的 <code>Diagnostic</code> 就是为它设计的）：每一层都不直接往终端写东西，而是把问题装进 <code>Vec&lt;Diagnostic&gt;</code> 交给上层，最后由命令行统一渲染。所以解析器的返回值是两样东西：</p><ul>
<li>能恢复多少 AST 就给多少；</li>
<li>一份诊断列表。</li>
</ul><p>我们已经有的零件，写解析器时会直接用到：</p><ul>
<li><code>pebble_lexer::tokenize(source, text)</code> 返回 <code>(Vec&lt;Token&gt;, Vec&lt;Diagnostic&gt;)</code>，而且保证最后一个 token 是 <code>Eof</code>。</li>
<li><code>pebble_ast</code> 里的 <code>Program</code>、<code>Stmt</code>、<code>Expr</code>、<code>Ident</code>、三种运算符枚举，每个节点都带一个 <code>span</code>，并且 <code>#[derive(Spanned)]</code> 让 <code>.span()</code> 可用。</li>
<li><code>pebble_diag::Diagnostic</code>，有 <code>error(msg)</code>、<code>.with_code("E1001")</code>、<code>.with_span(span)</code>、<code>.with_note(text)</code> 这套 builder。</li>
</ul><p>先建 crate。在 <code>~/pebble</code> 下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-parser</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把 <code>crates/pebble-parser/Cargo.toml</code> 改成下面这样（四个内部依赖都是我们前几篇写的）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-parser"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Recursive-descent and Pratt parser producing the Pebble AST."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div><span>pebble-lexer.workspace</span><span> = </span><span>true</span></div></div><div><div><div>15</div></div><div><span>pebble-ast.workspace</span><span> = </span><span>true</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>18</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>根目录的 <code>Cargo.toml</code> 还要做两处小改动。<code>[workspace.dependencies]</code> 的内部 crate 区加一行 <code>pebble-parser</code>，后面写命令行要用的两个外部库 <code>anyhow</code> 和 <code>clap</code> 也一并加进去（clap 的 <code>derive</code> 特性让我们能用属性宏描述命令行，下面会讲）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-parser</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-parser"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>anyhow</span><span> = </span><span>"1"</span></div></div><div><div><div>4</div></div><div><span>clap</span><span> = { </span><span>version</span><span> = </span><span>"4"</span><span>, </span><span>features</span><span> = [</span><span>"derive"</span><span>, </span><span>"wrap_help"</span><span>] }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前一行放在 <code>pebble-macros</code> 那一行下面，后两行放在 <code>[workspace.dependencies]</code> 的 external 区域，和 <code>proc-macro2</code>、<code>quote</code>、<code>syn</code> 并列。</p></section>
<section><h2><code>Result</code>：把失败放进返回值里<a href="#result把失败放进返回值里"><span>#</span></a></h2><p>在写第一行解析器代码之前，我得先把 Rust 处理错误的方式讲清楚，因为这个解析器里几乎每个函数的返回类型都是 <code>Result&lt;_, Diagnostic&gt;</code>。</p><p>C 的老办法是返回码加输出参数：<code>int parse_let(Parser *p, Stmt *out)</code>，返回 0 表示成功。毛病大家都知道：返回码可以被忽略，而且出错时 <code>out</code> 里是什么没人保证。C++ 有两条路：异常（<code>throw</code>/<code>catch</code>，错误会无声地穿过中间所有的栈帧）和 C++23 的 <code>std::expected&lt;T, E&gt;</code>。Rust 选的是第二条，而且把它做成了语言的基础设施：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>enum</span><span> </span><span>Result</span><span>&lt;</span><span>T</span><span>, </span><span>E</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Ok</span><span>(</span><span>T</span><span>),</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Err</span><span>(</span><span>E</span><span>),</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>就是一个普通的泛型 <code>enum</code>（泛型第 5 篇讲过，这里只需要知道 <code>T</code> 和 <code>E</code> 是占位的类型）。函数返回 <code>Result&lt;Stmt, Diagnostic&gt;</code>，意思是 “要么给你一个 <code>Stmt</code>，要么给你一个 <code>Diagnostic</code>”。想用里面的 <code>Stmt</code>，你必须先 <code>match</code> 掉 <code>Err</code>（第 3 篇学过的穷尽匹配）。更狠的一点：<code>Result</code> 被标了 <code>#[must_use]</code>，忽略它编译器就警告。我在实验室里验证过（<code>~/rust-lab/src/bin/ch07_must_use.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>parse_digit</span><span>(</span><span>c</span><span>: </span><span>char</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u32</span><span>, </span><span>String</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>c</span><span>.</span><span>to_digit</span><span>(</span><span>10</span><span>).</span><span>ok_or</span><span>(</span><span>format!</span><span>(</span><span>"`{c}` is not a digit"</span><span>))</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>parse_digit</span><span>(</span><span>'x'</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"done"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch07_must_use</span></div></div><div><div><div>2</div></div><div><span>warning: unused `Result` that must be used</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch07_must_use.rs:6:5</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>6 |     parse_digit('x');</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|     ^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= note: this `Result` may be an `Err` variant, which should be handled</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default</span></div></div><div><div><div>10</div></div><div><span>help: use `let _ = ...` to ignore the resulting value</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>6 |     let _ = parse_digit('x');</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|     +++++++</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>warning: `rust-lab` (bin "ch07_must_use") generated 1 warning</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这就是它比 C 的返回码强的地方：忽略错误需要你明确写出来（<code>let _ = ...</code>）。</p><section><h3><code>?</code>：错误向上传，一个字符的事<a href="#错误向上传一个字符的事"><span>#</span></a></h3><p>纯用 <code>match</code> 处理每一个 <code>Result</code> 会把代码淹没在 <code>Err(e) =&gt; return Err(e)</code> 里。<code>?</code> 运算符就是这个模式的缩写：<code>Ok(v)</code> 就取出 <code>v</code> 继续，<code>Err(e)</code> 就立刻从当前函数 <code>return Err(e)</code>。实验室里把两种写法并排放（<code>~/rust-lab/src/bin/ch07_question.rs</code>（节选））：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>with_question</span><span>(</span><span>a</span><span>: &amp;</span><span>str</span><span>, </span><span>b</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>i64</span><span>, </span><span>ParseIntError</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>x</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;()?;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>y</span><span><span> </span><span>=</span><span> </span></span><span>b</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;()?;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Ok</span><span>(</span><span>x</span><span> + </span><span>y</span><span>)</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>by_hand</code> 版本把每个 <code>?</code> 展开成 <code>match { Ok(value) =&gt; value, Err(error) =&gt; return Err(error) }</code>，两个函数在好的和坏的输入上行为一致：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_question</span></div></div><div><div><div>2</div></div><div><span>Ok(3)</span></div></div><div><div><div>3</div></div><div><span>Ok(3)</span></div></div><div><div><div>4</div></div><div><span>Err(ParseIntError { kind: InvalidDigit })</span></div></div><div><div><div>5</div></div><div><span>Err(ParseIntError { kind: InvalidDigit })</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>parse::&lt;i64&gt;()</code> 里的 <code>::&lt;i64&gt;</code> 叫 turbofish，告诉 <code>parse</code> 你要解析成什么类型，第 4 篇见过。）</p><p>严格地说 <code>?</code> 在 <code>Err</code> 分支上还会调用一次 <code>From::from(e)</code>，把错误转换成函数返回类型里声明的错误类型。我们这一篇里错误类型总是同一个，所以转换是恒等的；等到命令行那一节，<code>io::Error</code> 变成 <code>anyhow::Error</code> 就是靠它。</p><p>和 C++ 异常比，这里有一个本质区别：<strong><code>?</code> 是可见的</strong>。你读一个函数，每一处可能失败的调用后面都有一个 <code>?</code>，每一条提前返回的路径都写在代码里。异常是你看不见的 <code>goto</code>。代价是你得老老实实写，没有 “反正上面会 catch” 这回事。我个人觉得解析器是这个设计最适合的场合：每一步都可能失败，而且每一次失败的处理（“记下来，然后恢复”）都是同一个动作。</p></section></section>
<section><h2>搭骨架：游标和 <code>todo!()</code><a href="#搭骨架游标和-todo"><span>#</span></a></h2><p>递归下降解析器的骨架其实就是一个游标（cursor）：一个 <code>Vec&lt;Token&gt;</code> 加一个当前位置，外加几个原语：看当前 token（<code>peek</code>）、看上一个（<code>previous</code>）、前进（<code>advance</code>）、判断类型（<code>check</code>）、有条件前进（<code>matches</code>）、强制要求（<code>expect</code>）。</p><p>先写 <code>crates/pebble-parser/src/lib.rs</code> 的开头：crate 属性和文档，导入，结果类型，以及入口函数 <code>parse</code>。</p><p>第 1 行是 crate 属性（和第 2 篇一样，禁止 <code>unsafe</code>）；第 2 到 13 行是一段 <code>//!</code> 的 crate 文档，说明这个解析器是递归下降加优先级爬升、错误都是值、出错会恢复。那段文字我不在文章里重复了（里面有两个我没改的破折号，要逐字节一致的话请从 <code>~/pebble-tag</code> 里原样复制；这是 tag 的本地克隆，没建的话命令在下面复制那一节），然后是导入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div><span>//! The Pebble parser: tokens in, [`Program`] out.</span></div></div><div><div><div>3</div></div><div><span>//! ... (lines 3 to 13: copy them from the reference)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::{</span></span><span>BinaryOp</span><span>, </span><span>Expr</span><span>, </span><span>Ident</span><span>, </span><span>LogicalOp</span><span>, </span><span>Program</span><span>, </span><span>Spanned</span><span> </span><span>as</span><span> _, </span><span>Stmt</span><span>, </span><span>UnaryOp</span><span>};</span></div></div><div><div><div>6</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::</span></span><span>Diagnostic</span><span>;</span></div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>pebble_lexer</span><span>::{</span></span><span>Token</span><span>, </span><span>TokenKind</span><span>, tokenize};</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceId</span><span>, </span><span>Span</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>use pebble_ast::{... Spanned as _ ...}</code> 里的 <code>Spanned as _</code> 只把 trait 的方法带进作用域（后面要对 <code>Expr</code> 调用 <code>.span()</code>），不引入它的名字。现在先把这些导入都写上，编译时会有几条 “unused import” 的警告，等后面的函数写出来就消失了。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The result of parsing a source file.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>ParseResult</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Whatever AST could be recovered.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>program</span><span>: </span><span>Program</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Lex and parse diagnostics, in source order as best we can manage.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>diagnostics</span><span>: </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>impl</span><span> </span><span>ParseResult</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// Whether parsing produced no diagnostics at all.</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>is_ok</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span>.diagnostics.</span><span>is_empty</span><span>()</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>/// Parses a whole source file. Never fails; inspect [`ParseResult::diagnostics`].</span></div></div><div><div><div>19</div></div><div><span>#[must_use]</span></div></div><div><div><div>20</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>parse</span><span>(</span><span>source</span><span>: </span><span>SourceId</span><span>, </span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>ParseResult</span><span> {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>mut</span><span> </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>source</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>parser</span><span><span> </span><span>=</span><span> </span></span><span>Parser</span><span>::</span><span>new</span><span>(</span><span>tokens</span><span>);</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>parser</span><span>.</span><span>parse_program</span><span>();</span></div></div><div><div><div>24</div></div><div><span>    </span><span>diagnostics</span><span>.</span><span>append</span><span>(&amp;</span><span>mut</span><span> </span><span>parser</span><span>.errors);</span></div></div><div><div><div>25</div></div><div><span>    </span><span>ParseResult</span><span> {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>program</span><span>,</span></div></div><div><div><div>27</div></div><div><span>        </span><span>diagnostics</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>ParseResult</code> 的字段是 <code>pub</code> 的：它是个纯粹装数据的结构体，没有什么要保护的不变量。<code>parse</code> 本身永远不会失败，签名里没有 <code>Result</code>：所有问题都装进 <code>diagnostics</code>。注意 <code>diagnostics.append(&amp;mut parser.errors)</code>：<code>append</code> 把另一个 <code>Vec</code> 的内容整个搬过来并清空它，所以它要 <code>&amp;mut</code>。词法错误在前，解析错误在后，用户看到的顺序就是这个。</p><p>接着是 <code>Parser</code> 本身。这里的 <code>'src</code> 就是第 4 篇的那个生命周期：token 借用着源码字符串，所以整个解析器不能比源码活得更久。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The recursive-descent parser state.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Parser</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>tokens</span><span>: </span><span>Vec</span><span>&lt;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt;&gt;,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>current</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>errors</span><span>: </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>放在 <code>parse</code> 函数下面，然后是 <code>impl</code> 块的开头：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span>&lt;'</span><span>src</span><span>&gt; </span><span>Parser</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Wraps a token stream.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>tokens</span><span>: </span><span>Vec</span><span>&lt;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt;&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>tokens</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>current</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>8</div></div><div><span>            </span><span>errors</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个 <code>impl</code> 块还没有闭合，后面所有的方法都往里面加。游标原语如下，我分开讲：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- token cursor -----------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>peek</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>// The lexer guarantees a trailing `Eof`, so indexing is always valid.</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.tokens[</span><span>self</span><span>.current.</span><span>min</span><span>(</span><span>self</span><span>.tokens.</span><span>len</span><span>() - </span><span>1</span><span>)]</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>previous</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.tokens[</span><span>self</span><span>.current.</span><span>saturating_sub</span><span>(</span><span>1</span><span>)]</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>is_at_end</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span>.</span><span>peek</span><span><span>().kind </span><span>==</span><span> </span></span><span>TokenKind</span><span>::</span><span>Eof</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>fn</span><span> </span><span>check</span><span>(&amp;</span><span>self</span><span>, </span><span>kind</span><span>: &amp;</span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt;) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>core</span><span>::</span><span>mem</span><span>::</span><span>discriminant</span><span>(&amp;</span><span>self</span><span>.</span><span>peek</span><span><span>().kind) </span><span>==</span><span> </span><span>core</span><span>::</span><span>mem</span><span>::</span></span><span>discriminant</span><span>(</span><span>kind</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>    </span><span>fn</span><span> </span><span>advance</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>token</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>().</span><span>clone</span><span>();</span></div></div><div><div><div>22</div></div><div><span>        </span><span>if</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>is_at_end</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>self</span><span><span>.current </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>        </span><span>token</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个值得停一下的点：</p><ul>
<li><code>peek</code> 返回 <code>&amp;Token&lt;'src&gt;</code>，一个借用，不是拷贝。<code>self.current.min(self.tokens.len() - 1)</code> 的注释说 “词法分析器保证末尾有 <code>Eof</code>，所以下标永远有效”。这是一个<strong>不变量</strong>：如果有人给 <code>Parser::new</code> 传了空的 <code>Vec</code>，这行代码会 panic。这是 bug，不是用户输入错误，所以用 panic 而不是 <code>Result</code>；本章后面会再谈这个界线。</li>
<li><code>check</code> 比较的是 <code>mem::discriminant</code>，也就是 “这是 <code>TokenKind</code> 的哪个变体”，不看变体里的数据。为什么要这样？因为我们想问 “当前 token 是不是一个标识符”，却不关心它叫什么名字，而 <code>==</code> 会把 <code>Ident("total")</code> 和 <code>Ident("")</code> 判成不相等。实验室里验证（<code>~/rust-lab/src/bin/ch07_discriminant.rs</code>（节选））：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>wanted</span><span><span> </span><span>=</span><span> </span></span><span>Tok</span><span>::</span><span>Ident</span><span>(</span><span>""</span><span>);</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>found</span><span><span> </span><span>=</span><span> </span></span><span>Tok</span><span>::</span><span>Ident</span><span>(</span><span>"total"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"== : {}"</span><span>, </span><span>wanted</span><span><span> </span><span>==</span><span> </span></span><span>found</span><span>);</span></div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"discriminant ==: {}"</span><span>, </span><span>discriminant</span><span>(&amp;</span><span>wanted</span><span><span>) </span><span>==</span><span> </span></span><span>discriminant</span><span>(&amp;</span><span>found</span><span>));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_discriminant</span></div></div><div><div><div>2</div></div><div><span>== : false</span></div></div><div><div><div>3</div></div><div><span>discriminant ==: true</span></div></div><div><div><div>4</div></div><div><span>different variant: false</span></div></div><div><div><div>5</div></div><div><span>Semi</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C 程序员可以把 <code>discriminant</code> 理解成 tagged union 的那个 tag 字段，只是 Rust 不让你直接摸它，要通过这个函数拿到一个只能比较相等的不透明值。所以后面你会看到 <code>self.expect(&amp;TokenKind::Ident(""), ...)</code>：那个空字符串只是个占位，<code>check</code> 不看它。</p><ul>
<li><code>advance</code> 返回 token 的<strong>克隆</strong>。这有点浪费（<code>Str</code> token 里有个 <code>Cow</code>，克隆时可能分配），但它让后面的代码简单很多：拿到手的 token 和游标没有借用关系，我们可以边用它边继续前进。我没有量过这笔开销在 Pebble 的规模上值不值得优化，我猜不值得。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>matches</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>kind</span><span>: &amp;</span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt;) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>check</span><span>(</span><span>kind</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>self</span><span>.</span><span>advance</span><span>();</span></div></div><div><div><div>4</div></div><div><span>            </span><span>true</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>false</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>expect</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>kind</span><span>: &amp;</span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt;, </span><span>context</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt;, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>check</span><span>(</span><span>kind</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>self</span><span>.</span><span>advance</span><span>());</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>found</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>();</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Err</span><span>(</span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>format!</span><span>(</span></div></div><div><div><div>16</div></div><div><span>            </span><span>"expected {}, found {}"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>            </span><span>kind</span><span>.</span><span>describe</span><span>(),</span></div></div><div><div><div>18</div></div><div><span>            </span><span>found</span><span>.kind</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>))</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>.</span><span>with_code</span><span>(</span><span>"E1001"</span><span>)</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>.</span><span>with_span</span><span>(</span><span>found</span><span>.span)</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>.</span><span>with_note</span><span>(</span><span>context</span><span>.</span><span>to_string</span><span>()))</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span>fn</span><span> </span><span>error</span><span>&lt;</span><span>T</span><span>&gt;(&amp;</span><span>self</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>, </span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Result</span><span>&lt;</span><span>T</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>Err</span><span>(</span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>message</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>.</span><span>with_code</span><span>(</span><span>"E1001"</span><span>)</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>.</span><span>with_span</span><span>(</span><span>span</span><span>))</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>expect</code> 是整个解析器里最重要的一个函数：它不只是 “检查并前进”，它还负责生成错误消息。<code>context</code> 参数是给人看的提示，比如 <code>"`let` needs an initialiser"</code>，它会变成诊断里的 <code>= note:</code> 一行。你会发现用户看到的所有 “帮助性” 文字，都来自调用 <code>expect</code> 时传的这个字符串。</p><p>最后一个，<code>error&lt;T&gt;</code>，值得单独说。它的返回类型是 <code>Result&lt;T, Diagnostic&gt;</code>，<strong>函数体里却永远返回 <code>Err</code></strong>。<code>T</code> 是什么？调用的地方决定。<code>return self.error(expr.span(), "invalid assignment target")</code> 出现在返回 <code>Result&lt;Expr, _&gt;</code> 的函数里，<code>T</code> 就被推导成 <code>Expr</code>；出现在返回 <code>Result&lt;Stmt, _&gt;</code> 的函数里，<code>T</code> 就是 <code>Stmt</code>。这是泛型函数而且 <code>T</code> 只出现在返回位置：C++ 里你得写模板参数 <code>error&lt;Expr&gt;(...)</code>，因为 C++ 不会根据期望的返回类型反推模板参数；Rust 会。</p><p>然后是主循环 <code>parse_program</code>：只要没到文件末尾，就解析一个声明；成功就收集，失败就把诊断记下来，调用 <code>synchronize</code> 恢复，然后继续。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- statements -------------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>/// Parses statements until end of input, recovering from errors.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>parse_program</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Program</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>stmts</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>6</div></div><div><span>        </span><span>while</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>is_at_end</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span>.</span><span>declaration</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>Ok</span><span>(</span><span>stmt</span><span>) =&gt; </span><span>stmts</span><span>.</span><span>push</span><span>(</span><span>stmt</span><span>),</span></div></div><div><div><div>9</div></div><div><span>                </span><span>Err</span><span>(</span><span>diagnostic</span><span>) =&gt; {</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>self</span><span>.errors.</span><span>push</span><span>(</span><span>diagnostic</span><span>);</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>self</span><span>.</span><span>synchronize</span><span>();</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Program</span><span>::</span><span>new</span><span>(</span><span>stmts</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我们把 <code>synchronize</code> 一起写掉（它也是在 <code>impl</code> 里，放在最后），这样骨架才能编译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- helpers ----------------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>/// Skips tokens until we are probably at the start of the next statement.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>synchronize</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>while</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>is_at_end</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.</span><span>previous</span><span><span>().kind </span><span>==</span><span> </span></span><span>TokenKind</span><span>::</span><span>Semicolon</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>return</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span>.</span><span>peek</span><span>().kind {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Fn</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Let</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>If</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>While</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>For</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Return</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Break</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>Continue</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>|</span><span> </span><span>TokenKind</span><span>::</span><span>RBrace</span><span> =&gt; </span><span>return</span><span>,</span></div></div><div><div><div>19</div></div><div><span>                </span><span>_</span><span> =&gt; {</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>self</span><span>.</span><span>advance</span><span>();</span></div></div><div><div><div>21</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>synchronize</code> 的作用本篇后面详细讲。现在先留着，往 <code>impl</code> 块里加上三个<strong>占位函数</strong>，让它能编译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>declaration</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Stmt</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>self</span><span>.</span><span>statement</span><span>()</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>statement</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Stmt</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>todo!</span><span>(</span><span>"statements"</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>/// Parses a full expression, including assignment.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>expression</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Expr</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>todo!</span><span>(</span><span>"expressions"</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（要放在 <code>parse_program</code> 和 <code>// --- helpers</code> 之间，注意不要把 <code>synchronize</code> 那段挤到 <code>impl</code> 外面去。）</p><p><code>todo!()</code> 是标准库的宏，作用是 “这里还没写，编译器别管，运行到了就 panic”。它能出现在任何需要某个类型的地方，因为它的类型是 <code>!</code>（永不返回），可以变成任何类型：这是 Rust 里 “表达式有类型” 的一个直接后果，<code>return</code>、<code>break</code>、<code>panic!</code> 也是 <code>!</code> 类型。C 里你大概会写 <code>assert(0 &amp;&amp; "not implemented")</code>，它在发布构建里会消失，然后静悄悄地返回垃圾；<code>todo!()</code> 不会。</p><p>给骨架一个测试，看看它真的会 panic：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>first_light</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"1;"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert!</span><span>(</span><span>result</span><span>.</span><span>is_ok</span><span>());</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-parser</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>running 1 test</span></div></div><div><div><div>4</div></div><div><span>test tests::first_light ... FAILED</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>failures:</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>---- tests::first_light stdout ----</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>thread 'tests::first_light' (61762) panicked at crates/pebble-parser/src/lib.rs:147:9:</span></div></div><div><div><div>11</div></div><div><span>not yet implemented: statements</span></div></div><div><div><div>12</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>failures:</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>tests::first_light</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>error: test failed, to rerun pass `-p pebble-parser --lib`</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（<code>...</code> 是我删掉的编译警告，有 “未使用的导入” 和 “从未使用的方法”。）<code>panicked at</code> 之后是我们的 <code>todo!</code> 消息。进程的 panic 默认会展开（unwind）栈并让测试线程退出；<code>cargo test</code> 把它报告成一个失败的测试。如果是一个普通的可执行文件，panic 之后的退出码是 101，实验室里我们见过（下面命令行那一节会再见到）。</p><section><h3>panic、<code>unwrap</code>、<code>expect</code>：什么时候可以用<a href="#panicunwrapexpect什么时候可以用"><span>#</span></a></h3><p>这里是个合适的地方说清楚界线，因为之后你会在代码里到处看到 <code>.unwrap()</code> 和 <code>.expect("...")</code>。</p><p><code>Option::unwrap()</code>、<code>Result::unwrap()</code> 的意思是 “是 <code>Some</code>/<code>Ok</code> 就取出来，否则 panic”；<code>expect("理由")</code> 是同一个东西，只是 panic 消息里带着你写的理由。我的经验规则只有一条：</p><ul>
<li><strong>用户可以触发的失败，用 <code>Result</code>。</strong> 文件不存在、源码里有语法错误、网络断开。这些都不是程序的 bug。</li>
<li><strong>只有程序写错了才会发生的失败，才可以 panic。</strong> 违反了自己的不变量，例如 “词法分析器保证最后一个 token 是 <code>Eof</code>”。</li>
<li>测试里 <code>unwrap</code>/<code>expect</code> 随便用：测试失败本来就应该大声喊出来。</li>
</ul><p>下面是实验室里一个 “不该 unwrap” 的例子：13 行的命令行解析，用户给了一个不存在的文件，程序直接 panic：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_args -- count nope.pebble; echo $?</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>thread 'main' (80062) panicked at src/bin/ch07_args.rs:11:45:</span></div></div><div><div><div>4</div></div><div><span>called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }</span></div></div><div><div><div>5</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>6</div></div><div><span>101</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出里的 <code>Os { code: 2, kind: NotFound, ... }</code> 是个 Debug 打印，用户看到的是栈上的一团结构，不是一句人话。这个问题我们到命令行那一节解决。</p></section></section>
<section><h2>表达式：优先级爬升<a href="#表达式优先级爬升"><span>#</span></a></h2><p>先说算法，再看代码，因为这段代码很短，但初见时很难看出它为什么对。</p><p>问题是：<code>1 + 2 * 3</code> 必须分成 <code>1 + (2 * 3)</code>，<code>1 - 2 - 3</code> 必须分成 <code>(1 - 2) - 3</code>。教科书的办法是给每一级优先级写一个函数（<code>term</code>、<code>factor</code>、<code>comparison</code>……），一共七八层。Pebble 用的办法叫<strong>优先级爬升</strong>（precedence climbing）：只写一个 <code>binary(min_prec)</code> 函数，用一张表告诉它每个运算符有多 “紧”。Pratt 解析器是这一家的近亲，我猜如果你读过 Pratt 的论文，会发现它的主循环和下面这个几乎同形，只是 Pratt 把前缀和后缀运算符也放进了同一个循环，而 Pebble 把它们留给 <code>unary</code> 和 <code>call</code> 两个单独的函数，所以我更愿意老实叫它优先级爬升。</p><p>表是这样的，数字越大越紧：</p>

<table><thead><tr><th>优先级</th><th>运算符</th></tr></thead><tbody><tr><td>1</td><td><code>or</code></td></tr><tr><td>2</td><td><code>and</code></td></tr><tr><td>3</td><td><code>==</code> <code>!=</code></td></tr><tr><td>4</td><td><code>&lt;</code> <code>&lt;=</code> <code>&gt;</code> <code>&gt;=</code></td></tr><tr><td>5</td><td><code>+</code> <code>-</code></td></tr><tr><td>6</td><td><code>*</code> <code>/</code> <code>%</code></td></tr></tbody></table><p>整个解析器处理表达式的调用层次，从外到内：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>右操作数: binary(prec + 1)</p></span></div><div><span></span></div><div><span></span></div><div><span><p>右结合: assignment</p></span></div><div><span></span></div><div><span><p>expression</p></span></div><div><span><p>assignment</p></span></div><div><span><p>binary(min_prec)</p></span></div><div><span><p>unary</p></span></div><div><span><p>call</p></span></div><div><span><p>primary</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>右操作数: binary(prec + 1)</p></span></div><div><span></span></div><div><span></span></div><div><span><p>右结合: assignment</p></span></div><div><span></span></div><div><span><p>expression</p></span></div><div><span><p>assignment</p></span></div><div><span><p>binary(min_prec)</p></span></div><div><span><p>unary</p></span></div><div><span><p>call</p></span></div><div><span><p>primary</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div>
</div></div></div><p><code>binary(min_prec)</code> 的逻辑是：先读一个操作数 <code>left</code>；然后只要下一个 token 是中缀运算符并且它的优先级 <code>&gt;= min_prec</code>，就吃掉它，递归读右边（右边只允许 <code>prec + 1</code> 或更高的运算符），把两边拼成一个节点作为新的 <code>left</code>，继续循环。</p><p>手工跑一下 <code>1 + 2 * 3</code>：</p><ol>
<li><code>binary(0)</code>：<code>left = 1</code>；看到 <code>+</code>（5），<code>5 &gt;= 0</code>，吃掉；右边 <code>binary(6)</code>：</li>
<li><code>binary(6)</code>：<code>left = 2</code>；看到 <code>*</code>（6），<code>6 &gt;= 6</code>，吃掉；右边 <code>binary(7)</code> 读到 <code>3</code>，没有更多运算符，返回 <code>3</code>；<code>left = 2 * 3</code>，返回。</li>
<li>回到第 1 步：<code>left = 1 + (2 * 3)</code>。</li>
</ol><p>再跑 <code>1 - 2 - 3</code>：<code>binary(0)</code> 里 <code>left = 1</code>，吃掉第一个 <code>-</code>（5），右边 <code>binary(6)</code>：读到 <code>2</code>，看到第二个 <code>-</code>（5），<code>5 &lt; 6</code>，<strong>不吃</strong>，返回 <code>2</code>。所以 <code>left = (1 - 2)</code>，循环回头，吃掉第二个 <code>-</code>，得到 <code>((1 - 2) - 3)</code>。左结合的秘密就是那个 <code>prec + 1</code>：右边只允许更紧的运算符，同级的留给外层循环。</p><p>我不想只讲不证。把 <code>prec + 1</code> 改成 <code>prec</code>，跑 <code>subtraction_is_left_associative</code> 测试：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-parser subtraction</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>running 1 test</span></div></div><div><div><div>4</div></div><div><span>test tests::subtraction_is_left_associative ... FAILED</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>failures:</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>---- tests::subtraction_is_left_associative stdout ----</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>thread 'tests::subtraction_is_left_associative' (73307) panicked at crates/pebble-parser/src/lib.rs:656:9:</span></div></div><div><div><div>11</div></div><div><span>assertion `left == right` failed</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>left: "expr\n  binary -\n    int 1\n    binary -\n      int 2\n      int 3\n"</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>right: "expr\n  binary -\n    binary -\n      int 1\n      int 2\n    int 3\n"</span></div></div><div><div><div>14</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>failures:</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>tests::subtraction_is_left_associative</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>error: test failed, to rerun pass `-p pebble-parser --lib`</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>left</code> 是 <code>1 - (2 - 3)</code>：右结合了。改一个字符，一个测试精准地告诉你错在哪里。</p><section><h3>写代码<a href="#写代码"><span>#</span></a></h3><p>先是内部用的小枚举 <code>Infix</code>。AST 把 “算术/比较” 和 “逻辑” 分成了 <code>Binary</code> 和 <code>Logical</code> 两个节点（因为 <code>and</code>/<code>or</code> 要短路，求值器处理它们的方式不一样），而 <code>binary</code> 函数想用同一个循环处理两种，所以需要一个标记告诉它这次读到的是哪一种。放在 <code>pub struct Parser</code> 之前：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Internal marker for the operators handled by the expression ladder.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>enum</span><span> </span><span>Infix</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Arithmetic</span><span>(</span><span>BinaryOp</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Logic</span><span>(</span><span>LogicalOp</span><span>),</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个枚举是私有的，也不需要对外导出。<code>#[derive(Copy, Clone)]</code>：每个变体里装的 <code>BinaryOp</code>/<code>LogicalOp</code> 本身就是 <code>Copy</code>（第 5 篇的 AST 里就派生好了），所以这个标记也可以被按位复制。</p><p>接着是 <code>infix_op</code>：把 token 类型映射成 <code>(优先级, 含义)</code>，不是中缀运算符就返回 <code>None</code>。放在 <code>impl</code> 块之后，作为独立函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Infix operators mapped to `(precedence, meaning)`. Higher binds tighter.</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>infix_op</span><span>(</span><span>kind</span><span>: &amp;</span><span>TokenKind</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Option</span><span>&lt;(</span><span>u8</span><span>, </span><span>Infix</span><span>)&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Some</span><span>(</span><span>match</span><span> </span><span>kind</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Or</span><span> =&gt; (</span><span>1</span><span>, </span><span>Infix</span><span>::</span><span>Logic</span><span><span>(</span><span>LogicalOp</span><span>::</span></span><span>Or</span><span>)),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>And</span><span> =&gt; (</span><span>2</span><span>, </span><span>Infix</span><span>::</span><span>Logic</span><span><span>(</span><span>LogicalOp</span><span>::</span></span><span>And</span><span>)),</span></div></div><div><div><div>6</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Eq</span><span> =&gt; (</span><span>3</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Eq</span><span>)),</span></div></div><div><div><div>7</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>NotEq</span><span> =&gt; (</span><span>3</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>NotEq</span><span>)),</span></div></div><div><div><div>8</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Lt</span><span> =&gt; (</span><span>4</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Lt</span><span>)),</span></div></div><div><div><div>9</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>LtEq</span><span> =&gt; (</span><span>4</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>LtEq</span><span>)),</span></div></div><div><div><div>10</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Gt</span><span> =&gt; (</span><span>4</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Gt</span><span>)),</span></div></div><div><div><div>11</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>GtEq</span><span> =&gt; (</span><span>4</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>GtEq</span><span>)),</span></div></div><div><div><div>12</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Plus</span><span> =&gt; (</span><span>5</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Add</span><span>)),</span></div></div><div><div><div>13</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Minus</span><span> =&gt; (</span><span>5</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Sub</span><span>)),</span></div></div><div><div><div>14</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Star</span><span> =&gt; (</span><span>6</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Mul</span><span>)),</span></div></div><div><div><div>15</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Slash</span><span> =&gt; (</span><span>6</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Div</span><span>)),</span></div></div><div><div><div>16</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>Percent</span><span> =&gt; (</span><span>6</span><span>, </span><span>Infix</span><span>::</span><span>Arithmetic</span><span><span>(</span><span>BinaryOp</span><span>::</span></span><span>Rem</span><span>)),</span></div></div><div><div><div>17</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>return</span><span> </span><span>None</span><span>,</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>整个函数体是一个 <code>Some(match kind { ... })</code>。最后一个分支 <code>_ =&gt; return None</code> 里的 <code>return</code> 本身是表达式，类型是 <code>!</code>，可以塞进 <code>match</code> 的任何一个分支里，不影响其他分支的类型是 <code>(u8, Infix)</code>。你可以想一下 C 里做同样的事要写什么：一个 <code>switch</code> 加一堆 <code>*out_prec = ...; return true;</code>。</p><p><code>binary</code> 本身：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Precedence climbing over the infix operators.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// `min_prec` is the level we are allowed to consume; higher binds tighter.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>binary</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>min_prec</span><span>: </span><span>u8</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Expr</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>left</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>unary</span><span>()?;</span></div></div><div><div><div>6</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>((</span><span>prec</span><span>, </span><span>infix</span><span><span>)) </span><span>=</span><span> </span></span><span>infix_op</span><span>(&amp;</span><span>self</span><span>.</span><span>peek</span><span>().kind) {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>if</span><span> </span><span>prec</span><span><span> </span><span>&lt;</span><span> </span></span><span>min_prec</span><span> {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>break</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>            </span><span>self</span><span>.</span><span>advance</span><span>();</span></div></div><div><div><div>11</div></div><div><span>            </span><span>// Left-associative: recurse one level higher.</span></div></div><div><div><div>12</div></div><div><span>            </span><span>let</span><span> </span><span>right</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>binary</span><span>(</span><span>prec</span><span> + </span><span>1</span><span>)?;</span></div></div><div><div><div>13</div></div><div><span>            </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>left</span><span>.</span><span>span</span><span>().</span><span>merge</span><span>(</span><span>right</span><span>.</span><span>span</span><span>());</span></div></div><div><div><div>14</div></div><div><span>            </span><span>left</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>infix</span><span> {</span></div></div><div><div><div>15</div></div><div><span>                </span><span>Infix</span><span>::</span><span>Arithmetic</span><span>(</span><span>op</span><span>) =&gt; </span><span>Expr</span><span>::</span><span>Binary</span><span> {</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>op</span><span>,</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>lhs</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>left</span><span>),</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>rhs</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>right</span><span>),</span></div></div><div><div><div>19</div></div><div><span>                    </span><span>span</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>},</span></div></div><div><div><div>21</div></div><div><span>                </span><span>Infix</span><span>::</span><span>Logic</span><span>(</span><span>op</span><span>) =&gt; </span><span>Expr</span><span>::</span><span>Logical</span><span> {</span></div></div><div><div><div>22</div></div><div><span>                    </span><span>op</span><span>,</span></div></div><div><div><div>23</div></div><div><span>                    </span><span>lhs</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>left</span><span>),</span></div></div><div><div><div>24</div></div><div><span>                    </span><span>rhs</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>right</span><span>),</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>span</span><span>,</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>},</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>        </span><span>Ok</span><span>(</span><span>left</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>有三件事值得慢一点看。</p><p><strong>第一，<code>while let Some((prec, infix)) = infix_op(&amp;self.peek().kind)</code>。</strong> 这里把一个借用交给了 <code>infix_op</code>（<code>&amp;self.peek().kind</code> 借用了 <code>self</code>），但是 <code>infix_op</code> 返回的 <code>(u8, Infix)</code> 里没有任何引用，所以借用在这条语句结束时就终止了。因此循环体里可以放心地调用 <code>self.advance()</code>（需要 <code>&amp;mut self</code>）。如果 <code>infix_op</code> 返回的是一个指向 token 内部的引用，这段代码就过不了借用检查。借用在这条语句结束时就终止，是因为返回值里没有引用。</p><p><strong>第二，<code>left = match infix { ... Box::new(left) ... }</code>。</strong> 在赋值语句的右边，我们把 <code>left</code> <strong>移动</strong>进了 <code>Box</code>，然后马上把新节点赋值回 <code>left</code>。移动之后的 <code>left</code> 是未初始化状态，但赋值给它就重新初始化了，编译器看得懂这件事。C++ 里同样的逻辑你会写 <code>left = std::make_unique&lt;Binary&gt;(std::move(left), std::move(right))</code>，Rust 里没有 <code>std::move</code> 这个动词，因为移动是默认行为。我在实验室里用一个玩具枚举验证过这个模式：</p><p><code>~/rust-lab/src/bin/ch07_move_reassign.rs</code>（开头那行 <code>#![allow(dead_code)]</code> 只是为了压掉 “字段从未被读取” 的警告）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![allow(dead_code)]</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>enum</span><span> </span><span>E</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Add</span><span>(</span><span>Box</span><span>&lt;</span><span>E</span><span>&gt;, </span><span>Box</span><span>&lt;</span><span>E</span><span>&gt;),</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>left</span><span><span> </span><span>=</span><span> </span></span><span>E</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> </span><span>n</span><span> </span><span>in</span><span> </span><span>2</span><span>..=</span><span>4</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>right</span><span><span> </span><span>=</span><span> </span></span><span>E</span><span>::</span><span>Int</span><span>(</span><span>n</span><span>);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>// `left` moves into the Box, then the assignment gives it a new value.</span></div></div><div><div><div>13</div></div><div><span>        </span><span>left</span><span><span> </span><span>=</span><span> </span></span><span>E</span><span>::</span><span>Add</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>left</span><span><span>), </span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>right</span><span>));</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{left:?}"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_move_reassign</span></div></div><div><div><div>2</div></div><div><span>Add(Add(Add(Int(1), Int(2)), Int(3)), Int(4))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而如果你把节点存到一个新变量里，却还想用旧的 <code>left</code>，编译器就不答应了（第 2 篇的 E0382）：</p><p><code>~/rust-lab/src/bin/ch07_move_fail.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![allow(dead_code)]</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>enum</span><span> </span><span>E</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Add</span><span>(</span><span>Box</span><span>&lt;</span><span>E</span><span>&gt;, </span><span>Box</span><span>&lt;</span><span>E</span><span>&gt;),</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>left</span><span><span> </span><span>=</span><span> </span></span><span>E</span><span>::</span><span>Int</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>right</span><span><span> </span><span>=</span><span> </span></span><span>E</span><span>::</span><span>Int</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>sum</span><span><span> </span><span>=</span><span> </span></span><span>E</span><span>::</span><span>Add</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>left</span><span><span>), </span><span>Box</span><span>::</span></span><span>new</span><span>(</span><span>right</span><span>));</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{sum:?} from {left:?}"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch07_move_fail</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0382]: borrow of moved value: `left`</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch07_move_fail.rs:12:29</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>9 |     let left = E::Int(1);</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|         ---- move occurs because `left` has type `E`, which does not implement the `Copy` trait</span></div></div><div><div><div>8</div></div><div><span>10 |     let right = E::Int(2);</span></div></div><div><div><div>9</div></div><div><span>11 |     let sum = E::Add(Box::new(left), Box::new(right));</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|                               ---- value moved here</span></div></div><div><div><div>11</div></div><div><span>12 |     println!("{sum:?} from {left:?}");</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|                             ^^^^ value borrowed here after move</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>note: if `E` implemented `Clone`, you could clone the value</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch07_move_fail.rs:3:1</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>3 | enum E {</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>| ^^^^^^ consider implementing `Clone` for this type</span></div></div><div><div><div>19</div></div><div><span>...</span></div></div><div><div><div>20</div></div><div><span>11 |     let sum = E::Add(Box::new(left), Box::new(right));</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>|                               ---- you could clone this value</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>For more information about this error, try `rustc --explain E0382`.</span></div></div><div><div><div>24</div></div><div><span>error: could not compile `rust-lab` (bin "ch07_move_fail") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>第三，<code>Box::new(left)</code> 是第 5 篇的内容：</strong> 递归的枚举必须在堆上存放子节点。</p><p>下面几个函数是表达式的下层：<code>unary</code>（前缀 <code>-</code> 和 <code>not</code>，右递归）、<code>call</code>（函数调用和下标，<code>loop</code> 里不停吃 <code>(...)</code> 和 <code>[...]</code>，所以 <code>f(1)(2)[0]</code> 都能解析）、<code>primary</code>（字面量、变量、括号、列表、map、匿名函数）。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>unary</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Expr</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Minus</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>previous</span><span>().span;</span></div></div><div><div><div>4</div></div><div><span>            </span><span>let</span><span> </span><span>operand</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>unary</span><span>()?;</span></div></div><div><div><div>5</div></div><div><span>            </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>start</span><span>.</span><span>merge</span><span>(</span><span>operand</span><span>.</span><span>span</span><span>());</span></div></div><div><div><div>6</div></div><div><span>            </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Unary</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>op</span><span>: </span><span>UnaryOp</span><span>::</span><span>Neg</span><span>,</span></div></div><div><div><div>8</div></div><div><span>                </span><span>operand</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>operand</span><span>),</span></div></div><div><div><div>9</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Not</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>previous</span><span>().span;</span></div></div><div><div><div>14</div></div><div><span>            </span><span>let</span><span> </span><span>operand</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>unary</span><span>()?;</span></div></div><div><div><div>15</div></div><div><span>            </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>start</span><span>.</span><span>merge</span><span>(</span><span>operand</span><span>.</span><span>span</span><span>());</span></div></div><div><div><div>16</div></div><div><span>            </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Unary</span><span> {</span></div></div><div><div><div>17</div></div><div><span>                </span><span>op</span><span>: </span><span>UnaryOp</span><span>::</span><span>Not</span><span>,</span></div></div><div><div><div>18</div></div><div><span>                </span><span>operand</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>operand</span><span>),</span></div></div><div><div><div>19</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>self</span><span>.</span><span>call</span><span>()</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>    </span><span>fn</span><span> </span><span>call</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Expr</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>expr</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>primary</span><span>()?;</span></div></div><div><div><div>27</div></div><div><span>        </span><span>loop</span><span> {</span></div></div><div><div><div>28</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>LParen</span><span>) {</span></div></div><div><div><div>29</div></div><div><span>                </span><span>let</span><span> </span><span>mut</span><span> </span><span>args</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>30</div></div><div><span>                </span><span>if</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>check</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>RParen</span><span>) {</span></div></div><div><div><div>31</div></div><div><span>                    </span><span>loop</span><span> {</span></div></div><div><div><div>32</div></div><div><span>                        </span><span>args</span><span>.</span><span>push</span><span>(</span><span>self</span><span>.</span><span>expression</span><span>()?);</span></div></div><div><div><div>33</div></div><div><span>                        </span><span>if</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Comma</span><span>) {</span></div></div><div><div><div>34</div></div><div><span>                            </span><span>break</span><span>;</span></div></div><div><div><div>35</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>                </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span></div></div><div><div><div>39</div></div><div><span><span>                    </span></span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>RParen</span><span>, </span><span>"unclosed argument list"</span><span>)?</span></div></div><div><div><div>40</div></div><div><span><span>                    </span></span><span>.span;</span></div></div><div><div><div>41</div></div><div><span>                </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>expr</span><span>.</span><span>span</span><span>().</span><span>merge</span><span>(</span><span>end</span><span>);</span></div></div><div><div><div>42</div></div><div><span>                </span><span>expr</span><span><span> </span><span>=</span><span> </span></span><span>Expr</span><span>::</span><span>Call</span><span> {</span></div></div><div><div><div>43</div></div><div><span>                    </span><span>callee</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>expr</span><span>),</span></div></div><div><div><div>44</div></div><div><span>                    </span><span>args</span><span>,</span></div></div><div><div><div>45</div></div><div><span>                    </span><span>span</span><span>,</span></div></div><div><div><div>46</div></div><div><span><span>                </span></span><span>};</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>LBracket</span><span>) {</span></div></div><div><div><div>48</div></div><div><span>                </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expression</span><span>()?;</span></div></div><div><div><div>49</div></div><div><span>                </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>RBracket</span><span>, </span><span>"unclosed index"</span><span>)?.span;</span></div></div><div><div><div>50</div></div><div><span>                </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>expr</span><span>.</span><span>span</span><span>().</span><span>merge</span><span>(</span><span>end</span><span>);</span></div></div><div><div><div>51</div></div><div><span>                </span><span>expr</span><span><span> </span><span>=</span><span> </span></span><span>Expr</span><span>::</span><span>Index</span><span> {</span></div></div><div><div><div>52</div></div><div><span>                    </span><span>object</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>expr</span><span>),</span></div></div><div><div><div>53</div></div><div><span>                    </span><span>index</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>index</span><span>),</span></div></div><div><div><div>54</div></div><div><span>                    </span><span>span</span><span>,</span></div></div><div><div><div>55</div></div><div><span><span>                </span></span><span>};</span></div></div><div><div><div>56</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>57</div></div><div><span>                </span><span>break</span><span>;</span></div></div><div><div><div>58</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>60</div></div><div><span>        </span><span>Ok</span><span>(</span><span>expr</span><span>)</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>call</code> 里 <code>expr = Expr::Call { callee: Box::new(expr), ... }</code> 是同一个 “移动再赋值” 的模式。</p><p><code>primary</code> 稍微特别一点：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>primary</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Expr</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>token</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>advance</span><span>();</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>token</span><span>.span;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>match</span><span> </span><span>token</span><span>.kind {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Nil</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Nil</span><span> { </span><span>span</span><span> }),</span></div></div><div><div><div>6</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>True</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Bool</span><span> { </span><span>value</span><span>: </span><span>true</span><span>, </span><span>span</span><span> }),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>False</span><span> =&gt; </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Bool</span><span> { </span><span>value</span><span>: </span><span>false</span><span>, </span><span>span</span><span> }),</span></div></div><div><div><div>8</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Int</span><span>(</span><span>value</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Int</span><span> { </span><span>value</span><span>, </span><span>span</span><span> }),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Float</span><span>(</span><span>value</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Float</span><span> { </span><span>value</span><span>, </span><span>span</span><span> }),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Str</span><span>(</span><span>value</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Str</span><span> {</span></div></div><div><div><div>11</div></div><div><span>                </span><span>value</span><span>: </span><span>value</span><span>.</span><span>into_owned</span><span>(),</span></div></div><div><div><div>12</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>name</span><span>) =&gt; </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Var</span><span> {</span></div></div><div><div><div>15</div></div><div><span>                </span><span>name</span><span>: </span><span>Ident</span><span>::</span><span>new</span><span>(</span><span>name</span><span>, </span><span>span</span><span>),</span></div></div><div><div><div>16</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>}),</span></div></div><div><div><div>18</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>LParen</span><span> =&gt; {</span></div></div><div><div><div>19</div></div><div><span>                </span><span>let</span><span> </span><span>expr</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expression</span><span>()?;</span></div></div><div><div><div>20</div></div><div><span>                </span><span>self</span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>RParen</span><span>, </span><span>"unclosed `(`"</span><span>)?;</span></div></div><div><div><div>21</div></div><div><span>                </span><span>Ok</span><span>(</span><span>expr</span><span>)</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>LBracket</span><span><span> </span><span>|</span><span> </span></span><span>TokenKind</span><span>::</span><span>HashBrace</span><span><span> </span><span>|</span><span> </span></span><span>TokenKind</span><span>::</span><span>Fn</span><span> =&gt; {</span></div></div><div><div><div>24</div></div><div><span>                </span><span>todo!</span><span>(</span><span>"list, map and closure literals"</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>self</span><span>.</span><span>error</span><span>(</span><span>span</span><span>, </span><span>format!</span><span>(</span><span>"expected an expression, found {other}"</span><span>)),</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（中间三行是临时占位，下一节会换成真的。）它做了一件很 Rust 的事：<code>let token = self.advance(); ... match token.kind { ... }</code>，<code>match</code> 的分支里直接<strong>把 <code>token.kind</code> 拆开</strong>：<code>TokenKind::Int(value) =&gt; Ok(Expr::Int { value, span })</code> 里的 <code>value</code> 是从 token 里移出来的 <code>i64</code>；<code>TokenKind::Str(value)</code> 里的 <code>value</code> 是 <code>Cow&lt;str&gt;</code>，<code>.into_owned()</code> 把它变成拥有所有权的 <code>String</code>，因为 AST 里的字符串必须能活得比源码久。最后的 <code>other =&gt; ...</code> 分支把剩下的所有 token 绑定到 <code>other</code> 上，用它的 <code>Display</code> 生成 “expected an expression, found …” 的消息。</p><p>再看 <code>Expr::Var { name: Ident::new(name, span), span }</code>：<code>name</code> 是借用源码的 <code>&amp;'src str</code>，<code>Ident::new(impl Into&lt;String&gt;, ..)</code> 把它变成拥有的 <code>String</code>。AST 不再借用源码，这是 token 和 AST 之间的分界线。</p><p>然后是 <code>expression</code> 与赋值。赋值是<strong>右结合</strong>的（<code>a = b = 3</code> 是 <code>a = (b = 3)</code>），而且它的目标必须是变量或下标，不能是 <code>1 = 2</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>// --- expressions ------------------------------------------------------</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>    </span><span>/// Parses a full expression, including assignment.</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>expression</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Expr</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.</span><span>assignment</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>assignment</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Expr</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>expr</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>binary</span><span>(</span><span>0</span><span>)?;</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Assign</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>assignment</span><span>()?; </span><span>// right-associative</span></div></div><div><div><div>12</div></div><div><span>            </span><span>if</span><span><span> </span><span>!</span></span><span>is_assignable</span><span>(&amp;</span><span>expr</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>return</span><span> </span><span>self</span><span>.</span><span>error</span><span>(</span><span>expr</span><span>.</span><span>span</span><span>(), </span><span>"invalid assignment target"</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>            </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>expr</span><span>.</span><span>span</span><span>().</span><span>merge</span><span>(</span><span>value</span><span>.</span><span>span</span><span>());</span></div></div><div><div><div>16</div></div><div><span>            </span><span>return</span><span> </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Assign</span><span> {</span></div></div><div><div><div>17</div></div><div><span>                </span><span>target</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>expr</span><span>),</span></div></div><div><div><div>18</div></div><div><span>                </span><span>value</span><span>: </span><span>Box</span><span>::</span><span>new</span><span>(</span><span>value</span><span>),</span></div></div><div><div><div>19</div></div><div><span>                </span><span>span</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>});</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>        </span><span>Ok</span><span>(</span><span>expr</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>递归调用 <code>self.assignment()</code>（不是 <code>binary</code>）读右边，这就是右结合。<code>!is_assignable(&amp;expr)</code> 先把右边解析完再检查左边：这是个小取舍，让错误位置落在左边的 <code>expr.span()</code> 上，而且右边的语法错误会优先被报告。<code>is_assignable</code> 在文件的末尾：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>is_assignable</span><span>(</span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>matches!</span><span>(</span><span>expr</span><span>, </span><span>Expr</span><span>::</span><span>Var</span><span><span> { .. } </span><span>|</span><span> </span></span><span>Expr</span><span>::</span><span>Index</span><span> { .. })</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>matches!(expr, Pattern)</code> 是标准库的宏，展开成 <code>match expr { Pattern =&gt; true, _ =&gt; false }</code>。<code>Expr::Var { .. }</code> 里的 <code>..</code> 的意思是 “我不关心其他字段”。</p><p>先让它能编译。我们还缺一个语句：表达式语句，求值后丢掉结果，必须以分号结束：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>expression_statement</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Stmt</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>expr</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expression</span><span>()?;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>expr</span><span>.</span><span>span</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>expect</span><span>(</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>&amp;</span><span>TokenKind</span><span>::</span><span>Semicolon</span><span>,</span></div></div><div><div><div>7</div></div><div><span>                </span><span>"expression statements are terminated with `;`"</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>)?</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>.span;</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Ok</span><span>(</span><span>Stmt</span><span>::</span><span>Expr</span><span> {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>expr</span><span>,</span></div></div><div><div><div>12</div></div><div><span>            </span><span>span</span><span>: </span><span>span</span><span>.</span><span>merge</span><span>(</span><span>end</span><span>),</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把占位的 <code>statement</code> 改成只调用它（<code>self.expression_statement()</code>），删掉占位的 <code>expression</code>（上面已经有真的了）。把 <code>tests</code> 模块整个换成下面这份（<code>first_light</code> 已完成使命，删掉），先写四个测试：优先级、括号、减法左结合，加上 <code>rejects_assignment_to_literal</code>。测试里的 <code>parse_ok</code> 是一个小助手：解析一段源码，断言没有诊断，把 <code>Program</code> 还给调用者。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>use</span><span><span> </span><span>pebble_ast</span><span>::pretty;</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>parse_ok</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Program</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>9</div></div><div><span>            </span><span>result</span><span>.</span><span>is_ok</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>"expected clean parse, got {:?}"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>            </span><span>result</span><span>.diagnostics</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>result</span><span>.program</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>parses_precedence_correctly</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>// 1 + 2 * 3 must group as 1 + (2 * 3).</span></div></div><div><div><div>19</div></div><div><span>        </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>parse_ok</span><span>(</span><span>"1 + 2 * 3;"</span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>pretty</span><span>(&amp;</span><span>program</span><span>);</span></div></div><div><div><div>21</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>22</div></div><div><span>            </span><span>rendered</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>"expr</span><span>\n</span><span>  binary +</span><span>\n</span><span>    int 1</span><span>\n</span><span>    binary *</span><span>\n</span><span>      int 2</span><span>\n</span><span>      int 3</span><span>\n</span><span>"</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>28</div></div><div><span>    </span><span>fn</span><span> </span><span>parentheses_override_precedence</span><span>() {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>parse_ok</span><span>(</span><span>"(1 + 2) * 3;"</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>pretty</span><span>(&amp;</span><span>program</span><span>);</span></div></div><div><div><div>31</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>32</div></div><div><span>            </span><span>rendered</span><span>.</span><span>starts_with</span><span>(</span><span>"expr</span><span>\n</span><span>  binary *</span><span>\n</span><span>    binary +</span><span>\n</span><span>"</span><span>),</span></div></div><div><div><div>33</div></div><div><span>            </span><span>"{rendered}"</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>38</div></div><div><span>    </span><span>fn</span><span> </span><span>subtraction_is_left_associative</span><span>() {</span></div></div><div><div><div>39</div></div><div><span>        </span><span>// 1 - 2 - 3 == (1 - 2) - 3</span></div></div><div><div><div>40</div></div><div><span>        </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>parse_ok</span><span>(</span><span>"1 - 2 - 3;"</span><span>);</span></div></div><div><div><div>41</div></div><div><span>        </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>pretty</span><span>(&amp;</span><span>program</span><span>);</span></div></div><div><div><div>42</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>43</div></div><div><span>            </span><span>rendered</span><span>,</span></div></div><div><div><div>44</div></div><div><span>            </span><span>"expr</span><span>\n</span><span>  binary -</span><span>\n</span><span>    binary -</span><span>\n</span><span>      int 1</span><span>\n</span><span>      int 2</span><span>\n</span><span>    int 3</span><span>\n</span><span>"</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>49</div></div><div><span>    </span><span>fn</span><span> </span><span>rejects_assignment_to_literal</span><span>() {</span></div></div><div><div><div>50</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"1 = 2;"</span><span>);</span></div></div><div><div><div>51</div></div><div><span>        </span><span>assert!</span><span><span>(</span><span>!</span></span><span>result</span><span>.</span><span>is_ok</span><span>());</span></div></div><div><div><div>52</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>53</div></div><div><span>            </span><span>result</span></div></div><div><div><div>54</div></div><div><span><span>                </span></span><span>.diagnostics</span></div></div><div><div><div>55</div></div><div><span><span>                </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>56</div></div><div><span><span>                </span></span><span>.</span><span>any</span><span><span>(</span><span>|</span></span><span>d</span><span><span>|</span><span> </span></span><span>d</span><span>.message.</span><span>contains</span><span>(</span><span>"invalid assignment"</span><span>))</span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-parser</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span>running 4 tests</span></div></div><div><div><div>4</div></div><div><span>test tests::parentheses_override_precedence ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::subtraction_is_left_associative ... ok</span></div></div><div><div><div>6</div></div><div><span>test tests::parses_precedence_correctly ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::rejects_assignment_to_literal ... ok</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个测试通过。到这里我们手里是一个能解析 <code>1 + 2 * 3;</code> 但还不认识 <code>let</code>、<code>if</code> 的解析器。</p></section></section>
<section><h2>语句，以及 “错了也要继续”<a href="#语句以及-错了也要继续"><span>#</span></a></h2><p>语句部分的函数大多是一个模板：读关键字，<code>expect</code> 一个又一个部分，<code>?</code> 把任何失败向上抛，最后把各部分的 span 合并成一个。<code>let_declaration</code> 是最清晰的例子：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>let_declaration</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Stmt</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>advance</span><span>().span; </span><span>// `let`</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>name_token</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Ident</span><span>(</span><span>""</span><span>), </span><span>"`let` needs a name"</span><span>)?;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>ident_from</span><span>(&amp;</span><span>name_token</span><span>)?;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.</span><span>expect</span><span>(</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>&amp;</span><span>TokenKind</span><span>::</span><span>Assign</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>"`let` needs an initialiser: `let x = ...;`"</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>)?;</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>value</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expression</span><span>()?;</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Semicolon</span><span>, </span><span>"statements end with `;`"</span><span>)?</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>.span;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Ok</span><span>(</span><span>Stmt</span><span>::</span><span>Let</span><span> {</span></div></div><div><div><div>14</div></div><div><span>            </span><span>name</span><span>,</span></div></div><div><div><div>15</div></div><div><span>            </span><span>value</span><span>,</span></div></div><div><div><div>16</div></div><div><span>            </span><span>span</span><span>: </span><span>start</span><span>.</span><span>merge</span><span>(</span><span>end</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>一个 <code>let</code> 语句有四处可能失败，每一处后面都有一个 <code>?</code>；每个 <code>expect</code> 的第二个参数是将来出现在错误报告里的提示。最后 <code>start.merge(end)</code> 把 <code>let</code> 到 <code>;</code> 的整段范围合成一个 span，这个 <code>merge</code> 是第 1 篇写的。</p><p><code>declaration</code> 决定一个声明是函数、变量还是普通语句：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>declaration</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Stmt</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>check</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Fn</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>self</span><span>.</span><span>fn_declaration</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>check</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Let</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>self</span><span>.</span><span>let_declaration</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>self</span><span>.</span><span>statement</span><span>()</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>statement</code> 在 <code>if</code>、<code>while</code>、<code>for</code>、<code>return</code>、<code>break</code>、<code>continue</code> 之间选择，不是则落回表达式语句：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>statement</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Stmt</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>If</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>self</span><span>.</span><span>if_statement</span><span>(</span><span>self</span><span>.</span><span>previous</span><span>().span)</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>While</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>self</span><span>.</span><span>while_statement</span><span>(</span><span>self</span><span>.</span><span>previous</span><span>().span)</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>For</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>self</span><span>.</span><span>for_statement</span><span>(</span><span>self</span><span>.</span><span>previous</span><span>().span)</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Return</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>self</span><span>.</span><span>return_statement</span><span>(</span><span>self</span><span>.</span><span>previous</span><span>().span)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Break</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>previous</span><span>().span;</span></div></div><div><div><div>12</div></div><div><span>            </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Semicolon</span><span>, </span><span>"`break;`"</span><span>)?.span;</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Ok</span><span>(</span><span>Stmt</span><span>::</span><span>Break</span><span> {</span></div></div><div><div><div>14</div></div><div><span>                </span><span>span</span><span>: </span><span>start</span><span>.</span><span>merge</span><span>(</span><span>end</span><span>),</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>})</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Continue</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>previous</span><span>().span;</span></div></div><div><div><div>18</div></div><div><span>            </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Semicolon</span><span>, </span><span>"`continue;`"</span><span>)?.span;</span></div></div><div><div><div>19</div></div><div><span>            </span><span>Ok</span><span>(</span><span>Stmt</span><span>::</span><span>Continue</span><span> {</span></div></div><div><div><div>20</div></div><div><span>                </span><span>span</span><span>: </span><span>start</span><span>.</span><span>merge</span><span>(</span><span>end</span><span>),</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>})</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>self</span><span>.</span><span>expression_statement</span><span>()</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>一个让我停了一下的借用<a href="#一个让我停了一下的借用"><span>#</span></a></h3><p>看 <code>self.if_statement(self.previous().span)</code>。直觉上这是一个借用冲突：<code>if_statement</code> 需要 <code>&amp;mut self</code>，而参数 <code>self.previous()</code> 要 <code>&amp;self</code>。Rust 的借用规则不是说同一时刻要么一个 <code>&amp;mut</code> 要么多个 <code>&amp;</code> 吗？</p><p>这行代码能编译，原因叫 <strong>two-phase borrow</strong>（两阶段借用）。方法调用 <code>receiver.method(args)</code> 里的 <code>&amp;mut</code> 自动借用不是一上来就生效，而是分两步：先 “预订”（reservation，这时候还可以读），等所有参数都求值完毕，调用真正发生的那一刻才 “激活”。参数里读 <code>self.previous().span</code> 发生在预订阶段，读完时拿到的是一个 <code>Span</code>（它是 <code>Copy</code>，所以借用在这个表达式结束时就结束了），然后才激活可变借用。在实验室里把这个现象完整地跑一遍：</p><p><code>~/rust-lab/src/bin/ch07_two_phase.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Parser</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>tokens</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span>&gt;,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>current</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>impl</span><span> </span><span>Parser</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>previous</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>self</span><span>.tokens[</span><span>self</span><span>.current.</span><span>saturating_sub</span><span>(</span><span>1</span><span>)]</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>fn</span><span> </span><span>if_statement</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>start</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span><span>.current </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>start</span><span> + </span><span>self</span><span>.</span><span>previous</span><span>()</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>// `v.push(v.len())`: the receiver is borrowed mutably, the argument reads `v`.</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>v</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>];</span></div></div><div><div><div>20</div></div><div><span>    </span><span>v</span><span>.</span><span>push</span><span>(</span><span>v</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{v:?}"</span><span>);</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>parser</span><span><span> </span><span>=</span><span> </span></span><span>Parser</span><span> { </span><span>tokens</span><span>: </span><span>vec!</span><span>[</span><span>10</span><span>, </span><span>20</span><span>, </span><span>30</span><span>], </span><span>current</span><span>: </span><span>1</span><span> };</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>parser</span><span>.</span><span>if_statement</span><span>(</span><span>parser</span><span>.</span><span>previous</span><span>());</span></div></div><div><div><div>25</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{r}"</span><span>);</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_two_phase</span></div></div><div><div><div>2</div></div><div><span>[1, 2, 3, 3]</span></div></div><div><div><div>3</div></div><div><span>30</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>v.push(v.len())</code> 是教科书例子：C++ 程序员写了二十年，在 Rust 里原本该是错的。它能编译。但是这个宽限只给<strong>自动借用的方法接收者</strong>。如果你把方法调用手写成函数调用，两阶段就没有了：</p><p><code>~/rust-lab/src/bin/ch07_two_phase_fail.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>v</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>];</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Vec</span><span>::</span><span>push</span><span>(&amp;</span><span>mut</span><span> </span><span>v</span><span>, </span><span>v</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>4</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{v:?}"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch07_two_phase_fail</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0502]: cannot borrow `v` as immutable because it is also borrowed as mutable</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch07_two_phase_fail.rs:3:23</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>3 |     Vec::push(&amp;mut v, v.len());</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|     --------- ------  ^ immutable borrow occurs here</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|     |         |</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|     |         mutable borrow occurs here</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|     mutable borrow later used by call</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>For more information about this error, try `rustc --explain E0502`.</span></div></div><div><div><div>13</div></div><div><span>error: could not compile `rust-lab` (bin "ch07_two_phase_fail") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同样的两个操作，换一种写法就是错的。这个例外是为了让最常见的写法（<code>v.push(v.len())</code>）不需要先绑一个临时变量；我对它的理解是 “编译器对自己的一个宽容让步”，不是语言底层的什么新规则。（顺带，C++ 里 <code>f(a.g(), a.h())</code> 的参数求值顺序是未指定的；Rust 保证从左到右。）</p></section><section><h3><code>let end;</code>：延迟初始化<a href="#let-end延迟初始化"><span>#</span></a></h3><p><code>if_statement</code> 里有一个 C 程序员没见过的写法：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>if_statement</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>start</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Stmt</span><span>, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>cond</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>expression</span><span>()?;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>then_branch</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>block</span><span>()?;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>else_branch</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span>;</span></div></div><div><div><div>6</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>Else</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>if</span><span> </span><span>self</span><span>.</span><span>matches</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>If</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>// `else if` desugars into a one-statement else block.</span></div></div><div><div><div>9</div></div><div><span>                </span><span>let</span><span> </span><span>nested</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>if_statement</span><span>(</span><span>self</span><span>.</span><span>previous</span><span>().span)?;</span></div></div><div><div><div>10</div></div><div><span>                </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>nested</span><span>.</span><span>span</span><span>();</span></div></div><div><div><div>11</div></div><div><span>                </span><span>else_branch</span><span>.</span><span>push</span><span>(</span><span>nested</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>let</span><span> </span><span>block</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>block</span><span>()?;</span></div></div><div><div><div>14</div></div><div><span>                </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>previous</span><span>().span;</span></div></div><div><div><div>15</div></div><div><span>                </span><span>else_branch</span><span><span> </span><span>=</span><span> </span></span><span>block</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>previous</span><span>().span;</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>        </span><span>Ok</span><span>(</span><span>Stmt</span><span>::</span><span>If</span><span> {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>cond</span><span>,</span></div></div><div><div><div>22</div></div><div><span>            </span><span>then_branch</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>else_branch</span><span>,</span></div></div><div><div><div>24</div></div><div><span>            </span><span>span</span><span>: </span><span>start</span><span>.</span><span>merge</span><span>(</span><span>end</span><span>),</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意 <code>let end;</code> 没有初始值，类型从后面的赋值推出来（<code>Span</code>）。每一条控制路径最终都必须给 <code>end</code> 赋值恰好一次，编译器检查这一点。C 里同样的写法是未初始化变量，漏掉一条分支你只会得到一个垃圾值或者一个 <code>-Wmaybe-uninitialized</code> 的警告；Rust 里是一个硬错误。实验室验证：</p><p><code>~/rust-lab/src/bin/ch07_deferred_fail.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>args</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>env</span><span>::</span></span><span>args</span><span>().</span><span>count</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>label</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> </span><span>args</span><span><span> </span><span>&gt;</span><span> </span></span><span>1</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>label</span><span><span> </span><span>=</span><span> </span></span><span>"with arguments"</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{label}"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch07_deferred_fail</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0381]: used binding `label` is possibly-uninitialized</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch07_deferred_fail.rs:7:16</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>3 |     let label;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|         ----- binding declared here but left uninitialized</span></div></div><div><div><div>8</div></div><div><span>4 |     if args &gt; 1 {</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|        -------- if this `if` condition is `false`, `label` is not initialized</span></div></div><div><div><div>10</div></div><div><span>5 |         label = "with arguments";</span></div></div><div><div><div>11</div></div><div><span>6 |     }</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|      - an `else` arm might be missing here, initializing `label`</span></div></div><div><div><div>13</div></div><div><span>7 |     println!("{label}");</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|                ^^^^^ `label` used here but it is possibly-uninitialized</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>For more information about this error, try `rustc --explain E0381`.</span></div></div><div><div><div>17</div></div><div><span>error: could not compile `rust-lab` (bin "ch07_deferred_fail") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（给 <code>else</code> 补上赋值，<code>ch07_deferred_ok</code> 就能通过，我也跑过：它打印 <code>no arguments</code>。）</p><p><code>else if</code> 在这里被<strong>脱糖</strong>（desugar）成一个只有一条语句的 else 块：<code>else_branch.push(nested)</code>。AST 里因此不需要 <code>ElseIf</code> 这种节点，求值器和编译器都不用特殊处理它。</p><p><code>block</code> 解析 <code>{ ... }</code>，内部的每个语句也走和 <code>parse_program</code> 一样的 “出错就记录并恢复”：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>block</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>self</span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>LBrace</span><span>, </span><span>"expected a `{`-delimited block"</span><span>)?;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>stmts</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>while</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>check</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>RBrace</span><span><span>) </span><span>&amp;&amp;</span><span> </span><span>!</span></span><span>self</span><span>.</span><span>is_at_end</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span>.</span><span>declaration</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>Ok</span><span>(</span><span>stmt</span><span>) =&gt; </span><span>stmts</span><span>.</span><span>push</span><span>(</span><span>stmt</span><span>),</span></div></div><div><div><div>7</div></div><div><span>                </span><span>Err</span><span>(</span><span>diagnostic</span><span>) =&gt; {</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>self</span><span>.errors.</span><span>push</span><span>(</span><span>diagnostic</span><span>);</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>self</span><span>.</span><span>synchronize</span><span>();</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>        </span><span>self</span><span>.</span><span>expect</span><span><span>(&amp;</span><span>TokenKind</span><span>::</span></span><span>RBrace</span><span>, </span><span>"this `{` is never closed"</span><span>)?;</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Ok</span><span>(</span><span>stmts</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>最后一行的提示 <code>this `{` is never closed</code> 是我很喜欢的一个错误信息。用户忘了 <code>}</code> 时，解析器一路读到文件末尾才发现，所以主错误指向文件末尾，而 <code>context</code> 参数把真正的原因写进了 note。用一个两行的文件试一下（第一行 <code>fn f() {</code>，第二行 <code>    print(1);</code>，以换行结尾；命令行在本篇后半才写好，现在先看输出，写完之后可以回来复现）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- check unclosed.pebble; echo $?</span></div></div><div><div><div>2</div></div><div><span>error[E1001]: expected `}`, found end of file</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; unclosed.pebble:3:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>3 |</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>| ^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= note: this `{` is never closed</span></div></div><div><div><div>9</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>恢复：<code>synchronize</code><a href="#恢复synchronize"><span>#</span></a></h3><p>现在回到那个开头的三行文件，看看恢复机制到底做了什么。核心是 <code>parse_program</code> 的循环：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>parse_program</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Program</span><span> {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>stmts</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>3</div></div><div><span>        </span><span>while</span><span><span> </span><span>!</span></span><span>self</span><span>.</span><span>is_at_end</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span>.</span><span>declaration</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>                </span><span>Ok</span><span>(</span><span>stmt</span><span>) =&gt; </span><span>stmts</span><span>.</span><span>push</span><span>(</span><span>stmt</span><span>),</span></div></div><div><div><div>6</div></div><div><span>                </span><span>Err</span><span>(</span><span>diagnostic</span><span>) =&gt; {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>self</span><span>.errors.</span><span>push</span><span>(</span><span>diagnostic</span><span>);</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>self</span><span>.</span><span>synchronize</span><span>();</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Program</span><span>::</span><span>new</span><span>(</span><span>stmts</span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当 <code>declaration()</code> 返回 <code>Err</code>，我们把诊断存下来，然后调用 <code>synchronize</code> 往前跳，跳到 “下一个看起来像语句开头的地方”。这种策略叫<strong>恐慌模式恢复</strong>（panic-mode recovery）：这里的 “panic” 是编译原理里的老术语，和 Rust 的 <code>panic!</code> 没有关系，别搞混。<code>synchronize</code> 的规则：</p><ol>
<li>如果上一个 token 是 <code>;</code>，说明上一个语句刚结束，停。</li>
<li>如果当前 token 是 <code>fn</code>/<code>let</code>/<code>if</code>/<code>while</code>/<code>for</code>/<code>return</code>/<code>break</code>/<code>continue</code>/<code>}</code> 之一，说明新语句开始了（或者块结束了），停。</li>
<li>否则吞掉一个 token，继续。</li>
</ol><p>用这三条规则跑那个三行文件：</p><ol>
<li><code>let = ;</code>：<code>let_declaration</code> 吃掉 <code>let</code>，<code>expect(Ident)</code> 看到的是 <code>=</code>，失败。诊断 1 入库。<code>synchronize</code>：上一个 token 是 <code>let</code>，当前是 <code>=</code>，都不满足停止条件，吞掉 <code>=</code>；现在上一个是 <code>=</code>，当前是 <code>;</code>，再吞掉；上一个是 <code>;</code>，停。</li>
<li><code>let y = 2</code>：解析到表达式 <code>2</code> 之后 <code>expect(Semicolon)</code>，看到的是下一行的 <code>let</code>，失败，诊断 2。<code>synchronize</code>：上一个 token 是 <code>2</code>，当前是 <code>let</code>，满足规则 2，立刻停止（什么也没吞）。</li>
<li><code>let z = 3;</code>：干净地解析出来。</li>
</ol><p>所以输出是两条诊断，加上一个 <code>let z</code>。<code>let y = 2</code> 这一句因为失败而整个被丢掉了，这是代价：恢复的目标是<strong>继续找到后面的错误</strong>，不是修好前面的。顺便注意第二条诊断指向的是第 3 行的 <code>let</code>（缺的分号本来应该在第 2 行末尾），这是 “在看到意外的 token 时报错” 这种策略的固有缺点，Pebble 没有去修它。</p></section><section><h3>把剩下的复制进来<a href="#把剩下的复制进来"><span>#</span></a></h3><p>到这里，我们写过的函数已经覆盖了这个解析器里每一种技巧。剩下的函数只是同一个模板的重复，没有新的 Rust 知识，所以我们<strong>复制</strong>它们。打开 <code>https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-parser/src/lib.rs</code>（或者你本地的 <code>~/pebble-tag</code>，没建的话 <code>git clone --branch tutorial-v1 https://github.com/bfmhno3/pebble ~/pebble-tag</code>），把下面这些项复制到你的文件里，位置和 <code>tutorial-v1</code> 里一样：</p><ul>
<li><code>parse_expression</code>（紧跟在 <code>parse</code> 之后）：给以后的 REPL 用的入口，只解析一个表达式。</li>
<li><code>Parser::errors</code>（<code>new</code> 之后）：对外暴露已收集的诊断。</li>
<li><code>fn_declaration</code>（<code>declaration</code> 之后），<code>while_statement</code>、<code>for_statement</code>、<code>return_statement</code>（<code>if_statement</code> 之后）：和 <code>let_declaration</code> 同一个模板。</li>
<li><code>parameter_list</code>（<code>block</code> 之后）、<code>list_literal</code>、<code>map_literal</code>（<code>primary</code> 之后）：处理 <code>(a, b)</code>、<code>[1, 2,]</code>、<code>#{ k: v }</code>，都带尾逗号处理。</li>
<li><code>ident_from</code>（<code>// --- helpers</code> 注释之后、<code>synchronize</code> 之前）：把一个 <code>Ident</code> token 变成 AST 的 <code>Ident</code>。</li>
</ul><p>然后把 <code>primary</code> 里那个三行的 <code>todo!</code> 分支换成真的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>LBracket</span><span> =&gt; </span><span>self</span><span>.</span><span>list_literal</span><span>(</span><span>span</span><span>),</span></div></div><div><div><div>2</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>HashBrace</span><span> =&gt; </span><span>self</span><span>.</span><span>map_literal</span><span>(</span><span>span</span><span>),</span></div></div><div><div><div>3</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Fn</span><span> =&gt; {</span></div></div><div><div><div>4</div></div><div><span>                </span><span>let</span><span> </span><span>params</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>parameter_list</span><span>()?;</span></div></div><div><div><div>5</div></div><div><span>                </span><span>let</span><span> </span><span>body</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>block</span><span>()?;</span></div></div><div><div><div>6</div></div><div><span>                </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>span</span><span>.</span><span>merge</span><span>(</span><span>self</span><span>.</span><span>previous</span><span>().span);</span></div></div><div><div><div>7</div></div><div><span>                </span><span>Ok</span><span>(</span><span>Expr</span><span>::</span><span>Fn</span><span> { </span><span>params</span><span>, </span><span>body</span><span>, </span><span>span</span><span> })</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把占位的 <code>declaration</code> 和 <code>statement</code> 换成上面写的完整版本（<code>declaration</code> 里引用了刚复制进来的 <code>fn_declaration</code>）。函数在文件里的先后顺序以参考克隆为准，最后的 <code>diff</code> 会告诉你哪里还不一样。最后把测试模块整个换成 <code>tutorial-v1</code> 里的那一份（一共 13 个测试：前面写过的 4 个，下面贴出来的 2 个，另外 7 个照抄，它们大多是同一类断言：解析一小段源码，检查 <code>pretty</code> 打印出来的树）。下面这两个是我觉得值得读一读的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>recovers_and_reports_multiple_errors</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"let = ;</span><span>\n</span><span>let y = 2</span><span>\n</span><span>let z = 3;"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>assert!</span><span>(</span><span>result</span><span>.diagnostics.</span><span>len</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>2</span><span>, </span><span>"{:?}"</span><span>, </span><span>result</span><span>.diagnostics);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>// Recovery still yields the later, valid statement.</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert!</span><span>(</span><span>result</span><span>.program.stmts.</span><span>iter</span><span>().</span><span>any</span><span><span>(</span><span>|</span></span><span>s</span><span><span>|</span><span> </span></span><span>matches!</span><span>(</span></div></div><div><div><div>7</div></div><div><span>            </span><span>s</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>Stmt</span><span>::</span><span>Let</span><span> { </span><span>name</span><span>, .. } </span><span>if</span><span> </span><span>name</span><span><span>.name </span><span>==</span><span> </span></span><span>"z"</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>)));</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>fn</span><span> </span><span>reports_missing_semicolon_with_a_span</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"let x = 1"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>result</span><span>.diagnostics.</span><span>len</span><span>(), </span><span>1</span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>result</span><span>.diagnostics[</span><span>0</span><span>].code.</span><span>as_deref</span><span>(), </span><span>Some</span><span>(</span><span>"E1001"</span><span>));</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>第一个测试验证了恢复：错误不止一个，而且后面的 <code>let z</code> 还在。第二个验证了错误码和位置信息真的被带出来了。还有一个叫 <code>skips_empty_statements_never_panics_on_garbage</code> 的测试，把一堆乱七八糟的输入（<code>")))"</code>、<code>"{{{"</code>、<code>"a["</code>……）扔给解析器，只要求不 panic：解析用户输入的函数不应该有 panic 路径，这个测试就是这条规则的一个小小守卫。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>...</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 4.92s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_parser-d9282dee6a0168a0)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>running 13 tests</span></div></div><div><div><div>6</div></div><div><span>test tests::parentheses_override_precedence ... ok</span></div></div><div><div><div>7</div></div><div><span>test tests::parses_break_and_continue ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::parse_expression_entry_point_works ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::parses_let_and_variables ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::parses_control_flow ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::parses_closures_lists_and_maps ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::parses_functions_and_returns ... ok</span></div></div><div><div><div>13</div></div><div><span>test tests::parses_precedence_correctly ... ok</span></div></div><div><div><div>14</div></div><div><span>test tests::recovers_and_reports_multiple_errors ... ok</span></div></div><div><div><div>15</div></div><div><span>test tests::reports_missing_semicolon_with_a_span ... ok</span></div></div><div><div><div>16</div></div><div><span>test tests::rejects_assignment_to_literal ... ok</span></div></div><div><div><div>17</div></div><div><span>test tests::subtraction_is_left_associative ... ok</span></div></div><div><div><div>18</div></div><div><span>test tests::skips_empty_statements_never_panics_on_garbage ... ok</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span><span>   </span></span><span>Doc-tests pebble_parser</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>running 0 tests</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>13 个测试全部通过。<code>pebble-parser</code> 完成了：它必须和 <code>tutorial-v1</code> 逐字节一致，后面的检查点会 <code>diff</code> 它。</p></section></section>
<section><h2>第一个命令行<a href="#第一个命令行"><span>#</span></a></h2><p>有解析器了，我们想从终端里看到它工作。这一节做 <code>pebble</code> 命令行的第一个版本，只有三个子命令：</p><ul>
<li><code>pebble tokens &lt;file&gt;</code>：每个 token 打印一行。</li>
<li><code>pebble parse &lt;file&gt;</code>：打印语法树。</li>
<li><code>pebble check &lt;file&gt;</code>：只检查词法和语法错误，没有错误就什么也不说。</li>
</ul><section><h3>先用标准库试一试<a href="#先用标准库试一试"><span>#</span></a></h3><p>在拉进任何库之前，我先在实验室里用 <code>std::env::args()</code> 写了一个 13 行的版本，目的是看看没有 clap 的日子有多难过：</p><p><code>~/rust-lab/src/bin/ch07_args.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::env;</span></span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::fs;</span></span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>process</span><span>::exit;</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>args</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span><span>&gt; </span><span>=</span><span> </span><span>env</span><span>::</span></span><span>args</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> </span><span>args</span><span>.</span><span>len</span><span><span>() </span><span>!=</span><span> </span></span><span>3</span><span><span> </span><span>||</span><span> </span></span><span>args</span><span>[</span><span>1</span><span><span>] </span><span>!=</span><span> </span></span><span>"count"</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>eprintln!</span><span>(</span><span>"usage: {} count &lt;file&gt;"</span><span>, </span><span>args</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>exit</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span><span> </span><span>fs</span><span>::</span></span><span>read_to_string</span><span>(&amp;</span><span>args</span><span>[</span><span>2</span><span>]).</span><span>unwrap</span><span>();</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{} bytes, {} lines"</span><span>, </span><span>text</span><span>.</span><span>len</span><span>(), </span><span>text</span><span>.</span><span>lines</span><span>().</span><span>count</span><span>());</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>env::args()</code> 是一个迭代器，<code>collect()</code> 把它收进 <code>Vec&lt;String&gt;</code>（第一个元素是程序名，和 C 的 <code>argv[0]</code> 一样）。几种运行方式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_args; echo $?</span></div></div><div><div><div>2</div></div><div><span>usage: target/debug/ch07_args count &lt;file&gt;</span></div></div><div><div><div>3</div></div><div><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_args -- count src/bin/ch07_args.rs</span></div></div><div><div><div>2</div></div><div><span>365 bytes, 13 lines</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>出了错就是前面见过的那个 panic（退出码 101）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch07_args -- count nope.pebble; echo $?</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>thread 'main' (80062) panicked at src/bin/ch07_args.rs:11:45:</span></div></div><div><div><div>4</div></div><div><span>called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }</span></div></div><div><div><div>5</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>6</div></div><div><span>101</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个版本能工作，但每加一个子命令都要自己写一遍参数检查、用法信息、<code>--help</code>，而且文件不存在时给用户看的是一团 <code>Os { code: 2, ... }</code>。这就是 clap 要解决的问题。</p></section><section><h3>clap：用类型描述命令行<a href="#clap用类型描述命令行"><span>#</span></a></h3><p>clap 的 derive API 的想法是：<strong>命令行的形状就是一个数据类型</strong>。顶层是一个结构体，子命令是一个枚举，每个子命令的参数是另一个结构体；你在类型上加上 <code>#[derive(Parser)]</code> 等，clap 的过程宏（第 6 篇学过的同一种东西，只是这回是别人写的）在编译期展开成解析代码。<code>///</code> 文档注释会被 clap 读出来当帮助文字，所以文档和 <code>--help</code> 永远一致。</p><p>先在 <code>~/pebble</code> 下建这个 crate。这次是二进制 crate，<code>cargo new</code> 不加 <code>--lib</code>，会生成 <code>src/main.rs</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>crates/pebble-cli</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>接下来要写的五个文件是 <code>Cargo.toml</code>、<code>src/cli.rs</code>、<code>src/sources.rs</code>、<code>src/commands.rs</code>、<code>src/main.rs</code>，外加 <code>tests/cli.rs</code>；<code>cargo new</code> 生成的 <code>main.rs</code> 里那个 hello world 会被替换掉。先写 <code>crates/pebble-cli/src/cli.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! The command-line surface, declared with clap's derive API.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Everything here is data: the actual work lives in [`crate::commands`].</span></div></div><div><div><div>4</div></div><div><span>//! Keeping the shape of the interface in one small file makes it easy to see at</span></div></div><div><div><div>5</div></div><div><span>//! a glance what the driver can do.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>path</span><span>::</span></span><span>PathBuf</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>clap</span><span>::{</span></span><span>Args</span><span>, </span><span>Parser</span><span>, </span><span>Subcommand</span><span>};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>/// Pebble: a small scripting language and toolchain.</span></div></div><div><div><div>12</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Parser</span><span>)]</span></div></div><div><div><div>13</div></div><div><span>#[command(name </span><span>=</span><span> </span><span>"pebble"</span><span>, version)]</span></div></div><div><div><div>14</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>Cli</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>/// The action to perform.</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>#[command(subcommand)]</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>command</span><span>: </span><span>Command</span><span>,</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>/// Every top-level action the driver understands.</span></div></div><div><div><div>21</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Subcommand</span><span>)]</span></div></div><div><div><div>22</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>enum</span><span> </span><span>Command</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>/// Print the token stream of a source file.</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Tokens</span><span>(</span><span>FileArgs</span><span>),</span></div></div><div><div><div>25</div></div><div><span>    </span><span>/// Print the parsed abstract syntax tree of a source file.</span></div></div><div><div><div>26</div></div><div><span>    </span><span>Parse</span><span>(</span><span>FileArgs</span><span>),</span></div></div><div><div><div>27</div></div><div><span>    </span><span>/// Lex and parse a source file without running it.</span></div></div><div><div><div>28</div></div><div><span>    </span><span>Check</span><span>(</span><span>FileArgs</span><span>),</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>/// A subcommand whose only input is one source file.</span></div></div><div><div><div>32</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Args</span><span>)]</span></div></div><div><div><div>33</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>FileArgs</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>/// The source file to read.</span></div></div><div><div><div>35</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>file</span><span>: </span><span>PathBuf</span><span>,</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐个说一下：</p><ul>
<li><code>Cli::parse()</code>（<code>Parser</code> trait 提供）读取 <code>std::env::args()</code>，匹配失败时自己打印错误并以退出码 2 结束进程，调用者永远拿不到一个无效的 <code>Cli</code>。</li>
<li><code>Command</code> 是个带数据的枚举：<code>Tokens(FileArgs)</code> 这样的变体里装着该子命令的参数。你在第 3 篇学过的 “枚举能带数据”，在 C++ 里对应 <code>std::variant</code>；这里它还有一个额外的作用：<code>match cli.command { ... }</code> 是穷尽的，新加一个子命令却忘了处理，编译器会指出来。</li>
<li>三个子命令共用一个 <code>FileArgs</code>（只有一个位置参数 <code>file: PathBuf</code>）。<code>PathBuf</code> 是拥有所有权的路径，对应 <code>&amp;Path</code> 这个借用版本，和 <code>String</code>/<code>&amp;str</code> 的关系一模一样。</li>
<li><code>pub(crate)</code>：这里是我之前绕过去的一个知识点，下一小节讲。</li>
</ul></section><section><h3>文件模块、可见性和 <code>use crate::</code><a href="#文件模块可见性和-use-crate"><span>#</span></a></h3><p>C/C++ 里 “一个源文件” 和 “一个翻译单元” 是由构建系统决定的，头文件是你手工维护的接口。Rust 的模块系统是语言的一部分：<code>main.rs</code> 里写 <code>mod cli;</code> 就告诉编译器，“有一个叫 <code>cli</code> 的模块，它的内容在同目录的 <code>cli.rs</code> 里”。<strong>没有头文件</strong>，一个模块的接口就是它标了 <code>pub</code> 的那些项。</p><p>可见性默认是私有的，“私有” 的意思是 “只有这个模块和它的子模块能看见”（和 C 的 <code>static</code> 有点像，但粒度是模块）。<code>pub</code> 是 “所有人”，但在一个二进制 crate 里，<strong>没有任何别的 crate 会依赖你</strong>，所以 <code>pub</code> 其实是撒谎：没人能用它。<code>pub(crate)</code> 说的是真话：“这个 crate 里的任何地方可见”。所以我把命令行 crate 的所有项都写成 <code>pub(crate)</code>。（<code>pebble-cli</code> 在 <code>tutorial-v1</code> 里已经是这样了。）</p><p>在别的模块里引用它们用 <code>use crate::cli::{Cli, Command};</code>，<code>crate::</code> 从当前 crate 的根开始，不依赖当前文件在哪，这比 C 的 <code>#include "../../x.h"</code> 的相对路径健壮得多。</p></section><section><h3>报告：<code>sources.rs</code><a href="#报告sourcesrs"><span>#</span></a></h3><p>所有子命令的流程是一样的：读文件，登记进 <code>SourceMap</code>，把文本交给前端，最后用同一个 <code>SourceMap</code> 渲染诊断。这些重复的东西放进 <code>sources.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Loading source text and turning diagnostics into printable text.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Every subcommand works the same way: read a file into a [`SourceMap`], hand</span></div></div><div><div><div>4</div></div><div><span>//! the text and its id to the lexer and parser, then render whatever came back</span></div></div><div><div><div>5</div></div><div><span>//! against that same map so the carets land on the right characters.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::fs;</span></span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>path</span><span>::</span></span><span>Path</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>anyhow</span><span>::{</span></span><span>Context</span><span>, </span><span>Result</span><span>};</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::{</span></span><span>Diagnostic</span><span>, </span><span>Severity</span><span>};</span></div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceId</span><span>, </span><span>SourceMap</span><span>};</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>/// One registered source file plus the map that owns it.</span></div></div><div><div><div>15</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>Source</span><span> {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>/// Maps spans back to the text they came from.</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>map</span><span>: </span><span>SourceMap</span><span>,</span></div></div><div><div><div>18</div></div><div><span>    </span><span>/// The id the text was registered under.</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>id</span><span>: </span><span>SourceId</span><span>,</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>impl</span><span> </span><span>Source</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>/// Reads `path` and registers its text in a fresh map.</span></div></div><div><div><div>24</div></div><div><span>    </span><span>///</span></div></div><div><div><div>25</div></div><div><span>    </span><span>/// # Errors</span></div></div><div><div><div>26</div></div><div><span>    </span><span>/// Returns an error if the file cannot be read as UTF-8 text.</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>load</span><span>(</span><span>path</span><span>: &amp;</span><span>Path</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>Self</span><span>&gt; {</span></div></div><div><div><div>28</div></div><div><span>        </span><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span></span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>fs</span><span>::</span><span>read_to_string</span><span>(</span><span>path</span><span>).</span><span>with_context</span><span><span>(</span><span>||</span><span> </span></span><span>format!</span><span>(</span><span>"cannot read {}"</span><span>, </span><span>path</span><span>.</span><span>display</span><span>()))?;</span></div></div><div><div><div>30</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>31</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>map</span><span>.</span><span>add</span><span>(</span><span>path</span><span>.</span><span>display</span><span>().</span><span>to_string</span><span>(), </span><span>text</span><span>);</span></div></div><div><div><div>32</div></div><div><span>        </span><span>Ok</span><span>(</span><span>Self</span><span> { </span><span>map</span><span>, </span><span>id</span><span> })</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span>/// The registered text.</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>37</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>text</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>38</div></div><div><span>        </span><span>self</span><span>.map[</span><span>self</span><span>.id].</span><span>text</span><span>()</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>/// Whether any diagnostic is an error (as opposed to a warning).</span></div></div><div><div><div>43</div></div><div><span>#[must_use]</span></div></div><div><div><div>44</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>has_errors</span><span>(</span><span>diagnostics</span><span>: &amp;[</span><span>Diagnostic</span><span>]) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>45</div></div><div><span>    </span><span>diagnostics</span></div></div><div><div><div>46</div></div><div><span><span>        </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>.</span><span>any</span><span><span>(</span><span>|</span></span><span>diagnostic</span><span><span>|</span><span> </span></span><span>diagnostic</span><span><span>.severity </span><span>==</span><span> </span><span>Severity</span><span>::</span></span><span>Error</span><span>)</span></div></div><div><div><div>48</div></div><div><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>/// Renders every diagnostic, one block after another.</span></div></div><div><div><div>51</div></div><div><span>#[must_use]</span></div></div><div><div><div>52</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>render</span><span>(</span><span>diagnostics</span><span>: &amp;[</span><span>Diagnostic</span><span>], </span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>53</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>out</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>54</div></div><div><span>    </span><span>for</span><span> </span><span>diagnostic</span><span> </span><span>in</span><span> </span><span>diagnostics</span><span> {</span></div></div><div><div><div>55</div></div><div><span>        </span><span>out</span><span>.</span><span>push_str</span><span>(&amp;</span><span>diagnostic</span><span>.</span><span>render</span><span>(</span><span>sources</span><span>));</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>57</div></div><div><span>    </span><span>out</span></div></div><div><div><div>58</div></div><div><span>}</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>/// Prints every diagnostic to stderr; returns `true` if any of them is an error.</span></div></div><div><div><div>61</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>emit</span><span>(</span><span>diagnostics</span><span>: &amp;[</span><span>Diagnostic</span><span>], </span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>62</div></div><div><span>    </span><span>eprint!</span><span>(</span><span>"{}"</span><span>, </span><span>render</span><span>(</span><span>diagnostics</span><span>, </span><span>sources</span><span>));</span></div></div><div><div><div>63</div></div><div><span>    </span><span>has_errors</span><span>(</span><span>diagnostics</span><span>)</span></div></div><div><div><div>64</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个要说明的地方：</p><ul>
<li><code>Source::load(&amp;args.file)</code> 传的是 <code>&amp;PathBuf</code>，而函数要的是 <code>&amp;Path</code>：这是第 5 篇讲过的解引用强制转换（deref coercion），编译器自动插入解引用，和 <code>&amp;String</code> 变 <code>&amp;str</code> 一样。<code>Source::load</code> 用到 <code>anyhow</code>（下面细讲）；<code>fs::read_to_string(path)</code> 返回 <code>io::Result&lt;String&gt;</code>，我们用 <code>.with_context(|| format!(...))</code> 给错误加上 “cannot read 文件名” 这层说明，再用 <code>?</code> 抛出去。<code>with_context</code> 接受的是一个<strong>闭包</strong>，只有出错时才会调用，所以成功路径上不会白白格式化一个字符串。</li>
<li><code>Source::text(&amp;self) -&gt; &amp;str</code>：返回的借用来自 <code>self.map</code>，所以只要 <code>Source</code> 还活着它就有效。这是第 2 篇的 “返回借用” 的又一次出场。<code>self.map[self.id]</code> 是第 2 篇给 <code>SourceMap</code> 写的 <code>Index&lt;SourceId&gt;</code>。</li>
<li><code>emit</code> 把所有诊断 <code>eprint!</code> 到<strong>标准错误</strong>，返回有没有真正的错误。诊断是给人看的消息，输出到 stderr；程序真正的输出（token 列表、语法树）去 stdout。这样 <code>pebble parse x.pebble &gt; tree.txt</code> 得到的是一棵干净的树，错误还在终端里。</li>
</ul></section><section><h3>子命令：<code>commands.rs</code><a href="#子命令commandsrs"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! The work behind each subcommand.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Every function returns the process exit code it wants: `0` on success and</span></div></div><div><div><div>4</div></div><div><span>//! `1` when the input produced diagnostics. Only host problems (an unreadable</span></div></div><div><div><div>5</div></div><div><span>//! file, say) escape as an [`anyhow::Error`].</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>anyhow</span><span>::</span></span><span>Result</span><span>;</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>pebble_ast</span><span>::pretty;</span></span></div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>pebble_lexer</span><span>::tokenize;</span></span></div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>pebble_parser</span><span>::parse </span></span><span>as</span><span> parse_program;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>cli</span><span>::</span></span><span>FileArgs</span><span>;</span></div></div><div><div><div>13</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>sources</span><span>::{</span></span><span>self</span><span>, </span><span>Source</span><span>};</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>/// `pebble tokens`: one `line:col kind` line per token.</span></div></div><div><div><div>16</div></div><div><span>///</span></div></div><div><div><div>17</div></div><div><span>/// # Errors</span></div></div><div><div><div>18</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>19</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>tokens</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>diagnostics</span><span>, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>file</span><span><span> </span><span>=</span><span> &amp;</span></span><span>source</span><span>.map[</span><span>source</span><span>.id];</span></div></div><div><div><div>25</div></div><div><span>    </span><span>for</span><span> </span><span>token</span><span> </span><span>in</span><span> &amp;</span><span>tokens</span><span> {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>let</span><span> </span><span>position</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>position</span><span>(</span><span>token</span><span>.span.start);</span></div></div><div><div><div>27</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{}:{} {}"</span><span>, </span><span>position</span><span>.line, </span><span>position</span><span>.column, </span><span>token</span><span>.kind);</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>/// `pebble parse`: print the recovered syntax tree.</span></div></div><div><div><div>33</div></div><div><span>///</span></div></div><div><div><div>34</div></div><div><span>/// # Errors</span></div></div><div><div><div>35</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>36</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>parse</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>38</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>39</div></div><div><span>    </span><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>40</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>pretty</span><span>(&amp;</span><span>parsed</span><span>.program));</span></div></div><div><div><div>41</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>/// `pebble check`: lex and parse a file, reporting diagnostics and nothing else.</span></div></div><div><div><div>45</div></div><div><span>///</span></div></div><div><div><div>46</div></div><div><span>/// # Errors</span></div></div><div><div><div>47</div></div><div><span>/// Returns an error only for host-level failures such as an unreadable file.</span></div></div><div><div><div>48</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>fn</span><span> </span><span>check</span><span>(</span><span>args</span><span>: &amp;</span><span>FileArgs</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>49</div></div><div><span>    </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>Source</span><span>::</span><span>load</span><span>(&amp;</span><span>args</span><span>.file)?;</span></div></div><div><div><div>50</div></div><div><span>    </span><span>let</span><span> </span><span>parsed</span><span><span> </span><span>=</span><span> </span></span><span>parse_program</span><span>(</span><span>source</span><span>.id, </span><span>source</span><span>.</span><span>text</span><span>());</span></div></div><div><div><div>51</div></div><div><span>    </span><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>emit</span><span>(&amp;</span><span>parsed</span><span>.diagnostics, &amp;</span><span>source</span><span>.map);</span></div></div><div><div><div>52</div></div><div><span>    </span><span>Ok</span><span>(</span><span>if</span><span> </span><span>failed</span><span> { </span><span>1</span><span> } </span><span>else</span><span> { </span><span>0</span><span> })</span></div></div><div><div><div>53</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里有几件小事：</p><ul>
<li><code>use pebble_parser::parse as parse_program;</code>：本模块里有一个函数也叫 <code>parse</code>（对应子命令），名字撞了。<code>use ... as ...</code> 给导入的名字起别名。</li>
<li>每个子命令返回 <code>Result&lt;u8&gt;</code>，这里的 <code>Result</code> 是 <code>anyhow::Result</code>，也就是 <code>Result&lt;u8, anyhow::Error&gt;</code>。<code>Ok(code)</code> 是 “这个子命令想要的退出码”：<code>0</code> 表示成功，<code>1</code> 表示输入有诊断；<code>Err(_)</code> 留给<strong>宿主层面</strong>的失败，比如读不了文件。这一区别很重要：用户的代码有语法错误，是一次正常的、成功完成的 <code>pebble check</code>，只是结论是 “有错”。</li>
<li><code>Ok(if failed { 1 } else { 0 })</code>：<code>if</code> 是表达式（第 1 篇），所以可以直接塞进 <code>Ok(...)</code>。</li>
<li><code>for token in &amp;tokens</code>：循环借用 <code>Vec</code>，不消耗它。</li>
</ul></section><section><h3><code>anyhow</code> 和 <code>ExitCode</code><a href="#anyhow-和-exitcode"><span>#</span></a></h3><p><code>anyhow::Error</code> 是一个 “装得下任何错误” 的类型，外加一条 “上下文” 的链。<code>?</code> 作用在错误上时，会借助上面提到的 <code>From</code> 转换，把它装进 <code>anyhow::Error</code>（上面的 <code>Source::load</code> 里是 <code>with_context</code> 做的这一步，<code>?</code> 只是把它抛出去）。实验室里（新建一个依赖 <code>anyhow</code> 的 crate，<code>main.rs</code> 如下；完整版在 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch07-anyhow/src/main.rs" target="_blank"><code>lab/lab-ch07-anyhow</code></a>）看它的三种打印方式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>anyhow</span><span>::{</span></span><span>Context</span><span>, </span><span>Result</span><span>};</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::fs;</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>load</span><span>(</span><span>path</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>String</span><span>&gt; {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>fs</span><span>::</span><span>read_to_string</span><span>(</span><span>path</span><span>).</span><span>with_context</span><span><span>(</span><span>||</span><span> </span></span><span>format!</span><span>(</span><span>"cannot read {path}"</span><span>))</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>error</span><span><span> </span><span>=</span><span> </span></span><span>load</span><span>(</span><span>"nope.pebble"</span><span>).</span><span>unwrap_err</span><span>();</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{{error}}   : {error}"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{{error:#}}  : {error:#}"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{{error:?}}  :</span><span>\n</span><span>{error:?}"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet</span></div></div><div><div><div>2</div></div><div><span>{error}   : cannot read nope.pebble</span></div></div><div><div><div>3</div></div><div><span>{error:#}  : cannot read nope.pebble: No such file or directory (os error 2)</span></div></div><div><div><div>4</div></div><div><span>{error:?}  :</span></div></div><div><div><div>5</div></div><div><span>cannot read nope.pebble</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>Caused by:</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>No such file or directory (os error 2)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>{error}</code> 只有最外层的上下文；<code>{error:#}</code> 把整条链用冒号连成一行；<code>{error:?}</code> 是多行的。我们在 <code>main</code> 里用 <code>{error:#}</code>，一行就能给用户足够的信息。</p><p>现在是 <code>main.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! The `pebble` command-line driver.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! This binary is the front door to the toolchain: it wires the lexer, the</span></div></div><div><div><div>4</div></div><div><span>//! parser, and the diagnostics renderer together behind a handful of</span></div></div><div><div><div>5</div></div><div><span>//! subcommands.</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! Exit codes follow the usual convention: `0` for success, `1` when the input</span></div></div><div><div><div>8</div></div><div><span>//! produced diagnostics, and `2` (clap's default) for a usage error.</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>mod</span><span> cli;</span></div></div><div><div><div>13</div></div><div><span>mod</span><span> commands;</span></div></div><div><div><div>14</div></div><div><span>mod</span><span> sources;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>process</span><span>::</span></span><span>ExitCode</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>use</span><span><span> </span><span>clap</span><span>::</span></span><span>Parser</span><span>;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>use</span><span> </span><span>crate</span><span><span>::</span><span>cli</span><span>::{</span></span><span>Cli</span><span>, </span><span>Command</span><span>};</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>fn</span><span> </span><span>main</span><span>() -&gt; </span><span>ExitCode</span><span> {</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>cli</span><span><span> </span><span>=</span><span> </span></span><span>Cli</span><span>::</span><span>parse</span><span>();</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>cli</span><span>.command {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>Command</span><span>::</span><span>Tokens</span><span>(</span><span>args</span><span><span>) =&gt; </span><span>commands</span><span>::</span></span><span>tokens</span><span>(&amp;</span><span>args</span><span>),</span></div></div><div><div><div>26</div></div><div><span>        </span><span>Command</span><span>::</span><span>Parse</span><span>(</span><span>args</span><span><span>) =&gt; </span><span>commands</span><span>::</span></span><span>parse</span><span>(&amp;</span><span>args</span><span>),</span></div></div><div><div><div>27</div></div><div><span>        </span><span>Command</span><span>::</span><span>Check</span><span>(</span><span>args</span><span><span>) =&gt; </span><span>commands</span><span>::</span></span><span>check</span><span>(&amp;</span><span>args</span><span>),</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>29</div></div><div><span>    </span><span>match</span><span> </span><span>result</span><span> {</span></div></div><div><div><div>30</div></div><div><span>        </span><span>Ok</span><span>(</span><span>code</span><span>) =&gt; </span><span>ExitCode</span><span>::</span><span>from</span><span>(</span><span>code</span><span>),</span></div></div><div><div><div>31</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; {</span></div></div><div><div><div>32</div></div><div><span>            </span><span>eprintln!</span><span>(</span><span>"pebble: {error:#}"</span><span>);</span></div></div><div><div><div>33</div></div><div><span>            </span><span>ExitCode</span><span>::</span><span>from</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>fn main() -&gt; ExitCode</code> 对应 C 的 <code>int main()</code>：<code>ExitCode::from(u8)</code> 构造退出码。<code>Cli::parse()</code> 在用户输入有误时自己就以码 2 退出了；其余的都走下面的 <code>match</code>。<code>match</code> 的结果是一个 <code>Result&lt;u8&gt;</code>，再 <code>match</code> 一次：<code>Ok(code)</code> 用 <code>code</code> 退出，<code>Err(error)</code> 打印 <code>pebble: ...</code> 并以 1 退出。</p><p>最后是 <code>Cargo.toml</code>。包名叫 <code>pebble-cli</code>，但我们想让编译出来的可执行文件叫 <code>pebble</code>，所以用 <code>[[bin]]</code> 显式指定：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-cli"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"The pebble command-line driver: run, compile, disassemble, repl."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[[</span><span>bin</span><span>]]</span></div></div><div><div><div>12</div></div><div><span>name</span><span> = </span><span>"pebble"</span></div></div><div><div><div>13</div></div><div><span>path</span><span> = </span><span>"src/main.rs"</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>16</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>17</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>18</div></div><div><span>pebble-lexer.workspace</span><span> = </span><span>true</span></div></div><div><div><div>19</div></div><div><span>pebble-ast.workspace</span><span> = </span><span>true</span></div></div><div><div><div>20</div></div><div><span>pebble-parser.workspace</span><span> = </span><span>true</span></div></div><div><div><div>21</div></div><div><span>anyhow.workspace</span><span> = </span><span>true</span></div></div><div><div><div>22</div></div><div><span>clap.workspace</span><span> = </span><span>true</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>25</div></div><div><span>assert_cmd</span><span> = </span><span>"2"</span></div></div><div><div><div>26</div></div><div><span>predicates</span><span> = </span><span>"3"</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>29</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>[dev-dependencies]</code> 里的 <code>assert_cmd</code> 和 <code>predicates</code> 只在 <code>cargo test</code> 时才编译，它们不会进发布的二进制。</p></section><section><h3>示例文件<a href="#示例文件"><span>#</span></a></h3><p>仓库里的 <code>examples/</code> 下有五个 Pebble 程序，后面每一篇都会用到。现在我们先把它们建出来（每个文件都很短，直接抄；它们要和 <code>tutorial-v1</code> 逐字节一致）。</p><p><code>examples/hello.pebble</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// The smallest useful Pebble program.</span></div></div><div><div><div>2</div></div><div><span>print("hello, pebble!");</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>examples/fib.pebble</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Recursive Fibonacci: the classic "is the backend fast enough?" test.</span></div></div><div><div><div>2</div></div><div><span>fn fib(n) {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>if n &lt; 2 {</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>return n;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>return fib(n - 1) + fib(n - 2);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>print(fib(20));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>examples/errors.pebble</code>（故意写坏的程序）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A deliberately broken program.</span></div></div><div><div><div>2</div></div><div><span>//</span></div></div><div><div><div>3</div></div><div><span>// `pebble run examples/errors.pebble` renders the diagnostic below to stderr</span></div></div><div><div><div>4</div></div><div><span>// and exits with status 1 instead of running anything.</span></div></div><div><div><div>5</div></div><div><span>let total = 1 + ;</span></div></div><div><div><div>6</div></div><div><span>print(total);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>examples/closures.pebble</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// A closure captures its environment, so the counter's state survives</span></div></div><div><div><div>2</div></div><div><span>// between calls.</span></div></div><div><div><div>3</div></div><div><span>fn make_counter() {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>let count = 0;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>return fn() {</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>count = count + 1;</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>return count;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>let next = make_counter();</span></div></div><div><div><div>12</div></div><div><span>print(next());</span></div></div><div><div><div>13</div></div><div><span>print(next());</span></div></div><div><div><div>14</div></div><div><span>print(next());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>examples/collections.pebble</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Lists, maps, and `for`.</span></div></div><div><div><div>2</div></div><div><span>let squares = [];</span></div></div><div><div><div>3</div></div><div><span>for i in range(1, 6) {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>push(squares, i * i);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>print(squares);</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>let capitals = #{</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>"fr": "Paris",</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>"jp": "Tokyo",</span></div></div><div><div><div>11</div></div><div><span>};</span></div></div><div><div><div>12</div></div><div><span>capitals["de"] = "Berlin";</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>// Indexing a map with a string key, written out in a fixed order so the</span></div></div><div><div><div>15</div></div><div><span>// output does not depend on the map's internal ordering.</span></div></div><div><div><div>16</div></div><div><span>for country in ["fr", "jp", "de"] {</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>print(country + " -&gt; " + capitals[country]);</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div><span>print(len(capitals));</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>跑起来<a href="#跑起来"><span>#</span></a></h3><p>终于可以运行了。<code>cargo run -p pebble-cli -- &lt;参数&gt;</code> 中间的 <code>--</code> 之后的东西都是传给我们程序的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- tokens examples/hello.pebble</span></div></div><div><div><div>2</div></div><div><span>2:1 print</span></div></div><div><div><div>3</div></div><div><span>2:6 `(`</span></div></div><div><div><div>4</div></div><div><span>2:7 "hello, pebble!"</span></div></div><div><div><div>5</div></div><div><span>2:23 `)`</span></div></div><div><div><div>6</div></div><div><span>2:24 `;`</span></div></div><div><div><div>7</div></div><div><span>3:1 end of file</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>行号和列号来自第 2 篇的 <code>SourceFile::position</code>（列是字符数，不是字节数）。开头的注释被词法分析器跳过了，所以第一个 token 在第 2 行。最后的 <code>end of file</code> 是 <code>Eof</code>，在第 3 行是因为文件以换行结尾。</p><p><code>parse</code> 一个带闭包的文件，看到的是第 5 篇的 <code>AstPrinter</code> 的输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- parse examples/closures.pebble</span></div></div><div><div><div>2</div></div><div><span>fn make_counter()</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>let count</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>int 0</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>return</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>fn()</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>expr</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>assign</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>var count</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>binary +</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>var count</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>int 1</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>return</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>var count</span></div></div><div><div><div>15</div></div><div><span>let next</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>call(0 args)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>var make_counter</span></div></div><div><div><div>18</div></div><div><span>expr</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>call(1 args)</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>var print</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>call(0 args)</span></div></div><div><div><div>22</div></div><div><span><span>      </span></span><span>var next</span></div></div><div><div><div>23</div></div><div><span>expr</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>call(1 args)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>var print</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>call(0 args)</span></div></div><div><div><div>27</div></div><div><span><span>      </span></span><span>var next</span></div></div><div><div><div>28</div></div><div><span>expr</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>call(1 args)</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>var print</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>call(0 args)</span></div></div><div><div><div>32</div></div><div><span><span>      </span></span><span>var next</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>check</code> 是安静的：成功时什么也不输出，退出码 0；有错时在 stderr 里出报告，退出码 1：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- check examples/fib.pebble; echo $?</span></div></div><div><div><div>2</div></div><div><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- check examples/errors.pebble; echo $?</span></div></div><div><div><div>2</div></div><div><span>error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用法错误和宿主错误是两回事。未知子命令由 clap 处理，退出码 2；读不到文件由我们的 <code>main</code> 处理，退出码 1，而且是一句人话，不是一团结构：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- frobnicate; echo $?</span></div></div><div><div><div>2</div></div><div><span>error: unrecognized subcommand 'frobnicate'</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>Usage: pebble &lt;COMMAND&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>For more information, try '--help'.</span></div></div><div><div><div>7</div></div><div><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- tokens nope.pebble; echo $?</span></div></div><div><div><div>2</div></div><div><span>pebble: cannot read nope.pebble: No such file or directory (os error 2)</span></div></div><div><div><div>3</div></div><div><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>回到开头的 <code>broken.pebble</code>（建在 <code>~/pebble</code> 根目录下，三行内容见本文开头），<code>cargo run --quiet -p pebble-cli -- parse broken.pebble</code> 现在会给出和文章开头一模一样的输出。</p><p><code>--help</code> 是免费的，而且文字正是我们写在 <code>///</code> 注释里的那些：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet -p pebble-cli -- --help</span></div></div><div><div><div>2</div></div><div><span>Pebble: a small scripting language and toolchain</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>Usage: pebble &lt;COMMAND&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Commands:</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>tokens  Print the token stream of a source file</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>parse   Print the parsed abstract syntax tree of a source file</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>check   Lex and parse a source file without running it</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>help    Print this message or the help of the given subcommand(s)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>Options:</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>-h, --help     Print help</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>-V, --version  Print version</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>集成测试<a href="#集成测试"><span>#</span></a></h3><p>最后一块：测试这个二进制本身。Rust 的测试有三种：单元测试（放在源文件里的 <code>#[cfg(test)] mod tests</code>，可以访问私有项）、<strong>集成测试</strong>（放在 <code>tests/</code> 目录下，每个文件被编译成一个独立的 crate，只能用公开的接口）、文档测试（第 10 篇会写第一个）。对一个二进制 crate 来说，集成测试有一个更强的含义：它没法 <code>use</code> 你 <code>main.rs</code> 里的函数，所以只能把整个程序当作黑盒启动起来，看它的输出和退出码。这正是我们想要的：测试用户实际会碰到的东西。</p><p>创建 <code>crates/pebble-cli/tests/cli.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! End-to-end tests for the `pebble` binary.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! The tests drive the real executable against the example programs in the</span></div></div><div><div><div>4</div></div><div><span>//! workspace's `examples/` directory, which is why every command runs with the</span></div></div><div><div><div>5</div></div><div><span>//! workspace root as its working directory.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>path</span><span>::{</span></span><span>Path</span><span>, </span><span>PathBuf</span><span>};</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>process</span><span>::</span></span><span>Output</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>assert_cmd</span><span>::</span></span><span>Command</span><span>;</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>predicates</span><span>::</span><span>prelude</span><span>::*;</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>/// The workspace root, two levels above this crate.</span></div></div><div><div><div>14</div></div><div><span>fn</span><span> </span><span>workspace_root</span><span>() -&gt; </span><span>PathBuf</span><span> {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Path</span><span>::</span><span>new</span><span>(</span><span>env!</span><span>(</span><span>"CARGO_MANIFEST_DIR"</span><span>)).</span><span>join</span><span>(</span><span>"../.."</span><span>)</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>/// A `pebble` invocation rooted at the workspace, so `examples/...` resolves.</span></div></div><div><div><div>19</div></div><div><span>fn</span><span> </span><span>pebble</span><span>() -&gt; </span><span>Command</span><span> {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>command</span><span><span> </span><span>=</span><span> </span></span><span>Command</span><span>::</span><span>cargo_bin</span><span>(</span><span>"pebble"</span><span>).</span><span>expect</span><span>(</span><span>"the pebble binary is built"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>command</span><span>.</span><span>current_dir</span><span>(</span><span>workspace_root</span><span>());</span></div></div><div><div><div>22</div></div><div><span>    </span><span>command</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>/// Runs the binary with `args` and captures its output.</span></div></div><div><div><div>26</div></div><div><span>fn</span><span> </span><span>pebble_output</span><span>(</span><span>args</span><span>: &amp;[&amp;</span><span>str</span><span>]) -&gt; </span><span>Output</span><span> {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pebble</span><span>().</span><span>args</span><span>(</span><span>args</span><span>).</span><span>output</span><span>().</span><span>expect</span><span>(</span><span>"pebble runs"</span><span>)</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>#[test]</span></div></div><div><div><div>31</div></div><div><span>fn</span><span> </span><span>parse_prints_the_syntax_tree</span><span>() {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"parse"</span><span>, </span><span>"examples/closures.pebble"</span><span>])</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>.</span><span>success</span><span>()</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>.</span><span>stdout</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>contains</span><span>(</span><span>"fn"</span><span>));</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>#[test]</span></div></div><div><div><div>40</div></div><div><span>fn</span><span> </span><span>tokens_prints_one_line_per_token</span><span>() {</span></div></div><div><div><div>41</div></div><div><span>    </span><span>let</span><span> </span><span>output</span><span><span> </span><span>=</span><span> </span></span><span>pebble_output</span><span>(&amp;[</span><span>"tokens"</span><span>, </span><span>"examples/hello.pebble"</span><span>]);</span></div></div><div><div><div>42</div></div><div><span>    </span><span>assert!</span><span>(</span><span>output</span><span>.status.</span><span>success</span><span>(), </span><span>"tokens failed: {output:?}"</span><span>);</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span>let</span><span> </span><span>stdout</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from_utf8</span><span>(</span><span>output</span><span>.stdout).</span><span>expect</span><span>(</span><span>"stdout is UTF-8"</span><span>);</span></div></div><div><div><div>45</div></div><div><span>    </span><span>let</span><span> </span><span>lines</span><span>: </span><span>Vec</span><span>&lt;&amp;</span><span>str</span><span><span>&gt; </span><span>=</span><span> </span></span><span>stdout</span><span>.</span><span>lines</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>46</div></div><div><span>    </span><span>assert!</span><span><span>(</span><span>!</span></span><span>lines</span><span>.</span><span>is_empty</span><span>(), </span><span>"expected at least one token line"</span><span>);</span></div></div><div><div><div>47</div></div><div><span>    </span><span>assert!</span><span>(</span></div></div><div><div><div>48</div></div><div><span>        </span><span>lines</span><span>[</span><span>0</span><span>].</span><span>contains</span><span>(</span><span>':'</span><span><span>) </span><span>&amp;&amp;</span><span> </span></span><span>lines</span><span>[</span><span>0</span><span>].</span><span>contains</span><span>(</span><span>' '</span><span>),</span></div></div><div><div><div>49</div></div><div><span>        </span><span>"expected `line:col kind`, got {first:?}"</span><span>,</span></div></div><div><div><div>50</div></div><div><span>        </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>lines</span><span>[</span><span>0</span><span>]</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>52</div></div><div><span>}</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>#[test]</span></div></div><div><div><div>55</div></div><div><span>fn</span><span> </span><span>check_is_silent_on_a_valid_file</span><span>() {</span></div></div><div><div><div>56</div></div><div><span>    </span><span>pebble</span><span>()</span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>.</span><span>args</span><span>([</span><span>"check"</span><span>, </span><span>"examples/fib.pebble"</span><span>])</span></div></div><div><div><div>58</div></div><div><span><span>        </span></span><span>.</span><span>assert</span><span>()</span></div></div><div><div><div>59</div></div><div><span><span>        </span></span><span>.</span><span>success</span><span>()</span></div></div><div><div><div>60</div></div><div><span><span>        </span></span><span>.</span><span>stdout</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>is_empty</span><span>())</span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>.</span><span>stderr</span><span><span>(</span><span>predicate</span><span>::</span><span>str</span><span>::</span></span><span>is_empty</span><span>());</span></div></div><div><div><div>62</div></div><div><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>#[test]</span></div></div><div><div><div>65</div></div><div><span>fn</span><span> </span><span>unknown_subcommands_are_usage_errors</span><span>() {</span></div></div><div><div><div>66</div></div><div><span>    </span><span>pebble</span><span>().</span><span>arg</span><span>(</span><span>"frobnicate"</span><span>).</span><span>assert</span><span>().</span><span>code</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>67</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>env!("CARGO_MANIFEST_DIR")</code> 是一个<strong>编译期</strong>的宏，展开成 <code>Cargo.toml</code> 所在目录的绝对路径。我们用它找到工作区根目录（往上两层），这样 <code>examples/fib.pebble</code> 这样的相对路径就能解析，不管 <code>cargo test</code> 是从哪里启动的。</li>
<li><code>Command::cargo_bin("pebble")</code> 是 <code>assert_cmd</code> 提供的：找到刚编译好的 <code>pebble</code> 二进制，包装成一个可以断言的命令。（注意这个 <code>Command</code> 和 clap 的 <code>Command</code> 枚举重名，但它们在不同的 crate 里：测试文件是独立的 crate，互不干扰。）</li>
<li><code>.expect("...")</code>：测试里的 <code>unwrap</code> 的带说明版本。</li>
<li><code>predicate::str::contains("fn")</code> 之类来自 <code>predicates</code>，用 <code>.stdout(...)</code> 断言标准输出满足谓词。</li>
<li><code>unknown_subcommands_are_usage_errors</code> 里的 <code>.code(2)</code>：clap 的约定退出码。</li>
</ul></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 <code>~/pebble</code> 里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-parser</span><span> </span><span>-p</span><span> </span><span>pebble-cli</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-parser -p pebble-cli</span></div></div><div><div><div>2</div></div><div><span>...</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.85s</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>Running unittests src/main.rs (target/debug/deps/pebble-3cbe36229cfe436a)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>running 0 tests</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>     </span></span><span>Running tests/cli.rs (target/debug/deps/cli-8b4f67d6453b19d9)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>running 4 tests</span></div></div><div><div><div>13</div></div><div><span>test unknown_subcommands_are_usage_errors ... ok</span></div></div><div><div><div>14</div></div><div><span>test parse_prints_the_syntax_tree ... ok</span></div></div><div><div><div>15</div></div><div><span>test check_is_silent_on_a_valid_file ... ok</span></div></div><div><div><div>16</div></div><div><span>test tokens_prints_one_line_per_token ... ok</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_parser-d9282dee6a0168a0)</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>running 13 tests</span></div></div><div><div><div>23</div></div><div><span>test tests::parse_expression_entry_point_works ... ok</span></div></div><div><div><div>24</div></div><div><span>test tests::parentheses_override_precedence ... ok</span></div></div><div><div><div>25</div></div><div><span>test tests::parses_break_and_continue ... ok</span></div></div><div><div><div>26</div></div><div><span>test tests::parses_closures_lists_and_maps ... ok</span></div></div><div><div><div>27</div></div><div><span>test tests::parses_control_flow ... ok</span></div></div><div><div><div>28</div></div><div><span>test tests::parses_functions_and_returns ... ok</span></div></div><div><div><div>29</div></div><div><span>test tests::parses_let_and_variables ... ok</span></div></div><div><div><div>30</div></div><div><span>test tests::parses_precedence_correctly ... ok</span></div></div><div><div><div>31</div></div><div><span>test tests::recovers_and_reports_multiple_errors ... ok</span></div></div><div><div><div>32</div></div><div><span>test tests::rejects_assignment_to_literal ... ok</span></div></div><div><div><div>33</div></div><div><span>test tests::reports_missing_semicolon_with_a_span ... ok</span></div></div><div><div><div>34</div></div><div><span>test tests::subtraction_is_left_associative ... ok</span></div></div><div><div><div>35</div></div><div><span>test tests::skips_empty_statements_never_panics_on_garbage ... ok</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span><span>   </span></span><span>Doc-tests pebble_parser</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>running 0 tests</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pebble-cli</code> 的主程序没有单元测试（第一行 <code>running 0 tests</code>），4 个集成测试通过；<code>pebble-parser</code> 13 个测试通过。</p><p><strong>必须和 <code>tutorial-v1</code> 逐字节一致的文件（已完成）：</strong></p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-parser/src/lib.rs" target="_blank"><code>crates/pebble-parser/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-parser/Cargo.toml" target="_blank"><code>crates/pebble-parser/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/examples/hello.pebble" target="_blank"><code>examples/hello.pebble</code></a>、<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/examples/fib.pebble" target="_blank"><code>fib.pebble</code></a>、<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/examples/errors.pebble" target="_blank"><code>errors.pebble</code></a>、<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/examples/closures.pebble" target="_blank"><code>closures.pebble</code></a>、<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/examples/collections.pebble" target="_blank"><code>collections.pebble</code></a></li>
</ul><p>用本地克隆 <code>~/pebble-tag</code> 来验证：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~/pebble</span></div></div><div><div><div>2</div></div><div><span>for</span><span> </span><span>f</span><span> </span><span>in</span><span> </span><span>crates/pebble-parser/src/lib.rs</span><span> </span><span>crates/pebble-parser/Cargo.toml</span><span> \</span></div></div><div><div><div>3</div></div><div><span>         </span><span>examples/hello.pebble</span><span> </span><span>examples/fib.pebble</span><span> </span><span>examples/closures.pebble</span><span> \</span></div></div><div><div><div>4</div></div><div><span>         </span><span>examples/collections.pebble</span><span> </span><span>examples/errors.pebble</span><span>; </span><span>do</span></div></div><div><div><div>5</div></div><div><span>  </span><span>diff</span><span> </span><span>-q</span><span> </span><span>~/pebble-tag/</span><span>$f</span><span> </span><span>~/pebble/</span><span>$f</span><span> &gt;</span><span>/dev/null</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>"same  </span><span>$f</span><span>"</span></div></div><div><div><div>6</div></div><div><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cd ~/pebble</span></div></div><div><div><div>2</div></div><div><span>same  crates/pebble-parser/src/lib.rs</span></div></div><div><div><div>3</div></div><div><span>same  crates/pebble-parser/Cargo.toml</span></div></div><div><div><div>4</div></div><div><span>same  examples/hello.pebble</span></div></div><div><div><div>5</div></div><div><span>same  examples/fib.pebble</span></div></div><div><div><div>6</div></div><div><span>same  examples/closures.pebble</span></div></div><div><div><div>7</div></div><div><span>same  examples/collections.pebble</span></div></div><div><div><div>8</div></div><div><span>same  examples/errors.pebble</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>七行都是 <code>same</code>，说明没有差异。前几篇完成的 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-lexer</code>、<code>pebble-ast</code>、<code>pebble-macros</code> 也应该和标签一致，我在重建时用 <code>diff -r</code> 逐个确认过。</p><p><strong>中间状态（可以编译、测试通过，但之后会改动）：</strong></p><ul>
<li><code>crates/pebble-cli/</code> 下的全部六个文件：<code>Cargo.toml</code>、<code>src/main.rs</code>、<code>src/cli.rs</code>、<code>src/commands.rs</code>、<code>src/sources.rs</code>、<code>tests/cli.rs</code>。最终版本多出来的东西：运行子命令和 REPL（第 9 篇）、字节码和反汇编（第 10 篇）、两个后端的 <code>Backend</code> trait 和 <code>bench</code>（第 12 篇）、并行的 <code>check</code>（第 14 篇）、可选的 <code>repl</code> 特性（第 15 篇）。</li>
<li>根目录 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 现在只列了我们用到的那些，之后每一篇会补上新的依赖。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇用到的实验文件都在仓库的 <code>lab/</code> 目录里（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）；如果你把仓库克隆到了 <code>~/pebble-tag</code>，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，先 <code>cd ~/pebble-tag/lab/rust-lab</code> 再运行下表的命令（也可以把这个目录复制成 <code>~/rust-lab</code>）。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_must_use.rs" target="_blank"><code>ch07_must_use.rs</code></a></td><td><code>cargo run --quiet --bin ch07_must_use</code></td><td>运行，打印 <code>done</code>；编译警告 <code>unused `Result` that must be used</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_question.rs" target="_blank"><code>ch07_question.rs</code></a></td><td><code>cargo run --quiet --bin ch07_question</code></td><td>运行，首行 <code>Ok(3)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_discriminant.rs" target="_blank"><code>ch07_discriminant.rs</code></a></td><td><code>cargo run --quiet --bin ch07_discriminant</code></td><td>运行，首行 <code>== : false</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_args.rs" target="_blank"><code>ch07_args.rs</code></a></td><td><code>cargo run --quiet --bin ch07_args</code></td><td>不带参数：退出码 2，打印 <code>usage: target/debug/ch07_args count &lt;file&gt;</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_args.rs" target="_blank"><code>ch07_args.rs</code></a></td><td><code>cargo run --quiet --bin ch07_args -- count src/bin/ch07_args.rs</code></td><td>运行，打印 <code>365 bytes, 13 lines</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_args.rs" target="_blank"><code>ch07_args.rs</code></a></td><td><code>cargo run --quiet --bin ch07_args -- count nope.pebble</code></td><td><code>unwrap</code> 在 <code>Os { code: 2, kind: NotFound, .. }</code> 上 panic，退出码 101</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_move_reassign.rs" target="_blank"><code>ch07_move_reassign.rs</code></a></td><td><code>cargo run --quiet --bin ch07_move_reassign</code></td><td>运行，首行 <code>Add(Add(Add(Int(1), Int(2)), Int(3)), Int(4))</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_move_fail.rs" target="_blank"><code>ch07_move_fail.rs</code></a></td><td><code>cargo build --bin ch07_move_fail</code></td><td>编译失败：E0382</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_two_phase.rs" target="_blank"><code>ch07_two_phase.rs</code></a></td><td><code>cargo run --quiet --bin ch07_two_phase</code></td><td>运行，首行 <code>[1, 2, 3, 3]</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_two_phase_fail.rs" target="_blank"><code>ch07_two_phase_fail.rs</code></a></td><td><code>cargo build --bin ch07_two_phase_fail</code></td><td>编译失败：E0502</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_deferred_fail.rs" target="_blank"><code>ch07_deferred_fail.rs</code></a></td><td><code>cargo build --bin ch07_deferred_fail</code></td><td>编译失败：E0381</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_deferred_ok.rs" target="_blank"><code>ch07_deferred_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch07_deferred_ok</code></td><td>运行，打印 <code>no arguments</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch07-anyhow/src/main.rs" target="_blank"><code>main.rs</code></a></td><td><code>cd ../lab-ch07-anyhow &amp;&amp; cargo run --quiet</code></td><td>运行，首行 <code>{error}   : cannot read nope.pebble</code>（侧 crate，依赖 <code>anyhow</code>）</td></tr></tbody></table><p>仓库里还有 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch07_deref.rs" target="_blank"><code>ch07_deref.rs</code></a>（运行打印 <code>examples/fib.pebble (2 components)</code>，演示 <code>&amp;PathBuf</code> 到 <code>&amp;Path</code> 的解引用强制转换），文章没有直接用到它。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>练习 1。</strong> 很多语言有 <code>x %= 2;</code> 这样的复合赋值，Pebble 没有。写一个测试，固定 <code>x %= 2;</code> 现在产生的诊断。先预测它说什么。</p>
参考答案<p>词法分析器把 <code>%=</code> 切成 <code>%</code> 和 <code>=</code> 两个 token。<code>x</code> 之后 <code>binary</code> 看到 <code>%</code>（优先级 6）就吞掉它，右边 <code>unary → call → primary</code> 拿到的是 <code>=</code>，<code>primary</code> 的最后一个分支报 “expected an expression, found <code>=</code>”。所以恰好一条诊断：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>compound_assignment_is_not_pebble_syntax</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>parse</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"x %= 2;"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>result</span><span>.diagnostics.</span><span>len</span><span>(), </span><span>1</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>6</div></div><div><span>        </span><span>result</span><span>.diagnostics[</span><span>0</span><span>].message,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"expected an expression, found `=`"</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把它加到测试模块末尾跑过，通过。这个错误信息其实相当友好：它指向 <code>=</code>，用户一看就知道 “它在 <code>%</code> 后面等一个表达式”。</p><p><strong>练习 2。</strong> <code>pebble tokens</code> 遇到词法错误时，诊断打到 stderr，token 打到 stdout，两者混在终端里。让它<strong>只</strong>往 stdout 打 token（退出码仍要反映有错误）。</p>
参考答案<p>不要 <code>emit</code> 诊断，只问 “有没有错误”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// commands.rs, in `tokens`</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>failed</span><span><span> </span><span>=</span><span> </span><span>sources</span><span>::</span></span><span>has_errors</span><span>(&amp;</span><span>diagnostics</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我拿一个有未终止字符串的文件（<code>let s = "oops;</code>）验证过：改动前 stderr 里有 <code>error[E0003]: unterminated string literal</code>，改动后 stderr 是空的，stdout 的 token 列表不变，退出码仍然是 1。</p><p><strong>练习 3。</strong> 给 <code>pebble parse</code> 加一个 <code>--quiet</code> 选项：不打印语法树，只报告诊断。<code>tokens</code> 和 <code>check</code> 不受影响。</p>
参考答案<p>三个子命令共用 <code>FileArgs</code>，所以要给 <code>parse</code> 单独一个参数结构体：</p><div><figure><figcaption><span>cli.rs</span></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Arguments to `pebble parse`.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Debug</span><span>, </span><span>Args</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span>(</span><span>crate</span><span>) </span><span>struct</span><span> </span><span>ParseArgs</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// The source file to read.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>file</span><span>: </span><span>PathBuf</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Report diagnostics only; do not print the tree.</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>#[arg(long)]</span></div></div><div><div><div>8</div></div><div><span>    </span><span>pub</span><span>(</span><span>crate</span><span>) </span><span>quiet</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把 <code>Command::Parse(FileArgs)</code> 改成 <code>Parse(ParseArgs)</code>，在 <code>commands.rs</code> 里 <code>parse</code> 的参数类型换成 <code>&amp;ParseArgs</code>（并把 <code>use crate::cli::FileArgs;</code> 改成 <code>use crate::cli::{FileArgs, ParseArgs};</code>），打印那一行变成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span><span> </span><span>!</span></span><span>args</span><span>.quiet {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>pretty</span><span>(&amp;</span><span>parsed</span><span>.program));</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>main.rs</code> 不用改（<code>commands::parse(&amp;args)</code> 的写法没变）。我跑过：<code>pebble parse --quiet examples/fib.pebble</code> 输出为空，<code>pebble parse --quiet broken.pebble</code> 照常输出两条诊断。</p></section>
<section><h2>下一站<a href="#下一站"><span>#</span></a></h2><p>这篇我们让 Pebble 第一次 “读懂” 了一个文件，但它还不会做任何事：<code>pebble parse</code> 只是把树打印出来。要执行它，我们需要一个<strong>值</strong>的概念：数字、字符串、列表、map，以及它们在运行时是怎么活着的。</p><p>这里有一个我一直想先让你踩的坑。在 Pebble 里，<code>let a = []; let b = a; push(b, 1); a</code> 必须是 <code>[1]</code>：两个变量指向<strong>同一个</strong>列表。如果你想用我们到目前为止学到的所有权规则来表达 “两个名字共享一个可变对象”，你会发现办不到，Rust 的规则恰好是为了禁止这件事而设计的。第 8 篇会先让编译器把这个想法打回来，然后我们用 <code>Rc</code> 和 <code>RefCell</code> 把它做成：<a href="/posts/rust-by-pebble-08-rc-refcell-values/">共享与可变：Rc、RefCell 和运行时的值</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-06-proc-macros/</id>
      <title type="text">过程宏：让编译器替我写 impl Spanned</title>
      <published>2026-10-13T05:00:00.000Z</published>
      <updated>2026-10-13T05:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-06-proc-macros/"/>
      <summary type="text">上一篇我们手写了三个一模一样的 impl Spanned。这一篇写一个 derive 过程宏，让编译器在编译时替我们生成它：从 TokenStream、syn 与 quote 讲起，撞上泛型、引用绑定、卫生性和自定义编译错误，最后删掉三段手写代码，测试一个字不改。</summary>
      <content type="html"><![CDATA[<p>上一篇结尾我们手写了三个 <code>impl Spanned</code>，一共 44 行，每个分支都在做同一件事：把叫 <code>span</code> 的字段取出来。我给 <code>Stmt</code> 加一个变体，编译器老老实实指出三处要改，其中一处纯属体力活。C++ 程序员遇到这种事有两条路：写一个宏，或者让代码生成器生成。Rust 给了第三条路：写一个<strong>在编译器内部运行的 Rust 函数</strong>，输入是你的类型定义，输出是要补进程序里的代码。这一篇我们写出这个函数，然后把那 44 行删掉。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>你将写出 <code>pebble-macros</code> 的<strong>中间版本</strong>（只有 <code>#[derive(Spanned)]</code> 这一个宏，约 100 行），然后回到 <code>pebble-ast</code>：加一行依赖、一行 <code>pub use</code>、三处 <code>#[derive(.., Spanned)]</code>，删掉三个手写的 <code>impl</code>。做完以后 <code>pebble-ast</code> 与仓库逐字节相同，<code>cargo test -p pebble-ast</code> 的 4 个测试一个字不改。用到的 Rust 特性：<code>proc-macro = true</code> 的 crate、<code>TokenStream</code>、<code>syn</code>、<code>quote!</code>、泛型的 <code>split_for_impl</code>、绝对路径与卫生性、<code>syn::Error</code>、<code>collect</code> 成 <code>Result</code>。
上一篇是 <a href="/posts/rust-by-pebble-05-ast-generics/">递归类型、泛型与 trait：搭一棵 AST</a>，这一篇从它的中间版本继续。下一篇是 <a href="/posts/rust-by-pebble-07-parser-errors-cli/">Result、? 与递归下降</a>。</p></div></div>
<section><h2>先把痛点原样复现一遍<a href="#先把痛点原样复现一遍"><span>#</span></a></h2><p>上一篇的实验我再做一次，这回是在做完本篇之后的 <code>pebble-ast</code> 里。往 <code>Stmt</code> 末尾加一个带 <code>span</code> 的变体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// `{ .. }`</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Block</span><span> { </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo build -p pebble-ast</code>（这是我在工作区的一份拷贝里做的，不是你的主目录）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling pebble-ast v0.1.0 (/tmp/scratch-ast06/crates/pebble-ast)</span></div></div><div><div><div>2</div></div><div><span>error[E0004]: non-exhaustive patterns: `&amp;Stmt::Block { .. }` not covered</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-ast/src/lib.rs:271:11</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>271 |     match stmt {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|           ^^^^ pattern `&amp;Stmt::Block { .. }` not covered</span></div></div><div><div><div>7</div></div><div><span>...</span></div></div><div><div><div>8</div></div><div><span>error[E0004]: non-exhaustive patterns: `&amp;Stmt::Block { .. }` not covered</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-ast/src/lib.rs:394:27</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div><div><div><div>12</div></div><div><span>error: could not compile `pebble-ast` (lib) due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两处，不是上一篇的三处。<code>Spanned</code> 那个 <code>match</code> 已经不存在了：剩下的两处是 <code>walk_stmt</code> 和打印器，它们需要人来决定”怎样遍历、怎样显示”。上一篇的三处里最没有信息量的那一处，被我们这一篇写的东西接管了。下面从头讲这是怎么做到的。</p></section>
<section><h2>宏有几种<a href="#宏有几种"><span>#</span></a></h2><p>我先给 C 程序员一个参照。C 的 <code>#define</code> 是<strong>文本替换</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#define</span><span> </span><span>SPAN_OF</span><span><span>(</span><span>x</span><span>) ((x).</span></span><span>span</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>预处理器不知道 <code>x</code> 是什么，括号加得不对就产生奇怪的结果，也永远不知道 <code>span</code> 是不是真的存在。Rust 的宏不一样：它们工作在 <strong>token 树</strong>上，括号永远配对，替换的单位是”一个表达式”而不是”几个字符”。而且 Rust 有四种宏：</p>

<table><thead><tr><th>种类</th><th>写法</th><th>谁写、在哪里</th><th>本系列在哪讲</th></tr></thead><tbody><tr><td>声明宏</td><td><code>macro_rules! name { ... }</code></td><td>模式匹配 token，同一个 crate 里就能写</td><td>第 10 篇（<code>chunk!</code>）</td></tr><tr><td>derive 宏</td><td><code>#[derive(Name)]</code></td><td>过程宏：给一个类型生成额外的 <code>impl</code></td><td>这一篇</td></tr><tr><td>函数式过程宏</td><td><code>name!(...)</code></td><td>过程宏：输入 token，输出任意 item</td><td>第 10 篇（<code>opcode_set!</code>）</td></tr><tr><td>属性宏</td><td><code>#[name]</code></td><td>过程宏：整个 item 进、整个 item 出</td><td>第 12 篇（<code>#[differential]</code>）</td></tr></tbody></table><p>你已经天天在用其中三种了：<code>println!</code> 是（声明）宏，<code>#[derive(Debug)]</code> 是 derive 宏，<code>#[test]</code> 是属性宏。它们没什么神秘的，只是别人写好的 Rust 函数。<strong>过程宏</strong>就是这样一个函数，签名大概是”一串 token 进，一串 token 出”，编译器在展开时调用它。</p><p>打个比方：过程宏像 C 工程里的 <code>protoc</code> 或 <code>flex</code>/<code>bison</code>：构建过程中先跑一个程序，它读你的声明，吐出一些源文件，再被编译。区别在于，它不是一个外部命令加一个 Makefile 规则，而是 <code>cargo build</code> 自动在编译器<strong>进程内</strong>调用的一个动态库，输入输出是 token，不是文本。</p><section><h3>为什么必须是单独的 crate<a href="#为什么必须是单独的-crate"><span>#</span></a></h3><p>过程宏必须放在一个 <code>proc-macro = true</code> 的 crate 里。为什么不能直接写在 <code>pebble-ast</code> 里？因为编译 <code>pebble-ast</code> 的时候，编译器要<strong>运行</strong>宏：宏得先被编译成可以加载的库。同一个 crate 不可能先于自己编译完成，所以必须拆成两个 crate，先编译宏的那个。</p><p>这种 crate 还有一条硬规则。我建了一个 <code>proc-macro = true</code> 的 crate（仓库里是 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-export" target="_blank"><code>lab/lab-ch06-export</code></a>），里面放了一个普通的 <code>pub fn helper()</code>，再加一个 derive：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>proc_macro</span><span>::</span></span><span>TokenStream</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>/// Not a macro: just a helper.</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>helper</span><span>() -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>42</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#[proc_macro_derive(</span><span>Nothing</span><span>)]</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>derive_nothing</span><span>(</span><span>_input</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>TokenStream</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling lab-macros-export v0.1.0 (/tmp/lab-ch06-export)</span></div></div><div><div><div>2</div></div><div><span>error: `proc-macro` crate types currently cannot export any items other than functions tagged with `#[proc_macro]`, `#[proc_macro_derive]`, or `#[proc_macro_attribute]`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/lib.rs:4:1</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>4 | pub fn helper() -&gt; u32 {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>error: could not compile `lab-macros-export` (lib) due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个过程宏 crate 只能导出宏。要共享的辅助函数、类型，必须另起一个普通 crate。（<code>pebble-macros</code> 里的辅助函数 <code>expand_spanned</code>、<code>span_member</code> 没有 <code>pub</code>，所以没事。）</p><p>第二件事：过程宏 crate 里能用一个特殊的内置 crate，叫 <code>proc_macro</code>，里面有 <code>TokenStream</code>。这个 crate 只有在编译器里才存在。在普通二进制里用它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>tokens</span><span><span> </span><span>=</span><span> </span><span>proc_macro</span><span>::</span></span><span>TokenStream</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>tokens</span><span>.</span><span>is_empty</span><span>());</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实验文件 <code>ch06_proc_macro_outside.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0433]: cannot find module or crate `proc_macro` in this scope</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_proc_macro_outside.rs:2:18</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>2 |     let tokens = proc_macro::TokenStream::new();</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                  ^^^^^^^^^^ use of unresolved module or unlinked crate `proc_macro`</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>help: there is a crate or module with a similar name</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>2 |     let tokens = proc_macro2::TokenStream::new();</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|                            +</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器自己建议的 <code>proc_macro2</code> 正是下面要讲的第二种 <code>TokenStream</code>。</p></section></section>
<section><h2>第一个 derive：Hello<a href="#第一个-derivehello"><span>#</span></a></h2><p>我们先不碰 Pebble，在实验室里写一个最小的 derive，只为看清机器怎么转。新建一个实验 crate（你的可以放在 <code>~/lab-ch06-derive</code>；完整版在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive" target="_blank"><code>lab/lab-ch06-derive</code></a>，不想手敲就 <code>cd ~/pebble-tag/lab/lab-ch06-derive</code> 直接用），它自己的 <code>Cargo.toml</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"lab-ch06-derive"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>7</div></div><div><span>pebble-span</span><span> = { </span><span>path</span><span> = </span><span>"../pebble/crates/pebble-span"</span><span> }</span></div></div><div><div><div>8</div></div><div><span>lab-macros-step</span><span> = { </span><span>path</span><span> = </span><span>"lab-macros-step"</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>pebble-span</code> 那一行指向你在 <code>~/pebble</code> 里的 <code>pebble-span</code>；仓库里的版本写的是 <code>../../crates/pebble-span</code>，指向 tag 里的 <code>pebble-span</code>，别的没区别。）再建 <code>lab-macros-step/Cargo.toml</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"lab-macros-step"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[</span><span>lib</span><span>]</span></div></div><div><div><div>7</div></div><div><span>proc-macro</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>10</div></div><div><span>proc-macro2</span><span> = </span><span>"1"</span></div></div><div><div><div>11</div></div><div><span>quote</span><span> = </span><span>"1"</span></div></div><div><div><div>12</div></div><div><span>syn</span><span> = { </span><span>version</span><span> = </span><span>"3"</span><span>, </span><span>features</span><span> = [</span><span>"full"</span><span>] }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>[lib] proc-macro = true</code> 就是上面说的那个开关。三个依赖：<code>syn</code> 把 token 解析成语法树，<code>quote</code> 把语法树片段变回 token，<code>proc-macro2</code> 稍后解释。然后是 <code>lab-macros-step/src/lib.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>proc_macro</span><span>::</span></span><span>TokenStream</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>quote</span><span>::quote;</span></span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>syn</span><span>::{</span></span><span>DeriveInput</span><span>, parse_macro_input};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#[proc_macro_derive(</span><span>Hello</span><span>)]</span></div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>derive_hello</span><span>(</span><span>input</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>input</span><span><span> </span><span>=</span><span> </span></span><span>parse_macro_input!</span><span>(</span><span>input</span><span> </span><span>as</span><span> </span><span>DeriveInput</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>input</span><span>.ident;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>eprintln!</span><span>(</span><span>"deriving Hello for {name}"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>quote!</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>impl</span><span> #</span><span>name</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>pub</span><span> </span><span>fn</span><span> </span><span>hello</span><span>() -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>concat!</span><span>(</span><span>"hello from "</span><span>, </span><span>stringify!</span><span>(#</span><span>name</span><span>))</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>.</span><span>into</span><span>()</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐行看：</p><ul>
<li><code>#[proc_macro_derive(Hello)]</code>：把函数登记为 <code>#[derive(Hello)]</code> 背后的宏。函数名随意，括号里的名字才是用户写的名字。</li>
<li>参数 <code>input: TokenStream</code>：用户给的类型定义，原样的 token。返回值是<strong>要追加</strong>的 item，不是替换（derive 不会改掉原类型）。</li>
<li><code>parse_macro_input!(input as DeriveInput)</code>：用 <code>syn</code> 把 token 解析成 <code>DeriveInput</code>，也就是”一个 struct/enum/union 的定义”这个语法树。解析失败的话，它会直接 <code>return</code> 一个编译错误，所以宏里不用手写错误处理。</li>
<li><code>quote! { ... }</code>：写一段 Rust 代码，其中 <code>#name</code> 被替换成变量 <code>name</code> 里的 token。<code>#</code> 是 <code>quote!</code> 的插值记号，和 C 的 <code>#define</code> 没有关系。</li>
<li><code>.into()</code>：<code>quote!</code> 返回的是 <code>proc_macro2::TokenStream</code>，而宏函数要返回 <code>proc_macro::TokenStream</code>，<code>.into()</code> 在两者之间转换。</li>
</ul><p>使用它，实验文件 <code>src/bin/ch06_hello.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>lab_macros_step</span><span>::</span></span><span>Hello</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[derive(</span><span>Hello</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>struct</span><span> </span><span>Anything</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>Anything</span><span>::</span><span>hello</span><span>());</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>构建，然后<strong>直接运行二进制</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch06_hello</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling lab-macros-step v0.1.0 (/tmp/lab-ch06-derive/lab-macros-step)</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Compiling lab-ch06-derive v0.1.0 (/tmp/lab-ch06-derive)</span></div></div><div><div><div>4</div></div><div><span>deriving Hello for Anything</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s</span></div></div><div><div><div>6</div></div><div><span>$ ./target/debug/ch06_hello</span></div></div><div><div><div>7</div></div><div><span>hello from Anything</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意 <code>deriving Hello for Anything</code> 出现在<strong>构建</strong>输出里，运行二进制时没有。那一句 <code>eprintln!</code> 是宏函数里的，它在 <code>rustc</code> 编译 <code>ch06_hello.rs</code> 的时候执行，不在你的程序里。（顺带一个坑：<code>cargo run</code> 会把缓存的编译器输出重放一遍，所以用 <code>cargo run</code> 时你会在程序输出前再看到这行，像是运行期打印的。我第一次看到的时候就上当了。）这就是过程宏最重要的事实：<strong>宏是在编译时运行的普通 Rust 程序，<code>eprintln!</code> 可以用，死循环会卡住编译，<code>panic!</code> 会变成编译错误。</strong></p></section>
<section><h2>宏看到的是什么：只有 token<a href="#宏看到的是什么只有-token"><span>#</span></a></h2><p>我们让宏把输入的 token 原样打印出来，看它到底拿到了什么。<code>lab-macros-step/src/lib.rs</code> 末尾追加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[proc_macro_derive(</span><span>Show</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>derive_show</span><span>(</span><span>input</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>eprintln!</span><span>(</span><span>"input tokens: {input}"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>TokenStream</span><span>::</span><span>new</span><span>()</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实验文件 <code>src/bin/ch06_tokens.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>lab_macros_step</span><span>::</span></span><span>Show</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Show</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>Ident</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>name</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>main</span><span>() {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling lab-macros-step v0.1.0 (/tmp/lab-ch06-derive/lab-macros-step)</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling lab-ch06-derive v0.1.0 (/tmp/lab-ch06-derive)</span></div></div><div><div><div>3</div></div><div><span>input tokens: struct Ident { name: String, span: Span, }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这就是宏看到的一切：<code>struct</code>、<code>Ident</code>、一对大括号和里面的 token。<code>Span</code> 对宏而言只是一个叫 <code>Span</code> 的标识符，宏<strong>不知道</strong>它是 <code>pebble_span::Span</code>，也不知道它是不是 <code>Copy</code>。过程宏运行在类型检查<strong>之前</strong>，它看得到语法，看不到语义。这一点后面会咬我们一口，也会成为第三道练习题。</p><section><h3>为什么有两种 <code>TokenStream</code><a href="#为什么有两种-tokenstream"><span>#</span></a></h3><p>刚才的 <code>.into()</code> 要回答一个问题：为什么有 <code>proc_macro::TokenStream</code> 和 <code>proc_macro2::TokenStream</code> 两种？</p><p><code>proc_macro</code> 是编译器内置的，只有宏被编译器调用的时候才能用（刚才的 E0433 实验只是最直观的一面：普通 crate 里根本找不到它）。我的理解是，这让依赖它的代码很难测试，也很难在宏之外复用；<code>syn</code> 和 <code>quote</code> 如果直接依赖它，它们自己就没法写测试了。<code>proc-macro2</code> 是一个普通 crate，提供一个同样形状的 <code>TokenStream</code>，在宏里和 <code>proc_macro</code> 的那个来回转换（<code>.into()</code>），在宏外面也能跑。我的实验文件 <code>ch06_proc_macro2_outside.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>tokens</span><span><span> </span><span>=</span><span> </span><span>proc_macro2</span><span>::</span></span><span>TokenStream</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>3</div></div><div><span>    </span><span>println!</span><span>(</span><span>"empty = {}"</span><span>, </span><span>tokens</span><span>.</span><span>is_empty</span><span>());</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>empty = true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以约定很简单：入口函数的签名用 <code>proc_macro::TokenStream</code>，里面的所有函数都用 <code>proc_macro2::TokenStream</code>。你在后面的代码里会看到 <code>use proc_macro2::TokenStream as TokenStream2;</code>，<code>2</code> 就是这个意思。</p></section></section>
<section><h2>从 <code>Hello</code> 到 <code>Spanned</code>：一步一步长大<a href="#从-hello-到-spanned一步一步长大"><span>#</span></a></h2><p>目标是对这样的类型生成 <code>impl Spanned</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Ident</span><span> { </span><span>name</span><span>: </span><span>String</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一版：写死 <code>self.span</code>，像 <code>Hello</code> 一样。往 <code>lab-macros-step</code> 里加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[proc_macro_derive(</span><span>SpannedStruct</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>derive_spanned_struct</span><span>(</span><span>input</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>input</span><span><span> </span><span>=</span><span> </span></span><span>parse_macro_input!</span><span>(</span><span>input</span><span> </span><span>as</span><span> </span><span>DeriveInput</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>input</span><span>.ident;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>quote!</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>impl</span><span><span> ::</span><span>pebble_span</span><span>::</span></span><span>Spanned</span><span> </span><span>for</span><span> #</span><span>name</span><span> {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span><span>) -&gt; ::</span><span>pebble_span</span><span>::</span></span><span>Span</span><span> {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>self</span><span>.span</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>.</span><span>into</span><span>()</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实验文件 <code>src/bin/ch06_step.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>lab_macros_step</span><span>::{</span></span><span>Hello</span><span>, </span><span>SpannedStruct</span><span>};</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceId</span><span>, </span><span>Span</span><span>, </span><span>Spanned</span><span>};</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Hello</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>Anything</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#[derive(</span><span>SpannedStruct</span><span>)]</span></div></div><div><div><div>8</div></div><div><span>struct</span><span> </span><span>Ident</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>name</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>Anything</span><span>::</span><span>hello</span><span>());</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>ident</span><span><span> </span><span>=</span><span> </span></span><span>Ident</span><span> { </span><span>name</span><span>: </span><span>"x"</span><span>.</span><span>to_string</span><span>(), </span><span>span</span><span>: </span><span>Span</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>4</span><span>, </span><span>5</span><span>) };</span></div></div><div><div><div>16</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{} at {:?}"</span><span>, </span><span>ident</span><span>.name, </span><span>ident</span><span>.</span><span>span</span><span>());</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hello from Anything</span></div></div><div><div><div>2</div></div><div><span>x at 4..5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一版可以工作。（<code>Span</code> 的 <code>Debug</code> 是手写的，打印成 <code>起点..终点</code>，所以是 <code>4..5</code>。）</p><section><h3>第一堵墙：泛型<a href="#第一堵墙泛型"><span>#</span></a></h3><p>加一个泛型结构体，再用第一版：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>lab_macros_step</span><span>::</span></span><span>SpannedStruct</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>Span</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>SpannedStruct</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>Tagged</span><span>&lt;</span><span>T</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>inner</span><span>: </span><span>T</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>main</span><span>() {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实验文件 <code>ch06_naive_generic.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0107]: missing generics for struct `Tagged`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_naive_generic.rs:5:8</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 | struct Tagged&lt;T&gt; {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|        ^^^^^^ expected 1 generic argument</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>note: struct defined here, with 1 generic parameter: `T`</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_naive_generic.rs:5:8</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>5 | struct Tagged&lt;T&gt; {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|        ^^^^^^ -</span></div></div><div><div><div>12</div></div><div><span>help: add missing generic argument</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>5 | struct Tagged&lt;T&gt;&lt;T&gt; {</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|              +++</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>For more information about this error, try `rustc --explain E0107`.</span></div></div><div><div><div>18</div></div><div><span>error: could not compile `lab-ch06-derive` (bin "ch06_naive_generic") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（编译器的 <code>help</code> 在这里是错的，别照做。）宏生成的是 <code>impl Spanned for Tagged</code>，而 <code>Tagged</code> 不是类型，<code>Tagged&lt;T&gt;</code> 才是。一个泛型类型的 <code>impl</code> 要把参数一起带上：<code>impl&lt;T&gt; Spanned for Tagged&lt;T&gt; where ...</code>。这三段（<code>impl</code> 后面的参数列表、类型名后面的参数列表、<code>where</code> 子句）<code>syn</code> 帮你切好了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> (</span><span>impl_generics</span><span>, </span><span>ty_generics</span><span>, </span><span>where_clause</span><span><span>) </span><span>=</span><span> </span></span><span>input</span><span>.generics.</span><span>split_for_impl</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>名字很直白：“为 impl 切开泛型”。三个值都能直接放进 <code>quote!</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> #</span><span>impl_generics</span><span><span> ::</span><span>pebble_span</span><span>::</span></span><span>Spanned</span><span> </span><span>for</span><span> #</span><span>name</span><span> #</span><span>ty_generics</span><span> #</span><span>where_clause</span><span> {</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把这个版本写成了 <code>lab-macros-v2</code>（只有一个 derive <code>Spanned</code>，其余同上），然后跑一个带约束和 <code>where</code> 的泛型结构体（<code>ch06_generic_v2.rs</code>）。顺带说明：本篇里这类 <code>lab-macros-*</code> 变体（<code>-rel</code>、<code>-debug</code>、<code>-attr</code>、<code>-panic</code> 等）都是可选的对照实验：把 <code>lab-macros-step</code> 复制成同名的新目录，改 <code>Cargo.toml</code> 里的包名，把 <code>lib.rs</code> 换成该处描述的版本，让实验文件依赖它就行；不照做也不影响后面的内容。它们的完整版都在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive" target="_blank"><code>lab/lab-ch06-derive/</code></a> 下（目录清单见检查点末尾的实验清单）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Spanned</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Tagged</span><span>&lt;</span><span>T</span><span>: </span><span>Clone</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span>where</span></div></div><div><div><div>4</div></div><div><span>    </span><span>T</span><span><span>: </span><span>std</span><span>::</span><span>fmt</span><span>::</span></span><span>Debug</span><span>,</span></div></div><div><div><div>5</div></div><div><span>{</span></div></div><div><div><div>6</div></div><div><span>    </span><span>inner</span><span>: </span><span>T</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>4..9 at 5 (inner = hello)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>T: Clone</code> 和 <code>where T: Debug</code> 都被原样搬进了生成的 <code>impl</code>。这是 C++ 模板里需要手写 <code>template &lt;typename T&gt; requires ...</code> 的地方，宏帮我们照抄。</p></section><section><h3>第二堵墙：enum<a href="#第二堵墙enum"><span>#</span></a></h3><p><code>Stmt</code> 和 <code>Expr</code> 是枚举。第二版只认识 <code>self.span</code>，用在枚举上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Spanned</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>Expr</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Int</span><span> { </span><span>value</span><span>: </span><span>i64</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Nil</span><span> { </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0615]: attempted to take value of method `span` on type `&amp;Expr`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_enum_v2.rs:4:10</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>4 | #[derive(Spanned)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|          ^^^^^^^ method, not a field</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= note: this error originates in the derive macro `Spanned` (in Nightly builds, run with -Z macro-backtrace for more info)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意错误指向的是 <code>#[derive(Spanned)]</code>：生成的代码里出错，编译器没有别的地方可指。枚举没有 <code>self.span</code>，需要一个 <code>match</code>，每个变体一个分支，每个分支取出自己的 <code>span</code> 字段。这就是手写的那个 <code>match</code>，只不过现在由宏循环生成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>arms</span><span><span> </span><span>=</span><span> </span></span><span>data</span><span>.variants.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>variant</span><span><span>|</span><span> {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>variant_name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>variant</span><span>.ident;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>quote!</span><span>(</span><span>Self</span><span>::#</span><span>variant_name</span><span> { </span><span>span</span><span>, .. } =&gt; *</span><span>span</span><span>)</span></div></div><div><div><div>4</div></div><div><span>});</span></div></div><div><div><div>5</div></div><div><span>quote!</span><span> { </span><span>match</span><span> </span><span>self</span><span> { #(#</span><span>arms</span><span>),* } }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>新东西是 <code>#(#arms),*</code>：<code>quote!</code> 的<strong>重复</strong>语法，意思是”对 <code>arms</code> 里每个元素展开一次，中间用逗号隔开”，和 <code>macro_rules!</code> 的 <code>$(...),*</code> 是同一个设计。外面的 <code>#</code> 提示”这里是插值”，里面再 <code>#arms</code> 取每一项。<code>arms</code> 是一个迭代器，<code>quote!</code> 直接吃。</p><p>仓库的版本多两个细节，我故意各弄坏一次，看错误是什么样。</p><p><strong>为什么是 <code>* #member</code>？</strong> 因为 <code>match self</code> 的 <code>self</code> 是 <code>&amp;Expr</code>，分支里绑定出来的 <code>span</code> 是 <code>&amp;Span</code>（第 5 篇说过的默认绑定模式）。去掉 <code>*</code>（实验 <code>ch06_no_star.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_no_star.rs:6:23</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>4 | #[derive(Spanned)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|          ------- expected `Span` because of return type</span></div></div><div><div><div>6</div></div><div><span>5 | enum Expr {</span></div></div><div><div><div>7</div></div><div><span>6 |     Int { value: i64, span: Span },</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|                       ^^^^ expected `Span`, found `&amp;Span`</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>help: consider dereferencing the borrow</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>6 |     Int { value: i64, *span: Span },</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|                       +</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意这个错误的位置：它指向用户源码里的 <code>span: Span</code>，因为生成的 <code>span</code> 标识符继承了字段的位置。宏生成的代码出错时，错误位置像是在用户代码里，这是 <code>quote!</code> 用 <code>#member</code> 插值时保留 token 位置的结果。<code>help</code> 同样是错的（<code>*span: Span</code> 不是合法语法），别信它。<code>Span</code> 是 <code>Copy</code>，<code>*span</code> 就是一次按位拷贝。</p><p><strong>为什么分隔符是 <code>,</code>？</strong> 去掉 <code>#(#arms),*</code> 里的逗号，写成 <code>#(#arms)*</code>（实验 <code>ch06_no_comma.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: expected `,` following `match` arm</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_no_comma.rs:4:17</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>4 | #[derive(Spanned)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= note: this error originates in the derive macro `Spanned` (in Nightly builds, run with -Z macro-backtrace for more info)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>error: proc-macro derive produced unparsable tokens</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_no_comma.rs:4:10</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>4 | #[derive(Spanned)]</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|          ^^^^^^^</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>error: could not compile `lab-ch06-derive` (bin "ch06_no_comma") due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>proc-macro derive produced unparsable tokens</code>：宏吐出来的 token 不是合法 Rust。这类错误在 <code>macro_rules!</code> 里也常见，调试办法是把宏的输出打印出来看（下面”看一次展开”一节）。</p></section><section><h3>绝对路径：<code>::pebble_span::Spanned</code><a href="#绝对路径pebble_spanspanned"><span>#</span></a></h3><p>生成的代码里我一直写 <code>::pebble_span::Spanned</code>，开头有两个冒号。为什么不写 <code>pebble_span::Spanned</code> 甚至 <code>Spanned</code>？</p><p>因为宏生成的 token 会被放进<strong>用户的</strong>源文件，名字在用户的作用域里解析。如果用户的文件里恰好有一个叫 <code>pebble_span</code> 的模块呢？我做了这个实验。先看写成相对路径的宏（<code>lab-macros-rel</code>，唯一区别是去掉了开头的 <code>::</code>），用户代码（<code>ch06_relative.rs</code>）是这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>lab_macros_rel</span><span>::</span></span><span>Spanned</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>// A local module with the same name as the real crate.</span></div></div><div><div><div>4</div></div><div><span>#[allow(dead_code)]</span></div></div><div><div><div>5</div></div><div><span>mod</span><span> pebble_span {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>struct</span><span> </span><span>Span</span><span>;</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#[derive(</span><span>Spanned</span><span>)]</span></div></div><div><div><div>10</div></div><div><span>struct</span><span> </span><span>Ident</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>span</span><span><span>: ::</span><span>pebble_span</span><span>::</span></span><span>Span</span><span>,</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0405]: cannot find trait `Spanned` in module `pebble_span`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_relative.rs:9:10</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>9 | #[derive(Spanned)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|          ^^^^^^^ not found in `pebble_span`</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= note: this error originates in the derive macro `Spanned` (in Nightly builds, run with -Z macro-backtrace for more info)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>宏生成的 <code>pebble_span::Spanned</code> 找到了用户本地的那个模块，而不是外部 crate。把同样的用户代码对着带 <code>::</code> 的版本（<code>lab-macros</code>，也就是后面要写的最终版）运行（<code>ch06_shadow.rs</code>，<code>main</code> 里取一次 <code>span()</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1..4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>开头的 <code>::</code> 意思是”从 crate 根（外部 crate 列表）开始找”，不受用户本地名字影响。这是过程宏的基本礼仪：<strong>生成的代码里，所有路径都写绝对路径。</strong> 过程宏默认没有 <code>macro_rules!</code> 那种对局部变量的卫生保护（第 10 篇讲），名字全靠这条纪律保护。这不是 Pebble 的发明，标准库的 <code>#[derive(Debug)]</code> 展开出来的也是 <code>::core::fmt::Debug</code>。</p><p>代价是：用 <code>#[derive(Spanned)]</code> 的 crate 必须自己依赖 <code>pebble-span</code>，宏没法替它引入。我做了一个不依赖 <code>pebble-span</code> 的 crate（<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-nodep" target="_blank"><code>lab/lab-ch06-nodep</code></a>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0433]: cannot find `pebble_span` in the crate root</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/main.rs:5:10</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 | #[derive(Spanned)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|          ^^^^^^^ could not find `pebble_span` in the list of imported crates</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= note: this error originates in the derive macro `Spanned` (in Nightly builds, run with -Z macro-backtrace for more info)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pebble-ast</code> 已经依赖 <code>pebble-span</code>（它本来就要 <code>pub use pebble_span::{Span, Spanned}</code>），所以没有问题。</p></section></section>
<section><h2>正式写 <code>pebble-macros</code><a href="#正式写-pebble-macros"><span>#</span></a></h2><p>现在回到你的工作区 <code>~/pebble</code>。先建 crate：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-macros</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>打开根 <code>Cargo.toml</code>，在 <code>[workspace.dependencies]</code> 里加四行（<code>pebble-macros</code> 放在 <code>pebble-ast</code> 下面，另外三个是外部依赖，第一次出现）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-macros</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-macros"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>proc-macro2</span><span> = </span><span>"1"</span></div></div><div><div><div>4</div></div><div><span>quote</span><span> = </span><span>"1"</span></div></div><div><div><div>5</div></div><div><span>syn</span><span> = { </span><span>version</span><span> = </span><span>"3"</span><span>, </span><span>features</span><span> = [</span><span>"full"</span><span>] }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>"1"</code> 和 <code>"3"</code> 是 Cargo 的版本要求：兼容 1.x.y、兼容 3.x.y 的最新版本。具体解析到哪个版本由 <code>Cargo.lock</code> 记录。（我这次解析出来的是 <code>syn v3.0.9</code>、<code>proc-macro2 v1.0.107</code>、<code>quote v1.0.47</code>。）<code>syn</code> 打开 <code>full</code> 特性：解析 <code>DeriveInput</code> 用不着它，但第 10、12 篇要解析 <code>enum</code> 项和函数项，工作区里干脆一次打开。</p><p>然后把 <code>crates/pebble-macros/Cargo.toml</code> 整个替换成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-macros"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Procedural macros (derive only, so far) used across the Pebble crates."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>lib</span><span>]</span></div></div><div><div><div>12</div></div><div><span>proc-macro</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>15</div></div><div><span>proc-macro2.workspace</span><span> = </span><span>true</span></div></div><div><div><div>16</div></div><div><span>quote.workspace</span><span> = </span><span>true</span></div></div><div><div><div>17</div></div><div><span>syn.workspace</span><span> = </span><span>true</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>20</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>description</code> 里的 “so far” 是这个中间版本的特征，仓库版本写的是 “(derive + attribute)“，后面的章节会改。仓库版本还多一个 <code>[dev-dependencies]</code>，也是后面才加。</p><p>接下来是 <code>crates/pebble-macros/src/lib.rs</code>。我一块一块写，最后给出完整文件对照。</p><section><h3>文件头与入口<a href="#文件头与入口"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Procedural macros for the Pebble toolchain.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! This crate must be a `proc-macro = true` crate: procedural macros run at</span></div></div><div><div><div>4</div></div><div><span>//! compile time, in the compiler's process, and speak in tokens. So far it has</span></div></div><div><div><div>5</div></div><div><span>//! one flavour:</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! * [`macro@Spanned`] - a **derive** macro that writes the boring</span></div></div><div><div><div>8</div></div><div><span>//!   `impl pebble_span::Spanned` for AST nodes.</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>proc_macro</span><span>::</span></span><span>TokenStream</span><span>;</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>proc_macro2</span><span>::</span></span><span>TokenStream</span><span> </span><span>as</span><span> </span><span>TokenStream2</span><span>;</span></div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>quote</span><span>::quote;</span></span></div></div><div><div><div>13</div></div><div><span>use</span><span><span> </span><span>syn</span><span>::{</span></span><span>Data</span><span>, </span><span>DeriveInput</span><span>, </span><span>Fields</span><span>, parse_macro_input};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>[</code>macro@Spanned<code>]</code> 是文档链接的消歧义写法：这个名字既可能指 trait 也可能指宏，<code>macro@</code> 前缀说是宏。（这个名字冲突的问题稍后专门讲。）</p><p>入口函数薄薄一层，真正的逻辑放进一个普通函数，签名里用 <code>TokenStream2</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[proc_macro_derive(</span><span>Spanned</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>derive_spanned</span><span>(</span><span>input</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>input</span><span><span> </span><span>=</span><span> </span></span><span>parse_macro_input!</span><span>(</span><span>input</span><span> </span><span>as</span><span> </span><span>DeriveInput</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>match</span><span> </span><span>expand_spanned</span><span>(&amp;</span><span>input</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Ok</span><span>(</span><span>tokens</span><span>) =&gt; </span><span>tokens</span><span>.</span><span>into</span><span>(),</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; </span><span>error</span><span>.</span><span>to_compile_error</span><span>().</span><span>into</span><span>(),</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里没有直接 <code>quote!</code> 返回，因为生成过程<strong>可能失败</strong>：用户可能把 derive 用在没有 <code>span</code> 字段的类型上。<code>expand_spanned</code> 返回 <code>syn::Result&lt;TokenStream2&gt;</code>，也就是 <code>Result&lt;TokenStream2, syn::Error&gt;</code>。出错时调用 <code>error.to_compile_error()</code>：它生成的 token 是一个 <code>compile_error!("...")</code> 调用，位置落在你指定的 token 上。编译器展开后看到它就报错。这个设计让宏<strong>永远不 panic</strong>：错误是返回值，位置是精确的。<code>Result</code> 和 <code>?</code> 到第 7 篇才系统讲，这里先当成”可能是 <code>Ok(值)</code> 也可能是 <code>Err(错误)</code>，用 <code>match</code> 分开处理”。</p></section><section><h3><code>expand_spanned</code>：结构体<a href="#expand_spanned结构体"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>expand_spanned</span><span>(</span><span>input</span><span>: &amp;</span><span>DeriveInput</span><span><span>) -&gt; </span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>TokenStream2</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>input</span><span>.ident;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> (</span><span>impl_generics</span><span>, </span><span>ty_generics</span><span>, </span><span>where_clause</span><span><span>) </span><span>=</span><span> </span></span><span>input</span><span>.generics.</span><span>split_for_impl</span><span>();</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>body</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> &amp;</span><span>input</span><span>.data {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Data</span><span>::</span><span>Struct</span><span>(</span><span>data</span><span>) =&gt; {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>let</span><span> </span><span>member</span><span><span> </span><span>=</span><span> </span></span><span>span_member</span><span>(&amp;</span><span>data</span><span>.fields)?;</span></div></div><div><div><div>8</div></div><div><span>            </span><span>// `Span` is `Copy`, so reading the field does not move anything.</span></div></div><div><div><div>9</div></div><div><span>            </span><span>quote!</span><span>(</span><span>self</span><span>.#</span><span>member</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>        </span><span>// ... enum and union arms below</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>Ok</span><span>(</span><span>quote!</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>impl</span><span> #</span><span>impl_generics</span><span><span> ::</span><span>pebble_span</span><span>::</span></span><span>Spanned</span><span> </span><span>for</span><span> #</span><span>name</span><span> #</span><span>ty_generics</span><span> #</span><span>where_clause</span><span> {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span><span>) -&gt; ::</span><span>pebble_span</span><span>::</span></span><span>Span</span><span> {</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>#</span><span>body</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>input.data</code> 是 <code>Data</code> 枚举：<code>Struct</code>、<code>Enum</code>、<code>Union</code> 三种，对应 Rust 的三种自定义类型。<code>match &amp;input.data</code> 对它做穷尽匹配（第 3 篇的老朋友），所以 union 也得处理。结构体分支把字段名取出来，生成 <code>self.span</code>。<code>body</code> 是一小段 <code>TokenStream2</code>，最后被插进 <code>impl</code> 里。</p></section><section><h3><code>span_member</code>：找到叫 <code>span</code> 的字段<a href="#span_member找到叫-span-的字段"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>span_member</span><span>(</span><span>fields</span><span>: &amp;</span><span>Fields</span><span><span>) -&gt; </span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>syn</span><span>::</span><span>Ident</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>match</span><span> </span><span>fields</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>Fields</span><span>::</span><span>Named</span><span>(</span><span>named</span><span>) =&gt; </span><span>named</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>.named</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>find</span><span><span>(</span><span>|</span></span><span>field</span><span><span>|</span><span> </span></span><span>field</span><span>.ident.</span><span>as_ref</span><span>().</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>id</span><span><span>|</span><span> </span></span><span>id</span><span><span> </span><span>==</span><span> </span></span><span>"span"</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>and_then</span><span><span>(</span><span>|</span></span><span>field</span><span><span>|</span><span> </span></span><span>field</span><span>.ident.</span><span>clone</span><span>())</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>syn</span><span>::</span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>fields</span><span>,</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>"expected a field named `span` for `#[derive(Spanned)]`"</span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>)</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}),</span></div></div><div><div><div>14</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>Err</span><span><span>(</span><span>syn</span><span>::</span></span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>15</div></div><div><span>            </span><span>fields</span><span>,</span></div></div><div><div><div>16</div></div><div><span>            </span><span>"`#[derive(Spanned)]` requires named fields with a `span` field"</span><span>,</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>)),</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这是整个宏里 Rust 味最浓的一段，我一层层剥：</p><ul>
<li><code>Fields</code> 也是枚举：<code>Named</code>（<code>{ a: T }</code>）、<code>Unnamed</code>（<code>(T, U)</code>）、<code>Unit</code>（没有字段）。Pebble 的所有 AST 节点用具名字段，所以只接受 <code>Named</code>，其余用 <code>_ =&gt;</code> 兜底并返回错误。</li>
<li><code>named.named</code> 是字段的有序列表：外层的 <code>named</code> 是 <code>Fields::Named</code> 里装的那个结构（<code>FieldsNamed</code>），里面的 <code>named</code> 才是字段的集合，名字撞了，不是笔误。<code>.iter().find(...)</code> 找第一个满足条件的字段，返回 <code>Option&lt;&amp;Field&gt;</code>。</li>
<li><code>field.ident</code> 是 <code>Option&lt;Ident&gt;</code>（具名字段一定是 <code>Some</code>，但类型对元组字段也通用）。<code>.as_ref()</code> 把 <code>&amp;Option&lt;Ident&gt;</code> 变成 <code>Option&lt;&amp;Ident&gt;</code>，<code>.is_some_and(|id| id == "span")</code> 在有名字时比较它是否等于 <code>"span"</code>。<code>syn::Ident</code> 可以直接和字符串比较，这是它实现的 <code>PartialEq</code>。</li>
<li><code>.and_then(|field| field.ident.clone())</code>：找到了字段，就把它的名字克隆出来；没找到（<code>None</code>）还是 <code>None</code>。</li>
<li><code>.ok_or_else(|| ...)</code>：<code>Option</code> 到 <code>Result</code> 的桥：<code>Some(x)</code> 变 <code>Ok(x)</code>，<code>None</code> 变 <code>Err(闭包的返回值)</code>。闭包只在 <code>None</code> 时才被调用，所以不会白白构造错误。</li>
<li><code>syn::Error::new_spanned(fields, "...")</code>：创建一个错误，位置取自第一个参数的 token。这是自定义编译错误的关键：位置<strong>指向用户的代码</strong>，而不是宏的代码。</li>
</ul></section><section><h3><code>expand_spanned</code>：枚举和 union<a href="#expand_spanned枚举和-union"><span>#</span></a></h3><p>回到 <code>expand_spanned</code> 里的 <code>match</code>，补上另外两个分支：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>Data</span><span>::</span><span>Enum</span><span>(</span><span>data</span><span>) =&gt; {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>let</span><span> </span><span>arms</span><span><span> </span><span>=</span><span> </span></span><span>data</span></div></div><div><div><div>3</div></div><div><span><span>                </span></span><span>.variants</span></div></div><div><div><div>4</div></div><div><span><span>                </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>variant</span><span><span>|</span><span> {</span></span></div></div><div><div><div>6</div></div><div><span>                    </span><span>let</span><span> </span><span>variant_name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>variant</span><span>.ident;</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>let</span><span> </span><span>member</span><span><span> </span><span>=</span><span> </span></span><span>span_member</span><span>(&amp;</span><span>variant</span><span>.fields)?;</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>Ok</span><span>(</span><span>quote!</span><span>(</span><span>Self</span><span>::#</span><span>variant_name</span><span> { #</span><span>member</span><span>, .. } =&gt; * #</span><span>member</span><span>))</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>})</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>.</span><span>collect</span><span><span>::&lt;</span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;&gt;()?;</span></div></div><div><div><div>11</div></div><div><span>            </span><span>quote!</span><span> {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>13</div></div><div><span><span>                    </span></span><span>#(#</span><span>arms</span><span>),*</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>        </span><span>Data</span><span>::</span><span>Union</span><span>(</span><span>data</span><span>) =&gt; {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span><span>(</span><span>syn</span><span>::</span></span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>19</div></div><div><span>                </span><span>data</span><span>.union_token,</span></div></div><div><div><div>20</div></div><div><span>                </span><span>"`Spanned` cannot be derived for unions"</span><span>,</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>));</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这段有一个值得停下来的类型技巧：<code>.map(|variant| { ... Ok(...) })</code> 里的闭包返回 <code>syn::Result&lt;TokenStream2&gt;</code>（里面有 <code>?</code>，而 <code>?</code> 要求闭包返回 <code>Result</code>）。对一个迭代器做 <code>.map</code> 得到的是<strong>一串 <code>Result</code></strong>。我要的是”要么全部成功，得到一个 <code>Vec</code>；要么有任何一个失败，就得到那个错误”。标准库的 <code>collect</code> 能做这件事：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.</span><span>collect</span><span><span>::&lt;</span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;&gt;()?</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>::&lt;...&gt;</code> 是 turbofish，第 4 篇见过：告诉 <code>collect</code> 目标类型是 <code>Result&lt;Vec&lt;_&gt;, syn::Error&gt;</code>。遇到第一个 <code>Err</code> 它立即停下并返回那个 <code>Err</code>；全部是 <code>Ok</code> 就收集成 <code>Vec</code>。末尾的 <code>?</code> 再把外层的 <code>Result</code> 拆开：成功拿到 <code>Vec</code>，失败就让 <code>expand_spanned</code> 返回那个错误。（一个 <code>Vec&lt;Result&lt;..&gt;&gt;</code> 变成一个 <code>Result&lt;Vec&lt;..&gt;&gt;</code>，函数式编程里叫 <code>sequence</code>，这里不用名字。）<code>Data::Union</code> 分支的 <code>return Err(...)</code> 直接从函数返回，所以那个分支的类型是”永不返回”，不会和另外两个分支冲突。</p></section><section><h3>完整文件<a href="#完整文件"><span>#</span></a></h3><p>全部拼起来，<code>crates/pebble-macros/src/lib.rs</code> 是这样（102 行，含文档；去掉注释和空行是 70 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Procedural macros for the Pebble toolchain.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! This crate must be a `proc-macro = true` crate: procedural macros run at</span></div></div><div><div><div>4</div></div><div><span>//! compile time, in the compiler's process, and speak in tokens. So far it has</span></div></div><div><div><div>5</div></div><div><span>//! one flavour:</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! * [`macro@Spanned`] - a **derive** macro that writes the boring</span></div></div><div><div><div>8</div></div><div><span>//!   `impl pebble_span::Spanned` for AST nodes.</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>use</span><span><span> </span><span>proc_macro</span><span>::</span></span><span>TokenStream</span><span>;</span></div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>proc_macro2</span><span>::</span></span><span>TokenStream</span><span> </span><span>as</span><span> </span><span>TokenStream2</span><span>;</span></div></div><div><div><div>12</div></div><div><span>use</span><span><span> </span><span>quote</span><span>::quote;</span></span></div></div><div><div><div>13</div></div><div><span>use</span><span><span> </span><span>syn</span><span>::{</span></span><span>Data</span><span>, </span><span>DeriveInput</span><span>, </span><span>Fields</span><span>, parse_macro_input};</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>/// Derives [`pebble_span::Spanned`](https://docs.rs/pebble-span).</span></div></div><div><div><div>16</div></div><div><span>///</span></div></div><div><div><div>17</div></div><div><span>/// Works on structs and enums that carry a field literally named `span`:</span></div></div><div><div><div>18</div></div><div><span>///</span></div></div><div><div><div>19</div></div><div><span>/// ```ignore</span></div></div><div><div><div>20</div></div><div><span>/// #[derive(Spanned)]</span></div></div><div><div><div>21</div></div><div><span>/// struct Ident { name: String, span: Span }</span></div></div><div><div><div>22</div></div><div><span>///</span></div></div><div><div><div>23</div></div><div><span>/// #[derive(Spanned)]</span></div></div><div><div><div>24</div></div><div><span>/// enum Expr {</span></div></div><div><div><div>25</div></div><div><span>///     Int { value: i64, span: Span },</span></div></div><div><div><div>26</div></div><div><span>///     Var { name: Ident, span: Span },</span></div></div><div><div><div>27</div></div><div><span>/// }</span></div></div><div><div><div>28</div></div><div><span>/// ```</span></div></div><div><div><div>29</div></div><div><span>///</span></div></div><div><div><div>30</div></div><div><span>/// For an enum, *every* variant must have the `span` field, because the derived</span></div></div><div><div><div>31</div></div><div><span>/// `span()` matches on the variant and returns its span.</span></div></div><div><div><div>32</div></div><div><span>#[proc_macro_derive(</span><span>Spanned</span><span>)]</span></div></div><div><div><div>33</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>derive_spanned</span><span>(</span><span>input</span><span>: </span><span>TokenStream</span><span>) -&gt; </span><span>TokenStream</span><span> {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>let</span><span> </span><span>input</span><span><span> </span><span>=</span><span> </span></span><span>parse_macro_input!</span><span>(</span><span>input</span><span> </span><span>as</span><span> </span><span>DeriveInput</span><span>);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>match</span><span> </span><span>expand_spanned</span><span>(&amp;</span><span>input</span><span>) {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>Ok</span><span>(</span><span>tokens</span><span>) =&gt; </span><span>tokens</span><span>.</span><span>into</span><span>(),</span></div></div><div><div><div>37</div></div><div><span>        </span><span>Err</span><span>(</span><span>error</span><span>) =&gt; </span><span>error</span><span>.</span><span>to_compile_error</span><span>().</span><span>into</span><span>(),</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>}</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>fn</span><span> </span><span>expand_spanned</span><span>(</span><span>input</span><span>: &amp;</span><span>DeriveInput</span><span><span>) -&gt; </span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>TokenStream2</span><span>&gt; {</span></div></div><div><div><div>42</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>input</span><span>.ident;</span></div></div><div><div><div>43</div></div><div><span>    </span><span>let</span><span> (</span><span>impl_generics</span><span>, </span><span>ty_generics</span><span>, </span><span>where_clause</span><span><span>) </span><span>=</span><span> </span></span><span>input</span><span>.generics.</span><span>split_for_impl</span><span>();</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span>let</span><span> </span><span>body</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> &amp;</span><span>input</span><span>.data {</span></div></div><div><div><div>46</div></div><div><span>        </span><span>Data</span><span>::</span><span>Struct</span><span>(</span><span>data</span><span>) =&gt; {</span></div></div><div><div><div>47</div></div><div><span>            </span><span>let</span><span> </span><span>member</span><span><span> </span><span>=</span><span> </span></span><span>span_member</span><span>(&amp;</span><span>data</span><span>.fields)?;</span></div></div><div><div><div>48</div></div><div><span>            </span><span>// `Span` is `Copy`, so reading the field does not move anything.</span></div></div><div><div><div>49</div></div><div><span>            </span><span>quote!</span><span>(</span><span>self</span><span>.#</span><span>member</span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span>        </span><span>Data</span><span>::</span><span>Enum</span><span>(</span><span>data</span><span>) =&gt; {</span></div></div><div><div><div>52</div></div><div><span>            </span><span>let</span><span> </span><span>arms</span><span><span> </span><span>=</span><span> </span></span><span>data</span></div></div><div><div><div>53</div></div><div><span><span>                </span></span><span>.variants</span></div></div><div><div><div>54</div></div><div><span><span>                </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>55</div></div><div><span><span>                </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>variant</span><span><span>|</span><span> {</span></span></div></div><div><div><div>56</div></div><div><span>                    </span><span>let</span><span> </span><span>variant_name</span><span><span> </span><span>=</span><span> &amp;</span></span><span>variant</span><span>.ident;</span></div></div><div><div><div>57</div></div><div><span>                    </span><span>let</span><span> </span><span>member</span><span><span> </span><span>=</span><span> </span></span><span>span_member</span><span>(&amp;</span><span>variant</span><span>.fields)?;</span></div></div><div><div><div>58</div></div><div><span>                    </span><span>Ok</span><span>(</span><span>quote!</span><span>(</span><span>Self</span><span>::#</span><span>variant_name</span><span> { #</span><span>member</span><span>, .. } =&gt; * #</span><span>member</span><span>))</span></div></div><div><div><div>59</div></div><div><span><span>                </span></span><span>})</span></div></div><div><div><div>60</div></div><div><span><span>                </span></span><span>.</span><span>collect</span><span><span>::&lt;</span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;&gt;()?;</span></div></div><div><div><div>61</div></div><div><span>            </span><span>quote!</span><span> {</span></div></div><div><div><div>62</div></div><div><span>                </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>63</div></div><div><span><span>                    </span></span><span>#(#</span><span>arms</span><span>),*</span></div></div><div><div><div>64</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>65</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>66</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>67</div></div><div><span>        </span><span>Data</span><span>::</span><span>Union</span><span>(</span><span>data</span><span>) =&gt; {</span></div></div><div><div><div>68</div></div><div><span>            </span><span>return</span><span> </span><span>Err</span><span><span>(</span><span>syn</span><span>::</span></span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>69</div></div><div><span>                </span><span>data</span><span>.union_token,</span></div></div><div><div><div>70</div></div><div><span>                </span><span>"`Spanned` cannot be derived for unions"</span><span>,</span></div></div><div><div><div>71</div></div><div><span><span>            </span></span><span>));</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>73</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>    </span><span>Ok</span><span>(</span><span>quote!</span><span> {</span></div></div><div><div><div>76</div></div><div><span>        </span><span>impl</span><span> #</span><span>impl_generics</span><span><span> ::</span><span>pebble_span</span><span>::</span></span><span>Spanned</span><span> </span><span>for</span><span> #</span><span>name</span><span> #</span><span>ty_generics</span><span> #</span><span>where_clause</span><span> {</span></div></div><div><div><div>77</div></div><div><span>            </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span><span>) -&gt; ::</span><span>pebble_span</span><span>::</span></span><span>Span</span><span> {</span></div></div><div><div><div>78</div></div><div><span><span>                </span></span><span>#</span><span>body</span></div></div><div><div><div>79</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>})</span></div></div><div><div><div>82</div></div><div><span>}</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>fn</span><span> </span><span>span_member</span><span>(</span><span>fields</span><span>: &amp;</span><span>Fields</span><span><span>) -&gt; </span><span>syn</span><span>::</span></span><span>Result</span><span>&lt;</span><span>syn</span><span>::</span><span>Ident</span><span>&gt; {</span></div></div><div><div><div>85</div></div><div><span>    </span><span>match</span><span> </span><span>fields</span><span> {</span></div></div><div><div><div>86</div></div><div><span>        </span><span>Fields</span><span>::</span><span>Named</span><span>(</span><span>named</span><span>) =&gt; </span><span>named</span></div></div><div><div><div>87</div></div><div><span><span>            </span></span><span>.named</span></div></div><div><div><div>88</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>89</div></div><div><span><span>            </span></span><span>.</span><span>find</span><span><span>(</span><span>|</span></span><span>field</span><span><span>|</span><span> </span></span><span>field</span><span>.ident.</span><span>as_ref</span><span>().</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>id</span><span><span>|</span><span> </span></span><span>id</span><span><span> </span><span>==</span><span> </span></span><span>"span"</span><span>))</span></div></div><div><div><div>90</div></div><div><span><span>            </span></span><span>.</span><span>and_then</span><span><span>(</span><span>|</span></span><span>field</span><span><span>|</span><span> </span></span><span>field</span><span>.ident.</span><span>clone</span><span>())</span></div></div><div><div><div>91</div></div><div><span><span>            </span></span><span>.</span><span>ok_or_else</span><span><span>(</span><span>||</span><span> {</span></span></div></div><div><div><div>92</div></div><div><span><span>                </span></span><span>syn</span><span>::</span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>93</div></div><div><span>                    </span><span>fields</span><span>,</span></div></div><div><div><div>94</div></div><div><span>                    </span><span>"expected a field named `span` for `#[derive(Spanned)]`"</span><span>,</span></div></div><div><div><div>95</div></div><div><span><span>                </span></span><span>)</span></div></div><div><div><div>96</div></div><div><span><span>            </span></span><span>}),</span></div></div><div><div><div>97</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>Err</span><span><span>(</span><span>syn</span><span>::</span></span><span>Error</span><span>::</span><span>new_spanned</span><span>(</span></div></div><div><div><div>98</div></div><div><span>            </span><span>fields</span><span>,</span></div></div><div><div><div>99</div></div><div><span>            </span><span>"`#[derive(Spanned)]` requires named fields with a `span` field"</span><span>,</span></div></div><div><div><div>100</div></div><div><span><span>        </span></span><span>)),</span></div></div><div><div><div>101</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>102</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>文档注释里的 <code>```ignore</code> 代码块告诉 <code>cargo test</code> 不要把它当 doctest 编译：它需要 <code>pebble_span</code> 在作用域里，而宏 crate 不依赖它。所以 <code>cargo test -p pebble-macros</code> 的结果是 0 个测试加 1 个被忽略的 doctest：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 0 tests</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>Doc-tests pebble_macros</span></div></div><div><div><div>4</div></div><div><span>running 1 test</span></div></div><div><div><div>5</div></div><div><span>test crates/pebble-macros/src/lib.rs - derive_spanned (line 19) ... ignored</span></div></div><div><div><div>6</div></div><div><span>test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>宏本身没有单元测试，<strong>测试它的是用它的 crate</strong>：<code>pebble-ast</code> 里的 <code>derive_spanned_*</code> 两个测试。这是过程宏的常态，因为要测宏就得编译一个用它的 crate；<code>trybuild</code> 这类库可以做”这段代码应该报这个错”的快照测试，Pebble 没有用。</p></section><section><h3>看一次展开<a href="#看一次展开"><span>#</span></a></h3><p>宏写好了，它到底吐出了什么？在实验室里，我把 <code>expand_spanned</code> 的结果用 <code>eprintln!("expansion for </code>{name}<code>:\n{expanded}\n")</code> 打印出来（<code>lab-macros-debug</code>，实验 <code>ch06_expand.rs</code>），对 <code>Ident</code> 和一个两变体的 <code>Expr</code> 用宏：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>expansion for `Ident`:</span></div></div><div><div><div>2</div></div><div><span>impl :: pebble_span :: Spanned for Ident</span></div></div><div><div><div>3</div></div><div><span>{ fn span(&amp; self) -&gt; :: pebble_span :: Span { self.span } }</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>expansion for `Expr`:</span></div></div><div><div><div>6</div></div><div><span>impl :: pebble_span :: Spanned for Expr</span></div></div><div><div><div>7</div></div><div><span>{</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>fn span(&amp; self) -&gt; :: pebble_span :: Span</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>match self</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>Self :: Int { span, .. } =&gt; * span, Self :: Var { span, .. } =&gt; *</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>span</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>TokenStream</code> 的 <code>Display</code> 在每个 token 之间放空格，所以 <code>::</code> 显示成 <code>:: </code>。这提醒了我们：token 流没有”格式”这回事，空格只是打印时的装饰。</p><p>真实的展开可以在装配好之后，对整个 <code>pebble-ast</code> 看。稳定版编译器不接受 <code>-Zunpretty</code>，但 <code>RUSTC_BOOTSTRAP=1</code> 可以让它接受（<strong>只用来观察</strong>，不要放进任何构建脚本）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>RUSTC_BOOTSTRAP</span><span>=</span><span>1</span><span> </span><span>cargo</span><span> </span><span>rustc</span><span> </span><span>-p</span><span> </span><span>pebble-ast</span><span> </span><span>--lib</span><span> </span><span>--</span><span> </span><span>-Zunpretty=expanded</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个 crate 展开后 1135 行，我挑出和 <code>Spanned</code> 有关的两段。<code>Ident</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl ::pebble_span::Spanned for Ident {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>fn span(&amp;self) -&gt; ::pebble_span::Span { self.span }</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Stmt</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl ::pebble_span::Spanned for Stmt {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>fn span(&amp;self) -&gt; ::pebble_span::Span {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>match self {</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>Self::Let { span, .. } =&gt; *span,</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>Self::Fn { span, .. } =&gt; *span,</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>Self::Expr { span, .. } =&gt; *span,</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>Self::If { span, .. } =&gt; *span,</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>Self::While { span, .. } =&gt; *span,</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>Self::For { span, .. } =&gt; *span,</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>Self::Return { span, .. } =&gt; *span,</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>Self::Break { span, .. } =&gt; *span,</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>Self::Continue { span, .. } =&gt; *span,</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>和上一篇手写的比，是同一个意思，唯一的区别是每个变体一个分支而不是 <code>|</code> 合并。<code>Self::Break { span, .. }</code> 里的 <code>..</code> 对只有一个字段的变体是多余的，宏不在乎。生成的代码不必漂亮，只需正确。</p></section></section>
<section><h2>错误信息：宏的”用户界面”<a href="#错误信息宏的用户界面"><span>#</span></a></h2><p>宏的用户是写 <code>#[derive(Spanned)]</code> 的人（比如半年后的我）。给他们看的东西只有编译错误，所以错误的位置和措辞就是宏的 UI。下面是 <code>pebble-macros</code> 最终版（<code>lab-macros</code>，实验里用的是同一份代码）对几种误用的回应。</p><p><strong>元组结构体</strong>（<code>ch06_derive_error.rs</code>：<code>struct Pair(Span, u32);</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: `#[derive(Spanned)]` requires named fields with a `span` field</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_derive_error.rs:5:12</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 | struct Pair(Span, u32);</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|            ^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下划线正好落在字段列表上，这是 <code>new_spanned(fields, ..)</code> 的功劳。</p><p><strong>枚举里有一个变体没有 <code>span</code> 字段</strong>（<code>ch06_derive_error_variant.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Spanned</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>Node</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Leaf</span><span> { </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Branch</span><span> { </span><span>children</span><span>: </span><span>u32</span><span> },</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: expected a field named `span` for `#[derive(Spanned)]`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_derive_error_variant.rs:7:12</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>7 |     Branch { children: u32 },</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|            ^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>枚举里有一个单元变体</strong>（<code>ch06_derive_error_unit.rs</code>：<code>Leaf { span: Span }, Empty</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: `#[derive(Spanned)]` requires named fields with a `span` field</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_derive_error_unit.rs:4:10</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>4 | #[derive(Spanned)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|          ^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= note: this error originates in the derive macro `Spanned` (in Nightly builds, run with -Z macro-backtrace for more info)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一条消息，位置却不一样：下划线落在了 <code>#[derive(Spanned)]</code> 上。原因是 <code>Empty</code> 没有字段，<code>Fields::Unit</code> 里没有任何 token 可以指，<code>new_spanned</code> 只好退回”宏的调用位置”。这是 <code>new_spanned</code> 的一个局限。要改进的话可以改用变体自己的 <code>ident</code> 作位置，我把这个留作一个小改进，没有放进仓库版本。</p><p><strong>union</strong>（<code>ch06_derive_union.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: `Spanned` cannot be derived for unions</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_derive_union.rs:5:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 | union Raw {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>| ^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>字段叫 <code>span</code>，但类型不对</strong>（<code>ch06_wrong_type.rs</code>：<code>struct Oops { span: u32 }</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_wrong_type.rs:3:10</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>3 | #[derive(Spanned)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|          ^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|          |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|          expected `Span`, found `u32`</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|          expected `Span` because of return type</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>= note: this error originates in the derive macro `Spanned` (in Nightly builds, run with -Z macro-backtrace for more info)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条最有意思：宏<strong>没有</strong>拒绝它。宏只看到标识符 <code>u32</code>，不知道 <code>Span</code> 是什么，也不知道 <code>u32</code> 不是它。它照样生成 <code>self.span</code>，直到类型检查阶段编译器才发现类型不符。回想上面的 <code>Show</code> 实验：宏看到的只有 token。<strong>过程宏运行在类型检查之前</strong>，它能做的检查只有语法级别的。不过这里的下场还不错：编译器的错误在这个例子里仍然足够清楚。</p><div><div><div></div><div>宏里 panic 会怎样</div></div><div><p>你可以在宏函数里 <code>panic!</code>。我试了一下（<code>lab-macros-panic</code>，<code>ch06_panic.rs</code>），编译器把它显示成这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: proc-macro derive panicked</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch06_panic.rs:3:10</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>3 | #[derive(Boom)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|          ^^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= help: message: this macro does not like its input</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>位置只有 <code>#[derive(..)]</code> 一处，没法指到用户出问题的那个 token。<code>syn::Error</code> 能给出精确位置，所以仓库的宏里一个 <code>panic!</code>、<code>unwrap</code> 也没有。</p></div></div></section>
<section><h2>接入 <code>pebble-ast</code><a href="#接入-pebble-ast"><span>#</span></a></h2><p>宏的 crate 完成了，回到 <code>pebble-ast</code>。五处改动。</p><p><strong>一，</strong> <code>crates/pebble-ast/Cargo.toml</code> 的 <code>[dependencies]</code> 里 <code>pebble-span</code> 下面加一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-macros.workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>二，</strong> <code>crates/pebble-ast/src/lib.rs</code> 头部文档的最后一行改成两行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//!   [`AstPrinter`] is one implementation. Deriving [`Spanned`] comes from the</span></div></div><div><div><div>2</div></div><div><span>//!   `pebble-macros` crate.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>三，</strong> <code>pub use pebble_span::{Span, Spanned};</code> 下面加：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Parses `#[derive(Spanned)]` into scope as a *macro*; the trait of the same</span></div></div><div><div><div>2</div></div><div><span>/// name comes from `pebble-span`. The two live in different namespaces, which</span></div></div><div><div><div>3</div></div><div><span>/// is why `Spanned` can mean both.</span></div></div><div><div><div>4</div></div><div><span>pub</span><span> </span><span>use</span><span><span> </span><span>pebble_macros</span><span>::</span></span><span>Spanned</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有一件让人疑惑的事：同一个文件里有两行 <code>pub use ...::Spanned</code>，名字一样，不冲突吗？Rust 的名字分属三个<strong>命名空间</strong>：类型（包括 trait）、值（函数、常量）、宏。<code>pebble_span::Spanned</code> 是 trait，在类型命名空间里；<code>pebble_macros::Spanned</code> 是 derive 宏，在宏命名空间里。两个 <code>use</code> 各占一个，互不干扰。<code>#[derive(Spanned)]</code> 在宏的命名空间里找，<code>impl Spanned for X</code> 在类型的命名空间里找。标准库的 <code>#[derive(Debug)]</code> 和 <code>trait Debug</code> 也是这样同名的。</p><p><strong>四，</strong> <code>Ident</code>、<code>Stmt</code>、<code>Expr</code> 三个类型上方的 <code>#[derive(Clone, Debug, PartialEq)]</code> 各加一个 <code>Spanned</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>, </span><span>Spanned</span><span>)]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>五，</strong> 删掉三个手写的 <code>impl Spanned for ...</code>（<code>Ident</code> 的 6 行，<code>Stmt</code> 的 16 行，<code>Expr</code> 的 22 行，各含后面的一个空行）。</p><p>在 <code>~/pebble</code> 下，把 <code>pebble-ast</code> 和仓库对一下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>~/pebble-tag/crates/pebble-ast/src/lib.rs</span><span> </span><span>crates/pebble-ast/src/lib.rs</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>identical</span></div></div><div><div><div>2</div></div><div><span>diff</span><span> </span><span>~/pebble-tag/crates/pebble-ast/Cargo.toml</span><span> </span><span>crates/pebble-ast/Cargo.toml</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>identical</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>identical</span></div></div><div><div><div>2</div></div><div><span>identical</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后是要紧的那一步，测试一个字不改：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-ast</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 4 tests</span></div></div><div><div><div>2</div></div><div><span>test tests::default_visit_reaches_every_expression ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::derive_spanned_works_on_enum_variants ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::derive_spanned_reads_the_span_field ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::printer_walks_the_whole_tree ... ok</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>还是上一篇那 4 个测试（顺序略有变化，是并行执行的顺序），这就是重构的标准：行为不变，测试不动。</p></section>
<section><h2>值不值<a href="#值不值"><span>#</span></a></h2><p>诚实地算一笔账。</p><ul>
<li><strong>代码行数</strong>：删掉 44 行手写代码，新增一个 70 行（不含注释和空行）的宏 crate。就这个项目的规模，<strong>光看行数是亏的</strong>。</li>
<li><strong>编译时间</strong>：我在一个全新的 <code>target</code> 目录里各编译一次 <code>pebble-ast</code>。没有宏的版本，0.45 秒；有宏的版本，4.56 秒（<code>proc-macro2</code>、<code>quote</code>、<code>unicode-ident</code>、<code>syn</code> 和 <code>pebble-macros</code> 都要先编译一遍）。大约 10 倍，单次测量，机器有 16 核，只能当个量级。不过它们是外部依赖，只在干净构建时付一次，增量编译里不会重复。</li>
<li><strong>收益</strong>：加变体的时候，<code>Spanned</code> 那一处不再需要你记得改。上一篇的实验是 3 个错误，现在是 2 个；而且更重要的是，另一种漏改（给新变体忘了加 <code>span</code> 字段）会得到一条专门的、指向那个变体的错误：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: expected a field named `span` for `#[derive(Spanned)]`</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-ast/src/lib.rs:120:11</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>120 |     Block { body: Vec&lt;Stmt&gt; },</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|           ^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的判断是：对 Pebble 这个体量，宏是为了学它而加的，不是为了省事。但同样的思路在 <code>serde</code> 的 derive 里省下的是几千行。想想成熟的项目：你手上有 40 个 AST 节点，每个都要 <code>Spanned</code>、<code>Visit</code>、<code>Clone</code>、<code>Debug</code>，derive 就是让这些<strong>一行</strong>搞定的东西。</p></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 <code>~/pebble</code> 下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>--workspace</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--workspace</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试：<code>pebble-span</code> 6 个、<code>pebble-diag</code> 4 个、<code>pebble-lexer</code> 12 个、<code>pebble-ast</code> 4 个，<code>pebble-macros</code> 0 个（加 1 个被忽略的 doctest）。clippy 和 fmt 没有输出。</p><p><strong>此时与 tag <code>tutorial-v1</code> 逐字节相同的文件（已完成）：</strong></p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-ast/src/lib.rs" target="_blank"><code>crates/pebble-ast/src/lib.rs</code></a> 与 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-ast/Cargo.toml" target="_blank"><code>crates/pebble-ast/Cargo.toml</code></a>（我用 <code>diff</code> 对照过）</li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/src/lib.rs" target="_blank"><code>crates/pebble-span/src/lib.rs</code></a>、<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-diag/src/lib.rs" target="_blank"><code>crates/pebble-diag/src/lib.rs</code></a>、<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-lexer/src/lib.rs" target="_blank"><code>crates/pebble-lexer/src/lib.rs</code></a>（之前的章节）</li>
</ul><p><strong>中间状态的文件（后面的章节会补成仓库版本）：</strong></p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-macros/src/lib.rs" target="_blank"><code>crates/pebble-macros/src/lib.rs</code></a>：现在只有 <code>#[derive(Spanned)]</code> 这一个宏；头部文档说 “So far it has one flavour”，只有一个宏的描述。仓库版本里还有 <code>opcode_set!</code>（第 10 篇）和 <code>#[differential]</code>（第 12 篇）两个宏，以及相应的文档。<strong>derive 的那部分（<code>derive_spanned</code>、<code>expand_spanned</code>、<code>span_member</code>）和仓库逐字相同。</strong></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-macros/Cargo.toml" target="_blank"><code>crates/pebble-macros/Cargo.toml</code></a>：<code>description</code> 是 “derive only, so far”，没有 <code>[dev-dependencies]</code>。</li>
<li>根 <code>Cargo.toml</code>：<code>[workspace.dependencies]</code> 现在有 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-lexer</code>、<code>pebble-ast</code>、<code>pebble-macros</code>，以及 <code>proc-macro2</code>、<code>quote</code>、<code>syn</code> 三个外部依赖。tag 里的版本还有解析器、解释器等其他 crate 的条目、几个外部依赖和注释分隔线，随后续章节加入。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇的实验文件都在仓库的 <code>lab/</code> 目录里，宏实验都是侧边 crate，各自是一个独立的 Cargo 包，依赖 tag 里的最终版 <code>pebble-span</code>（主实验室 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a> 在这一篇没有新文件）。如果你把 tag 克隆在 <code>~/pebble-tag</code>，它们就在 <code>~/pebble-tag/lab/</code> 下，命令都从那里的对应目录运行。</p><p>下表的 <code>ch06_*.rs</code> 全在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/src/bin" target="_blank"><code>lab/lab-ch06-derive/src/bin/</code></a>，命令都在 <code>lab/lab-ch06-derive</code> 里运行；结果列括号里是该实验依赖的 <code>lab-macros-*</code> 变体：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_proc_macro_outside.rs" target="_blank"><code>ch06_proc_macro_outside.rs</code></a></td><td><code>cargo build --bin ch06_proc_macro_outside</code></td><td>E0433</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_proc_macro2_outside.rs" target="_blank"><code>ch06_proc_macro2_outside.rs</code></a></td><td><code>cargo run --quiet --bin ch06_proc_macro2_outside</code></td><td>运行，输出 <code>empty = true</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_hello.rs" target="_blank"><code>ch06_hello.rs</code></a></td><td><code>cargo run --quiet --bin ch06_hello</code></td><td>运行，输出 <code>hello from Anything</code>（<code>lab-macros-step</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_tokens.rs" target="_blank"><code>ch06_tokens.rs</code></a></td><td><code>cargo run --quiet --bin ch06_tokens</code></td><td>运行，标准输出为空，输入 token 在构建时的 stderr 里（<code>lab-macros-step</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_step.rs" target="_blank"><code>ch06_step.rs</code></a></td><td><code>cargo run --quiet --bin ch06_step</code></td><td>运行，输出 <code>hello from Anything</code>（<code>lab-macros-step</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_naive_generic.rs" target="_blank"><code>ch06_naive_generic.rs</code></a></td><td><code>cargo build --bin ch06_naive_generic</code></td><td>E0107（<code>lab-macros-step</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_generic_v2.rs" target="_blank"><code>ch06_generic_v2.rs</code></a></td><td><code>cargo run --quiet --bin ch06_generic_v2</code></td><td>运行，输出 <code>4..9 at 5 (inner = hello)</code>（<code>lab-macros-v2</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_enum_v2.rs" target="_blank"><code>ch06_enum_v2.rs</code></a></td><td><code>cargo build --bin ch06_enum_v2</code></td><td>E0615（<code>lab-macros-v2</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_no_star.rs" target="_blank"><code>ch06_no_star.rs</code></a></td><td><code>cargo build --bin ch06_no_star</code></td><td>E0308（<code>lab-macros-nostar</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_no_comma.rs" target="_blank"><code>ch06_no_comma.rs</code></a></td><td><code>cargo build --bin ch06_no_comma</code></td><td>无 E 码：<code>error: expected `,` following `match` arm</code>（<code>lab-macros-nocomma</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_relative.rs" target="_blank"><code>ch06_relative.rs</code></a></td><td><code>cargo build --bin ch06_relative</code></td><td>E0405（<code>lab-macros-rel</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_shadow.rs" target="_blank"><code>ch06_shadow.rs</code></a></td><td><code>cargo run --quiet --bin ch06_shadow</code></td><td>运行，输出 <code>1..4</code>（<code>lab-macros</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_expand.rs" target="_blank"><code>ch06_expand.rs</code></a></td><td><code>cargo run --quiet --bin ch06_expand</code></td><td>运行，标准输出为空，展开结果在构建时的 stderr 里（<code>lab-macros-debug</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_derive_error.rs" target="_blank"><code>ch06_derive_error.rs</code></a></td><td><code>cargo build --bin ch06_derive_error</code></td><td>无 E 码：<code>error: `#[derive(Spanned)]` requires named fields with a `span` field</code>（<code>lab-macros</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_derive_error_variant.rs" target="_blank"><code>ch06_derive_error_variant.rs</code></a></td><td><code>cargo build --bin ch06_derive_error_variant</code></td><td>无 E 码：<code>error: expected a field named `span` for `#[derive(Spanned)]`</code>（<code>lab-macros</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_derive_error_unit.rs" target="_blank"><code>ch06_derive_error_unit.rs</code></a></td><td><code>cargo build --bin ch06_derive_error_unit</code></td><td>无 E 码：<code>error: `#[derive(Spanned)]` requires named fields with a `span` field</code>（<code>lab-macros</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_derive_union.rs" target="_blank"><code>ch06_derive_union.rs</code></a></td><td><code>cargo build --bin ch06_derive_union</code></td><td>无 E 码：<code>error: `Spanned` cannot be derived for unions</code>（<code>lab-macros</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_wrong_type.rs" target="_blank"><code>ch06_wrong_type.rs</code></a></td><td><code>cargo build --bin ch06_wrong_type</code></td><td>E0308（<code>lab-macros</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_panic.rs" target="_blank"><code>ch06_panic.rs</code></a></td><td><code>cargo build --bin ch06_panic</code></td><td>无 E 码：<code>error: proc-macro derive panicked</code>（<code>lab-macros-panic</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_attr.rs" target="_blank"><code>ch06_attr.rs</code></a></td><td><code>cargo run --quiet --bin ch06_attr</code></td><td>运行，输出 <code>let -&gt; 0..3</code>（<code>lab-macros-attr</code>，练习 1 的答案）</td></tr></tbody></table><p>另外两个独立的侧边 crate，各自在自己的目录里：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-export/src/lib.rs" target="_blank"><code>lab/lab-ch06-export/src/lib.rs</code></a></td><td><code>cd lab/lab-ch06-export &amp;&amp; cargo build</code></td><td>无 E 码：<code>error: `proc-macro` crate types currently cannot export any items other than functions tagged with ...</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-nodep/src/main.rs" target="_blank"><code>lab/lab-ch06-nodep/src/main.rs</code></a></td><td><code>cd lab/lab-ch06-nodep &amp;&amp; cargo build --bin lab-ch06-nodep</code></td><td>E0433</td></tr></tbody></table><p>实验用到的宏 crate 都在 <code>lab/lab-ch06-derive/</code> 里，每个都是一个只含 <code>Cargo.toml</code> 和 <code>src/lib.rs</code> 的 <code>proc-macro = true</code> 包：<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros" target="_blank"><code>lab-macros</code></a>（最终版 <code>Spanned</code>）、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-step" target="_blank"><code>lab-macros-step</code></a>、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-v2" target="_blank"><code>lab-macros-v2</code></a>、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-nostar" target="_blank"><code>lab-macros-nostar</code></a>、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-nocomma" target="_blank"><code>lab-macros-nocomma</code></a>、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-rel" target="_blank"><code>lab-macros-rel</code></a>、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-debug" target="_blank"><code>lab-macros-debug</code></a>、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-panic" target="_blank"><code>lab-macros-panic</code></a>、<a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch06-derive/lab-macros-attr" target="_blank"><code>lab-macros-attr</code></a>。仓库里还有 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch06-derive/src/bin/ch06_generic.rs" target="_blank"><code>ch06_generic.rs</code></a>（对 <code>lab-macros</code> 跑一个泛型结构体，输出 <code>4..9 at 5 (inner = hello)</code>），文章没有用到。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 让 derive 支持别的字段名。</strong> 现在宏只认叫 <code>span</code> 的字段。设计一个让 <code>struct Token { text: String, range: Span }</code> 也能 derive 的方法，不要改成”任何类型是 <code>Span</code> 的字段”（理由见练习 2）。</p>
参考答案<p>用一个<strong>辅助属性</strong>（helper attribute）：让用户在字段上标一个 <code>#[spanned]</code>，宏认这个标记。改两处。第一，宏的登记要声明这个属性存在，否则编译器会因为 <code>#[spanned]</code> 是未知属性而报错：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[proc_macro_derive(</span><span>Spanned</span><span>, attributes(spanned))]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二，<code>span_member</code> 里找字段的条件，从”名字是 <code>span</code>”改成”带 <code>#[spanned]</code> 属性，或者名字是 <code>span</code>”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.</span><span>find</span><span><span>(</span><span>|</span></span><span>field</span><span><span>|</span><span> {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>field</span><span>.attrs.</span><span>iter</span><span>().</span><span>any</span><span><span>(</span><span>|</span></span><span>attr</span><span><span>|</span><span> </span></span><span>attr</span><span>.</span><span>path</span><span>().</span><span>is_ident</span><span>(</span><span>"spanned"</span><span>))</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>||</span><span> </span><span>field</span><span>.ident.</span><span>as_ref</span><span>().</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>id</span><span><span>|</span><span> </span></span><span>id</span><span><span> </span><span>==</span><span> </span></span><span>"span"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>})</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我在 <code>lab-macros-attr</code> 里这样改了，对下面的类型实验（<code>ch06_attr.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Spanned</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Token</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>text</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>#[spanned]</span></div></div><div><div><div>5</div></div><div><span>    </span><span>range</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>打印 <code>token.span()</code> 得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let -&gt; 0..3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不要用”类型是 <code>Span</code> 的字段”，原因见下一题。</p><p><strong>2. 为什么过程宏不能”找类型是 <code>Span</code> 的字段”？</strong> 你写一个宏，想让它在字段 <code>range: Span</code> 上也工作，办法是比较字段的类型。这为什么靠不住？</p>
参考答案<p>宏拿到的只是 token：字段类型在它眼里是一个标识符 <code>Span</code>。它不知道那个 <code>Span</code> 是 <code>pebble_span::Span</code>、<code>std::ops::Range</code> 的别名，还是用户自己定义的另一个类型。用户也可以写 <code>use pebble_span::Span as Loc;</code>，那时类型的 token 是 <code>Loc</code>；或者写成 <code>pebble_span::Span</code>，token 是三段路径。宏要做的比较在名字解析（以及类型别名展开）之前，没有可靠的办法。</p><p>上面 <code>ch06_wrong_type.rs</code> 是对称的证据：字段名叫 <code>span</code> 但类型是 <code>u32</code>，宏毫无察觉，一直到类型检查阶段才被拒绝。要让宏看见类型信息，必须由用户在宏参数里明确说出来，所以辅助属性（练习 1）是惯用做法。</p><p><strong>3. 故意弄坏。</strong> 给 <code>Stmt</code> 加一个变体 <code>Block { body: Vec&lt;Stmt&gt; }</code>（不带 <code>span</code>），运行 <code>cargo build -p pebble-ast</code>，读错误。再想想：为什么这个错误里<strong>只有一条</strong>来自 derive 的，而不是三个类型各一条？</p>
参考答案<p>我在一份拷贝里做了，错误开头是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: expected a field named `span` for `#[derive(Spanned)]`</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-ast/src/lib.rs:120:11</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>120 |     Block { body: Vec&lt;Stmt&gt; },</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|           ^^^^^^^^^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>后面还跟着 <code>walk_stmt</code> 和打印器里的两条 <code>E0004</code>（<code>Stmt::Block</code> 没有被覆盖），因为这两个 <code>match</code> 还没有处理新变体。derive 错误只有一条，因为只有 <code>Stmt</code> 被弄坏了：<code>Ident</code> 和 <code>Expr</code> 各自独立展开，互不影响。同一个类型里如果有两个变体都缺 <code>span</code>，也只会报第一个：<code>collect::&lt;syn::Result&lt;Vec&lt;_&gt;&gt;&gt;()</code> 遇到第一个 <code>Err</code> 就停了。想一次报全的话，<code>syn::Error</code> 有 <code>combine</code> 方法可以把多个错误合成一个，那是另一个练习。</p><p>写宏的头一个小时，你会觉得自己在和编译器玩一种怪游戏：输入是 token，输出是 token，出错信息指向别人的代码。一旦习惯了，它就像一把电锯：很少需要，需要时没有替代品。下一篇回到更朴素的 Rust：我们要把 token 变成这一篇装好的那棵 AST。这意味着要写一个真正的递归下降解析器，也意味着要认真对待 <code>Result</code>、<code>?</code>，和那个所有 C++ 程序员都想知道答案的问题：Rust 里的错误，到底去哪儿了。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-05-ast-generics/</id>
      <title type="text">递归类型、泛型与 trait：搭一棵 AST</title>
      <published>2026-10-13T04:00:00.000Z</published>
      <updated>2026-10-13T04:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-05-ast-generics/"/>
      <summary type="text">把 token 流变成一棵树：用 Box 打破递归枚举的无限大小，用泛型和 trait bound 写出能处理任何访问者的遍历函数，再亲手撞上 ?Sized、IntoIterator 与手写 Spanned 的重复劳动。</summary>
      <content type="html"><![CDATA[<p>词法分析器吐出来的是一串平铺的 token，<code>let x = 1 + 2 * 3;</code> 在它眼里和一串字母没有区别。要让解释器知道 <code>2 * 3</code> 先算，数据结构本身必须带着”谁是谁的孩子”这件事。这就是 AST（抽象语法树）：同一个程序，从一维的 token 变成二维的树。这一篇我们把这棵树的类型定义写出来，顺路把 Rust 最有名也最让 C++ 程序员别扭的两个特性一次讲透：递归类型必须显式画出堆的边界，泛型的约束写在定义处而不是实例化处。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>你将写完 <code>pebble-ast</code> 的<strong>中间版本</strong>：<code>Program</code>、<code>Ident</code>、<code>Stmt</code>、<code>Expr</code> 三种节点，三个运算符枚举，<code>Visit</code> 访问者 trait 加三个 <code>walk_*</code> 函数，<code>AstPrinter</code>，以及三个<strong>手写</strong>的 <code>impl Spanned</code>。用到的 Rust 特性：递归枚举与 <code>Box</code>、泛型、trait bound、<code>where</code>（概念）、<code>?Sized</code>、<code>impl Trait</code> 参数、<code>IntoIterator</code>、<code>pub use</code>。
上一篇是 <a href="/posts/rust-by-pebble-04-lifetimes-lexer/">生命周期与词法分析器</a>。这一篇的 AST 不借用源码，所有字符串都是自己拥有的 <code>String</code>，所以这里没有生命周期标注。下一篇会用过程宏把三个手写的 <code>impl Spanned</code> 全部删掉。</p></div></div>
<section><h2>一棵树要装什么<a href="#一棵树要装什么"><span>#</span></a></h2><p>先想清楚 Pebble 需要表达什么。程序是一串语句；语句里有表达式；表达式里又有表达式。<code>1 + 2 * 3</code> 是一个”加法”节点，左孩子是 <code>1</code>，右孩子是一个”乘法”节点。每个节点还要记住自己来自源码的哪一段（<code>Span</code>，第 1、2 篇的主角），因为后面的所有错误信息都靠它画脱字符。</p><p>我们先用最小的例子试试直觉。进入第 1 篇建好的实验室 <code>~/rust-lab</code>，新建 <code>src/bin/ch05_recursive.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>enum</span><span> </span><span>Expr</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Add</span><span>(</span><span>Expr</span><span>, </span><span>Expr</span><span>),</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>fn</span><span> </span><span>main</span><span>() {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 C++ 程序员的第一反应：一个 <code>Add</code> 里面放两个 <code>Expr</code>。运行 <code>cargo build --bin ch05_recursive</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0072]: recursive type `Expr` has infinite size</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch05_recursive.rs:1:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>1 | enum Expr {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span>2 |     Int(i64),</span></div></div><div><div><div>7</div></div><div><span>3 |     Add(Expr, Expr),</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|         ---- recursive without indirection</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>help: insert some indirection (e.g., a `Box`, `Rc`, or `&amp;`) to break the cycle</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>3 |     Add(Box&lt;Expr&gt;, Expr),</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|         ++++    +</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>For more information about this error, try `rustc --explain E0072`.</span></div></div><div><div><div>16</div></div><div><span>error: could not compile `rust-lab` (bin "ch05_recursive") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>C++ 里同样的代码也过不了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> Expr;</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> Add { Expr lhs; Expr rhs; };</span><span>   // 错误：Expr 是不完整类型</span></div></div><div><div><div>3</div></div><div><span>struct</span><span> Expr { Add add; </span><span>long</span><span> value; };</span><span> // 一个 Expr 里装两个 Expr，大小无穷</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>问题不是语法，是算术：<code>sizeof(Expr)</code> 要包含两个 <code>Expr</code>，而两个 <code>Expr</code> 又各自包含两个 <code>Expr</code>。编译器必须在编译期知道每个类型占多少字节，因为栈帧、字段偏移、数组步长全靠它。C++ 的解法是 <code>std::unique_ptr&lt;Expr&gt;</code>：指针的大小是固定的 8 字节，不管它指向什么。Rust 的解法完全一样，名字叫 <code>Box&lt;T&gt;</code>。</p><section><h3><code>Box&lt;T&gt;</code>：堆上的独占所有权<a href="#boxt堆上的独占所有权"><span>#</span></a></h3><p>编译器的提示已经把答案写在脸上了。把 <code>Add</code> 改成两个 <code>Box&lt;Expr&gt;</code>，再给枚举加一个装 <code>Vec&lt;Expr&gt;</code> 的变体，顺手量一下大小。新建 <code>src/bin/ch05_box.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>mem</span><span>::size_of;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>enum</span><span> </span><span>Expr</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Add</span><span>(</span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;, </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>List</span><span>(</span><span>Vec</span><span>&lt;</span><span>Expr</span><span>&gt;),</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>eval</span><span>(</span><span>e</span><span>: &amp;</span><span>Expr</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>match</span><span> </span><span>e</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Int</span><span>(</span><span>n</span><span>) =&gt; *</span><span>n</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Add</span><span>(</span><span>a</span><span>, </span><span>b</span><span>) =&gt; </span><span>eval</span><span>(</span><span>a</span><span>) + </span><span>eval</span><span>(</span><span>b</span><span>),</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Expr</span><span>::</span><span>List</span><span>(</span><span>items</span><span>) =&gt; </span><span>items</span><span>.</span><span>iter</span><span>().</span><span>map</span><span>(</span><span>eval</span><span>).</span><span>sum</span><span>(),</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>tree</span><span><span> </span><span>=</span><span> </span></span><span>Expr</span><span>::</span><span>Add</span><span>(</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>Box</span><span>::</span><span>new</span><span><span>(</span><span>Expr</span><span>::</span></span><span>Int</span><span>(</span><span>1</span><span>)),</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>Box</span><span>::</span><span>new</span><span><span>(</span><span>Expr</span><span>::</span></span><span>Add</span><span><span>(</span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Expr</span><span>::</span></span><span>Int</span><span>(</span><span>2</span><span><span>)), </span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Expr</span><span>::</span></span><span>Int</span><span>(</span><span>3</span><span>)))),</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>println!</span><span>(</span><span>"eval = {}"</span><span>, </span><span>eval</span><span>(&amp;</span><span>tree</span><span>));</span></div></div><div><div><div>23</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Box&lt;Expr&gt;      = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;&gt;());</span></div></div><div><div><div>24</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Vec&lt;Expr&gt;      = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Vec</span><span>&lt;</span><span>Expr</span><span>&gt;&gt;());</span></div></div><div><div><div>25</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Expr           = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Expr</span><span>&gt;());</span></div></div><div><div><div>26</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Option&lt;Box&lt;Expr&gt;&gt; = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Option</span><span>&lt;</span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;&gt;&gt;());</span></div></div><div><div><div>27</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Option&lt;i64&gt;    = {} bytes"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Option</span><span>&lt;</span><span>i64</span><span>&gt;&gt;());</span></div></div><div><div><div>28</div></div><div><span>    </span><span>let</span><span> </span><span>list</span><span><span> </span><span>=</span><span> </span></span><span>Expr</span><span>::</span><span>List</span><span>(</span><span>vec!</span><span><span>[</span><span>Expr</span><span>::</span></span><span>Int</span><span>(</span><span>10</span><span><span>), </span><span>Expr</span><span>::</span></span><span>Int</span><span>(</span><span>20</span><span>)]);</span></div></div><div><div><div>29</div></div><div><span>    </span><span>println!</span><span>(</span><span>"list = {}"</span><span>, </span><span>eval</span><span>(&amp;</span><span>list</span><span>));</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>cargo run --quiet --bin ch05_box</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>eval = 6</span></div></div><div><div><div>2</div></div><div><span>Box&lt;Expr&gt;      = 8 bytes</span></div></div><div><div><div>3</div></div><div><span>Vec&lt;Expr&gt;      = 24 bytes</span></div></div><div><div><div>4</div></div><div><span>Expr           = 24 bytes</span></div></div><div><div><div>5</div></div><div><span>Option&lt;Box&lt;Expr&gt;&gt; = 8 bytes</span></div></div><div><div><div>6</div></div><div><span>Option&lt;i64&gt;    = 16 bytes</span></div></div><div><div><div>7</div></div><div><span>list = 30</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>几件事一次看清：</p><ul>
<li><code>Box&lt;Expr&gt;</code> 8 字节，就是一个指针。它是 <code>unique_ptr</code>：独占所有权，离开作用域时释放堆内存，而且 <code>Drop</code> 是递归的，丢掉根节点会沿着树把所有节点释放干净。你不需要写析构函数，也不会忘记写。</li>
<li><code>Vec&lt;Expr&gt;</code> 24 字节：指针、容量、长度三个字，和元素数量无关。所以**<code>Vec&lt;Expr&gt;</code> 本身就是间接层**，<code>List(Vec&lt;Expr&gt;)</code> 不需要再套 <code>Box</code>。第三个练习会让你自己确认这一点。</li>
<li><code>Expr</code> 整个枚举 24 字节：一个标签加上最大变体（<code>Vec</code> 的 24 字节），编译器把标签塞进了某处的空位（我猜是 <code>Vec</code> 容量字段里不可能出现的值，这叫 niche 优化，不过我没有去验证具体位置）。</li>
<li><code>Option&lt;Box&lt;Expr&gt;&gt;</code> 还是 8 字节：<code>Box</code> 永远非空，所以 <code>None</code> 直接用空指针表示。<code>Option&lt;i64&gt;</code> 就没这个好运气，要 16 字节。这是 C 里”用 NULL 表示没有”那个老技巧，被编译器保证成了类型系统的一部分。</li>
</ul><div><div><div></div><div>和 <code>unique_ptr</code> 的区别</div></div><div><p><code>Box&lt;T&gt;</code> 不会为空，不像 <code>unique_ptr</code> 可以是 <code>nullptr</code>，所以不存在对着空盒子解引用的问题。想表达”可能没有”，写 <code>Option&lt;Box&lt;T&gt;&gt;</code>，上面的数字说明它不花额外的钱。</p></div></div><p>好，类型层面的问题解决了。现在开始写真正的 AST。</p></section></section>
<section><h2>搭 <code>pebble-ast</code><a href="#搭-pebble-ast"><span>#</span></a></h2><p>在工作区根目录（上一篇你已经有 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-lexer</code> 三个 crate）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-ast</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>打开根 <code>Cargo.toml</code>，在 <code>[workspace.dependencies]</code> 里加一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-ast</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-ast"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>members = ["crates/*"]</code> 会自动把新目录纳入工作区。然后把 <code>crates/pebble-ast/Cargo.toml</code> 整个替换成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-ast"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"The Pebble abstract syntax tree and its visitor traits."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>[</span><span>dev-dependencies</span><span>]</span></div></div><div><div><div>15</div></div><div><span>pebble-lexer.workspace</span><span> = </span><span>true</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>18</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pebble-span</code> 提供 <code>Span</code> 和 <code>Spanned</code>。<code>[dev-dependencies]</code> 里的 <code>pebble-lexer</code> 只在测试和示例里可见，不会进入下游的依赖树；说实话，这一篇的四个测试没有用到它，我保留它是为了让这个文件和仓库只差一行（下一篇会加上的那一行）。Cargo 不会为没用到的依赖报警。</p><div><div><div></div><div>仓库里还有一行</div></div><div><p>仓库里的 <code>pebble-ast/Cargo.toml</code> 在 <code>[dependencies]</code> 下面多一行 <code>pebble-macros.workspace = true</code>。那是第 6 篇才加的依赖，现在不要写。</p></div></div><section><h3>文件头与 <code>Program</code><a href="#文件头与-program"><span>#</span></a></h3><p><code>crates/pebble-ast/src/lib.rs</code> 从头写。这一篇的版本头部文档比仓库短一句（仓库的那句”<code>Spanned</code> 由 <code>pebble-macros</code> 派生”要等第 6 篇才成立）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>2</div></div><div><span>//! The Pebble abstract syntax tree.</span></div></div><div><div><div>3</div></div><div><span>//!</span></div></div><div><div><div>4</div></div><div><span>//! The AST is where the language stops being text and starts being *structure*.</span></div></div><div><div><div>5</div></div><div><span>//! Three Rust ideas do most of the work here:</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! * **Algebraic data types.** [`Expr`] and [`Stmt`] are recursive enums; a</span></div></div><div><div><div>8</div></div><div><span>//!   program is a `Vec&lt;Stmt&gt;` and an expression is a tree. Rust's `enum` is the</span></div></div><div><div><div>9</div></div><div><span>//!   single feature that makes writing a compiler pleasant.</span></div></div><div><div><div>10</div></div><div><span>//! * **`Box`.** Recursive enums need indirection, so child expressions are</span></div></div><div><div><div>11</div></div><div><span>//!   `Box&lt;Expr&gt;`.</span></div></div><div><div><div>12</div></div><div><span>//! * **Traits + a visitor.** [`Visit`] is a trait with default methods, and</span></div></div><div><div><div>13</div></div><div><span>//!   [`AstPrinter`] is one implementation.</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>use</span><span><span> </span><span>core</span><span>::fmt;</span></span></div></div><div><div><div>16</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>string</span><span>::</span></span><span>String</span><span>;</span></div></div><div><div><div>17</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>vec</span><span>::</span></span><span>Vec</span><span>;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>pub</span><span> </span><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>Span</span><span>, </span><span>Spanned</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>#![forbid(unsafe_code)]</code> 是 crate 级属性（第 2 篇见过）：这个 crate 里写 <code>unsafe</code> 就是编译错误。<code>String</code> 和 <code>Vec</code> 的两行 <code>use</code> 其实多余，它们本来就在 prelude 里；我保留它们只是为了和仓库逐字节一致。</p><p>最后一行 <code>pub use</code> 值得停一下。<code>use</code> 把名字引入<strong>当前</strong>作用域；<code>pub use</code> 还把它<strong>重新导出</strong>，下游的 crate 可以写 <code>pebble_ast::Span</code>，不必再单独依赖 <code>pebble-span</code>。C++ 里最接近的是在头文件里 <code>using lib::Span;</code>。我们这样做是因为 AST 的使用者几乎总要同时碰到节点和它们的 <code>Span</code>，让他们只 <code>use pebble_ast::{...}</code> 一行就够。</p><p>接着是程序本身：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A whole parsed program.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Program</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Top-level statements, in source order.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>stmts</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>Program</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// Wraps a statement list.</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>stmts</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Self</span><span> { </span><span>stmts</span><span> }</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>derive 三件套你熟了：<code>Clone</code>（深拷贝，整棵树复制一份）、<code>Debug</code>（<code>{:?}</code> 打印）、<code>PartialEq</code>（逐字段比较，测试里直接 <code>assert_eq!</code> 两棵树）。注意这里<strong>没有</strong> <code>Eq</code>：<code>Expr::Float</code> 里装着 <code>f64</code>，而 <code>f64</code> 的 <code>NaN != NaN</code> 违反了 <code>Eq</code> 要求的自反性。这一点第 4 篇的 <code>TokenKind</code> 已经让你撞过一次墙。</p></section><section><h3>标识符<a href="#标识符"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// An identifier: its name plus where it appeared.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Ident</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>name</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>Ident</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// Builds an identifier.</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>name</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>name</span><span>: </span><span>name</span><span>.</span><span>into</span><span>(),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Ident</span><span> {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>f</span><span>.</span><span>write_str</span><span>(&amp;</span><span>self</span><span>.name)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>name</code> 是拥有的 <code>String</code>，不是 <code>&amp;'src str</code>。第 4 篇的 token 借用源码，因为 token 流是用完即弃的；AST 要活得久，解释器和编译器都会拿着它，还要能被克隆、跨线程传递（第 14 篇的并行 <code>check</code> 就会这样做），所以让每个节点自己拥有数据，生命周期标注就不会像传染病一样爬满整个编译器。这是一个典型的取舍：多一次 <code>String</code> 分配，换来所有类型签名里没有 <code>'src</code>。</p><p><code>new</code> 的参数 <code>name: impl Into&lt;String&gt;</code> 你在第 2 篇见过：任何能转成 <code>String</code> 的东西（<code>&amp;str</code>、<code>String</code>）都收。<code>#[must_use]</code> 同样来自第 1 篇：忘了用返回值，编译器会警告。</p></section><section><h3>语句<a href="#语句"><span>#</span></a></h3><p>语句是”发生一件事、可能改变状态”的东西。按 Pebble 的语法，一共九种：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A statement: something that happens, and may change state.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Stmt</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// `let name = value;`</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Let</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>name</span><span>: </span><span>Ident</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>value</span><span>: </span><span>Expr</span><span>,</span></div></div><div><div><div>8</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>10</div></div><div><span>    </span><span>/// `fn name(params) { body }`</span></div></div><div><div><div>11</div></div><div><span>    </span><span>Fn</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>name</span><span>: </span><span>Ident</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>params</span><span>: </span><span>Vec</span><span>&lt;</span><span>Ident</span><span>&gt;,</span></div></div><div><div><div>14</div></div><div><span>        </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>15</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// An expression evaluated for its side effects.</span></div></div><div><div><div>18</div></div><div><span>    </span><span>Expr</span><span> { </span><span>expr</span><span>: </span><span>Expr</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>19</div></div><div><span>    </span><span>/// `if cond { .. } else { .. }`</span></div></div><div><div><div>20</div></div><div><span>    </span><span>If</span><span> {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>cond</span><span>: </span><span>Expr</span><span>,</span></div></div><div><div><div>22</div></div><div><span>        </span><span>then_branch</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>23</div></div><div><span>        </span><span>else_branch</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>24</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>26</div></div><div><span>    </span><span>/// `while cond { .. }`</span></div></div><div><div><div>27</div></div><div><span>    </span><span>While</span><span> {</span></div></div><div><div><div>28</div></div><div><span>        </span><span>cond</span><span>: </span><span>Expr</span><span>,</span></div></div><div><div><div>29</div></div><div><span>        </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>30</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>32</div></div><div><span>    </span><span>/// `for var in iter { .. }`</span></div></div><div><div><div>33</div></div><div><span>    </span><span>For</span><span> {</span></div></div><div><div><div>34</div></div><div><span>        </span><span>var</span><span>: </span><span>Ident</span><span>,</span></div></div><div><div><div>35</div></div><div><span>        </span><span>iter</span><span>: </span><span>Expr</span><span>,</span></div></div><div><div><div>36</div></div><div><span>        </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>37</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>39</div></div><div><span>    </span><span>/// `return value?;`</span></div></div><div><div><div>40</div></div><div><span>    </span><span>Return</span><span> { </span><span>value</span><span>: </span><span>Option</span><span>&lt;</span><span>Expr</span><span>&gt;, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>41</div></div><div><span>    </span><span>/// `break;`</span></div></div><div><div><div>42</div></div><div><span>    </span><span>Break</span><span> { </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>43</div></div><div><span>    </span><span>/// `continue;`</span></div></div><div><div><div>44</div></div><div><span>    </span><span>Continue</span><span> { </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里的变体不是 C 枚举那种光秃秃的整数，也不是 <code>Let(Ident, Expr, Span)</code> 那种按位置记字段的元组变体，而是<strong>结构体式变体</strong>：每个变体像一个匿名 struct，字段有名字。<code>Stmt::Let { name, value, span }</code> 比 <code>Stmt::Let(a, b, c)</code> 好读得多，模式匹配时还能用 <code>..</code> 忽略不关心的字段。这相当于 C++ <code>std::variant&lt;LetStmt, FnStmt, ...&gt;</code> 加上每个类型的定义，只不过写在一个地方，并且 <code>match</code> 必须穷尽所有变体（第 3 篇的 E0004）。</p><p>几个细节：</p><ul>
<li><code>Stmt::Fn</code> 的 <code>body</code> 是 <code>Vec&lt;Stmt&gt;</code>：<code>Stmt</code> 里直接包含 <code>Vec&lt;Stmt&gt;</code>，没有 <code>Box</code>，原因前面解释过：<code>Vec</code> 自己就是堆指针。</li>
<li><code>Return { value: Option&lt;Expr&gt;, .. }</code>：这里 <code>Option&lt;Expr&gt;</code> 也没有 <code>Box</code>。<code>Expr</code> 的大小有限（我们等会儿量），<code>Option&lt;Expr&gt;</code> 就是”<code>Expr</code> 加一个标签”，不是递归。<code>Expr</code> 里面递归的地方才需要间接层。</li>
<li>每个变体都带一个 <code>span</code>。<code>If</code> 的 <code>else_branch</code> 没有 <code>Option</code>：没有 <code>else</code> 就是空的 <code>Vec</code>，少一层分支。</li>
</ul></section><section><h3>表达式<a href="#表达式"><span>#</span></a></h3><p>表达式是”产生一个值”的东西，也是整棵树里真正递归的部分：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// An expression: something that produces a value.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Expr</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// `nil`</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Nil</span><span> { </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// `true` / `false`</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Bool</span><span> { </span><span>value</span><span>: </span><span>bool</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// `42`</span></div></div><div><div><div>9</div></div><div><span>    </span><span>Int</span><span> { </span><span>value</span><span>: </span><span>i64</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>10</div></div><div><span>    </span><span>/// `4.2`</span></div></div><div><div><div>11</div></div><div><span>    </span><span>Float</span><span> { </span><span>value</span><span>: </span><span>f64</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>12</div></div><div><span>    </span><span>/// `"hi"`</span></div></div><div><div><div>13</div></div><div><span>    </span><span>Str</span><span> { </span><span>value</span><span>: </span><span>String</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>14</div></div><div><span>    </span><span>/// A variable reference.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Var</span><span> { </span><span>name</span><span>: </span><span>Ident</span><span>, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>16</div></div><div><span>    </span><span>/// `[a, b, c]`</span></div></div><div><div><div>17</div></div><div><span>    </span><span>List</span><span> { </span><span>items</span><span>: </span><span>Vec</span><span>&lt;</span><span>Expr</span><span>&gt;, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div><div><div><div>18</div></div><div><span>    </span><span>/// `#{ key: value, .. }`</span></div></div><div><div><div>19</div></div><div><span>    </span><span>Map</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>entries</span><span>: </span><span>Vec</span><span>&lt;(</span><span>Expr</span><span>, </span><span>Expr</span><span>)&gt;,</span></div></div><div><div><div>21</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>23</div></div><div><span>    </span><span>/// `-x`, `not x`</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Unary</span><span> {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>op</span><span>: </span><span>UnaryOp</span><span>,</span></div></div><div><div><div>26</div></div><div><span>        </span><span>operand</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>27</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>29</div></div><div><span>    </span><span>/// `a + b`, `a &lt; b`</span></div></div><div><div><div>30</div></div><div><span>    </span><span>Binary</span><span> {</span></div></div><div><div><div>31</div></div><div><span>        </span><span>op</span><span>: </span><span>BinaryOp</span><span>,</span></div></div><div><div><div>32</div></div><div><span>        </span><span>lhs</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>33</div></div><div><span>        </span><span>rhs</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>34</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>36</div></div><div><span>    </span><span>/// `a and b`, short-circuiting</span></div></div><div><div><div>37</div></div><div><span>    </span><span>Logical</span><span> {</span></div></div><div><div><div>38</div></div><div><span>        </span><span>op</span><span>: </span><span>LogicalOp</span><span>,</span></div></div><div><div><div>39</div></div><div><span>        </span><span>lhs</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>40</div></div><div><span>        </span><span>rhs</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>41</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>43</div></div><div><span>    </span><span>/// `target = value`</span></div></div><div><div><div>44</div></div><div><span>    </span><span>Assign</span><span> {</span></div></div><div><div><div>45</div></div><div><span>        </span><span>target</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>46</div></div><div><span>        </span><span>value</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>47</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>49</div></div><div><span>    </span><span>/// `callee(args)`</span></div></div><div><div><div>50</div></div><div><span>    </span><span>Call</span><span> {</span></div></div><div><div><div>51</div></div><div><span>        </span><span>callee</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>52</div></div><div><span>        </span><span>args</span><span>: </span><span>Vec</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>53</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>55</div></div><div><span>    </span><span>/// `object[index]`</span></div></div><div><div><div>56</div></div><div><span>    </span><span>Index</span><span> {</span></div></div><div><div><div>57</div></div><div><span>        </span><span>object</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>58</div></div><div><span>        </span><span>index</span><span>: </span><span>Box</span><span>&lt;</span><span>Expr</span><span>&gt;,</span></div></div><div><div><div>59</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>61</div></div><div><span>    </span><span>/// An anonymous function / closure.</span></div></div><div><div><div>62</div></div><div><span>    </span><span>Fn</span><span> {</span></div></div><div><div><div>63</div></div><div><span>        </span><span>params</span><span>: </span><span>Vec</span><span>&lt;</span><span>Ident</span><span>&gt;,</span></div></div><div><div><div>64</div></div><div><span>        </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;,</span></div></div><div><div><div>65</div></div><div><span>        </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>67</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对照上一节的实验：凡是”孩子是单个 <code>Expr</code>”的地方（<code>Unary</code>、<code>Binary</code>、<code>Logical</code>、<code>Assign</code>、<code>Call</code> 的被调用者、<code>Index</code>），都是 <code>Box&lt;Expr&gt;</code>；凡是孩子是一串的地方（<code>List</code>、<code>Call</code> 的参数、<code>Map</code> 的键值对），是 <code>Vec</code>。<code>Map</code> 的 <code>Vec&lt;(Expr, Expr)&gt;</code> 里的 <code>(Expr, Expr)</code> 是元组：C++ 的 <code>std::pair&lt;Expr, Expr&gt;</code>，匿名的、字段按位置取。</p><p><code>Stmt</code> 里的 <code>Expr::Fn</code> 和 <code>Stmt::Fn</code> 同名但互不干扰，<code>Stmt::Fn</code> 是 <code>fn name() {}</code> 声明，<code>Expr::Fn</code> 是 <code>fn() {}</code> 匿名函数值。枚举变体的名字属于枚举自己的命名空间。</p><p>写完这两个枚举，把 <code>ch05_box</code> 里的数字换成真货。我在实验目录之外另建了一个小 crate 去依赖刚写好的 <code>pebble-ast</code>（步骤略，核心就是一个 <code>main</code> 里的几行 <code>size_of</code>；仓库里的版本是 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch05-ast/src/bin/ch05_sizes.rs" target="_blank"><code>lab/lab-ch05-ast/src/bin/ch05_sizes.rs</code></a>），输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Span         = 12 bytes</span></div></div><div><div><div>2</div></div><div><span>Ident        = 40 bytes</span></div></div><div><div><div>3</div></div><div><span>Stmt         = 144 bytes</span></div></div><div><div><div>4</div></div><div><span>Expr         = 64 bytes</span></div></div><div><div><div>5</div></div><div><span>Box&lt;Expr&gt;    = 8 bytes</span></div></div><div><div><div>6</div></div><div><span>Vec&lt;Expr&gt;    = 24 bytes</span></div></div><div><div><div>7</div></div><div><span>(Expr, Expr) = 128 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Expr</code> 的 64 字节来自最大的变体 <code>Fn</code>（两个 <code>Vec</code> 加一个 <code>Span</code>，60 字节，补齐到 64）；<code>Stmt</code> 的 144 字节来自 <code>For</code>（<code>Ident</code> 40 + <code>Expr</code> 64 + <code>Vec</code> 24 + <code>Span</code> 12 = 140，对齐到 144）。这两个结论是我拿字段大小加出来的，没有去让编译器报告布局。重点是：<strong>枚举的大小等于最大变体加标签</strong>，写 <code>Nil</code> 也要付 64 字节。如果你在乎，可以把大变体里的东西装箱，clippy 有一个 <code>large_enum_variant</code> 的 lint 专门提醒这件事。Pebble 没有做，因为程序的 AST 不大，没必要为它牺牲可读性。</p></section><section><h3>运算符<a href="#运算符"><span>#</span></a></h3><p>三个小枚举。<code>Copy</code> 在第 1 篇里解释过：“像 C 结构体一样按位拷贝”，运算符就是一个字节的标签，拷贝它比借用它还便宜：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Prefix operators.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>UnaryOp</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// `-`</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Neg</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// `not` / `!`</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Not</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>/// Infix arithmetic and comparison operators.</span></div></div><div><div><div>11</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>)]</span></div></div><div><div><div>12</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>BinaryOp</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>Add</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Sub</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>Mul</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>Div</span><span>,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>Rem</span><span>,</span></div></div><div><div><div>18</div></div><div><span>    </span><span>Eq</span><span>,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>NotEq</span><span>,</span></div></div><div><div><div>20</div></div><div><span>    </span><span>Lt</span><span>,</span></div></div><div><div><div>21</div></div><div><span>    </span><span>LtEq</span><span>,</span></div></div><div><div><div>22</div></div><div><span>    </span><span>Gt</span><span>,</span></div></div><div><div><div>23</div></div><div><span>    </span><span>GtEq</span><span>,</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>BinaryOp</span><span> {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>28</div></div><div><span>        </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Add</span><span> =&gt; </span><span>"+"</span><span>,</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Sub</span><span> =&gt; </span><span>"-"</span><span>,</span></div></div><div><div><div>31</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Mul</span><span> =&gt; </span><span>"*"</span><span>,</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Div</span><span> =&gt; </span><span>"/"</span><span>,</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Rem</span><span> =&gt; </span><span>"%"</span><span>,</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Eq</span><span> =&gt; </span><span>"=="</span><span>,</span></div></div><div><div><div>35</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>NotEq</span><span> =&gt; </span><span>"!="</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Lt</span><span> =&gt; </span><span>"&lt;"</span><span>,</span></div></div><div><div><div>37</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>LtEq</span><span> =&gt; </span><span>"&lt;="</span><span>,</span></div></div><div><div><div>38</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>Gt</span><span> =&gt; </span><span>"&gt;"</span><span>,</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>BinaryOp</span><span>::</span><span>GtEq</span><span> =&gt; </span><span>"&gt;="</span><span>,</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>/// Short-circuiting logical operators.</span></div></div><div><div><div>45</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>)]</span></div></div><div><div><div>46</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>LogicalOp</span><span> {</span></div></div><div><div><div>47</div></div><div><span>    </span><span>And</span><span>,</span></div></div><div><div><div>48</div></div><div><span>    </span><span>Or</span><span>,</span></div></div><div><div><div>49</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>and</code> 和 <code>or</code> 为什么不放进 <code>BinaryOp</code>？因为它们的求值规则不同：<code>a and b</code> 在 <code>a</code> 为假时<strong>不求值 <code>b</code></strong>，所以在解释器和编译器里需要完全不同的处理（第 9、10 篇会看到）。把”语法上长得像”和”语义上要分开对待”的东西在类型里分开，是让 <code>match</code> 替你记住差别的办法。这里 <code>Eq</code> 能 derive：这些枚举里没有 <code>f64</code>。</p></section></section>
<section><h2><code>Spanned</code>：三个手写的大 <code>match</code><a href="#spanned三个手写的大-match"><span>#</span></a></h2><p><code>Spanned</code> trait 来自第 2 篇，只有一个方法 <code>fn span(&amp;self) -&gt; Span</code>。解释器、编译器、错误报告都会问”这个节点在源码哪一段”。三个类型要实现它：<code>Ident</code>、<code>Stmt</code>、<code>Expr</code>。<strong>这一篇我们手写</strong>，把重复的痛苦体验一遍，下一篇你就会明白为什么值得为它专门写一个宏。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Spanned</span><span> </span><span>for</span><span> </span><span>Ident</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.span</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Ident</code> 是 struct，直接返回字段。<code>Span</code> 是 <code>Copy</code>，<code>self.span</code> 从 <code>&amp;self</code> 里拷出一份，不是移动。</p><p><code>Stmt</code> 是枚举：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Spanned</span><span> </span><span>for</span><span> </span><span>Stmt</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Let</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>Fn</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>Expr</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>If</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>While</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>For</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>Return</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>Break</span><span> { </span><span>span</span><span> }</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Stmt</span><span>::</span><span>Continue</span><span> { </span><span>span</span><span> } =&gt; *</span><span>span</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个第 3 篇没细讲的语法：</p><ul>
<li><strong>或模式</strong> <code>A | B | C =&gt; ...</code>：多个模式共享一个分支体。<strong>每个模式必须绑定同样名字、同样类型的变量</strong>，这里每个都绑出一个 <code>span</code>，所以分支体里能统一使用。<code>{ span, .. }</code> 里的 <code>..</code> 表示”其他字段我不关心”；<code>Break</code> 和 <code>Continue</code> 只有 <code>span</code> 一个字段，不需要写 <code>..</code>（写了也可以，只是多余）。</li>
<li><strong><code>*span</code></strong>：<code>match self</code> 的 <code>self</code> 是 <code>&amp;Stmt</code>，通过引用匹配出来的字段也是引用（编译器自动把绑定变成 <code>&amp;Span</code>，这叫默认绑定模式，下一篇会碰到它的麻烦），所以要解引用一次才能得到 <code>Span</code>。<code>Span</code> 是 <code>Copy</code>，所以 <code>*span</code> 是按位拷贝，不是移动。</li>
</ul><p><code>Expr</code> 同理，十五个变体：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Spanned</span><span> </span><span>for</span><span> </span><span>Expr</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Nil</span><span> { </span><span>span</span><span> }</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Bool</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Int</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Float</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Str</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Var</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>List</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Map</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Unary</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Binary</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Logical</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Assign</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Call</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Index</span><span> { </span><span>span</span><span>, .. }</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Fn</span><span> { </span><span>span</span><span>, .. } =&gt; *</span><span>span</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>把这三段放进 <code>lib.rs</code>：<code>impl Spanned for Ident</code> 紧跟在 <code>Ident</code> 的 <code>Display</code> 之后，<code>impl Spanned for Stmt</code> 紧跟 <code>Stmt</code> 枚举，<code>impl Spanned for Expr</code> 紧跟 <code>Expr</code> 枚举。（顺序不影响编译，但这样和仓库的文件布局一致。）</p><section><h3>我为什么说这是痛苦<a href="#我为什么说这是痛苦"><span>#</span></a></h3><p>不妨做个实验，感受一下。把 <code>Stmt</code> 的最后加一个变体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// `{ .. }`</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Block</span><span> { </span><span>body</span><span>: </span><span>Vec</span><span>&lt;</span><span>Stmt</span><span>&gt;, </span><span>span</span><span>: </span><span>Span</span><span> },</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我在一份拷贝里做了这件事，<code>cargo build -p pebble-ast</code> 立刻给出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0004]: non-exhaustive patterns: `&amp;Stmt::Block { .. }` not covered</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-ast/src/lib.rs:125:15</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>125 |         match self {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|               ^^^^ pattern `&amp;Stmt::Block { .. }` not covered</span></div></div><div><div><div>6</div></div><div><span>...</span></div></div><div><div><div>7</div></div><div><span>error[E0004]: non-exhaustive patterns: `&amp;Stmt::Block { .. }` not covered</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-ast/src/lib.rs:309:11</span></div></div><div><div><div>9</div></div><div><span>...</span></div></div><div><div><div>10</div></div><div><span>error[E0004]: non-exhaustive patterns: `&amp;Stmt::Block { .. }` not covered</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-ast/src/lib.rs:432:27</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div><div><div><div>13</div></div><div><span>error: could not compile `pebble-ast` (lib) due to 3 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>好消息是编译器一处不漏地指出所有要改的地方（C++ 的 <code>std::visit</code> 漏掉一个类型的分支，在编译期也会被拒绝，只不过报的是一大段模板错误，不太好读；真正要等到运行时才发现的，是带 <code>default:</code> 的 <code>switch</code> 或者带通用兜底分支的访问者）。坏消息是三处里有一处纯粹是机械劳动：<code>Spanned</code> 那个 <code>match</code> 里的每个分支都在做完全相同的事，“把叫 <code>span</code> 的字段取出来”。其余两处（<code>walk_stmt</code> 和打印器）是真有信息量的工作，每个变体怎样遍历、怎样显示，只有人知道。</p><p>这就是第 6 篇的动机：让编译器自己去生成那个机械的 <code>match</code>。现在先把实验撤回，继续往下。</p></section></section>
<section><h2>访问者：让遍历和节点分家<a href="#访问者让遍历和节点分家"><span>#</span></a></h2><p>AST 建好了，接下来谁来用它？打印器用，解释器用，字节码编译器用，将来任何分析工具都要用。它们各自要”走遍整棵树”，但对每种节点要做的事不同。如果每个工具都自己写一遍递归 <code>match</code>，十五个表达式变体和九个语句变体的”孩子在哪”信息就会被复制到每个工具里。</p><p>标准的解决办法是<strong>访问者模式</strong>。C++ 的经典写法（我把它写成了一个可运行的小程序 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch05_visitor.cpp" target="_blank"><code>lab/cpp/ch05_visitor.cpp</code></a>，用 <code>g++ -std=c++20</code> 编译通过，输出 <code>add</code>、<code>int 1</code>、<code>int 2</code> 三行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> Visitor {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>virtual </span><span>void</span><span> </span><span>visit</span><span>(</span><span>const</span><span> Int</span><span>&amp;</span><span>) </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>virtual </span><span>void</span><span> </span><span>visit</span><span>(</span><span>const</span><span> Add</span><span>&amp;</span><span>) </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>virtual </span><span>~</span><span>Visitor</span><span>() </span><span>=</span><span> default;</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>struct</span><span> Expr {</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>virtual </span><span>void</span><span> </span><span>accept</span><span>(Visitor</span><span>&amp;</span><span>) </span><span>const</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>virtual </span><span>~</span><span>Expr</span><span>() </span><span>=</span><span> default;</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>struct</span><span> Int : Expr {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>long</span><span> value;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>void</span><span> </span><span>accept</span><span>(Visitor</span><span>&amp;</span><span> v) </span><span>const</span><span> override { </span><span>v</span><span>.</span><span>visit</span><span>(</span><span>*</span><span>this); }</span></div></div><div><div><div>15</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>每个节点类要写 <code>accept</code>，通过它把自己的具体类型”弹回”给 <code>Visitor</code> 的对应重载，这叫双重分派：一次虚调用确定节点类型，一次重载解析确定访问者的方法。每加一个节点类，每个访问者基类要加一个纯虚函数，每个子类要实现它，哪怕是个空操作。</p><p>Rust 的版本利用的是枚举：节点类型已经是一个封闭的 <code>enum</code>，<code>match</code> 就是分派，不需要 <code>accept</code>。我们只需要一个 trait 来表示”访问者”，并且给它的方法<strong>默认实现</strong>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A read-only AST visitor with sensible defaults.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Override only the methods you care about; the rest fall back to a full</span></div></div><div><div><div>4</div></div><div><span>/// recursive walk, so a visitor that does nothing visits every node.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>trait</span><span> </span><span>Visit</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Visits a whole program.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_program</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>walk_program</span><span>(</span><span>self</span><span>, </span><span>program</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>/// Visits a statement.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_stmt</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>stmt</span><span>: &amp;</span><span>Stmt</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>walk_stmt</span><span>(</span><span>self</span><span>, </span><span>stmt</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>    </span><span>/// Visits an expression.</span></div></div><div><div><div>17</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>walk_expr</span><span>(</span><span>self</span><span>, </span><span>expr</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>默认方法在第 3 篇的 <code>DiagnosticSink</code> 里已经出现过。这里每个默认实现都转手交给一个自由函数 <code>walk_*</code>，由它负责”遍历孩子”。关键的设计：<strong><code>walk_*</code> 只管”孩子在哪”，<code>visit_*</code> 只管”到了这个节点做什么”</strong>。覆盖 <code>visit_expr</code> 的访问者可以在自己的逻辑里选择是否调用 <code>walk_expr</code> 继续往下走。</p><section><h3>写 <code>walk_*</code>：泛型函数第一次出场<a href="#写-walk_泛型函数第一次出场"><span>#</span></a></h3><p>现在写 <code>walk_*</code>。先用一个直觉的版本。在 <code>Visit</code> 之后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_program</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>program</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先别管 <code>for stmt in program</code>（下一节解释），盯着签名：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>walk_program</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>&lt;V: Visit&gt;</code> 声明一个类型参数 <code>V</code>，并且约束 <code>V</code> 必须实现 <code>Visit</code>。调用时 <code>V</code> 由实参推断：传入 <code>&amp;mut AstPrinter</code> 就是 <code>V = AstPrinter</code>。这就是 C++ 的 <code>template &lt;typename V&gt; void walk_program(V&amp; visitor, ...)</code>，两者实现方式也一样：<strong>单态化</strong>。编译器为每个实际用到的 <code>V</code> 生成一份专门的机器码，没有虚调用、没有装箱，<code>visitor.visit_stmt(stmt)</code> 可以被内联。</p></section><section><h3>约束写在定义处：Rust 泛型和 C++ 模板最大的不同<a href="#约束写在定义处rust-泛型和-c-模板最大的不同"><span>#</span></a></h3><p>看一个对比。C++ 里，你可以写一个对类型毫无要求的模板：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template </span><span>&lt;</span><span>typename T</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>T </span><span>largest</span><span><span>(</span><span>T </span><span>a</span><span>,</span><span> T </span><span>b</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> a </span><span>&gt;</span><span> b </span><span>?</span><span> a </span><span>:</span><span> b;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它对 <code>T</code> 的要求（要有 <code>operator&gt;</code>）是<strong>隐藏在函数体里</strong>的，直到有人用不支持的类型实例化它才报错。我写了个空结构体 <code>Pebble</code> 试了一下（<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch05_largest.cpp" target="_blank"><code>lab/cpp/ch05_largest.cpp</code></a>，<code>g++ -std=c++20</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>largest.cpp: In instantiation of 'T largest(T, T) [with T = Pebble]':</span></div></div><div><div><div>2</div></div><div><span>largest.cpp:9:12:   required from here</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>9 |     largest(Pebble{}, Pebble{});</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>|     ~~~~~~~^~~~~~~~~~~~~~~~~~~~</span></div></div><div><div><div>5</div></div><div><span>largest.cpp:5:14: error: no match for 'operator&gt;' (operand types are 'Pebble' and 'Pebble')</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>5 |     return a &gt; b ? a : b;</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>|            ~~^~~</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>错误出现在<strong>调用处</strong>要求的实例化里，你得顺着 “required from here” 一路往上读。（C++20 的 concepts 可以把约束写在签名里，但它是可选的。另外 g++ 输出里的弯引号我换成了直引号，别的字符没动。）</p><p>Rust 做不到这种偷懒。在实验室新建 <code>src/bin/ch05_bound.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>largest</span><span>&lt;</span><span>T</span><span>&gt;(</span><span>a</span><span>: </span><span>T</span><span>, </span><span>b</span><span>: </span><span>T</span><span>) -&gt; </span><span>T</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> </span><span>a</span><span><span> </span><span>&gt;</span><span> </span></span><span>b</span><span> { </span><span>a</span><span> } </span><span>else</span><span> { </span><span>b</span><span> }</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>largest</span><span>(</span><span>3</span><span>, </span><span>9</span><span>));</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo build --bin ch05_bound</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0369]: binary operation `&gt;` cannot be applied to type `T`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch05_bound.rs:2:10</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>2 |     if a &gt; b { a } else { b }</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|        - ^ - T</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|        |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|        T</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>help: consider restricting type parameter `T` with trait `PartialOrd`</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>1 | fn largest&lt;T: std::cmp::PartialOrd&gt;(a: T, b: T) -&gt; T {</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|             ++++++++++++++++++++++</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>For more information about this error, try `rustc --explain E0369`.</span></div></div><div><div><div>15</div></div><div><span>error: could not compile `rust-lab` (bin "ch05_bound") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意：错误出现在<strong>定义处</strong>，而且 <code>main</code> 里调用 <code>largest(3, 9)</code> 完全合法，但函数本身就不合格：泛型函数体只能使用约束里声明过的能力。这样做的好处是，库的作者在写这个函数的时候就知道它对不对；调用者只看签名就知道要提供什么，不用读函数体。代价是你必须把”我需要什么”写出来。照提示把签名改成 <code>fn largest&lt;T: PartialOrd&gt;(a: T, b: T) -&gt; T</code>（文件另存为 <code>ch05_mono.rs</code>，<code>main</code> 里分别用 <code>i32</code>、<code>f64</code>、<code>&amp;str</code> 调用三次）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>largest</span><span>&lt;</span><span>T</span><span>: </span><span>PartialOrd</span><span>&gt;(</span><span>a</span><span>: </span><span>T</span><span>, </span><span>b</span><span>: </span><span>T</span><span>) -&gt; </span><span>T</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> </span><span>a</span><span><span> </span><span>&gt;</span><span> </span></span><span>b</span><span> { </span><span>a</span><span> } </span><span>else</span><span> { </span><span>b</span><span> }</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>largest</span><span>(</span><span>3_</span><span>i32</span><span>, </span><span>9</span><span>));</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>largest</span><span>(</span><span>2.5_</span><span>f64</span><span>, </span><span>1.5</span><span>));</span></div></div><div><div><div>8</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>largest</span><span>(</span><span>"pebble"</span><span>, </span><span>"stone"</span><span>));</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>9</span></div></div><div><div><div>2</div></div><div><span>2.5</span></div></div><div><div><div>3</div></div><div><span>stone</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我想亲眼确认”单态化”不是个比喻，所以构建调试版并列出符号表：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --quiet --bin ch05_mono</span></div></div><div><div><div>2</div></div><div><span>$ nm -C target/debug/ch05_mono | grep largest</span></div></div><div><div><div>3</div></div><div><span>000000000000a2d0 t ch05_mono::largest</span></div></div><div><div><div>4</div></div><div><span>000000000000a3a0 t ch05_mono::largest</span></div></div><div><div><div>5</div></div><div><span>000000000000a430 t ch05_mono::largest</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三种类型，三份机器码，同一个名字（符号里编码的类型参数没有被 <code>-C</code> 显示出来，但地址是三个）。这和 C++ 模板实例化是一回事，也是 Rust 泛型”零成本”的来源：运行时没有任何泛型的痕迹。代价同样一样：二进制体积变大、编译变慢。</p><p>当约束多起来，签名会变得很挤，这时可以把约束挪到 <code>where</code> 子句里：<code>fn f&lt;T, U&gt;(t: T, u: U) where T: Display + Clone, U: Into&lt;T&gt; { ... }</code>。两种写法完全等价，<code>where</code> 只是排版工具；Pebble 里目前只有第 14 篇的 <code>pebble-net</code> 用到它，所以我不为它单独做实验。</p></section><section><h3>第二面墙：默认方法里的 <code>?Sized</code><a href="#第二面墙默认方法里的-sized"><span>#</span></a></h3><p>如果你把三个 <code>walk_*</code> 都按 <code>&lt;V: Visit&gt;</code> 写完（函数体见下面的小节，此刻先别管它们），再编译，会失败吗？</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_program</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>) { ... }</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_stmt</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>stmt</span><span>: &amp;</span><span>Stmt</span><span>) { ... }</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_expr</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) { ... }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它们自己能编译，问题出在 <code>Visit</code> 的默认方法里：<code>walk_program(self, program)</code> 把 <code>self</code> 传了出去。实验室里用最小的例子重现。新建 <code>src/bin/ch05_sized.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Expr</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>walk_expr</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> (</span></span><span>visitor</span><span>, </span><span>expr</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>trait</span><span> </span><span>Visit</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>walk_expr</span><span>(</span><span>self</span><span>, </span><span>expr</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>fn</span><span> </span><span>main</span><span>() {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo build --bin ch05_sized</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: the size for values of type `Self` cannot be known at compilation time</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch05_sized.rs:9:19</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>9 |         walk_expr(self, expr);</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|         --------- ^^^^ doesn't have a size known at compile-time</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|         |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|         required by a bound introduced by this call</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>note: required by an implicit `Sized` bound in `walk_expr`</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch05_sized.rs:3:14</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>3 | fn walk_expr&lt;V: Visit&gt;(visitor: &amp;mut V, expr: &amp;Expr) {</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|              ^ required by the implicit `Sized` requirement on this type parameter in `walk_expr`</span></div></div><div><div><div>14</div></div><div><span>help: consider further restricting `Self`</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>16</div></div><div><span>8 |     fn visit_expr(&amp;mut self, expr: &amp;Expr) where Self: Sized {</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|                                           +++++++++++++++++</span></div></div><div><div><div>18</div></div><div><span>help: consider relaxing the implicit `Sized` restriction</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>20</div></div><div><span>3 | fn walk_expr&lt;V: Visit + ?Sized&gt;(visitor: &amp;mut V, expr: &amp;Expr) {</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>|                       ++++++++</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>24</div></div><div><span>error: could not compile `rust-lab` (bin "ch05_sized") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>真实的 <code>pebble-ast</code> 里，同样的操作（把三个签名里的 <code>+ ?Sized</code> 去掉）得到三个一模一样的错误，每个默认方法一个。这个改动存成了补丁 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch05-sized.patch" target="_blank"><code>lab/exercises/ch05-sized.patch</code></a>，对 tag 应用后 <code>cargo build -p pebble-ast</code> 就能复现。</p><p>怎么理解？Rust 里<strong>泛型参数默认带一个隐含约束 <code>Sized</code></strong>：类型大小必须在编译期已知。绝大多数时候这正是你想要的。但 trait 本身（<code>Self</code>）不隐含 <code>Sized</code>，因为 Rust 想让 <code>dyn Visit</code> 这种”动态大小类型”（编译期不知道具体是谁）也能成为 <code>Self</code>。于是默认方法里的 <code>Self</code> 不一定有大小，不能传给要求 <code>V: Sized</code> 的 <code>walk_expr</code>。</p><p>C++ 里没有对应物，因为 C++ 的所有类型都是”定长”的：虚基类的对象你只能通过指针或引用持有，对象本身不会出现在模板参数里被按值传递，所以不会碰到这个问题。Rust 把这条界线显式化了。（第 3 篇你在 <code>impl&lt;T: Display + ?Sized&gt; ToString for T</code> 里已经见过这个 <code>?Sized</code>，它读作”<code>T</code> <strong>可能</strong>没有固定大小”。那里它是为了让 <code>str</code> 也能 <code>to_string()</code>。）</p><p>解决办法就是编译器提示的第二条：放宽约束。三个函数的签名全部改成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_program</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span> + ?</span><span>Sized</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>program</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>?Sized</code> 是 Rust 里仅有的”放宽”约束的写法（别的约束都是在加条件）。放宽之后，<code>V</code> 可以是 <code>AstPrinter</code> 这样的具体类型，也可以是 <code>dyn Visit</code>。第二种用法在实验室验证一下，新建 <code>src/bin/ch05_sized_ok.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Expr</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>walk_expr</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span> + ?</span><span>Sized</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>expr</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>trait</span><span> </span><span>Visit</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>_expr</span><span>: &amp;</span><span>Expr</span><span>) {}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>struct</span><span> </span><span>Counter</span><span>(</span><span>usize</span><span>);</span></div></div><div><div><div>12</div></div><div><span>impl</span><span> </span><span>Visit</span><span> </span><span>for</span><span> </span><span>Counter</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>_expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span>.</span><span>0</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>counter</span><span><span> </span><span>=</span><span> </span></span><span>Counter</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>20</div></div><div><span>    </span><span>walk_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>counter</span><span>, &amp;</span><span>Expr</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"static dispatch: {}"</span><span>, </span><span>counter</span><span>.</span><span>0</span><span>);</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>dynamic</span><span>: &amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Visit</span><span><span> </span><span>=</span><span> &amp;</span></span><span>mut</span><span> </span><span>counter</span><span>;</span></div></div><div><div><div>24</div></div><div><span>    </span><span>walk_expr</span><span>(</span><span>dynamic</span><span>, &amp;</span><span>Expr</span><span>);</span></div></div><div><div><div>25</div></div><div><span>    </span><span>println!</span><span>(</span><span>"dynamic dispatch: {}"</span><span>, </span><span>counter</span><span>.</span><span>0</span><span>);</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>static dispatch: 1</span></div></div><div><div><div>2</div></div><div><span>dynamic dispatch: 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个 <code>walk_expr</code>，第一次 <code>V = Counter</code>（静态分派，单态化出一份专门的代码），第二次 <code>V = dyn Visit</code>（动态分派，通过虚表调用 <code>visit_expr</code>）。<code>&amp;mut dyn Visit</code> 就是 C++ 里 <code>Visitor&amp;</code> 指向一个虚基类。你现在可以把它当作”带虚表的引用”；<code>dyn Trait</code> 的内部结构（胖指针、虚表布局、对象安全）留给 <a href="/posts/rust-by-pebble-08-rc-refcell-values/">第 8 篇</a>，那里的 <code>&amp;mut dyn Output</code> 会用到。</p></section><section><h3>把三个 <code>walk_*</code> 写完<a href="#把三个-walk_-写完"><span>#</span></a></h3><p>遍历的内容就是”每个变体的孩子在哪”。这是编译器逼你想清楚的部分：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Recursively visits every statement of a program.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_program</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span> + ?</span><span>Sized</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>program</span><span>: &amp;</span><span>Program</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>program</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/// Recursively visits a statement and its children.</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_stmt</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span> + ?</span><span>Sized</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>stmt</span><span>: &amp;</span><span>Stmt</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>match</span><span> </span><span>stmt</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>Let</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>value</span><span>),</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>Fn</span><span> { </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>body</span><span> {</span></div></div><div><div><div>14</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>Expr</span><span> { </span><span>expr</span><span>, .. } =&gt; </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>expr</span><span>),</span></div></div><div><div><div>18</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>If</span><span> {</span></div></div><div><div><div>19</div></div><div><span>            </span><span>cond</span><span>,</span></div></div><div><div><div>20</div></div><div><span>            </span><span>then_branch</span><span>,</span></div></div><div><div><div>21</div></div><div><span>            </span><span>else_branch</span><span>,</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>..</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>} =&gt; {</span></div></div><div><div><div>24</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>cond</span><span>);</span></div></div><div><div><div>25</div></div><div><span>            </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>then_branch</span><span> {</span></div></div><div><div><div>26</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>            </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>else_branch</span><span> {</span></div></div><div><div><div>29</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>While</span><span> { </span><span>cond</span><span>, </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>33</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>cond</span><span>);</span></div></div><div><div><div>34</div></div><div><span>            </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>body</span><span> {</span></div></div><div><div><div>35</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>36</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>For</span><span> { </span><span>iter</span><span>, </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>39</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>iter</span><span>);</span></div></div><div><div><div>40</div></div><div><span>            </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>body</span><span> {</span></div></div><div><div><div>41</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>Return</span><span> { </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>45</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>value</span><span><span>) </span><span>=</span><span> </span></span><span>value</span><span> {</span></div></div><div><div><div>46</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span>        </span><span>Stmt</span><span>::</span><span>Break</span><span><span> { .. } </span><span>|</span><span> </span></span><span>Stmt</span><span>::</span><span>Continue</span><span> { .. } =&gt; {}</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意两处细节：<code>Stmt::Let</code> 只访问 <code>value</code>，不访问 <code>name</code>：<code>Ident</code> 不是表达式，也不是语句，这个访问者接口里没有它的位置（如果以后要处理变量声明，在覆盖的 <code>visit_stmt</code> 里读 <code>name</code> 即可）。<code>Break</code> 和 <code>Continue</code> 没有孩子，分支体是空块。</p><p><code>walk_expr</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Recursively visits an expression and its children.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>walk_expr</span><span>&lt;</span><span>V</span><span>: </span><span>Visit</span><span> + ?</span><span>Sized</span><span>&gt;(</span><span>visitor</span><span>: &amp;</span><span>mut</span><span> </span><span>V</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>match</span><span> </span><span>expr</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Nil</span><span> { .. }</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Bool</span><span> { .. }</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Int</span><span> { .. }</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Float</span><span> { .. }</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Str</span><span> { .. }</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>|</span><span> </span><span>Expr</span><span>::</span><span>Var</span><span> { .. } =&gt; {}</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Expr</span><span>::</span><span>List</span><span> { </span><span>items</span><span>, .. } =&gt; {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>items</span><span> {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>item</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Map</span><span> { </span><span>entries</span><span>, .. } =&gt; {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>for</span><span> (</span><span>key</span><span>, </span><span>value</span><span>) </span><span>in</span><span> </span><span>entries</span><span> {</span></div></div><div><div><div>17</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>key</span><span>);</span></div></div><div><div><div>18</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Unary</span><span> { </span><span>operand</span><span>, .. } =&gt; </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>operand</span><span>),</span></div></div><div><div><div>22</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Binary</span><span> { </span><span>lhs</span><span>, </span><span>rhs</span><span><span>, .. } </span><span>|</span><span> </span></span><span>Expr</span><span>::</span><span>Logical</span><span> { </span><span>lhs</span><span>, </span><span>rhs</span><span>, .. } =&gt; {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>lhs</span><span>);</span></div></div><div><div><div>24</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>rhs</span><span>);</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Assign</span><span> { </span><span>target</span><span>, </span><span>value</span><span>, .. } =&gt; {</span></div></div><div><div><div>27</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>target</span><span>);</span></div></div><div><div><div>28</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>value</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Call</span><span> { </span><span>callee</span><span>, </span><span>args</span><span>, .. } =&gt; {</span></div></div><div><div><div>31</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>callee</span><span>);</span></div></div><div><div><div>32</div></div><div><span>            </span><span>for</span><span> </span><span>arg</span><span> </span><span>in</span><span> </span><span>args</span><span> {</span></div></div><div><div><div>33</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>arg</span><span>);</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Index</span><span> { </span><span>object</span><span>, </span><span>index</span><span>, .. } =&gt; {</span></div></div><div><div><div>37</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>object</span><span>);</span></div></div><div><div><div>38</div></div><div><span>            </span><span>visitor</span><span>.</span><span>visit_expr</span><span>(</span><span>index</span><span>);</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>        </span><span>Expr</span><span>::</span><span>Fn</span><span> { </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>41</div></div><div><span>            </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>body</span><span> {</span></div></div><div><div><div>42</div></div><div><span>                </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>43</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个值得细看的 Rust 点：</p><ul>
<li><strong>或模式里绑定同一个名字</strong>：<code>Expr::Binary { lhs, rhs, .. } | Expr::Logical { lhs, rhs, .. }</code>。两个变体都有 <code>lhs</code> 和 <code>rhs</code>，类型都是 <code>Box&lt;Expr&gt;</code>，所以一个分支体处理两者。</li>
<li><strong><code>visitor.visit_expr(operand)</code>：<code>operand</code> 是 <code>&amp;Box&lt;Expr&gt;</code>，参数要的是 <code>&amp;Expr</code>。</strong> 这能编译是因为<strong>解引用强制转换</strong>（deref coercion）：<code>Box&lt;T&gt;</code> 实现了 <code>Deref&lt;Target = T&gt;</code>，编译器在需要 <code>&amp;Expr</code> 的地方看到 <code>&amp;Box&lt;Expr&gt;</code>，自动插入 <code>&amp;**operand</code>。编译器替你写了 <code>&amp;**operand</code>；C++ 里这一步要自己写 <code>*operand</code>。第 7 篇的 <code>&amp;PathBuf</code> 变 <code>&amp;Path</code> 是同一个机制。</li>
</ul></section></section>
<section><h2><code>for stmt in program</code>：<code>IntoIterator</code> 和 <code>for</code> 的真面目<a href="#for-stmt-in-programintoiterator-和-for-的真面目"><span>#</span></a></h2><p>回头看 <code>walk_program</code> 里那句 <code>for stmt in program</code>。<code>program</code> 的类型是 <code>&amp;Program</code>，不是 <code>Vec</code>。能这样写，是因为我们给 <code>&amp;Program</code> 实现了 <code>IntoIterator</code>。把这段加在 <code>impl Program</code> 之后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span>&lt;'</span><span>a</span><span>&gt; </span><span>IntoIterator</span><span> </span><span>for</span><span> &amp;'</span><span>a</span><span> </span><span>Program</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Item</span><span><span> </span><span>=</span><span> &amp;'</span></span><span>a</span><span> </span><span>Stmt</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>type</span><span> </span><span>IntoIter</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>slice</span><span>::</span></span><span>Iter</span><span>&lt;'</span><span>a</span><span>, </span><span>Stmt</span><span>&gt;;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>into_iter</span><span>(</span><span>self</span><span>) -&gt; </span><span>Self</span><span>::</span><span>IntoIter</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span>.stmts.</span><span>iter</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先回忆第 4 篇的 <code>Iterator</code>：一个有关联类型 <code>Item</code> 和一个 <code>next</code> 方法的 trait。<code>IntoIterator</code> 是它的”入口”：<strong>任何可以变成迭代器的东西</strong>。它有两个关联类型（<code>Item</code> 是元素类型，<code>IntoIter</code> 是具体的迭代器类型）和一个 <code>into_iter(self)</code> 方法。</p><p><code>for x in expr { body }</code> 并不是语言内置的魔法，它是对下面这段的<strong>语法糖</strong>。为了让你看到，我写了一个独立的小程序 <code>src/bin/ch05_into_iter.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Program</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>stmts</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>impl</span><span>&lt;'</span><span>a</span><span>&gt; </span><span>IntoIterator</span><span> </span><span>for</span><span> &amp;'</span><span>a</span><span> </span><span>Program</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>type</span><span> </span><span>Item</span><span><span> </span><span>=</span><span> &amp;'</span></span><span>a</span><span> </span><span>String</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>type</span><span> </span><span>IntoIter</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>slice</span><span>::</span></span><span>Iter</span><span>&lt;'</span><span>a</span><span>, </span><span>String</span><span>&gt;;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>into_iter</span><span>(</span><span>self</span><span>) -&gt; </span><span>Self</span><span>::</span><span>IntoIter</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span>.stmts.</span><span>iter</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>Program</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>stmts</span><span>: </span><span>vec!</span><span>[</span><span>"let x = 1;"</span><span>.</span><span>to_string</span><span>(), </span><span>"print(x);"</span><span>.</span><span>to_string</span><span>()],</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> &amp;</span><span>program</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>println!</span><span>(</span><span>"for:      {stmt}"</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>// what the compiler rewrites that loop into</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>iter</span><span><span> </span><span>=</span><span> </span></span><span>IntoIterator</span><span>::</span><span>into_iter</span><span>(&amp;</span><span>program</span><span>);</span></div></div><div><div><div>25</div></div><div><span>    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>stmt</span><span><span>) </span><span>=</span><span> </span></span><span>iter</span><span>.</span><span>next</span><span>() {</span></div></div><div><div><div>26</div></div><div><span>        </span><span>println!</span><span>(</span><span>"desugared: {stmt}"</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>for:      let x = 1;</span></div></div><div><div><div>2</div></div><div><span>for:      print(x);</span></div></div><div><div><div>3</div></div><div><span>desugared: let x = 1;</span></div></div><div><div><div>4</div></div><div><span>desugared: print(x);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++ 的 range-for 也是语法糖（展开成 <code>begin()</code>/<code>end()</code> 与迭代器），区别在于 Rust 的入口是 trait，所以你可以为<strong>自己的类型</strong>以及它们的<strong>不同借用形式</strong>分别实现。标准库里 <code>Vec&lt;T&gt;</code> 就有三个实现：<code>for x in vec</code> 吃掉 <code>Vec</code>（<code>Item = T</code>），<code>for x in &amp;vec</code> 借出（<code>Item = &amp;T</code>），<code>for x in &amp;mut vec</code> 可变借出（<code>Item = &amp;mut T</code>）。</p><p>为什么我们为 <code>&amp;Program</code> 而不是 <code>Program</code> 实现？实验：把 <code>main</code> 里的 <code>for stmt in &amp;program</code> 改成 <code>for stmt in program</code>（文件另存为 <code>src/bin/ch05_into_iter_fail.rs</code>，<code>program</code> 里 <code>stmts</code> 留空）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `Program` is not an iterator</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch05_into_iter_fail.rs:16:17</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>16 |     for stmt in program {</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                 ^^^^^^^ the trait `IntoIterator` is not implemented for `Program`</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= note: the trait bound `Program: IntoIterator` is not satisfied</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: required for `Program` to implement `IntoIterator`</span></div></div><div><div><div>9</div></div><div><span>help: consider borrowing here</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>16 |     for stmt in &amp;program {</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|                 +</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Program</code> 值本身没实现，只有 <code>&amp;Program</code> 实现了。这正是我们要的：AST 遍历全是借用，没有任何人需要”吃掉”一棵树。如果将来需要，再加一个 <code>impl IntoIterator for Program</code> 即可，它们互不冲突。</p><p><code>impl&lt;'a&gt;</code> 里的 <code>'a</code> 是第 4 篇的生命周期：返回的迭代器和元素借用自 <code>&amp;'a Program</code>，所以它们不能比被借用的 <code>Program</code> 活得久。</p></section>
<section><h2><code>AstPrinter</code>：第一个访问者<a href="#astprinter第一个访问者"><span>#</span></a></h2><p>一个能真正跑起来的访问者，同时给我们一个肉眼可见的测试。打印器把树按缩进打印出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Pretty-prints a program as an indented tree.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// This is one concrete [`Visit`] implementation; the interpreter is another.</span></div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Default</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>AstPrinter</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>out</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>indent</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>impl</span><span> </span><span>AstPrinter</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// A fresh printer.</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>Self</span><span>::</span><span>default</span><span>()</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>/// Consumes the printer, returning the rendered tree.</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>finish</span><span>(</span><span>self</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span>.out</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>fn</span><span> </span><span>line</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>text</span><span>: </span><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span>) {</span></div></div><div><div><div>24</div></div><div><span>        </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>self</span><span>.indent {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>self</span><span>.out.</span><span>push_str</span><span>(</span><span>"  "</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>        </span><span>self</span><span>.out.</span><span>push_str</span><span>(&amp;</span><span>text</span><span>.</span><span>to_string</span><span>());</span></div></div><div><div><div>28</div></div><div><span>        </span><span>self</span><span>.out.</span><span>push</span><span>(</span><span>'</span><span>\n</span><span>'</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三点：</p><ul>
<li>
<p><code>#[derive(Default)]</code> 给出”全零值”：<code>String::new()</code> 和 <code>0</code>。第 2 篇的 <code>SourceMap</code> 也这样。</p>
</li>
<li>
<p><code>finish(self)</code> 按值接收 <code>self</code>：打印器被<strong>消耗</strong>，调用者拿到里面的 <code>String</code>，不用拷贝。这是 C++ 里 <code>std::move(out_)</code> 再返回的写法，只是 Rust 的移动由编译器保证之后没人能再碰旧值。</p>
</li>
<li>
<p><code>fn line(&amp;mut self, text: impl fmt::Display)</code>：参数位置的 <code>impl Trait</code>。它等价于 <code>fn line&lt;T: fmt::Display&gt;(&amp;mut self, text: T)</code>，一个<strong>匿名</strong>的泛型参数。我在实验里并排写了两种形式（<code>src/bin/ch05_impl_trait.rs</code>），输出一致：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>shout_generic</span><span>&lt;</span><span>T</span><span>: </span><span>Display</span><span>&gt;(</span><span>value</span><span>: </span><span>T</span><span>) -&gt; </span><span>String</span><span> { </span><span>format!</span><span>(</span><span>"{value}!"</span><span>) }</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>shout_impl</span><span>(</span><span>value</span><span>: </span><span>impl</span><span> </span><span>Display</span><span>) -&gt; </span><span>String</span><span> { </span><span>format!</span><span>(</span><span>"{value}!"</span><span>) }</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>42!</span></div></div><div><div><div>2</div></div><div><span>pebble!</span></div></div><div><div><div>3</div></div><div><span>1.5!</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>最后一行来自 <code>shout_generic::&lt;f64&gt;(1.5)</code>，用到了<strong>涡轮鱼</strong>语法 <code>::&lt;T&gt;</code> 显式指定类型参数（第 4 篇的 <code>parse::&lt;i64&gt;</code> 同款）。<code>impl Trait</code> 的好处是不用起名字，缺点是调用者不方便手动指定类型，所以它适合”我只关心它能 Display”的小函数。</p>
</li>
</ul><p>现在实现 <code>Visit</code>，只覆盖两个方法：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Visit</span><span> </span><span>for</span><span> </span><span>AstPrinter</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_stmt</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>stmt</span><span>: &amp;</span><span>Stmt</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>label</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>stmt</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Let</span><span> { </span><span>name</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"let {name}"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Fn</span><span> { </span><span>name</span><span>, </span><span>params</span><span>, .. } =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>format!</span><span>(</span><span>"fn {name}({})"</span><span>, </span><span>join_params</span><span>(</span><span>params</span><span>))</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Expr</span><span> { .. } =&gt; </span><span>"expr"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>If</span><span> { .. } =&gt; </span><span>"if"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>While</span><span> { .. } =&gt; </span><span>"while"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>For</span><span> { </span><span>var</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"for {var}"</span><span>),</span></div></div><div><div><div>12</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Return</span><span> { .. } =&gt; </span><span>"return"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Break</span><span> { .. } =&gt; </span><span>"break"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Stmt</span><span>::</span><span>Continue</span><span> { .. } =&gt; </span><span>"continue"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.</span><span>line</span><span>(</span><span>label</span><span>);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span><span>.indent </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>18</div></div><div><span>        </span><span>walk_stmt</span><span>(</span><span>self</span><span>, </span><span>stmt</span><span>);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span><span><span>.indent </span><span>-=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这就是访问者的使用方式：先打印自己，缩进加一，<strong>调用 <code>walk_stmt</code> 让默认逻辑去走孩子</strong>（孩子又回到我们覆盖的 <code>visit_stmt</code>/<code>visit_expr</code>），再缩进减一。<code>walk_stmt(self, stmt)</code> 在这里 <code>self</code> 是 <code>&amp;mut AstPrinter</code>，是一个有大小的具体类型，没有 <code>?Sized</code> 的问题。</p><p><code>visit_expr</code> 同理，标签的内容是我们对每个节点想看到的信息：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>label</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>expr</span><span> {</span></div></div><div><div><div>3</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Nil</span><span> { .. } =&gt; </span><span>"nil"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>4</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Bool</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"bool {value}"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Int</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"int {value}"</span><span>),</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Float</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"float {value}"</span><span>),</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Str</span><span> { </span><span>value</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"str {value:?}"</span><span>),</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Var</span><span> { </span><span>name</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"var {name}"</span><span>),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Expr</span><span>::</span><span>List</span><span> { </span><span>items</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"list[{}]"</span><span>, </span><span>items</span><span>.</span><span>len</span><span>()),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Map</span><span> { </span><span>entries</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"map{{{} entries}}"</span><span>, </span><span>entries</span><span>.</span><span>len</span><span>()),</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Unary</span><span> { </span><span>op</span><span>, .. } =&gt; {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>format!</span><span>(</span><span>"unary {}"</span><span>, </span><span>if</span><span> *</span><span>op</span><span><span> </span><span>==</span><span> </span></span><span>UnaryOp</span><span>::</span><span>Neg</span><span> { </span><span>"-"</span><span> } </span><span>else</span><span> { </span><span>"not"</span><span> })</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Binary</span><span> { </span><span>op</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"binary {op}"</span><span>),</span></div></div><div><div><div>15</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Logical</span><span> { </span><span>op</span><span>, .. } =&gt; {</span></div></div><div><div><div>16</div></div><div><span>                </span><span>format!</span><span>(</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>"logical {}"</span><span>,</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>if</span><span> *</span><span>op</span><span><span> </span><span>==</span><span> </span></span><span>LogicalOp</span><span>::</span><span>And</span><span> { </span><span>"and"</span><span> } </span><span>else</span><span> { </span><span>"or"</span><span> }</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Assign</span><span> { .. } =&gt; </span><span>"assign"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>22</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Call</span><span> { </span><span>args</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"call({} args)"</span><span>, </span><span>args</span><span>.</span><span>len</span><span>()),</span></div></div><div><div><div>23</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Index</span><span> { .. } =&gt; </span><span>"index"</span><span>.</span><span>to_string</span><span>(),</span></div></div><div><div><div>24</div></div><div><span>            </span><span>Expr</span><span>::</span><span>Fn</span><span> { </span><span>params</span><span>, .. } =&gt; </span><span>format!</span><span>(</span><span>"fn({})"</span><span>, </span><span>join_params</span><span>(</span><span>params</span><span>)),</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>26</div></div><div><span>        </span><span>self</span><span>.</span><span>line</span><span>(</span><span>label</span><span>);</span></div></div><div><div><div>27</div></div><div><span>        </span><span>self</span><span><span>.indent </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>28</div></div><div><span>        </span><span>walk_expr</span><span>(</span><span>self</span><span>, </span><span>expr</span><span>);</span></div></div><div><div><div>29</div></div><div><span>        </span><span>self</span><span><span>.indent </span><span>-=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>{value:?}</code> 用 <code>Debug</code> 格式，字符串会带上引号并转义；<code>map{{{} entries}}</code> 里 <code>{{</code> 和 <code>}}</code> 是格式字符串里的字面花括号。<code>*op == UnaryOp::Neg</code>：<code>op</code> 是 <code>&amp;UnaryOp</code>，先解引用再比较；这样写要求 <code>UnaryOp: PartialEq</code>，我们 derive 了。</p><p>还差一个小辅助和一个入口函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>join_params</span><span>(</span><span>params</span><span>: &amp;[</span><span>Ident</span><span>]) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>params</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>param</span><span><span>|</span><span> </span></span><span>param</span><span>.name.</span><span>clone</span><span>())</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>.</span><span>collect</span><span>::&lt;</span><span>Vec</span><span>&lt;</span><span>_</span><span>&gt;&gt;()</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>.</span><span>join</span><span>(</span><span>", "</span><span>)</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>/// Convenience wrapper around [`AstPrinter`].</span></div></div><div><div><div>10</div></div><div><span>#[must_use]</span></div></div><div><div><div>11</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>pretty</span><span>(</span><span>program</span><span>: &amp;</span><span>Program</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>printer</span><span><span> </span><span>=</span><span> </span></span><span>AstPrinter</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>13</div></div><div><span>    </span><span>printer</span><span>.</span><span>visit_program</span><span>(</span><span>program</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>printer</span><span>.</span><span>finish</span><span>()</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>join_params</code> 接收 <code>&amp;[Ident]</code>（切片），而调用处传的是 <code>&amp;Vec&lt;Ident&gt;</code>，又是一次解引用强制转换（<code>Vec&lt;T&gt;</code> 解引用成 <code>[T]</code>）。<code>.map(|param| ...)</code> 是第 2 篇的闭包，<code>.collect::&lt;Vec&lt;_&gt;&gt;()</code> 里的 <code>_</code> 让编译器自己推元素类型。<code>pretty</code> 里 <code>printer.visit_program(program)</code> 没有覆盖 <code>visit_program</code>，用的就是 trait 的默认实现，它会调用 <code>walk_program</code>，也就是我们前面写的，里面用 <code>for stmt in program</code>（<code>IntoIterator</code>）逐个交给 <code>visit_stmt</code>，正好把前面所有零件串起来。</p></section>
<section><h2>测试<a href="#测试"><span>#</span></a></h2><p>最后是测试，放在文件末尾。四个测试，两个看 <code>Spanned</code>，一个看打印，一个看默认访问者：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>dummy</span><span>() -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>Span</span><span>::</span><span>DUMMY</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>derive_spanned_reads_the_span_field</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>ident</span><span><span> </span><span>=</span><span> </span></span><span>Ident</span><span>::</span><span>new</span><span>(</span><span>"x"</span><span><span>, </span><span>Span</span><span>::</span></span><span>new</span><span><span>(</span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>(</span><span>3</span><span>), </span><span>5</span><span>, </span><span>6</span><span>));</span></div></div><div><div><div>12</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>ident</span><span>.</span><span>span</span><span>(), </span><span>Span</span><span>::</span><span>new</span><span><span>(</span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>(</span><span>3</span><span>), </span><span>5</span><span>, </span><span>6</span><span>));</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>fn</span><span> </span><span>derive_spanned_works_on_enum_variants</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>let</span><span> </span><span>expr</span><span><span> </span><span>=</span><span> </span></span><span>Expr</span><span>::</span><span>Int</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>value</span><span>: </span><span>7</span><span>,</span></div></div><div><div><div>19</div></div><div><span>            </span><span>span</span><span>: </span><span>Span</span><span>::</span><span>new</span><span><span>(</span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>10</span><span>, </span><span>11</span><span>),</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>21</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>expr</span><span>.</span><span>span</span><span>(), </span><span>Span</span><span>::</span><span>new</span><span><span>(</span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>10</span><span>, </span><span>11</span><span>));</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>        </span><span>let</span><span> </span><span>stmt</span><span><span> </span><span>=</span><span> </span></span><span>Stmt</span><span>::</span><span>Break</span><span> { </span><span>span</span><span>: </span><span>dummy</span><span>() };</span></div></div><div><div><div>24</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>stmt</span><span>.</span><span>span</span><span>(), </span><span>dummy</span><span>());</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>测试名里有 <code>derive_</code>，这是仓库的名字：下一篇我们删掉手写 <code>impl</code>、换成派生宏之后，这两个测试一字不改继续通过，它们验证的是行为，不是实现。这就是测试存在的理由。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>printer_walks_the_whole_tree</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>Program</span><span>::</span><span>new</span><span>(</span><span>vec!</span><span><span>[</span><span>Stmt</span><span>::</span></span><span>Fn</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>name</span><span><span>: </span><span>Ident</span><span>::</span></span><span>new</span><span>(</span><span>"add"</span><span>, </span><span>dummy</span><span>()),</span></div></div><div><div><div>5</div></div><div><span>            </span><span>params</span><span>: </span><span>vec!</span><span><span>[</span><span>Ident</span><span>::</span></span><span>new</span><span>(</span><span>"a"</span><span>, </span><span>dummy</span><span><span>()), </span><span>Ident</span><span>::</span></span><span>new</span><span>(</span><span>"b"</span><span>, </span><span>dummy</span><span>())],</span></div></div><div><div><div>6</div></div><div><span>            </span><span>body</span><span>: </span><span>vec!</span><span><span>[</span><span>Stmt</span><span>::</span></span><span>Return</span><span> {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>value</span><span>: </span><span>Some</span><span><span>(</span><span>Expr</span><span>::</span></span><span>Binary</span><span> {</span></div></div><div><div><div>8</div></div><div><span>                    </span><span>op</span><span><span>: </span><span>BinaryOp</span><span>::</span></span><span>Add</span><span>,</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>lhs</span><span><span>: </span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Expr</span><span>::</span></span><span>Var</span><span> {</span></div></div><div><div><div>10</div></div><div><span>                        </span><span>name</span><span><span>: </span><span>Ident</span><span>::</span></span><span>new</span><span>(</span><span>"a"</span><span>, </span><span>dummy</span><span>()),</span></div></div><div><div><div>11</div></div><div><span>                        </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>12</div></div><div><span><span>                    </span></span><span>}),</span></div></div><div><div><div>13</div></div><div><span>                    </span><span>rhs</span><span><span>: </span><span>Box</span><span>::</span></span><span>new</span><span><span>(</span><span>Expr</span><span>::</span></span><span>Var</span><span> {</span></div></div><div><div><div>14</div></div><div><span>                        </span><span>name</span><span><span>: </span><span>Ident</span><span>::</span></span><span>new</span><span>(</span><span>"b"</span><span>, </span><span>dummy</span><span>()),</span></div></div><div><div><div>15</div></div><div><span>                        </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>16</div></div><div><span><span>                    </span></span><span>}),</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>}),</span></div></div><div><div><div>19</div></div><div><span>                </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>}],</span></div></div><div><div><div>21</div></div><div><span>            </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}]);</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>        </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>pretty</span><span>(&amp;</span><span>program</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>expected</span><span><span> </span><span>=</span><span> </span></span><span>"\</span></div></div><div><div><div>26</div></div><div><span>fn add(a, b)</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>return</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>binary +</span></div></div><div><div><div>29</div></div><div><span><span>      </span></span><span>var a</span></div></div><div><div><div>30</div></div><div><span><span>      </span></span><span>var b</span></div></div><div><div><div>31</div></div><div><span>"</span><span>;</span></div></div><div><div><div>32</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>rendered</span><span>, </span><span>expected</span><span>);</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>那个 <code>"\</code> 开头的字符串：行尾的反斜杠吞掉紧随其后的换行，让 <code>fn add(a, b)</code> 紧贴开头的引号，这样期望值的每一行从第一列开始，读起来就是输出本身。我在一个独立的小程序里把同一棵树直接 <code>print!("{}", pretty(&amp;program))</code>，得到的就是上面的五行（仓库里是 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch05-ast/src/bin/ch05_pretty.rs" target="_blank"><code>lab/lab-ch05-ast/src/bin/ch05_pretty.rs</code></a>）。（那个”手搓一棵树”的程序之所以这么啰嗦，是因为解析器要到第 7 篇才有。）</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>default_visit_reaches_every_expression</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>struct</span><span> </span><span>Counter</span><span>(</span><span>usize</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>impl</span><span> </span><span>Visit</span><span> </span><span>for</span><span> </span><span>Counter</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>fn</span><span> </span><span>visit_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>self</span><span>.</span><span>0</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>7</div></div><div><span>                </span><span>walk_expr</span><span>(</span><span>self</span><span>, </span><span>expr</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>        </span><span>let</span><span> </span><span>program</span><span><span> </span><span>=</span><span> </span></span><span>Program</span><span>::</span><span>new</span><span>(</span><span>vec!</span><span><span>[</span><span>Stmt</span><span>::</span></span><span>Let</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>name</span><span><span>: </span><span>Ident</span><span>::</span></span><span>new</span><span>(</span><span>"x"</span><span>, </span><span>dummy</span><span>()),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>value</span><span><span>: </span><span>Expr</span><span>::</span></span><span>List</span><span> {</span></div></div><div><div><div>14</div></div><div><span>                </span><span>items</span><span>: </span><span>vec!</span><span>[</span></div></div><div><div><div>15</div></div><div><span><span>                    </span></span><span>Expr</span><span>::</span><span>Int</span><span> {</span></div></div><div><div><div>16</div></div><div><span>                        </span><span>value</span><span>: </span><span>1</span><span>,</span></div></div><div><div><div>17</div></div><div><span>                        </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>18</div></div><div><span><span>                    </span></span><span>},</span></div></div><div><div><div>19</div></div><div><span><span>                    </span></span><span>Expr</span><span>::</span><span>Int</span><span> {</span></div></div><div><div><div>20</div></div><div><span>                        </span><span>value</span><span>: </span><span>2</span><span>,</span></div></div><div><div><div>21</div></div><div><span>                        </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>22</div></div><div><span><span>                    </span></span><span>},</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>],</span></div></div><div><div><div>24</div></div><div><span>                </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>26</div></div><div><span>            </span><span>span</span><span>: </span><span>dummy</span><span>(),</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>}]);</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>counter</span><span><span> </span><span>=</span><span> </span></span><span>Counter</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>counter</span><span>.</span><span>visit_program</span><span>(&amp;</span><span>program</span><span>);</span></div></div><div><div><div>31</div></div><div><span>        </span><span>// list, 1, 2</span></div></div><div><div><div>32</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>counter</span><span>.</span><span>0</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>最后一个测试在<strong>函数里定义了一个类型和它的 trait 实现</strong>：Rust 允许在任何块里声明 <code>struct</code> 和 <code>impl</code>，它们只在这个块内可见，不污染外面。它只覆盖 <code>visit_expr</code> 一个方法，数一下表达式节点个数，就足以证明”默认遍历到达每个节点”。</p></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在工作区根目录运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-ast</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的输出（真实运行，Rust 1.95.0）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 4 tests</span></div></div><div><div><div>2</div></div><div><span>test tests::default_visit_reaches_every_expression ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::derive_spanned_reads_the_span_field ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::derive_spanned_works_on_enum_variants ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::printer_walks_the_whole_tree ... ok</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个工作区也应该是绿的（<code>cargo test --workspace</code>）：<code>pebble-span</code> 6 个、<code>pebble-diag</code> 4 个、<code>pebble-lexer</code> 12 个、<code>pebble-ast</code> 4 个测试。顺手跑一遍 <code>cargo clippy --workspace --all-targets -- -D warnings</code> 和 <code>cargo fmt --all --check</code>，两个都应该没有输出。</p><p><strong>已经等于仓库（tag <code>tutorial-v1</code>）的文件：</strong></p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/src/lib.rs" target="_blank"><code>crates/pebble-span/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-diag/src/lib.rs" target="_blank"><code>crates/pebble-diag/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-lexer/src/lib.rs" target="_blank"><code>crates/pebble-lexer/src/lib.rs</code></a></li>
</ul><p><strong>中间状态的文件（第 6 篇会改成仓库版本）：</strong></p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-ast/src/lib.rs" target="_blank"><code>crates/pebble-ast/src/lib.rs</code></a>：我用 <code>diff</code> 对照过，和仓库的区别只有这些：头部文档少了最后一句；少了 <code>pub use pebble_macros::Spanned;</code> 那一段；<code>Ident</code>、<code>Stmt</code>、<code>Expr</code> 上的 <code>#[derive(...)]</code> 里没有 <code>Spanned</code>；多了你手写的三个 <code>impl Spanned</code>。其余每一行，包括四个测试，逐字节相同。</li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-ast/Cargo.toml" target="_blank"><code>crates/pebble-ast/Cargo.toml</code></a>：少一行 <code>pebble-macros.workspace = true</code>。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇的所有实验文件都在仓库的 <code>lab/</code> 目录里（主实验室是 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）。如果你把 tag 克隆在 <code>~/pebble-tag</code>，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，下面的命令先 <code>cd ~/pebble-tag/lab/rust-lab</code>（或者把它复制到 <code>~/rust-lab</code>）再运行。</p><p>主实验室，每个文件都是 <code>src/bin/</code> 下的一个二进制：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_recursive.rs" target="_blank"><code>ch05_recursive.rs</code></a></td><td><code>cargo build --bin ch05_recursive</code></td><td>E0072</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_box.rs" target="_blank"><code>ch05_box.rs</code></a></td><td><code>cargo run --quiet --bin ch05_box</code></td><td>运行，第一行 <code>eval = 6</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_bound.rs" target="_blank"><code>ch05_bound.rs</code></a></td><td><code>cargo build --bin ch05_bound</code></td><td>E0369</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_mono.rs" target="_blank"><code>ch05_mono.rs</code></a></td><td><code>cargo run --quiet --bin ch05_mono</code></td><td>运行，第一行 <code>9</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_sized.rs" target="_blank"><code>ch05_sized.rs</code></a></td><td><code>cargo build --bin ch05_sized</code></td><td>E0277</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_sized_ok.rs" target="_blank"><code>ch05_sized_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch05_sized_ok</code></td><td>运行，第一行 <code>static dispatch: 1</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_into_iter.rs" target="_blank"><code>ch05_into_iter.rs</code></a></td><td><code>cargo run --quiet --bin ch05_into_iter</code></td><td>运行，第一行 <code>for:      let x = 1;</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_into_iter_fail.rs" target="_blank"><code>ch05_into_iter_fail.rs</code></a></td><td><code>cargo build --bin ch05_into_iter_fail</code></td><td>E0277</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch05_impl_trait.rs" target="_blank"><code>ch05_impl_trait.rs</code></a></td><td><code>cargo run --quiet --bin ch05_impl_trait</code></td><td>运行，第一行 <code>42!</code></td></tr></tbody></table><p>侧边 crate、C++ 程序和对 <code>pebble-ast</code> 的补丁。命令里写了 <code>cd</code> 的在侧边 crate 的目录里运行，其余在仓库根目录（<code>~/pebble-tag</code>）运行；补丁试完用 <code>git apply -R</code> 还原。侧边 crate 依赖 tag 里的最终版 <code>pebble-ast</code>：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch05-ast/src/bin/ch05_sizes.rs" target="_blank"><code>lab/lab-ch05-ast/src/bin/ch05_sizes.rs</code></a></td><td><code>cd lab/lab-ch05-ast &amp;&amp; cargo run --quiet --bin ch05_sizes</code></td><td>运行，第一行 <code>Span         = 12 bytes</code>，<code>Stmt</code> 144、<code>Expr</code> 64、<code>Box&lt;Expr&gt;</code> 8、<code>Vec&lt;Expr&gt;</code> 24 字节</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch05-ast/src/bin/ch05_pretty.rs" target="_blank"><code>lab/lab-ch05-ast/src/bin/ch05_pretty.rs</code></a></td><td><code>cd lab/lab-ch05-ast &amp;&amp; cargo run --quiet --bin ch05_pretty</code></td><td>运行，五行树，第一行 <code>fn add(a, b)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch05-ast/src/bin/ch05_ex_counter.rs" target="_blank"><code>lab/lab-ch05-ast/src/bin/ch05_ex_counter.rs</code></a></td><td><code>cd lab/lab-ch05-ast &amp;&amp; cargo run --quiet --bin ch05_ex_counter</code></td><td>运行，<code>lets = 1, reads = 2</code>，然后 <code>through dyn Visit: lets = 2, reads = 4</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch05_visitor.cpp" target="_blank"><code>lab/cpp/ch05_visitor.cpp</code></a></td><td><code>g++ -std=c++20 -o /tmp/visitor lab/cpp/ch05_visitor.cpp &amp;&amp; /tmp/visitor</code></td><td>运行，输出 <code>add</code>、<code>int 1</code>、<code>int 2</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch05_largest.cpp" target="_blank"><code>lab/cpp/ch05_largest.cpp</code></a></td><td><code>g++ -std=c++20 -c lab/cpp/ch05_largest.cpp</code></td><td>编译失败：<code>no match for 'operator&gt;' (operand types are 'Pebble' and 'Pebble')</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch05-sized.patch" target="_blank"><code>lab/exercises/ch05-sized.patch</code></a></td><td><code>git apply lab/exercises/ch05-sized.patch &amp;&amp; cargo build -p pebble-ast</code></td><td>编译失败：三个 E0277（<code>the size for values of type Self cannot be known at compilation time</code>）</td></tr></tbody></table></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 给 AST 加一种语句。</strong> 按上文的实验，在 <code>Stmt</code> 里加一个 <code>Block { body: Vec&lt;Stmt&gt;, span: Span }</code> 变体，让 <code>cargo test -p pebble-ast</code> 重新变绿。具体要改哪几处？打印器里 <code>Block</code> 该显示成什么？</p>
参考答案<p>编译器会列出三处 <code>E0004</code>，每处补一个分支：</p><ol>
<li><code>impl Spanned for Stmt</code>：在或模式里加一个分支 <code>| Stmt::Block { span, .. }</code>，让 <code>=&gt; *span,</code> 跟在这个新分支后面。</li>
<li><code>walk_stmt</code>：加
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Stmt</span><span>::</span><span>Block</span><span> { </span><span>body</span><span>, .. } =&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>for</span><span> </span><span>stmt</span><span> </span><span>in</span><span> </span><span>body</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>visitor</span><span>.</span><span>visit_stmt</span><span>(</span><span>stmt</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
</li>
<li><code>AstPrinter::visit_stmt</code>：加 <code>Stmt::Block { .. } =&gt; "block".to_string(),</code>。</li>
</ol><p>我在一份拷贝上按这三处改完，<code>cargo test -p pebble-ast</code> 依然 4 个测试全过。第 1 处是纯机械劳动，这正是下一篇要消灭的东西。</p><p><strong>2. 写一个访问者，数变量引用和 <code>let</code> 语句各有多少。</strong> 构造 <code>let y = x + x;</code> 这棵树，期望输出 <code>lets = 1, reads = 2</code>。再通过 <code>&amp;mut dyn Visit</code> 调用一遍，数字应该是多少？</p>
参考答案<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Default</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>VarCounter</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>reads</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>lets</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>Visit</span><span> </span><span>for</span><span> </span><span>VarCounter</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_stmt</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>stmt</span><span>: &amp;</span><span>Stmt</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> </span><span>matches!</span><span>(</span><span>stmt</span><span>, </span><span>Stmt</span><span>::</span><span>Let</span><span> { .. }) {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>self</span><span><span>.lets </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>walk_stmt</span><span>(</span><span>self</span><span>, </span><span>stmt</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>visit_expr</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>expr</span><span>: &amp;</span><span>Expr</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>if</span><span> </span><span>matches!</span><span>(</span><span>expr</span><span>, </span><span>Expr</span><span>::</span><span>Var</span><span> { .. }) {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>self</span><span><span>.reads </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>        </span><span>walk_expr</span><span>(</span><span>self</span><span>, </span><span>expr</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>matches!(value, pattern)</code> 是标准库宏，展开成一个返回 <code>bool</code> 的 <code>match</code>（第 7 篇的解析器里会大量使用）。我构造了那棵树，运行结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>lets = 1, reads = 2</span></div></div><div><div><div>2</div></div><div><span>through dyn Visit: lets = 2, reads = 4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二行是第二次遍历累加的结果（计数器没有被清零），关键是 <code>let dynamic: &amp;mut dyn Visit = &amp;mut counter; dynamic.visit_program(&amp;program);</code> 能编译，这就是 <code>?Sized</code> 放开的那条路。完整程序是 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch05-ast/src/bin/ch05_ex_counter.rs" target="_blank"><code>lab/lab-ch05-ast/src/bin/ch05_ex_counter.rs</code></a>。</p><p><strong>3. 为什么 <code>Expr::List { items: Vec&lt;Expr&gt; }</code> 不需要 <code>Box</code>，而 <code>Expr::Unary { operand: Box&lt;Expr&gt; }</code> 需要？</strong></p>
参考答案<p>编译器要求每个类型有固定大小。<code>Vec&lt;T&gt;</code> 不管 <code>T</code> 是什么，自己永远是三个字（指针、容量、长度），共 24 字节，元素存在堆上。所以 <code>Expr</code> 包含一个 <code>Vec&lt;Expr&gt;</code> 时，大小里只算这 24 字节，没有无穷递归。<code>operand: Expr</code>（不装箱）则会让 <code>Expr</code> 直接包含 <code>Expr</code>，得到 E0072。实验 <code>ch05_box</code> 的输出就是证据：带 <code>List(Vec&lt;Expr&gt;)</code> 变体的 <code>Expr</code> 编译通过，<code>Vec&lt;Expr&gt;</code> 是 24 字节，<code>Box&lt;Expr&gt;</code> 是 8 字节。</p><p>附带一个推论：<code>Vec</code> 和 <code>Box</code> 在这里是等价的”间接层”，选哪个看你是要一个还是一串。</p><p>AST 现在立起来了，而且你也有了第一份值得怀疑的代码：三段 <code>| ... { span, .. }</code> 几乎一模一样的 <code>match</code>。下一篇我们走进 Rust 最容易被说成”黑魔法”的部分：写一个<strong>过程宏</strong>，让编译器在每次编译时替我们生成这些代码，删掉它们，并且让测试一个字不改继续通过。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-04-lifetimes-lexer/</id>
      <title type="text">生命周期：让 token 借用源码</title>
      <published>2026-10-13T03:00:00.000Z</published>
      <updated>2026-10-13T03:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-04-lifetimes-lexer/"/>
      <summary type="text">写 pebble-lexer：token 不复制源码而是借用它。从 C++ string_view 的悬垂说起，亲手撞上 E0106 和 E0597，再学生命周期标注、省略规则、&apos;static、Cow、Iterator 和模式匹配，最后让词法分析器在报错之后继续往下走。</summary>
      <content type="html"><![CDATA[<p>下面这段 C++ 我用 <code>g++ -std=c++17 -Wall -Wextra</code> 编译，一条警告都没有，运行打印出 <code>let</code>，看起来一切正常。其实它读的是一块已经死掉的栈内存：<code>token.text</code> 指向 <code>source</code> 里面，而 <code>source</code> 在内层花括号结束时就析构了。</p>
<div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>Token</span><span> {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>std::</span><span>string_view</span><span> </span><span>text</span><span>;</span></div></div><div><div><div>7</div></div><div><span>};</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>Token</span><span> </span><span>first_word</span><span>(</span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>src</span><span>) {</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>Token</span><span><span>{</span><span>std</span><span>::</span></span><span>string_view</span><span>(</span><span>src</span><span>).</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>src</span><span>.</span><span>find</span><span>(</span><span>' '</span><span>))};</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Token</span><span> </span><span>token</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>std::</span><span>string</span><span> </span><span>source</span><span> </span><span>=</span><span> </span><span>"let answer = 42"</span><span>;</span></div></div><div><div><div>17</div></div><div><span>        </span><span>token</span><span> </span><span>=</span><span> </span><span>first_word</span><span>(</span><span>source</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>token</span><span>.</span><span>text</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>
<p>这个程序在仓库里叫 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch04_view.cpp" target="_blank"><code>lab/cpp/ch04_view.cpp</code></a>，下面命令里的 <code>view.cpp</code> 就是它：</p>
<div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>g++</span><span> </span><span>-std=c++17</span><span> </span><span>-Wall</span><span> </span><span>-Wextra</span><span> </span><span>-o</span><span> </span><span>view</span><span> </span><span>view.cpp</span><span> &amp;&amp; </span><span>./view</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>换成 <code>-fsanitize=address</code> 再跑一遍，AddressSanitizer 才把它抓出来（输出很长，我只留下关键的三行）：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>==102059==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7531c2700090 at pc 0x7931c4e8835f bp 0x7ffe99eb73e0 sp 0x7ffe99eb6bb0</span></div></div><div><div><div>2</div></div><div><span>READ of size 3 at 0x7531c2700090 thread T0</span></div></div><div><div><div>3</div></div><div><span>...</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>#3 0x5a16aacc43c2 in main /tmp/cpp-lab-ch04/view.cpp:19</span></div></div><div><div><div>5</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>词法分析器恰好是这类 bug 的重灾区。我们想让每个 token 只是源码的一个”视图”，不复制任何字节，这正是 <code>std::string_view</code> 的用法，也正是上面这个 bug 的来源。Rust 的回答是：视图可以有，但编译器必须能证明它不会比源码活得更久。这一篇我们写 <code>pebble-lexer</code>，711 行，其中 134 行是 12 个测试，学会这套”证明”的语法：生命周期。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>我们将写完 <code>pebble-lexer</code>：<code>TokenKind&lt;'src&gt;</code>、<code>Token&lt;'src&gt;</code>、<code>Lexer&lt;'src&gt;</code>、<code>tokenize</code>，以及一个在遇到坏字符后继续往下走的错误恢复机制。用到的 Rust 特性：生命周期参数与省略规则、<code>'static</code>、<code>Cow</code>、<code>Clone</code> 与 <code>Copy</code> 的区别、<code>f64</code> 为什么不能 <code>Eq</code>、<code>char</code>、match 守卫 / 范围模式 / 或模式、<code>while let</code>、<code>Iterator</code> trait 与关联类型、<code>by_ref</code>、turbofish。
本篇结束时 <code>crates/pebble-lexer/</code> 的两个文件与 tag <code>tutorial-v1</code> 逐字节相同。上一篇：<a href="/posts/rust-by-pebble-03-enums-traits-diagnostics/">enum、trait 与第一份错误报告</a>，<code>pebble-diag</code> 已经完成，词法分析器会用它报告错误。</p></div></div>
<section><h2>起步：第三个 crate<a href="#起步第三个-crate"><span>#</span></a></h2><p>在工作区根目录运行（路径写法和前两篇一样，<code>~/pebble-tag</code> 是 tag 的本地克隆）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-lexer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 里，<code>pebble-diag</code> 那行下面加一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-lexer</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-lexer"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>crates/pebble-lexer/Cargo.toml</code> 写成这样（和 tag 一致）。它依赖前两个 crate：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-lexer"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"A zero-copy lexer for the Pebble language."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div><span>pebble-diag.workspace</span><span> = </span><span>true</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>16</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>token 应该长什么样<a href="#token-应该长什么样"><span>#</span></a></h2><p>词法分析器把 <code>let total = 1 + 2;</code> 这样的字符数组切成 token：<code>let</code>、<code>total</code>、<code>=</code>、<code>1</code>、<code>+</code>、<code>2</code>、<code>;</code>。标识符 token 要带着名字，所以最直觉的写法是让它自己拥有一份：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>enum</span><span> </span><span>TokenKind</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Ident</span><span>(</span><span>String</span><span>),</span></div></div><div><div><div>3</div></div><div><span>    </span><span>// ...</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每个标识符一次堆分配，一次拷贝，对一个只要读一遍就丢掉的 token 流来说很浪费：名字本来就躺在源码里，token 只要记住”从第几个字节到第几个字节”就够了。C++ 程序员的反应是 <code>std::string_view</code>，Rust 里对应的是 <code>&amp;str</code>：一个指针加一个长度，不拥有任何东西。第 2 篇已经见过它在函数参数里的样子，现在我们要把它<strong>放进结构体</strong>。</p><p>试一下（实验文件 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_struct_lifetime.rs" target="_blank"><code>src/bin/ch04_struct_lifetime.rs</code></a>，还在第 1 篇建的 <code>~/rust-lab</code> 里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Token</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>text</span><span>: &amp;</span><span>str</span><span>,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>t</span><span><span> </span><span>=</span><span> </span></span><span>Token</span><span> { </span><span>text</span><span>: </span><span>"let"</span><span> };</span></div></div><div><div><div>7</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>t</span><span>.text);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>--bin</span><span> </span><span>ch04_struct_lifetime</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0106]: missing lifetime specifier</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch04_struct_lifetime.rs:2:11</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>2 |     text: &amp;str,</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|           ^ expected named lifetime parameter</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>help: consider introducing a named lifetime parameter</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>1 ~ struct Token&lt;'a&gt; {</span></div></div><div><div><div>10</div></div><div><span>2 ~     text: &amp;'a str,</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0106`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_struct_lifetime") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器说：结构体里的引用必须有名字。这个名字叫<strong>生命周期参数</strong>，写成 <code>'a</code> 或者 <code>'src</code> 这样以单引号开头的标识符，和泛型参数一样写在尖括号里。编译器连修法都给了，我们按它的做：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>text</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读法：<code>Token&lt;'src&gt;</code> 是”一个借用了某段 <code>'src</code> 内有效的文本的 token”。<code>'src</code> 不是一个具体的时间，更像一个<strong>区域</strong>的名字：这段文本在哪个区域里有效，这个 token 就只能活在那个区域里。名字随便起，我起 <code>'src</code> 是因为它借的就是源码。</p><p>有了名字，编译器就能做 C++ 做不到的事：检查每一处使用。实验文件 <code>src/bin/ch04_outlives.rs</code> 把前面那段 C++ 原样翻译成 Rust：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>text</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>first_word</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Token</span><span>&lt;'</span><span>_</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Token</span><span> { </span><span>text</span><span>: </span><span>src</span><span>.</span><span>split</span><span>(</span><span>' '</span><span>).</span><span>next</span><span>().</span><span>unwrap_or</span><span>(</span><span>""</span><span>) }</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>token</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"let answer = 42"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>token</span><span><span> </span><span>=</span><span> </span></span><span>first_word</span><span>(&amp;</span><span>source</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>token</span><span>.text);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>--bin</span><span> </span><span>ch04_outlives</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0597]: `source` does not live long enough</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch04_outlives.rs:13:28</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>12 |         let source = String::from("let answer = 42");</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|             ------ binding `source` declared here</span></div></div><div><div><div>6</div></div><div><span>13 |         token = first_word(&amp;source);</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|                            ^^^^^^^ borrowed value does not live long enough</span></div></div><div><div><div>8</div></div><div><span>14 |     }</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|     - `source` dropped here while still borrowed</span></div></div><div><div><div>10</div></div><div><span>15 |     println!("{}", token.text);</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|                    ---------- borrow later used here</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0597`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_outlives") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这份报告几乎就是 AddressSanitizer 那三行的编译期版本，区别是它在你按下回车之前就出现了，而且不需要任何测试恰好踩到那条路径。它的读法是一个故事：<code>source</code> 在第 12 行出生，<code>token</code> 在第 13 行借了它，<code>source</code> 在第 14 行的 <code>}</code> 处死亡，<code>token</code> 在第 15 行还被使用。借用的区域（从 13 行到 15 行）超出了被借用者的生命（到 14 行）。</p><p><code>fn first_word(src: &amp;str) -&gt; Token&lt;'_&gt;</code> 里那个 <code>'_</code> 的意思是”这里有一个生命周期，请编译器按规则填”，规则一会儿讲。先看这个概念在词法分析器里怎么落地。</p></section>
<section><h2>最小的词法分析器<a href="#最小的词法分析器"><span>#</span></a></h2><p>我们先写一个最小版本，只认标识符、整数、<code>(</code>、<code>)</code>、<code>+</code>，让生命周期在一个小得可以一眼看完的程序里出现。把下面这些写进 <code>crates/pebble-lexer/src/lib.rs</code>（先清空）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::</span></span><span>Diagnostic</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceId</span><span>, </span><span>Span</span><span>};</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Ident</span><span>(&amp;'</span><span>src</span><span> </span><span>str</span><span>),</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>8</div></div><div><span>    </span><span>LParen</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>RParen</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Plus</span><span>,</span></div></div><div><div><div>11</div></div><div><span>    </span><span>Eof</span><span>,</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>15</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>kind</span><span>: </span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt;,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span> </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>source</span><span>: </span><span>SourceId</span><span>,</span></div></div><div><div><div>22</div></div><div><span>    </span><span>src</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>23</div></div><div><span>    </span><span>pos</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>impl</span><span>&lt;'</span><span>src</span><span>&gt; </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>source</span><span>: </span><span>SourceId</span><span>, </span><span>src</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>28</div></div><div><span>        </span><span>Self</span><span> { </span><span>source</span><span>, </span><span>src</span><span>, </span><span>pos</span><span>: </span><span>0</span><span> }</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span>fn</span><span> </span><span>rest</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;'</span><span>src</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.src[</span><span>self</span><span>.pos..]</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>    </span><span>fn</span><span> </span><span>peek</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>char</span><span>&gt; {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>self</span><span>.</span><span>rest</span><span>().</span><span>chars</span><span>().</span><span>next</span><span>()</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>    </span><span>fn</span><span> </span><span>bump</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>char</span><span>&gt; {</span></div></div><div><div><div>40</div></div><div><span>        </span><span>let</span><span> </span><span>ch</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>()?;</span></div></div><div><div><div>41</div></div><div><span>        </span><span>self</span><span><span>.pos </span><span>+=</span><span> </span></span><span>ch</span><span>.</span><span>len_utf8</span><span>();</span></div></div><div><div><div>42</div></div><div><span>        </span><span>Some</span><span>(</span><span>ch</span><span>)</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span>fn</span><span> </span><span>span_from</span><span>(&amp;</span><span>self</span><span>, </span><span>start</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>46</div></div><div><span>        </span><span>Span</span><span>::</span><span>new</span><span>(</span><span>self</span><span>.source, </span><span>start</span><span> </span><span>as</span><span> </span><span>u32</span><span>, </span><span>self</span><span>.pos </span><span>as</span><span> </span><span>u32</span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>next_token</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Result</span><span>&lt;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt;, </span><span>Diagnostic</span><span>&gt;&gt; {</span></div></div><div><div><div>50</div></div><div><span>        </span><span>while</span><span> </span><span>self</span><span>.</span><span>peek</span><span>().</span><span>is_some_and</span><span><span>(</span><span>char</span><span>::</span></span><span>is_whitespace</span><span>) {</span></div></div><div><div><div>51</div></div><div><span>            </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>53</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.pos;</span></div></div><div><div><div>54</div></div><div><span>        </span><span>let</span><span> </span><span>ch</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>bump</span><span>()?;</span></div></div><div><div><div>55</div></div><div><span>        </span><span>let</span><span> </span><span>kind</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>ch</span><span> {</span></div></div><div><div><div>56</div></div><div><span>            </span><span>'('</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>LParen</span><span>,</span></div></div><div><div><div>57</div></div><div><span>            </span><span>')'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>RParen</span><span>,</span></div></div><div><div><div>58</div></div><div><span>            </span><span>'+'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Plus</span><span>,</span></div></div><div><div><div>59</div></div><div><span>            </span><span>'0'</span><span>..=</span><span>'9'</span><span> =&gt; {</span></div></div><div><div><div>60</div></div><div><span>                </span><span>while</span><span> </span><span>self</span><span>.</span><span>peek</span><span>().</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>c</span><span><span>|</span><span> </span></span><span>c</span><span>.</span><span>is_ascii_digit</span><span>()) {</span></div></div><div><div><div>61</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>62</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>63</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Int</span><span>(</span><span>self</span><span>.src[</span><span>start</span><span>..</span><span>self</span><span>.pos].</span><span>parse</span><span>().</span><span>unwrap</span><span>())</span></div></div><div><div><div>64</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>65</div></div><div><span>            </span><span>ch</span><span> </span><span>if</span><span> </span><span>ch</span><span><span> </span><span>==</span><span> </span></span><span>'_'</span><span><span> </span><span>||</span><span> </span></span><span>ch</span><span>.</span><span>is_alphabetic</span><span>() =&gt; {</span></div></div><div><div><div>66</div></div><div><span>                </span><span>while</span><span> </span><span>self</span><span>.</span><span>peek</span><span>().</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>c</span><span><span>|</span><span> </span></span><span>c</span><span><span> </span><span>==</span><span> </span></span><span>'_'</span><span><span> </span><span>||</span><span> </span></span><span>c</span><span>.</span><span>is_alphanumeric</span><span>()) {</span></div></div><div><div><div>67</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>68</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>69</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(&amp;</span><span>self</span><span>.src[</span><span>start</span><span>..</span><span>self</span><span>.pos])</span></div></div><div><div><div>70</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>71</div></div><div><span>            </span><span>other</span><span> =&gt; {</span></div></div><div><div><div>72</div></div><div><span>                </span><span>return</span><span> </span><span>Some</span><span>(</span><span>Err</span><span>(</span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>format!</span><span>(</span><span>"unexpected character `{other}`"</span><span>))</span></div></div><div><div><div>73</div></div><div><span><span>                    </span></span><span>.</span><span>with_span</span><span>(</span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>))));</span></div></div><div><div><div>74</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>75</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>76</div></div><div><span>        </span><span>Some</span><span>(</span><span>Ok</span><span>(</span><span>Token</span><span> { </span><span>kind</span><span>, </span><span>span</span><span>: </span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>) }))</span></div></div><div><div><div>77</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>78</div></div><div><span>}</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>81</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>82</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>85</div></div><div><span>    </span><span>fn</span><span> </span><span>identifiers_borrow_the_source</span><span>() {</span></div></div><div><div><div>86</div></div><div><span>        </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"hello"</span><span>);</span></div></div><div><div><div>87</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), &amp;</span><span>source</span><span>);</span></div></div><div><div><div>88</div></div><div><span>        </span><span>let</span><span> </span><span>token</span><span><span> </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_token</span><span>().</span><span>unwrap</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>89</div></div><div><span>        </span><span>match</span><span> </span><span>token</span><span>.kind {</span></div></div><div><div><div>90</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>name</span><span>) =&gt; {</span></div></div><div><div><div>91</div></div><div><span>                </span><span>let</span><span> </span><span>base</span><span><span> </span><span>=</span><span> </span></span><span>source</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>92</div></div><div><span>                </span><span>let</span><span> </span><span>borrowed</span><span><span> </span><span>=</span><span> </span></span><span>name</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>93</div></div><div><span>                </span><span>assert!</span><span>((</span><span>base</span><span>..</span><span>base</span><span> + </span><span>source</span><span>.</span><span>len</span><span>()).</span><span>contains</span><span>(&amp;</span><span>borrowed</span><span>));</span></div></div><div><div><div>94</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>95</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>panic!</span><span>(</span><span>"expected identifier, got {other:?}"</span><span>),</span></div></div><div><div><div>96</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>97</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>98</div></div><div>
</div></div><div><div><div>99</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>100</div></div><div><span>    </span><span>fn</span><span> </span><span>a_token_outlives_its_lexer</span><span>() {</span></div></div><div><div><div>101</div></div><div><span>        </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"answer + 42"</span><span>);</span></div></div><div><div><div>102</div></div><div><span>        </span><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> {</span></span></div></div><div><div><div>103</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), &amp;</span><span>source</span><span>);</span></div></div><div><div><div>104</div></div><div><span>            </span><span>lexer</span><span>.</span><span>next_token</span><span>().</span><span>unwrap</span><span>().</span><span>unwrap</span><span>()</span></div></div><div><div><div>105</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>106</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>first</span><span>.kind, </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>"answer"</span><span>));</span></div></div><div><div><div>107</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>108</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这是脚手架，不是最终文件：最终文件我们会在后面一块一块地换掉它。先把几个新语法看清楚。</p><p><strong><code>impl&lt;'src&gt; Lexer&lt;'src&gt;</code>。</strong> 泛型参数要先声明再使用：<code>impl&lt;'src&gt;</code> 声明”接下来有一个生命周期参数叫 <code>'src</code>”，<code>Lexer&lt;'src&gt;</code> 是在使用它。和 C++ 的 <code>template&lt;typename T&gt; class Foo</code> 对应：<code>impl&lt;'src&gt;</code> 像 <code>template&lt;...&gt;</code>，只是生命周期参数在运行时根本不存在，编译完就擦掉了，不产生任何代码。</p><p><strong><code>src: &amp;'src str</code>。</strong> <code>Lexer</code> 持有对源码的借用，借用区域叫 <code>'src</code>。<code>new</code> 接收的 <code>&amp;'src str</code> 和结构体里的 <code>'src</code> 是同一个，所以”词法分析器活多久”被”源码活多久”限制住了。</p><p><strong><code>fn rest(&amp;self) -&gt; &amp;'src str</code>。</strong> 这是整个文件里最值得停下来看的签名。<code>&amp;self</code> 本身也是一个借用（借的是 <code>Lexer</code>），但返回值写的是 <code>'src</code>，不是 <code>&amp;self</code> 的区域。意思是：返回的这段文本借自<strong>源码</strong>，不是借自词法分析器。这个区别马上要用到，它决定了 token 能不能比词法分析器活得久。</p><p><strong><code>Option&lt;Result&lt;Token&lt;'src&gt;, Diagnostic&gt;&gt;</code>。</strong> 三层含义：外层 <code>Option</code> 的 <code>None</code> 表示”没有更多 token 了”；<code>Some(Ok(token))</code> 是一个正常 token；<code>Some(Err(diagnostic))</code> 是”这个位置有个错误，但后面可能还有 token”。注意 <code>Diagnostic</code> 没有生命周期参数，它拥有自己的 <code>String</code>，所以和源码无关。</p><p><strong><code>self.bump()?</code>。</strong> 第 2 篇见过 <code>?</code> 用在 <code>Option</code> 上：<code>None</code> 就整个函数直接返回 <code>None</code>。这里 <code>bump</code> 在输入结束时返回 <code>None</code>，<code>next_token</code> 也就跟着返回 <code>None</code>。</p><p><strong><code>'0'..='9'</code>。</strong> 一个范围模式，匹配这个闭区间里的任何字符。后面细讲。</p><p><code>Option::is_some_and(f)</code> 是”<code>Some</code> 并且里面的值满足 <code>f</code> 才为真”，<code>char::is_whitespace</code> 这样的函数路径可以直接当参数传（和写成闭包等价）；<code>ch if ...</code> 是匹配守卫，<code>while let</code> 是”模式匹配成功就继续循环”，这两个也留到后面细讲。</p><p>脚手架末尾已经带着下面两个测试，不用再抄一遍，这里只解释它们。第一个是这篇的核心证明：标识符 token 里的 <code>&amp;str</code> 确实指向源码内部，而不是副本。<code>as_ptr()</code> 返回指向第一个字节的裸指针，<code>as usize</code> 把它当整数比较（这是一个纯整数运算，没有任何 <code>unsafe</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>identifiers_borrow_the_source</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"hello"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), &amp;</span><span>source</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>token</span><span><span> </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_token</span><span>().</span><span>unwrap</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>10</div></div><div><span>        </span><span>match</span><span> </span><span>token</span><span>.kind {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>name</span><span>) =&gt; {</span></div></div><div><div><div>12</div></div><div><span>                </span><span>let</span><span> </span><span>base</span><span><span> </span><span>=</span><span> </span></span><span>source</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>13</div></div><div><span>                </span><span>let</span><span> </span><span>borrowed</span><span><span> </span><span>=</span><span> </span></span><span>name</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>14</div></div><div><span>                </span><span>assert!</span><span>((</span><span>base</span><span>..</span><span>base</span><span> + </span><span>source</span><span>.</span><span>len</span><span>()).</span><span>contains</span><span>(&amp;</span><span>borrowed</span><span>));</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>panic!</span><span>(</span><span>"expected identifier, got {other:?}"</span><span>),</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>(base..base + source.len())</code> 是一个半开区间值（第 1 篇 <code>for</code> 里见过它），<code>.contains(&amp;x)</code> 问 x 在不在里面。<code>unwrap()</code> 在 <code>None</code> 或 <code>Err</code> 时 panic，<code>.unwrap().unwrap()</code> 剥掉两层；在测试里这没问题，第 7 篇会专门讨论什么时候可以这么做。</p><p>第二个测试证明刚才说的那个区别：token 可以比词法分析器活得久，只要源码还在。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>a_token_outlives_its_lexer</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"answer + 42"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> {</span></span></div></div><div><div><div>5</div></div><div><span>            </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), &amp;</span><span>source</span><span>);</span></div></div><div><div><div>6</div></div><div><span>            </span><span>lexer</span><span>.</span><span>next_token</span><span>().</span><span>unwrap</span><span>().</span><span>unwrap</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>first</span><span>.kind, </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>"answer"</span><span>));</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内层块结束时 <code>lexer</code> 被销毁，<code>first</code> 还在用，而且编译通过。因为 <code>first</code> 的类型是 <code>Token&lt;'src&gt;</code>，<code>'src</code> 是源码的区域，和 <code>lexer</code> 变量本身的死活无关。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-lexer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 2 tests</span></div></div><div><div><div>2</div></div><div><span>test tests::a_token_outlives_its_lexer ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::identifiers_borrow_the_source ... ok</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再让它跑起来看看。临时建一个 <code>crates/pebble-lexer/src/bin/stage1.rs</code>（跑完删掉；完整版在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/lab-ch04-stages" target="_blank"><code>lab/lab-ch04-stages</code></a>，它依赖 tag 里的最终版 crate，输出相同）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_lexer</span><span>::</span></span><span>Lexer</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceId</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"total + (x1 + 42)"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>item</span><span><span>) </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_token</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{item:?}"</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"a @ b"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>item</span><span><span>) </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_token</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>match</span><span> </span><span>item</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>Ok</span><span>(</span><span>token</span><span>) =&gt; </span><span>println!</span><span>(</span><span>"ok  {:?}"</span><span>, </span><span>token</span><span>.kind),</span></div></div><div><div><div>13</div></div><div><span>            </span><span>Err</span><span>(</span><span>diagnostic</span><span>) =&gt; </span><span>println!</span><span>(</span><span>"err {}"</span><span>, </span><span>diagnostic</span><span>.message),</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>-p</span><span> </span><span>pebble-lexer</span><span> </span><span>--bin</span><span> </span><span>stage1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Ok(Token { kind: Ident("total"), span: 0..5 })</span></div></div><div><div><div>2</div></div><div><span>Ok(Token { kind: Plus, span: 6..7 })</span></div></div><div><div><div>3</div></div><div><span>Ok(Token { kind: LParen, span: 8..9 })</span></div></div><div><div><div>4</div></div><div><span>Ok(Token { kind: Ident("x1"), span: 9..11 })</span></div></div><div><div><div>5</div></div><div><span>Ok(Token { kind: Plus, span: 12..13 })</span></div></div><div><div><div>6</div></div><div><span>Ok(Token { kind: Int(42), span: 14..16 })</span></div></div><div><div><div>7</div></div><div><span>Ok(Token { kind: RParen, span: 16..17 })</span></div></div><div><div><div>8</div></div><div><span>ok  Ident("a")</span></div></div><div><div><div>9</div></div><div><span>err unexpected character `@`</span></div></div><div><div><div>10</div></div><div><span>ok  Ident("b")</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出的后三行是 <code>a @ b</code>：<code>@</code> 产出了一个错误，但 <code>b</code> 照样出来了。这就是”错误恢复”的雏形，后面的完整版本只是在它上面加细节。</p></section>
<section><h2>省略规则：编译器替你写了什么<a href="#省略规则编译器替你写了什么"><span>#</span></a></h2><p><code>rest(&amp;self) -&gt; &amp;'src str</code> 里我手写了 <code>'src</code>。如果写成 <code>-&gt; &amp;str</code> 会怎样？第 2 篇说过，有 <code>&amp;self</code> 的方法，返回的引用默认借自 <code>self</code>。这条规则的完整版本是 Rust 的<strong>生命周期省略规则</strong>，一共三条：</p><ol>
<li>函数参数里每个被省略的生命周期，各自变成一个独立的生命周期参数。</li>
<li>如果输入位置<strong>恰好有一个</strong>生命周期（不管省不省略），它被赋给所有省略的输出生命周期。</li>
<li>如果输入里有 <code>&amp;self</code> 或 <code>&amp;mut self</code>，<code>self</code> 的生命周期被赋给所有省略的输出生命周期。</li>
</ol><p>三条都不适用，就必须手写，否则 E0106（我们已经见过一次）。</p><p>规则 3 看起来很方便，对我们的词法分析器却是个陷阱。实验文件 <code>src/bin/ch04_elision_trap.rs</code> 是一个玩具版：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>src</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pos</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>impl</span><span>&lt;'</span><span>src</span><span>&gt; </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>// Elided: the returned &amp;str is tied to the &amp;mut self borrow, not to 'src.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>next_word</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>rest</span><span><span> </span><span>=</span><span> &amp;</span></span><span>self</span><span>.src[</span><span>self</span><span>.pos..];</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>rest</span><span>.</span><span>find</span><span>(</span><span>' '</span><span>).</span><span>unwrap_or</span><span>(</span><span>rest</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span><span>.pos </span><span>+=</span><span> (</span></span><span>end</span><span> + </span><span>1</span><span>).</span><span>min</span><span>(</span><span>rest</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>&amp;</span><span>rest</span><span>[..</span><span>end</span><span>]</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span> { </span><span>src</span><span>: </span><span>"let answer"</span><span>, </span><span>pos</span><span>: </span><span>0</span><span> };</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_word</span><span>();</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_word</span><span>();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{a} {b}"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>next_word</code> 的返回类型 <code>&amp;str</code> 被省略了，按规则 3 它借自 <code>&amp;mut self</code>，也就是<strong>词法分析器本身</strong>。结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0499]: cannot borrow `lexer` as mutable more than once at a time</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch04_elision_trap.rs:19:13</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>18 |     let a = lexer.next_word();</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|             ----- first mutable borrow occurs here</span></div></div><div><div><div>6</div></div><div><span>19 |     let b = lexer.next_word();</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|             ^^^^^ second mutable borrow occurs here</span></div></div><div><div><div>8</div></div><div><span>20 |     println!("{a} {b}");</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|                - first borrow later used here</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>For more information about this error, try `rustc --explain E0499`.</span></div></div><div><div><div>12</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_elision_trap") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一次调用返回的 <code>a</code> 借着整个 <code>lexer</code>（而且是可变借用），<code>a</code> 还没用完，就不能再次借用 <code>lexer</code> 去取 <code>b</code>。可是 <code>a</code> 明明只是源码里的一段文本，和 <code>lexer</code> 的游标状态毫无关系。这就是我在上一节强调那个区别的原因：把返回类型写成 <code>&amp;'src str</code>，就是告诉编译器”这个结果借自源码，不借自我”。<code>src/bin/ch04_elision_fixed.rs</code> 里只改了一处：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>src</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pos</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>impl</span><span>&lt;'</span><span>src</span><span>&gt; </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>// Named: the result borrows the source, not the lexer.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>next_word</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; &amp;'</span><span>src</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>rest</span><span><span> </span><span>=</span><span> &amp;</span></span><span>self</span><span>.src[</span><span>self</span><span>.pos..];</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>rest</span><span>.</span><span>find</span><span>(</span><span>' '</span><span>).</span><span>unwrap_or</span><span>(</span><span>rest</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span><span>.pos </span><span>+=</span><span> (</span></span><span>end</span><span> + </span><span>1</span><span>).</span><span>min</span><span>(</span><span>rest</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>&amp;</span><span>rest</span><span>[..</span><span>end</span><span>]</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span> { </span><span>src</span><span>: </span><span>"let answer"</span><span>, </span><span>pos</span><span>: </span><span>0</span><span> };</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_word</span><span>();</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_word</span><span>();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{a} {b}"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let answer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我不想只用玩具说话，所以在真的词法分析器上也做了同样的实验：把最终版 <code>next_token</code> 的返回类型从 <code>Option&lt;Result&lt;Token&lt;'src&gt;, Diagnostic&gt;&gt;</code> 改成 <code>Option&lt;Result&lt;Token&lt;'_&gt;, Diagnostic&gt;&gt;</code>（让规则 3 去填），编译 <code>pebble-lexer</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: lifetime may not live long enough</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-lexer/src/lib.rs:504:9</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>500 | impl&lt;'src&gt; Iterator for Lexer&lt;'src&gt; {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|      ---- lifetime `'src` defined here</span></div></div><div><div><div>6</div></div><div><span>...</span></div></div><div><div><div>7</div></div><div><span>503 |     fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt; {</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>|             - let's call the lifetime of this reference `'1`</span></div></div><div><div><div>9</div></div><div><span>504 |         self.next_token()</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|         ^^^^^^^^^^^^^^^^^ method was supposed to return data with lifetime `'src` but it is returning data with lifetime `'1`</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>error[E0499]: cannot borrow `lexer` as mutable more than once at a time</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-lexer/src/lib.rs:517:28</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>513 | pub fn tokenize(source: SourceId, src: &amp;str) -&gt; (Vec&lt;Token&lt;'_&gt;&gt;, Vec&lt;Diagnostic&gt;) {</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>|                                        - let's call the lifetime of this reference `'1`</span></div></div><div><div><div>17</div></div><div><span>...</span></div></div><div><div><div>18</div></div><div><span>517 |     while let Some(item) = lexer.next_token() {</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|                            ^^^^^ `lexer` was mutably borrowed here in the previous iteration of the loop</span></div></div><div><div><div>20</div></div><div><span>...</span></div></div><div><div><div>21</div></div><div><span>529 |     (tokens, diagnostics)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>|     --------------------- returning this value requires that `lexer` is borrowed for `'1`</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>For more information about this error, try `rustc --explain E0499`.</span></div></div><div><div><div>25</div></div><div><span>error: could not compile `pebble-lexer` (lib) due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个错误，同一个病根。第一个来自 <code>Iterator</code> 的 <code>next</code>：它承诺返回 <code>'src</code> 的数据，<code>next_token</code> 却只给了 <code>&amp;mut self</code> 的区域；第二个来自 <code>tokenize</code> 里的循环，第二圈要再次借用 <code>lexer</code>，而第一圈的 token 还借着它。把 <code>'_</code> 换回 <code>'src</code>，两个错误一起消失。</p><p>这个实验我觉得是理解生命周期标注最好的一个：标注不改变任何运行时行为，它只是让你<strong>说清楚</strong>一个返回值借自哪里，编译器才能放心地放行。</p><section><h3><code>tokenize</code> 的签名<a href="#tokenize-的签名"><span>#</span></a></h3><p><code>tokenize</code> 没有 <code>&amp;self</code>，规则 3 不适用；它只有一个输入生命周期（<code>src: &amp;str</code>，<code>SourceId</code> 是个普通整数），所以规则 2 生效，返回类型里所有被省略的生命周期都等于它。我们写成 <code>Vec&lt;Token&lt;'_&gt;&gt;</code>。如果你偷懒写 <code>Vec&lt;Token&gt;</code>，工作区的 lint 会提醒你（这个改动是 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch04-elide.patch" target="_blank"><code>lab/exercises/ch04-elide.patch</code></a>，对 tag 应用后 <code>cargo build -p pebble-lexer</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>warning: hidden lifetime parameters in types are deprecated</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-lexer/src/lib.rs:513:54</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>513 | pub fn tokenize(source: SourceId, src: &amp;str) -&gt; (Vec&lt;Token&gt;, Vec&lt;Diagnostic&gt;) {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|                                                      ^^^^^ expected lifetime parameter</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>= note: `-W elided-lifetimes-in-paths` implied by `-W rust-2018-idioms`</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>= help: to override `-W rust-2018-idioms` add `#[allow(elided_lifetimes_in_paths)]`</span></div></div><div><div><div>9</div></div><div><span>help: indicate the anonymous lifetime</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>513 | pub fn tokenize(source: SourceId, src: &amp;str) -&gt; (Vec&lt;Token&lt;'_&gt;&gt;, Vec&lt;Diagnostic&gt;) {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>|                                                           ++++</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>warning: hiding a lifetime that's elided elsewhere is confusing</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-lexer/src/lib.rs:513:40</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>17</div></div><div><span>513 | pub fn tokenize(source: SourceId, src: &amp;str) -&gt; (Vec&lt;Token&gt;, Vec&lt;Diagnostic&gt;) {</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>|                                        ^^^^          ^^^^^ the same lifetime is hidden here</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>|                                        |</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>|                                        the lifetime is elided here</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>= help: the same lifetime is referred to in inconsistent ways, making the signature confusing</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>= note: `#[warn(mismatched_lifetime_syntaxes)]` on by default</span></div></div><div><div><div>24</div></div><div><span>help: use `'_` for type paths</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>26</div></div><div><span>513 | pub fn tokenize(source: SourceId, src: &amp;str) -&gt; (Vec&lt;Token&lt;'_&gt;&gt;, Vec&lt;Diagnostic&gt;) {</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>|                                                           ++++</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>warning: `pebble-lexer` (lib) generated 2 warnings (run `cargo fix --lib -p pebble-lexer` to apply 1 suggestion)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这条警告来自 <code>Cargo.toml</code> 里的 <code>rust_2018_idioms = "warn"</code>。它要求借用关系必须在类型里<strong>写出来</strong>（哪怕只写一个 <code>'_</code>），读签名的人才看得见这里藏着一个借用。你在第 1 篇的 <code>[workspace.lints]</code> 里写过这一行，它就是为这类情况准备的。</p></section><section><h3><code>'static</code>：永远有效的那个区域<a href="#static永远有效的那个区域"><span>#</span></a></h3><p>还有一个特殊的名字：<code>'static</code>，意思是”在整个程序运行期间都有效”。字符串字面量 <code>"let"</code> 的类型是 <code>&amp;'static str</code>，因为它被编译进了可执行文件的只读数据段，永远在那里（第 2 篇的表里提过一次）。我们后面的 <code>TokenKind::describe</code> 返回 <code>&amp;'static str</code>，每个描述都是字面量。实验文件 <code>src/bin/ch04_static.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>keyword</span><span>(</span><span>word</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Option</span><span>&lt;&amp;'</span><span>static</span><span> </span><span>str</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>match</span><span> </span><span>word</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>"let"</span><span> =&gt; </span><span>Some</span><span>(</span><span>"Let"</span><span>),</span></div></div><div><div><div>4</div></div><div><span>        </span><span>"fn"</span><span> =&gt; </span><span>Some</span><span>(</span><span>"Fn"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>None</span><span>,</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>owned</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"let"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>        </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>keyword</span><span>(&amp;</span><span>owned</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{result:?}"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Some("Let")</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>keyword</code> 的参数是一个会被销毁的局部 <code>String</code>，但它的返回值是 <code>'static</code>，和参数没有任何借用关系，所以 <code>owned</code> 死了之后 <code>result</code> 照样可用。这和上面的 <code>E0597</code> 形成对照：返回值借不借输入，由签名决定，不由函数体决定。</p></section></section>
<section><h2><code>TokenKind</code>：完整的 token 词汇表<a href="#tokenkind完整的-token-词汇表"><span>#</span></a></h2><p>脚手架到此为止。我们现在开始写真的文件。先是文件头，前两行属性你在第 2、3 篇见过，接着是模块文档和导入：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![cfg_attr(not(test), no_std)]</span></div></div><div><div><div>2</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>3</div></div><div><span>//! A hand-written, zero-copy lexer for Pebble.</span></div></div><div><div><div>4</div></div><div><span>//!</span></div></div><div><div><div>5</div></div><div><span>//! Two design decisions shape this crate:</span></div></div><div><div><div>6</div></div><div><span>//!</span></div></div><div><div><div>7</div></div><div><span>//! 1. **Zero-copy.** An identifier token does not own a `String`; it borrows a</span></div></div><div><div><div>8</div></div><div><span>//!    slice of the original source (`&amp;'src str`). The token stream is therefore</span></div></div><div><div><div>9</div></div><div><span>//!    a set of views into the input, and lexing allocates nothing except in the</span></div></div><div><div><div>10</div></div><div><span>//!    rare case of a string literal that contains escape sequences.</span></div></div><div><div><div>11</div></div><div><span>//! 2. **It keeps going.** A bad character produces a [`Diagnostic`] and the</span></div></div><div><div><div>12</div></div><div><span>//!    lexer resumes at the next character, so one run can report every typo in</span></div></div><div><div><div>13</div></div><div><span>//!    a file instead of just the first.</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>extern</span><span> </span><span>crate</span><span> alloc;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::</span><span>borrow</span><span>::</span></span><span>Cow</span><span>;</span></div></div><div><div><div>18</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::format;</span></span></div></div><div><div><div>19</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::</span><span>string</span><span>::</span></span><span>String</span><span>;</span></div></div><div><div><div>20</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::</span><span>vec</span><span>::</span></span><span>Vec</span><span>;</span></div></div><div><div><div>21</div></div><div><span>use</span><span><span> </span><span>core</span><span>::fmt;</span></span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::</span></span><span>Diagnostic</span><span>;</span></div></div><div><div><div>24</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceId</span><span>, </span><span>Span</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三个新东西。<code>use alloc::borrow::Cow</code> 我们马上讲。<code>use core::fmt;</code> 让下面能写 <code>fmt::Display</code>。<code>extern crate alloc;</code> 和 <code>alloc::</code> 前缀是第 2 篇说的 <code>no_std</code> 套路：这个 crate 在测试之外不链接标准库（原因第 13 篇揭晓：<code>pebble-nostd</code> 会直接复用这个词法分析器）。</p><p>接下来是 <code>TokenKind</code>，Pebble 的全部词汇。它有点长，但每个变体都是一行，没有任何新语法，你可以照着写：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The kind of a token, plus any payload it carries.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// The `&amp;'src str` payloads tie a token to the source string it came from: a</span></div></div><div><div><div>4</div></div><div><span>/// [`Token`] may outlive the [`Lexer`] but never the input, which is exactly</span></div></div><div><div><div>5</div></div><div><span>/// the guarantee we want. `Clone` (but not `Copy`) because [`Cow`] may own data.</span></div></div><div><div><div>6</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>7</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>// --- literals -----------------------------------------------------------</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// `123`</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// `1.5`, `0.25`</span></div></div><div><div><div>12</div></div><div><span>    </span><span>Float</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>13</div></div><div><span>    </span><span>/// `"..."`, with escapes already resolved.</span></div></div><div><div><div>14</div></div><div><span>    </span><span>Str</span><span>(</span><span>Cow</span><span>&lt;'</span><span>src</span><span>, </span><span>str</span><span>&gt;),</span></div></div><div><div><div>15</div></div><div><span>    </span><span>/// `true`</span></div></div><div><div><div>16</div></div><div><span>    </span><span>True</span><span>,</span></div></div><div><div><div>17</div></div><div><span>    </span><span>/// `false`</span></div></div><div><div><div>18</div></div><div><span>    </span><span>False</span><span>,</span></div></div><div><div><div>19</div></div><div><span>    </span><span>/// `nil`</span></div></div><div><div><div>20</div></div><div><span>    </span><span>Nil</span><span>,</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>// --- identifiers and keywords ------------------------------------------</span></div></div><div><div><div>23</div></div><div><span>    </span><span>/// `foo`, `_bar`</span></div></div><div><div><div>24</div></div><div><span>    </span><span>Ident</span><span>(&amp;'</span><span>src</span><span> </span><span>str</span><span>),</span></div></div><div><div><div>25</div></div><div><span>    </span><span>/// `let`</span></div></div><div><div><div>26</div></div><div><span>    </span><span>Let</span><span>,</span></div></div><div><div><div>27</div></div><div><span>    </span><span>/// `fn`</span></div></div><div><div><div>28</div></div><div><span>    </span><span>Fn</span><span>,</span></div></div><div><div><div>29</div></div><div><span>    </span><span>/// `if`</span></div></div><div><div><div>30</div></div><div><span>    </span><span>If</span><span>,</span></div></div><div><div><div>31</div></div><div><span>    </span><span>/// `else`</span></div></div><div><div><div>32</div></div><div><span>    </span><span>Else</span><span>,</span></div></div><div><div><div>33</div></div><div><span>    </span><span>/// `while`</span></div></div><div><div><div>34</div></div><div><span>    </span><span>While</span><span>,</span></div></div><div><div><div>35</div></div><div><span>    </span><span>/// `for`</span></div></div><div><div><div>36</div></div><div><span>    </span><span>For</span><span>,</span></div></div><div><div><div>37</div></div><div><span>    </span><span>/// `in`</span></div></div><div><div><div>38</div></div><div><span>    </span><span>In</span><span>,</span></div></div><div><div><div>39</div></div><div><span>    </span><span>/// `return`</span></div></div><div><div><div>40</div></div><div><span>    </span><span>Return</span><span>,</span></div></div><div><div><div>41</div></div><div><span>    </span><span>/// `break`</span></div></div><div><div><div>42</div></div><div><span>    </span><span>Break</span><span>,</span></div></div><div><div><div>43</div></div><div><span>    </span><span>/// `continue`</span></div></div><div><div><div>44</div></div><div><span>    </span><span>Continue</span><span>,</span></div></div><div><div><div>45</div></div><div><span>    </span><span>/// `and` or `&amp;&amp;`</span></div></div><div><div><div>46</div></div><div><span>    </span><span>And</span><span>,</span></div></div><div><div><div>47</div></div><div><span>    </span><span>/// `or` or `||`</span></div></div><div><div><div>48</div></div><div><span>    </span><span>Or</span><span>,</span></div></div><div><div><div>49</div></div><div><span>    </span><span>/// `not` or `!`</span></div></div><div><div><div>50</div></div><div><span>    </span><span>Not</span><span>,</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>    </span><span>// --- punctuation --------------------------------------------------------</span></div></div><div><div><div>53</div></div><div><span>    </span><span>/// `(`</span></div></div><div><div><div>54</div></div><div><span>    </span><span>LParen</span><span>,</span></div></div><div><div><div>55</div></div><div><span>    </span><span>/// `)`</span></div></div><div><div><div>56</div></div><div><span>    </span><span>RParen</span><span>,</span></div></div><div><div><div>57</div></div><div><span>    </span><span>/// `{`</span></div></div><div><div><div>58</div></div><div><span>    </span><span>LBrace</span><span>,</span></div></div><div><div><div>59</div></div><div><span>    </span><span>/// `}`</span></div></div><div><div><div>60</div></div><div><span>    </span><span>RBrace</span><span>,</span></div></div><div><div><div>61</div></div><div><span>    </span><span>/// `[`</span></div></div><div><div><div>62</div></div><div><span>    </span><span>LBracket</span><span>,</span></div></div><div><div><div>63</div></div><div><span>    </span><span>/// `]`</span></div></div><div><div><div>64</div></div><div><span>    </span><span>RBracket</span><span>,</span></div></div><div><div><div>65</div></div><div><span>    </span><span>/// `#{`, the start of a map literal</span></div></div><div><div><div>66</div></div><div><span>    </span><span>HashBrace</span><span>,</span></div></div><div><div><div>67</div></div><div><span>    </span><span>/// `,`</span></div></div><div><div><div>68</div></div><div><span>    </span><span>Comma</span><span>,</span></div></div><div><div><div>69</div></div><div><span>    </span><span>/// `:`</span></div></div><div><div><div>70</div></div><div><span>    </span><span>Colon</span><span>,</span></div></div><div><div><div>71</div></div><div><span>    </span><span>/// `;`</span></div></div><div><div><div>72</div></div><div><span>    </span><span>Semicolon</span><span>,</span></div></div><div><div><div>73</div></div><div><span>    </span><span>/// `+`</span></div></div><div><div><div>74</div></div><div><span>    </span><span>Plus</span><span>,</span></div></div><div><div><div>75</div></div><div><span>    </span><span>/// `-`</span></div></div><div><div><div>76</div></div><div><span>    </span><span>Minus</span><span>,</span></div></div><div><div><div>77</div></div><div><span>    </span><span>/// `*`</span></div></div><div><div><div>78</div></div><div><span>    </span><span>Star</span><span>,</span></div></div><div><div><div>79</div></div><div><span>    </span><span>/// `/`</span></div></div><div><div><div>80</div></div><div><span>    </span><span>Slash</span><span>,</span></div></div><div><div><div>81</div></div><div><span>    </span><span>/// `%`</span></div></div><div><div><div>82</div></div><div><span>    </span><span>Percent</span><span>,</span></div></div><div><div><div>83</div></div><div><span>    </span><span>/// `=`</span></div></div><div><div><div>84</div></div><div><span>    </span><span>Assign</span><span>,</span></div></div><div><div><div>85</div></div><div><span>    </span><span>/// `==`</span></div></div><div><div><div>86</div></div><div><span>    </span><span>Eq</span><span>,</span></div></div><div><div><div>87</div></div><div><span>    </span><span>/// `!=`</span></div></div><div><div><div>88</div></div><div><span>    </span><span>NotEq</span><span>,</span></div></div><div><div><div>89</div></div><div><span>    </span><span>/// `&lt;`</span></div></div><div><div><div>90</div></div><div><span>    </span><span>Lt</span><span>,</span></div></div><div><div><div>91</div></div><div><span>    </span><span>/// `&lt;=`</span></div></div><div><div><div>92</div></div><div><span>    </span><span>LtEq</span><span>,</span></div></div><div><div><div>93</div></div><div><span>    </span><span>/// `&gt;`</span></div></div><div><div><div>94</div></div><div><span>    </span><span>Gt</span><span>,</span></div></div><div><div><div>95</div></div><div><span>    </span><span>/// `&gt;=`</span></div></div><div><div><div>96</div></div><div><span>    </span><span>GtEq</span><span>,</span></div></div><div><div><div>97</div></div><div>
</div></div><div><div><div>98</div></div><div><span>    </span><span>/// Synthetic token emitted once at the end of input.</span></div></div><div><div><div>99</div></div><div><span>    </span><span>Eof</span><span>,</span></div></div><div><div><div>100</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意第一行的 <code>&lt;'src&gt;</code>。它让整个枚举参数化：<code>Ident(&amp;'src str)</code> 和 <code>Str(Cow&lt;'src, str&gt;)</code> 这两个变体里的借用都叫 <code>'src</code>。其他变体（<code>Let</code>、<code>Plus</code> 等）根本不借用任何东西，但整个枚举仍然带着这个参数，因为类型是一个整体。你可以把 <code>TokenKind&lt;'src&gt;</code> 读成”一个 token 种类，如果它借用了东西，借的是 <code>'src</code>”。</p><section><h3>为什么是 <code>Clone</code> 而不是 <code>Copy</code>，为什么没有 <code>Eq</code><a href="#为什么是-clone-而不是-copy为什么没有-eq"><span>#</span></a></h3><p>第 1 篇我们给 <code>Span</code> 派生了 <code>Copy</code>。这里只派生了 <code>Clone, Debug, PartialEq</code>。两个不同的原因，各自有一个实验。</p><p>先试着加上 <code>Copy</code>（<code>src/bin/ch04_copy_cow.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>borrow</span><span>::</span></span><span>Cow</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Copy</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>4</div></div><div><span>enum</span><span> </span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Ident</span><span>(&amp;'</span><span>src</span><span> </span><span>str</span><span>),</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Str</span><span>(</span><span>Cow</span><span>&lt;'</span><span>src</span><span>, </span><span>str</span><span>&gt;),</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>"x"</span><span>));</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0204]: the trait `Copy` cannot be implemented for this type</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch04_copy_cow.rs:4:6</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>3 | #[derive(Clone, Copy, Debug)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                 ---- in this derive macro expansion</span></div></div><div><div><div>6</div></div><div><span>4 | enum TokenKind&lt;'src&gt; {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|      ^^^^^^^^^</span></div></div><div><div><div>8</div></div><div><span>5 |     Ident(&amp;'src str),</span></div></div><div><div><div>9</div></div><div><span>6 |     Str(Cow&lt;'src, str&gt;),</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|         -------------- this field does not implement `Copy`</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>For more information about this error, try `rustc --explain E0204`.</span></div></div><div><div><div>13</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_copy_cow") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Copy</code> 的意思是”按位复制就够了”，要求每个字段都是 <code>Copy</code>。<code>&amp;str</code> 是（复制一个指针和长度），<code>Cow&lt;'src, str&gt;</code> 不是，因为它可能装着一个 <code>String</code>，复制一个 <code>String</code> 需要分配新内存。这条规则第 2 篇已经解释过：<code>String</code> 不能是 <code>Copy</code>。所以 <code>TokenKind</code> 只能 <code>Clone</code>，需要复制时显式写 <code>.clone()</code>。</p><p><code>Cow</code> 是什么？“clone on write”，一个<strong>要么借用、要么拥有</strong>的枚举。实验文件 <code>src/bin/ch04_cow.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>borrow</span><span>::</span></span><span>Cow</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>unescape_newlines</span><span>(</span><span>raw</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Cow</span><span>&lt;'</span><span>_</span><span>, </span><span>str</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> </span><span>raw</span><span>.</span><span>contains</span><span>(</span><span>"</span><span>\\</span><span>n"</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Cow</span><span>::</span><span>Owned</span><span>(</span><span>raw</span><span>.</span><span>replace</span><span>(</span><span>"</span><span>\\</span><span>n"</span><span>, </span><span>"</span><span>\n</span><span>"</span><span>))</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>Cow</span><span>::</span><span>Borrowed</span><span>(</span><span>raw</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>plain</span><span><span> </span><span>=</span><span> </span></span><span>unescape_newlines</span><span>(</span><span>"plain"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>escaped</span><span><span> </span><span>=</span><span> </span></span><span>unescape_newlines</span><span>(</span><span>"a</span><span>\\</span><span>nb"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> (</span><span>label</span><span>, </span><span>cow</span><span>) </span><span>in</span><span> [(</span><span>"plain"</span><span>, &amp;</span><span>plain</span><span>), (</span><span>"escaped"</span><span>, &amp;</span><span>escaped</span><span>)] {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>kind</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>cow</span><span> {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>Cow</span><span>::</span><span>Borrowed</span><span>(</span><span>_</span><span>) =&gt; </span><span>"Borrowed"</span><span>,</span></div></div><div><div><div>17</div></div><div><span>            </span><span>Cow</span><span>::</span><span>Owned</span><span>(</span><span>_</span><span>) =&gt; </span><span>"Owned"</span><span>,</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>19</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{label:8} {kind:9} {cow:?}"</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;&amp;str&gt;()       = {}"</span><span>, </span><span>size_of</span><span>::&lt;&amp;</span><span>str</span><span>&gt;());</span></div></div><div><div><div>22</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;String&gt;()     = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>String</span><span>&gt;());</span></div></div><div><div><div>23</div></div><div><span>    </span><span>println!</span><span>(</span><span>"size_of::&lt;Cow&lt;str&gt;&gt;()   = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>Cow</span><span>&lt;'</span><span>static</span><span>, </span><span>str</span><span>&gt;&gt;());</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>plain    Borrowed  "plain"</span></div></div><div><div><div>2</div></div><div><span>escaped  Owned     "a\nb"</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;&amp;str&gt;()       = 16</span></div></div><div><div><div>4</div></div><div><span>size_of::&lt;String&gt;()     = 24</span></div></div><div><div><div>5</div></div><div><span>size_of::&lt;Cow&lt;str&gt;&gt;()   = 24</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>unescape_newlines</code> 的返回类型 <code>Cow&lt;'_, str&gt;</code> 说：这是一段文本，可能借自输入（没有转义的时候，一个字节都不复制），也可能是新分配的 <code>String</code>（有转义，需要把 <code>\n</code> 两个字符换成一个换行符）。调用者用 <code>match</code> 区分，或者干脆当 <code>&amp;str</code> 用（<code>Cow</code> 能自动当 <code>&amp;str</code> 用，这背后的 deref 强制转换第 5 篇讲）。这对字符串字面量正合适：绝大多数 Pebble 字符串没有转义，借；偶尔有 <code>\n</code>，拥有。</p><p>最后一行输出里有个有意思的细节：<code>size_of::&lt;Cow&lt;str&gt;&gt;()</code> 是 24，和 <code>String</code> 一样大，比 <code>&amp;str</code> 大 8 个字节。一个本该多出判别标签的枚举居然没有变大。我猜这是编译器利用了 <code>String</code> 内部某个不会出现的位模式来表示”借用”（Rust 里叫 niche 优化），但我没有去看布局，所以只算猜测。同样地，我们的 <code>TokenKind</code> 整体：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size_of::&lt;TokenKind&gt;() = 24</span></div></div><div><div><div>2</div></div><div><span>size_of::&lt;Token&gt;()     = 40</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（这是临时放在 <code>crates/pebble-lexer/examples/sizes.rs</code> 里跑的，内容就是两句 <code>size_of</code>，跑完删掉。）一个 token 40 字节：24 字节的 <code>TokenKind</code>，加上 12 字节的 <code>Span</code>（三个 <code>u32</code>），再补 4 字节对齐。</p><p>再试 <code>Eq</code>。<code>Float(f64)</code> 是 <code>TokenKind</code> 的一个变体，实验文件 <code>src/bin/ch04_float_eq.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>TokenKind</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Int</span><span>(</span><span>i64</span><span>),</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Float</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>TokenKind</span><span>::</span><span>Float</span><span>(</span><span>1.5</span><span>));</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: the trait bound `f64: Eq` is not satisfied</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch04_float_eq.rs:4:11</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>1 | #[derive(Clone, Debug, PartialEq, Eq)]</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                                   -- in this derive macro expansion</span></div></div><div><div><div>6</div></div><div><span>...</span></div></div><div><div><div>7</div></div><div><span>4 |     Float(f64),</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|           ^^^ the trait `Eq` is not implemented for `f64`</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>= help: the following other types implement trait `Eq`:</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>i128</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>i16</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>i32</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>i64</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>i8</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>isize</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>u128</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>u16</span></div></div><div><div><div>19</div></div><div><span><span>          </span></span><span>and 4 others</span></div></div><div><div><div>20</div></div><div><span>note: required by a bound in `std::cmp::AssertParamIsEq`</span></div></div><div><div><div>21</div></div><div><span><span> </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/cmp.rs:379:0</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>24</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_float_eq") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Eq</code> 在 <code>PartialEq</code> 的基础上额外保证”自己等于自己”，而 IEEE 754 里 <code>NaN != NaN</code>，所以 <code>f64</code> 没有 <code>Eq</code>，含有 <code>f64</code> 的类型也不能有。第 5 篇的 <code>Expr::Float</code> 会再撞这一次墙。注意编译器指出的不是 <code>derive</code> 那一行，而是<strong>字段</strong>：<code>derive</code> 生成的代码要求每个字段都满足条件，哪个不满足就指哪个。</p></section><section><h3><code>keyword</code> 和 <code>describe</code><a href="#keyword-和-describe"><span>#</span></a></h3><p><code>TokenKind</code> 上有两个函数。<code>keyword</code> 把一个已经读出来的单词变成关键字 token，是我们要写的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Maps an identifier to its keyword token, if it is one.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// The classic keyword table: the lexer reads one generic `Ident` and</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// decides afterwards whether it was really a keyword.</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>keyword</span><span>(</span><span>word</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Self</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>Some</span><span>(</span><span>match</span><span> </span><span>word</span><span> {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>"let"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Let</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>"fn"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Fn</span><span>,</span></div></div><div><div><div>10</div></div><div><span>            </span><span>"if"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>If</span><span>,</span></div></div><div><div><div>11</div></div><div><span>            </span><span>"else"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Else</span><span>,</span></div></div><div><div><div>12</div></div><div><span>            </span><span>"while"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>While</span><span>,</span></div></div><div><div><div>13</div></div><div><span>            </span><span>"for"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>For</span><span>,</span></div></div><div><div><div>14</div></div><div><span>            </span><span>"in"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>In</span><span>,</span></div></div><div><div><div>15</div></div><div><span>            </span><span>"return"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Return</span><span>,</span></div></div><div><div><div>16</div></div><div><span>            </span><span>"break"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Break</span><span>,</span></div></div><div><div><div>17</div></div><div><span>            </span><span>"continue"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Continue</span><span>,</span></div></div><div><div><div>18</div></div><div><span>            </span><span>"true"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>True</span><span>,</span></div></div><div><div><div>19</div></div><div><span>            </span><span>"false"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>False</span><span>,</span></div></div><div><div><div>20</div></div><div><span>            </span><span>"nil"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Nil</span><span>,</span></div></div><div><div><div>21</div></div><div><span>            </span><span>"and"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>And</span><span>,</span></div></div><div><div><div>22</div></div><div><span>            </span><span>"or"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Or</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>"not"</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Not</span><span>,</span></div></div><div><div><div>24</div></div><div><span>            </span><span>_</span><span> =&gt; </span><span>return</span><span> </span><span>None</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Option&lt;Self&gt;</code> 里的 <code>Self</code> 就是 <code>TokenKind&lt;'_&gt;</code>。<code>Some(match word { ... })</code> 是个小技巧：每个分支产出一个 <code>TokenKind</code>，最后一个分支 <code>_ =&gt; return None</code> 直接从函数返回，不产出值（<code>return</code> 的类型是 <code>!</code>，能放进任何类型的位置），其余分支被包进外面的 <code>Some</code>。</p><p>为什么先读一个通用的 <code>Ident</code> 再回头查表，而不是在字符层面区分关键字？因为 <code>iffy</code> 以 <code>if</code> 开头，<code>letter</code> 以 <code>let</code> 开头，在字符层面判断很容易出错。我们有个测试 <code>lexes_keywords_and_identifiers</code> 专门盯着 <code>iffy</code>。这是词法分析器的经典做法：先把整个单词读完，再整体查表。</p><p><code>describe</code>（给每个变体一个简短描述，解析器报错时用）是几十行没有新语法的 <code>match</code>，<code>Display</code> 的实现是 11 行，它们不值得你敲，直接从 tag 里取：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>-C</span><span> </span><span>~/pebble-tag</span><span> </span><span>show</span><span> </span><span>tutorial-v1:crates/pebble-lexer/src/lib.rs</span><span> | </span><span>sed</span><span> </span><span>-n</span><span> </span><span>127,176p</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条命令输出的是 <code>impl TokenKind&lt;'_&gt; {</code> 开头到 <code>describe</code> 函数结束的那一段，末尾带一个空行。把它放在 <code>TokenKind</code> 枚举后面（中间空一行），接着是上面写的 <code>keyword</code>，再用一个 <code>}</code> 结束这个 <code>impl</code> 块，合起来就是 tag 文件的第 127 到 203 行。<code>Display</code> 同理：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>-C</span><span> </span><span>~/pebble-tag</span><span> </span><span>show</span><span> </span><span>tutorial-v1:crates/pebble-lexer/src/lib.rs</span><span> | </span><span>sed</span><span> </span><span>-n</span><span> </span><span>205,215p</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不过 <code>describe</code> 里有两件事值得一看。<code>pub const fn describe(&amp;self) -&gt; &amp;'static str</code>：<code>const fn</code> 第 1 篇讲过，返回值是 <code>'static</code>，刚刚讲过。<code>impl TokenKind&lt;'_&gt;</code> 里的 <code>'_</code> 是”这个 impl 对任何 <code>'src</code> 成立，我不需要用到这个名字”，比写 <code>impl&lt;'src&gt; TokenKind&lt;'src&gt;</code> 简洁。</p></section></section>
<section><h2>游标：<code>Token</code> 与 <code>Lexer</code><a href="#游标token-与-lexer"><span>#</span></a></h2><p>接着 <code>Token</code> 和 <code>Lexer</code> 的结构（文件第 217 到 233 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A token: a kind plus where it came from.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>Debug</span><span>, </span><span>PartialEq</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>kind</span><span>: </span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/// The zero-copy lexer.</span></div></div><div><div><div>9</div></div><div><span>///</span></div></div><div><div><div>10</div></div><div><span>/// It borrows the source for its whole lifetime, which is why the tokens it</span></div></div><div><div><div>11</div></div><div><span>/// produces can borrow from the same string.</span></div></div><div><div><div>12</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>source</span><span>: </span><span>SourceId</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>src</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pos</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>16</div></div><div><span>    </span><span>done</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Lexer</code> 比脚手架多了一个 <code>done</code> 字段，用于记录”已经吐出过 <code>Eof</code>”，下面解释 <code>Iterator</code> 的时候会用到。我们先写构造函数和两个查询（第 235 到 257 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span>&lt;'</span><span>src</span><span>&gt; </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Creates a lexer over `src`, tagging every span with `source`.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>source</span><span>: </span><span>SourceId</span><span>, </span><span>src</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>source</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>src</span><span>,</span></div></div><div><div><div>8</div></div><div><span>            </span><span>pos</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>done</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>/// Byte offset the lexer has reached.</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>position</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.pos</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>    </span><span>/// Whether the underlying input has been exhausted.</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>21</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>is_at_end</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>self</span><span><span>.pos </span><span>&gt;=</span><span> </span></span><span>self</span><span>.src.</span><span>len</span><span>()</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>const fn new</code>：词法分析器可以在编译期构造，没有什么特别原因，只是能写 <code>const</code> 就写，<code>Span::new</code> 同理。两个 <code>#[must_use]</code> 防止调用者算了个值又忘了用。</p><p>然后是脚手架里那四个游标函数，加上一个 <code>peek_second</code>（偷看第二个字符），位置在文件的 259 到 281 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>rest</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;'</span><span>src</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.src[</span><span>self</span><span>.pos..]</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>peek</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>char</span><span>&gt; {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>self</span><span>.</span><span>rest</span><span>().</span><span>chars</span><span>().</span><span>next</span><span>()</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>fn</span><span> </span><span>peek_second</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>char</span><span>&gt; {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>chars</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>rest</span><span>().</span><span>chars</span><span>();</span></div></div><div><div><div>11</div></div><div><span>        </span><span>chars</span><span>.</span><span>next</span><span>();</span></div></div><div><div><div>12</div></div><div><span>        </span><span>chars</span><span>.</span><span>next</span><span>()</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>bump</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>char</span><span>&gt; {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>ch</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>()?;</span></div></div><div><div><div>17</div></div><div><span>        </span><span>self</span><span><span>.pos </span><span>+=</span><span> </span></span><span>ch</span><span>.</span><span>len_utf8</span><span>();</span></div></div><div><div><div>18</div></div><div><span>        </span><span>Some</span><span>(</span><span>ch</span><span>)</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>    </span><span>fn</span><span> </span><span>span_from</span><span>(&amp;</span><span>self</span><span>, </span><span>start</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>22</div></div><div><span>        </span><span>Span</span><span>::</span><span>new</span><span>(</span><span>self</span><span>.source, </span><span>start</span><span> </span><span>as</span><span> </span><span>u32</span><span>, </span><span>self</span><span>.pos </span><span>as</span><span> </span><span>u32</span><span>)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3><code>char</code> 与 UTF-8<a href="#char-与-utf-8"><span>#</span></a></h3><p>这里每一行都和 UTF-8 有关。<code>peek</code> 用 <code>chars().next()</code> 取第一个<strong>字符</strong>（<code>char</code>，一个 Unicode 标量值，4 字节）；<code>bump</code> 把 <code>pos</code> 前进 <code>ch.len_utf8()</code>，也就是这个字符编码成 UTF-8 占几个字节。实验文件 <code>src/bin/ch04_patterns.rs</code> 里我顺便打印了几个字符的长度：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>classify</span><span>(</span><span>ch</span><span>: </span><span>char</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>match</span><span> </span><span>ch</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>'0'</span><span>..=</span><span>'9'</span><span> =&gt; </span><span>"digit"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>'_'</span><span><span> </span><span>|</span><span> </span></span><span>'a'</span><span>..=</span><span>'z'</span><span><span> </span><span>|</span><span> </span></span><span>'A'</span><span>..=</span><span>'Z'</span><span> =&gt; </span><span>"ident start"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>        </span><span>c</span><span> </span><span>if</span><span> </span><span>c</span><span>.</span><span>is_alphabetic</span><span>() =&gt; </span><span>"unicode letter (guard)"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>c</span><span> </span><span>if</span><span> </span><span>c</span><span>.</span><span>is_whitespace</span><span>() =&gt; </span><span>"space"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>_</span><span> =&gt; </span><span>"other"</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> </span><span>ch</span><span> </span><span>in</span><span> [</span><span>'7'</span><span>, </span><span>'_'</span><span>, </span><span>'x'</span><span>, </span><span>'中'</span><span>, </span><span>' '</span><span>, </span><span>'@'</span><span>] {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{ch:?} -&gt; {}"</span><span>, </span><span>classify</span><span>(</span><span>ch</span><span>));</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>stack</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>];</span></div></div><div><div><div>16</div></div><div><span>    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>top</span><span><span>) </span><span>=</span><span> </span></span><span>stack</span><span>.</span><span>pop</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>print!</span><span>(</span><span>"{top} "</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>    </span><span>println!</span><span>();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>"123"</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;());</span></div></div><div><div><div>21</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>"99999999999999999999"</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;());</span></div></div><div><div><div>22</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>"99999999999999999999"</span><span>.</span><span>parse</span><span>::&lt;</span><span>f64</span><span>&gt;());</span></div></div><div><div><div>23</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>"1.5"</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;());</span></div></div><div><div><div>24</div></div><div><span>    </span><span>println!</span><span>(</span><span>"'a'.len_utf8() = {}, '中'.len_utf8() = {}, '🦀'.len_utf8() = {}"</span><span>, </span><span>'a'</span><span>.</span><span>len_utf8</span><span>(), </span><span>'中'</span><span>.</span><span>len_utf8</span><span>(), </span><span>'🦀'</span><span>.</span><span>len_utf8</span><span>());</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>'7' -&gt; digit</span></div></div><div><div><div>2</div></div><div><span>'_' -&gt; ident start</span></div></div><div><div><div>3</div></div><div><span>'x' -&gt; ident start</span></div></div><div><div><div>4</div></div><div><span>'中' -&gt; unicode letter (guard)</span></div></div><div><div><div>5</div></div><div><span>' ' -&gt; space</span></div></div><div><div><div>6</div></div><div><span>'@' -&gt; other</span></div></div><div><div><div>7</div></div><div><span>3 2 1</span></div></div><div><div><div>8</div></div><div><span>Ok(123)</span></div></div><div><div><div>9</div></div><div><span>Err(ParseIntError { kind: PosOverflow })</span></div></div><div><div><div>10</div></div><div><span>Ok(1e20)</span></div></div><div><div><div>11</div></div><div><span>Err(ParseIntError { kind: InvalidDigit })</span></div></div><div><div><div>12</div></div><div><span>'a'.len_utf8() = 1, '中'.len_utf8() = 3, '🦀'.len_utf8() = 4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一行：<code>'a'</code> 占 1 字节，<code>'中'</code> 占 3 字节，<code>'🦀'</code> 占 4 字节。如果 <code>bump</code> 总是 <code>+= 1</code>，遇到中文标识符之后 <code>pos</code> 就会落在一个字符的中间，下一次 <code>&amp;self.src[self.pos..]</code> 切片会 panic（第 2 篇的 <code>ch02_utf8</code> 演示过）。用 <code>len_utf8()</code> 推进，<code>pos</code> 永远停在字符边界上，这就是为什么整个词法分析器里的切片都不会出事。</p><p><code>span_from</code> 里的 <code>start as u32</code>：把 <code>usize</code> 截成 <code>u32</code>。<code>Span</code> 的字段是 <code>u32</code>（第 1 篇的选择），所以超过 4 GiB 的源文件会被截断。Pebble 的源文件不会那么大，我选择不为此加检查；如果你介意，这是一个有意义的练习。</p></section><section><h3><code>skip_trivia</code>：嵌套注释<a href="#skip_trivia嵌套注释"><span>#</span></a></h3><p>词法分析器要跳过空白和注释，统称 trivia。这是文件第 283 到 331 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Skips whitespace and comments, returning at most one diagnostic.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>skip_trivia</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>loop</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span>.</span><span>peek</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>                </span><span>Some</span><span>(</span><span>ch</span><span>) </span><span>if</span><span> </span><span>ch</span><span>.</span><span>is_whitespace</span><span>() =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>                </span><span>Some</span><span>(</span><span>'/'</span><span>) </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek_second</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'/'</span><span>) =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>ch</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>                        </span><span>if</span><span> </span><span>ch</span><span><span> </span><span>==</span><span> </span></span><span>'</span><span>\n</span><span>'</span><span> {</span></div></div><div><div><div>11</div></div><div><span>                            </span><span>break</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>                        </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>14</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>                </span><span>Some</span><span>(</span><span>'/'</span><span>) </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek_second</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'*'</span><span>) =&gt; {</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.pos;</span></div></div><div><div><div>18</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>19</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>let</span><span> </span><span>mut</span><span> </span><span>depth</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>usize</span><span>;</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>while</span><span> </span><span>depth</span><span><span> </span><span>&gt;</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>22</div></div><div><span>                        </span><span>match</span><span> </span><span>self</span><span>.</span><span>peek</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>                            </span><span>None</span><span> =&gt; {</span></div></div><div><div><div>24</div></div><div><span>                                </span><span>return</span><span> </span><span>Some</span><span>(</span></div></div><div><div><div>25</div></div><div><span>                                    </span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"unterminated block comment"</span><span>)</span></div></div><div><div><div>26</div></div><div><span><span>                                        </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0002"</span><span>)</span></div></div><div><div><div>27</div></div><div><span><span>                                        </span></span><span>.</span><span>with_span</span><span>(</span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>)),</span></div></div><div><div><div>28</div></div><div><span><span>                                </span></span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>                            </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>                            </span><span>Some</span><span>(</span><span>'/'</span><span>) </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek_second</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'*'</span><span>) =&gt; {</span></div></div><div><div><div>31</div></div><div><span>                                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>32</div></div><div><span>                                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>33</div></div><div><span>                                </span><span>depth</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>34</div></div><div><span><span>                            </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span>                            </span><span>Some</span><span>(</span><span>'*'</span><span>) </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek_second</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'/'</span><span>) =&gt; {</span></div></div><div><div><div>36</div></div><div><span>                                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>37</div></div><div><span>                                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>38</div></div><div><span>                                </span><span>depth</span><span><span> </span><span>-=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>                            </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span>                            </span><span>Some</span><span>(</span><span>_</span><span>) =&gt; {</span></div></div><div><div><div>41</div></div><div><span>                                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>42</div></div><div><span><span>                            </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span>                </span><span>_</span><span> =&gt; </span><span>return</span><span> </span><span>None</span><span>,</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>有三处值得看：</p><ul>
<li><code>Some(ch) if ch.is_whitespace() =&gt; ...</code> 是<strong>匹配守卫</strong>：模式 <code>Some(ch)</code> 先匹配，再检查 <code>if</code> 后面的条件，不满足就尝试下一个分支。<code>Some('/') if self.peek_second() == Some('/')</code> 同理，它把”两个字符一起看”这件事写进了分支条件。</li>
<li><code>while let Some(ch) = self.peek() { ... }</code> 是”只要还能匹配就继续”：每圈重新求值右边，<code>Some</code> 就解构出 <code>ch</code> 进循环，<code>None</code> 就退出。它是 <code>loop { match x { Some(..) =&gt; ..., None =&gt; break } }</code> 的缩写。</li>
<li>嵌套块注释用一个 <code>depth</code> 计数：<code>/*</code> 加一，<code>*/</code> 减一，到 0 结束。输入结束时 <code>depth</code> 还大于 0 就返回一个”未终止的块注释”诊断，错误码是 <code>E0002</code>。</li>
</ul><p>注意函数返回 <code>Option&lt;Diagnostic&gt;</code>，不是 <code>Result</code>：它的”成功”是什么都不返回，所以 <code>None</code> 代表一切正常。这看起来有点反直觉，我后来也没想到更好的表示。</p></section></section>
<section><h2><code>next_token</code>：模式匹配的练习场<a href="#next_token模式匹配的练习场"><span>#</span></a></h2><p>这是文件里最长的函数（第 333 到 420 行），也是模式匹配的各种形态一次出齐：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Lexes the next token, or `None` once the stream is finished.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// A lexer that has already emitted [`TokenKind::Eof`] returns `None`</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// forever after, so the [`Iterator`] impl terminates.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>next_token</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Result</span><span>&lt;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt;, </span><span>Diagnostic</span><span>&gt;&gt; {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.done {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>diagnostic</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>skip_trivia</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>return</span><span> </span><span>Some</span><span>(</span><span>Err</span><span>(</span><span>diagnostic</span><span>));</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span>.</span><span>is_at_end</span><span>() {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>self</span><span><span>.done </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>14</div></div><div><span>            </span><span>return</span><span> </span><span>Some</span><span>(</span><span>Ok</span><span>(</span><span>Token</span><span> {</span></div></div><div><div><div>15</div></div><div><span>                </span><span>kind</span><span>: </span><span>TokenKind</span><span>::</span><span>Eof</span><span>,</span></div></div><div><div><div>16</div></div><div><span>                </span><span>span</span><span>: </span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>self</span><span>.pos),</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>}));</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.pos;</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>ch</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>bump</span><span>()?;</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>kind</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>ch</span><span> {</span></div></div><div><div><div>23</div></div><div><span>            </span><span>'('</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>LParen</span><span>,</span></div></div><div><div><div>24</div></div><div><span>            </span><span>')'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>RParen</span><span>,</span></div></div><div><div><div>25</div></div><div><span>            </span><span>'{'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>LBrace</span><span>,</span></div></div><div><div><div>26</div></div><div><span>            </span><span>'}'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>RBrace</span><span>,</span></div></div><div><div><div>27</div></div><div><span>            </span><span>'['</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>LBracket</span><span>,</span></div></div><div><div><div>28</div></div><div><span>            </span><span>']'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>RBracket</span><span>,</span></div></div><div><div><div>29</div></div><div><span>            </span><span>'#'</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'{'</span><span>) =&gt; {</span></div></div><div><div><div>30</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>31</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>HashBrace</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>            </span><span>','</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Comma</span><span>,</span></div></div><div><div><div>34</div></div><div><span>            </span><span>':'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Colon</span><span>,</span></div></div><div><div><div>35</div></div><div><span>            </span><span>';'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Semicolon</span><span>,</span></div></div><div><div><div>36</div></div><div><span>            </span><span>'+'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Plus</span><span>,</span></div></div><div><div><div>37</div></div><div><span>            </span><span>'-'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Minus</span><span>,</span></div></div><div><div><div>38</div></div><div><span>            </span><span>'*'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Star</span><span>,</span></div></div><div><div><div>39</div></div><div><span>            </span><span>'/'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Slash</span><span>,</span></div></div><div><div><div>40</div></div><div><span>            </span><span>'%'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Percent</span><span>,</span></div></div><div><div><div>41</div></div><div><span>            </span><span>'='</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'='</span><span>) =&gt; {</span></div></div><div><div><div>42</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>43</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Eq</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>            </span><span>'='</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Assign</span><span>,</span></div></div><div><div><div>46</div></div><div><span>            </span><span>'!'</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'='</span><span>) =&gt; {</span></div></div><div><div><div>47</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>48</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>NotEq</span></div></div><div><div><div>49</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span>            </span><span>'!'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Not</span><span>,</span></div></div><div><div><div>51</div></div><div><span>            </span><span>'&lt;'</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'='</span><span>) =&gt; {</span></div></div><div><div><div>52</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>53</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>LtEq</span></div></div><div><div><div>54</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>55</div></div><div><span>            </span><span>'&lt;'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Lt</span><span>,</span></div></div><div><div><div>56</div></div><div><span>            </span><span>'&gt;'</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'='</span><span>) =&gt; {</span></div></div><div><div><div>57</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>58</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>GtEq</span></div></div><div><div><div>59</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>60</div></div><div><span>            </span><span>'&gt;'</span><span> =&gt; </span><span>TokenKind</span><span>::</span><span>Gt</span><span>,</span></div></div><div><div><div>61</div></div><div><span>            </span><span>'&amp;'</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'&amp;'</span><span>) =&gt; {</span></div></div><div><div><div>62</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>63</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>And</span></div></div><div><div><div>64</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>65</div></div><div><span>            </span><span>'|'</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek</span><span><span>() </span><span>==</span><span> </span></span><span>Some</span><span>(</span><span>'|'</span><span>) =&gt; {</span></div></div><div><div><div>66</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>67</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Or</span></div></div><div><div><div>68</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>69</div></div><div><span>            </span><span>'"'</span><span> =&gt; </span><span>match</span><span> </span><span>self</span><span>.</span><span>lex_string</span><span>(</span><span>start</span><span>) {</span></div></div><div><div><div>70</div></div><div><span>                </span><span>Ok</span><span>(</span><span>kind</span><span>) =&gt; </span><span>kind</span><span>,</span></div></div><div><div><div>71</div></div><div><span>                </span><span>Err</span><span>(</span><span>diagnostic</span><span>) =&gt; </span><span>return</span><span> </span><span>Some</span><span>(</span><span>Err</span><span>(</span><span>diagnostic</span><span>)),</span></div></div><div><div><div>72</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>73</div></div><div><span>            </span><span>'0'</span><span>..=</span><span>'9'</span><span> =&gt; </span><span>self</span><span>.</span><span>lex_number</span><span>(</span><span>start</span><span>),</span></div></div><div><div><div>74</div></div><div><span>            </span><span>ch</span><span> </span><span>if</span><span> </span><span>ch</span><span><span> </span><span>==</span><span> </span></span><span>'_'</span><span><span> </span><span>||</span><span> </span></span><span>ch</span><span>.</span><span>is_alphabetic</span><span>() =&gt; </span><span>self</span><span>.</span><span>lex_identifier</span><span>(</span><span>start</span><span>),</span></div></div><div><div><div>75</div></div><div><span>            </span><span>other</span><span> =&gt; {</span></div></div><div><div><div>76</div></div><div><span>                </span><span>return</span><span> </span><span>Some</span><span>(</span><span>Err</span><span>(</span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>format!</span><span>(</span></div></div><div><div><div>77</div></div><div><span>                    </span><span>"unexpected character `{other}`"</span></div></div><div><div><div>78</div></div><div><span><span>                </span></span><span>))</span></div></div><div><div><div>79</div></div><div><span><span>                </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0001"</span><span>)</span></div></div><div><div><div>80</div></div><div><span><span>                </span></span><span>.</span><span>with_span</span><span>(</span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>))));</span></div></div><div><div><div>81</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>82</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>        </span><span>Some</span><span>(</span><span>Ok</span><span>(</span><span>Token</span><span> {</span></div></div><div><div><div>85</div></div><div><span>            </span><span>kind</span><span>,</span></div></div><div><div><div>86</div></div><div><span>            </span><span>span</span><span>: </span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>),</span></div></div><div><div><div>87</div></div><div><span><span>        </span></span><span>}))</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>按模式的种类过一遍：</p><ul>
<li><strong>字面量模式</strong>：<code>'(' =&gt; TokenKind::LParen</code>，一个字符对一个 token。</li>
<li><strong>守卫</strong>：<code>'=' if self.peek() == Some('=')</code> 先看下一个字符是不是 <code>=</code>，是的话再吃掉它，产出 <code>Eq</code>。顺序很重要：带守卫的 <code>'='</code> 在前，不带守卫的 <code>'=' =&gt; Assign</code> 在后，输入 <code>==</code> 时第一个先匹配。测试 <code>multi_char_operators_win_over_single_char</code> 就在盯这个顺序。</li>
<li><strong>范围模式</strong>：<code>'0'..='9' =&gt; self.lex_number(start)</code> 匹配这个闭区间里的任何字符。我最初写的是 <code>ch if ch.is_ascii_digit()</code>，守卫版本也对，但范围模式更短，也更直接。</li>
<li><strong>带绑定的守卫</strong>：<code>ch if ch == '_' || ch.is_alphabetic()</code>，把匹配到的字符起名为 <code>ch</code>，守卫里用。标识符不能用范围模式，因为 <code>is_alphabetic</code> 是 Unicode 意义上的”字母”，包括中文和希腊文。<code>src/bin/ch04_patterns.rs</code> 打印了 <code>'中' -&gt; unicode letter (guard)</code>，也就是说 <code>let 变量 = 1;</code> 在 Pebble 里是合法的。</li>
<li><strong>或模式</strong> <code>|</code>：<code>next_token</code> 里没有，下一节的 <code>lex_number</code> 里有。</li>
<li><strong>兜底</strong>：<code>other =&gt; { return Some(Err(...)) }</code> 绑定剩下的所有字符并返回一个错误诊断，错误码 <code>E0001</code>。</li>
</ul><p><code>ch04_patterns</code> 里的 <code>classify</code> 是这些模式的一个独立小演示，上面的输出你已经看到了。</p><p>再看 <code>'"' =&gt; match self.lex_string(start) { Ok(kind) =&gt; kind, Err(diagnostic) =&gt; return Some(Err(diagnostic)) }</code>：一个 <code>match</code> 的某个分支本身是一个 <code>match</code>，错误分支用 <code>return</code> 提前退出，正常分支的值变成外层 <code>let kind = match ch { ... }</code> 的值。这个”在表达式里提前返回”的写法，第 7 篇会被 <code>?</code> 取代。这里不能用 <code>?</code>，因为 <code>next_token</code> 返回的是 <code>Option&lt;Result&lt;..&gt;&gt;</code>，而 <code>lex_string</code> 返回的 <code>Result</code> 要包进 <code>Some(Err(..))</code>，类型对不上。</p></section>
<section><h2>数字、标识符、字符串<a href="#数字标识符字符串"><span>#</span></a></h2><p>文件的下一段（第 422 到 459 行）是数字和标识符：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>lex_number</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>start</span><span>: </span><span>usize</span><span>) -&gt; </span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>is_float</span><span><span> </span><span>=</span><span> </span></span><span>false</span><span>;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>ch</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>match</span><span> </span><span>ch</span><span> {</span></div></div><div><div><div>5</div></div><div><span>                </span><span>'0'</span><span>..=</span><span>'9'</span><span><span> </span><span>|</span><span> </span></span><span>'_'</span><span> =&gt; {</span></div></div><div><div><div>6</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>                </span><span>'.'</span><span> </span><span>if</span><span> </span><span>self</span><span>.</span><span>peek_second</span><span>().</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>c</span><span><span>|</span><span> </span></span><span>c</span><span>.</span><span>is_ascii_digit</span><span>()) =&gt; {</span></div></div><div><div><div>9</div></div><div><span>                    </span><span>is_float</span><span><span> </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>10</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>                </span><span>_</span><span> =&gt; </span><span>break</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span><span> &amp;</span></span><span>self</span><span>.src[</span><span>start</span><span>..</span><span>self</span><span>.pos];</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>cleaned</span><span>: </span><span>String</span><span><span> </span><span>=</span><span> </span></span><span>text</span><span>.</span><span>chars</span><span>().</span><span>filter</span><span><span>(</span><span>|</span><span>&amp;</span></span><span>c</span><span><span>|</span><span> </span></span><span>c</span><span><span> </span><span>!=</span><span> </span></span><span>'_'</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>17</div></div><div><span>        </span><span>if</span><span> </span><span>is_float</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Float</span><span>(</span><span>cleaned</span><span>.</span><span>parse</span><span>().</span><span>unwrap_or</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span>))</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>match</span><span> </span><span>cleaned</span><span>.</span><span>parse</span><span>::&lt;</span><span>i64</span><span>&gt;() {</span></div></div><div><div><div>21</div></div><div><span>                </span><span>Ok</span><span>(</span><span>value</span><span>) =&gt; </span><span>TokenKind</span><span>::</span><span>Int</span><span>(</span><span>value</span><span>),</span></div></div><div><div><div>22</div></div><div><span>                </span><span>// Too big for i64: fall back to a float rather than failing.</span></div></div><div><div><div>23</div></div><div><span>                </span><span>Err</span><span>(</span><span>_</span><span>) =&gt; </span><span>TokenKind</span><span>::</span><span>Float</span><span>(</span><span>cleaned</span><span>.</span><span>parse</span><span>().</span><span>unwrap_or</span><span><span>(</span><span>f64</span><span>::</span></span><span>NAN</span><span>)),</span></div></div><div><div><div>24</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>    </span><span>fn</span><span> </span><span>lex_identifier</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>start</span><span>: </span><span>usize</span><span>) -&gt; </span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>29</div></div><div><span>        </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>ch</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>() {</span></div></div><div><div><div>30</div></div><div><span>            </span><span>if</span><span> </span><span>ch</span><span><span> </span><span>==</span><span> </span></span><span>'_'</span><span><span> </span><span>||</span><span> </span></span><span>ch</span><span>.</span><span>is_alphanumeric</span><span>() {</span></div></div><div><div><div>31</div></div><div><span>                </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>33</div></div><div><span>                </span><span>break</span><span>;</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span>        </span><span>let</span><span> </span><span>word</span><span><span> </span><span>=</span><span> &amp;</span></span><span>self</span><span>.src[</span><span>start</span><span>..</span><span>self</span><span>.pos];</span></div></div><div><div><div>37</div></div><div><span>        </span><span>TokenKind</span><span>::</span><span>keyword</span><span>(</span><span>word</span><span>).</span><span>unwrap_or</span><span><span>(</span><span>TokenKind</span><span>::</span></span><span>Ident</span><span>(</span><span>word</span><span>))</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>lex_number</code> 是第 2 篇没教、但这里我们真的需要的东西。它的核心是 <code>while let</code> 里的一个 <code>match</code>。我最初写的是一条 <code>if / else if / else</code> 链：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span> </span><span>ch</span><span>.</span><span>is_ascii_digit</span><span><span>() </span><span>||</span><span> </span></span><span>ch</span><span><span> </span><span>==</span><span> </span></span><span>'_'</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>3</div></div><div><span>} </span><span>else</span><span> </span><span>if</span><span> </span><span>ch</span><span><span> </span><span>==</span><span> </span></span><span>'.'</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>self</span><span>.</span><span>peek_second</span><span>().</span><span>is_some_and</span><span><span>(</span><span>|</span></span><span>c</span><span><span>|</span><span> </span></span><span>c</span><span>.</span><span>is_ascii_digit</span><span>()) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>is_float</span><span><span> </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>6</div></div><div><span>} </span><span>else</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>break</span><span>;</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>后来改成上面 <code>match</code> 的形式。行为一模一样，但<strong>或模式</strong>（<code>'0'..='9' | '_'</code>）把”数字或下划线”写成了一个分支，<code>'.' if ...</code> 是一个守卫分支，<code>_ =&gt; break</code> 是兜底。<code>'.' if self.peek_second().is_some_and(...)</code> 的守卫要求小数点后面紧跟一个数字：<code>1.5</code> 是浮点数，<code>1.foo</code> 是整数 1 加一个点再加标识符（点在 Pebble 里什么也不是，会报 <code>E0001</code>），<code>1.</code> 也同样。</p><p>几个小语法：</p><ul>
<li><code>text.chars().filter(|&amp;c| c != '_').collect()</code>：<code>1_000</code> 允许下划线分隔，解析前先去掉。<code>|&amp;c| ...</code> 是第 2 篇的”参数里的模式”：<code>filter</code> 给闭包的是 <code>&amp;char</code>，写 <code>|&amp;c|</code> 直接把引用拆掉。<code>.collect()</code> 的目标类型由左边的 <code>let cleaned: String</code> 决定，一串 <code>char</code> 可以收集成 <code>String</code>。</li>
<li><code>cleaned.parse::&lt;i64&gt;()</code>：<code>parse</code> 对任何实现了 <code>FromStr</code> 的类型都能用，要解析成哪个由类型决定。当类型没法从上下文推出来时，就用 <code>::&lt;i64&gt;</code> 显式指定，因为这个形状像一条鱼，所以叫 <strong>turbofish</strong>。<code>Float</code> 分支里的 <code>cleaned.parse().unwrap_or(f64::NAN)</code> 就没有写 turbofish：<code>TokenKind::Float(...)</code> 的参数类型是 <code>f64</code>，编译器自己推出来了。</li>
<li>太大放不进 <code>i64</code> 的整数怎么办？<code>ch04_patterns</code> 里我也试了：<code>"99999999999999999999".parse::&lt;i64&gt;()</code> 返回 <code>Err(ParseIntError { kind: PosOverflow })</code>，而同一个字符串 <code>parse::&lt;f64&gt;()</code> 返回 <code>Ok(1e20)</code>。所以我选择退化成浮点数，而不是报错。这是个设计选择，不是 Rust 的要求。<code>"1.5".parse::&lt;i64&gt;()</code> 是 <code>Err(.. InvalidDigit ..)</code>，所以必须先靠 <code>is_float</code> 区分。</li>
</ul><p><code>lex_identifier</code> 平淡无奇：继续吃字母数字下划线，然后 <code>&amp;self.src[start..self.pos]</code> 取出整个单词，查关键字表，查不到就是 <code>Ident(word)</code>。重点在返回类型 <code>TokenKind&lt;'src&gt;</code>：切片 <code>&amp;self.src[..]</code> 借自 <code>self.src</code>（那个 <code>&amp;'src str</code>），不借自 <code>&amp;mut self</code>，所以 <code>Ident(word)</code> 的区域是 <code>'src</code>。这正是前面那个实验里第二个错误想保护的东西。</p><section><h3><code>lex_string</code>：<code>Cow</code> 真正派上用场<a href="#lex_stringcow-真正派上用场"><span>#</span></a></h3><p>字符串字面量是这样处理的（文件第 461 到 498 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>fn</span><span> </span><span>lex_string</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>start</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt;, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>        </span><span>let</span><span> </span><span>body_start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.pos;</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>has_escape</span><span><span> </span><span>=</span><span> </span></span><span>false</span><span>;</span></div></div><div><div><div>4</div></div><div><span>        </span><span>loop</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span>.</span><span>peek</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>                </span><span>None</span><span> =&gt; {</span></div></div><div><div><div>7</div></div><div><span>                    </span><span>return</span><span> </span><span>Err</span><span>(</span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"unterminated string literal"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>                        </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0003"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>                        </span></span><span>.</span><span>with_span</span><span>(</span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>)));</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>                </span><span>// Recovery point: Pebble strings are single-line, so a newline</span></div></div><div><div><div>12</div></div><div><span>                </span><span>// ends the (broken) literal without swallowing the next line.</span></div></div><div><div><div>13</div></div><div><span>                </span><span>Some</span><span>(</span><span>'</span><span>\n</span><span>'</span><span>) =&gt; {</span></div></div><div><div><div>14</div></div><div><span>                    </span><span>return</span><span> </span><span>Err</span><span>(</span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"unterminated string literal"</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>                        </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0003"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>                        </span></span><span>.</span><span>with_span</span><span>(</span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>)));</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>                </span><span>Some</span><span>(</span><span>'"'</span><span>) =&gt; </span><span>break</span><span>,</span></div></div><div><div><div>19</div></div><div><span>                </span><span>Some</span><span>(</span><span>'</span><span>\\</span><span>'</span><span>) =&gt; {</span></div></div><div><div><div>20</div></div><div><span>                    </span><span>has_escape</span><span><span> </span><span>=</span><span> </span></span><span>true</span><span>;</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>22</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>                </span><span>Some</span><span>(</span><span>_</span><span>) =&gt; {</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>raw</span><span><span> </span><span>=</span><span> &amp;</span></span><span>self</span><span>.src[</span><span>body_start</span><span>..</span><span>self</span><span>.pos];</span></div></div><div><div><div>30</div></div><div><span>        </span><span>self</span><span>.</span><span>bump</span><span>(); </span><span>// closing quote.</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>        </span><span>if</span><span> </span><span>has_escape</span><span> {</span></div></div><div><div><div>33</div></div><div><span>            </span><span>Ok</span><span>(</span><span>TokenKind</span><span>::</span><span>Str</span><span><span>(</span><span>Cow</span><span>::</span></span><span>Owned</span><span>(</span><span>unescape</span><span>(</span><span>raw</span><span>))))</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>35</div></div><div><span>            </span><span>Ok</span><span>(</span><span>TokenKind</span><span>::</span><span>Str</span><span><span>(</span><span>Cow</span><span>::</span></span><span>Borrowed</span><span>(</span><span>raw</span><span>)))</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个出口返回 <code>Err</code>：输入结束，或者遇到换行。Pebble 的字符串是单行的，所以换行也算”没有闭合”。关键的是遇到换行<strong>不吃掉它</strong>：<code>Some('\n')</code> 分支只是 <code>return</code>，换行字符还在输入里，下一次 <code>next_token</code> 会把它当空白跳过，然后继续正常词法分析。这就是错误恢复：下面的例子会展示一整个流程。</p><p>循环里用一个 <code>has_escape</code> 记录有没有遇到反斜杠。扫描结束后用 <code>&amp;self.src[body_start..self.pos]</code> 取出原始文本，没有转义就 <code>Cow::Borrowed(raw)</code>，<strong>零拷贝</strong>；有转义就 <code>Cow::Owned(unescape(raw))</code>，分配一个新 <code>String</code>。</p><p><code>Cow&lt;'src, str&gt;</code> 的生命周期 <code>'src</code> 在两种情况下含义不同：<code>Borrowed</code> 时它是真正的借用；<code>Owned</code> 时没有借用，但类型一样，所以放进同一个 <code>TokenKind&lt;'src&gt;</code> 里互不干扰。这是”借用或拥有”类型的价值：调用者不必关心。</p><p><code>unescape</code> 和 <code>read_unicode_escape</code> 是两个纯字符串函数，合起来 45 行，处理 <code>\n</code>、<code>\t</code>、<code>\u{1F980}</code> 等转义，没有新概念，直接复制（文件的第 532 到 576 行）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>-C</span><span> </span><span>~/pebble-tag</span><span> </span><span>show</span><span> </span><span>tutorial-v1:crates/pebble-lexer/src/lib.rs</span><span> | </span><span>sed</span><span> </span><span>-n</span><span> </span><span>532,576p</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2><code>Iterator</code>：让词法分析器成为一个迭代器<a href="#iterator让词法分析器成为一个迭代器"><span>#</span></a></h2><p><code>next_token</code> 已经足够用了，但我们想让 Rust 的标准工具，<code>for</code>、<code>collect</code>、<code>filter</code>，都能直接作用在词法分析器上。这需要实现 <code>Iterator</code> trait。先用一个最小的例子认识它，实验文件 <code>src/bin/ch04_iterator.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Countdown</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>n</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>impl</span><span> </span><span>Iterator</span><span> </span><span>for</span><span> </span><span>Countdown</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>type</span><span> </span><span>Item</span><span><span> </span><span>=</span><span> </span></span><span>u32</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>next</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>u32</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.n </span><span>==</span><span> </span></span><span>0</span><span> {</span></div></div><div><div><div>10</div></div><div><span>            </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span><span><span>.n </span><span>-=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Some</span><span>(</span><span>self</span><span>.n + </span><span>1</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>countdown</span><span><span> </span><span>=</span><span> </span></span><span>Countdown</span><span> { </span><span>n</span><span>: </span><span>5</span><span> };</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>first_two</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>countdown</span><span>.</span><span>by_ref</span><span>().</span><span>take</span><span>(</span><span>2</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>println!</span><span>(</span><span>"first_two = {first_two:?}"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> </span><span>rest</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>countdown</span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>22</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rest      = {rest:?}"</span><span>);</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>by_value</span><span><span> </span><span>=</span><span> </span></span><span>Countdown</span><span> { </span><span>n</span><span>: </span><span>5</span><span> };</span></div></div><div><div><div>25</div></div><div><span>    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> (&amp;</span></span><span>mut</span><span> </span><span>by_value</span><span>).</span><span>take</span><span>(</span><span>2</span><span>).</span><span>count</span><span>();</span></div></div><div><div><div>26</div></div><div><span>    </span><span>println!</span><span>(</span><span>"left over = {:?}"</span><span>, </span><span>by_value</span><span>.</span><span>next</span><span>());</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Iterator</code> 的定义里只有一个必须实现的方法：<code>fn next(&amp;mut self) -&gt; Option&lt;Self::Item&gt;</code>。<code>Item</code> 是一个<strong>关联类型</strong>：实现者声明”我产出什么类型”，<code>impl Iterator for Countdown { type Item = u32; ... }</code>。它和泛型参数很像，区别是一个类型只能对 <code>Iterator</code> 实现<strong>一次</strong>（<code>Countdown</code> 只有一个 <code>Item</code>），而泛型 trait 可以多次实现（<code>From&lt;A&gt;</code> 和 <code>From&lt;B&gt;</code> 可以同时存在）。<code>take</code>、<code>map</code>、<code>filter</code>、<code>collect</code> 这几十个方法都是 <code>Iterator</code> 的<strong>默认方法</strong>，<code>next</code> 一实现就免费得到。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--bin</span><span> </span><span>ch04_iterator</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>first_two = [5, 4]</span></div></div><div><div><div>2</div></div><div><span>rest      = [3, 2, 1]</span></div></div><div><div><div>3</div></div><div><span>left over = Some(3)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出最后一行的 <code>left over = Some(3)</code> 是第二段代码的结果：<code>(&amp;mut by_value).take(2).count()</code> 吃掉两个元素，<code>by_value</code> 还剩 3。这就引出 <code>countdown.by_ref().take(2).collect()</code> 里的 <code>by_ref()</code>。<code>take</code> 会<strong>拿走</strong> <code>self</code>（它的签名是 <code>fn take(self, n: usize) -&gt; Take&lt;Self&gt;</code>），所以直接写 <code>countdown.take(2)</code> 之后 <code>countdown</code> 就没了。实验文件 <code>src/bin/ch04_by_ref_moved.rs</code> 是同一个 <code>Countdown</code>，<code>main</code> 改成下面这样（可选），把这个错误做了出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>countdown</span><span><span> </span><span>=</span><span> </span></span><span>Countdown</span><span> { </span><span>n</span><span>: </span><span>5</span><span> };</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>first_two</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>countdown</span><span>.</span><span>take</span><span>(</span><span>2</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>rest</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>countdown</span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{first_two:?} {rest:?}"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>报错如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0382]: use of moved value: `countdown`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch04_by_ref_moved.rs:20:26</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>18 |     let countdown = Countdown { n: 5 };</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|         --------- move occurs because `countdown` has type `Countdown`, which does not implement the `Copy` trait</span></div></div><div><div><div>6</div></div><div><span>19 |     let first_two: Vec&lt;u32&gt; = countdown.take(2).collect();</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|                                         ------- `countdown` moved due to this method call</span></div></div><div><div><div>8</div></div><div><span>20 |     let rest: Vec&lt;u32&gt; = countdown.collect();</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|                          ^^^^^^^^^ value used here after move</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>note: `std::iter::Iterator::take` takes ownership of the receiver `self`, which moves `countdown`</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/iter/traits/iterator.rs:1414:12</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>For more information about this error, try `rustc --explain E0382`.</span></div></div><div><div><div>15</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_by_ref_moved") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>by_ref()</code> 返回 <code>&amp;mut Self</code>，而 <code>&amp;mut I</code> 对任何迭代器 <code>I</code> 自己也是迭代器，所以 <code>take</code> 拿走的只是一个临时的可变借用，原来的 <code>countdown</code> 还在，可以继续消耗剩下的元素。输出里的 <code>rest = [3, 2, 1]</code> 就是证明。我们的词法分析器测试 <code>iterator_terminates_after_eof</code> 里会用同一招。</p><p>现在为 <code>Lexer</code> 实现它（文件第 500 到 506 行）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span>&lt;'</span><span>src</span><span>&gt; </span><span>Iterator</span><span> </span><span>for</span><span> </span><span>Lexer</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Item</span><span><span> </span><span>=</span><span> </span></span><span>Result</span><span>&lt;</span><span>Token</span><span>&lt;'</span><span>src</span><span>&gt;, </span><span>Diagnostic</span><span>&gt;;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>next</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Self</span><span>::</span><span>Item</span><span>&gt; {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span><span>.</span><span>next_token</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>type Item = Result&lt;Token&lt;'src&gt;, Diagnostic&gt;</code>：迭代器的每一项是一个 <code>Result</code>，<code>next</code> 返回 <code>Option&lt;Self::Item&gt;</code>，也就是我们的 <code>Option&lt;Result&lt;..&gt;&gt;</code>，一一对应，所以 <code>next</code> 直接转发 <code>next_token</code>。<code>Self::Item</code> 是写法，等价于 <code>Result&lt;Token&lt;'src&gt;, Diagnostic&gt;</code>。</p><p>为什么还要有一个 <code>done</code> 字段？看 <code>next_token</code> 的开头：<code>if self.done { return None; }</code>，吐出 <code>Eof</code> 的同时把 <code>done</code> 设成 <code>true</code>。如果没有这个标志，<code>is_at_end()</code> 永远为真，每次调用都会产出一个新的 <code>Eof</code>，<code>for</code> 循环就永远不结束。标准库给这种”返回 <code>None</code> 之后永远返回 <code>None</code>”的迭代器起了个名字，叫 fused，<code>Iterator::fuse</code> 可以包装一个不守规矩的迭代器，我们这里是自己守规矩。</p><section><h3><code>tokenize</code><a href="#tokenize"><span>#</span></a></h3><p>最后一个函数把一切串起来（文件第 508 到 530 行）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Lexes an entire source into tokens plus diagnostics, always ending in `Eof`.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// Never fails, so callers can render every diagnostic before deciding what to</span></div></div><div><div><div>4</div></div><div><span>/// do with the (possibly partial) token stream.</span></div></div><div><div><div>5</div></div><div><span>#[must_use]</span></div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>tokenize</span><span>(</span><span>source</span><span>: </span><span>SourceId</span><span>, </span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; (</span><span>Vec</span><span>&lt;</span><span>Token</span><span>&lt;'</span><span>_</span><span>&gt;&gt;, </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt;) {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>tokens</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>diagnostics</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>source</span><span>, </span><span>src</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>item</span><span><span>) </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>next_token</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>match</span><span> </span><span>item</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>Ok</span><span>(</span><span>token</span><span>) =&gt; {</span></div></div><div><div><div>13</div></div><div><span>                </span><span>let</span><span> </span><span>is_eof</span><span><span> </span><span>=</span><span> </span></span><span>token</span><span><span>.kind </span><span>==</span><span> </span></span><span>TokenKind</span><span>::</span><span>Eof</span><span>;</span></div></div><div><div><div>14</div></div><div><span>                </span><span>tokens</span><span>.</span><span>push</span><span>(</span><span>token</span><span>);</span></div></div><div><div><div>15</div></div><div><span>                </span><span>if</span><span> </span><span>is_eof</span><span> {</span></div></div><div><div><div>16</div></div><div><span>                    </span><span>break</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>            </span><span>Err</span><span>(</span><span>diagnostic</span><span>) =&gt; </span><span>diagnostics</span><span>.</span><span>push</span><span>(</span><span>diagnostic</span><span>),</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>(</span><span>tokens</span><span>, </span><span>diagnostics</span><span>)</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它在一个 <code>while let</code> 里反复调用 <code>next_token</code>，<code>Ok</code> 的 token 放进 <code>tokens</code>，<code>Err</code> 的诊断放进 <code>diagnostics</code>，遇到 <code>Eof</code> 就退出。<strong>从不失败</strong>：返回的是一个元组 <code>(Vec&lt;Token&gt;, Vec&lt;Diagnostic&gt;)</code>，调用者可以先把所有诊断打印出来再决定怎么办。<code>let is_eof = token.kind == TokenKind::Eof;</code> 用到了 <code>PartialEq</code>，这就是为什么要先算出这个布尔值：<code>tokens.push(token)</code> 会把 <code>token</code> 移走（第 2 篇），之后就不能再读它的字段了。</p><p>标准库的迭代器适配器本可以让它短很多，比如 <code>lexer.collect::&lt;Result&lt;Vec&lt;_&gt;, _&gt;&gt;()</code> 一行就能得到”全部成功或第一个错误”。但那样遇到第一个错误就停了，不符合”报告一切”。这是我故意不用它的原因，第 9 篇 <code>collect::&lt;Result&lt;..&gt;&gt;</code> 会出现在一个合适的位置。</p></section></section>
<section><h2>它真的工作吗<a href="#它真的工作吗"><span>#</span></a></h2><p>把整个文件拼好之后（顺序见检查点），我在 <code>crates/pebble-lexer/examples/dump.rs</code> 里放了一个临时程序，用 <code>SourceMap</code> 登记一段有两处错误的源码，调用 <code>tokenize</code>，打印 token，再用第 3 篇的 <code>render</code> 打印诊断：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_lexer</span><span>::tokenize;</span></span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span><span> </span></span><span>"let total = 1_000 + 2.5;</span><span>\n</span><span>let s = </span><span>\"</span><span>a</span><span>\\</span><span>n</span><span>\\</span><span>u{1F980}</span><span>\"</span><span>;</span><span>\n</span><span>let bad = @;</span><span>\n</span><span>let t = </span><span>\"</span><span>oops</span><span>\n</span><span>print(total);</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"demo.pebble"</span><span>, </span><span>text</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>id</span><span>, </span><span>text</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>for</span><span> </span><span>token</span><span> </span><span>in</span><span> &amp;</span><span>tokens</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{:&gt;2}..{:&lt;2} {:?}"</span><span>, </span><span>token</span><span>.span.start, </span><span>token</span><span>.span.end, </span><span>token</span><span>.kind);</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>    </span><span>for</span><span> </span><span>diagnostic</span><span> </span><span>in</span><span> &amp;</span><span>diagnostics</span><span> {</span></div></div><div><div><div>13</div></div><div><span>        </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>diagnostic</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>));</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>-p</span><span> </span><span>pebble-lexer</span><span> </span><span>--example</span><span> </span><span>dump</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span> </span></span><span>0..3  Let</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>4..9  Ident("total")</span></div></div><div><div><div>3</div></div><div><span>10..11 Assign</span></div></div><div><div><div>4</div></div><div><span>12..17 Int(1000)</span></div></div><div><div><div>5</div></div><div><span>18..19 Plus</span></div></div><div><div><div>6</div></div><div><span>20..23 Float(2.5)</span></div></div><div><div><div>7</div></div><div><span>23..24 Semicolon</span></div></div><div><div><div>8</div></div><div><span>25..28 Let</span></div></div><div><div><div>9</div></div><div><span>29..30 Ident("s")</span></div></div><div><div><div>10</div></div><div><span>31..32 Assign</span></div></div><div><div><div>11</div></div><div><span>33..47 Str("a\n🦀")</span></div></div><div><div><div>12</div></div><div><span>47..48 Semicolon</span></div></div><div><div><div>13</div></div><div><span>49..52 Let</span></div></div><div><div><div>14</div></div><div><span>53..56 Ident("bad")</span></div></div><div><div><div>15</div></div><div><span>57..58 Assign</span></div></div><div><div><div>16</div></div><div><span>60..61 Semicolon</span></div></div><div><div><div>17</div></div><div><span>62..65 Let</span></div></div><div><div><div>18</div></div><div><span>66..67 Ident("t")</span></div></div><div><div><div>19</div></div><div><span>68..69 Assign</span></div></div><div><div><div>20</div></div><div><span>76..81 Ident("print")</span></div></div><div><div><div>21</div></div><div><span>81..82 LParen</span></div></div><div><div><div>22</div></div><div><span>82..87 Ident("total")</span></div></div><div><div><div>23</div></div><div><span>87..88 RParen</span></div></div><div><div><div>24</div></div><div><span>88..89 Semicolon</span></div></div><div><div><div>25</div></div><div><span>90..90 Eof</span></div></div><div><div><div>26</div></div><div><span>error[E0001]: unexpected character `@`</span></div></div><div><div><div>27</div></div><div><span><span> </span></span><span>--&gt; demo.pebble:3:11</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>29</div></div><div><span>3 | let bad = @;</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>|           ^</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>32</div></div><div><span>error[E0003]: unterminated string literal</span></div></div><div><div><div>33</div></div><div><span><span> </span></span><span>--&gt; demo.pebble:4:9</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>35</div></div><div><span>4 | let t = "oops</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>|         ^^^^^</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>对照着读。第一行是 <code>let total = 1_000 + 2.5;</code>：<code>1_000</code> 变成 <code>Int(1000)</code>，<code>2.5</code> 变成 <code>Float(2.5)</code>，范围 <code>12..17</code> 是它在源码里的字节区间。第二行 <code>"a\n🦀"</code> 有转义，变成 <code>Str("a\n🦀")</code>，是 <code>Cow::Owned</code>。</p><p>注意第三行的 <code>let bad = @;</code>：<code>@</code> 产出一个 <code>E0001</code>，但 token 序列里 <code>Assign</code>（57..58）之后直接是 <code>Semicolon</code>（60..61），中间没有 <code>@</code> 的 token，其他 token 都在。第四行的 <code>"oops</code> 没有闭合，<code>E0003</code>，下面的 <code>print(total);</code> 完整地被词法分析了：<code>Ident("print")</code>、<code>LParen</code>、<code>Ident("total")</code>、<code>RParen</code>、<code>Semicolon</code>。一次运行，两个错误，一个也没漏掉，后面的代码一个也没丢。这是第 7 篇解析器恢复机制的基础：如果词法分析器遇到第一个错误就停下，解析器连”后面还有什么”都不知道。</p><p>（跑完把 <code>examples</code> 目录删掉，它不是 crate 的一部分。）</p></section>
<section><h2>两个只用一小节讲的概念<a href="#两个只用一小节讲的概念"><span>#</span></a></h2><p>我答应过只花一段讲这两个。</p><p><strong>高阶生命周期（HRTB）。</strong> <code>fn(&amp;str) -&gt; &amp;str</code> 是一个函数指针类型。它没有写任何生命周期，按省略规则 1 和 2，每个输入引用有自己的生命周期，输出借自它。但<strong>这个生命周期是谁的？</strong> 函数指针不绑定到某个具体的区域，而是”对任何区域都成立”，完整写法是 <code>for&lt;'a&gt; fn(&amp;'a str) -&gt; &amp;'a str</code>，读作”对所有 <code>'a</code>”。实验文件 <code>src/bin/ch04_hrtb.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>trim</span><span>(</span><span>s</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>s</span><span>.</span><span>trim</span><span>()</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>apply</span><span>(</span><span>f</span><span>: </span><span>fn</span><span>(&amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span>, </span><span>input</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>f</span><span>(</span><span>input</span><span>).</span><span>len</span><span>()</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>apply_explicit</span><span>(</span><span>f</span><span>: </span><span>for</span><span>&lt;'</span><span>a</span><span>&gt; </span><span>fn</span><span>(&amp;'</span><span>a</span><span> </span><span>str</span><span>) -&gt; &amp;'</span><span>a</span><span> </span><span>str</span><span>, </span><span>input</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>f</span><span>(</span><span>input</span><span>).</span><span>len</span><span>()</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>owned</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"  padded  "</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>apply</span><span>(</span><span>trim</span><span>, &amp;</span><span>owned</span><span>));</span></div></div><div><div><div>16</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>apply_explicit</span><span>(</span><span>trim</span><span>, </span><span>"  other  "</span><span>));</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>6</span></div></div><div><div><div>2</div></div><div><span>5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两种写法等价，都能接受同一个 <code>trim</code>，并且第一次调用传的是局部 <code>String</code>，第二次传的是字面量，生命周期各不相同。你会在第 8 篇的函数指针里再见到它。</p><p><strong>型变。</strong> <code>Token&lt;'static&gt;</code> 能不能用在要 <code>Token&lt;'a&gt;</code> 的地方？实验文件 <code>src/bin/ch04_variance.rs</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Tok</span><span>&lt;'</span><span>src</span><span>&gt; {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>text</span><span>: &amp;'</span><span>src</span><span> </span><span>str</span><span>,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>fn</span><span> </span><span>pick</span><span>&lt;'</span><span>a</span><span>&gt;(</span><span>a</span><span>: </span><span>Tok</span><span>&lt;'</span><span>a</span><span>&gt;, </span><span>b</span><span>: </span><span>Tok</span><span>&lt;'</span><span>a</span><span>&gt;) -&gt; </span><span>Tok</span><span>&lt;'</span><span>a</span><span>&gt; {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>if</span><span> </span><span>a</span><span>.text.</span><span>len</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>b</span><span>.text.</span><span>len</span><span>() { </span><span>a</span><span> } </span><span>else</span><span> { </span><span>b</span><span> }</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>global</span><span>: </span><span>Tok</span><span>&lt;'</span><span>static</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Tok</span><span> { </span><span>text</span><span>: </span><span>"global"</span><span> };</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>local</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"local-and-longer"</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>borrowed</span><span><span> </span><span>=</span><span> </span></span><span>Tok</span><span> { </span><span>text</span><span>: &amp;</span><span>local</span><span> };</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>pick</span><span>(</span><span>global</span><span>, </span><span>borrowed</span><span>));</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Tok { text: "local-and-longer" }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pick</code> 要求两个参数的生命周期<strong>相同</strong>，我们传了一个 <code>'static</code> 的和一个借自局部变量的，居然编译通过。原因是 <code>Tok&lt;'a&gt;</code> 在 <code>'a</code> 上是<strong>协变</strong>的：活得更久的可以当成活得更短的来用（<code>'static</code> 缩成 <code>'a</code>），就像一个人可以出现在一个比自己存在得更短的会议里。反过来就不行。可变引用 <code>&amp;mut T</code> 在 <code>T</code> 上是不变的，这是一个更微妙的话题，Rustonomicon 有专门一章，我们这个系列不需要。</p></section>
<section><h2>测试：写两个，复制其余<a href="#测试写两个复制其余"><span>#</span></a></h2><p>最后是测试。</p><p>我们已经写了第一个关键测试 <code>identifiers_borrow_the_source</code>（最终版用的是 <code>tokenize</code>，因为现在有了它）。第二个关键测试证明错误恢复：一个未闭合的字符串之后，词法分析器还能继续：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>reports_unterminated_string_but_keeps_going</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"</span><span>\"</span><span>oops</span><span>\n</span><span>42"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>diagnostics</span><span>.</span><span>len</span><span>(), </span><span>1</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>diagnostics</span><span>[</span><span>0</span><span>].code.</span><span>as_deref</span><span>(), </span><span>Some</span><span>(</span><span>"E0003"</span><span>));</span></div></div><div><div><div>6</div></div><div><span>    </span><span>assert!</span><span>(</span><span>tokens</span><span>.</span><span>iter</span><span>().</span><span>any</span><span><span>(</span><span>|</span></span><span>t</span><span><span>|</span><span> </span></span><span>t</span><span><span>.kind </span><span>==</span><span> </span><span>TokenKind</span><span>::</span></span><span>Int</span><span>(</span><span>42</span><span>)));</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输入是 <code>"oops</code> 换行 <code>42</code>。断言三件事：恰好一个诊断；它的错误码是 <code>E0003</code>；<code>42</code> 那个 token 还在。<code>.code.as_deref()</code> 把 <code>Option&lt;String&gt;</code> 借成 <code>Option&lt;&amp;str&gt;</code>，这样才能和 <code>Some("E0003")</code> 比较。<code>iter().any(|t| ...)</code> 是”有任何一个元素满足条件”，也是第 2 篇见过的闭包语法。</p><p>整个 <code>tests</code> 模块（文件第 578 到 711 行）的组装方法是：写模块头、辅助函数 <code>kinds</code> 和前两个测试；复制第 3 到 8 个测试；写 <code>reports_unterminated_string_but_keeps_going</code>；再复制最后三个。模块头、辅助函数和前两个测试：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>kinds</span><span>(</span><span>src</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Vec</span><span>&lt;</span><span>TokenKind</span><span>&lt;'</span><span>_</span><span>&gt;&gt; {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>src</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>8</div></div><div><span>            </span><span>diagnostics</span><span>.</span><span>is_empty</span><span>(),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>"unexpected diagnostics: {diagnostics:?}"</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>tokens</span><span>.</span><span>into_iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>token</span><span><span>|</span><span> </span></span><span>token</span><span>.kind).</span><span>collect</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>fn</span><span> </span><span>lexes_keywords_and_identifiers</span><span>() {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>17</div></div><div><span>            </span><span>kinds</span><span>(</span><span>"let x = iffy;"</span><span>),</span></div></div><div><div><div>18</div></div><div><span>            </span><span>vec!</span><span>[</span></div></div><div><div><div>19</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Let</span><span>,</span></div></div><div><div><div>20</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>"x"</span><span>),</span></div></div><div><div><div>21</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Assign</span><span>,</span></div></div><div><div><div>22</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>"iffy"</span><span>),</span></div></div><div><div><div>23</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Semicolon</span><span>,</span></div></div><div><div><div>24</div></div><div><span>                </span><span>TokenKind</span><span>::</span><span>Eof</span><span>,</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>]</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>30</div></div><div><span>    </span><span>fn</span><span> </span><span>identifiers_borrow_the_source</span><span>() {</span></div></div><div><div><div>31</div></div><div><span>        </span><span>let</span><span> </span><span>source</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"hello"</span><span>);</span></div></div><div><div><div>32</div></div><div><span>        </span><span>let</span><span> (</span><span>tokens</span><span>, </span><span>_</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), &amp;</span><span>source</span><span>);</span></div></div><div><div><div>33</div></div><div><span>        </span><span>match</span><span> &amp;</span><span>tokens</span><span>[</span><span>0</span><span>].kind {</span></div></div><div><div><div>34</div></div><div><span>            </span><span>TokenKind</span><span>::</span><span>Ident</span><span>(</span><span>name</span><span>) =&gt; {</span></div></div><div><div><div>35</div></div><div><span>                </span><span>let</span><span> </span><span>base</span><span><span> </span><span>=</span><span> </span></span><span>source</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>36</div></div><div><span>                </span><span>let</span><span> </span><span>borrowed</span><span><span> </span><span>=</span><span> </span></span><span>name</span><span>.</span><span>as_ptr</span><span>() </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>37</div></div><div><span>                </span><span>assert!</span><span>((</span><span>base</span><span>..</span><span>base</span><span> + </span><span>source</span><span>.</span><span>len</span><span>()).</span><span>contains</span><span>(&amp;</span><span>borrowed</span><span>));</span></div></div><div><div><div>38</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>            </span><span>other</span><span> =&gt; </span><span>panic!</span><span>(</span><span>"expected identifier, got {other:?}"</span><span>),</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>接着复制第 620 到 679 行（六个测试：没有转义的字符串是借用的、有转义的是拥有且已解码的、数字、多字符运算符优先、关键字和符号拼写一致、嵌套注释）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>-C</span><span> </span><span>~/pebble-tag</span><span> </span><span>show</span><span> </span><span>tutorial-v1:crates/pebble-lexer/src/lib.rs</span><span> | </span><span>sed</span><span> </span><span>-n</span><span> </span><span>620,679p</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后是我们手写的那个测试（前后各空一行），最后复制第 688 到 711 行（空行、位置报告、span 覆盖字节、迭代器终止，以及模块的结束括号）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>-C</span><span> </span><span>~/pebble-tag</span><span> </span><span>show</span><span> </span><span>tutorial-v1:crates/pebble-lexer/src/lib.rs</span><span> | </span><span>sed</span><span> </span><span>-n</span><span> </span><span>688,711p</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一个测试 <code>iterator_terminates_after_eof</code> 就用到了 <code>by_ref</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>iterator_terminates_after_eof</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>lexer</span><span><span> </span><span>=</span><span> </span></span><span>Lexer</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"1"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>collected</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> </span></span><span>lexer</span><span>.</span><span>by_ref</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>collected</span><span>.</span><span>len</span><span>(), </span><span>2</span><span>); </span><span>// Int(1), Eof</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>lexer</span><span>.</span><span>next</span><span>(), </span><span>None</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>lexer.by_ref().collect()</code> 先把迭代器耗尽，<code>lexer</code> 还在，再调一次 <code>next()</code> 应该是 <code>None</code>，<code>assert_eq!(lexer.next(), None)</code> 能写，是因为 <code>Result&lt;Token, Diagnostic&gt;</code> 派生了 <code>PartialEq</code>，<code>Option</code> 也就有了。</p></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>先把 <code>examples</code> 目录（如果你还留着）删掉，然后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>--workspace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_diag-e12009e933f49099)</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_lexer-e41783417c6bf9a0)</span></div></div><div><div><div>4</div></div><div><span>test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>5</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_span-3b8ae416ebb198e2)</span></div></div><div><div><div>6</div></div><div><span>test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（过滤掉了编译提示和三个”0 passed”的 <code>Doc-tests</code> 汇总行。）<code>pebble-lexer</code> 12 个测试，<code>pebble-diag</code> 4 个，<code>pebble-span</code> 6 个。再检查和 tag 的差异：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>~/pebble-tag/crates/pebble-lexer/src/lib.rs</span><span> </span><span>crates/pebble-lexer/src/lib.rs</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>identical</span></div></div><div><div><div>2</div></div><div><span>diff</span><span> </span><span>~/pebble-tag/crates/pebble-lexer/Cargo.toml</span><span> </span><span>crates/pebble-lexer/Cargo.toml</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>identical</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>identical</span></div></div><div><div><div>2</div></div><div><span>identical</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>lib.rs</code> 的组装顺序：文件头（1 到 24 行）、<code>TokenKind</code>、<code>impl TokenKind&lt;'_&gt;</code>（复制的 <code>describe</code> 加上写的 <code>keyword</code>）、<code>Display</code>（复制）、<code>Token</code>、<code>Lexer</code> 结构体、<code>impl&lt;'src&gt; Lexer&lt;'src&gt;</code>（<code>new</code>、<code>position</code>、<code>is_at_end</code>，游标函数，<code>skip_trivia</code>，<code>next_token</code>，<code>lex_number</code>，<code>lex_identifier</code>，<code>lex_string</code>，各方法之间空一行）、<code>Iterator</code>、<code>tokenize</code>、<code>unescape</code> 与 <code>read_unicode_escape</code>（复制）、<code>tests</code>。每一块之间空一行。</p><p>此时应当与 tag 逐字节相同的文件（已完成）：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-lexer/Cargo.toml" target="_blank"><code>crates/pebble-lexer/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-lexer/src/lib.rs" target="_blank"><code>crates/pebble-lexer/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/crates/pebble-diag" target="_blank"><code>crates/pebble-diag/</code></a> 与 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/crates/pebble-span" target="_blank"><code>crates/pebble-span/</code></a> 的两个文件（第 2、3 篇完成）</li>
</ul><p>中间状态（现在这样就对，后面的章节会补全）：</p><ul>
<li>根 <code>Cargo.toml</code>：<code>[workspace.dependencies]</code> 目前有 <code>pebble-span</code>、<code>pebble-diag</code>、<code>pebble-lexer</code> 三行，其余和 tag 的一样。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇的所有实验文件都在仓库的 <code>lab/</code> 目录里（主实验室是 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>）。如果你把 tag 克隆在 <code>~/pebble-tag</code>，它们就在 <code>~/pebble-tag/lab/rust-lab</code>，下面的命令先 <code>cd ~/pebble-tag/lab/rust-lab</code>（或者把它复制到 <code>~/rust-lab</code>）再运行。</p><p>主实验室，每个文件都是 <code>src/bin/</code> 下的一个二进制：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_struct_lifetime.rs" target="_blank"><code>ch04_struct_lifetime.rs</code></a></td><td><code>cargo build --bin ch04_struct_lifetime</code></td><td>E0106</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_outlives.rs" target="_blank"><code>ch04_outlives.rs</code></a></td><td><code>cargo build --bin ch04_outlives</code></td><td>E0597</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_elision_trap.rs" target="_blank"><code>ch04_elision_trap.rs</code></a></td><td><code>cargo build --bin ch04_elision_trap</code></td><td>E0499</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_elision_fixed.rs" target="_blank"><code>ch04_elision_fixed.rs</code></a></td><td><code>cargo run --quiet --bin ch04_elision_fixed</code></td><td>运行，第一行 <code>let answer</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_static.rs" target="_blank"><code>ch04_static.rs</code></a></td><td><code>cargo run --quiet --bin ch04_static</code></td><td>运行，第一行 <code>Some("Let")</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_copy_cow.rs" target="_blank"><code>ch04_copy_cow.rs</code></a></td><td><code>cargo build --bin ch04_copy_cow</code></td><td>E0204</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_cow.rs" target="_blank"><code>ch04_cow.rs</code></a></td><td><code>cargo run --quiet --bin ch04_cow</code></td><td>运行，第一行 <code>plain    Borrowed  "plain"</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_float_eq.rs" target="_blank"><code>ch04_float_eq.rs</code></a></td><td><code>cargo build --bin ch04_float_eq</code></td><td>E0277</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_patterns.rs" target="_blank"><code>ch04_patterns.rs</code></a></td><td><code>cargo run --quiet --bin ch04_patterns</code></td><td>运行，第一行 <code>'7' -&gt; digit</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_iterator.rs" target="_blank"><code>ch04_iterator.rs</code></a></td><td><code>cargo run --quiet --bin ch04_iterator</code></td><td>运行，第一行 <code>first_two = [5, 4]</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_by_ref_moved.rs" target="_blank"><code>ch04_by_ref_moved.rs</code></a></td><td><code>cargo build --bin ch04_by_ref_moved</code></td><td>E0382</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_hrtb.rs" target="_blank"><code>ch04_hrtb.rs</code></a></td><td><code>cargo run --quiet --bin ch04_hrtb</code></td><td>运行，输出 <code>6</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_variance.rs" target="_blank"><code>ch04_variance.rs</code></a></td><td><code>cargo run --quiet --bin ch04_variance</code></td><td>运行，输出 <code>Tok { text: "local-and-longer" }</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_longest.rs" target="_blank"><code>ch04_longest.rs</code></a></td><td><code>cargo build --bin ch04_longest</code></td><td>E0106</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_longest_scope.rs" target="_blank"><code>ch04_longest_scope.rs</code></a></td><td><code>cargo build --bin ch04_longest_scope</code></td><td>E0597</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch04_longest_ok.rs" target="_blank"><code>ch04_longest_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch04_longest_ok</code></td><td>运行，输出 <code>abc</code></td></tr></tbody></table><p>侧边 crate、C++ 程序和对 <code>pebble-lexer</code> 的练习补丁。命令里写了 <code>cd</code> 的在侧边 crate 的目录里运行，其余在仓库根目录（<code>~/pebble-tag</code>）运行：</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch04-stages/src/bin/stage1.rs" target="_blank"><code>lab/lab-ch04-stages/src/bin/stage1.rs</code></a></td><td><code>cd lab/lab-ch04-stages &amp;&amp; cargo run --quiet --bin stage1</code></td><td>运行，第一行 <code>Ok(Token { kind: Ident("total"), span: 0..5 })</code>，后面还有 <code>err unexpected character</code> 那一行</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch04-stages/src/bin/hidden.rs" target="_blank"><code>lab/lab-ch04-stages/src/bin/hidden.rs</code></a></td><td><code>cd lab/lab-ch04-stages &amp;&amp; cargo run --quiet --bin hidden</code></td><td>运行，输出 <code>Some(Token { kind: Ident("hello"), span: 0..5 })</code>，构建时有两条警告（隐藏生命周期参数已弃用；隐藏了别处省略的生命周期）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch04_view.cpp" target="_blank"><code>lab/cpp/ch04_view.cpp</code></a></td><td><code>g++ -std=c++17 -Wall -Wextra -fsanitize=address -o /tmp/view lab/cpp/ch04_view.cpp &amp;&amp; /tmp/view</code></td><td>AddressSanitizer 报告 <code>stack-use-after-scope</code>（不加 <code>-fsanitize=address</code> 时打印 <code>let</code>）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch04-elide.patch" target="_blank"><code>lab/exercises/ch04-elide.patch</code></a></td><td><code>git apply lab/exercises/ch04-elide.patch &amp;&amp; cargo build -p pebble-lexer</code></td><td>能编译，两条警告：<code>hidden lifetime parameters in types are deprecated</code>、<code>hiding a lifetime that's elided elsewhere is confusing</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch04-hex.patch" target="_blank"><code>lab/exercises/ch04-hex.patch</code></a></td><td><code>git apply lab/exercises/ch04-hex.patch &amp;&amp; cargo test -p pebble-lexer</code></td><td><code>14 passed; 0 failed</code>（练习 1 的 <code>0x</code> 十六进制字面量）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch04-peek.patch" target="_blank"><code>lab/exercises/ch04-peek.patch</code></a></td><td><code>git apply lab/exercises/ch04-peek.patch &amp;&amp; cargo test -p pebble-lexer</code></td><td><code>12 passed; 0 failed</code>（练习 3 的 <code>peek_second_byte</code>）</td></tr></tbody></table><p>补丁彼此独立，每个都在干净的 tag 上应用；换下一个之前先 <code>git apply -R</code> 还原。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 支持 <code>0x</code> 十六进制字面量。</strong> <code>0xff</code> 应该得到 <code>Int(255)</code>，<code>0x_1F</code>、<code>0X1_0</code> 同样合法，<code>0x</code> 后面什么也没有应该报错。（提示：<code>i64::from_str_radix</code>，错误码可以叫 <code>E0004</code>。）</p>
参考答案<p>在 <code>next_token</code> 里，<code>'0'..='9'</code> 分支之前加一个带守卫的分支，让 <code>0</code> 后面跟着 <code>x</code> 或 <code>X</code> 时走新函数：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>'0'</span><span> </span><span>if</span><span> </span><span>matches!</span><span>(</span><span>self</span><span>.</span><span>peek</span><span>(), </span><span>Some</span><span>(</span><span>'x'</span><span><span> </span><span>|</span><span> </span></span><span>'X'</span><span>)) =&gt; </span><span>match</span><span> </span><span>self</span><span>.</span><span>lex_hex</span><span>(</span><span>start</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Ok</span><span>(</span><span>kind</span><span>) =&gt; </span><span>kind</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Err</span><span>(</span><span>diagnostic</span><span>) =&gt; </span><span>return</span><span> </span><span>Some</span><span>(</span><span>Err</span><span>(</span><span>diagnostic</span><span>)),</span></div></div><div><div><div>4</div></div><div><span>},</span></div></div><div><div><div>5</div></div><div><span>'0'</span><span>..=</span><span>'9'</span><span> =&gt; </span><span>self</span><span>.</span><span>lex_number</span><span>(</span><span>start</span><span>),</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>matches!(expr, pattern)</code> 是一个小宏，求值后看是否匹配模式，返回 <code>bool</code>，这里也用上了<strong>或模式</strong>。<code>lex_hex</code> 放在 <code>lex_number</code> 前面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>lex_hex</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>start</span><span>: </span><span>usize</span><span>) -&gt; </span><span>Result</span><span>&lt;</span><span>TokenKind</span><span>&lt;'</span><span>src</span><span>&gt;, </span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>self</span><span>.</span><span>bump</span><span>(); </span><span>// the `x`</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>digits_start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.pos;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>ch</span><span><span>) </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>peek</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>if</span><span> </span><span>ch</span><span>.</span><span>is_ascii_hexdigit</span><span><span>() </span><span>||</span><span> </span></span><span>ch</span><span><span> </span><span>==</span><span> </span></span><span>'_'</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>self</span><span>.</span><span>bump</span><span>();</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>break</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> </span><span>digits</span><span>: </span><span>String</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.src[</span><span>digits_start</span><span>..</span><span>self</span><span>.pos]</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>.</span><span>chars</span><span>()</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>.</span><span>filter</span><span><span>(</span><span>|</span><span>&amp;</span></span><span>c</span><span><span>|</span><span> </span></span><span>c</span><span><span> </span><span>!=</span><span> </span></span><span>'_'</span><span>)</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>15</div></div><div><span>    </span><span>i64</span><span>::</span><span>from_str_radix</span><span>(&amp;</span><span>digits</span><span>, </span><span>16</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>.</span><span>map</span><span><span>(</span><span>TokenKind</span><span>::</span></span><span>Int</span><span>)</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>.</span><span>map_err</span><span><span>(</span><span>|</span></span><span>error</span><span><span>|</span><span> {</span></span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>format!</span><span>(</span><span>"invalid hexadecimal literal: {error}"</span><span>))</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0004"</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>.</span><span>with_span</span><span>(</span><span>self</span><span>.</span><span>span_from</span><span>(</span><span>start</span><span>))</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>from_str_radix</code> 对空字符串返回 <code>Err</code>，所以 <code>0x;</code> 自动被拒绝，不用单独检查；太大放不进 <code>i64</code> 也是 <code>Err</code>。<code>.map(TokenKind::Int)</code> 把枚举变体当函数用（<code>Int</code> 是一个 <code>fn(i64) -&gt; TokenKind</code>），<code>map_err</code> 把标准库的错误转成我们的 <code>Diagnostic</code>。</p><p>我在这个 crate 的一份临时拷贝里跑了它，加了两个测试：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>hex_literals</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>assert_eq!</span><span>(</span></div></div><div><div><div>4</div></div><div><span>        </span><span>kinds</span><span>(</span><span>"0xff 0X1_0 0"</span><span>),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>vec!</span><span>[</span><span>TokenKind</span><span>::</span><span>Int</span><span>(</span><span>255</span><span>), </span><span>TokenKind</span><span>::</span><span>Int</span><span>(</span><span>16</span><span>), </span><span>TokenKind</span><span>::</span><span>Int</span><span>(</span><span>0</span><span>), </span><span>TokenKind</span><span>::</span><span>Eof</span><span>]</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>#[test]</span></div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>empty_hex_literal_is_an_error</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>let</span><span> (</span><span>_</span><span>, </span><span>diagnostics</span><span><span>) </span><span>=</span><span> </span></span><span>tokenize</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"0x;"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>diagnostics</span><span>.</span><span>len</span><span>(), </span><span>1</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>diagnostics</span><span>[</span><span>0</span><span>].code.</span><span>as_deref</span><span>(), </span><span>Some</span><span>(</span><span>"E0004"</span><span>));</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结果 <code>14 passed</code>：原来的 12 个加这两个。注意 <code>0</code> 本身（后面不跟 <code>x</code>）走的还是原来的数字分支。</p><p><strong>2. 为什么 <code>fn longest(a: &amp;str, b: &amp;str) -&gt; &amp;str</code> 编译不过？</strong> 说出省略规则里哪一条在这里失效，写出修法，并构造一个让修好的版本依然报错的调用。</p>
参考答案<p>实验文件 <code>src/bin/ch04_longest.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>longest</span><span>(</span><span>a</span><span>: &amp;</span><span>str</span><span>, </span><span>b</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>if</span><span> </span><span>a</span><span>.</span><span>len</span><span><span>() </span><span>&gt;=</span><span> </span></span><span>b</span><span>.</span><span>len</span><span>() { </span><span>a</span><span> } </span><span>else</span><span> { </span><span>b</span><span> }</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>longest</span><span>(</span><span>"ab"</span><span>, </span><span>"abc"</span><span>));</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0106]: missing lifetime specifier</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch04_longest.rs:1:33</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>1 | fn longest(a: &amp;str, b: &amp;str) -&gt; &amp;str {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|               ----     ----     ^ expected named lifetime parameter</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`</span></div></div><div><div><div>8</div></div><div><span>help: consider introducing a named lifetime parameter</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>1 | fn longest&lt;'a&gt;(a: &amp;'a str, b: &amp;'a str) -&gt; &amp;'a str {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|           ++++     ++          ++          ++</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0106`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_longest") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>规则 1 给两个参数各一个独立的生命周期；规则 2 要求”恰好一个”输入生命周期，现在有两个，不适用；规则 3 要求有 <code>&amp;self</code>，没有。三条都落空，所以返回值借自谁没有答案。编译器的提示一语中的：“不知道是借自 <code>a</code> 还是 <code>b</code>”。</p><p>修法是显式地写：<code>fn longest&lt;'a&gt;(a: &amp;'a str, b: &amp;'a str) -&gt; &amp;'a str</code>，意思是”返回值借自两个参数里<strong>较短</strong>的那个区域”（<code>'a</code> 会被取成两者的交集）。把 <code>ch04_longest.rs</code> 里的签名换成这一行，<code>main</code> 不变，就是修好后的版本，打印 <code>abc</code>（文件名 <code>ch04_longest_ok.rs</code>，可选）。让它再次报错的调用是把结果用到某个参数已经死去的地方，<code>src/bin/ch04_longest_scope.rs</code> 的 <code>main</code>（<code>longest</code> 用修好后的签名）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>outer</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"abc"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>result</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>inner</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"ab"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>result</span><span><span> </span><span>=</span><span> </span></span><span>longest</span><span>(&amp;</span><span>outer</span><span>, &amp;</span><span>inner</span><span>);</span></div></div><div><div><div>7</div></div><div><span>        </span><span>println!</span><span>(</span><span>"inside:  {result}"</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>    </span><span>println!</span><span>(</span><span>"outside: {result}"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>报错如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0597]: `inner` does not live long enough</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch04_longest_scope.rs:10:34</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>9 |         let inner = String::from("ab");</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|             ----- binding `inner` declared here</span></div></div><div><div><div>6</div></div><div><span>10 |         result = longest(&amp;outer, &amp;inner);</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|                                  ^^^^^^ borrowed value does not live long enough</span></div></div><div><div><div>8</div></div><div><span>11 |         println!("inside:  {result}");</span></div></div><div><div><div>9</div></div><div><span>12 |     }</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|     - `inner` dropped here while still borrowed</span></div></div><div><div><div>11</div></div><div><span>13 |     println!("outside: {result}");</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|                         ------ borrow later used here</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>For more information about this error, try `rustc --explain E0597`.</span></div></div><div><div><div>15</div></div><div><span>error: could not compile `rust-lab` (bin "ch04_longest_scope") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>inner</code> 在内层块结束时死了，而 <code>result</code> 因为 <code>longest</code> 的签名被绑定到了两个参数中较短的那个区域，在块外还被使用。这个错误很讲理：签名说”结果可能借自 <code>inner</code>”，函数体里也许根本没借，但编译器只信签名。</p><p><strong>3. <code>peek_second</code> 每次都要重新解码，能不能避免？</strong> 看看 <code>skip_trivia</code> 和 <code>lex_number</code> 里的五处调用，想想它们都在比较什么。</p>
参考答案<p><code>peek_second</code> 调用 <code>chars()</code> 取两个字符，每次调用都要解码两个 UTF-8 字符。但五处调用有一个共同点：调用前，第一个字符都已经被匹配成了 ASCII（<code>'/'</code>、<code>'*'</code> 或 <code>'.'</code>），而且比较的第二个字符也都是 ASCII（<code>'/'</code>、<code>'*'</code>、数字）。ASCII 字符在 UTF-8 里恰好占一个字节，所以”第二个字符”就是原始字节的第二个字节，不需要解码：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>peek_second_byte</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>self</span><span>.</span><span>rest</span><span>().</span><span>as_bytes</span><span>().</span><span>get</span><span>(</span><span>1</span><span>).</span><span>copied</span><span>()</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>五处调用改成 <code>self.peek_second_byte() == Some(b'/')</code> 和 <code>self.peek_second_byte().is_some_and(|b| b.is_ascii_digit())</code>（<code>u8</code> 也有 <code>is_ascii_digit</code>，<code>b'/'</code> 是字节字面量）。就算第一个字符不是 ASCII，它的后续字节都 <code>&gt;= 0x80</code>，也不可能和任何 ASCII 字节相等，所以不会误判。</p><p>我在一份临时拷贝里做了这个替换（补丁是 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/exercises/ch04-peek.patch" target="_blank"><code>lab/exercises/ch04-peek.patch</code></a>），<code>cargo test -p pebble-lexer</code> 仍然是 <code>12 passed</code>。我没有测量它快了多少：这个词法分析器的瓶颈几乎肯定不在这里，我也就没有把它放进 tag。这是个”能做，但你应该先量一量值不值得”的优化。</p><p>我们现在有了一个能把源码切成 token、在出错之后继续往下走的词法分析器，而且每个 token 都被编译器证明活得不比源码长。下一篇我们要把 token 流变成一棵树。你会看到 AST 里没有 <code>'src</code>：每个节点自己拥有 <code>String</code>，这是一个有意识的取舍，生命周期不会像传染病一样爬满整个编译器。而这一篇没有回答的问题，一个类型可以包含它自己，会让我们遇到下一堵墙：递归类型的大小。继续阅读：<a href="/posts/rust-by-pebble-05-ast-generics/">递归类型、泛型与 trait：搭一棵 AST</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-03-enums-traits-diagnostics/</id>
      <title type="text">enum、trait 与第一份错误报告</title>
      <published>2026-10-13T02:00:00.000Z</published>
      <updated>2026-10-13T02:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-03-enums-traits-diagnostics/"/>
      <summary type="text">写 pebble-diag：用 enum 表示严重程度，用 builder 拼出带下划线的错误报告，用 trait 决定错误去哪。顺带弄清 Display 与 Debug、fmt::Write 与 io::Write、supertrait 和孤儿规则。</summary>
      <content type="html"><![CDATA[<p>Pebble 里最小的一个 bug 只有两个字符：把 <code>writeln!</code> 写成 <code>write!</code>，删掉 <code>ln</code>。编译器不抱怨，三个测试照样通过，但整份错误报告的第一行会变成 <code>error[E1001]: expected an expression, found `;` --&gt; examples/errors.pebble:5:17</code>，标题和位置箭头粘在了一起。抓住它的是整个 crate 里唯一一条断言，我们在本篇末尾会真的把这个 bug 种进去再看它怎么死。在那之前，先把这个 crate 从零写出来：357 行，4 个测试，用到 <code>enum</code>、<code>Option</code>、builder、<code>Display</code>、<code>Error</code>、带默认方法的 trait，以及一次和孤儿规则的正面冲突。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>我们将写完 <code>pebble-diag</code>：<code>Severity</code>、<code>Label</code>、<code>Diagnostic</code>、<code>DiagnosticSink</code> 以及把它们画成带 <code>^^^^</code> 下划线报告的 <code>render</code>。用到的 Rust 特性：带数据的 <code>enum</code> 与穷尽 <code>match</code>、<code>Option</code>、<code>if let</code> / <code>let ... else</code>、builder 模式与结构体更新语法、<code>Display</code> 与 <code>Debug</code>、<code>fmt::Write</code>、supertrait、默认方法、孤儿规则、类型别名、迭代器初体验。
本篇结束时 <code>crates/pebble-diag/</code> 的两个文件与 tag <code>tutorial-v1</code> 逐字节相同。上一篇：<a href="/posts/rust-by-pebble-02-ownership/">所有权：SourceMap 里的 String 归谁</a>，<code>pebble-span</code> 已经完成，本篇直接依赖它。</p></div></div>
<section><h2>错误应该是数据<a href="#错误应该是数据"><span>#</span></a></h2><p>先说我们要的东西长什么样。这是 Pebble 最终版本对 <code>examples/errors.pebble</code> 的报告（真实输出，不是手写的）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C 里的第一反应是在解析器报错的地方直接 <code>fprintf(stderr, "%s:%d: error: ...")</code>。我也写过很多次。它有三个问题：</p><ol>
<li>解析器深处不知道源码文本，打不出那行带 <code>^</code> 的原文。</li>
<li>没法测试：你要么捕获 stderr，要么 mock 一个 <code>FILE*</code>。</li>
<li>没法”先收集，后输出”。我们的解析器遇到第一个错误后会同步到下一条语句继续解析（第 7 篇），一次报出所有错误。边解析边打印的话，输出顺序和格式全由解析器决定。</li>
</ol><p>所以 Pebble 的设计是：<strong>报告一切，打印一无所有</strong>。这像实验室里的记录本：现场只负责把读数记下来，排版成报告是回办公室以后的事，而且同一份记录可以排成屏幕上的一页，也可以排成测试里的一个字符串。深处的代码只构造一个 <code>Diagnostic</code> 值（纯数据），交给某个”接收者”；真正需要文字的时候，再拿 <code>SourceMap</code> 去渲染。</p><div><div><div><div><span><p>emit</p></span></div><div><span></span></div><div><span><p>render 加 SourceMap</p></span></div><div><span></span></div><div><span><p>lexer / parser<br /><br />构造 Diagnostic</p></span></div><div><span><p>DiagnosticSink<br /><br />(Vec 或丢弃)</p></span></div><div><span><p>收集到的诊断列表</p></span></div><div><span><p>带 ^^^^ 的文字</p></span></div><div><span><p>stderr</p></span></div>
</div><div><div><span><p>emit</p></span></div><div><span></span></div><div><span><p>render 加 SourceMap</p></span></div><div><span></span></div><div><span><p>lexer / parser<br /><br />构造 Diagnostic</p></span></div><div><span><p>DiagnosticSink<br /><br />(Vec 或丢弃)</p></span></div><div><span><p>收集到的诊断列表</p></span></div><div><span><p>带 ^^^^ 的文字</p></span></div><div><span><p>stderr</p></span></div>
</div></div></div><p>这个图里每个方框都会变成本篇的一个类型或 trait。</p></section>
<section><h2>起步：第二个 crate<a href="#起步第二个-crate"><span>#</span></a></h2><p>在第 1 篇的工作区根目录（我的叫 <code>pebble-rebuild</code>，你的随意）运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>--lib</span><span> </span><span>crates/pebble-diag</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Creating library `pebble-diag` package</span></div></div><div><div><div>2</div></div><div><span>note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为目录在 workspace 里，<code>cargo new</code> 自己把 <code>version.workspace = true</code> 之类的继承行和 <code>[lints] workspace = true</code> 都写好了（其中也有 <code>description.workspace</code>、<code>keywords.workspace</code>、<code>categories.workspace</code> 三行）。我们要把 <code>description</code> 换成这个 crate 自己的一句话，删掉 <code>keywords</code> 和 <code>categories</code> 两行，再补一个依赖。先在根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 里，<code>pebble-span</code> 那行下面加一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pebble-diag</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-diag"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后让 <code>crates/pebble-diag/Cargo.toml</code> 成为下面这样（和 tag 一致）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-diag"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Diagnostics, labels, and human-readable rendering for Pebble."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>12</div></div><div><span>pebble-span.workspace</span><span> = </span><span>true</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>15</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>pebble-span.workspace = true</code> 的意思是”依赖 <code>pebble-span</code>，具体路径和版本去根 <code>Cargo.toml</code> 的 <code>[workspace.dependencies]</code> 里查”。它相当于 CMake 里 <code>target_link_libraries(pebble-diag PUBLIC pebble-span)</code>，区别是这里没有头文件要 include：依赖一声明，<code>pebble_span::Span</code> 就能直接用（crate 名里的 <code>-</code> 在代码里变成 <code>_</code>）。</p><p>现在把 <code>crates/pebble-diag/src/lib.rs</code> 清空，写文件头。前两行属性你在第 2 篇见过：测试之外 <code>no_std</code>，禁用 <code>unsafe</code>。接下来是 7 行 <code>//!</code> 模块文档（第 3 到 9 行）。第 8 行里有一个全角破折号，手敲容易走样，而我们的检查点要求逐字节相同，所以这 7 行直接从 tag 取：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>-C</span><span> </span><span>~/pebble-tag</span><span> </span><span>show</span><span> </span><span>tutorial-v1:crates/pebble-diag/src/lib.rs</span><span> | </span><span>sed</span><span> </span><span>-n</span><span> </span><span>3,9p</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（<code>~/pebble-tag</code> 是 tag 的本地克隆：<code>git clone --branch tutorial-v1 https://github.com/bfmhno3/pebble ~/pebble-tag</code>。第 1 篇的检查点已经让你克隆过；没有的话现在建。）</p><p>其余部分的文件头是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![cfg_attr(not(test), no_std)]</span></div></div><div><div><div>2</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>3</div></div><div><span>//! (这里是上面取到的第 3 到 9 行模块文档)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>extern</span><span> </span><span>crate</span><span> alloc;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::</span><span>string</span><span>::</span></span><span>String</span><span>;</span></div></div><div><div><div>8</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::</span><span>vec</span><span>::</span></span><span>Vec</span><span>;</span></div></div><div><div><div>9</div></div><div><span>use</span><span><span> </span><span>core</span><span>::</span><span>fmt</span><span>::{</span></span><span>self</span><span>, </span><span>Write</span><span> </span><span>as</span><span> _};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceMap</span><span>, </span><span>Span</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Write as _</code> 这个奇怪的写法我们很快会遇到，先照抄。</p></section>
<section><h2><code>Severity</code>：一个 enum，但不是 C 的那种<a href="#severity一个-enum但不是-c-的那种"><span>#</span></a></h2><p>错误、警告、提示、帮助：四个严重程度。C 程序员一秒钟写完：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>enum</span><span> Severity { ERROR, WARNING, NOTE, HELP };</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后在某处写一个 <code>switch</code> 把它转成字符串，几个月后有人加了第五个枚举值，忘了改 <code>switch</code>。我把这个场景真的编出来跑了一遍（<a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch03_sev.c" target="_blank"><code>lab/cpp/ch03_sev.c</code></a>，<code>gcc -Wall -Wextra</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sev.c: In function 'as_str':</span></div></div><div><div><div>2</div></div><div><span>sev.c:6:5: warning: enumeration value 'HELP' not handled in switch [-Wswitch]</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>6 |     switch (s) {</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>|     ^~~~~~</span></div></div><div><div><div>5</div></div><div><span>??? ???</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一条 warning，程序照常跑，打印出两个 <code>???</code>（后一个是我故意塞进去的 <code>(enum Severity)42</code>，C 的枚举不过是个 <code>int</code>，任何值都能强转进去）。</p><p>Rust 的 <code>enum</code> 是另一种东西：它是一个<strong>只能取列出的那几个值</strong>的类型，不能把任意整数强转成它，而且 <code>match</code> 必须穷尽。把同样的错误写给 Rust（实验文件 <code>src/bin/ch03_match.rs</code>，在第 1 篇建的 <code>~/rust-lab</code> 里）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>Severity</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Error</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Warning</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Note</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Help</span><span>,</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>fn</span><span> </span><span>as_str</span><span>(</span><span>severity</span><span>: </span><span>Severity</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>match</span><span> </span><span>severity</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Severity</span><span>::</span><span>Error</span><span> =&gt; </span><span>"error"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Severity</span><span>::</span><span>Warning</span><span> =&gt; </span><span>"warning"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>Severity</span><span>::</span><span>Note</span><span> =&gt; </span><span>"note"</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>--bin</span><span> </span><span>ch03_match</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0004]: non-exhaustive patterns: `Severity::Help` not covered</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch03_match.rs:10:11</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>10 |     match severity {</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|           ^^^^^^^^ pattern `Severity::Help` not covered</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>note: `Severity` defined here</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch03_match.rs:2:6</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>2 | enum Severity {</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|      ^^^^^^^^</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>6 |     Help,</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>|     ---- not covered</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>= note: the matched value is of type `Severity`</span></div></div><div><div><div>16</div></div><div><span>help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>18</div></div><div><span>13 ~         Severity::Note =&gt; "note",</span></div></div><div><div><div>19</div></div><div><span>14 ~         Severity::Help =&gt; todo!(),</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>For more information about this error, try `rustc --explain E0004`.</span></div></div><div><div><div>23</div></div><div><span>error: could not compile `rust-lab` (bin "ch03_match") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>同样的疏忽，C 给一条可以忽略的 warning，Rust 给一条无法忽略的 error。这是我最喜欢 <code>enum</code> 的地方：以后每增加一个变体，编译器会带我找到每一处需要修改的 <code>match</code>。第 5 篇里 AST 有几十个变体，这个性质会救我们很多次。</p><p>顺带认识语法：变体用 <code>Severity::Error</code> 这样的路径引用（不会像 C 那样污染外层作用域）；<code>match</code> 的每个分支是 <code>模式 =&gt; 表达式</code>，整个 <code>match</code> 本身是个表达式，值就是被选中分支的值，所以函数体里不需要 <code>return</code>（第 1 篇的”最后一个表达式不加分号”）。</p><section><h3>enum 还能带数据<a href="#enum-还能带数据"><span>#</span></a></h3><p>C 的 <code>enum</code> 只是整数。Rust 的变体可以各自携带不同形状的数据，这时它更像 C 里的 <code>tagged union</code>，或者 C++ 的 <code>std::variant</code>。我们这一章的 <code>Severity</code> 用不到这点，但整个系列都要用，所以趁早看一眼（实验文件 <code>src/bin/ch03_variant.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>enum</span><span> </span><span>Shape</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Point</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Circle</span><span>(</span><span>f64</span><span>),</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Rect</span><span> { </span><span>w</span><span>: </span><span>f64</span><span>, </span><span>h</span><span>: </span><span>f64</span><span> },</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>fn</span><span> </span><span>area</span><span>(</span><span>shape</span><span>: &amp;</span><span>Shape</span><span>) -&gt; </span><span>f64</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>match</span><span> </span><span>shape</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Shape</span><span>::</span><span>Point</span><span> =&gt; </span><span>0.0</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Shape</span><span>::</span><span>Circle</span><span>(</span><span>r</span><span>) =&gt; </span><span>3.0</span><span> * </span><span>r</span><span> * </span><span>r</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Shape</span><span>::</span><span>Rect</span><span> { </span><span>w</span><span>, </span><span>h</span><span> } =&gt; </span><span>w</span><span> * </span><span>h</span><span>,</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>--quiet</span><span> </span><span>--bin</span><span> </span><span>ch03_variant</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Point has area 0</span></div></div><div><div><div>2</div></div><div><span>Circle(2.0) has area 12</span></div></div><div><div><div>3</div></div><div><span>Rect { w: 2.0, h: 5.0 } has area 10</span></div></div><div><div><div>4</div></div><div><span>size_of::&lt;Shape&gt;() = 24</span></div></div><div><div><div>5</div></div><div><span>size_of::&lt;Option&lt;String&gt;&gt;() = 24</span></div></div><div><div><div>6</div></div><div><span>size_of::&lt;String&gt;() = 24</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三种形状：没有数据的、元组式的 <code>Circle(f64)</code>、结构体式的 <code>Rect { w, h }</code>。<code>match</code> 的分支里直接把数据解构成局部变量 <code>r</code>、<code>w</code>、<code>h</code>，没有 <code>std::get&lt;1&gt;(v)</code> 或 <code>std::holds_alternative</code> 那一套；并且和 <code>std::visit</code> 要求访问者覆盖所有备选类型一样，漏一个变体就编译失败。区别是语法：<code>std::variant</code> 是库类型，<code>std::visit</code> 加 <code>overloaded</code> 的写法大家都懂的；Rust 的 <code>enum</code> 是语言内建，<code>match</code> 是语言内建，错误信息也因此能直接告诉你缺哪个变体。</p><p>最后三行数字是个小彩蛋：<code>Option&lt;String&gt;</code> 和 <code>String</code> 一样大，都是 24 字节。<code>Option</code> 本质上就是一个带数据的 <code>enum</code>（<code>None</code> 和 <code>Some(T)</code>），而 <code>String</code> 的容量字段永远不会超过 <code>isize::MAX</code>，编译器把 <code>None</code> 编码成容量字段里的一个非法值，不需要额外的标签字节。我们很快就大量使用 <code>Option&lt;String&gt;</code>，这里记住：它不贵。</p></section><section><h3>写 <code>Severity</code><a href="#写-severity"><span>#</span></a></h3><p>回到 <code>pebble-diag</code>。在文件头后面写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// How serious a diagnostic is.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>enum</span><span> </span><span>Severity</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Error</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Warning</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>Note</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>Help</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>#[derive(...)]</code> 你在第 1 篇见过：<code>Copy</code> 和 <code>Clone</code> 让它能按位复制（四个无数据的变体，本质上就是一个字节），<code>PartialEq</code> 和 <code>Eq</code> 让它支持 <code>==</code>，<code>Debug</code> 让它能被 <code>{:?}</code> 打印。<code>Severity::Error</code> 里的 <code>Error</code> 是变体名，和后面的 <code>Error</code> trait 不冲突，因为变体总是通过 <code>Severity::</code> 前缀访问。</p><p>接着是把它转成字符串的方法，<code>as_str</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Severity</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// A lowercase, stable name for the severity.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>as_str</span><span>(</span><span>self</span><span>) -&gt; &amp;'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>match</span><span> </span><span>self</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Severity</span><span>::</span><span>Error</span><span> =&gt; </span><span>"error"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>Severity</span><span>::</span><span>Warning</span><span> =&gt; </span><span>"warning"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>            </span><span>Severity</span><span>::</span><span>Note</span><span> =&gt; </span><span>"note"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>Severity</span><span>::</span><span>Help</span><span> =&gt; </span><span>"help"</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有几个小点。<code>match self</code> 里直接写 <code>Severity::Error</code> 而不写 <code>*self</code>，因为 <code>self</code> 是按值传入的（<code>Severity</code> 是 <code>Copy</code>，传值是免费的）。<code>const fn</code> 表示这个函数可以在编译期求值。返回类型 <code>&amp;'static str</code> 的 <code>'static</code> 表示这个字符串活在整个程序运行期（字符串字面量就放在可执行文件的只读段里），它的完整含义留到<a href="/posts/rust-by-pebble-04-lifetimes-lexer/">第 4 篇</a>。<code>#[must_use]</code> 告诉编译器：如果调用者调用了 <code>as_str()</code> 又丢掉结果，给 warning，因为这样做几乎一定是 bug。</p></section><section><h3>Display 与 Debug：给人看的和给程序员看的<a href="#display-与-debug给人看的和给程序员看的"><span>#</span></a></h3><p>我们已经 derive 了 <code>Debug</code>，它会打印 <code>Error</code>、<code>Warning</code>，但那是给程序员调试的格式。用户在终端里希望看到小写的 <code>error</code>。Rust 里这两种输出是两个不同的 trait：</p><ul>
<li><code>Debug</code>（占位符 <code>{:?}</code>）：给程序员看，通常 <code>#[derive]</code> 一个。</li>
<li><code>Display</code>（占位符 <code>{}</code>）：给最终用户看，必须手写，因为”用户该看到什么”只有你知道。</li>
</ul><p>做一次对比（实验文件 <code>src/bin/ch03_format.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[derive(</span><span>Debug</span><span>)]</span></div></div><div><div><div>2</div></div><div><span>struct</span><span> </span><span>Label</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>start</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>message</span><span>: </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>label</span><span><span> </span><span>=</span><span> </span></span><span>Label</span><span> { </span><span>start</span><span>: </span><span>4</span><span>, </span><span>message</span><span>: </span><span>Some</span><span>(</span><span>"found `;`"</span><span>.</span><span>to_string</span><span>()) };</span></div></div><div><div><div>2</div></div><div><span>println!</span><span>(</span><span>"{label:?}"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"{label:#?}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Label { start: 4, message: Some("found `;`") }</span></div></div><div><div><div>2</div></div><div><span>Label {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>start: 4,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>message: Some(</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>"found `;`",</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>),</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>{:?}</code> 一行，<code>{:#?}</code> 多行缩进，两者都是编译器替你生成的。<code>{label}</code> 这样直接把变量名写进花括号是 Rust 1.58 起的”内联格式参数”，等价于 <code>println!("{}", label)</code>。</p><p>手写 <code>Display</code> 的样子是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Severity</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>self</span><span>.</span><span>as_str</span><span>())</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>impl fmt::Display for Severity</code> 读作”为 <code>Severity</code> 实现 <code>Display</code> trait”。trait 要求的唯一方法是 <code>fmt</code>，参数是 <code>&amp;self</code> 和一个 <code>Formatter</code>（可以理解成”写到哪里去”的句柄），返回 <code>fmt::Result</code>。<code>f.write_str(...)</code> 把一段文本写进去。这里没有 <code>return</code>，也没有 <code>;</code>：又是最后一个表达式。</p><p>以后任何地方写 <code>println!("{}", severity)</code> 或者 <code>format!("{severity}")</code>，调用的都是这个 <code>fmt</code>。</p></section></section>
<section><h2><code>Label</code> 和 <code>Diagnostic</code><a href="#label-和-diagnostic"><span>#</span></a></h2><p>一份诊断由三种东西组成：一个标题（“expected an expression”），若干个指向源码的标签（“这里”），和若干条末尾备注。标签可以带说明文字，也可以不带；诊断可以带错误码（<code>E0001</code>），也可以不带。</p><p>C 里”可能没有”通常用 <code>NULL</code>，或者在结构体里加一个 <code>has_message</code> 布尔。Rust 里是 <code>Option&lt;T&gt;</code>：它是 <code>enum Option&lt;T&gt; { None, Some(T) }</code>，不是特殊的语言机制。在你把 <code>Some</code> 拆开之前，没法碰里面的 <code>T</code>，所以不可能”忘了检查空指针”。</p><p>先写 <code>Label</code>（紧接在 <code>Severity</code> 的 <code>Display</code> 之后）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A pointer at one span of source, with an optional message.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Label</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Where to point.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// What to say about it.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>message</span><span>: </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Primary labels get the `^^^^` underline; secondary ones get `----`.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>primary</span><span>: </span><span>bool</span><span>,</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个字段都是 <code>pub</code>，意思是 crate 之外的代码可以直接读写它们（<code>Span</code> 在第 1 篇也是这样）。<code>primary</code> 决定下划线是 <code>^^^^</code> 还是 <code>----</code>：主标签指向”错误本身”，次标签指向”相关的东西”（比如”<code>a</code> 在这里定义”）。</p><p>两个构造函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Label</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// A primary label.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>primary</span><span>(</span><span>span</span><span>: </span><span>Span</span><span>, </span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>message</span><span>: </span><span>Some</span><span>(</span><span>message</span><span>.</span><span>into</span><span>()),</span></div></div><div><div><div>8</div></div><div><span>            </span><span>primary</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>/// A secondary label.</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>secondary</span><span>(</span><span>span</span><span>: </span><span>Span</span><span>, </span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>16</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>17</div></div><div><span>            </span><span>message</span><span>: </span><span>Some</span><span>(</span><span>message</span><span>.</span><span>into</span><span>()),</span></div></div><div><div><div>18</div></div><div><span>            </span><span>primary</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意这里有一个小细节：<code>Label</code> 有一个字段叫 <code>primary</code>，也有一个关联函数叫 <code>primary</code>。这没有冲突，Rust 里字段和方法在不同的命名空间，<code>label.primary</code> 是字段，<code>Label::primary(...)</code> 是函数。<code>impl Into&lt;String&gt;</code> 是第 2 篇的老朋友：调用者可以传 <code>&amp;str</code>，也可以传 <code>String</code>，函数内部 <code>message.into()</code> 统一变成 <code>String</code>。</p><p>再写 <code>Diagnostic</code> 本身：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A single compiler message.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Diagnostic</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// Error / warning / note / help.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>severity</span><span>: </span><span>Severity</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// An optional stable code, e.g. `E0001`.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>code</span><span>: </span><span>Option</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// The headline.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>message</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>/// Pointers into the source.</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>labels</span><span>: </span><span>Vec</span><span>&lt;</span><span>Label</span><span>&gt;,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>/// Trailing `= note:` lines.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>notes</span><span>: </span><span>Vec</span><span>&lt;</span><span>String</span><span>&gt;,</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Vec&lt;Label&gt;</code> 和 <code>Vec&lt;String&gt;</code> 为空时不分配堆内存，所以”没有标签""没有备注”不用 <code>Option</code> 包一层，空 <code>Vec</code> 就是”没有”。</p><section><h3>Builder：拼出一份诊断<a href="#builder拼出一份诊断"><span>#</span></a></h3><p>构造一份带错误码、带标签、带备注的诊断，你不想写一个十参数的构造函数。C++ 里常见的做法是链式调用 setter，返回 <code>*this</code>。Rust 的版本叫 builder：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Diagnostic</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// Creates an error with just a headline.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>error</span><span>(</span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>severity</span><span>: </span><span>Severity</span><span>::</span><span>Error</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>code</span><span>: </span><span>None</span><span>,</span></div></div><div><div><div>8</div></div><div><span>            </span><span>message</span><span>: </span><span>message</span><span>.</span><span>into</span><span>(),</span></div></div><div><div><div>9</div></div><div><span>            </span><span>labels</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>10</div></div><div><span>            </span><span>notes</span><span>: </span><span>Vec</span><span>::</span><span>new</span><span>(),</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>/// Creates a warning with just a headline.</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>16</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>warning</span><span>(</span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>severity</span><span>: </span><span>Severity</span><span>::</span><span>Warning</span><span>,</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>..</span><span>Self</span><span>::</span><span>error</span><span>(</span><span>message</span><span>)</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Diagnostic::error("...")</code> 是最基本的构造，其余字段都取默认值。<code>warning</code> 用到了<strong>结构体更新语法</strong>：<code>..Self::error(message)</code> 的意思是”其余字段照 <code>Self::error(message)</code> 那个值填”，我们只覆盖了 <code>severity</code>。（<code>Self</code> 是当前类型的别名，在 <code>impl Diagnostic</code> 里就是 <code>Diagnostic</code>。）</p><p>然后是一串”附加”方法。注意签名：<code>mut self</code>，返回 <code>Self</code>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Attaches a stable error code.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_code</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>code</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span><span>.code </span><span>=</span><span> </span></span><span>Some</span><span>(</span><span>code</span><span>.</span><span>into</span><span>());</span></div></div><div><div><div>5</div></div><div><span>        </span><span>self</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>/// Attaches a primary label.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_primary</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>, </span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span>.labels.</span><span>push</span><span><span>(</span><span>Label</span><span>::</span></span><span>primary</span><span>(</span><span>span</span><span>, </span><span>message</span><span>));</span></div></div><div><div><div>12</div></div><div><span>        </span><span>self</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>/// Attaches a secondary label.</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>17</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_secondary</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>, </span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>self</span><span>.labels.</span><span>push</span><span><span>(</span><span>Label</span><span>::</span></span><span>secondary</span><span>(</span><span>span</span><span>, </span><span>message</span><span>));</span></div></div><div><div><div>19</div></div><div><span>        </span><span>self</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>/// Attaches an unlabeled primary span.</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>24</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_span</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>self</span><span>.labels.</span><span>push</span><span>(</span><span>Label</span><span> {</span></div></div><div><div><div>26</div></div><div><span>            </span><span>span</span><span>,</span></div></div><div><div><div>27</div></div><div><span>            </span><span>message</span><span>: </span><span>None</span><span>,</span></div></div><div><div><div>28</div></div><div><span>            </span><span>primary</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>30</div></div><div><span>        </span><span>self</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>    </span><span>/// Appends a `= note:` line.</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>35</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>with_note</span><span>(</span><span>mut</span><span> </span><span>self</span><span>, </span><span>note</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>36</div></div><div><span>        </span><span>self</span><span>.notes.</span><span>push</span><span>(</span><span>note</span><span>.</span><span>into</span><span>());</span></div></div><div><div><div>37</div></div><div><span>        </span><span>self</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>with_code(mut self, ...) -&gt; Self</code> 拿走整个 <code>Diagnostic</code> 的所有权（第 2 篇的”移动”），改完再还回去，所以可以这样串起来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>diag</span><span><span> </span><span>=</span><span> </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"bad name"</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0001"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>.</span><span>with_span</span><span>(</span><span>span</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>.</span><span>with_note</span><span>(</span><span>"did you mean `y`?"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每一步旧值被移动进下一步，没有一次拷贝，没有任何地方持有半成品。<code>mut self</code> 的 <code>mut</code> 只是说”函数体里我可以改自己这份”，调用者那边的变量没有被改，因为它已经被移走了。这和 C++ 里返回 <code>Diagnostic&amp;</code> 的链式 setter 形成对比：那种写法把半成品的引用暴露给外面，Rust 的写法每一步都是一个独立的值。</p><p>每个 builder 方法前面的 <code>#[must_use]</code> 是有用的：如果你写了 <code>diag.with_code("E0001");</code> 然后丢掉返回值，编译器会警告，因为 <code>diag</code> 已经被移走，这行代码什么效果也没有。</p><p>我们把一个小例子放到实验文件 <code>src/bin/ch03_builder_update.rs</code> 里，同时顺便跑一下 <code>if let</code> 和 <code>let ... else</code>，它们马上要用在 <code>render</code> 里：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>describe</span><span>(</span><span>label</span><span>: </span><span>Option</span><span>&lt;&amp;</span><span>str</span><span>&gt;) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>Some</span><span>(</span><span>label</span><span><span>) </span><span>=</span><span> </span></span><span>label</span><span> </span><span>else</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>return</span><span> </span><span>"no label"</span><span>.</span><span>to_string</span><span>();</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>first</span><span><span>) </span><span>=</span><span> </span></span><span>label</span><span>.</span><span>chars</span><span>().</span><span>next</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>format!</span><span>(</span><span>"label {label} starts with {first}"</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>"empty label"</span><span>.</span><span>to_string</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Config { name: "demo", retries: 5, verbose: true }</span></div></div><div><div><div>2</div></div><div><span>Config { name: "demo", retries: 5, verbose: false }</span></div></div><div><div><div>3</div></div><div><span>no label</span></div></div><div><div><div>4</div></div><div><span>empty label</span></div></div><div><div><div>5</div></div><div><span>label x starts with x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（前两行来自同一个文件里的 <code>Config</code>，演示 <code>Config { verbose: false, ..base.clone() }</code> 这样的结构体更新。）</p><ul>
<li><code>if let Some(x) = opt { ... } else { ... }</code>：只关心一个模式的 <code>match</code>。</li>
<li><code>let Some(x) = opt else { return/continue/break };</code>：解包成功就继续往下用 <code>x</code>，失败就必须离开当前流程（<code>else</code> 块必须发散）。这比嵌套 <code>if let</code> 少缩进一层。</li>
</ul></section><section><h3>第一次迭代器：<code>primary_span</code><a href="#第一次迭代器primary_span"><span>#</span></a></h3><p>最后一个方法回答”该把箭头指到哪”：第一个主标签的 span。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The span the printer should focus on: the first primary label, if any.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>primary_span</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Span</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.labels</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>iter</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>find</span><span><span>(</span><span>|</span></span><span>label</span><span><span>|</span><span> </span></span><span>label</span><span>.primary)</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>label</span><span><span>|</span><span> </span></span><span>label</span><span>.span)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里是迭代器的第一次登场。<code>self.labels.iter()</code> 产生一个”借用每个元素”的迭代器（元素类型是 <code>&amp;Label</code>，没有复制任何标签）；<code>.find(|label| label.primary)</code> 找到第一个满足条件的元素，返回 <code>Option&lt;&amp;Label&gt;</code>；<code>.map(|label| label.span)</code> 对 <code>Option</code> 里的值做变换：<code>Some(label)</code> 变成 <code>Some(label.span)</code>，<code>None</code> 保持 <code>None</code>。<code>Span</code> 是 <code>Copy</code>，所以 <code>label.span</code> 直接复制出来，没有借用残留。竖线里的 <code>|label|</code> 是闭包参数，你在第 2 篇的 <code>partition_point</code> 里已经见过。</p><p>这几个适配器都是惰性的：<code>iter()</code> 本身什么也不做，<code>find</code> 一旦找到就停。我写了一个小实验（<code>src/bin/ch03_iter.rs</code>）确认迭代之后原集合依旧可用，同时看看 <code>sort_by_key</code> 和 <code>repeat_n</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>first primary starts at Some(4)</span></div></div><div><div><div>2</div></div><div><span>[Label { start: 4, primary: true }, Label { start: 12, primary: true }, Label { start: 30, primary: false }]</span></div></div><div><div><div>3</div></div><div><span>^^^^^</span></div></div><div><div><div>4</div></div><div><span>labels still usable: 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>impl Diagnostic</code> 还没写完，下一节要往里再加两个方法。为了现在就能编译，先在 <code>primary_span</code> 后面临时补一个 <code>}</code>；下一节会在它<strong>前面</strong>插新方法，插到最后一块（它自带 <code>impl</code> 真正的收尾 <code>}</code>）时，把这个临时的删掉。</p><p>此刻编译一下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>-p</span><span> </span><span>pebble-diag</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>Compiling pebble-diag v0.1.0 (/tmp/pebble-rebuild-03/crates/pebble-diag)</span></div></div><div><div><div>2</div></div><div><span>warning: unused import: `SourceMap`</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-diag/src/lib.rs:17:19</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>17 | use pebble_span::{SourceMap, Span};</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|                   ^^^^^^^^^</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>warning: unused import: `Write`</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-diag/src/lib.rs:15:23</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>15 | use core::fmt::{self, Write as _};</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>|                       ^^^^^</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>warning: `pebble-diag` (lib) generated 2 warnings (run `cargo fix --lib -p pebble-diag` to apply 1 suggestion)</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>通过，只是提示我们有两个 <code>use</code> 还没派上用场。它们下一节就会用到。</p></section></section>
<section><h2><code>render</code>：把诊断画出来<a href="#render把诊断画出来"><span>#</span></a></h2><p><code>render</code> 要做的是：对每个标签，找到所在文件，算出行号和列号，打印 <code>--&gt; 文件:行:列</code>，打印原文那一行，再在下面画 <code>^^^^</code> 和说明。</p><section><h3>往 <code>String</code> 里 <code>write!</code><a href="#往-string-里-write"><span>#</span></a></h3><p>最自然的做法是：新建一个 <code>String</code>，一行一行地”打印”到里面去。C 里你用 <code>snprintf</code> 或者 <code>std::ostringstream</code>；Rust 里对应的是 <code>write!</code> 宏，它像 <code>println!</code> 但目标是任何”能写文本的东西”。把这个试一下（实验文件 <code>src/bin/ch03_write_import.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::fmt;</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>header</span><span>(</span><span>out</span><span>: &amp;</span><span>mut</span><span> </span><span>String</span><span>, </span><span>code</span><span>: </span><span>u32</span><span><span>) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>write!</span><span>(</span><span>out</span><span>, </span><span>"error[E{code:04}]"</span><span>)</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>build</span><span> </span><span>--bin</span><span> </span><span>ch03_write_import</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0599]: cannot write into `&amp;mut String`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch03_write_import.rs:4:12</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>4 |     write!(out, "error[E{code:04}]")</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|            ^^^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>note: must implement `io::Write`, `fmt::Write`, or have a `write_fmt` method</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch03_write_import.rs:4:12</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>4 |     write!(out, "error[E{code:04}]")</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|            ^^^</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>= help: items from traits can only be used if the trait is in scope</span></div></div><div><div><div>13</div></div><div><span>help: trait `Write` which provides `write_fmt` is implemented but not in scope; perhaps you want to import it</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>1 + use std::fmt::Write;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>For more information about this error, try `rustc --explain E0599`.</span></div></div><div><div><div>19</div></div><div><span>error: could not compile `rust-lab` (bin "ch03_write_import") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>String</code> 当然实现了写入，但要调用 trait 的方法，<strong>这个 trait 必须在作用域里</strong>。这是 Rust 和 C++ 一个重要区别：C++ 的成员函数属于类，写在类里面，永远在；Rust 的 trait 方法是”外挂”的，你得 <code>use</code> 那个 trait。</p><p><code>write!</code> 展开后调用的是 <code>out.write_fmt(...)</code>，而标准库里有<strong>两个</strong>都叫 <code>Write</code> 的 trait：</p><ul>
<li><code>std::fmt::Write</code>：往<strong>文本</strong>里写（<code>String</code>、<code>Formatter</code>）。</li>
<li><code>std::io::Write</code>：往<strong>字节流</strong>里写（文件、socket、<code>Stdout</code>）。</li>
</ul><p><code>io::Write</code> 是 <code>std</code> 的东西；<code>no_std</code> 的 <code>pebble-diag</code> 没有 <code>std::io</code>，所以我们用 <code>fmt::Write</code>。导入时写成 <code>use core::fmt::Write as _;</code>：<code>as _</code> 的意思是”我只要这个 trait 的方法，不要它的名字”，免得和 <code>fmt::Result</code>、<code>fmt::Display</code> 这些混在一起。你刚才写的文件头里已经有了这一行。</p><p>还有一点：<code>write!</code> 返回 <code>fmt::Result</code>，因为对”任意写入目标”来说，写入可能失败；对 <code>String</code> 来说永远不会。<code>Result</code> 是 <code>#[must_use]</code> 的，直接丢掉会警告，所以我们写 <code>let _ = write!(...)</code>，明确表示”我知道有个结果，我丢掉它”。</p></section><section><h3>格式化小语言<a href="#格式化小语言"><span>#</span></a></h3><p><code>render</code> 里用到了不少格式规则，我在 <code>ch03_format.rs</code> 里把它们都试了一遍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>line_no</span><span><span> </span><span>=</span><span> </span></span><span>7</span><span>;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>width</span><span><span> </span><span>=</span><span> </span></span><span>4</span><span>;</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"[{line_no}]"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"[{line_no:&gt;width$}]"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>println!</span><span>(</span><span>"[{line_no:&lt;width$}]"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>println!</span><span>(</span><span>"[{line_no:0&gt;width$}]"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>println!</span><span>(</span><span>"[{:&gt;1$}]"</span><span>, </span><span>line_no</span><span>, </span><span>6</span><span>);</span></div></div><div><div><div>8</div></div><div><span>println!</span><span>(</span><span>"{:x} {:#x} {:b}"</span><span>, </span><span>255</span><span>, </span><span>255</span><span>, </span><span>5</span><span>);</span></div></div><div><div><div>9</div></div><div><span>println!</span><span>(</span><span>"{:.3}"</span><span>, </span><span>3.14159</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[7]</span></div></div><div><div><div>2</div></div><div><span>[   7]</span></div></div><div><div><div>3</div></div><div><span>[7   ]</span></div></div><div><div><div>4</div></div><div><span>[0007]</span></div></div><div><div><div>5</div></div><div><span>[     7]</span></div></div><div><div><div>6</div></div><div><span>ff 0xff 101</span></div></div><div><div><div>7</div></div><div><span>3.142</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>{:&gt;width$}</code> 的意思是右对齐，宽度取自变量 <code>width</code>；<code>{:0&gt;4}</code> 用 <code>0</code> 填充；<code>{:x}</code> 十六进制，<code>{:#x}</code> 带 <code>0x</code> 前缀，<code>{:b}</code> 二进制，<code>{:.3}</code> 三位小数。语法对 C 程序员几乎是 <code>printf</code> 的平行宇宙，唯一的大区别是编译期检查：格式串和参数个数、类型对不上，直接编译失败，而不是 <code>-Wformat</code> 的 warning 或者运行时的 UB。</p></section><section><h3><code>render</code> 本体<a href="#render-本体"><span>#</span></a></h3><p>有了这些，把 <code>render</code> 写在 <code>primary_span</code> 后面、临时的 <code>}</code> 前面（先看开头）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Renders the diagnostic against a source map into a multi-line string.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// The output mimics the `rustc` style:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>///</span></div></div><div><div><div>5</div></div><div><span>    </span><span>/// ```text</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// error[E0001]: unexpected token</span></div></div><div><div><div>7</div></div><div><span>    </span><span>///   --&gt; demo.pebble:4:13</span></div></div><div><div><div>8</div></div><div><span>    </span><span>///    |</span></div></div><div><div><div>9</div></div><div><span>    </span><span>///  4 |     let x = ;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>///    |             ^ expected an expression</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// ```</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>render</span><span>(&amp;</span><span>self</span><span>, </span><span>sources</span><span>: &amp;</span><span>SourceMap</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>out</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>15</div></div><div><span>        </span><span>self</span><span>.</span><span>write_header</span><span>(&amp;</span><span>mut</span><span> </span><span>out</span><span>);</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>        </span><span>// Stable order so snapshots do not depend on construction order.</span></div></div><div><div><div>18</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>labels</span><span>: </span><span>Vec</span><span>&lt;&amp;</span><span>Label</span><span><span>&gt; </span><span>=</span><span> </span></span><span>self</span><span>.labels.</span><span>iter</span><span>().</span><span>collect</span><span>();</span></div></div><div><div><div>19</div></div><div><span>        </span><span>labels</span><span>.</span><span>sort_by_key</span><span><span>(</span><span>|</span></span><span>label</span><span><span>|</span><span> (</span></span><span>label</span><span>.span.source, </span><span>label</span><span>.span.start));</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>sources.get(...)</code> 返回的是 <code>Option&lt;&amp;SourceFile&gt;</code>（第 2 篇），有些标签可能指向一个不在 <code>SourceMap</code> 里的文件 id，我们选择跳过它们。</p><p><code>labels.sort_by_key(|label| (label.span.source, label.span.start))</code> 有一个小技巧：元组按字典序比较，所以先按文件，再按偏移排序。能这样排序，是因为 <code>SourceId</code> 的 <code>#[derive(...)]</code> 里有 <code>Ord</code>（回头看一眼 <code>pebble-span</code>）。排序的是 <code>Vec&lt;&amp;Label&gt;</code>：里面装的是对原标签的引用，没有复制任何一个 <code>Label</code>。我们这么做是为了让输出不依赖于调用者添加标签的先后（“稳定顺序，快照测试才不会随机失败”）。</p><p>接着是循环内部：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>for</span><span> </span><span>label</span><span> </span><span>in</span><span> </span><span>labels</span><span> {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>let</span><span> </span><span>Some</span><span>(</span><span>file</span><span><span>) </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>get</span><span>(</span><span>label</span><span>.span.source) </span><span>else</span><span> {</span></div></div><div><div><div>3</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>};</span></div></div><div><div><div>5</div></div><div><span>            </span><span>let</span><span> </span><span>pos</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>position</span><span>(</span><span>label</span><span>.span.start);</span></div></div><div><div><div>6</div></div><div><span>            </span><span>let</span><span> </span><span>line_no</span><span><span> </span><span>=</span><span> </span></span><span>pos</span><span>.line;</span></div></div><div><div><div>7</div></div><div><span>            </span><span>let</span><span> </span><span>width</span><span><span> </span><span>=</span><span> </span></span><span>digits</span><span>(</span><span>line_no</span><span>);</span></div></div><div><div><div>8</div></div><div><span>            </span><span>let</span><span> </span><span>pad</span><span><span> </span><span>=</span><span> </span></span><span>" "</span><span>.</span><span>repeat</span><span>(</span><span>width</span><span>);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>"{pad}--&gt; {}:{}:{}"</span><span>, </span><span>file</span><span>.</span><span>name</span><span>(), </span><span>line_no</span><span>, </span><span>pos</span><span>.column);</span></div></div><div><div><div>11</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>"{pad} |"</span><span>);</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>            </span><span>let</span><span> </span><span>line_text</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>line</span><span>(</span><span>line_no</span><span>).</span><span>unwrap_or</span><span>(</span><span>""</span><span>);</span></div></div><div><div><div>14</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>"{line_no:&gt;width$} | {line_text}"</span><span>);</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>            </span><span>let</span><span> (</span><span>caret_col</span><span>, </span><span>caret_len</span><span><span>) </span><span>=</span><span> </span></span><span>caret_geometry</span><span>(</span><span>file</span><span>, </span><span>label</span><span>.span, </span><span>line_no</span><span>, </span><span>line_text</span><span>);</span></div></div><div><div><div>17</div></div><div><span>            </span><span>let</span><span> </span><span>marker</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>label</span><span>.primary { </span><span>'^'</span><span> } </span><span>else</span><span> { </span><span>'-'</span><span> };</span></div></div><div><div><div>18</div></div><div><span>            </span><span>let</span><span> </span><span>carets</span><span>: </span><span>String</span><span><span> </span><span>=</span><span> </span><span>core</span><span>::</span><span>iter</span><span>::</span></span><span>repeat_n</span><span>(</span><span>marker</span><span>, </span><span>caret_len</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>19</div></div><div><span>            </span><span>let</span><span> </span><span>lead</span><span><span> </span><span>=</span><span> </span></span><span>" "</span><span>.</span><span>repeat</span><span>(</span><span>caret_col</span><span>);</span></div></div><div><div><div>20</div></div><div><span>            </span><span>match</span><span> &amp;</span><span>label</span><span>.message {</span></div></div><div><div><div>21</div></div><div><span>                </span><span>Some</span><span>(</span><span>message</span><span>) =&gt; {</span></div></div><div><div><div>22</div></div><div><span>                    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>"{pad} | {lead}{carets} {message}"</span><span>);</span></div></div><div><div><div>23</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>                </span><span>None</span><span> =&gt; {</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>"{pad} | {lead}{carets}"</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>"{pad} |"</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>循环的第一行就是 <code>let ... else</code>：找不到文件就 <code>continue</code> 去处理下一个标签。<code>"{pad}--&gt; ..."</code> 里的 <code>{pad}</code> 是内联参数，<code>{line_no:&gt;width$}</code> 是我们刚学的右对齐。<code>core::iter::repeat_n(marker, caret_len).collect()</code> 产生 <code>caret_len</code> 个 <code>marker</code> 字符，收集成 <code>String</code>，<code>let carets: String = ...</code> 左边的类型标注告诉 <code>collect</code> 应该收集成什么。<code>" ".repeat(n)</code> 是 <code>str</code> 的方法。</p><div><div><div></div><div><code>{pad}</code> 的宽度从哪来？</div></div><div><p><code>digits(line_no)</code> 是行号的位数，<code>pad</code> 是同样宽度的空格，让 <code>--&gt;</code> 和 <code>|</code> 在不同行号位数下都对齐。位数从哪来，下一小节说。</p></div></div><p>最后是备注和返回，然后是 <code>write_header</code>，然后是 <code>impl</code> 真正的收尾 <code>}</code>（现在可以删掉那个临时的了）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>        </span><span>for</span><span> </span><span>note</span><span> </span><span>in</span><span> &amp;</span><span>self</span><span>.notes {</span></div></div><div><div><div>2</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>"  = note: {note}"</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>        </span><span>out</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>write_header</span><span>(&amp;</span><span>self</span><span>, </span><span>out</span><span>: &amp;</span><span>mut</span><span> </span><span>String</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>write!</span><span>(</span><span>out</span><span>, </span><span>"{}"</span><span>, </span><span>self</span><span>.severity);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>code</span><span><span>) </span><span>=</span><span> &amp;</span></span><span>self</span><span>.code {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>write!</span><span>(</span><span>out</span><span>, </span><span>"[{code}]"</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>        </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>writeln!</span><span>(</span><span>out</span><span>, </span><span>": {}"</span><span>, </span><span>self</span><span>.message);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>write_header</code> 里 <code>if let Some(code) = &amp;self.code</code> 的 <code>&amp;self.code</code> 是 <code>&amp;Option&lt;String&gt;</code>，所以 <code>code</code> 的类型是 <code>&amp;String</code>，没有移动任何东西。<strong>注意最后一行的 <code>writeln!</code></strong>：它在消息后面追加换行。留着这个细节，本篇结尾我们会回来。</p></section><section><h3><code>digits</code> 的三种写法<a href="#digits-的三种写法"><span>#</span></a></h3><p><code>render</code> 里用了一个小函数 <code>digits</code>，数 <code>n</code> 有几位十进制数。写在 <code>impl Diagnostic</code> 块后面（后面的 <code>Display</code> 和 <code>DiagnosticSink</code> 会插进来，最终 <code>digits</code> 在它们后面）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Number of decimal digits in `n` (at least 1).</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>digits</span><span>(</span><span>n</span><span>: </span><span>u32</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>n</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>count</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>while</span><span> </span><span>n</span><span><span> </span><span>&gt;=</span><span> </span></span><span>10</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>n</span><span><span> </span><span>/=</span><span> </span></span><span>10</span><span>;</span></div></div><div><div><div>7</div></div><div><span>        </span><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>    </span><span>count</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我同时写了三个版本放到实验里（<code>src/bin/ch03_digits.rs</code>），顺便展示几个语法点：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>digits_loop</span><span>(</span><span>mut</span><span> </span><span>n</span><span>: </span><span>u32</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>count</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> </span><span>n</span><span><span> </span><span>&lt;</span><span> </span></span><span>10</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>break</span><span> </span><span>count</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>        </span><span>n</span><span><span> </span><span>/=</span><span> </span></span><span>10</span><span>;</span></div></div><div><div><div>8</div></div><div><span>        </span><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>fn</span><span> </span><span>digits_ilog</span><span>(</span><span>n</span><span>: </span><span>u32</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>n</span><span>.</span><span>checked_ilog10</span><span>().</span><span>map_or</span><span>(</span><span>1</span><span><span>, </span><span>|</span></span><span>log</span><span><span>|</span><span> </span></span><span>log</span><span> </span><span>as</span><span> </span><span>usize</span><span> + </span><span>1</span><span>)</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第 1 篇的 <code>loop { break value }</code> 在这里派上用场；<code>checked_ilog10</code> 对 0 返回 <code>None</code>（0 没有对数），否则返回 <code>Some(floor(log10(n)))</code>，<code>map_or(1, ...)</code> 把 <code>None</code> 映射成 1。跑一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>         </span></span><span>0: while=1 loop=1 ilog=1</span></div></div><div><div><div>2</div></div><div><span><span>         </span></span><span>9: while=1 loop=1 ilog=1</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>10: while=2 loop=2 ilog=2</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>99: while=2 loop=2 ilog=2</span></div></div><div><div><div>5</div></div><div><span><span>       </span></span><span>100: while=3 loop=3 ilog=3</span></div></div><div><div><div>6</div></div><div><span><span>     </span></span><span>12345: while=5 loop=5 ilog=5</span></div></div><div><div><div>7</div></div><div><span>4294967295: while=10 loop=10 ilog=10</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个版本结果一致（实验里有 <code>assert_eq!</code>）。仓库保留了 <code>while</code> 版本：它是最没有”聪明”的那一个，在 <code>render</code> 里调一次，没有理由选更难读的。<code>let mut n = n;</code> 这一行是<strong>遮蔽</strong>（第 1 篇）：参数 <code>n</code> 是不可变的，我们新建一个同名可变的绑定，把原来的盖住。</p></section><section><h3><code>caret_geometry</code>：字符，不是字节<a href="#caret_geometry字符不是字节"><span>#</span></a></h3><p>下划线要画在哪一列、画多长，在 UTF-8 里不能直接用字节偏移：<code>"中c"</code> 里 <code>c</code> 的字节偏移是 3，但它在第 2 列。第 2 篇已经遇到过这个问题。这个函数把 <code>Span</code> 的字节范围换算成字符列，放在 <code>digits</code> 之后、<code>#[cfg(test)]</code> 之前（它是文件里最后一个非测试项）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Computes the caret start column and length, in characters, for a span.</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>caret_geometry</span><span>(</span></div></div><div><div><div>3</div></div><div><span>    </span><span>file</span><span><span>: &amp;</span><span>pebble_span</span><span>::</span></span><span>SourceFile</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>span</span><span>: </span><span>Span</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>line_no</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>line_text</span><span>: &amp;</span><span>str</span><span>,</span></div></div><div><div><div>7</div></div><div><span>) -&gt; (</span><span>usize</span><span>, </span><span>usize</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>line_start</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>line_start</span><span>(</span><span>line_no</span><span>).</span><span>unwrap_or</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>let</span><span> </span><span>byte_in_line</span><span><span> </span><span>=</span><span> </span></span><span>span</span><span>.start.</span><span>saturating_sub</span><span>(</span><span>line_start</span><span>) </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>let</span><span> </span><span>byte_in_line</span><span><span> </span><span>=</span><span> </span></span><span>byte_in_line</span><span>.</span><span>min</span><span>(</span><span>line_text</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>// Character columns, not byte columns, so CJK lines line up.</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>caret_col</span><span><span> </span><span>=</span><span> </span></span><span>line_text</span><span>[..</span><span>byte_in_line</span><span>].</span><span>chars</span><span>().</span><span>count</span><span>();</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>line_end</span><span><span> </span><span>=</span><span> </span></span><span>line_start</span><span> + </span><span>line_text</span><span>.</span><span>len</span><span>() </span><span>as</span><span> </span><span>u32</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>let</span><span> </span><span>covered_end</span><span><span> </span><span>=</span><span> </span></span><span>span</span><span>.end.</span><span>min</span><span>(</span><span>line_end</span><span>).</span><span>max</span><span>(</span><span>span</span><span>.start);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>covered</span><span><span> </span><span>=</span><span> (</span></span><span>covered_end</span><span> - </span><span>span</span><span>.start) </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>slice_end</span><span><span> </span><span>=</span><span> (</span></span><span>byte_in_line</span><span> + </span><span>covered</span><span>).</span><span>min</span><span>(</span><span>line_text</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>caret_len</span><span><span> </span><span>=</span><span> </span></span><span>line_text</span><span>[</span><span>byte_in_line</span><span>..</span><span>slice_end</span><span>].</span><span>chars</span><span>().</span><span>count</span><span>().</span><span>max</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>(</span><span>caret_col</span><span>, </span><span>caret_len</span><span>)</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>saturating_sub</code>、<code>.min</code>、<code>.max</code> 都是为了不 panic：<code>Span</code> 可能超出这一行，也可能被 <code>Span::DUMMY</code> 之类的东西弄成怪值。<code>line_text[..byte_in_line].chars().count()</code> 就是第 2 篇的”切片后数字符”，<code>byte_in_line.min(line_text.len())</code> 保证切片不越界。返回元组 <code>(usize, usize)</code>，调用处用 <code>let (caret_col, caret_len) = ...</code> 解构。</p></section><section><h3>三个测试<a href="#三个测试"><span>#</span></a></h3><p>在文件末尾写测试模块（先写前三个，第四个和下一节的 trait 一起写；写完这三个之后，补一个 <code>}</code> 关闭 <code>mod tests</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>renders_a_single_caret_line</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"demo.pebble"</span><span>, </span><span>"let x = 1;</span><span>\n</span><span>let y = ;</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>let</span><span> </span><span>span</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span>::</span><span>new</span><span>(</span><span>id</span><span>, </span><span>19</span><span>, </span><span>19</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>diag</span><span><span> </span><span>=</span><span> </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"expected an expression"</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0001"</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>.</span><span>with_primary</span><span>(</span><span>span</span><span>, </span><span>"found `;`"</span><span>);</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>        </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>diag</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>assert!</span><span>(</span></div></div><div><div><div>16</div></div><div><span>            </span><span>rendered</span><span>.</span><span>starts_with</span><span>(</span><span>"error[E0001]: expected an expression</span><span>\n</span><span>"</span><span>),</span></div></div><div><div><div>17</div></div><div><span>            </span><span>"header must be on its own line:</span><span>\n</span><span>{rendered}"</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>assert!</span><span>(</span><span>rendered</span><span>.</span><span>contains</span><span>(</span><span>"--&gt; demo.pebble:2:9"</span><span>));</span></div></div><div><div><div>20</div></div><div><span>        </span><span>assert!</span><span>(</span><span>rendered</span><span>.</span><span>contains</span><span>(</span><span>"^ found `;`"</span><span>));</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>24</div></div><div><span>    </span><span>fn</span><span> </span><span>caret_length_tracks_span_length</span><span>() {</span></div></div><div><div><div>25</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>26</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"demo.pebble"</span><span>, </span><span>"let abcdef = 1;</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>27</div></div><div><span>        </span><span>// Span covering `abcdef` (bytes 4..10).</span></div></div><div><div><div>28</div></div><div><span>        </span><span>let</span><span> </span><span>diag</span><span><span> </span><span>=</span><span> </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"bad name"</span><span>).</span><span>with_span</span><span><span>(</span><span>Span</span><span>::</span></span><span>new</span><span>(</span><span>id</span><span>, </span><span>4</span><span>, </span><span>10</span><span>));</span></div></div><div><div><div>29</div></div><div><span>        </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>diag</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>assert!</span><span>(</span><span>rendered</span><span>.</span><span>contains</span><span>(</span><span>"^^^^^^"</span><span>), </span><span>"got:</span><span>\n</span><span>{rendered}"</span><span>);</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>34</div></div><div><span>    </span><span>fn</span><span> </span><span>notes_are_rendered</span><span>() {</span></div></div><div><div><div>35</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>36</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"demo.pebble"</span><span>, </span><span>"x</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>37</div></div><div><span>        </span><span>let</span><span> </span><span>diag</span><span><span> </span><span>=</span><span> </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"nope"</span><span>)</span></div></div><div><div><div>38</div></div><div><span><span>            </span></span><span>.</span><span>with_span</span><span><span>(</span><span>Span</span><span>::</span></span><span>new</span><span>(</span><span>id</span><span>, </span><span>0</span><span>, </span><span>1</span><span>))</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>.</span><span>with_note</span><span>(</span><span>"did you mean `y`?"</span><span>);</span></div></div><div><div><div>40</div></div><div><span>        </span><span>assert!</span><span>(</span><span>diag</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>).</span><span>contains</span><span>(</span><span>"= note: did you mean `y`?"</span><span>));</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>assert!</code> 和 <code>assert_eq!</code> 是你熟悉的断言。注意 <code>assert!</code> 的第二个参数：失败时打印的消息，可以是带 <code>{rendered}</code> 的格式串。第一个测试里 <code>starts_with("error[E0001]: expected an expression\n")</code> 就是我说的”整个 crate 里最重要的断言”，保存好这个印象。第二个测试里 <code>Span::new(id, 4, 10)</code> 刚好盖住 <code>abcdef</code>（字节 4 到 10），所以应该有六个 <code>^</code>。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-diag</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 3 tests</span></div></div><div><div><div>2</div></div><div><span>test tests::caret_length_tracks_span_length ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::notes_are_rendered ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::renders_a_single_caret_line ... ok</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（上面的输出省略了 <code>cargo test</code> 的编译提示和 <code>Doc-tests</code> 部分，每一行都没有改。）</p></section><section><h3>亲眼看看<a href="#亲眼看看"><span>#</span></a></h3><p>测试是对的，但我想看到一份真正的报告，一个两个标签的那种。在 <code>crates/pebble-diag/examples/demo.rs</code> 写（<code>examples/</code> 里的每个 <code>.rs</code> 文件都是一个独立的小程序，<code>cargo run --example demo</code> 运行；我们只是借它用一下，检查点之前会删掉）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_diag</span><span>::</span></span><span>Diagnostic</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::{</span></span><span>SourceMap</span><span>, </span><span>Span</span><span>};</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>sources</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>sources</span><span>.</span><span>add</span><span>(</span><span>"demo.pebble"</span><span>, </span><span>"let a = 1;</span><span>\n</span><span>let b = a + c;</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>diag</span><span><span> </span><span>=</span><span> </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"undefined variable `c`"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>.</span><span>with_code</span><span>(</span><span>"E0100"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>.</span><span>with_primary</span><span><span>(</span><span>Span</span><span>::</span></span><span>new</span><span>(</span><span>id</span><span>, </span><span>23</span><span>, </span><span>24</span><span>), </span><span>"not found in this scope"</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>.</span><span>with_secondary</span><span><span>(</span><span>Span</span><span>::</span></span><span>new</span><span>(</span><span>id</span><span>, </span><span>4</span><span>, </span><span>5</span><span>), </span><span>"`a` is defined here"</span><span>)</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>.</span><span>with_note</span><span>(</span><span>"variables must be declared with `let`"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{}"</span><span>, </span><span>diag</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>));</span></div></div><div><div><div>13</div></div><div><span>    </span><span>println!</span><span>(</span><span>"---"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{diag}"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{diag:?}"</span><span>);</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>-q</span><span> </span><span>-p</span><span> </span><span>pebble-diag</span><span> </span><span>--example</span><span> </span><span>demo</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `Diagnostic` doesn't implement `std::fmt::Display`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; crates/pebble-diag/examples/demo.rs:14:15</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>14 |     println!("{diag}");</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|               ^^^^^^ `Diagnostic` cannot be formatted with the default formatter</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= help: the trait `std::fmt::Display` is not implemented for `Diagnostic`</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>11</div></div><div><span>error: could not compile `pebble-diag` (example "demo") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是我故意埋的：我们给 <code>Severity</code> 写了 <code>Display</code>，没给 <code>Diagnostic</code> 写。<code>Diagnostic</code> 上 <code>{:?}</code> 能用（它 derive 了 <code>Debug</code>），<code>{}</code> 不行。这个错误是编译期的，没有”链接时才发现缺 <code>operator&lt;&lt;</code>”。</p></section></section>
<section><h2><code>Display</code>、<code>Error</code>，以及孤儿规则<a href="#displayerror以及孤儿规则"><span>#</span></a></h2><p>把下面这段写在 <code>impl Diagnostic</code> 块之后、<code>digits</code> 之前，先给 <code>Diagnostic</code> 写 <code>Display</code>。为什么不是 <code>render</code>？因为 <code>render</code> 需要 <code>SourceMap</code>，而 <code>Display::fmt</code> 只有 <code>&amp;self</code> 和一个 <code>Formatter</code>。所以 <code>Display</code> 输出的是不带源码的一行摘要，<code>error[E0100]: undefined variable `c`</code>。同一段里还有一行很短的代码，<code>Error</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// `core::error::Error` is available even in `no_std` since Rust 1.81.</span></div></div><div><div><div>2</div></div><div><span>impl</span><span><span> </span><span>core</span><span>::</span><span>error</span><span>::</span></span><span>Error</span><span> </span><span>for</span><span> </span><span>Diagnostic</span><span> {}</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Diagnostic</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>code</span><span><span>) </span><span>=</span><span> &amp;</span></span><span>self</span><span>.code {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{}[{}]: {}"</span><span>, </span><span>self</span><span>.severity, </span><span>code</span><span>, </span><span>self</span><span>.message)</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{}: {}"</span><span>, </span><span>self</span><span>.severity, </span><span>self</span><span>.message)</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>if let Some(code) = &amp;self.code { ... } else { ... }</code> 在这里是表达式（两个分支的值类型都是 <code>fmt::Result</code>），所以它直接是函数的返回值。<code>write!</code> 对 <code>Formatter</code> 同样有效：<code>Formatter</code> 也实现了 <code>fmt::Write</code>。</p><p><code>Error</code> 是标准库里”这是一个错误类型”的标志 trait。<code>impl core::error::Error for Diagnostic {}</code> 里<strong>什么方法都没写</strong>，因为 <code>Error</code> 的方法全有默认实现。它有什么用？以后 <code>Result&lt;T, Box&lt;dyn Error&gt;&gt;</code> 和 <code>anyhow::Error</code> 能装下任何实现了 <code>Error</code> 的类型（第 7 篇会用到）。</p><section><h3>supertrait：为什么 <code>Error</code> 要求你先有 <code>Display</code><a href="#supertrait为什么-error-要求你先有-display"><span>#</span></a></h3><p><code>Error</code> 的定义大致是 <code>trait Error: Debug + Display</code>，冒号后面叫 <strong>supertrait</strong>：想实现 <code>Error</code>，你得先实现 <code>Debug</code> 和 <code>Display</code>。我们的 <code>Diagnostic</code> 两个都有，所以空 <code>impl</code> 能过。如果没有呢？实验文件 <code>src/bin/ch03_supertrait.rs</code> 里写了个只有 <code>Display</code> 的 <code>Oops</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Oops</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Oops</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>f</span><span>.</span><span>write_str</span><span>(</span><span>"oops"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>impl</span><span><span> </span><span>std</span><span>::</span><span>error</span><span>::</span></span><span>Error</span><span> </span><span>for</span><span> </span><span>Oops</span><span> {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `Oops` doesn't implement `Debug`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch03_supertrait.rs:11:28</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>11 | impl std::error::Error for Oops {}</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                            ^^^^ the trait `Debug` is not implemented for `Oops`</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>= note: add `#[derive(Debug)]` to `Oops` or manually `impl Debug for Oops`</span></div></div><div><div><div>8</div></div><div><span>note: required by a bound in `std::error::Error`</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>--&gt; /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/error.rs:59:0</span></div></div><div><div><div>10</div></div><div><span>help: consider annotating `Oops` with `#[derive(Debug)]`</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>3 + #[derive(Debug)]</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>4 | struct Oops;</span></div></div><div><div><div>14</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>For more information about this error, try `rustc --explain E0277`.</span></div></div><div><div><div>17</div></div><div><span>error: could not compile `rust-lab` (bin "ch03_supertrait") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器甚至给出了修复：加 <code>#[derive(Debug)]</code>。supertrait 有点像 C++ 里”继承”的约束一面：<code>class Error : public Debug, public Display</code>，但没有内存布局、没有虚表上的关系，纯粹是”能实现 A 的前提是先实现 B”。</p><div><div><div></div><div><code>core::error::Error</code></div></div><div><p>源码里有一行文档注释说：<code>core::error::Error</code> 从 Rust 1.81 起在 <code>no_std</code> 里也能用。<code>std::error::Error</code> 就是它的重导出。因为 <code>pebble-diag</code> 是 <code>no_std</code>，所以我们用 <code>core::</code> 路径。</p></div></div></section><section><h3>孤儿规则：我想给 <code>Vec&lt;u8&gt;</code> 实现 <code>Display</code><a href="#孤儿规则我想给-vecu8-实现-display"><span>#</span></a></h3><p>既然 trait 是”外挂”的，你可能想：那我能不能给别人的类型实现别人的 trait？比如给标准库的 <code>Vec&lt;u8&gt;</code> 实现 <code>Display</code>，让 <code>println!("{}", vec![1u8, 2, 3])</code> 打印 <code>3 bytes</code>？（实验文件 <code>src/bin/ch03_orphan.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Vec</span><span>&lt;</span><span>u8</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{} bytes"</span><span>, </span><span>self</span><span>.</span><span>len</span><span>())</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch03_orphan.rs:3:1</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>3 | impl fmt::Display for Vec&lt;u8&gt; {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^^^^^^^^^^^^^^-------</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                       |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                       `Vec` is not defined in the current crate</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>= note: impl doesn't have any local type before any uncovered type parameters</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>= note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>= note: define and implement a trait or new type instead</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0117`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `rust-lab` (bin "ch03_orphan") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这叫<strong>孤儿规则</strong>（orphan rule）：<code>impl Trait for Type</code> 要求 <code>Trait</code> 或 <code>Type</code> 至少有一个是<strong>你的 crate 自己定义的</strong>。C++ 里你随时可以在任何命名空间里为别人的类型加一个 <code>operator&lt;&lt;</code> 重载，靠 ADL 找得到；两个库各自加一个，链接或调用时才出现歧义。Rust 选择了更严的规则：整个依赖图里，对任何 <code>(trait, type)</code> 组合最多有一个实现，而保证这点的办法就是”只有 trait 或 type 的主人能写 impl”。</p><p>这个规则怎么影响我们？我们的 <code>Diagnostic</code> 是自己的类型，<code>DiagnosticSink</code> 是自己的 trait，所以：</p><ul>
<li><code>impl Error for Diagnostic</code>：trait 是别人的，类型是我们的。允许。</li>
<li><code>impl DiagnosticSink for Vec&lt;Diagnostic&gt;</code>（下一节）：类型是别人的，trait 是我们的。允许。</li>
<li><code>impl Display for Vec&lt;u8&gt;</code>：两个都是别人的。拒绝。</li>
</ul><p>编译器的最后一行给了出路：定义一个自己的类型（newtype，比如 <code>struct Bytes(Vec&lt;u8&gt;);</code>，你在第 1 篇的 <code>SourceId</code> 里写过）再给它实现。</p></section><section><h3>为什么 <code>.to_string()</code> 凭空出现<a href="#为什么-to_string-凭空出现"><span>#</span></a></h3><p>你也许注意到，我们从没写过 <code>to_string</code>，却可以对任何实现了 <code>Display</code> 的类型调用它。标准库里有这样一个实现（简化）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span>&lt;</span><span>T</span><span>: </span><span>Display</span><span> + ?</span><span>Sized</span><span>&gt; </span><span>ToString</span><span> </span><span>for</span><span> </span><span>T</span><span> { </span><span>/* ... */</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读作”对每一个实现了 <code>Display</code> 的类型 <code>T</code>，<code>T</code> 自动实现 <code>ToString</code>”。这叫 <strong>blanket impl</strong>（覆盖式实现）。<code>T: Display</code> 是<strong>trait bound</strong>，即泛型参数的约束；<code>?Sized</code> 先别管，第 5 篇再讲。它和 C++ 里受 concept 约束的模板函数同源：<code>template&lt;typename T&gt; requires Displayable&lt;T&gt; std::string to_string(T)</code>。实验文件 <code>src/bin/ch03_blanket.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Pt</span><span>(</span><span>i32</span><span>, </span><span>i32</span><span>);</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Pt</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"({}, {})"</span><span>, </span><span>self</span><span>.</span><span>0</span><span>, </span><span>self</span><span>.</span><span>1</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// We never wrote `to_string` for Pt.</span></div></div><div><div><div>10</div></div><div><span>let</span><span> </span><span>s</span><span>: </span><span>String</span><span><span> </span><span>=</span><span> </span></span><span>Pt</span><span>(</span><span>1</span><span>, </span><span>2</span><span>).</span><span>to_string</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(1, 2)</span></div></div><div><div><div>2</div></div><div><span>42x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们只写了 <code>Display</code>，就白得了 <code>ToString</code>。这就是 trait 系统的经济学：一个小的 trait（一个方法），换回一堆功能。泛型的完整故事留到<a href="/posts/rust-by-pebble-05-ast-generics/">第 5 篇</a>。</p></section><section><h3>再跑一次 demo<a href="#再跑一次-demo"><span>#</span></a></h3><p><code>Display</code> 补上以后，回到刚才失败的 demo：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>run</span><span> </span><span>-q</span><span> </span><span>-p</span><span> </span><span>pebble-diag</span><span> </span><span>--example</span><span> </span><span>demo</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0100]: undefined variable `c`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; demo.pebble:1:5</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>1 | let a = 1;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|     - `a` is defined here</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>--&gt; demo.pebble:2:13</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>2 | let b = a + c;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|             ^ not found in this scope</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>= note: variables must be declared with `let`</span></div></div><div><div><div>13</div></div><div><span>---</span></div></div><div><div><div>14</div></div><div><span>error[E0100]: undefined variable `c`</span></div></div><div><div><div>15</div></div><div><span>Diagnostic { severity: Error, code: Some("E0100"), message: "undefined variable `c`", labels: [Label { span: 23..24, message: Some("not found in this scope"), primary: true }, Label { span: 4..5, message: Some("`a` is defined here"), primary: false }], notes: ["variables must be declared with `let`"] }</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三种输出各司其职：<code>render</code> 是给用户的完整报告（次标签先出现，因为它的偏移更小）；<code>{diag}</code> 是 <code>Display</code> 的一行摘要；<code>{diag:?}</code> 是编译器替我们生成的 <code>Debug</code>，里面的 <code>23..24</code> 是 <code>pebble-span</code> 里 <code>Span</code> 手写的 <code>Debug</code> 输出。</p></section></section>
<section><h2><code>DiagnosticSink</code>：错误去哪<a href="#diagnosticsink错误去哪"><span>#</span></a></h2><p>最后一块：诊断被构造出来之后交给谁？解析器要么把它们收集起来（命令行），要么扔掉它们（“我只是想知道能不能解析”的测试和某些调用者）。这是个典型的”同一个接口，两种实现”。本节的 <code>DiagnosticSink</code>、两个 <code>impl</code>、<code>SilentSink</code> 和 <code>DiagResult</code> 依次写在 <code>Display</code> 实现之后、<code>digits</code> 之前（这也是 tag 里的顺序，检查点的 <code>diff</code> 要求一致）。</p><p>C++ 里你会写抽象基类加虚函数：<code>struct Sink { virtual void emit(Diagnostic) = 0; }</code>。Rust 里是 <strong>trait</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Something that accepts diagnostics.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// This is the trait that lets the parser be generic over "where errors go":</span></div></div><div><div><div>4</div></div><div><span>/// a `Vec&lt;Diagnostic&gt;` collects them, while [`SilentSink`] throws them away.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>trait</span><span> </span><span>DiagnosticSink</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Records one diagnostic.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>emit</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>diagnostic</span><span>: </span><span>Diagnostic</span><span>);</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>/// Convenience: emit an error built from a span.</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>error</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>span</span><span>: </span><span>Span</span><span>, </span><span>message</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span>.</span><span>emit</span><span><span>(</span><span>Diagnostic</span><span>::</span></span><span>error</span><span>(</span><span>message</span><span>).</span><span>with_span</span><span>(</span><span>span</span><span>));</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个方法，性质不同。<code>emit</code> 只有签名，没有函数体：<strong>实现者必须提供</strong>。<code>error</code> 带了函数体：这是<strong>默认方法</strong>，实现者不写就用这个。默认方法里可以调用同一个 trait 的其他方法（<code>self.emit</code>），所以实现者只实现最小集合 <code>emit</code>，白得一个便利方法 <code>error</code>。这有点像 C++ 里 <code>std::ranges::view_interface</code>（或 <code>boost::iterator_facade</code>）靠几个核心操作派生出一堆方法，只是它们要借助 CRTP，而 trait 默认方法不需要。</p><p>两个实现，是给两个具体类型的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>DiagnosticSink</span><span> </span><span>for</span><span> </span><span>Vec</span><span>&lt;</span><span>Diagnostic</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>emit</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>diagnostic</span><span>: </span><span>Diagnostic</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>self</span><span>.</span><span>push</span><span>(</span><span>diagnostic</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>/// A sink that drops everything; handy for tests and "just parse it" callers.</span></div></div><div><div><div>8</div></div><div><span>#[derive(</span><span>Default</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>9</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>SilentSink</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>impl</span><span> </span><span>DiagnosticSink</span><span> </span><span>for</span><span> </span><span>SilentSink</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>emit</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>_diagnostic</span><span>: </span><span>Diagnostic</span><span>) {}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>impl DiagnosticSink for Vec&lt;Diagnostic&gt;</code> 是孤儿规则允许的那一类（trait 是我们的）。给标准库的 <code>Vec</code> “加一个接口”，在 C++ 里只能包装一层或者写自由函数，这里是原生的。</p><p><code>SilentSink</code> 是<strong>单元结构体</strong>（unit struct）：没有字段，写成 <code>pub struct SilentSink;</code>，大小为零。它的值就写作 <code>SilentSink</code>。<code>#[derive(Default, Debug)]</code> 额外给它 <code>SilentSink::default()</code>。<code>emit(&amp;mut self, _diagnostic: Diagnostic)</code> 里参数名前的 <code>_</code> 告诉编译器”我故意不用它”，免得”未使用变量”的警告。</p><p>还剩一个类型别名：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Either a value or the diagnostics that stopped us producing one.</span></div></div><div><div><div>2</div></div><div><span>pub</span><span> </span><span>type</span><span> </span><span>DiagResult</span><span>&lt;</span><span>T</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Result</span><span>&lt;</span><span>T</span><span>, </span><span>Diagnostic</span><span>&gt;;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>type DiagResult&lt;T&gt; = Result&lt;T, Diagnostic&gt;;</code> 等同于 C++ 里的 <code>template&lt;class T&gt; using DiagResult = std::expected&lt;T, Diagnostic&gt;;</code>：只是个名字，不是新类型。写它是因为”要么得到一个值，要么得到阻止我们产生它的那份诊断”这句话会在后面几乎每个签名里出现。</p><p>最后补上测试模块里第四个测试，放在 <code>notes_are_rendered</code> 后面、<code>mod tests</code> 的 <code>}</code> 前面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>sink_collects_errors</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>sink</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>sink</span><span>.</span><span>error</span><span><span>(</span><span>Span</span><span>::</span></span><span>DUMMY</span><span>, </span><span>"boom"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>sink</span><span>.</span><span>len</span><span>(), </span><span>1</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Vec::new()</code> 的元素类型没写，编译器是怎么知道它是 <code>Vec&lt;Diagnostic&gt;</code> 的？我的理解是：<code>error</code> 这个方法只在 <code>DiagnosticSink</code> 里，而 <code>DiagnosticSink</code> 只为 <code>Vec&lt;Diagnostic&gt;</code> 实现过，所以类型被唯一确定了。这是我隐约觉得很神奇的地方：类型推断不仅向前（从初始值），也向后（从使用方式）。</p><section><h3>题外话：这个 trait 不能变成 <code>dyn</code><a href="#题外话这个-trait-不能变成-dyn"><span>#</span></a></h3><p>C++ 的虚基类可以持有 <code>std::unique_ptr&lt;Sink&gt;</code>，然后在运行期换实现。<code>DiagnosticSink</code> 能不能这样做？我在实验里试了一下（<code>src/bin/ch03_sink_dyn.rs</code>，一个同形状的缩小版 trait）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0038]: the trait `Sink` is not dyn compatible</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch03_sink_dyn.rs:16:27</span></div></div><div><div><div>3</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>16 |     let mut sink: Box&lt;dyn Sink&gt; = Box::new(Vec::new());</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|                           ^^^^ `Sink` is not dyn compatible</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>note: for a trait to be dyn compatible it needs to allow building a vtable</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>for more information, visit &lt;https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility&gt;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch03_sink_dyn.rs:4:8</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>1 | trait Sink {</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|       ---- this trait is not dyn compatible...</span></div></div><div><div><div>13</div></div><div><span>...</span></div></div><div><div><div>14</div></div><div><span><span> </span></span><span>4 |     fn error(&amp;mut self, message: impl Into&lt;String&gt;) {</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>|        ^^^^^ ...because method `error` has generic type parameters</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>= help: consider moving `error` to another trait</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>= help: only type `std::vec::Vec&lt;std::string::String&gt;` implements `Sink`; consider using it directly instead.</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>For more information about this error, try `rustc --explain E0038`.</span></div></div><div><div><div>20</div></div><div><span>error: could not compile `rust-lab` (bin "ch03_sink_dyn") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>impl Into&lt;String&gt;</code> 参数让 <code>error</code> 成为泛型方法，泛型方法没法放进虚表（每种参数类型都是不同的函数）。所以这个 trait 只能通过泛型（<code>fn parse(sink: &amp;mut impl DiagnosticSink)</code>）使用，不能做运行期多态。第 8 篇会专门讲 <code>dyn Trait</code> 和虚表，第 12 篇在 <code>Backend</code> 上再碰到同一个问题。</p></section></section>
<section><h2>那个只有两个字符的 bug<a href="#那个只有两个字符的-bug"><span>#</span></a></h2><p>现在回到开头。我把这个 crate 在一个独立的 git worktree 里（<code>/tmp/pebble-repro-03</code>，tag 的一份检出，不碰原仓库）改了一行，把 <code>write_header</code> 末尾的 <code>writeln!</code> 换成 <code>write!</code>。</p><p>先看正确的输出（改之前，用最终的 <code>pebble</code> 命令行）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E1001]: expected an expression, found `;`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再看改之后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E1001]: expected an expression, found `;` --&gt; examples/errors.pebble:5:17</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>3</div></div><div><span>5 | let total = 1 + ;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|                 ^</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>肉眼就能看出来：标题和 <code>--&gt;</code> 粘在了同一行。再看测试：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-diag</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>running 4 tests</span></div></div><div><div><div>2</div></div><div><span>test tests::notes_are_rendered ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::caret_length_tracks_span_length ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::sink_collects_errors ... ok</span></div></div><div><div><div>5</div></div><div><span>test tests::renders_a_single_caret_line ... FAILED</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>failures:</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>---- tests::renders_a_single_caret_line stdout ----</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>thread 'tests::renders_a_single_caret_line' (55944) panicked at crates/pebble-diag/src/lib.rs:323:9:</span></div></div><div><div><div>12</div></div><div><span>header must be on its own line:</span></div></div><div><div><div>13</div></div><div><span>error[E0001]: expected an expression --&gt; demo.pebble:2:9</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>2 | let y = ;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>|         ^ found `;`</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>failures:</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>tests::renders_a_single_caret_line</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>error: test failed, to rerun pass `-p pebble-diag --lib`</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>死的是 <code>renders_a_single_caret_line</code>，就是我们前面写的那条 <code>starts_with("...\n")</code>。有意思的是：同一个测试里另外两条断言是 <code>rendered.contains("--&gt; demo.pebble:2:9")</code> 和 <code>rendered.contains("^ found </code>;<code>")</code>。这两个子串在坏掉的输出里<strong>一个都没少</strong>。为了确认，我把 <code>starts_with</code> 那条断言删掉再跑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test tests::caret_length_tracks_span_length ... ok</span></div></div><div><div><div>2</div></div><div><span>test tests::notes_are_rendered ... ok</span></div></div><div><div><div>3</div></div><div><span>test tests::sink_collects_errors ... ok</span></div></div><div><div><div>4</div></div><div><span>test tests::renders_a_single_caret_line ... ok</span></div></div><div><div><div>5</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四个测试全绿，bug 照样在。教训很具体：<code>contains</code> 只证明”这些片段出现了”，没证明”它们出现在该出现的位置”。要检测格式，至少有一条断言得盯住边界（开头、换行），这就是 <code>starts_with("error[E0001]: expected an expression\n")</code> 的用处。然后我 <code>git checkout -- .</code> 还原，并删除了 worktree。</p></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>删掉 <code>crates/pebble-diag/examples/demo.rs</code>（以及 <code>examples</code> 目录），然后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>--workspace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_diag-e12009e933f49099)</span></div></div><div><div><div>2</div></div><div><span>test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_span-3b8ae416ebb198e2)</span></div></div><div><div><div>4</div></div><div><span>test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（过滤掉了编译提示和两个”0 passed”的 <code>Doc-tests</code> 汇总行。）<code>pebble-diag</code> 4 个测试，<code>pebble-span</code> 6 个测试，共 10 个。再检查和 tag 的差异，在你的工作区根目录运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>~/pebble-tag/crates/pebble-diag/src/lib.rs</span><span> </span><span>crates/pebble-diag/src/lib.rs</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>identical</span></div></div><div><div><div>2</div></div><div><span>diff</span><span> </span><span>~/pebble-tag/crates/pebble-diag/Cargo.toml</span><span> </span><span>crates/pebble-diag/Cargo.toml</span><span> &amp;&amp; </span><span>echo</span><span> </span><span>identical</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>identical</span></div></div><div><div><div>2</div></div><div><span>identical</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>此时应当与 tag 逐字节相同的文件（已完成）：</p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-diag/Cargo.toml" target="_blank"><code>crates/pebble-diag/Cargo.toml</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-diag/src/lib.rs" target="_blank"><code>crates/pebble-diag/src/lib.rs</code></a></li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/src/lib.rs" target="_blank"><code>crates/pebble-span/src/lib.rs</code></a> 与 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/Cargo.toml" target="_blank"><code>crates/pebble-span/Cargo.toml</code></a>（第 2 篇完成）</li>
</ul><p>中间状态（现在这样就对，后面的章节会补全）：</p><ul>
<li>根 <code>Cargo.toml</code>：<code>[workspace.dependencies]</code> 目前只有 <code>pebble-span</code> 和 <code>pebble-diag</code> 两行，<code>[workspace.lints]</code> 与 tag 的一样；tag 里完整的版本还有外部依赖和 release profile，随后续章节逐步加入。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇的所有实验文件都在仓库的 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/</code> 目录</a>里。按第 1 篇克隆的 <code>~/pebble-tag</code>，它们在 <code>~/pebble-tag/lab/rust-lab</code>，先 <code>cd ~/pebble-tag/lab/rust-lab</code>（或者把这个目录复制成 <code>~/rust-lab</code>）再运行下面的命令。结果列是我用 <code>rustc 1.95.0</code> 在调试构建下重新跑出来的。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_match.rs" target="_blank"><code>ch03_match.rs</code></a></td><td><code>cargo build --bin ch03_match</code></td><td>编译失败：E0004</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_variant.rs" target="_blank"><code>ch03_variant.rs</code></a></td><td><code>cargo run --quiet --bin ch03_variant</code></td><td>退出码 0，输出 <code>Point has area 0</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_format.rs" target="_blank"><code>ch03_format.rs</code></a></td><td><code>cargo run --quiet --bin ch03_format</code></td><td>退出码 0，第一行 <code>[7]</code>；编译时有警告：fields start and message are never read</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_builder_update.rs" target="_blank"><code>ch03_builder_update.rs</code></a></td><td><code>cargo run --quiet --bin ch03_builder_update</code></td><td>退出码 0，第一行 <code>Config { name: "demo", retries: 5, verbose: true }</code>；编译时有警告：field name is never read</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_iter.rs" target="_blank"><code>ch03_iter.rs</code></a></td><td><code>cargo run --quiet --bin ch03_iter</code></td><td>退出码 0，第一行 <code>first primary starts at Some(4)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_write_import.rs" target="_blank"><code>ch03_write_import.rs</code></a></td><td><code>cargo build --bin ch03_write_import</code></td><td>编译失败：E0599</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_digits.rs" target="_blank"><code>ch03_digits.rs</code></a></td><td><code>cargo run --quiet --bin ch03_digits</code></td><td>退出码 0，第一行 <code>         0: while=1 loop=1 ilog=1</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_supertrait.rs" target="_blank"><code>ch03_supertrait.rs</code></a></td><td><code>cargo build --bin ch03_supertrait</code></td><td>编译失败：E0277</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_orphan.rs" target="_blank"><code>ch03_orphan.rs</code></a></td><td><code>cargo build --bin ch03_orphan</code></td><td>编译失败：E0117</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_blanket.rs" target="_blank"><code>ch03_blanket.rs</code></a></td><td><code>cargo run --quiet --bin ch03_blanket</code></td><td>退出码 0，输出 <code>(1, 2)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch03_sink_dyn.rs" target="_blank"><code>ch03_sink_dyn.rs</code></a></td><td><code>cargo build --bin ch03_sink_dyn</code></td><td>编译失败：E0038</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/cpp/ch03_sev.c" target="_blank"><code>ch03_sev.c</code></a></td><td><code>cd ~/pebble-tag/lab/cpp &amp;&amp; gcc -Wall -Wextra ch03_sev.c -o /tmp/sev &amp;&amp; /tmp/sev</code></td><td>编译通过但有警告 <code>enumeration value 'HELP' not handled in switch [-Wswitch]</code>；程序照常运行，输出 <code>??? ???</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch03-ex/src/lib.rs" target="_blank"><code>lab-ch03-ex/src/lib.rs</code></a></td><td><code>cd ~/pebble-tag/lab/lab-ch03-ex &amp;&amp; cargo test</code></td><td>3 个测试通过（<code>help_renders_with_its_own_word</code>、<code>counting_sink_counts_only_errors</code>、<code>two_labels_on_two_lines</code>）；有未使用导入的编译警告</td></tr></tbody></table><p>练习 1 和练习 3 的答案放在边车 crate <code>lab-ch03-ex</code> 里（依赖 tag 里最终的 <code>pebble-diag</code>，自带 <code>Cargo.toml</code>，不属于根 workspace）。C 程序是 <code>lab/cpp/</code> 里的 <code>ch03_sev.c</code>。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1. 给 <code>Severity::Help</code> 加一个渲染测试。</strong> 写一个测试，断言一份 <code>severity</code> 是 <code>Help</code> 的诊断渲染出来恰好是 <code>help: try `let`\n</code>。（提示：结构体更新语法。）</p>
参考答案<p><code>Diagnostic</code> 的字段都是 <code>pub</code>，所以可以直接用结构体更新语法改 <code>severity</code>，没有标签时 <code>render</code> 只输出标题行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>help_renders_with_its_own_word</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>diag</span><span><span> </span><span>=</span><span> </span></span><span>Diagnostic</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>severity</span><span>: </span><span>Severity</span><span>::</span><span>Help</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>..</span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"try `let`"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>rendered</span><span><span> </span><span>=</span><span> </span></span><span>diag</span><span>.</span><span>render</span><span><span>(&amp;</span><span>SourceMap</span><span>::</span></span><span>new</span><span>());</span></div></div><div><div><div>8</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>rendered</span><span>, </span><span>"help: try `let`</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我在一个依赖 <code>pebble-diag</code> 的临时 crate 里跑了它，通过；完整版在 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/lab-ch03-ex/src/lib.rs" target="_blank"><code>lab/lab-ch03-ex/src/lib.rs</code></a>。它同样也是”盯住边界”的断言：用 <code>assert_eq!</code> 对整个字符串，而不是 <code>contains</code>。</p><p><strong>2. 两个标签，两行源码。</strong> 用 <code>"let a = 1;\nlet b = a + c;\n"</code> 构造一份诊断：主标签盖住 <code>c</code>（字节 23..24），次标签盖住第一行的 <code>a</code>（字节 4..5）。先<strong>预测</strong>输出顺序，再运行。</p>
参考答案<p><code>render</code> 按 <code>(source, start)</code> 排序，次标签的 <code>start</code> 是 4，比主标签的 23 小，所以次标签先输出，尽管它是后加的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>diag</span><span><span> </span><span>=</span><span> </span></span><span>Diagnostic</span><span>::</span><span>error</span><span>(</span><span>"undefined variable `c`"</span><span>)</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>.</span><span>with_primary</span><span><span>(</span><span>Span</span><span>::</span></span><span>new</span><span>(</span><span>id</span><span>, </span><span>23</span><span>, </span><span>24</span><span>), </span><span>"not found"</span><span>)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>.</span><span>with_secondary</span><span><span>(</span><span>Span</span><span>::</span></span><span>new</span><span>(</span><span>id</span><span>, </span><span>4</span><span>, </span><span>5</span><span>), </span><span>"defined here"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>diag</span><span>.</span><span>render</span><span>(&amp;</span><span>sources</span><span>));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: undefined variable `c`</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; demo.pebble:1:5</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>1 | let a = 1;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|     - defined here</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>--&gt; demo.pebble:2:13</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>2 | let b = a + c;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|             ^ not found</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每个标签都输出自己的一整段（<code>--&gt;</code>、原文、下划线），没有合并相邻的行，这是个已知的简化。</p><p><strong>3. 写一个数错误的 sink。</strong> 实现 <code>DiagnosticSink</code> 的一个新类型 <code>ErrorCounter { errors: usize }</code>，只统计 <code>severity == Severity::Error</code> 的诊断。用 <code>sink.error(...)</code> 和 <code>sink.emit(Diagnostic::warning(...))</code> 各调用几次来验证。</p>
参考答案<p>只需要实现 <code>emit</code>，<code>error</code> 是默认方法白送的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>ErrorCounter</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>errors</span><span>: </span><span>usize</span><span>,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>impl</span><span> </span><span>DiagnosticSink</span><span> </span><span>for</span><span> </span><span>ErrorCounter</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>emit</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>diagnostic</span><span>: </span><span>Diagnostic</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>if</span><span> </span><span>diagnostic</span><span><span>.severity </span><span>==</span><span> </span></span><span>Severity</span><span>::</span><span>Error</span><span> {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>self</span><span><span>.errors </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>#[test]</span></div></div><div><div><div>14</div></div><div><span>fn</span><span> </span><span>counting_sink_counts_only_errors</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>sink</span><span><span> </span><span>=</span><span> </span></span><span>ErrorCounter</span><span> { </span><span>errors</span><span>: </span><span>0</span><span> };</span></div></div><div><div><div>16</div></div><div><span>    </span><span>sink</span><span>.</span><span>error</span><span><span>(</span><span>Span</span><span>::</span></span><span>DUMMY</span><span>, </span><span>"boom"</span><span>);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>sink</span><span>.</span><span>emit</span><span><span>(</span><span>Diagnostic</span><span>::</span></span><span>warning</span><span>(</span><span>"hmm"</span><span>));</span></div></div><div><div><div>18</div></div><div><span>    </span><span>sink</span><span>.</span><span>error</span><span><span>(</span><span>Span</span><span>::</span></span><span>DUMMY</span><span>, </span><span>"again"</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>assert_eq!</span><span>(</span><span>sink</span><span>.errors, </span><span>2</span><span>);</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>能写 <code>diagnostic.severity == Severity::Error</code>，是因为 <code>Severity</code> derive 了 <code>PartialEq</code>。这个测试我在临时 crate 里跑过，通过。</p><p>我们现在有了一个能说清”错在哪”的 crate，但它还没有任何东西可以报错。下一篇我们造词法分析器：它是第一个真正会产生 <code>Diagnostic</code> 的代码，并且第一次让我们撞上 Rust 里最著名的那面墙，生命周期。token 要借用源码而不是复制它，C++ 里这是 <code>std::string_view</code> 悬垂的老毛病；在 Rust 里，这个毛病会在编译期现形。继续阅读：<a href="/posts/rust-by-pebble-04-lifetimes-lexer/">生命周期：让 token 借用源码</a>。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-02-ownership/</id>
      <title type="text">所有权：SourceMap 里的 String 归谁</title>
      <published>2026-10-13T01:00:00.000Z</published>
      <updated>2026-10-13T01:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-02-ownership/"/>
      <summary type="text">用 Pebble 从零学 Rust 的第 2 篇：写完 pebble-span。为了把字节偏移翻译成行列，我们需要一个持有源码文本的 SourceMap，顺便学会 String 与 &amp;str、移动、借用、Vec、Option、闭包初见、UTF-8、Index 运算符重载和 no_std。</summary>
      <content type="html"><![CDATA[<p>下面这段 C++ 我用 <code>g++ -std=c++20 -Wall -Wextra</code> 编译，没有一条警告。它返回一个指向局部变量的 <code>std::string_view</code>，运行时打印出来的是乱码：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::string_view </span><span>file_name</span><span>() {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>std::string name </span><span>=</span><span> </span><span>"a.pebble"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> name;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>Rust 版本根本编译不过，而且报错告诉你哪一行有问题。这一篇我们就是要搞清楚它为什么能做到：我们给 <code>pebble-span</code> 加上 <code>SourceMap</code>，一个持有所有源文件文本、能把字节偏移翻译成”第几行第几列”的东西。文本放在哪、谁拥有、谁只是借用，这几个问题一旦问出口，Rust 的所有权系统就是答案。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时，<code>crates/pebble-span/src/lib.rs</code> 是 368 行，和仓库标签 <code>tutorial-v1</code> 里的文件逐字节一致，<code>cargo test -p pebble-span</code> 通过 6 个测试。
涉及的 Rust 特性：<code>String</code>/<code>&amp;str</code>、移动与 <code>Copy</code>/<code>Clone</code>、共享借用与可变借用、<code>Vec</code>、切片、<code>Option</code> 与 <code>?</code>、<code>match</code>、闭包初见、UTF-8、<code>impl Into&lt;String&gt;</code>、<code>std::mem::take</code>、trait 初见（<code>Spanned</code>）、<code>Index</code> 运算符重载、<code>no_std</code>。
接着<a href="/posts/rust-by-pebble-01-first-crate/">第 1 篇</a>：你需要先有 <code>pebble-span</code> 里的 <code>SourceId</code>、<code>Span</code> 和它们的两个测试。</p></div></div>
<section><h2>问题：从字节偏移到行列<a href="#问题从字节偏移到行列"><span>#</span></a></h2><p><code>Span</code> 只存字节偏移：<code>start</code> 和 <code>end</code>。用户想看到的是 <code>a.pebble:2:2</code>，第 2 行第 2 列。这中间差一张表，也差一份源码本身（要打印出错那一行，得有文本）。</p><p>最简单的表是”每一行从第几个字节开始”。对一个 7 字节的文件 <code>ab\n中c</code>（<code>中</code> 是 3 个字节的 UTF-8，后面验证）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>字节偏移:  0  1  2  3 4 5  6</span></div></div><div><div><div>2</div></div><div><span>内容:      a  b  \n 中      c</span></div></div><div><div><div>3</div></div><div><span>line_starts = [0, 3]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>偏移 6 落在第 2 行（<code>line_starts</code> 里最后一个 <code>&lt;= 6</code> 的是 3），行首是 3，所以列要数 <code>[3, 6)</code> 这一段里有几个<strong>字符</strong>再加 1，得到 2。这就是整个算法：一次扫描建表，每次查询做一次二分查找。</p><p>于是我们需要这些东西：</p><ul>
<li>一个 <code>SourceFile</code>，拥有文件名、文本和 <code>line_starts</code>。</li>
<li>一个 <code>SourceMap</code>，拥有所有 <code>SourceFile</code>，给它们发 <code>SourceId</code>。</li>
<li>一个 <code>Position { line, column }</code>。</li>
<li>一个 <code>Spanned</code> trait，“任何知道自己在哪儿的东西”。</li>
</ul><p>问题是：这些字符串谁拥有？</p></section>
<section><h2>谁拥有文本：<code>String</code> 和 <code>&amp;str</code><a href="#谁拥有文本string-和-str"><span>#</span></a></h2><p>C++ 里你有 <code>std::string</code>（拥有，堆上，会析构）、<code>const std::string&amp;</code>（借用）、<code>std::string_view</code>（带长度的借用）。Rust 的对应物几乎是一一的：</p>

<table><thead><tr><th>C++</th><th>Rust</th><th>含义</th></tr></thead><tbody><tr><td><code>std::string</code></td><td><code>String</code></td><td>拥有一块堆内存，离开作用域时自动释放</td></tr><tr><td><code>std::string_view</code></td><td><code>&amp;str</code></td><td>借用一段 UTF-8 文本：指针加长度</td></tr><tr><td><code>const std::string&amp;</code></td><td><code>&amp;String</code></td><td>借用一个 <code>String</code> 本身，实际中很少写</td></tr><tr><td>字符串字面量</td><td><code>&amp;'static str</code></td><td>编译进可执行文件的文本，永远有效（<code>'static</code> 第 4 篇讲）</td></tr></tbody></table><p>实验 <code>src/bin/ch02_string.rs</code>（你的 <code>~/rust-lab</code> 里）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>literal</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> </span></span><span>"a.pebble"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>owned</span><span>: </span><span>String</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"a.pebble"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>owned</span><span>.</span><span>push_str</span><span>(</span><span>".bak"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>let</span><span> </span><span>view</span><span>: &amp;</span><span>str</span><span><span> </span><span>=</span><span> &amp;</span></span><span>owned</span><span>[..</span><span>8</span><span>];</span></div></div><div><div><div>5</div></div><div><span>println!</span><span>(</span><span>"size_of::&lt;String&gt;() = {}"</span><span>, </span><span>size_of</span><span>::&lt;</span><span>String</span><span>&gt;());</span></div></div><div><div><div>6</div></div><div><span>println!</span><span>(</span><span>"size_of::&lt;&amp;str&gt;()   = {}"</span><span>, </span><span>size_of</span><span>::&lt;&amp;</span><span>str</span><span>&gt;());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch02_string</span></div></div><div><div><div>2</div></div><div><span>literal = a.pebble, owned = a.pebble.bak, view = a.pebble</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;String&gt;() = 24</span></div></div><div><div><div>4</div></div><div><span>size_of::&lt;&amp;str&gt;()   = 16</span></div></div><div><div><div>5</div></div><div><span>len = 12, capacity &gt;= len: true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>String</code> 是 24 字节：堆指针、容量、长度，三个字。<code>&amp;str</code> 是 16 字节：指针和长度，和 <code>string_view</code> 一样。（我在这台机器上编译了 C++ 的 <code>sizeof(std::string)</code>，是 32，libstdc++ 为了短字符串优化多放了一块内联缓冲区；<code>string_view</code> 同样是 16。）<code>&amp;owned[..8]</code> 从 <code>owned</code> 里切出前 8 个字节，没有复制任何字符，<code>view</code> 只是指回 <code>owned</code> 的堆内存。</p><p>区别在于 Rust 编译器<strong>知道</strong> <code>view</code> 借自 <code>owned</code>。这件事要用整篇文章来展开。我们先从最基础的一刀切起。</p></section>
<section><h2>移动：<code>std::move</code> 变成默认<a href="#移动stdmove-变成默认"><span>#</span></a></h2><p>实验 <code>ch02_move.rs</code>。一个函数拿走一个 <code>String</code> 的所有权，调用者之后还想用它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>byte_len</span><span>(</span><span>text</span><span>: </span><span>String</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>text</span><span>.</span><span>len</span><span>()</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"a.pebble"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>byte_len</span><span>(</span><span>name</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{n} bytes in {name}"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_move</span></div></div><div><div><div>2</div></div><div><span>error[E0382]: borrow of moved value: `name`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_move.rs:8:29</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>6 |     let name = String::from("a.pebble");</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait</span></div></div><div><div><div>7</div></div><div><span>7 |     let n = byte_len(name);</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|                      ---- value moved here</span></div></div><div><div><div>9</div></div><div><span>8 |     println!("{n} bytes in {name}");</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|                             ^^^^ value borrowed here after move</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>note: consider changing this parameter type in function `byte_len` to borrow instead if owning the value isn't necessary</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_move.rs:1:19</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>1 | fn byte_len(text: String) -&gt; usize {</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>|    --------       ^^^^^^ this parameter takes ownership of the value</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|    |</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>|    in this function</span></div></div><div><div><div>19</div></div><div><span>help: consider cloning the value if the performance cost is acceptable</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>21</div></div><div><span>7 |     let n = byte_len(name.clone());</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>|                          ++++++++</span></div></div><div><div><div>23</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>let n = byte_len(name)</code> 把 <code>name</code> <strong>移动</strong>进了函数。移动之后 <code>name</code> 就是一块”已经没人住的房子”，编译器记住了这件事，下一次用它就报错 E0382。</p><p>这和 C++ 的 <code>std::move</code> 有一个关键区别。我写了一个 C++ 对照：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::string name </span><span>=</span><span> </span><span>"a.pebble"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>consume</span><span>(</span><span>std::move</span><span><span>(</span><span>name</span><span>));</span></span></div></div><div><div><div>3</div></div><div><span>std::</span><span>printf</span><span>(</span><span>"after move: size=</span><span>%zu</span><span>, empty=</span><span>%d</span><span>\n</span><span>"</span><span><span>,</span><span> name.</span></span><span>size</span><span><span>(),</span><span> </span><span>(</span></span><span>int</span><span><span>)</span><span>name.</span></span><span>empty</span><span>());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它编译通过，输出 <code>after move: size=0, empty=1</code>。C++ 的移动是”调用移动构造函数，把源对象掏空成一个有效但未指定的状态”，掏空之后对象仍然存在，仍然会被析构，你仍然可以不小心用它（只是不知道里面是什么）。Rust 的移动是：<strong>按位复制 24 字节（指针、容量、长度）到新位置，同时把旧变量标成不可用</strong>。没有移动构造函数，没有”被掏空的对象”，也没有旧位置上的析构：编译器在每个作用域结尾，对<strong>仍然拥有值</strong>的变量恰好插入一次释放代码（RAII，和 C++ 析构的时机一致）。所以 Rust 里”忘了这个值已经被搬走”是编译错误，不是运行时的惊喜。</p><p>默认是移动，对不能随便复制的东西这是对的：<code>String</code> 如果可以按位复制，两个变量会指向同一块堆内存，释放两次。第 1 篇练习里的 <code>#[derive(Copy)]</code> 对 <code>String</code> 字段报 E0204 就是这个原因。反过来，<code>SourceId</code> 和 <code>Span</code> 只是几个整数，按位复制就是完整的复制，所以它们是 <code>Copy</code> 的：传参后原变量依然可用，就像 <code>int</code>。</p><p>如果你<strong>确实</strong>想要两份，说出来，<code>.clone()</code> 是显式的深拷贝（实验 <code>ch02_clone.rs</code>，输出 <code>8 bytes in a.pebble</code>、<code>a.pebble</code>）。C++ 里拷贝是默认的、移动是要说的（<code>std::move</code>）；Rust 反过来：移动是默认的、拷贝是要说的。我认为 Rust 的默认值更好，因为昂贵的操作（拷贝堆内存）在代码里是可见的。</p></section>
<section><h2>写 <code>SourceFile</code>：先搭骨架<a href="#写-sourcefile先搭骨架"><span>#</span></a></h2><p>现在可以动手了。打开 <code>crates/pebble-span/src/lib.rs</code>，在 <code>Span</code> 的 <code>Display</code> 实现和 <code>#[cfg(test)]</code> 之间加入下面的代码。先是 <code>Position</code> 和 <code>SourceFile</code> 的定义：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A one-based line and column, counted in Unicode scalar values.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Debug</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Position</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>pub</span><span> </span><span>line</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>pub</span><span> </span><span>column</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>/// One file of source text plus the offsets at which each line starts.</span></div></div><div><div><div>9</div></div><div><span>#[derive(</span><span>Clone</span><span>)]</span></div></div><div><div><div>10</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>SourceFile</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>id</span><span>: </span><span>SourceId</span><span>,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>name</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>text</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>/// Byte offset of the first character of every line. Always starts with 0.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>line_starts</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span>&gt;,</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Position</code> 是两个 <code>pub</code> 字段的普通结构体，<code>Copy</code>，因为它只是两个整数。（<code>Debug</code> 这次是派生的，因为 <code>Position { line: 2, column: 2 }</code> 就够读。）</p><p><code>SourceFile</code> 的字段<strong>没有</strong> <code>pub</code>。字段默认私有，私有的意思是”只有这个模块（这里是整个 crate 根）能直接访问”。这里的原因是不变量：<code>line_starts</code> 必须永远和 <code>text</code> 匹配，如果外面的代码能直接改 <code>text</code>，表就过时了。所以外面只能通过我们提供的方法读取。C++ 里你会写 <code>private:</code> 加 getter；Rust 的粒度是模块而不是类，同一个模块里的函数可以访问彼此的私有字段，这一点我们在测试里会用到。</p><p>三个字段里有两个 <code>String</code> 和一个 <code>Vec&lt;u32&gt;</code>（动态数组，对应 <code>std::vector&lt;uint32_t&gt;</code>），都是拥有堆内存的类型，所以 <code>SourceFile</code> 也拥有它们，整个结构体因此不是 <code>Copy</code> 的。<code>#[derive(Clone)]</code> 给它一个显式的深拷贝，会逐字段克隆（复制两段字符串和一个向量）；我保留这个能力是因为测试和别处偶尔需要一份独立的副本，但调用它的人必须把 <code>.clone()</code> 写出来。</p><section><h3>构造函数和 getter<a href="#构造函数和-getter"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>SourceFile</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>new</span><span>(</span><span>id</span><span>: </span><span>SourceId</span><span>, </span><span>name</span><span>: </span><span>String</span><span>, </span><span>text</span><span>: </span><span>String</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>line_starts</span><span><span> </span><span>=</span><span> </span></span><span>compute_line_starts</span><span>(&amp;</span><span>text</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>id</span><span>,</span></div></div><div><div><div>6</div></div><div><span>            </span><span>name</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>text</span><span>,</span></div></div><div><div><div>8</div></div><div><span>            </span><span>line_starts</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意 <code>new</code> 没有 <code>pub</code>：外面的人不能直接造一个 <code>SourceFile</code>，只有 <code>SourceMap::add</code> 可以（它会负责发 <code>id</code>）。这是 Rust 里表达”只能通过工厂创建”的标准办法，没有 <code>friend</code> 也没有 <code>private</code> 构造函数。</p><p>这四行里藏着所有权的第一次实战。<code>compute_line_starts(&amp;text)</code> 里的 <code>&amp;text</code> 是<strong>借用</strong>：把 <code>text</code> 的一个只读引用交给函数，函数用完还回来，<code>text</code> 仍然属于我们。接下来 <code>Self { id, name, text, line_starts }</code> 把 <code>name</code>、<code>text</code> 和 <code>line_starts</code> <strong>移动</strong>进新结构体，之后这三个局部变量不能再用。顺序很重要：如果先移动再借用，就是上面的 E0382。</p><p>接着三个 getter：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The id handed out by the owning [`SourceMap`].</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>id</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>SourceId</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.id</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>/// A human-readable name, e.g. a path.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>name</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.name</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>/// The raw source text.</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>text</span><span>(&amp;</span><span>self</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.text</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>id()</code> 返回 <code>SourceId</code>，<code>Copy</code>，直接按值复制一份。<code>name()</code> 返回 <code>&amp;str</code>：把内部 <code>String</code> 借出去。<code>&amp;self.name</code> 的类型是 <code>&amp;String</code>，函数声明要 <code>&amp;str</code>，这里编译器自动做了转换（<strong>解引用强制转换</strong>，<code>&amp;String</code> 能自动变成 <code>&amp;str</code>，因为 <code>String</code> 实现了”可以当 <code>str</code> 用”，第 5 篇再细讲这个机制；现在记住：需要 <code>&amp;str</code> 的地方放 <code>&amp;String</code> 总是可以的）。</p><p><code>&amp;self</code> 是第 1 篇预告过的东西：方法<strong>借用</strong>调用者，不拿走。也就是说 <code>file.name()</code> 之后 <code>file</code> 还在。返回的 <code>&amp;str</code> 因此必须不比 <code>file</code> 活得更久。我们当场验证编译器是否强制这一点（<code>ch02_getter.rs</code>：把一个 <code>File</code> 的 <code>name()</code> 存到块外面，然后让 <code>File</code> 先死）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_getter</span></div></div><div><div><div>2</div></div><div><span>error[E0597]: `file` does not live long enough</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch02_getter.rs:15:16</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>14 |         let file = File { name: String::from("a.pebble") };</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|             ---- binding `file` declared here</span></div></div><div><div><div>7</div></div><div><span>15 |         name = file.name();</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|                ^^^^ borrowed value does not live long enough</span></div></div><div><div><div>9</div></div><div><span>16 |     }</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|     - `file` dropped here while still borrowed</span></div></div><div><div><div>11</div></div><div><span>17 |     println!("{name}");</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|                ---- borrow later used here</span></div></div><div><div><div>13</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器从 <code>fn name(&amp;self) -&gt; &amp;str</code> 这个签名读出了：“返回值借自 <code>self</code>”。这叫<strong>生命周期省略</strong>：规则是”如果有 <code>&amp;self</code>，返回的引用默认借自 <code>self</code>”。<code>ch02_getter_ok.rs</code> 里我把编译器补上的完整写法也放了进去，<code>fn name_explicit&lt;'a&gt;(&amp;'a self) -&gt; &amp;'a str</code>，运行正常，输出 <code>a.pebble a.pebble</code>。那个 <code>'a</code> 是生命周期参数，第 4 篇是整整一篇的主题，现在你只要知道：<strong>签名里藏着一条”返回值借自谁”的声明，编译器据此检查调用点</strong>。</p><p><code>#[must_use]</code> 你在第 1 篇见过了。</p></section><section><h3>建表：<code>Vec</code>、迭代、<code>as</code><a href="#建表vec迭代as"><span>#</span></a></h3><p><code>new</code> 调用的 <code>compute_line_starts</code> 是一个自由函数，放在 <code>impl</code> 外面（文件里任何位置都行，我放在 <code>Debug</code> 实现的后面，下面一并给出完整位置）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>compute_line_starts</span><span>(</span><span>text</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>Vec</span><span>&lt;</span><span>u32</span><span>&gt; {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>starts</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>with_capacity</span><span>(</span><span>text</span><span>.</span><span>len</span><span>() / </span><span>24</span><span> + </span><span>1</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>starts</span><span>.</span><span>push</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>for</span><span> (</span><span>index</span><span>, </span><span>byte</span><span>) </span><span>in</span><span> </span><span>text</span><span>.</span><span>bytes</span><span>().</span><span>enumerate</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>if</span><span> </span><span>byte</span><span><span> </span><span>==</span><span> </span></span><span>b'</span><span>\n</span><span>'</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>starts</span><span>.</span><span>push</span><span>(</span><span>index</span><span> </span><span>as</span><span> </span><span>u32</span><span> + </span><span>1</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>    </span><span>starts</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>参数是 <code>&amp;str</code>：只读借用，调用者传 <code>&amp;String</code> 或字符串字面量都行。逐行看：</p><ul>
<li><code>Vec::with_capacity(n)</code> 预留 <code>n</code> 个元素的空间，对应 <code>std::vector::reserve</code>。我猜每行平均 24 个字节（拍脑袋的估计），所以预留 <code>len / 24 + 1</code>。<code>Vec&lt;u32&gt;</code> 的元素类型靠后面的 <code>push(0)</code> 和返回类型推断出来。</li>
<li><code>let mut starts</code>：<code>push</code> 要修改向量，所以绑定必须是 <code>mut</code>（第 1 篇）。</li>
<li><code>text.bytes()</code> 是字节迭代器，<code>.enumerate()</code> 给每个元素配上下标，产出 <code>(下标, 字节)</code> 的<strong>元组</strong>。<code>for (index, byte) in ...</code> 在循环头里直接把元组拆开，这叫解构模式。迭代器第 3 篇和第 4 篇会详细讲，这里把它当成一个范围 <code>for</code>。</li>
<li><code>b'\n'</code> 是<strong>字节字面量</strong>，类型是 <code>u8</code>，对应 C 的 <code>'\n'</code>（但 Rust 的 <code>'\n'</code> 是 <code>char</code>，是 Unicode 标量值，两者不同：字符和字节在 Rust 里是不同类型）。</li>
<li><code>index as u32 + 1</code>：<code>index</code> 是 <code>usize</code>，要存进 <code>Vec&lt;u32&gt;</code>，所以 <code>as u32</code>。<code>as</code> 比 <code>+</code> 优先级高，所以是 <code>(index as u32) + 1</code>。这是第 1 篇预告过的”我断言这个文件小于 4 GiB”。</li>
<li>最后一行 <code>starts</code> 没有分号，它是返回值，<strong>移动</strong>出函数：调用者拿到向量，没有拷贝。</li>
</ul><p>实验 <code>ch02_vec.rs</code> 看一眼 <code>Vec</code> 和切片：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>starts</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>0</span><span>, </span><span>4</span><span>, </span><span>9</span><span>];</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>middle</span><span>: &amp;[</span><span>u32</span><span><span>] </span><span>=</span><span> &amp;</span></span><span>starts</span><span>[</span><span>1</span><span>..];</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"get(1) = {:?}, get(10) = {:?}"</span><span>, </span><span>starts</span><span>.</span><span>get</span><span>(</span><span>1</span><span>), </span><span>starts</span><span>.</span><span>get</span><span>(</span><span>10</span><span>));</span></div></div><div><div><div>4</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>grown</span><span>: </span><span>Vec</span><span>&lt;</span><span>u32</span><span><span>&gt; </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>with_capacity</span><span>(</span><span>8</span><span>);</span></div></div><div><div><div>5</div></div><div><span>grown</span><span>.</span><span>push</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>6</div></div><div><span>grown</span><span>.</span><span>push</span><span>(</span><span>5</span><span>);</span></div></div><div><div><div>7</div></div><div><span>println!</span><span>(</span><span>"len = {}, capacity = {}"</span><span>, </span><span>grown</span><span>.</span><span>len</span><span>(), </span><span>grown</span><span>.</span><span>capacity</span><span>());</span></div></div><div><div><div>8</div></div><div><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>hint</span><span>::</span></span><span>black_box</span><span>(</span><span>10</span><span>);</span></div></div><div><div><div>9</div></div><div><span>println!</span><span>(</span><span>"starts[10] = {}"</span><span>, </span><span>starts</span><span>[</span><span>index</span><span>]);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch02_vec</span></div></div><div><div><div>2</div></div><div><span>slice = [4, 9], len = 2</span></div></div><div><div><div>3</div></div><div><span>get(1) = Some(4), get(10) = None</span></div></div><div><div><div>4</div></div><div><span>len = 2, capacity = 8</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>thread 'main' (79263) panicked at src/bin/ch02_vec.rs:13:39:</span></div></div><div><div><div>7</div></div><div><span>index out of bounds: the len is 3 but the index is 10</span></div></div><div><div><div>8</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>vec![...]</code> 是宏，创建并初始化向量。<code>&amp;starts[1..]</code> 是<strong>切片</strong>（<code>&amp;[u32]</code>）：指针加长度，对应 <code>std::span&lt;const uint32_t&gt;</code>，借自 <code>starts</code>。<code>starts[10]</code> 越界会 panic（和 <code>at()</code> 一样，而不是 <code>operator[]</code> 那种未定义行为）；<code>.get(10)</code> 返回 <code>None</code>，让你自己决定怎么办。后面会反复看到这对搭档：“会 panic 的下标”和”返回 <code>Option</code> 的 <code>get</code>”。（<code>black_box</code> 只是不让编译器在编译期看穿 10 越界。）</p></section></section>
<section><h2>写查询：<code>Option</code>、<code>?</code>、闭包<a href="#写查询option闭包"><span>#</span></a></h2><p>先把上面这些写进文件（<code>SourceFile</code> 定义、<code>impl SourceFile</code> 里的 <code>new</code> 和三个 getter、<code>compute_line_starts</code>），然后用 <code>cargo build -p pebble-span</code> 编译。你会看到编译器的一个小提醒，而且这是我想让你看到的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-span</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-span v0.1.0 (/tmp/pebble-rebuild-02/crates/pebble-span)</span></div></div><div><div><div>3</div></div><div><span>warning: associated function `new` is never used</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-span/src/lib.rs:120:8</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>119 | impl SourceFile {</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>| --------------- associated function in this implementation</span></div></div><div><div><div>8</div></div><div><span>120 |     fn new(id: SourceId, name: String, text: String) -&gt; Self {</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|        ^^^</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>= note: `#[warn(dead_code)]` (part of `#[warn(unused)]` for the whole crate)</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>new</code> 是私有的，整个 crate 里没人调用，编译器就如实告诉你。<code>pub</code> 的函数不会收到这种警告，因为外面的 crate 可能会用。这个警告会在写完 <code>SourceMap::add</code> 之后消失。</p><section><h3><code>Option</code>：没有空指针<a href="#option没有空指针"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Number of lines. A file always has at least one (possibly empty) line.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>line_count</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.line_starts.</span><span>len</span><span>() </span><span>as</span><span> </span><span>u32</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>/// Byte offset at which one-based `line` starts.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>line_start</span><span>(&amp;</span><span>self</span><span>, </span><span>line</span><span>: </span><span>u32</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>u32</span><span>&gt; {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>line</span><span>.</span><span>checked_sub</span><span>(</span><span>1</span><span>)? </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>11</div></div><div><span>        </span><span>self</span><span>.line_starts.</span><span>get</span><span>(</span><span>index</span><span>).</span><span>copied</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>line_start</code> 的返回类型是 <code>Option&lt;u32&gt;</code>：<code>Option&lt;T&gt;</code> 是一个标准库的枚举，要么是 <code>Some(值)</code>，要么是 <code>None</code>。它解决的问题和 C 里”返回 <code>-1</code> 表示没有”、“返回 <code>NULL</code> 表示没有”一样，区别是<strong>类型系统逼你处理 <code>None</code></strong>：你拿不到 <code>u32</code>，只拿到 <code>Option&lt;u32&gt;</code>，除非你拆开它。（枚举本身是第 3 篇的主题，先把 <code>Option</code> 当成”可能为空的值”。）</p><p><code>line_start(0)</code> 应该是 <code>None</code>，因为行号从 1 开始。<code>line.checked_sub(1)</code> 是第 1 篇的 <code>checked_*</code> 家族：<code>0 - 1</code> 溢出，返回 <code>None</code>。后面那个小小的 <code>?</code> 是本章第一个重要的语法：在返回 <code>Option</code> 的函数里，<code>expr?</code> 的意思是”如果 <code>expr</code> 是 <code>None</code>，<strong>立刻让整个函数返回 <code>None</code></strong>；如果是 <code>Some(v)</code>，把 <code>v</code> 取出来继续”。没有 <code>?</code> 的话，同样的逻辑是一个 <code>match</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>match</span><span> </span><span>line</span><span>.</span><span>checked_sub</span><span>(</span><span>1</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Some</span><span>(</span><span>v</span><span>) =&gt; </span><span>v</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>None</span><span> =&gt; </span><span>return</span><span> </span><span>None</span><span>,</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>match</code> 对每种情况给一个分支，编译器检查你没有漏掉（第 3 篇会让它报错）。<code>?</code> 就是这个 <code>match</code> 的缩写。</p><p>第二行 <code>self.line_starts.get(index)</code> 返回 <code>Option&lt;&amp;u32&gt;</code>：向量里元素的<strong>引用</strong>，因为 <code>get</code> 不想把元素搬出向量。我们的函数要 <code>Option&lt;u32&gt;</code>，所以需要 <code>.copied()</code>，它把 <code>Option&lt;&amp;u32&gt;</code> 里的引用解开、复制成值（<code>u32</code> 是 <code>Copy</code>，可以复制）。如果忘了，编译器直接告诉你该用什么（实验 <code>ch02_copied.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_copied</span></div></div><div><div><div>2</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_copied.rs:3:5</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>1 | fn line_start(starts: &amp;[u32], line: u32) -&gt; Option&lt;u32&gt; {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                                             ----------- expected `Option&lt;u32&gt;` because of return type</span></div></div><div><div><div>7</div></div><div><span>2 |     let index = line.checked_sub(1)? as usize;</span></div></div><div><div><div>8</div></div><div><span>3 |     starts.get(index)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|     ^^^^^^^^^^^^^^^^^ expected `Option&lt;u32&gt;`, found `Option&lt;&amp;u32&gt;`</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>= note: expected enum `Option&lt;_&gt;`</span></div></div><div><div><div>12</div></div><div><span><span>             </span></span><span>found enum `Option&lt;&amp;_&gt;`</span></div></div><div><div><div>13</div></div><div><span>help: use `Option::copied` to copy the value inside the `Option`</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>3 |     starts.get(index).copied()</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>|                      +++++++++</span></div></div><div><div><div>17</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>整个小函数在 <code>ch02_option.rs</code> 里跑一遍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch02_option</span></div></div><div><div><div>2</div></div><div><span>Some(3)</span></div></div><div><div><div>3</div></div><div><span>None</span></div></div><div><div><div>4</div></div><div><span>None</span></div></div><div><div><div>5</div></div><div><span>line 3 starts at byte 9</span></div></div><div><div><div>6</div></div><div><span>width = 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>line_start(&amp;starts, 2)</code> 是 <code>Some(3)</code>，行号 0 和 7 都是 <code>None</code>。顺带看到了 <code>match</code> 怎么拆 <code>Option</code>，以及 <code>map_or(默认值, 函数)</code>：<code>None</code> 时给默认值，<code>Some(x)</code> 时对 <code>x</code> 调函数。后面马上会用到。</p></section><section><h3>闭包初见：<code>partition_point</code><a href="#闭包初见partition_point"><span>#</span></a></h3><p>下一个方法，给一个字节偏移，找出它在第几行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Zero-based index of the line containing `offset`.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>line_index</span><span>(&amp;</span><span>self</span><span>, </span><span>offset</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>// `line_starts[0] == 0 &lt;= offset` always holds, so the result is &gt;= 1.</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>count</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.line_starts.</span><span>partition_point</span><span><span>(</span><span>|</span><span>&amp;</span></span><span>start</span><span><span>|</span><span> </span></span><span>start</span><span><span> </span><span>&lt;=</span><span> </span></span><span>offset</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>(</span><span>count</span><span> - </span><span>1</span><span>) </span><span>as</span><span> </span><span>u32</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>partition_point</code> 是二分查找：假设切片的前半部分让谓词返回 <code>true</code>、后半部分返回 <code>false</code>，返回第一个 <code>false</code> 的位置。C++ 里有同名的 <code>std::partition_point</code>。<code>line_starts</code> 是升序的，所以 <code>|start| start &lt;= offset</code> 对前一段成立、对后一段不成立，返回值就是”起点不超过 <code>offset</code> 的行数”，减 1 得到下标。</p><p><code>|&amp;start| start &lt;= offset</code> 是一个<strong>闭包</strong>（匿名函数）：竖线里是参数，后面是函数体。它可以直接使用周围的变量 <code>offset</code>，对应 C++ 的 <code>[&amp;](auto start) { return start &lt;= offset; }</code>。Rust 的闭包怎么捕获变量、什么时候是 <code>Fn</code>/<code>FnMut</code>/<code>FnOnce</code>，第 9 篇讲；今天只要会读。</p><p>有趣的是参数那个 <code>&amp;start</code>。<code>partition_point</code> 对切片 <code>[u32]</code> 的每个元素传的是 <code>&amp;u32</code>（引用，不是值），所以参数类型是 <code>&amp;u32</code>。<code>&amp;start</code> 写在<strong>参数位置</strong>，它是一个<strong>模式</strong>：意思是”这个参数是一个引用，把它指向的 <code>u32</code> 绑定到名字 <code>start</code>”。如果不这样写（实验 <code>ch02_closure_pattern.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_closure_pattern</span></div></div><div><div><div>2</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_closure_pattern.rs:8:60</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>8 |     let bad = line_starts.partition_point(|start| start &lt;= offset);</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                                                            ^^^^^^ expected `&amp;u32`, found `u32`</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>help: consider dereferencing the borrow</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>8 |     let bad = line_starts.partition_point(|start| *start &lt;= offset);</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|                                                   +</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>start</code> 是 <code>&amp;u32</code>，<code>offset</code> 是 <code>u32</code>，不能直接比。两种修法，都在 <code>ch02_closure_ok.rs</code> 里跑过（前两行都输出 3；这个文件的第三行用 <code>|&amp;start| start &lt;= 2</code>，输出 1，说明偏移 2 之前只有 <code>0</code> 一个行首）：<code>|&amp;start|</code> 在参数里解引用，或者 <code>|start| *start &lt;= offset</code> 在函数体里用 <code>*</code> 解引用（和 C 一样，<code>*</code> 是解引用）。我偏爱前者，把”它是个引用”这件事放在签名里。</p><p><code>(count - 1) as u32</code>：<code>count</code> 是 <code>usize</code>（切片下标的类型）。因为 <code>line_starts[0] == 0</code>，对任何 <code>offset</code> 谓词至少对第一个元素成立，<code>count &gt;= 1</code>，所以 <code>count - 1</code> 不会下溢。我把这个推理写在了注释里，这是”为什么这里不用 <code>checked_sub</code>”的理由。</p></section><section><h3>取一行文本：借用的切片<a href="#取一行文本借用的切片"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The text of one-based `line`, without its trailing line terminator.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>line</span><span>(&amp;</span><span>self</span><span>, </span><span>line</span><span>: </span><span>u32</span><span>) -&gt; </span><span>Option</span><span>&lt;&amp;</span><span>str</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>line</span><span>.</span><span>checked_sub</span><span>(</span><span>1</span><span>)? </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> *</span></span><span>self</span><span>.line_starts.</span><span>get</span><span>(</span><span>index</span><span>)? </span><span>as</span><span> </span><span>usize</span><span>;</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.line_starts</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>get</span><span>(</span><span>index</span><span> + </span><span>1</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>.</span><span>map_or</span><span>(</span><span>self</span><span>.text.</span><span>len</span><span><span>(), </span><span>|</span><span>&amp;</span></span><span>next</span><span><span>|</span><span> </span></span><span>next</span><span> </span><span>as</span><span> </span><span>usize</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>let</span><span> </span><span>raw</span><span><span> </span><span>=</span><span> &amp;</span></span><span>self</span><span>.text[</span><span>start</span><span>..</span><span>end</span><span>];</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Some</span><span>(</span><span>strip_line_ending</span><span>(</span><span>raw</span><span>))</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个函数返回 <code>Option&lt;&amp;str&gt;</code>：一段借自 <code>self.text</code> 的文本，<strong>没有复制</strong>。<code>*self.line_starts.get(index)? as usize</code> 的读法是：<code>get(index)</code> 得到 <code>Option&lt;&amp;u32&gt;</code>，<code>?</code> 在 <code>None</code> 时提前返回，否则得到 <code>&amp;u32</code>，<code>*</code> 解引用得 <code>u32</code>（运算顺序：<code>?</code> 先，<code>*</code> 再，<code>as</code> 最后）。<code>end</code> 是下一行的行首，如果这是最后一行，没有下一行（<code>get</code> 返回 <code>None</code>），就用 <code>map_or</code> 给默认值 <code>self.text.len()</code>，也就是文件末尾。<code>&amp;self.text[start..end]</code> 是范围下标，在 <code>String</code> 上切出 <code>str</code>，前面加 <code>&amp;</code> 得到 <code>&amp;str</code>。</p><p>行首总是紧跟在 <code>\n</code> 后面，所以 <code>start</code> 和 <code>end</code> 永远落在字符边界上，切片不会 panic。（不在字符边界上会怎样，下一节马上看。）</p><p>最后一个辅助函数把行尾的 <code>\n</code> 和 <code>\r</code> 去掉，处理 Windows 的 <code>\r\n</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>strip_line_ending</span><span>(</span><span>line</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>line</span><span><span> </span><span>=</span><span> </span></span><span>line</span><span>.</span><span>strip_suffix</span><span>(</span><span>'</span><span>\n</span><span>'</span><span>).</span><span>unwrap_or</span><span>(</span><span>line</span><span>);</span></div></div><div><div><div>3</div></div><div><span>    </span><span>line</span><span>.</span><span>strip_suffix</span><span>(</span><span>'</span><span>\r</span><span>'</span><span>).</span><span>unwrap_or</span><span>(</span><span>line</span><span>)</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>strip_suffix</code> 返回 <code>Option&lt;&amp;str&gt;</code>：有这个后缀就是去掉后缀后的 <code>Some</code>，没有就是 <code>None</code>；<code>unwrap_or(default)</code> 在 <code>None</code> 时给默认值。参数 <code>line</code> 被遮蔽了两次（第 1 篇的遮蔽），每次都是更短的 <code>&amp;str</code>。注意返回类型 <code>&amp;str</code> 没有任何生命周期标注，编译器用的是”只有一个引用参数，返回值就借自它”的规则。这就是下一节要认真对待的东西。</p></section></section>
<section><h2>悬垂引用：C++ 的坑，Rust 的编译错误<a href="#悬垂引用c-的坑rust-的编译错误"><span>#</span></a></h2><p>回到文章开头的那段 C++。把它翻译成 Rust 并运行（<code>ch02_dangling.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>file_name</span><span>() -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>name</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>::</span><span>from</span><span>(</span><span>"a.pebble"</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&amp;</span><span>name</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_dangling</span></div></div><div><div><div>2</div></div><div><span>error[E0106]: missing lifetime specifier</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_dangling.rs:1:19</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>1 | fn file_name() -&gt; &amp;str {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                   ^ expected named lifetime parameter</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from</span></div></div><div><div><div>9</div></div><div><span>help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>1 | fn file_name() -&gt; &amp;'static str {</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|                    +++++++</span></div></div><div><div><div>13</div></div><div><span>help: instead, you are more likely to want to return an owned value</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span>1 - fn file_name() -&gt; &amp;str {</span></div></div><div><div><div>16</div></div><div><span>1 + fn file_name() -&gt; String {</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>18</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>“this function’s return type contains a borrowed value, but there is no value for it to be borrowed from”：编译器说这个函数返回借来的东西，但没有任何参数可借。这里 <code>&amp;name</code> 借的是局部变量，局部变量在函数结束时被释放，这就是一个悬垂引用。它给的修复建议也很对：如果你真想把文本交出去，<strong>返回拥有它的 <code>String</code></strong>。</p><p>再看一个有参数的版本（<code>ch02_dangling_local.rs</code>），这次编译器能找到借用的来源，但来源是错的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>shout</span><span>(</span><span>text</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>loud</span><span><span> </span><span>=</span><span> </span></span><span>text</span><span>.</span><span>to_uppercase</span><span>();</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&amp;</span><span>loud</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_dangling_local</span></div></div><div><div><div>2</div></div><div><span>error[E0515]: cannot return reference to local variable `loud`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_dangling_local.rs:3:5</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>3 |     &amp;loud</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|     ^^^^^ returns a reference to data owned by the current function</span></div></div><div><div><div>7</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>签名说”返回值借自 <code>text</code>”，但函数体返回了 <code>loud</code>，一个局部的 <code>String</code>。编译器直接指出。对照 C++ 的两个版本，我用 <code>-Wall -Wextra</code> 编译：</p><ul>
<li>返回 <code>const std::string&amp;</code> 指向局部变量：GCC 报 <code>warning: reference to local variable 'copy' returned [-Wreturn-local-addr]</code>。它抓到了最明显的写法。</li>
<li>返回 <code>std::string_view</code> 指向局部 <code>std::string</code>：<strong>没有任何警告</strong>，运行时打印的是乱码。</li>
</ul><p>C++ 的情况是”有些写法被抓到，有些没有”；Rust 的情况是<strong>所有</strong>没有借用来源的引用都是错误，没有例外，不需要警告开关。能用的版本（<code>ch02_dangling_ok.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>first_word</span><span>(</span><span>text</span><span>: &amp;</span><span>str</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>match</span><span> </span><span>text</span><span>.</span><span>find</span><span>(</span><span>' '</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>Some</span><span>(</span><span>end</span><span>) =&gt; &amp;</span><span>text</span><span>[..</span><span>end</span><span>],</span></div></div><div><div><div>4</div></div><div><span>        </span><span>None</span><span> =&gt; </span><span>text</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运行 <code>first_word(&amp;line)</code>（<code>line</code> 是 <code>String</code>，所以这里又一次发生 <code>&amp;String</code> 到 <code>&amp;str</code> 的自动转换）输出 <code>let</code>。返回值是 <code>text</code> 的一个切片，借自参数，没有任何局部变量被借走。</p><p><code>strip_line_ending</code> 和我们的 <code>line</code> 方法都是这个形状：返回的 <code>&amp;str</code> 借自输入，输入活着，借用就合法。</p></section>
<section><h2>字符和字节：<code>position</code><a href="#字符和字节position"><span>#</span></a></h2><p>最后一个查询，把字节偏移换成行和列：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Converts a byte offset into a one-based [`Position`].</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>position</span><span>(&amp;</span><span>self</span><span>, </span><span>offset</span><span>: </span><span>u32</span><span>) -&gt; </span><span>Position</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>index</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.</span><span>line_index</span><span>(</span><span>offset</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>line_start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.line_starts[</span><span>index</span><span> </span><span>as</span><span> </span><span>usize</span><span>];</span></div></div><div><div><div>6</div></div><div><span>        </span><span>let</span><span> </span><span>head</span><span><span> </span><span>=</span><span> &amp;</span></span><span>self</span><span>.text[</span><span>line_start</span><span> </span><span>as</span><span> </span><span>usize</span><span>..(</span><span>offset</span><span> </span><span>as</span><span> </span><span>usize</span><span>).</span><span>min</span><span>(</span><span>self</span><span>.text.</span><span>len</span><span>())];</span></div></div><div><div><div>7</div></div><div><span>        </span><span>Position</span><span> {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>line</span><span>: </span><span>index</span><span> + </span><span>1</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>// +1 because columns are one-based.</span></div></div><div><div><div>10</div></div><div><span>            </span><span>column</span><span>: </span><span>head</span><span>.</span><span>chars</span><span>().</span><span>count</span><span>() </span><span>as</span><span> </span><span>u32</span><span> + </span><span>1</span><span>,</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>head</code> 是从行首到 <code>offset</code> 的那一段文本（<code>min</code> 把过大的 <code>offset</code> 夹到文件末尾，免得切片越界），列号是这一段里的<strong>字符数</strong>加 1。关键是 <code>.chars().count()</code> 而不是 <code>.len()</code>。Rust 的 <code>String</code> 和 <code>str</code> 永远是合法的 UTF-8，<code>.len()</code> 是字节数。实验 <code>ch02_utf8.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>text</span><span><span> </span><span>=</span><span> </span></span><span>"ab</span><span>\n</span><span>中c"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>println!</span><span>(</span><span>"len()           = {}"</span><span>, </span><span>text</span><span>.</span><span>len</span><span>());</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"chars().count() = {}"</span><span>, </span><span>text</span><span>.</span><span>chars</span><span>().</span><span>count</span><span>());</span></div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"find('c')       = {:?}"</span><span>, </span><span>text</span><span>.</span><span>find</span><span>(</span><span>'c'</span><span>));</span></div></div><div><div><div>5</div></div><div><span>let</span><span> </span><span>cut</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>hint</span><span>::</span></span><span>black_box</span><span>(</span><span>4</span><span>);</span></div></div><div><div><div>6</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, &amp;</span><span>text</span><span>[</span><span>cut</span><span>..]);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch02_utf8</span></div></div><div><div><div>2</div></div><div><span>len()           = 7</span></div></div><div><div><div>3</div></div><div><span>chars().count() = 5</span></div></div><div><div><div>4</div></div><div><span>find('c')       = Some(6)</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>byte 0: 'a' (1 bytes)</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>byte 1: 'b' (1 bytes)</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>byte 2: '\n' (1 bytes)</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>byte 3: '中' (3 bytes)</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>byte 6: 'c' (1 bytes)</span></div></div><div><div><div>10</div></div><div><span>is_char_boundary(4) = false</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>thread 'main' (78667) panicked at src/bin/ch02_utf8.rs:11:25:</span></div></div><div><div><div>13</div></div><div><span>start byte index 4 is not a char boundary; it is inside '中' (bytes 3..6) of `ab</span></div></div><div><div><div>14</div></div><div><span>中c`</span></div></div><div><div><div>15</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>7 个字节，5 个字符；<code>c</code> 的字节偏移是 6，但它在第 2 行的第 2 列。偏移 4 落在 <code>中</code> 的中间，切片直接 panic，消息说得很清楚。这是 Rust 对 <code>str</code> 的一个设计：它不会让你构造出非法的 UTF-8 切片。C++ 里 <code>std::string</code> 只是字节袋，你可以随便在字符中间切开，之后下游函数才会发现编码坏了。代价是 Rust 里 <code>text[3]</code> 这种按字符下标取值是不允许的（编译错误），因为 UTF-8 是变长的，O(1) 的”第 n 个字符”不存在；要么按字节切片（并保证边界合法），要么 <code>.chars()</code> 遍历。</p><p>我们的 <code>position</code> 只会在合法的偏移上被调用（偏移来自词法分析器，它按字符前进），所以不会触发这个 panic。如果有人传了落在字符中间的偏移，panic 是正确的响应：这是一个编译器内部的 bug，不是用户的错。</p><section><h3><code>Debug</code>：不打印全部文本<a href="#debug不打印全部文本"><span>#</span></a></h3><p><code>SourceFile</code> 里有整个文件的文本，派生 <code>Debug</code> 会把它全打出来，一个 1000 行的文件会淹没断言失败的输出。所以我手写了一个只打印摘要的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>SourceFile</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>f</span><span>.</span><span>debug_struct</span><span>(</span><span>"SourceFile"</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>.</span><span>field</span><span>(</span><span>"id"</span><span>, &amp;</span><span>self</span><span>.id)</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>field</span><span>(</span><span>"name"</span><span>, &amp;</span><span>self</span><span>.name)</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>.</span><span>field</span><span>(</span><span>"lines"</span><span>, &amp;</span><span>self</span><span>.</span><span>line_count</span><span>())</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>.</span><span>field</span><span>(</span><span>"bytes"</span><span>, &amp;</span><span>self</span><span>.text.</span><span>len</span><span>())</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>.</span><span>finish</span><span>()</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>debug_struct</code> 是标准库提供的”构造器”，一连串方法调用最后以 <code>.finish()</code> 结束（这种写法叫 builder，第 3 篇会自己写一个）。<code>&amp;self.line_count()</code> 借用的是一个临时值，这行结束就释放，没有问题。完整的文件顺序是：<code>impl SourceFile</code> 之后紧跟这个 <code>Debug</code> 实现，然后是 <code>compute_line_starts</code>，然后是 <code>strip_line_ending</code>。</p></section><section><h3>为 <code>SourceFile</code> 写测试<a href="#为-sourcefile-写测试"><span>#</span></a></h3><p>把下面三个测试加到 <code>mod tests</code> 的最前面，也就是 <code>use super::*;</code> 的下面、<code>merge_covers_both</code> 的上面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>line_starts_include_a_line_after_a_trailing_newline</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>map_file</span><span><span> </span><span>=</span><span> </span></span><span>SourceFile</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"t"</span><span>.</span><span>into</span><span>(), </span><span>"a</span><span>\n</span><span>b</span><span>\n</span><span>"</span><span>.</span><span>into</span><span>());</span></div></div><div><div><div>4</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>map_file</span><span>.</span><span>line_count</span><span>(), </span><span>3</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>map_file</span><span>.</span><span>line</span><span>(</span><span>1</span><span>), </span><span>Some</span><span>(</span><span>"a"</span><span>));</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>map_file</span><span>.</span><span>line</span><span>(</span><span>2</span><span>), </span><span>Some</span><span>(</span><span>"b"</span><span>));</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>map_file</span><span>.</span><span>line</span><span>(</span><span>3</span><span>), </span><span>Some</span><span>(</span><span>""</span><span>));</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>fn</span><span> </span><span>handles_crlf</span><span>() {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>file</span><span><span> </span><span>=</span><span> </span></span><span>SourceFile</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"t"</span><span>.</span><span>into</span><span>(), </span><span>"a</span><span>\r\n</span><span>b"</span><span>.</span><span>into</span><span>());</span></div></div><div><div><div>13</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>file</span><span>.</span><span>line</span><span>(</span><span>1</span><span>), </span><span>Some</span><span>(</span><span>"a"</span><span>));</span></div></div><div><div><div>14</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>file</span><span>.</span><span>line</span><span>(</span><span>2</span><span>), </span><span>Some</span><span>(</span><span>"b"</span><span>));</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>18</div></div><div><span>    </span><span>fn</span><span> </span><span>position_counts_characters_not_bytes</span><span>() {</span></div></div><div><div><div>19</div></div><div><span>        </span><span>// Each CJK character is three bytes; the second line's "c" is the 5th</span></div></div><div><div><div>20</div></div><div><span>        </span><span>// column even though it sits at byte offset 8.</span></div></div><div><div><div>21</div></div><div><span>        </span><span>let</span><span> </span><span>file</span><span><span> </span><span>=</span><span> </span></span><span>SourceFile</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>"t"</span><span>.</span><span>into</span><span>(), </span><span>"ab</span><span>\n</span><span>中c"</span><span>.</span><span>into</span><span>());</span></div></div><div><div><div>22</div></div><div><span>        </span><span>let</span><span> </span><span>offset</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>text</span><span>().</span><span>find</span><span>(</span><span>'c'</span><span>).</span><span>unwrap</span><span>() </span><span>as</span><span> </span><span>u32</span><span>;</span></div></div><div><div><div>23</div></div><div><span>        </span><span>let</span><span> </span><span>pos</span><span><span> </span><span>=</span><span> </span></span><span>file</span><span>.</span><span>position</span><span>(</span><span>offset</span><span>);</span></div></div><div><div><div>24</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>pos</span><span>.line, </span><span>2</span><span>);</span></div></div><div><div><div>25</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>pos</span><span>.column, </span><span>2</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我要诚实地指出：这第三个测试里的注释写得不准（<code>c</code> 实际在字节偏移 6，第 2 列，上面的实验已经证明；注释里的数字是我早期写的，后来改了断言但没改注释）。注释留着是因为这一篇的目标是文件和仓库逐字节一致，不过断言本身是对的，这也是为什么<strong>测试比注释可信</strong>。</p><p>几点：</p><ul>
<li><code>SourceFile::new</code> 是私有的，但测试在同一个模块的子模块里，<code>use super::*</code> 把它带进来，所以能调用。这就是”同模块可访问私有”的实际用处，单元测试可以直接测内部函数，不用为了测试而 <code>pub</code>。</li>
<li><code>"t".into()</code>：<code>new</code> 要的是 <code>String</code>，字面量是 <code>&amp;str</code>，<code>.into()</code> 做转换，目标类型靠函数签名推断。下面 <code>SourceMap::add</code> 会正式讲 <code>Into</code>。</li>
<li><code>assert_eq!(map_file.line(1), Some("a"))</code> 比较的是两个 <code>Option&lt;&amp;str&gt;</code>。<code>Option&lt;T&gt;</code> 在 <code>T</code> 可以比较时自己也可以比较，不需要拆开。</li>
<li><code>.find('c')</code> 返回 <code>Option&lt;usize&gt;</code>，<code>.unwrap()</code> 取出里面的值，没有的话 panic。测试里 <code>unwrap</code> 是可以的：这里我们<strong>知道</strong>有，没有就是测试写错了；在库代码里用 <code>unwrap</code> 要三思，第 7 篇再说。</li>
</ul><p>此时 <code>cargo test -p pebble-span</code> 应该有 5 个测试通过（原来的两个加上这三个）。</p></section></section>
<section><h2>第一个 trait：<code>Spanned</code><a href="#第一个-traitspanned"><span>#</span></a></h2><p>接下来是文件里的第一个手写 trait。放在 <code>strip_line_ending</code> 的后面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Anything that knows the source range it came from.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// The AST implements this for every node; the `pebble-macros` crate can derive</span></div></div><div><div><div>4</div></div><div><span>/// it, and `pebble-span` provides the blanket cases.</span></div></div><div><div><div>5</div></div><div><span>pub</span><span> </span><span>trait</span><span> </span><span>Spanned</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// The node's source range.</span></div></div><div><div><div>7</div></div><div><span>    </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Span</span><span>;</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>impl</span><span> </span><span>Spanned</span><span> </span><span>for</span><span> </span><span>Span</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>fn</span><span> </span><span>span</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>*</span><span>self</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>trait</strong> 是一组方法签名的集合，类型可以”实现”它。最接近的 C++ 概念是”带纯虚函数的抽象基类”，或者 C++20 的 concept。和基类相比有三点不同：一，实现与类型定义分开，<code>impl Spanned for Span</code> 写在任何地方都行（只要这个 crate 拥有 trait 或者类型，这条”孤儿规则”是第 3 篇的内容）；二，实现不改变类型的内存布局，没有隐藏的虚表指针；三，默认是<strong>静态分派</strong>，编译器知道具体类型，没有虚调用（需要动态分派时有 <code>dyn</code>，第 8 篇讲）。</p><p>注释说”AST 会为每个节点实现它”。这是预告：第 5 篇我们手写那些实现，第 6 篇写一个过程宏替我们写。<code>impl Spanned for Span</code> 里 <code>*self</code> 是把 <code>&amp;Span</code> 解引用复制出一个 <code>Span</code>，因为 <code>Span</code> 是 <code>Copy</code>，这一步不会被”不能从借用里移出”拦住，下面马上讲到的那种错误在这里不会出现。</p><p>顺带，<code>Spanned</code> 的方法要在 trait 被引入作用域之后才能用点语法调用（调用方需要 <code>use pebble_span::Spanned;</code>）。不然 <code>x.span()</code> 找不到方法，这是 Rust 避免多个 trait 的同名方法冲突的办法。</p></section>
<section><h2>写 <code>SourceMap</code>：<code>String</code> 的最终归宿<a href="#写-sourcemapstring-的最终归宿"><span>#</span></a></h2><p>现在轮到整个 crate 的核心，那个拥有所有文件的结构：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Owns every source file and hands out [`SourceId`]s.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Default</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>SourceMap</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>files</span><span>: </span><span>Vec</span><span>&lt;</span><span>SourceFile</span><span>&gt;,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span> </span><span>SourceMap</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>/// Creates an empty map.</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>10</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>new</span><span>() -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Self</span><span>::</span><span>default</span><span>()</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>SourceMap</code> 只有一个私有字段，一个 <code>Vec&lt;SourceFile&gt;</code>。<code>#[derive(Default)]</code> 是又一个可派生的 trait：<code>Default</code> 提供 <code>T::default()</code>，“这个类型的零值”，派生版本对每个字段调用它自己的 <code>default()</code>，<code>Vec</code> 的默认值是空向量。<code>new()</code> 只是转发给它。（C++ 里这相当于一个默认构造函数。）</p><section><h3><code>add</code>：<code>impl Into&lt;String&gt;</code> 和可变借用<a href="#addimpl-intostring-和可变借用"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Registers a new file and returns its id.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>add</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>, </span><span>name</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;, </span><span>text</span><span>: </span><span>impl</span><span> </span><span>Into</span><span>&lt;</span><span>String</span><span>&gt;) -&gt; </span><span>SourceId</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>id</span><span><span> </span><span>=</span><span> </span></span><span>SourceId</span><span>(</span><span>self</span><span>.files.</span><span>len</span><span>() </span><span>as</span><span> </span><span>u32</span><span>);</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.files</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>push</span><span><span>(</span><span>SourceFile</span><span>::</span></span><span>new</span><span>(</span><span>id</span><span>, </span><span>name</span><span>.</span><span>into</span><span>(), </span><span>text</span><span>.</span><span>into</span><span>()));</span></div></div><div><div><div>6</div></div><div><span>        </span><span>id</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有三件新事。</p><p><strong><code>&amp;mut self</code></strong>：可变借用。<code>add</code> 要往 <code>self.files</code> 里 <code>push</code>，所以必须能修改 <code>self</code>。<code>&amp;self</code> 是只读借用，<code>&amp;mut self</code> 是读写借用，调用者的变量必须是 <code>let mut map</code>。</p><p><strong><code>impl Into&lt;String&gt;</code></strong>：参数类型不是 <code>String</code>，也不是 <code>&amp;str</code>，而是”任何能转换成 <code>String</code> 的类型”。调用者可以传字符串字面量、<code>String</code>、<code>format!(...)</code> 的结果，函数体里统一用 <code>.into()</code> 转成 <code>String</code>。<code>Into</code> 是标准库的 trait，<code>&amp;str</code> 和 <code>String</code> 都实现了到 <code>String</code> 的转换。这样 API 对调用者友好：<code>map.add("a.pebble", "...")</code> 不需要 <code>.to_string()</code>。实验 <code>ch02_into.rs</code> 验证三种调用都能工作（输出 <code>8 9 8</code>）；传一个整数则是：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_into_bad</span></div></div><div><div><div>2</div></div><div><span>error[E0277]: the trait bound `String: From&lt;{integer}&gt;` is not satisfied</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_into_bad.rs:7:29</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>7 |     println!("{}", register(42));</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                    -------- ^^ the trait `From&lt;{integer}&gt;` is not implemented for `String`</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                    |</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|                    required by a bound introduced by this call</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>= help: the following other types implement trait `From&lt;T&gt;`:</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>`String` implements `From&lt;&amp;String&gt;`</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>`String` implements `From&lt;&amp;mut str&gt;`</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>`String` implements `From&lt;&amp;str&gt;`</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>`String` implements `From&lt;Box&lt;str&gt;&gt;`</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>`String` implements `From&lt;Cow&lt;'_, str&gt;&gt;`</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>`String` implements `From&lt;char&gt;`</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>= note: required for `{integer}` to implement `Into&lt;String&gt;`</span></div></div><div><div><div>18</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这是第一次出现”trait bound”：函数对参数类型提出的要求。<code>impl Trait</code> 写在参数位置是”某个实现了该 trait 的类型”的简写，它背后是<strong>泛型</strong>，第 5 篇完整讲。你现在可以把它读成”接受任何能转成 <code>String</code> 的东西”。（和 C++ 的 <code>template &lt;std::convertible_to&lt;std::string&gt; S&gt;</code> 很像。）</p><p><strong>所有权的流转</strong>：调用者传进来一个 <code>&amp;str</code>，<code>.into()</code> 把它复制成一个新的 <code>String</code>；如果调用者传进来的本来就是 <code>String</code>，<code>.into()</code> 直接移动，没有复制。无论哪种，<code>SourceMap</code> 里的 <code>SourceFile</code> <strong>拥有</strong>自己的文本，调用者的变量（如果有的话）和 <code>SourceMap</code> 之间没有任何关系。这回答了本篇标题的问题：<strong><code>String</code> 归 <code>SourceFile</code>，<code>SourceFile</code> 归 <code>SourceMap</code></strong>。其他所有人（词法分析器、诊断渲染器）只能借用。</p></section><section><h3>两种取文件的方式，以及可变借用的代价<a href="#两种取文件的方式以及可变借用的代价"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Looks up a file by id, returning `None` for an unknown id.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>get</span><span>(&amp;</span><span>self</span><span>, </span><span>id</span><span>: </span><span>SourceId</span><span>) -&gt; </span><span>Option</span><span>&lt;&amp;</span><span>SourceFile</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>self</span><span>.files.</span><span>get</span><span>(</span><span>id</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>/// Number of registered files.</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>len</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>usize</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>self</span><span>.files.</span><span>len</span><span>()</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>    </span><span>/// Whether no files have been registered.</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>15</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>is_empty</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>self</span><span>.files.</span><span>is_empty</span><span>()</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>get</code> 返回 <code>Option&lt;&amp;SourceFile&gt;</code>：借用，可能没有。<code>len</code> 和 <code>is_empty</code> 是一对：我试过只写 <code>len</code>，clippy 直接报错：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: struct `SourceMap` has a public `len` method, but no `is_empty` method</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-span/src/lib.rs:278:5</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>278 |     pub fn len(&amp;self) -&gt; usize {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|     ^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_without_is_empty</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>= note: `-D clippy::len-without-is-empty` implied by `-D warnings`</span></div></div><div><div><div>9</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 clippy 的 <code>len_without_is_empty</code> 规则，约定任何有 <code>len</code> 的容器都应该有 <code>is_empty</code>。</p><p>现在是这篇文章最重要的一个实验：<strong>共享借用和可变借用不能同时存在</strong>。<code>ch02_borrow.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>files</span><span><span> </span><span>=</span><span> </span></span><span>vec!</span><span>[</span><span>String</span><span>::</span><span>from</span><span>(</span><span>"a.pebble"</span><span>)];</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> &amp;</span></span><span>files</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>3</div></div><div><span>files</span><span>.</span><span>push</span><span><span>(</span><span>String</span><span>::</span></span><span>from</span><span>(</span><span>"b.pebble"</span><span>));</span></div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"first = {first}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_borrow</span></div></div><div><div><div>2</div></div><div><span>error[E0502]: cannot borrow `files` as mutable because it is also borrowed as immutable</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_borrow.rs:4:5</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>3 |     let first = &amp;files[0];</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                  ----- immutable borrow occurs here</span></div></div><div><div><div>7</div></div><div><span>4 |     files.push(String::from("b.pebble"));</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here</span></div></div><div><div><div>9</div></div><div><span>5 |     println!("first = {first}");</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|                        ----- immutable borrow later used here</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 C++ 里经典的迭代器失效：拿着 <code>vector</code> 里某个元素的引用，然后 <code>push_back</code>，<code>vector</code> 重新分配了内存，旧引用悬空。C++ 编译这段没问题，运行时是未定义行为，Rust 在编译期拒绝它。规则是：</p><ul>
<li>任何时刻，对一个值要么有<strong>任意多个共享借用</strong>（<code>&amp;T</code>，只读），要么<strong>恰好一个可变借用</strong>（<code>&amp;mut T</code>，读写），不能两者同时存在。</li>
<li>引用不能比它指向的值活得久（上面的悬垂引用）。</li>
</ul><p>这叫”<strong>别名与可变性互斥</strong>”。很多人觉得它很苛刻；我的体会是，它精确地对应了所有”在遍历时修改容器”一类的 bug，换来的是编译器可以保证没有数据竞争，也不需要 C++ 里那种”小心引用失效”的长期警惕。</p><p>注意报错里最后一行：<code>immutable borrow later used here</code>。借用的<strong>结束点是最后一次使用</strong>，不是作用域结束。这叫 NLL（non-lexical lifetimes）。把 <code>println!</code> 挪到 <code>push</code> 之前，同样的代码就合法了（<code>ch02_nll.rs</code>，输出 <code>first = a.pebble</code> 和 <code>now 2 files</code>）。借用检查器看的是”这个引用之后还会不会被用到”，而不是花括号的位置。</p><p><code>SourceMap::add</code> 是 <code>&amp;mut self</code>，所以一个 <code>&amp;SourceFile</code>（比如 <code>&amp;map[id]</code>）活着的时候不能再 <code>add</code>。这在真实代码里是个切实的约束：编译一个文件的过程中不能顺手注册新文件。我们放到练习里体验它。</p></section><section><h3>从 <code>&amp;mut</code> 里搬出东西：<code>std::mem::take</code><a href="#从-mut-里搬出东西stdmemtake"><span>#</span></a></h3><p>既然提到 <code>&amp;mut self</code>，还有一个新手几乎一定会撞上的墙。实验 <code>ch02_take.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Holder</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>name</span><span>: </span><span>String</span><span>,</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>impl</span><span> </span><span>Holder</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>steal</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>) -&gt; </span><span>String</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>self</span><span>.name</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_take</span></div></div><div><div><div>2</div></div><div><span>error[E0507]: cannot move out of `self.name` which is behind a mutable reference</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_take.rs:7:9</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>7 |         self.name</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|         ^^^^^^^^^ move occurs because `self.name` has type `String`, which does not implement the `Copy` trait</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span>help: consider cloning the value if the performance cost is acceptable</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>7 |         self.name.clone()</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|                  ++++++++</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我们只借了 <code>self</code>，不拥有它。把 <code>name</code> 搬走会让 <code>self</code> 留下一个”空洞”，这个对象之后还要被其他人使用甚至析构，对空洞做析构就是 double free。C++ 的做法是让你写 <code>std::move(self.name)</code>，把字符串掏空，留下一个有效的空串。Rust 的要求是：<strong>搬走的同时，必须放回一个有效的值</strong>。标准库给了三个工具（<code>ch02_take_ok.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mem</span><span>::</span></span><span>take</span><span>(&amp;</span><span>mut</span><span> </span><span>self</span><span>.name)                          </span><span>// 取走，留下 String::default()，即空串</span></div></div><div><div><div>2</div></div><div><span><span>mem</span><span>::</span></span><span>replace</span><span>(&amp;</span><span>mut</span><span> </span><span>holder</span><span><span>.name, </span><span>String</span><span>::</span></span><span>from</span><span>(</span><span>"b.pebble"</span><span>))  </span><span>// 取走，换上你指定的值</span></div></div><div><div><div>3</div></div><div><span><span>mem</span><span>::</span></span><span>swap</span><span>(&amp;</span><span>mut</span><span> </span><span>holder</span><span>.name, &amp;</span><span>mut</span><span> </span><span>other</span><span>)            </span><span>// 两个位置互换</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch02_take_ok</span></div></div><div><div><div>2</div></div><div><span>stolen = "a.pebble"</span></div></div><div><div><div>3</div></div><div><span>left   = ""</span></div></div><div><div><div>4</div></div><div><span>old = "", new = "b.pebble"</span></div></div><div><div><div>5</div></div><div><span>holder = "c.pebble", other = "b.pebble"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>take</code> 对应 C++ 的 <code>std::exchange(x, T{})</code>，<code>replace</code> 对应 <code>std::exchange(x, v)</code>，<code>swap</code> 对应 <code>std::swap</code>。老实说，我 <code>grep</code> 过整个 Pebble 仓库，一次也没有用到它们：Pebble 的数据流设计得不需要从借用里掏东西。但你一定会在别人的代码里到处遇到，而 E0507 是新手最常撞的墙之一，所以放在这里。</p></section><section><h3>运算符重载：<code>Index</code><a href="#运算符重载index"><span>#</span></a></h3><p><code>SourceMap</code> 里还差一个：<code>map[id]</code> 这种写法。C++ 里你重载 <code>operator[]</code>。Rust 里运算符是 trait：<code>a + b</code> 是 <code>Add::add(a, b)</code> 的语法糖，<code>a[b]</code> 是 <code>Index::index(&amp;a, b)</code> 的语法糖。第 1 篇你已经在 <code>u32 + usize</code> 的报错里看过它们的影子。实验 <code>ch02_ops_missing.rs</code>，没有实现 <code>Add</code> 的 <code>Offset</code> 做加法：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch02_ops_missing</span></div></div><div><div><div>2</div></div><div><span>error[E0369]: cannot add `Offset` to `Offset`</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_ops_missing.rs:7:24</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>7 |     println!("{:?}", a + b);</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|                      - ^ - Offset</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                      |</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|                      Offset</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>note: an implementation of `Add` might be missing for `Offset`</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_ops_missing.rs:2:1</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>2 | struct Offset(u32);</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>| ^^^^^^^^^^^^^ must implement `Add`</span></div></div><div><div><div>15</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>在 <code>ch02_ops.rs</code> 里我给 <code>Offset</code> 实现了 <code>Add</code>、<code>Add&lt;u32&gt;</code>（和不同类型相加）、<code>Sub</code>（返回 <code>u32</code> 而不是 <code>Offset</code>，<code>Output</code> 是关联类型，由你决定）、<code>AddAssign</code>（<code>+=</code>）和 <code>Index</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Add</span><span> </span><span>for</span><span> </span><span>Offset</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>Offset</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>add</span><span>(</span><span>self</span><span>, </span><span>other</span><span>: </span><span>Offset</span><span>) -&gt; </span><span>Offset</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Offset</span><span>(</span><span>self</span><span>.</span><span>0</span><span> + </span><span>other</span><span>.</span><span>0</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>impl</span><span> </span><span>Index</span><span>&lt;</span><span>Offset</span><span>&gt; </span><span>for</span><span> </span><span>Names</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>String</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>fn</span><span> </span><span>index</span><span>(&amp;</span><span>self</span><span>, </span><span>at</span><span>: </span><span>Offset</span><span>) -&gt; &amp;</span><span>String</span><span> {</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.</span><span>0</span><span>[</span><span>at</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch02_ops</span></div></div><div><div><div>2</div></div><div><span>Offset(7) Offset(4) 14</span></div></div><div><div><div>3</div></div><div><span>b.pebble</span></div></div><div><div><div>4</div></div><div><span>...</span></div></div><div><div><div>5</div></div><div><span>thread 'main' (78868) panicked at src/bin/ch02_ops.rs:51:25:</span></div></div><div><div><div>6</div></div><div><span>index out of bounds: the len is 2 but the index is 9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（最后几行是故意越界的 <code>names[Offset(9)]</code>。）一条规则：<code>impl Add for Offset</code> 的意思是 <code>Offset + Offset</code> 可用，<code>Add&lt;u32&gt;</code> 是 <code>Offset + u32</code>，每一种右操作数各自是一个 trait 实例，这比 C++ 里 <code>operator+</code> 重载解析要明确。<code>type Output</code> 是 trait 里的<strong>关联类型</strong>：实现者在这里声明”这个运算的结果类型是什么”。</p><p>现在为 <code>SourceMap</code> 写 <code>Index&lt;SourceId&gt;</code>。放在 <code>impl SourceMap</code> 的后面：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>core</span><span>::</span><span>ops</span><span>::</span></span><span>Index</span><span>&lt;</span><span>SourceId</span><span>&gt; </span><span>for</span><span> </span><span>SourceMap</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>SourceFile</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>/// Looks up a file by id, panicking on an unknown id.</span></div></div><div><div><div>5</div></div><div><span>    </span><span>///</span></div></div><div><div><div>6</div></div><div><span>    </span><span>/// Ids only ever come from this map, so a bad id is a bug in the compiler,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>/// not something the user did. Use [`SourceMap::get`] for a fallible lookup.</span></div></div><div><div><div>8</div></div><div><span>    </span><span>///</span></div></div><div><div><div>9</div></div><div><span>    </span><span>/// # Panics</span></div></div><div><div><div>10</div></div><div><span>    </span><span>///</span></div></div><div><div><div>11</div></div><div><span>    </span><span>/// Panics when `id` did not come from this map.</span></div></div><div><div><div>12</div></div><div><span>    </span><span>fn</span><span> </span><span>index</span><span>(&amp;</span><span>self</span><span>, </span><span>id</span><span>: </span><span>SourceId</span><span>) -&gt; &amp;</span><span>SourceFile</span><span> {</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>&amp;</span><span>self</span><span>.files[</span><span>id</span><span>.</span><span>0</span><span> </span><span>as</span><span> </span><span>usize</span><span>]</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>map[id]</code> 现在等价于 <code>*map.index(id)</code>。<code>index</code> 的实现用 <code>self.files[...]</code>，所以越界时 panic。一个 panic 的函数应该在文档里说明，惯例是 <code># Panics</code> 小节，clippy 会检查它。<code>get</code> 和 <code>[]</code> 同时存在，就像 <code>Vec::get</code> 和 <code>vec[i]</code>：<strong>调用者知道 id 一定合法就用 <code>[]</code>，不确定就用 <code>get</code></strong>。（C++ 里这对搭档是 <code>operator[]</code>（不检查）和 <code>at()</code>（检查，抛异常）。Rust 的 <code>[]</code> 检查并 panic，不会 UB。）</p><p>没写 <code>Index</code> 时，下面这个测试会得到什么（它就是第 6 个测试）？</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>source_map_assigns_sequential_ids</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>map</span><span>.</span><span>add</span><span>(</span><span>"a.pebble"</span><span>, </span><span>""</span><span>);</span></div></div><div><div><div>5</div></div><div><span>        </span><span>let</span><span> </span><span>second</span><span><span> </span><span>=</span><span> </span></span><span>map</span><span>.</span><span>add</span><span>(</span><span>"b.pebble"</span><span>, </span><span>""</span><span>);</span></div></div><div><div><div>6</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>first</span><span>, </span><span>SourceId</span><span>(</span><span>0</span><span>));</span></div></div><div><div><div>7</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>second</span><span>, </span><span>SourceId</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>8</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>map</span><span>[</span><span>second</span><span>].</span><span>name</span><span>(), </span><span>"b.pebble"</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>把它加到 <code>mod tests</code> 的末尾（<code>merge_across_files_is_a_bug</code> 的后面），然后在还没写 <code>Index</code> 的时候运行 <code>cargo test</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-span</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-span v0.1.0 (/tmp/pebble-rebuild-02/crates/pebble-span)</span></div></div><div><div><div>3</div></div><div><span>error[E0608]: cannot index into a value of type `SourceMap`</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-span/src/lib.rs:339:23</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>339 |         assert_eq!(map[second].name(), "b.pebble");</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|                       ^^^^^^^^</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>For more information about this error, try `rustc --explain E0608`.</span></div></div><div><div><div>10</div></div><div><span>error: could not compile `pebble-span` (lib test) due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>写上 <code>Index</code> 的实现，这个错误消失。为了让你看到 <code>[]</code> 越界到底说什么，我用一个临时的小程序对一个只有两个文件的 <code>SourceMap</code> 查 <code>SourceId(9)</code>：<code>map.get(SourceId(9))</code> 给出 <code>None</code>，而 <code>map[SourceId(9)]</code> 给出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>thread 'main' (83327) panicked at crates/pebble-span/examples/ex_index_panic.rs:8:23:</span></div></div><div><div><div>2</div></div><div><span>index out of bounds: the len is 2 but the index is 9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后，<code>SourceMap</code> 的 <code>Debug</code>，和 <code>SourceFile</code> 一样，只打印摘要。放在 <code>Index</code> 实现之后：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>SourceMap</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>f</span><span>.</span><span>debug_struct</span><span>(</span><span>"SourceMap"</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>.</span><span>field</span><span>(</span><span>"files"</span><span>, &amp;</span><span>self</span><span>.files.</span><span>len</span><span>())</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>.</span><span>finish</span><span>()</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>到这里 <code>SourceMap</code> 写完了，上面那个 dead_code 警告也消失了：<code>SourceMap::add</code> 调用了 <code>SourceFile::new</code>。这时的 <code>cargo test -p pebble-span</code> 应该有 6 个测试通过。但我们还差一件事。</p></section></section>
<section><h2><code>no_std</code>：这个 crate 不需要操作系统<a href="#no_std这个-crate-不需要操作系统"><span>#</span></a></h2><p>在文件的最前面加两行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![cfg_attr(not(test), no_std)]</span></div></div><div><div><div>2</div></div><div><span>#![forbid(unsafe_code)]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行：<code>#![...]</code> 是<strong>内层属性</strong>，作用于整个 crate（对比 <code>#[...]</code> 只作用于紧跟着的那一项）。<code>cfg_attr(条件, 属性)</code> 是”条件满足时才应用这个属性”，这里的意思是”<strong>除了测试时</strong>，这个 crate 是 <code>no_std</code>”。</p><p>第二行：<code>forbid(unsafe_code)</code> 让整个 crate 里出现 <code>unsafe</code> 成为编译错误。我加了一个临时函数里写 <code>unsafe { *(&amp;x as *const u8) }</code> 试了一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: usage of an `unsafe` block</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-span/src/lib.rs:236:5</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>236 |     unsafe { *(&amp;x as *const u8) }</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|     ^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span>note: the lint level is defined here</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-span/src/lib.rs:2:11</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>2 | #![forbid(unsafe_code)]</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|           ^^^^^^^^^^^</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>forbid</code> 比 <code>deny</code> 更强：连局部的 <code>#[allow]</code> 也不能放行。<code>unsafe</code> 是什么，第 11 篇讲；这里先记住 <code>pebble-span</code> 保证不用它。这对读者也是一个信号：这一个 crate 里的所有代码，编译器都替你证明了内存安全。</p><section><h3><code>core</code>、<code>alloc</code>、<code>std</code>：三层标准库<a href="#coreallocstd三层标准库"><span>#</span></a></h3><p><code>no_std</code> 是什么？Rust 的标准库分三层：</p><div><div><div><div><span></span></div><div><span></span></div><div><span><p>core<br /><br />语言基础：Option、fmt、整数方法、迭代器、Cell...<br /><br />不需要操作系统，不需要堆</p></span></div><div><span><p>alloc<br /><br />需要堆分配器：String、Vec、Box、Rc...<br /><br />不需要操作系统</p></span></div><div><span><p>std<br /><br />操作系统相关：文件、线程、网络、HashMap、println!...</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span><p>core<br /><br />语言基础：Option、fmt、整数方法、迭代器、Cell...<br /><br />不需要操作系统，不需要堆</p></span></div><div><span><p>alloc<br /><br />需要堆分配器：String、Vec、Box、Rc...<br /><br />不需要操作系统</p></span></div><div><span><p>std<br /><br />操作系统相关：文件、线程、网络、HashMap、println!...</p></span></div>
</div></div></div><p><code>std</code> 是你平常用的那个，它把 <code>core</code> 和 <code>alloc</code> 的东西重新导出，所以 <code>std::fmt</code> 和 <code>core::fmt</code> 是同一个东西，这也兑现了第 1 篇的承诺。<code>#![no_std]</code> 的意思是”我只链接 <code>core</code>，不链接 <code>std</code>”。对应 C 里的 freestanding 与 hosted：freestanding 环境（<code>-ffreestanding</code>）没有 <code>malloc</code>，没有 <code>printf</code>，没有文件，只有语言本身；hosted 才有完整的标准库。</p><p>我为什么要这样限制一个看起来是普通数据结构的 crate？因为第 13 篇会写一个跑在没有操作系统的环境里的求值器，它要用 Pebble 的词法分析器，而词法分析器依赖 <code>pebble-span</code>。依赖链上的每一个 crate 都得是 <code>no_std</code>，<strong>一个 crate 不能比它的依赖更自由</strong>。</p><p>我们试一试。只加第一行属性（不加 <code>forbid</code>），然后 <code>cargo build -p pebble-span</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build -p pebble-span</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-span v0.1.0 (/tmp/pebble-rebuild-02/crates/pebble-span)</span></div></div><div><div><div>3</div></div><div><span>error[E0425]: cannot find type `String` in this scope</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-span/src/lib.rs:114:11</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>114 |     name: String,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>|           ^^^^^^ not found in this scope</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>error[E0425]: cannot find type `String` in this scope</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>--&gt; crates/pebble-span/src/lib.rs:115:11</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>115 |     text: String,</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>|           ^^^^^^ not found in this scope</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>...</span></div></div><div><div><div>16</div></div><div><span>Some errors have detailed explanations: E0425, E0433.</span></div></div><div><div><div>17</div></div><div><span>For more information about this error, try `rustc --explain E0425`.</span></div></div><div><div><div>18</div></div><div><span>error: could not compile `pebble-span` (lib) due to 10 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>一共 10 条错误，都是同一件事：<code>String</code> 和 <code>Vec</code> 不在了。它们之前在<strong>预导入</strong>（prelude）里：每个 Rust 文件开头都隐式有 <code>use std::prelude::*</code>，里面有 <code>String</code>、<code>Vec</code>、<code>Option</code>、<code>Some</code>、<code>None</code>、<code>Box</code> 等。<code>no_std</code> 的预导入是 <code>core</code> 的那一份，没有堆类型。</p><p><code>String</code> 和 <code>Vec</code> 属于 <code>alloc</code> 层。要用它们，先把 <code>use alloc::string::String; use alloc::vec::Vec;</code> 写上，试一下（只加 <code>use</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0433]: cannot find module or crate `alloc` in this scope</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; crates/pebble-span/src/lib.rs:8:5</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>8 | use alloc::string::String;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|     ^^^^^ use of unresolved module or unlinked crate `alloc`</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= help: add `extern crate alloc` to use the `alloc` crate</span></div></div><div><div><div>8</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器告诉你要显式链接 <code>alloc</code>：<code>extern crate alloc;</code>（<code>extern crate</code> 是 2015 年时代的写法，现在几乎只用于 <code>alloc</code> 这种”标准库里不在预导入的 crate”）。把文件开头整个换成这样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#![cfg_attr(not(test), no_std)]</span></div></div><div><div><div>2</div></div><div><span>#![forbid(unsafe_code)]</span></div></div><div><div><div>3</div></div><div><span>//! Source offsets, spans, and source maps.</span></div></div><div><div><div>4</div></div><div><span>//!</span></div></div><div><div><div>5</div></div><div><span>//! Everything in the front end of the compiler talks about positions in terms</span></div></div><div><div><div>6</div></div><div><span>//! of *byte offsets into a source file*. Those offsets are grouped into</span></div></div><div><div><div>7</div></div><div><span>//! [`Span`]s, and [`SourceMap`] is the thing that turns an offset back into a</span></div></div><div><div><div>8</div></div><div><span>//! human position (`file.pebble:12:7`).</span></div></div><div><div><div>9</div></div><div><span>//!</span></div></div><div><div><div>10</div></div><div><span>//! The crate is `#![no_std]` on purpose: it only needs `core` and `alloc`, so</span></div></div><div><div><div>11</div></div><div><span>//! the same code runs on a microcontroller (see `pebble-nostd`). That is also</span></div></div><div><div><div>12</div></div><div><span>//! why you will see `alloc::string::String` instead of `std::string::String`.</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>extern</span><span> </span><span>crate</span><span> alloc;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::</span><span>string</span><span>::</span></span><span>String</span><span>;</span></div></div><div><div><div>17</div></div><div><span>use</span><span><span> </span><span>alloc</span><span>::</span><span>vec</span><span>::</span></span><span>Vec</span><span>;</span></div></div><div><div><div>18</div></div><div><span>use</span><span><span> </span><span>core</span><span>::fmt;</span></span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>/// Identifies one source file inside a [`SourceMap`].</span></div></div><div><div><div>21</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>PartialOrd</span><span>, </span><span>Ord</span><span>, </span><span>Hash</span><span>)]</span></div></div><div><div><div>22</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>SourceId</span><span>(</span><span>pub</span><span> </span><span>u32</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>也就是：删掉第 1 篇那段旧的 <code>//!</code> 文档和 <code>use core::fmt;</code>，换成这一段，并把 <code>SourceId</code> 的文档注释改成现在这个（现在 <code>SourceMap</code> 存在，文档链接才有意义；<code>[</code>SourceMap<code>]</code> 是 rustdoc 的<strong>文档链接</strong>，第 15 篇讲）。其余部分不变。</p><p>为什么是 <code>not(test)</code>？Rust 的测试框架依赖 <code>std</code>（线程、<code>println!</code>、panic 捕获），所以 <code>cargo test</code> 时 crate 仍然用 <code>std</code> 编译，只有正常构建才是 <code>no_std</code>。这也意味着：<strong>一个 <code>no_std</code> crate 在测试里即使偷用了 <code>std</code> 的东西，<code>cargo test</code> 也会通过，但 <code>cargo build</code> 会失败</strong>，所以光看测试通过是不够的，必须同时构建（检查点会跑）。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>整个文件写完了。在 <code>~/pebble</code> 下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-span</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--workspace</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-span</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-span v0.1.0 (/tmp/pebble-rebuild-02/crates/pebble-span)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.33s</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_span-3b8ae416ebb198e2)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>running 6 tests</span></div></div><div><div><div>7</div></div><div><span>test tests::line_starts_include_a_line_after_a_trailing_newline ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::handles_crlf ... ok</span></div></div><div><div><div>9</div></div><div><span>test tests::merge_covers_both ... ok</span></div></div><div><div><div>10</div></div><div><span>test tests::position_counts_characters_not_bytes ... ok</span></div></div><div><div><div>11</div></div><div><span>test tests::merge_across_files_is_a_bug - should panic ... ok</span></div></div><div><div><div>12</div></div><div><span>test tests::source_map_assigns_sequential_ids ... ok</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>Doc-tests pebble_span</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>running 0 tests</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（测试的执行顺序不固定，你的输出行序可能和我的不同。）clippy 输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Checking pebble-span v0.1.0 (/tmp/pebble-rebuild-02/crates/pebble-span)</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo fmt --all --check</code> 无输出。</p><p><strong>必须和标签一致的文件：</strong></p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/src/lib.rs" target="_blank"><code>crates/pebble-span/src/lib.rs</code></a>：现在是最终版，368 行。</li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/Cargo.toml" target="_blank"><code>crates/pebble-span/Cargo.toml</code></a>：第 1 篇已经完成。</li>
</ul><p>（<code>~/pebble-tag</code> 是第 1 篇检查点里克隆的 <code>tutorial-v1</code> 标签；还没有的话先运行 <code>git clone --branch tutorial-v1 https://github.com/bfmhno3/pebble ~/pebble-tag</code>。）</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>&lt;(</span><span>git</span><span> </span><span>-C</span><span> ~/pebble-tag show tutorial-v1:crates/pebble-span/src/lib.rs)</span><span> </span><span>crates/pebble-span/src/lib.rs</span></div></div><div><div><div>2</div></div><div><span>diff</span><span> </span><span>&lt;(</span><span>git</span><span> </span><span>-C</span><span> ~/pebble-tag show tutorial-v1:crates/pebble-span/Cargo.toml)</span><span> </span><span>crates/pebble-span/Cargo.toml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两条 <code>diff</code> 都应该没有输出。文件顺序（对照用）：属性和文档、<code>extern crate</code>、<code>use</code>、<code>SourceId</code> 与它的 <code>Debug</code>、<code>Span</code> 与它的 <code>Debug</code>/<code>Display</code>、<code>Position</code>、<code>SourceFile</code>、<code>impl SourceFile</code>、<code>SourceFile</code> 的 <code>Debug</code>、<code>compute_line_starts</code>、<code>strip_line_ending</code>、<code>Spanned</code> 与 <code>impl Spanned for Span</code>、<code>SourceMap</code>、<code>impl SourceMap</code>、<code>Index</code> 实现、<code>SourceMap</code> 的 <code>Debug</code>、<code>mod tests</code>（顺序是 <code>line_starts_...</code>、<code>handles_crlf</code>、<code>position_counts_...</code>、<code>merge_covers_both</code>、<code>merge_across_files_is_a_bug</code>、<code>source_map_assigns_...</code>）。</p><p><strong>中间版本（后面的章节继续改）：</strong></p><ul>
<li>根 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/Cargo.toml" target="_blank"><code>Cargo.toml</code></a> 和 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/.gitignore" target="_blank"><code>.gitignore</code></a>：和第 1 篇结尾一样，没有变化。</li>
</ul><p>如果你用了我下面练习里的 <code>examples/</code> 临时文件，记得删掉再 <code>diff</code>，不然 crate 目录会多出文件。</p><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇的所有实验文件都在仓库的 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/</code> 目录</a>里。按第 1 篇克隆的 <code>~/pebble-tag</code>，它们在 <code>~/pebble-tag/lab/rust-lab</code>，先 <code>cd ~/pebble-tag/lab/rust-lab</code>（或者把这个目录复制成 <code>~/rust-lab</code>）再运行下面的命令。结果列是我用 <code>rustc 1.95.0</code> 在调试构建下重新跑出来的。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_string.rs" target="_blank"><code>ch02_string.rs</code></a></td><td><code>cargo run --quiet --bin ch02_string</code></td><td>退出码 0，第一行 <code>literal = a.pebble, owned = a.pebble.bak, view = a.pebble</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_move.rs" target="_blank"><code>ch02_move.rs</code></a></td><td><code>cargo build --bin ch02_move</code></td><td>编译失败：E0382</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_clone.rs" target="_blank"><code>ch02_clone.rs</code></a></td><td><code>cargo run --quiet --bin ch02_clone</code></td><td>退出码 0，第一行 <code>8 bytes in a.pebble</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_getter.rs" target="_blank"><code>ch02_getter.rs</code></a></td><td><code>cargo build --bin ch02_getter</code></td><td>编译失败：E0597</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_getter_ok.rs" target="_blank"><code>ch02_getter_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch02_getter_ok</code></td><td>退出码 0，第一行 <code>a.pebble a.pebble</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_vec.rs" target="_blank"><code>ch02_vec.rs</code></a></td><td><code>cargo run --quiet --bin ch02_vec</code></td><td>退出码 101：先打印 <code>slice = [4, 9], len = 2</code>，随后越界 panic 于 <code>src/bin/ch02_vec.rs:13:39</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_option.rs" target="_blank"><code>ch02_option.rs</code></a></td><td><code>cargo run --quiet --bin ch02_option</code></td><td>退出码 0，第一行 <code>Some(3)</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_copied.rs" target="_blank"><code>ch02_copied.rs</code></a></td><td><code>cargo build --bin ch02_copied</code></td><td>编译失败：E0308</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_closure_pattern.rs" target="_blank"><code>ch02_closure_pattern.rs</code></a></td><td><code>cargo build --bin ch02_closure_pattern</code></td><td>编译失败：E0308</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_closure_ok.rs" target="_blank"><code>ch02_closure_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch02_closure_ok</code></td><td>退出码 0，第一行 <code>3</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_dangling.rs" target="_blank"><code>ch02_dangling.rs</code></a></td><td><code>cargo build --bin ch02_dangling</code></td><td>编译失败：E0106</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_dangling_local.rs" target="_blank"><code>ch02_dangling_local.rs</code></a></td><td><code>cargo build --bin ch02_dangling_local</code></td><td>编译失败：E0515</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_dangling_ok.rs" target="_blank"><code>ch02_dangling_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch02_dangling_ok</code></td><td>退出码 0，输出 <code>let</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_utf8.rs" target="_blank"><code>ch02_utf8.rs</code></a></td><td><code>cargo run --quiet --bin ch02_utf8</code></td><td>退出码 101：先打印 <code>len()           = 7</code>，随后切片 panic 于 <code>src/bin/ch02_utf8.rs:11:25</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_into.rs" target="_blank"><code>ch02_into.rs</code></a></td><td><code>cargo run --quiet --bin ch02_into</code></td><td>退出码 0，输出 <code>8 9 8</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_into_bad.rs" target="_blank"><code>ch02_into_bad.rs</code></a></td><td><code>cargo build --bin ch02_into_bad</code></td><td>编译失败：E0277</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_borrow.rs" target="_blank"><code>ch02_borrow.rs</code></a></td><td><code>cargo build --bin ch02_borrow</code></td><td>编译失败：E0502</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_nll.rs" target="_blank"><code>ch02_nll.rs</code></a></td><td><code>cargo run --quiet --bin ch02_nll</code></td><td>退出码 0，第一行 <code>first = a.pebble</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_take.rs" target="_blank"><code>ch02_take.rs</code></a></td><td><code>cargo build --bin ch02_take</code></td><td>编译失败：E0507</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_take_ok.rs" target="_blank"><code>ch02_take_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch02_take_ok</code></td><td>退出码 0，第一行 <code>stolen = "a.pebble"</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_ops_missing.rs" target="_blank"><code>ch02_ops_missing.rs</code></a></td><td><code>cargo build --bin ch02_ops_missing</code></td><td>编译失败：E0369</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_ops.rs" target="_blank"><code>ch02_ops.rs</code></a></td><td><code>cargo run --quiet --bin ch02_ops</code></td><td>退出码 101：先打印 <code>Offset(7) Offset(4) 14</code>，随后越界 panic 于 <code>src/bin/ch02_ops.rs:51:25</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch02_first_word.rs" target="_blank"><code>ch02_first_word.rs</code></a></td><td><code>cargo build --bin ch02_first_word</code></td><td>编译失败：E0106（练习 2）</td></tr></tbody></table><p>这里面的大多数（尤其是编译错误的那些）是上文给出的几行代码外加一个 <code>fn main</code>；<code>ch02_ops.rs</code> 是上面三个 <code>impl</code> 加一个 <code>Offset</code> 和一个 <code>Names(Vec&lt;String&gt;)</code> 的完整小程序。动手写一遍比抄快。</p><p>仓库里还有 <code>ch02_exercise_borrow.rs</code> 和 <code>ch02_exercise_borrow_ok.rs</code>（用 <code>Vec&lt;String&gt;</code> 做的练习 3 的缩小版，前者 E0502，后者输出 <code>b.pebble</code>），文章正文没有引用它们。</p></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1.</strong> 给 <code>SourceFile</code> 加一个方法 <code>lines(&amp;self) -&gt; Vec&lt;&amp;str&gt;</code>，返回所有行（不含行尾）。提示：用 <code>line_count()</code>、<code>line()</code> 和 <code>Vec::new()</code>，循环里 <code>match</code> 一下 <code>line()</code> 的结果。想一想返回的 <code>&amp;str</code> 借自谁。</p>
参考答案<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// Every line of the file, without line terminators.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>lines</span><span>(&amp;</span><span>self</span><span>) -&gt; </span><span>Vec</span><span>&lt;&amp;</span><span>str</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>let</span><span> </span><span>mut</span><span> </span><span>all</span><span><span> </span><span>=</span><span> </span></span><span>Vec</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span>        </span><span>for</span><span> </span><span>number</span><span> </span><span>in</span><span> </span><span>1</span><span>..=</span><span>self</span><span>.</span><span>line_count</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>match</span><span> </span><span>self</span><span>.</span><span>line</span><span>(</span><span>number</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>                </span><span>Some</span><span>(</span><span>text</span><span>) =&gt; </span><span>all</span><span>.</span><span>push</span><span>(</span><span>text</span><span>),</span></div></div><div><div><div>8</div></div><div><span>                </span><span>None</span><span> =&gt; </span><span>break</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>        </span><span>all</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把它放进一份拷贝里，加了一个测试 <code>assert_eq!(file.lines(), ["a", "bb", ""])</code>（文本是 <code>"a\r\nbb\n"</code>），7 个测试全部通过，clippy 也没有意见。返回类型 <code>Vec&lt;&amp;str&gt;</code> 里的 <code>&amp;str</code> 借自 <code>&amp;self</code>：生命周期省略规则把”借自 <code>self</code>”这个事实填进了 <code>Vec</code> 里元素的类型里。所以只要 <code>lines()</code> 的结果还活着，这个 <code>SourceFile</code> 就不能被修改或销毁。没有一次字符串复制，只有一个 <code>Vec</code> 分配。</p><p><strong>2.</strong> 下面这个函数为什么编译不过？有哪两种修法？</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>first_word</span><span>(</span><span>text</span><span>: </span><span>String</span><span>) -&gt; &amp;</span><span>str</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>match</span><span> </span><span>text</span><span>.</span><span>find</span><span>(</span><span>' '</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>Some</span><span>(</span><span>end</span><span>) =&gt; &amp;</span><span>text</span><span>[..</span><span>end</span><span>],</span></div></div><div><div><div>4</div></div><div><span>        </span><span>None</span><span> =&gt; &amp;</span><span>text</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
参考答案<p>编译器报 E0106（<code>ch02_first_word.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0106]: missing lifetime specifier</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch02_first_word.rs:1:32</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>1 | fn first_word(text: String) -&gt; &amp;str {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                                ^ expected named lifetime parameter</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from</span></div></div><div><div><div>8</div></div><div><span>help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>10</div></div><div><span>1 | fn first_word(text: String) -&gt; &amp;'static str {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|                                 +++++++</span></div></div><div><div><div>12</div></div><div><span>help: instead, you are more likely to want to change the argument to be borrowed...</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>1 | fn first_word(text: &amp;String) -&gt; &amp;str {</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>|                     +</span></div></div><div><div><div>16</div></div><div><span>help: ...or alternatively, you might want to return an owned value</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>18</div></div><div><span>1 - fn first_word(text: String) -&gt; &amp;str {</span></div></div><div><div><div>19</div></div><div><span>1 + fn first_word(text: String) -&gt; String {</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>21</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>根本原因：<code>text</code> 是<strong>按值</strong>传进来的，函数结束时它就被释放了。返回一个指向它内部的 <code>&amp;str</code> 就是悬垂引用，所以编译器连”借自谁”都找不到（参数里没有任何引用）。两种修法：把参数改成借用 <code>&amp;str</code>（<code>ch02_dangling_ok.rs</code> 里就是这个版本，输入 <code>"let x = 1;"</code> 输出 <code>let</code>），或者返回拥有所有权的 <code>String</code>（需要 <code>.to_string()</code> 复制出来）。哪一种更好取决于你想不想复制。</p><p><strong>3.</strong> 下面的程序有一个 E0502。不改变语义，调整语句顺序让它编译通过。（把它放进 <code>crates/pebble-span/examples/ex_borrow.rs</code>，用 <code>cargo build -p pebble-span --example ex_borrow</code> 编译，做完删掉这个文件。）</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>pebble_span</span><span>::</span></span><span>SourceMap</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>5</div></div><div><span>    </span><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>map</span><span>.</span><span>add</span><span>(</span><span>"a.pebble"</span><span>, </span><span>"let x = 1;"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>file</span><span><span> </span><span>=</span><span> &amp;</span></span><span>map</span><span>[</span><span>first</span><span>];</span></div></div><div><div><div>7</div></div><div><span>    </span><span>map</span><span>.</span><span>add</span><span>(</span><span>"b.pebble"</span><span>, </span><span>""</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>file</span><span>.</span><span>name</span><span>());</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
参考答案<p>原来的错误：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0502]: cannot borrow `map` as mutable because it is also borrowed as immutable</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; crates/pebble-span/examples/ex_borrow.rs:7:5</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>6 |     let file = &amp;map[first];</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|                 --- immutable borrow occurs here</span></div></div><div><div><div>6</div></div><div><span>7 |     map.add("b.pebble", "");</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|     ^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here</span></div></div><div><div><div>8</div></div><div><span>8 |     println!("{}", file.name());</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|                    ---- immutable borrow later used here</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>file</code> 是借自 <code>map</code> 的 <code>&amp;SourceFile</code>，而 <code>map.add</code> 需要 <code>&amp;mut map</code>，两者在 <code>file</code> 的使用期间重叠。把 <code>add</code> 提前，让借用在所有修改之后再开始：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>map</span><span><span> </span><span>=</span><span> </span></span><span>SourceMap</span><span>::</span><span>new</span><span>();</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>first</span><span><span> </span><span>=</span><span> </span></span><span>map</span><span>.</span><span>add</span><span>(</span><span>"a.pebble"</span><span>, </span><span>"let x = 1;"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>map</span><span>.</span><span>add</span><span>(</span><span>"b.pebble"</span><span>, </span><span>""</span><span>);</span></div></div><div><div><div>4</div></div><div><span>let</span><span> </span><span>file</span><span><span> </span><span>=</span><span> &amp;</span></span><span>map</span><span>[</span><span>first</span><span>];</span></div></div><div><div><div>5</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>file</span><span>.</span><span>name</span><span>());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个版本编译并输出 <code>a.pebble</code>。注意 <code>first</code> 是个 <code>SourceId</code>，<code>Copy</code> 的整数，不是借用，所以它在 <code>add</code> 之后仍然有效。这也是 Pebble 用 <code>SourceId</code> 而不是 <code>&amp;SourceFile</code> 到处传的原因之一：id 是普通的值，不会把整个 <code>SourceMap</code> 锁在借用里。</p></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p><code>pebble-span</code> 完成了。回头看，整个 crate 里谁拥有字符串这个问题，只有一个答案：<code>SourceMap</code>。词法分析器会借用它的文本，诊断渲染器会借用它的行，一旦谁想长期持有，就必须说清楚借自谁、能活多久。我一开始以为这套规则会很烦，后来发现它做的事，我在 C++ 里靠”约定”和 code review 做：每个 <code>string_view</code> 的背后都有一个”这玩意儿指向谁”的心理账本，Rust 只是把这本账交给了编译器。</p><p>下一章我们要开始报错了。一个编译器的错误报告，也是一段要在几个阶段之间传递、最后才被渲染的数据：严重程度、消息、位置、颜色，这些东西天然是一个 <code>enum</code> 加一堆 <code>struct</code>。第 3 篇，<a href="/posts/rust-by-pebble-03-enums-traits-diagnostics/">enum、trait 与第一份错误报告</a>，我们写 <code>pebble-diag</code>，用 <code>Span</code> 和 <code>SourceMap</code> 画出一个带 <code>^^^</code> 的错误框。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/rust-by-pebble-01-first-crate/</id>
      <title type="text">第一个 crate：从 cargo new 到 Span</title>
      <published>2026-10-13T00:00:00.000Z</published>
      <updated>2026-10-13T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/rust-by-pebble-01-first-crate/"/>
      <summary type="text">用 Pebble 从零学 Rust 的第 1 篇：先看完成品在两个后端上跑 fib，再从 cargo new 开始搭 workspace，亲手写出第一个 crate pebble-span，顺手学会 let、整数类型、表达式、控制流、struct、impl、derive 和单元测试。</summary>
      <content type="html"><![CDATA[<p>我手上有一个用 Rust 写的小语言，叫 Pebble：16 个 crate，一个词法分析器，一个解析器，一个树遍历解释器，一个字节码编译器加栈式虚拟机，外加一个手写的垃圾回收器。下面是它跑 <code>examples/fib.pebble</code> 的样子，同一个程序，两个后端：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run -q -p pebble-cli -- run examples/fib.pebble</span></div></div><div><div><div>2</div></div><div><span>6765</span></div></div><div><div><div>3</div></div><div><span>$ cargo run -q -p pebble-cli -- run --vm examples/fib.pebble</span></div></div><div><div><div>4</div></div><div><span>6765</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>这个系列要做的事很简单：我们从一个空目录开始，一章一章把这个项目重新写一遍。写完第 15 章，你手里的代码和我的仓库一致，而你需要的所有 Rust 知识，都会在它第一次被用到的地方讲清楚。不需要先去读《The Rust Programming Language》。</p>
<div><div><div></div><div>本篇检查点</div></div><div><p>这一篇结束时你会有：一个叫 <code>pebble</code> 的 Cargo workspace，里面有第一个 crate <code>pebble-span</code>（只含 <code>SourceId</code>、<code>Span</code> 和它们的两个测试），<code>cargo test -p pebble-span</code> 通过；还有一个叫 <code>rust-lab</code> 的小实验场，后面每一篇的编译错误演示都在里面做。
涉及的 Rust 特性：<code>cargo</code>、workspace、<code>let</code>/<code>mut</code>/遮蔽、整数类型与 <code>as</code>、溢出、表达式与分号、控制流、<code>struct</code>/<code>impl</code>、<code>const fn</code>、<code>#[derive]</code>、<code>#[must_use]</code>、<code>#[test]</code>。
这是第 1 篇，没有前置章节。</p></div></div>
<section><h2>Pebble 长什么样<a href="#pebble-长什么样"><span>#</span></a></h2><p>先把整个系统的数据流摆出来，后面每一章都是其中一个方框：</p><div><div><div><div><span></span></div><div><span><p>tokens</p></span></div><div><span><p>AST</p></span></div><div><span><p>AST</p></span></div><div><span><p>字节码</p></span></div><div><span></span></div><div><span><p>诊断</p></span></div><div><span><p>诊断</p></span></div><div><span></span></div><div><span></span></div><div><span><p>源码 (String)</p></span></div><div><span><p>词法分析器<br /><br />pebble-lexer</p></span></div><div><span><p>解析器<br /><br />pebble-parser</p></span></div><div><span><p>树遍历解释器<br /><br />pebble-interp</p></span></div><div><span><p>字节码编译器<br /><br />pebble-bytecode</p></span></div><div><span><p>栈式虚拟机<br /><br />pebble-vm</p></span></div><div><span><p>垃圾回收器<br /><br />pebble-gc</p></span></div><div><span><p>命令行<br /><br />pebble-cli</p></span></div>
</div><div><div><span></span></div><div><span><p>tokens</p></span></div><div><span><p>AST</p></span></div><div><span><p>AST</p></span></div><div><span><p>字节码</p></span></div><div><span></span></div><div><span><p>诊断</p></span></div><div><span><p>诊断</p></span></div><div><span></span></div><div><span></span></div><div><span><p>源码 (String)</p></span></div><div><span><p>词法分析器<br /><br />pebble-lexer</p></span></div><div><span><p>解析器<br /><br />pebble-parser</p></span></div><div><span><p>树遍历解释器<br /><br />pebble-interp</p></span></div><div><span><p>字节码编译器<br /><br />pebble-bytecode</p></span></div><div><span><p>栈式虚拟机<br /><br />pebble-vm</p></span></div><div><span><p>垃圾回收器<br /><br />pebble-gc</p></span></div><div><span><p>命令行<br /><br />pebble-cli</p></span></div>
</div></div></div><p>实线是数据，虚线是错误报告。所有阶段都要说”第几行第几列出错了”，所以最底下还有两个被所有人依赖的小 crate：<code>pebble-span</code>（位置）和 <code>pebble-diag</code>（错误报告）。我们今天从 <code>pebble-span</code> 起步，因为它是整张依赖图的叶子。</p></section>
<section><h2>这个系列怎么读<a href="#这个系列怎么读"><span>#</span></a></h2><p>我定了几条规矩，因为我自己踩过”看懂了但写不出来”的坑。</p><p><strong>边写边建。</strong> 每一章你会写某几个 crate 的核心部分，从一个最小版本开始，逐步长成仓库里的样子。重复、冗长、和本章主题无关的部分（比如 30 个只是名字不同的 builtin 函数），我会指名道姓让你从仓库里复制，并告诉你为什么现在可以不读。</p><p><strong>实验场。</strong> 每次我说”这段代码不会编译”，你都应该自己试一遍。所以现在就建一个 <code>~/rust-lab</code>（下面马上建），所有实验都是里面的一个文件，文件名我会写出来：<code>src/bin/ch01_bindings.rs</code>，<code>ch</code> 后面是章节号。（整个系列的全部实验已经放在仓库的 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a> 里，可以直接复制过来，不必一个文件一个文件地敲；我还是建议你至少手敲前几个，错误信息是自己撞出来才记得住的。）</p><p><strong>真实输出。</strong> 本系列里所有的编译错误、命令输出、测试统计，都是我在这台机器上用 <code>rustc 1.95.0</code> 实际跑出来的。输出偶尔会被截断（用 <code>...</code> 标出），但每一行都不会被我改写。路径里的 <code>/tmp/rust-lab</code> 是我机器上的位置，你的会是 <code>~/rust-lab</code>。如果你的编译器版本不同，措辞可能略有出入。</p><p><strong>固定的代码快照。</strong> 我的参考实现是仓库的 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1" target="_blank"><code>tutorial-v1</code></a> 标签。每章末尾有一个检查点，列出”到这里为止哪些文件必须和标签完全一致”（可以用 <code>diff</code> 验证），以及哪些文件是还会继续变的中间版本。</p><p>下面是整个系列的地图：</p>

<table><thead><tr><th>章</th><th>标题</th><th>主要的 Rust 内容</th></tr></thead><tbody><tr><td>1</td><td>第一个 crate：从 cargo new 到 Span</td><td>cargo、workspace、<code>let</code>、整数、表达式、控制流、<code>struct</code>/<code>impl</code>、<code>derive</code>、测试</td></tr><tr><td>2</td><td><a href="/posts/rust-by-pebble-02-ownership/">所有权：SourceMap 里的 String 归谁</a></td><td><code>String</code>/<code>&amp;str</code>、移动、借用、<code>Vec</code>、<code>Option</code>、闭包初见、UTF-8、<code>Index</code>、<code>no_std</code></td></tr><tr><td>3</td><td><a href="/posts/rust-by-pebble-03-enums-traits-diagnostics/">enum、trait 与第一份错误报告</a></td><td><code>enum</code>、<code>match</code>、<code>Display</code>/<code>Error</code>、trait 默认方法、孤儿规则</td></tr><tr><td>4</td><td><a href="/posts/rust-by-pebble-04-lifetimes-lexer/">生命周期：让 token 借用源码</a></td><td>生命周期、<code>Cow</code>、<code>Iterator</code>、模式匹配</td></tr><tr><td>5</td><td><a href="/posts/rust-by-pebble-05-ast-generics/">递归类型、泛型与 trait：搭一棵 AST</a></td><td><code>Box</code>、泛型、trait 约束、<code>?Sized</code>、<code>IntoIterator</code></td></tr><tr><td>6</td><td><a href="/posts/rust-by-pebble-06-proc-macros/">过程宏：让编译器替我写 impl Spanned</a></td><td>derive 宏、<code>syn</code>、<code>quote</code></td></tr><tr><td>7</td><td><a href="/posts/rust-by-pebble-07-parser-errors-cli/">Result、? 与递归下降：解析器和第一个命令行</a></td><td><code>Result</code>、<code>?</code>、panic、<code>clap</code>、<code>anyhow</code>、集成测试</td></tr><tr><td>8</td><td><a href="/posts/rust-by-pebble-08-rc-refcell-values/">共享与可变：Rc、RefCell 和运行时的值</a></td><td><code>Rc</code>、<code>RefCell</code>、<code>HashMap</code>、<code>Hash</code>/<code>Eq</code>、trait 对象、函数指针</td></tr><tr><td>9</td><td><a href="/posts/rust-by-pebble-09-interpreter-closures/">树遍历解释器：闭包、控制流与一个内存泄漏</a></td><td><code>Fn</code>/<code>FnMut</code>/<code>FnOnce</code>、<code>move</code>、<code>Weak</code>、<code>FromStr</code></td></tr><tr><td>10</td><td><a href="/posts/rust-by-pebble-10-bytecode-macros/">字节码编译器：macro_rules!、回填和常量池</a></td><td><code>macro_rules!</code>、函数式过程宏、<code>HashMap</code> entry</td></tr><tr><td>11</td><td><a href="/posts/rust-by-pebble-11-gc-unsafe/">手写垃圾回收器：泛型容器、Cell 与第一次 unsafe</a></td><td>泛型结构体、<code>Cell</code>、<code>unsafe</code>、裸指针、<code>MaybeUninit</code></td></tr><tr><td>12</td><td><a href="/posts/rust-by-pebble-12-vm-backends/">栈式虚拟机：GC 根、两个后端与 Criterion</a></td><td>关联常量、GAT、静态/动态分派、属性宏、Criterion</td></tr><tr><td>13</td><td><a href="/posts/rust-by-pebble-13-ffi-nostd/">越过边界：FFI 调 C 与 no_std</a></td><td>FFI、<code>build.rs</code>、<code>repr(C)</code>、<code>UnsafeCell</code>、原子、<code>GlobalAlloc</code></td></tr><tr><td>14</td><td><a href="/posts/rust-by-pebble-14-concurrency-async/">并发与 async：线程、Send/Sync 和一个 Tokio 服务</a></td><td>线程、<code>Arc</code>/<code>Mutex</code>、<code>Future</code>、<code>Pin</code>、Tokio</td></tr><tr><td>15</td><td><a href="/posts/rust-by-pebble-15-engineering/">收尾：Cargo features、测试、lint 与 CI</a></td><td>features、文档、clippy、CI、全特性索引</td></tr></tbody></table><p>没有哪一章是”选读”。顺序就是依赖顺序：每一章只会用到前面讲过的东西，偶尔需要提前露个脸的概念，我会写明”先当成 X，第 N 篇细讲”。</p></section>
<section><h2>装环境<a href="#装环境"><span>#</span></a></h2><p>我用的是 <code>rustc 1.95.0</code>。仓库里有一个 <code>flake.nix</code>，<code>nix develop</code> 进去就是这个版本；没有 Nix 的话，用 rustup 装 stable，仓库的 <code>rust-toolchain.toml</code> 要求的就是 <code>stable</code> 加 <code>rustfmt</code>、<code>clippy</code> 两个组件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>curl</span><span> </span><span>--proto</span><span> </span><span>'=https'</span><span> </span><span>--tlsv1.2</span><span> </span><span>-sSf</span><span> </span><span>https://sh.rustup.rs</span><span> | </span><span>sh</span></div></div><div><div><div>2</div></div><div><span>rustup</span><span> </span><span>component</span><span> </span><span>add</span><span> </span><span>rustfmt</span><span> </span><span>clippy</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>确认一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ rustc --version</span></div></div><div><div><div>2</div></div><div><span>rustc 1.95.0 (59807616e 2026-04-14) (built from a source tarball)</span></div></div><div><div><div>3</div></div><div><span>$ cargo --version</span></div></div><div><div><div>4</div></div><div><span>cargo 1.95.0 (f2d3ce0bd 2026-03-21)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>rustc</code> 是编译器，对应 <code>gcc</code>/<code>clang</code>。<code>cargo</code> 是构建系统加包管理器，对应 CMake 加 vcpkg/Conan 的合体。绝大多数时候你只和 <code>cargo</code> 说话，它再去调用 <code>rustc</code>。</p></section>
<section><h2>实验场：你的第一个 cargo 项目<a href="#实验场你的第一个-cargo-项目"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>new</span><span> </span><span>rust-lab</span><span> </span><span>--edition</span><span> </span><span>2024</span></div></div><div><div><div>3</div></div><div><span>cd</span><span> </span><span>rust-lab</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo new</code> 创建了这些东西：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>rust-lab/</span></div></div><div><div><div>2</div></div><div><span>├── Cargo.toml</span></div></div><div><div><div>3</div></div><div><span>├── .gitignore</span></div></div><div><div><div>4</div></div><div><span>└── src/</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>└── main.rs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Cargo.toml</code> 是项目清单（对应 <code>CMakeLists.txt</code>，但是是声明式的，没有脚本）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"rust-lab"</span></div></div><div><div><div>3</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>4</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>edition</code> 是 Rust 的”方言版本”，大约每三年一个（2015、2018、2021、2024）。不同 edition 的 crate 可以互相链接，所以语言可以做不兼容的改进而不分裂生态。本系列全部用 2024。</p><p><code>src/main.rs</code> 是 <code>cargo new</code> 替你写好的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>println!</span><span>(</span><span>"Hello, world!"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin rust-lab</span></div></div><div><div><div>2</div></div><div><span>Hello, world!</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有三件事值得停一下。</p><p>第一，<code>println!</code> 后面的 <code>!</code> 不是装饰：它表示这是一个<strong>宏</strong>，不是函数。Rust 的宏在编译期展开，所以 <code>println!("{}", x)</code> 的格式字符串是在编译期检查的：占位符数量不对，直接编译失败，而不是像 C 的 <code>printf</code> 那样运行时读栈上的垃圾。第 6 篇和第 10 篇我们会自己写宏。</p><p>第二，没有头文件。Rust 的编译单元是整个 crate（后面会讲），编译器能看到 crate 里所有的项，谁用谁不需要先声明。可见性靠 <code>pub</code> 关键字显式标注，默认私有。</p><p>第三，多出来的 <code>target/</code> 目录：编译产物都在这里，<code>target/debug/</code> 是调试构建，<code>cargo run --release</code> 会用 <code>target/release/</code>。它是 <code>.gitignore</code> 里的第一行，对应 CMake 的 <code>build/</code>。</p><p>接下来我们会在这个项目里往 <code>src/bin/</code> 下面放实验文件（完整的实验场在 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/rust-lab</code></a>，<code>cp -r ~/pebble-tag/lab/rust-lab ~/rust-lab</code> 就能得到所有章节的文件），每个文件自动成为一个独立的可执行程序，名字是文件名（不带 <code>.rs</code>）。运行某一个：<code>cargo run --quiet --bin ch01_bindings</code>。<code>--quiet</code> 去掉 cargo 自己的 “Compiling…” 噪音。注意，一旦 <code>src/bin/</code> 里有多个程序，单独一个 <code>cargo run</code> 会因为不知道跑哪个而报错，所以以后总是带 <code>--bin</code>（不带的话，cargo 会报错 “could not determine which binary to run”，并让你用 <code>--bin</code> 指定，我试过）。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>src/bin</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>搭一个 workspace<a href="#搭一个-workspace"><span>#</span></a></h2><p>Pebble 有 16 个 crate。这里先把三个词分清楚，因为 C/C++ 里没有一一对应的概念：</p><ul>
<li><strong>crate</strong>：Rust 的编译单元，一个库或一个可执行文件，从一个根文件（<code>lib.rs</code> 或 <code>main.rs</code>）开始，整个模块树编译成一个产物。粗略对应”一个静态库/可执行文件”，但编译器是把整个 crate 当成一个翻译单元来处理的。</li>
<li><strong>package</strong>：一个带 <code>Cargo.toml</code> 的目录，可以包含一个库 crate 和若干个二进制 crate。cargo 管理的是 package。</li>
<li><strong>workspace</strong>：一组共享同一个 <code>Cargo.lock</code> 和同一个 <code>target/</code> 的 package。对应 CMake 的顶层工程加 <code>add_subdirectory</code>。</li>
</ul><p>还有第四个词 <strong>module</strong>（模块），是 crate 内部的命名空间，对应 C++ 的 <code>namespace</code>，但和文件系统绑定（第 7 篇会写 <code>mod cli;</code>）。</p><p>现在开始。建一个新目录，不是 <code>rust-lab</code>，而是我们真正的项目：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>~</span></div></div><div><div><div>2</div></div><div><span>mkdir</span><span> </span><span>pebble</span><span> &amp;&amp; </span><span>cd</span><span> </span><span>pebble</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>init</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>写根目录的 <code>Cargo.toml</code>。注意它没有 <code>[package]</code>，只有 <code>[workspace]</code>，这叫虚拟 workspace：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>workspace</span><span>]</span></div></div><div><div><div>2</div></div><div><span>resolver</span><span> = </span><span>"3"</span></div></div><div><div><div>3</div></div><div><span>members</span><span> = [</span><span>"crates/*"</span><span>]</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>[</span><span>workspace</span><span>.</span><span>package</span><span>]</span></div></div><div><div><div>6</div></div><div><span>version</span><span> = </span><span>"0.1.0"</span></div></div><div><div><div>7</div></div><div><span>edition</span><span> = </span><span>"2024"</span></div></div><div><div><div>8</div></div><div><span>rust-version</span><span> = </span><span>"1.88"</span></div></div><div><div><div>9</div></div><div><span>authors</span><span> = [</span><span>"bfmhno3"</span><span>]</span></div></div><div><div><div>10</div></div><div><span>license</span><span> = </span><span>"MIT OR Apache-2.0"</span></div></div><div><div><div>11</div></div><div><span>repository</span><span> = </span><span>"https://github.com/bfmhno3/pebble"</span></div></div><div><div><div>12</div></div><div><span>description</span><span> = </span><span>"Pebble: a small scripting language and toolchain, built from scratch in Rust."</span></div></div><div><div><div>13</div></div><div><span>keywords</span><span> = [</span><span>"language"</span><span>, </span><span>"interpreter"</span><span>, </span><span>"compiler"</span><span>, </span><span>"vm"</span><span>, </span><span>"learning"</span><span>]</span></div></div><div><div><div>14</div></div><div><span>categories</span><span> = [</span><span>"development-tools"</span><span>, </span><span>"compilers"</span><span>]</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>[</span><span>workspace</span><span>.</span><span>dependencies</span><span>]</span></div></div><div><div><div>17</div></div><div><span>pebble-span</span><span> = { </span><span>path</span><span> = </span><span>"crates/pebble-span"</span><span>, </span><span>version</span><span> = </span><span>"0.1.0"</span><span> }</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>[</span><span>workspace</span><span>.</span><span>lints</span><span>.</span><span>rust</span><span>]</span></div></div><div><div><div>20</div></div><div><span>rust_2018_idioms</span><span> = </span><span>"warn"</span></div></div><div><div><div>21</div></div><div><span>unsafe_op_in_unsafe_fn</span><span> = </span><span>"warn"</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>[</span><span>workspace</span><span>.</span><span>lints</span><span>.</span><span>clippy</span><span>]</span></div></div><div><div><div>24</div></div><div><span>all</span><span> = </span><span>"warn"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐段看：</p><ul>
<li><code>members = ["crates/*"]</code>：<code>crates/</code> 下每个子目录都是成员，以后加 crate 不用改这一行。</li>
<li><code>resolver = "3"</code>：依赖解析算法的版本。虚拟 workspace 没有根 package，cargo 没法从 edition 推断，所以得显式写。我试过不写：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>warning: virtual workspace defaulting to `resolver = "1"` despite one or more workspace members being on edition 2024 which implies `resolver = "3"`</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>= note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>= note: to use the edition 2024 resolver, specify `workspace.resolver = "3"` in the workspace root's manifest</span></div></div><div><div><div>5</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>[workspace.package]</code>：所有成员共享的元数据，一处定义，各 crate 用 <code>version.workspace = true</code> 引用，类似 CMake 里的顶层变量。<code>rust-version = "1.88"</code> 是我承诺支持的最低编译器版本（MSRV）。</li>
<li><code>[workspace.dependencies]</code>：统一管理依赖的版本，成员写 <code>pebble-span.workspace = true</code> 就行。现在只有一行，随着章节推进会长到 16 个内部 crate 加十几个外部依赖。<code>path</code> 指向本地目录，<code>version</code> 在将来发布到 crates.io 时才有用。</li>
<li><code>[workspace.lints]</code>：lint 是编译器和 clippy（官方的 linter）的警告开关。<code>rust_2018_idioms</code> 要求你写 2018 以来的惯用法；<code>unsafe_op_in_unsafe_fn</code> 第 11 篇讲；<code>clippy::all</code> 打开 clippy 的默认检查组。所有成员通过 <code>[lints] workspace = true</code> 继承它们。</li>
</ul><p>再建 <code>.gitignore</code>，一行就够：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/target</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现在让 cargo 创建第一个 crate。<code>--lib</code> 表示这是库，不是可执行文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo new --lib crates/pebble-span</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Creating library `pebble-span` package</span></div></div><div><div><div>3</div></div><div><span>note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为目录在 workspace 里，cargo 已经替我们写好了继承行和 <code>[lints]</code>，<code>crates/pebble-span/Cargo.toml</code> 是这样的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-span"</span></div></div><div><div><div>3</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>4</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>description.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div><span>keywords.workspace</span><span> = </span><span>true</span></div></div><div><div><div>11</div></div><div><span>categories.workspace</span><span> = </span><span>true</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>[</span><span>dependencies</span><span>]</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>16</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我们只改两处：<code>description</code> 换成这个 crate 自己的一句话，<code>keywords</code> 和 <code>categories</code> 两行删掉（它们继承的是整个项目的描述，不适合单个 crate），空的 <code>[dependencies]</code> 也不需要。整个文件变成（文件 <code>crates/pebble-span/Cargo.toml</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>package</span><span>]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"pebble-span"</span></div></div><div><div><div>3</div></div><div><span>description</span><span> = </span><span>"Source offsets, spans, and source maps for the Pebble toolchain."</span></div></div><div><div><div>4</div></div><div><span>version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>edition.workspace</span><span> = </span><span>true</span></div></div><div><div><div>6</div></div><div><span>rust-version.workspace</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>authors.workspace</span><span> = </span><span>true</span></div></div><div><div><div>8</div></div><div><span>license.workspace</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>repository.workspace</span><span> = </span><span>true</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>[</span><span>lints</span><span>]</span></div></div><div><div><div>12</div></div><div><span>workspace</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>version.workspace = true</code> 这种点号写法是 TOML 的普通语法，等价于 <code>[package.version]</code> 表里写 <code>workspace = true</code>。这个文件和仓库标签里的<strong>完全一致</strong>，以后不会再改。</p><p><code>cargo new</code> 还生成了一个 <code>lib.rs</code>，里面有一个 <code>add</code> 函数和一个叫 <code>it_works</code> 的测试。这是它的模板，我们要整个换掉。先把这个文件清空，下面开始写真正的内容。</p></section>
<section><h2>写 <code>SourceId</code>：一个零开销的 newtype<a href="#写-sourceid一个零开销的-newtype"><span>#</span></a></h2><p>编译器每个阶段都要说”出错在哪”。最朴素的做法是传两个整数：文件号和字节偏移。C 程序员这时候会写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> span { </span><span>unsigned</span><span> source; </span><span>unsigned</span><span> start; </span><span>unsigned</span><span> end; };</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Rust 里可以写得几乎一样。但我想在这上面再加一点编译期的保护：文件号不是随便一个整数，不应该能和”偏移量”混着用。做法是把它包进一个只有一个字段的元组结构体，叫 <strong>newtype</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// Identifies one source file.</span></div></div><div><div><div>2</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>PartialOrd</span><span>, </span><span>Ord</span><span>, </span><span>Hash</span><span>)]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>SourceId</span><span>(</span><span>pub</span><span> </span><span>u32</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先把每个词过一遍：</p><ul>
<li><code>pub struct SourceId(pub u32);</code>：<code>struct</code> 定义结构体；这种圆括号形式叫<strong>元组结构体</strong>，字段没有名字，按位置访问：<code>id.0</code>。里面的 <code>pub</code> 说明这个字段对外可见（默认私有）；<code>u32</code> 是 32 位无符号整数。</li>
<li><code>///</code> 是文档注释，会进 <code>cargo doc</code> 生成的文档。仓库的规则是每个公开项都要有。</li>
<li><code>#[derive(...)]</code> 让编译器按字段自动生成 trait 的实现。trait 是第 2 篇才正式讲的东西，现在把它理解成”接口”，对应 C++ 的 concept 或抽象基类就够了。这里一口气派生了七个：
<ul>
<li><code>Copy</code> 和 <code>Clone</code>：复制。<code>Copy</code> 的意思是”按位复制就是完整的复制”，和 C 里 struct 赋值一样（<code>memcpy</code>）。后面会看到，有些类型（比如拥有堆内存的 <code>String</code>）不能 <code>Copy</code>，第 2 篇解释为什么。</li>
<li><code>PartialEq</code> 和 <code>Eq</code>：<code>==</code> 和 <code>!=</code>。</li>
<li><code>PartialOrd</code> 和 <code>Ord</code>：<code>&lt;</code>、<code>&gt;</code> 和排序。</li>
<li><code>Hash</code>：可以做哈希表的键。</li>
</ul>
</li>
</ul><p>这些在 C++ 里你需要手写 <code>operator==</code>（或者 C++20 的 <code>= default</code>），而 Rust 里一行 derive 搞定。</p><p>newtype 的价值我们实际看一下。下面是实验 <code>src/bin/ch01_newtype.rs</code>，里面有一个故意写错的调用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>ok</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>2</span><span>, </span><span>9</span><span>);</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>bad</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span>::</span><span>new</span><span>(</span><span>0</span><span>, </span><span>2</span><span>, </span><span>9</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_newtype</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch01_newtype.rs:21:25</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>21 |     let bad = Span::new(0, 2, 9);</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|               --------- ^ expected `SourceId`, found integer</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|               |</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|               arguments to this function are incorrect</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div><div><div><div>12</div></div><div><span>help: try wrapping the expression in `SourceId`</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>14</div></div><div><span>21 |     let bad = Span::new(SourceId(0), 2, 9);</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>|                         +++++++++ +</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>For more information about this error, try `rustc --explain E0308`.</span></div></div><div><div><div>18</div></div><div><span>error: could not compile `rust-lab` (bin "ch01_newtype") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>（完整文件在 <a href="#%E5%AE%9E%E9%AA%8C%E6%B8%85%E5%8D%95">lab 的实验清单</a> 里说明；<code>Span</code> 的完整定义在下一节，先照抄下一节的 <code>struct Span</code> 与 <code>Span::new</code> 即可，外加上面的 <code>SourceId</code>。）这条错误里最重要的是编码 <code>E0308</code>：以后你会无数次见到它。<code>rustc --explain E0308</code> 会打印一段详细解释。</p><p>那它有运行时代价吗？没有。实验 <code>ch01_sizes.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run -q --bin ch01_sizes</span></div></div><div><div><div>2</div></div><div><span>size_of::&lt;SourceId&gt;() = 4</span></div></div><div><div><div>3</div></div><div><span>size_of::&lt;Span&gt;()     = 12</span></div></div><div><div><div>4</div></div><div><span>size_of::&lt;usize&gt;()    = 8</span></div></div><div><div><div>5</div></div><div><span>0 2 9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>SourceId</code> 就是 4 个字节，和 <code>unsigned</code> 一模一样；<code>Span</code> 是 12 个字节，和上面那个 C 的 <code>struct span</code> 一样。类型安全完全是编译期的东西，编译完就蒸发了。顺便注意 <code>usize</code>：它是”指针宽度的无符号整数”，对应 C 的 <code>size_t</code>，在这台机器上是 8 个字节，用来做索引和长度。</p><section><h3>为什么偏移量是 <code>u32</code> 而不是 <code>usize</code><a href="#为什么偏移量是-u32-而不是-usize"><span>#</span></a></h3><p>我可以用 <code>usize</code> 表示偏移，那样索引切片的时候不用转换。我选 <code>u32</code> 是因为一个 <code>Span</code> 里有两个偏移，每个都从 8 字节降到 4 字节，<code>Span</code> 从 24 字节降到 12 字节。AST 的每个节点都带一个 <code>Span</code>，这个省出来的空间是实打实的。代价是源文件不能超过 4 GiB，对一个脚本语言我认为可以接受。Rust 不让你悄悄越界，所以代价会以”需要显式转换”的形式出现，下一节就会碰到。</p></section><section><h3>为 <code>SourceId</code> 手写 <code>Debug</code><a href="#为-sourceid-手写-debug"><span>#</span></a></h3><p>还差一个 trait。派生的 <code>Debug</code> 会把 <code>SourceId(0)</code> 打印成 <code>SourceId(0)</code>，我想让它更短，所以手写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>SourceId</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"source#{}"</span><span>, </span><span>self</span><span>.</span><span>0</span><span>)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一块你只需要照抄，我现在只解释大意：<code>impl Trait for Type { ... }</code> 是”为这个类型实现这个 trait”，<code>fmt</code> 是 <code>Debug</code> 要求你提供的唯一一个方法，<code>write!</code> 往格式化器里写字符。<code>&amp;self</code> 是”我只借用，不拿走”，<code>'_</code> 是一个我们第 4 篇才会讲的东西（生命周期），现在当成噪音；<code>fmt::Result</code> 是”写成功了还是失败了”的返回类型，第 3 篇、第 7 篇会彻底讲清楚。别紧张，今天的目标是你能抄对并看懂形状。</p><p>这个文件顶上需要 <code>use core::fmt;</code> 把名字带进来。文件开头写成这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//! Source offsets and spans.</span></div></div><div><div><div>2</div></div><div><span>//!</span></div></div><div><div><div>3</div></div><div><span>//! Everything in the front end of the compiler talks about positions in terms</span></div></div><div><div><div>4</div></div><div><span>//! of *byte offsets into a source file*. Those offsets are grouped into</span></div></div><div><div><div>5</div></div><div><span>//! [`Span`]s.</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>use</span><span><span> </span><span>core</span><span>::fmt;</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>//!</code> 是给”外层”（这里是整个 crate）写的文档注释。<code>use</code> 把路径引入作用域，类似 C++ 的 <code>using</code>，<code>core::fmt</code> 里的 <code>core</code> 是 Rust 的最小标准库，第 2 篇会讲它和 <code>std</code> 的关系；现在你知道 <code>core::fmt</code> 和 <code>std::fmt</code> 是同一个东西就够了。</p></section></section>
<section><h2>写 <code>Span</code>：值、方法和表达式<a href="#写-span值方法和表达式"><span>#</span></a></h2><p>把 <code>SourceId</code> 的代码放在 <code>use</code> 的后面，紧接着写 <code>Span</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// A half-open byte range `[start, end)` inside a single source file.</span></div></div><div><div><div>2</div></div><div><span>///</span></div></div><div><div><div>3</div></div><div><span>/// `Span` is `Copy`: it is three small integers, so passing it around is free</span></div></div><div><div><div>4</div></div><div><span>/// and never moves any source text.</span></div></div><div><div><div>5</div></div><div><span>#[derive(</span><span>Copy</span><span>, </span><span>Clone</span><span>, </span><span>PartialEq</span><span>, </span><span>Eq</span><span>, </span><span>Hash</span><span>)]</span></div></div><div><div><div>6</div></div><div><span>pub</span><span> </span><span>struct</span><span> </span><span>Span</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>pub</span><span> </span><span>source</span><span>: </span><span>SourceId</span><span>,</span></div></div><div><div><div>8</div></div><div><span>    </span><span>pub</span><span> </span><span>start</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>pub</span><span> </span><span>end</span><span>: </span><span>u32</span><span>,</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是普通的带名字段结构体。半开区间 <code>[start, end)</code> 是我和你都应该熟悉的惯例（C++ 迭代器区间、Python 切片都是这样）：长度是 <code>end - start</code>，空区间是 <code>start == end</code>，相邻两个区间首尾相接不重叠。</p><p><code>Span</code> 没有派生 <code>PartialOrd</code>：两个 span 比大小没有意义，我就不给它这个能力。另外它也没有派生 <code>Debug</code>，因为我想要紧凑的 <code>2..9</code>，一会儿手写。</p><section><h3><code>impl</code> 块：方法和关联函数<a href="#impl-块方法和关联函数"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span> </span><span>Span</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/// A span that points at nothing; useful as a placeholder.</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>DUMMY</span><span>: </span><span>Span</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>source</span><span>: </span><span>SourceId</span><span>(</span><span>0</span><span>),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>start</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>6</div></div><div><span>        </span><span>end</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>    </span><span>/// Builds a span from a source id and a byte range.</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>11</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>new</span><span>(</span><span>source</span><span>: </span><span>SourceId</span><span>, </span><span>start</span><span>: </span><span>u32</span><span>, </span><span>end</span><span>: </span><span>u32</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>Self</span><span> { </span><span>source</span><span>, </span><span>start</span><span>, </span><span>end</span><span> }</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>impl Span { ... }</code> 是”固有实现”，给类型加方法，相当于 C++ 在类体里写成员函数，只是数据和方法分开写。这里有几个新东西：</p><ul>
<li><strong>关联常量</strong> <code>pub const DUMMY: Span = ...</code>：属于类型的常量，用 <code>Span::DUMMY</code> 访问，对应 C++ 的 <code>static constexpr</code>。</li>
<li><strong>关联函数</strong> <code>new</code>：第一个参数不是 <code>self</code>，所以它不是方法，调用写成 <code>Span::new(...)</code>，对应 C++ 的静态成员函数。Rust 没有构造函数，<code>new</code> 只是社区约定的名字。</li>
<li><code>Self</code> 是”当前 impl 的类型”的别名，这里就是 <code>Span</code>。</li>
<li><code>Self { source, start, end }</code> 是结构体字面量；当变量名和字段名相同时可以简写（和 JavaScript 的对象简写一样）。</li>
<li>函数体最后一行没有分号，它就是返回值。下一节细讲。</li>
<li><code>const fn</code> 表示这个函数可以在编译期求值。实验 <code>ch01_const_fn.rs</code>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>fn</span><span> </span><span>double</span><span>(</span><span>x</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> { </span><span>x</span><span> * </span><span>2</span><span> }</span></div></div><div><div><div>2</div></div><div><span>fn</span><span> </span><span>triple</span><span>(</span><span>x</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> { </span><span>x</span><span> * </span><span>3</span><span> }</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>const</span><span> </span><span>A</span><span>: </span><span>u32</span><span><span> </span><span>=</span><span> </span></span><span>double</span><span>(</span><span>21</span><span>);</span></div></div><div><div><div>5</div></div><div><span>const</span><span> </span><span>B</span><span>: </span><span>u32</span><span><span> </span><span>=</span><span> </span></span><span>triple</span><span>(</span><span>14</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_const_fn</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0015]: cannot call non-const function `triple` in constants</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch01_const_fn.rs:10:16</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>10 | const B: u32 = triple(14);</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|                ^^^^^^^^^^</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span>note: function `triple` is not const</span></div></div><div><div><div>10</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对应 C++ 的 <code>constexpr</code>：只有标成 <code>const fn</code> 的函数才能在常量里被调用。<code>Span::DUMMY</code> 和任何 <code>const</code> 里用到 <code>Span::new</code> 的地方，都依赖 <code>new</code> 是 <code>const fn</code>。</p><ul>
<li><code>#[must_use]</code>：如果调用者忽略返回值，编译器发警告。实验 <code>ch01_must_use.rs</code>，一个 <code>#[must_use]</code> 的 <code>len</code> 被我丢掉了结果：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_must_use</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>warning: unused return value of `Span::len` that must be used</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch01_must_use.rs:15:5</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>15 |     s.len();</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|     ^^^^^^^</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default</span></div></div><div><div><div>10</div></div><div><span>help: use `let _ = ...` to ignore the resulting value</span></div></div><div><div><div>11</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++ 里对应的是 <code>[[nodiscard]]</code>。对 <code>merge</code>、<code>len</code> 这种纯函数，忽略结果几乎一定是 bug，所以我给它们都标上了。</p><p>接着写 <code>empty</code>、<code>len</code>、<code>is_empty</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// An empty span located at `at`.</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>empty</span><span>(</span><span>source</span><span>: </span><span>SourceId</span><span>, </span><span>at</span><span>: </span><span>u32</span><span>) -&gt; </span><span>Self</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>Self</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>source</span><span>,</span></div></div><div><div><div>6</div></div><div><span>            </span><span>start</span><span>: </span><span>at</span><span>,</span></div></div><div><div><div>7</div></div><div><span>            </span><span>end</span><span>: </span><span>at</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>/// Number of bytes covered by the span.</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>13</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>len</span><span>(</span><span>self</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>self</span><span>.end.</span><span>saturating_sub</span><span>(</span><span>self</span><span>.start)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>/// Whether the span covers zero bytes.</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>19</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>is_empty</span><span>(</span><span>self</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>20</div></div><div><span>        </span><span>self</span><span><span>.start </span><span>&gt;=</span><span> </span></span><span>self</span><span>.end</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>len(self)</code> 的第一个参数是 <code>self</code>，所以它是方法，用 <code>span.len()</code> 调用。注意是 <code>self</code> 而不是 <code>&amp;self</code>：按值接收。在 C++ 里你习惯写 <code>const Span&amp;</code> 来避免拷贝，但 <code>Span</code> 只有 12 个字节并且是 <code>Copy</code> 的，按值传和传引用一样便宜，调用者的 <code>span</code> 在调用后依然可用。这一点我们在练习里验证。第 2 篇讲借用时你会看到 <code>&amp;self</code>。</p><p><code>saturating_sub</code> 是”饱和减法”：结果小于 0 时停在 0，而不是回绕。这里我用它是防御性的：理论上 <code>end &gt;= start</code> 总成立，但万一有个 bug 造出了反向的 span，<code>len()</code> 返回 0 比返回 4294967295 好调试得多。</p></section><section><h3>整数：没有隐式转换，溢出不是未定义行为<a href="#整数没有隐式转换溢出不是未定义行为"><span>#</span></a></h3><p><code>len</code> 返回 <code>u32</code>，我想用它去索引一个切片，就需要 <code>usize</code>。你觉得应该怎么办？C 里你什么都不用做，编译器静默转换。Rust 不会。实验 <code>ch01_casts.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span>: </span><span>u32</span><span><span> </span><span>=</span><span> </span></span><span>7</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>let</span><span> </span><span>b</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>3</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>sum</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span> + </span><span>b</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>println!</span><span>(</span><span>"{sum}"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_casts</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch01_casts.rs:4:19</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>4 |     let sum = a + b;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|                   ^ expected `u32`, found `usize`</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>error[E0277]: cannot add `usize` to `u32`</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch01_casts.rs:4:17</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span>4 |     let sum = a + b;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>|                 ^ no implementation for `u32 + usize`</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>= help: the trait `Add&lt;usize&gt;` is not implemented for `u32`</span></div></div><div><div><div>16</div></div><div><span>help: the following other types implement trait `Add&lt;Rhs&gt;`</span></div></div><div><div><div>17</div></div><div><span>...</span></div></div><div><div><div>18</div></div><div><span>error: could not compile `rust-lab` (bin "ch01_casts") due to 2 previous errors</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里同时出现了两条错误，第二条透露了一个事实：<code>a + b</code> 其实是调用 <code>u32</code> 上的 <code>Add</code> trait，而 <code>u32</code> 只实现了 <code>Add&lt;u32&gt;</code>。运算符本质是 trait，第 2 篇我们会为自己的类型重载运算符。</p><p>要转换就明说，用 <code>as</code>（实验 <code>ch01_casts_ok.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>a</span><span> </span><span>as</span><span> </span><span>usize</span><span> + </span><span>b</span><span>);</span></div></div><div><div><div>2</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>a</span><span> + </span><span>b</span><span> </span><span>as</span><span> </span><span>u32</span><span>);</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>300_</span><span>i32</span><span> </span><span>as</span><span> </span><span>u8</span><span>);</span></div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, -</span><span>1_</span><span>i32</span><span> </span><span>as</span><span> </span><span>u32</span><span>);</span></div></div><div><div><div>5</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>3.9_</span><span>f64</span><span> </span><span>as</span><span> </span><span>i32</span><span>);</span></div></div><div><div><div>6</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, -</span><span>1.0_</span><span>f64</span><span> </span><span>as</span><span> </span><span>u8</span><span>);</span></div></div><div><div><div>7</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>u32</span><span>::</span><span>try_from</span><span>(</span><span>300_</span><span>i64</span><span>).</span><span>unwrap</span><span>());</span></div></div><div><div><div>8</div></div><div><span>println!</span><span>(</span><span>"{:?}"</span><span>, </span><span>u8</span><span>::</span><span>try_from</span><span>(</span><span>300_</span><span>i32</span><span>));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch01_casts_ok</span></div></div><div><div><div>2</div></div><div><span>10</span></div></div><div><div><div>3</div></div><div><span>10</span></div></div><div><div><div>4</div></div><div><span>44</span></div></div><div><div><div>5</div></div><div><span>4294967295</span></div></div><div><div><div>6</div></div><div><span>3</span></div></div><div><div><div>7</div></div><div><span>0</span></div></div><div><div><div>8</div></div><div><span>300</span></div></div><div><div><div>9</div></div><div><span>Err(TryFromIntError(()))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>读一下：前两行是我们要的；<code>300 as u8</code> 静默截断成 44（和 C 的 <code>(uint8_t)300</code> 一样）；<code>-1 as u32</code> 是 4294967295（和 C 一样）；<code>3.9 as i32</code> 截断成 3；<code>-1.0 as u8</code> 饱和成 0（C 里浮点数转整数越界是未定义行为，Rust 把它定义成了饱和）。<code>as</code> 永远不会失败，也永远不会警告，这既方便又危险：Pebble 的 <code>Span</code> 里 <code>u32</code> 和 <code>usize</code> 之间的转换点我都是刻意写的，第 2 篇你会在 <code>SourceFile</code> 里看到一堆 <code>as u32</code>，它们每一处都意味着”我断言这个文件小于 4 GiB”。需要检查的转换用 <code>TryFrom</code>，最后两行显示 300 能放进 <code>u32</code> 但放不进 <code>u8</code>，失败时返回 <code>Err</code>，我们在第 8 篇会自己实现一个。（<code>.unwrap()</code> 先读作”取出结果，失败就 panic”，第 2 篇细讲。）</p><p>溢出呢？实验 <code>ch01_overflow.rs</code>（里面的 <code>black_box</code> 只是让优化器看不出值，不然编译器会在编译期直接拒绝这个溢出）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>big</span><span>: </span><span>u8</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>hint</span><span>::</span></span><span>black_box</span><span>(</span><span>255</span><span>);</span></div></div><div><div><div>2</div></div><div><span>println!</span><span>(</span><span>"wrapping_add   = {}"</span><span>, </span><span>big</span><span>.</span><span>wrapping_add</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"saturating_add = {}"</span><span>, </span><span>big</span><span>.</span><span>saturating_add</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>4</div></div><div><span>println!</span><span>(</span><span>"checked_add    = {:?}"</span><span>, </span><span>big</span><span>.</span><span>checked_add</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>5</div></div><div><span>println!</span><span>(</span><span>"overflowing    = {:?}"</span><span>, </span><span>big</span><span>.</span><span>overflowing_add</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>6</div></div><div><span>let</span><span> </span><span>plain</span><span><span> </span><span>=</span><span> </span></span><span>big</span><span> + </span><span>1</span><span>;</span></div></div><div><div><div>7</div></div><div><span>println!</span><span>(</span><span>"big + 1        = {plain}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调试构建：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch01_overflow</span></div></div><div><div><div>2</div></div><div><span>wrapping_add   = 0</span></div></div><div><div><div>3</div></div><div><span>saturating_add = 255</span></div></div><div><div><div>4</div></div><div><span>checked_add    = None</span></div></div><div><div><div>5</div></div><div><span>overflowing    = (0, true)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>thread 'main' (51943) panicked at src/bin/ch01_overflow.rs:11:17:</span></div></div><div><div><div>8</div></div><div><span>attempt to add with overflow</span></div></div><div><div><div>9</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>发布构建：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --release --bin ch01_overflow</span></div></div><div><div><div>2</div></div><div><span>wrapping_add   = 0</span></div></div><div><div><div>3</div></div><div><span>saturating_add = 255</span></div></div><div><div><div>4</div></div><div><span>checked_add    = None</span></div></div><div><div><div>5</div></div><div><span>overflowing    = (0, true)</span></div></div><div><div><div>6</div></div><div><span>big + 1        = 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一行 <code>big + 1</code>，调试构建里 panic（程序带着错误信息终止），发布构建里回绕成 0。<strong>这两种行为都是有定义的</strong>，没有 C 里有符号溢出那种”编译器可以假设它不发生并删掉你的检查”的未定义行为。想要确定的语义，就明说：<code>wrapping_*</code>（回绕）、<code>saturating_*</code>（饱和）、<code>checked_*</code>（返回 <code>Option</code>，<code>None</code> 表示溢出）、<code>overflowing_*</code>（返回结果和是否溢出）。<code>Option</code> 是第 2 篇的内容，现在把 <code>None</code> 读成”没有结果”。</p></section><section><h3>表达式与分号<a href="#表达式与分号"><span>#</span></a></h3><p>回到 <code>len</code>。它的函数体是 <code>self.end.saturating_sub(self.start)</code>，没有 <code>return</code>，没有分号。Rust 是一门<strong>表达式语言</strong>：几乎一切都有值，一个块 <code>{ ... }</code> 的值是它最后一个没有分号的表达式。分号的意思是”丢掉这个表达式的值”，整个语句的值是 <code>()</code>（读作 unit，一个只有一个值的类型，近似 C 的 <code>void</code>，但它是真正的值）。</p><p>这条规则的后果可以用实验 <code>ch01_tail_expr.rs</code> 看：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>add_one</span><span>(</span><span>x</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>x</span><span> + </span><span>1</span><span>;</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_tail_expr</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0308]: mismatched types</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch01_tail_expr.rs:1:23</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>1 | fn add_one(x: u32) -&gt; u32 {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|    -------            ^^^ expected `u32`, found `()`</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>|    |</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|    implicitly returns `()` as its body has no tail or `return` expression</span></div></div><div><div><div>10</div></div><div><span>2 |     x + 1;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|          - help: remove this semicolon to return this value</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0308`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `rust-lab` (bin "ch01_tail_expr") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个多余的分号，改变了整个函数的类型。编译器的建议很准：去掉它。这是我从 C 转过来的前一个月，犯得最多的错误，之后它会变成你最先学会不再犯的错误，因为报错信息实在太好了。再看一个能编译的版本（<code>ch01_tail_expr_ok.rs</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>block_value</span><span><span> </span><span>=</span><span> {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>2</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>a</span><span> * </span><span>10</span><span> </span><span>// no semicolon: the block evaluates to this</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div><div><div><div>5</div></div><div><span>let</span><span> </span><span>unit</span><span><span> </span><span>=</span><span> {</span></span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>2</span><span>;</span></div></div><div><div><div>7</div></div><div><span>    </span><span>a</span><span> * </span><span>10</span><span>;</span></div></div><div><div><div>8</div></div><div><span>};</span></div></div><div><div><div>9</div></div><div><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>add_one</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>10</div></div><div><span>println!</span><span>(</span><span>"{block_value}"</span><span>);</span></div></div><div><div><div>11</div></div><div><span>println!</span><span>(</span><span>"{unit:?}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch01_tail_expr_ok</span></div></div><div><div><div>2</div></div><div><span>warning: unused arithmetic operation that must be used</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch01_tail_expr_ok.rs:12:9</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>5</div></div><div><span>12 |         a * 10;</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>|         ^^^^^^ the arithmetic operation produces a value</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default</span></div></div><div><div><div>9</div></div><div><span>help: use `let _ = ...` to ignore the resulting value</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>12 |         let _ = a * 10;</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>|         +++++++</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>2</span></div></div><div><div><div>15</div></div><div><span>20</span></div></div><div><div><div>16</div></div><div><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>块可以直接当值用，所以 Rust 里没有 <code>?:</code> 三元运算符：<code>if</code> 本身就是表达式，下面马上看到。</p></section><section><h3><code>let</code>、<code>mut</code> 和遮蔽<a href="#letmut-和遮蔽"><span>#</span></a></h3><p>我一直在用 <code>let</code> 而没有解释它。实验 <code>ch01_bindings.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>x</span><span><span> </span><span>=</span><span> </span></span><span>5</span><span>;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>x</span><span><span> </span><span>=</span><span> </span></span><span>x</span><span> * </span><span>2</span><span>; </span><span>// shadowing: a brand new variable that reuses the name</span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>x</span><span><span> </span><span>=</span><span> </span></span><span>"inner"</span><span>; </span><span>// shadowing may even change the type</span></div></div><div><div><div>5</div></div><div><span>    </span><span>println!</span><span>(</span><span>"inner x = {x}"</span><span>);</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div><span>println!</span><span>(</span><span>"outer x = {x}"</span><span>);</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>count</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>10</div></div><div><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>11</div></div><div><span>count</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>12</div></div><div><span>println!</span><span>(</span><span>"count = {count}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo run --quiet --bin ch01_bindings</span></div></div><div><div><div>2</div></div><div><span>inner x = inner</span></div></div><div><div><div>3</div></div><div><span>outer x = 10</span></div></div><div><div><div>4</div></div><div><span>count = 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三件事。一，类型是推断的：<code>let x = 5</code> 没写类型，编译器推断成 <code>i32</code>（默认整数类型）；也可以写 <code>let x: u32 = 5</code>。二，<strong>遮蔽</strong>（shadowing）：同一个作用域里可以再 <code>let x</code>，创建一个全新的变量，旧的就不可见了，它甚至可以换类型。三，<strong>绑定默认不可变</strong>，和 C++ 的默认相反：要改就得写 <code>let mut</code>。违反时（<code>ch01_immutable.rs</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_immutable</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0384]: cannot assign twice to immutable variable `y`</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch01_immutable.rs:3:5</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>2 |     let y = 1;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|         - first assignment to `y`</span></div></div><div><div><div>8</div></div><div><span>3 |     y = 2;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|     ^^^^^ cannot assign twice to immutable variable</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>11</div></div><div><span>help: consider making this binding mutable</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>13</div></div><div><span>2 |     let mut y = 1;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>|         +++</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>warning: value assigned to `y` is never read</span></div></div><div><div><div>17</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>可变性在类型系统里是显式的，这件事到第 2 篇会变成整个所有权体系的一半。</p></section><section><h3>控制流<a href="#控制流"><span>#</span></a></h3><p>下一个方法 <code>merge</code> 要用到 <code>min</code> 和 <code>max</code>，也用到一条 <code>debug_assert_eq!</code>，但控制流的几种形式我想先在实验里过一遍。<code>ch01_control_flow.rs</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>n</span><span><span> </span><span>=</span><span> </span></span><span>7</span><span>;</span></div></div><div><div><div>2</div></div><div><span>let</span><span> </span><span>kind</span><span><span> </span><span>=</span><span> </span></span><span>if</span><span> </span><span>n</span><span> % </span><span>2</span><span><span> </span><span>==</span><span> </span></span><span>0</span><span> { </span><span>"even"</span><span> } </span><span>else</span><span> { </span><span>"odd"</span><span> };</span></div></div><div><div><div>3</div></div><div><span>println!</span><span>(</span><span>"{n} is {kind}"</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>tries</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>6</div></div><div><span>let</span><span> </span><span>first_square_over_50</span><span><span> </span><span>=</span><span> </span></span><span>loop</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>tries</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>if</span><span> </span><span>tries</span><span> * </span><span>tries</span><span><span> </span><span>&gt;</span><span> </span></span><span>50</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>break</span><span> </span><span>tries</span><span> * </span><span>tries</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>};</span></div></div><div><div><div>12</div></div><div><span>println!</span><span>(</span><span>"first square over 50: {first_square_over_50}"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>7 is odd</span></div></div><div><div><div>2</div></div><div><span>first square over 50: 64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>if</code> 是表达式，两个分支的类型必须一致，条件不用括号，大括号不能省。<code>loop</code> 是无限循环，<code>break</code> 可以带一个值，整个 <code>loop</code> 就等于这个值（所以”重试直到成功并返回结果”可以写得很直白）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>let</span><span> </span><span>mut</span><span> </span><span>i</span><span><span> </span><span>=</span><span> </span></span><span>0</span><span>;</span></div></div><div><div><div>2</div></div><div><span>while</span><span> </span><span>i</span><span><span> </span><span>&lt;</span><span> </span></span><span>3</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{i} "</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>i</span><span><span> </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>println!</span><span>();</span></div></div><div><div><div>7</div></div><div><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{j} "</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div><span>println!</span><span>();</span></div></div><div><div><div>11</div></div><div><span>for</span><span> </span><span>j</span><span> </span><span>in</span><span> </span><span>0</span><span>..=</span><span>3</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print!</span><span>(</span><span>"{j} "</span><span>);</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div><span>println!</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>0 1 2</span></div></div><div><div><div>2</div></div><div><span>0 1 2</span></div></div><div><div><div>3</div></div><div><span>0 1 2 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>while</code> 和 C 的一样。<code>for</code> 没有 C 的三段式，它遍历一个”可迭代的东西”，<code>0..3</code> 是半开区间，<code>0..=3</code> 是闭区间。迭代器是第 3 篇和第 4 篇的主题。最后是标签 break：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>'</span><span>outer</span><span>: </span><span>for</span><span> </span><span>row</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>5</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>for</span><span> </span><span>col</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>5</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>if</span><span> </span><span>row</span><span> * </span><span>col</span><span><span> </span><span>==</span><span> </span></span><span>6</span><span> {</span></div></div><div><div><div>4</div></div><div><span>            </span><span>println!</span><span>(</span><span>"found {row} * {col} == 6"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>            </span><span>break</span><span> '</span><span>outer</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>found 2 * 3 == 6</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>'outer:</code> 是给循环起的标签（单引号开头），<code>break 'outer</code> 一次跳出两层，C 里你要用 <code>goto</code>。（那个单引号和第 4 篇的”生命周期”是同一个词法形态，不是巧合，但是两件事。）</p></section><section><h3><code>merge</code> 和 <code>contains</code><a href="#merge-和-contains"><span>#</span></a></h3><p>现在可以把 <code>Span</code> 的 impl 写完了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span>/// The smallest span covering both `self` and `other`.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>///</span></div></div><div><div><div>3</div></div><div><span>    </span><span>/// Both spans must belong to the same source file; the compiler guarantees</span></div></div><div><div><div>4</div></div><div><span>    </span><span>/// this, and a debug build will panic if it is ever violated.</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>pub</span><span> </span><span>fn</span><span> </span><span>merge</span><span>(</span><span>self</span><span>, </span><span>other</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Span</span><span> {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>debug_assert_eq!</span><span>(</span></div></div><div><div><div>8</div></div><div><span>            </span><span>self</span><span>.source, </span><span>other</span><span>.source,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>"tried to merge spans from different files"</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>);</span></div></div><div><div><div>11</div></div><div><span>        </span><span>Span</span><span> {</span></div></div><div><div><div>12</div></div><div><span>            </span><span>source</span><span>: </span><span>self</span><span>.source,</span></div></div><div><div><div>13</div></div><div><span>            </span><span>start</span><span>: </span><span>self</span><span>.start.</span><span>min</span><span>(</span><span>other</span><span>.start),</span></div></div><div><div><div>14</div></div><div><span>            </span><span>end</span><span>: </span><span>self</span><span>.end.</span><span>max</span><span>(</span><span>other</span><span>.end),</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>    </span><span>/// Whether `offset` falls inside the half-open range.</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>#[must_use]</span></div></div><div><div><div>20</div></div><div><span>    </span><span>pub</span><span> </span><span>const</span><span> </span><span>fn</span><span> </span><span>contains</span><span>(</span><span>self</span><span>, </span><span>offset</span><span>: </span><span>u32</span><span>) -&gt; </span><span>bool</span><span> {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>self</span><span><span>.start </span><span>&lt;=</span><span> </span></span><span>offset</span><span><span> </span><span>&amp;&amp;</span><span> </span></span><span>offset</span><span><span> </span><span>&lt;</span><span> </span></span><span>self</span><span>.end</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>merge</code> 不是 <code>const fn</code>：我试过把它改成 <code>pub const fn merge</code>，编译器回了 <code>error[E0015]: cannot call non-const operator in constant functions</code>，指向的正是 <code>debug_assert_eq!</code> 里对 <code>SourceId</code> 的 <code>==</code>（派生出来的 <code>PartialEq</code> 不是 <code>const</code> 的）。即使去掉断言也不行：<code>u32::min</code>/<code>max</code> 目前还不是 const 的（编译器还会报 E0658）。<code>contains</code> 只比较基本整数，所以可以是 <code>const fn</code>。</p><p><code>debug_assert_eq!(a, b, "message")</code> 断言 <code>a == b</code>，失败则 panic。它和 C 的 <code>assert</code> + <code>NDEBUG</code> 是一个思路：只在调试构建里检查，发布构建里整行消失。我们的 <code>merge</code> 的契约是”两个 span 来自同一个文件”，违反它是编译器自己的 bug，不是用户的错，所以用 debug 断言，不污染发布版本。<code>self.start.min(other.start)</code> 里的 <code>.min</code> 是整数的方法，不是宏，不需要 <code>&lt;algorithm&gt;</code>。</p><p>最后手写 <code>Span</code> 的 <code>Debug</code> 和 <code>Display</code>，二者输出相同的 <code>2..9</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Debug</span><span> </span><span>for</span><span> </span><span>Span</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{}..{}"</span><span>, </span><span>self</span><span>.start, </span><span>self</span><span>.end)</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>impl</span><span><span> </span><span>fmt</span><span>::</span></span><span>Display</span><span> </span><span>for</span><span> </span><span>Span</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>fmt</span><span>(&amp;</span><span>self</span><span>, </span><span>f</span><span>: &amp;</span><span>mut</span><span><span> </span><span>fmt</span><span>::</span></span><span>Formatter</span><span>&lt;'</span><span>_</span><span><span>&gt;) -&gt; </span><span>fmt</span><span>::</span></span><span>Result</span><span> {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>write!</span><span>(</span><span>f</span><span>, </span><span>"{}..{}"</span><span>, </span><span>self</span><span>.start, </span><span>self</span><span>.end)</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Debug</code> 是给程序员看的（<code>{:?}</code>），<code>Display</code> 是给用户看的（<code>{}</code>），第 3 篇会讲怎么区分，以及为什么要有两个。为什么不用 <code>#[derive(Debug)]</code>？我们等一下用测试失败时的输出来对比。</p><p>到这里 <code>crates/pebble-span/src/lib.rs</code> 的非测试部分写完了，按顺序是：文件头的 <code>//!</code> 和 <code>use</code>，<code>SourceId</code>，它的 <code>Debug</code>，<code>Span</code>，<code>impl Span</code>，<code>Span</code> 的 <code>Debug</code> 和 <code>Display</code>。</p></section></section>
<section><h2>写测试<a href="#写测试"><span>#</span></a></h2><p>Rust 的测试是语言内置的，没有 gtest，没有 CMake 的 <code>add_test</code>。在文件底部加：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#[cfg(test)]</span></div></div><div><div><div>2</div></div><div><span>mod</span><span> tests {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>use</span><span> </span><span>super</span><span>::*;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>6</div></div><div><span>    </span><span>fn</span><span> </span><span>merge_covers_both</span><span>() {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>2</span><span>, </span><span>5</span><span>);</span></div></div><div><div><div>8</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>7</span><span>, </span><span>9</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>assert_eq!</span><span>(</span><span>a</span><span>.</span><span>merge</span><span>(</span><span>b</span><span>), </span><span>Span</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>2</span><span>, </span><span>9</span><span>));</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#[test]</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>#[should_panic(expected </span><span>=</span><span> </span><span>"different files"</span><span>)]</span></div></div><div><div><div>14</div></div><div><span>    </span><span>fn</span><span> </span><span>merge_across_files_is_a_bug</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>let</span><span> </span><span>a</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>0</span><span>), </span><span>0</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>16</div></div><div><span>        </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>Span</span><span>::</span><span>new</span><span>(</span><span>SourceId</span><span>(</span><span>1</span><span>), </span><span>0</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>17</div></div><div><span>        </span><span>let</span><span> </span><span>_</span><span><span> </span><span>=</span><span> </span></span><span>a</span><span>.</span><span>merge</span><span>(</span><span>b</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>#[cfg(test)]</code>：条件编译，只有在 <code>cargo test</code> 下这个模块才存在，对应 <code>#ifdef TESTING</code>。发布版本里这些代码完全不存在。</li>
<li><code>mod tests { ... }</code>：一个内联的模块（命名空间）。<code>use super::*;</code> 把父模块的所有名字带进来，所以测试可以直接写 <code>Span</code>。单元测试住在被测代码的同一个文件里，因此它甚至可以访问私有项，第 2 篇你会用到。</li>
<li><code>#[test]</code> 标记一个测试函数。它不接收参数，正常返回就是通过，panic 就是失败。</li>
<li><code>assert_eq!(left, right)</code>：失败时打印两边的值，这要求类型实现了 <code>Debug</code>，这就是为什么前面我们给 <code>SourceId</code> 和 <code>Span</code> 都实现了它。</li>
<li><code>#[should_panic(expected = "different files")]</code>：这个测试<strong>期望</strong>发生 panic，并且 panic 消息必须包含这个子串。它验证的是 <code>merge</code> 里 <code>debug_assert_eq!</code> 的那条消息。</li>
</ul><p>注意这个 <code>should_panic</code> 测试依赖 <code>debug_assert</code>。我试了 <code>cargo test --release -p pebble-span</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>test tests::merge_covers_both ... ok</span></div></div><div><div><div>2</div></div><div><span>test tests::merge_across_files_is_a_bug - should panic ... FAILED</span></div></div><div><div><div>3</div></div><div><span>note: test did not panic as expected at crates/pebble-span/src/lib.rs:115:8</span></div></div><div><div><div>4</div></div><div><span>test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>发布构建里断言整行消失，函数不再 panic，测试就失败了。这不是 bug，是 <code>debug_assert</code> 的定义；你只需要记住：测试默认在调试构建下跑。</p><p>跑起来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo test -p pebble-span</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling pebble-span v0.1.0 (/tmp/pebble-rebuild-01/crates/pebble-span)</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Finished `test` profile [unoptimized + debuginfo] target(s) in 0.45s</span></div></div><div><div><div>4</div></div><div><span><span>     </span></span><span>Running unittests src/lib.rs (target/debug/deps/pebble_span-3b8ae416ebb198e2)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>running 2 tests</span></div></div><div><div><div>7</div></div><div><span>test tests::merge_covers_both ... ok</span></div></div><div><div><div>8</div></div><div><span>test tests::merge_across_files_is_a_bug - should panic ... ok</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>Doc-tests pebble_span</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>running 0 tests</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>-p pebble-span</code> 指定 package，在 workspace 里只测这一个。最后的 “Doc-tests” 是文档里的代码示例也会被当成测试编译运行，第 10 篇我们会写一个。</p><section><h3>看一眼失败长什么样<a href="#看一眼失败长什么样"><span>#</span></a></h3><p>通过的测试没有故事。我在一个临时副本里故意改坏：把期望值的 <code>9</code> 改成 <code>8</code>，并且去掉第二个测试上的 <code>#[should_panic]</code>。结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---- scratch::cross_file_without_should_panic stdout ----</span></div></div><div><div><div>2</div></div><div><span>thread 'scratch::cross_file_without_should_panic' (57352) panicked at crates/pebble-span/src/lib.rs:72:9:</span></div></div><div><div><div>3</div></div><div><span>assertion `left == right` failed: tried to merge spans from different files</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>left: source#0</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>right: source#1</span></div></div><div><div><div>6</div></div><div><span>note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace</span></div></div><div><div><div>7</div></div><div><span>---- scratch::wrong_on_purpose stdout ----</span></div></div><div><div><div>8</div></div><div><span>thread 'scratch::wrong_on_purpose' (57353) panicked at crates/pebble-span/src/lib.rs:130:9:</span></div></div><div><div><div>9</div></div><div><span>assertion `left == right` failed</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>left: 2..9</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>right: 2..8</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一个让你看到了我们手写 <code>Debug</code> 的另一层用处：<code>left: source#0</code> 和 <code>right: source#1</code>。第二个，<code>left: 2..9</code>，一眼看出差在哪。如果把 <code>Span</code> 改成 <code>#[derive(Debug)]</code>，同一个断言失败时是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>assertion `left == right` failed</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>left: Span { source: source#0, start: 2, end: 9 }</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>right: Span { source: source#0, start: 2, end: 8 }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也不是不能读，但是在一个会出现几千次 <code>Span</code> 的编译器测试里，你会很快理解我为什么偏爱短的那种。</p></section></section>
<section><h2>检查点<a href="#检查点"><span>#</span></a></h2><p>在 <code>~/pebble</code> 下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cargo</span><span> </span><span>test</span><span> </span><span>-p</span><span> </span><span>pebble-span</span></div></div><div><div><div>2</div></div><div><span>cargo</span><span> </span><span>clippy</span><span> </span><span>--workspace</span><span> </span><span>--all-targets</span><span> </span><span>--</span><span> </span><span>-D</span><span> </span><span>warnings</span></div></div><div><div><div>3</div></div><div><span>cargo</span><span> </span><span>fmt</span><span> </span><span>--all</span><span> </span><span>--check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>测试输出就是上面那个：2 个通过，0 个失败。clippy 加 <code>-D warnings</code>（把所有警告当作错误）应当安静地结束：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>    </span></span><span>Checking pebble-span v0.1.0 (/tmp/pebble-rebuild-01/crates/pebble-span)</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cargo fmt --all --check</code> 什么都不输出（退出码 0）就是格式正确。如果它打印了 diff，直接运行 <code>cargo fmt --all</code> 让 rustfmt 替你改。</p><p>先拿到标签的本地克隆，以后每一篇的 <code>diff</code> 都用它（只需要做一次）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>--branch</span><span> </span><span>tutorial-v1</span><span> </span><span>https://github.com/bfmhno3/pebble</span><span> </span><span>~/pebble-tag</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>必须和标签一致的文件：</strong></p><ul>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/Cargo.toml" target="_blank"><code>crates/pebble-span/Cargo.toml</code></a>：已经完成，以后不会再变。验证：</li>
</ul><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>diff</span><span> </span><span>&lt;(</span><span>git</span><span> </span><span>-C</span><span> ~/pebble-tag show tutorial-v1:crates/pebble-span/Cargo.toml)</span><span> </span><span>crates/pebble-span/Cargo.toml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>中间版本（后面的章节会继续改）：</strong></p><ul>
<li>根 <a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/Cargo.toml" target="_blank"><code>Cargo.toml</code></a>：我们的 <code>[workspace.dependencies]</code> 目前只有 <code>pebble-span</code> 一行，之后每章添一个 crate。<code>[workspace.package]</code>、<code>[workspace.lints]</code> 和仓库一致，<code>[profile.*]</code> 段第 15 篇再加。</li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/.gitignore" target="_blank"><code>.gitignore</code></a>：目前只有 <code>/target</code>，仓库里的是更完整的版本。</li>
<li><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/crates/pebble-span/src/lib.rs" target="_blank"><code>crates/pebble-span/src/lib.rs</code></a>：目前只有 <code>SourceId</code>、<code>Span</code> 和它们的两个测试，120 行（<code>wc -l</code> 应当是 120）；没有 <code>#![no_std]</code>、没有 <code>SourceFile</code>、没有 <code>SourceMap</code>。第 2 篇会把它写成标签里的最终版本，届时 <code>diff</code> 要求一致。</li>
</ul><section><h3>实验清单<a href="#实验清单"><span>#</span></a></h3><p>这一篇的所有实验文件都在仓库的 <a href="https://github.com/bfmhno3/pebble/tree/tutorial-v1/lab/rust-lab" target="_blank"><code>lab/</code> 目录</a>里。按第 1 篇克隆的 <code>~/pebble-tag</code>，它们在 <code>~/pebble-tag/lab/rust-lab</code>，先 <code>cd ~/pebble-tag/lab/rust-lab</code>（或者把这个目录复制成 <code>~/rust-lab</code>）再运行下面的命令。结果列是我用 <code>rustc 1.95.0</code> 在调试构建下重新跑出来的。</p>

<table><thead><tr><th>文件</th><th>命令</th><th>结果</th></tr></thead><tbody><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/main.rs" target="_blank"><code>main.rs</code></a></td><td><code>cargo run --quiet --bin rust-lab</code></td><td>退出码 0，输出 <code>Hello, world!</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_bindings.rs" target="_blank"><code>ch01_bindings.rs</code></a></td><td><code>cargo run --quiet --bin ch01_bindings</code></td><td>退出码 0，第一行 <code>inner x = inner</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_immutable.rs" target="_blank"><code>ch01_immutable.rs</code></a></td><td><code>cargo build --bin ch01_immutable</code></td><td>编译失败：E0384</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_casts.rs" target="_blank"><code>ch01_casts.rs</code></a></td><td><code>cargo build --bin ch01_casts</code></td><td>编译失败：E0308、E0277</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_casts_ok.rs" target="_blank"><code>ch01_casts_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch01_casts_ok</code></td><td>退出码 0，输出 <code>10</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_overflow.rs" target="_blank"><code>ch01_overflow.rs</code></a></td><td><code>cargo run --quiet --bin ch01_overflow</code></td><td>调试构建：退出码 101，已打印 <code>wrapping_add   = 0</code>，随后 panic 于 <code>src/bin/ch01_overflow.rs:11:17</code>（加 <code>--release</code> 则回绕，输出见上文）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_tail_expr.rs" target="_blank"><code>ch01_tail_expr.rs</code></a></td><td><code>cargo build --bin ch01_tail_expr</code></td><td>编译失败：E0308</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_tail_expr_ok.rs" target="_blank"><code>ch01_tail_expr_ok.rs</code></a></td><td><code>cargo run --quiet --bin ch01_tail_expr_ok</code></td><td>退出码 0，输出 <code>2</code>；编译时有警告 <code>unused arithmetic operation that must be used</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_control_flow.rs" target="_blank"><code>ch01_control_flow.rs</code></a></td><td><code>cargo run --quiet --bin ch01_control_flow</code></td><td>退出码 0，第一行 <code>7 is odd</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_newtype.rs" target="_blank"><code>ch01_newtype.rs</code></a></td><td><code>cargo build --bin ch01_newtype</code></td><td>编译失败：E0308</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_sizes.rs" target="_blank"><code>ch01_sizes.rs</code></a></td><td><code>cargo run --quiet --bin ch01_sizes</code></td><td>退出码 0，第一行 <code>size_of::&lt;SourceId&gt;() = 4</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_const_fn.rs" target="_blank"><code>ch01_const_fn.rs</code></a></td><td><code>cargo build --bin ch01_const_fn</code></td><td>编译失败：E0015</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_must_use.rs" target="_blank"><code>ch01_must_use.rs</code></a></td><td><code>cargo run --quiet --bin ch01_must_use</code></td><td>退出码 0，stdout 无输出；编译时有警告 <code>unused return value of `Span::len` that must be used</code></td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_copy.rs" target="_blank"><code>ch01_copy.rs</code></a></td><td><code>cargo build --bin ch01_copy</code></td><td>编译失败：E0382（练习 2）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_copy_string.rs" target="_blank"><code>ch01_copy_string.rs</code></a></td><td><code>cargo build --bin ch01_copy_string</code></td><td>编译失败：E0204（练习 2）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_intersect.rs" target="_blank"><code>ch01_intersect.rs</code></a></td><td><code>cargo run --quiet --bin ch01_intersect</code></td><td>退出码 0，第一行 <code>Some(Span { source: SourceId(0), start: 5, end: 8 })</code>（练习 1）</td></tr><tr><td><a href="https://github.com/bfmhno3/pebble/blob/tutorial-v1/lab/rust-lab/src/bin/ch01_loop_value.rs" target="_blank"><code>ch01_loop_value.rs</code></a></td><td><code>cargo run --quiet --bin ch01_loop_value</code></td><td>退出码 0，输出 <code>128</code>（练习 3）</td></tr></tbody></table></section></section>
<section><h2>练习<a href="#练习"><span>#</span></a></h2><p><strong>1.</strong> 给 <code>Span</code> 加一个方法 <code>intersect(self, other: Span) -&gt; Option&lt;Span&gt;</code>：两个 span 的交集，没有交集（或者不在同一个文件）时返回 <code>None</code>。<code>Option&lt;T&gt;</code> 是”要么有一个 <code>T</code>（写作 <code>Some(x)</code>），要么没有（<code>None</code>）“，第 2 篇会细讲，这里你只需要会构造它。提示：开头取 <code>max</code>，结尾取 <code>min</code>。</p>
参考答案<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/// The overlap of two spans, or `None` when they do not share a byte.</span></div></div><div><div><div>2</div></div><div><span>#[must_use]</span></div></div><div><div><div>3</div></div><div><span>pub</span><span> </span><span>fn</span><span> </span><span>intersect</span><span>(</span><span>self</span><span>, </span><span>other</span><span>: </span><span>Span</span><span>) -&gt; </span><span>Option</span><span>&lt;</span><span>Span</span><span>&gt; {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>if</span><span> </span><span>self</span><span><span>.source </span><span>!=</span><span> </span></span><span>other</span><span>.source {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>return</span><span> </span><span>None</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span><span> </span><span>start</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.start.</span><span>max</span><span>(</span><span>other</span><span>.start);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>end</span><span><span> </span><span>=</span><span> </span></span><span>self</span><span>.end.</span><span>min</span><span>(</span><span>other</span><span>.end);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>if</span><span> </span><span>start</span><span><span> </span><span>&lt;</span><span> </span></span><span>end</span><span> {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Some</span><span>(</span><span>Span</span><span>::</span><span>new</span><span>(</span><span>self</span><span>.source, </span><span>start</span><span>, </span><span>end</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>None</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用一份独立的拷贝在实验里验证过（<code>ch01_intersect.rs</code>）：<code>2..8</code> 与 <code>5..12</code> 的交集输出 <code>Some(Span { source: SourceId(0), start: 5, end: 8 })</code>，<code>2..8</code> 与 <code>9..12</code> 输出 <code>None</code>。注意 <code>start &lt; end</code> 而不是 <code>&lt;=</code>：半开区间里 <code>[5, 5)</code> 是空的，没有共享任何字节，所以相邻的两个 span 的交集是 <code>None</code>。</p><p><strong>2.</strong> 下面三段代码，哪几段能编译？先预测，再到实验场验证（<code>ch01_copy.rs</code>、<code>ch01_copy_string.rs</code>）。</p><ul>
<li>A：<code>#[derive(Copy, Clone)] struct Pt { x: u32 }</code>，把一个 <code>Pt</code> 变量连续两次传给 <code>fn show_pt(p: Pt)</code>。</li>
<li>B：<code>struct Name { len: u32 }</code>（没有 derive <code>Copy</code>），把一个 <code>Name</code> 变量连续两次传给 <code>fn show_name(n: Name)</code>。</li>
<li>C：<code>#[derive(Copy, Clone)] struct Label { name: String }</code>。</li>
</ul>
参考答案<p>A 能编译：<code>Pt</code> 是 <code>Copy</code>，传参是按位复制，原变量仍然可用，和 <code>Span</code> 的行为一样。</p><p>B 不能：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_copy</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0382]: use of moved value: `n`</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--&gt; src/bin/ch01_copy.rs:26:15</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>24 |     let n = Name { len: 1 };</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>|         - move occurs because `n` has type `Name`, which does not implement the `Copy` trait</span></div></div><div><div><div>8</div></div><div><span>25 |     show_name(n);</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>|               - value moved here</span></div></div><div><div><div>10</div></div><div><span>26 |     show_name(n); // snippet B</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>|               ^ value used here after move</span></div></div><div><div><div>12</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二次传参时 <code>n</code> 已经被移动走了。没有 <code>Copy</code> 的类型，按值传参就是<strong>移动</strong>，原变量不能再用。这就是第 2 篇的主题。</p><p>C 也不能：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ cargo build --bin ch01_copy_string</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>Compiling rust-lab v0.1.0 (/tmp/rust-lab)</span></div></div><div><div><div>3</div></div><div><span>error[E0204]: the trait `Copy` cannot be implemented for this type</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>--&gt; src/bin/ch01_copy_string.rs:2:8</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>6</div></div><div><span>1 | #[derive(Copy, Clone)]</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>|          ---- in this derive macro expansion</span></div></div><div><div><div>8</div></div><div><span>2 | struct Label {</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>|        ^^^^^</span></div></div><div><div><div>10</div></div><div><span>3 |     name: String,</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|     ------------ this field does not implement `Copy`</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>For more information about this error, try `rustc --explain E0204`.</span></div></div><div><div><div>14</div></div><div><span>error: could not compile `rust-lab` (bin "ch01_copy_string") due to 1 previous error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>String</code> 持有堆内存，按位复制会造成两个对象指向同一块堆，析构时 double free，Rust 干脆禁止 <code>Copy</code>。同样的道理，C++ 里你需要写拷贝构造函数。第 2 篇从这里开始。</p><p><strong>3.</strong> 把下面的 <code>while</code> 循环改写成 <code>loop { ... break value }</code> 的形式，不改变结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>first_power_of_two_over</span><span>(</span><span>limit</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>while</span><span> </span><span>p</span><span><span> </span><span>&lt;=</span><span> </span></span><span>limit</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>p</span><span><span> </span><span>*=</span><span> </span></span><span>2</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>    </span><span>p</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
参考答案<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fn</span><span> </span><span>first_power_of_two_over_loop</span><span>(</span><span>limit</span><span>: </span><span>u32</span><span>) -&gt; </span><span>u32</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>p</span><span><span> </span><span>=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>if</span><span> </span><span>p</span><span><span> </span><span>&gt;</span><span> </span></span><span>limit</span><span> {</span></div></div><div><div><div>5</div></div><div><span>            </span><span>break</span><span> </span><span>p</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>        </span><span>p</span><span><span> </span><span>*=</span><span> </span></span><span>2</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个函数在 <code>ch01_loop_value.rs</code> 里都对 <code>100</code> 调了一遍，输出都是 <code>128</code>。<code>while</code> 版本更短，也更清楚，我会选它；<code>loop</code> 版本只在”退出条件出现在循环中间”或者”循环本身要产生一个值”（比如重试直到成功）时才更好。</p></section>
<section><h2>下一篇<a href="#下一篇"><span>#</span></a></h2><p><code>Span</code> 有一个特点：它是 <code>Copy</code> 的，所以我们一直回避了一个问题，谁拥有源码文本？一个编译器要把文件名和文件内容存在某处，还要能根据字节偏移反查”第几行第几列”，这时候 <code>String</code> 出场，所有权问题也会出场。C++ 程序员在这里的肌肉记忆是 <code>std::string</code> 和 <code>const std::string&amp;</code>，还有”别返回指向局部变量的引用”这条纪律。Rust 的做法是把这条纪律写进编译器，而且不需要你背：它会在编译期告诉你哪一行违反了，并给出一个看得懂的错误。第 2 篇，<a href="/posts/rust-by-pebble-02-ownership/">所有权：SourceMap 里的 String 归谁</a>，我们把 <code>pebble-span</code> 写完。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/raii-in-cpp/</id>
      <title type="text">RAII 到底贵不贵：我把 C++ 的资源管理拆到了汇编</title>
      <published>2026-10-05T14:00:00.000Z</published>
      <updated>2026-10-05T14:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/raii-in-cpp/"/>
      <summary type="text">我把 RAII 拆成 8 个可运行的实验：用手工清理对比析构函数，用替换的 operator new/delete 数泄漏对象，用 objdump 比对热路径与栈展开路径的指令数，再实测 unique_ptr 与 shared_ptr 的分配开销，最后整理浅拷贝、自赋值、抛异常的析构函数这些真实的坑。</summary>
      <content type="html"><![CDATA[<blockquote><p>“RAII 不是’自动释放’，而是’让资源的生命周期等于一个作用域’。”</p></blockquote>
<p>RAII 是我在 C++ 里用得最多、却解释得最少的机制。每天都写 <code>std::unique_ptr</code>、<code>std::lock_guard</code>、<code>std::vector</code>，但要说清楚”析构函数凭什么一定会被调用""它到底有没有性能开销”，我以前只有直觉，没有证据。</p>
<p>所以这次我不先讲概念，先做实验。所有代码都在同一台机器上编译运行：GCC 15.3.0、binutils 2.46、Linux 6.18、x86-64。代码统一按 Google C++ Style 排版（2 空格缩进、80 列、成员变量尾随下划线），下面贴的每一段都真的编译过。</p>
<p>我本来想用 valgrind 和 perf，这台机器上都没有，于是用 <code>-fsanitize=address</code> 和 <code>std::chrono::steady_clock</code> 顶上。为了数清楚”泄漏了几个对象”，我还替换了全局的 <code>operator new</code> / <code>operator delete</code>，这个技巧后面单独讲。</p>
<section><h2>一个函数，四处清理<a href="#一个函数四处清理"><span>#</span></a></h2><p>先看一段很普通的代码。它打开一个文件、分配一块缓冲区，中途有两条失败路径：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// Four exit paths, one of them forgets std::free().</span></div></div><div><div><div>2</div></div><div><span>bool</span><span> </span><span>ProcessManual</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>path</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>std::</span><span>FILE</span><span>*</span><span> </span><span>f</span><span> </span><span>=</span><span> std::</span><span>fopen</span><span>(</span><span>path</span><span>, </span><span>"rb"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>f</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>char*</span><span> </span><span>buf</span><span> </span><span>=</span><span> </span><span><span>static_cast</span><span>&lt;char*&gt;</span></span><span><span>(</span><span>std</span><span>::</span></span><span>malloc</span><span>(</span><span>1024</span><span>));</span></div></div><div><div><div>7</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>buf</span><span>) {</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>std::</span><span>fclose</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>  </span><span>buf</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>'</span><span>\0</span><span>'</span><span>;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>Consume</span><span>(</span><span>buf</span><span>)) {</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>std::</span><span>fclose</span><span>(</span><span>f</span><span>);</span><span>  // Leaks buf.</span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>free</span><span>(</span><span>buf</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>fclose</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>20</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>ProcessManual</code> 里一共写了 4 处清理：3 次 <code>fclose</code>、1 次 <code>free</code>。其中第二条失败路径漏掉了一次 <code>free</code>，泄漏 1024 字节。</p><p>我拿 <code>grep -oE</code> 数了一下，手工版本 4 处清理，RAII 版本：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>FileCloser</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>void</span><span> </span><span>operator</span><span>()(std::</span><span>FILE</span><span>*</span><span><span> </span><span>f</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>if</span><span> (</span><span>f</span><span>) {</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>std::</span><span>fclose</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>};</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>using</span><span> </span><span>FilePtr</span><span> </span><span>=</span><span> std::</span><span>unique_ptr</span><span>&lt;std::</span><span>FILE</span><span>, </span><span>FileCloser</span><span>&gt;;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>bool</span><span> </span><span>ProcessRaii</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>path</span><span>) {</span></span></div></div><div><div><div>12</div></div><div><span>  </span><span>FilePtr</span><span> </span><span>f</span><span><span>(</span><span>std</span><span>::</span></span><span>fopen</span><span>(</span><span>path</span><span>, </span><span>"rb"</span><span>));</span></div></div><div><div><div>13</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>f</span><span>) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>  </span><span>auto</span><span> </span><span>buf</span><span> </span><span>=</span><span> std::</span><span>make_unique</span><span>&lt;</span><span>char</span><span>[]&gt;(</span><span>1024</span><span>);</span></div></div><div><div><div>16</div></div><div><span>  </span><span>buf</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>'</span><span>\0</span><span>'</span><span>;</span></div></div><div><div><div>17</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>Consume</span><span>(</span><span>buf</span><span>.</span><span>get</span><span>())) </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>  </span><span>return</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>0 处。</p><p>这就是 RAII 的全部卖点：清理动作不在函数体里，而在对象的生命周期里。函数怎么退出都行，只要对象死掉，资源就跟着死。</p><p>顺便说一句，我本来想让编译器直接抓出那个泄漏：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>g++</span><span> </span><span>-std=c++23</span><span> </span><span>-fanalyzer</span><span> </span><span>-c</span><span> </span><span>analyzer_probe.cpp</span></div></div><div><div><div>2</div></div><div><span>analyzer_probe.cpp:</span><span> </span><span>In</span><span> </span><span>function</span><span> </span><span>'void ObviousLeak()':</span></div></div><div><div><div>3</div></div><div><span>analyzer_probe.cpp:3:33:</span><span> </span><span>warning:</span><span> </span><span>ignoring</span><span> </span><span>return</span><span> </span><span>value</span><span> </span><span>of</span><span> </span><span>'void* malloc(size_t)'</span></div></div><div><div><div>4</div></div><div><span>    </span><span>declared</span><span> </span><span>with</span><span> </span><span>attribute</span><span> </span><span>'warn_unused_result'</span><span> [-Wunused-result]</span></div></div><div><div><div>5</div></div><div><span>    </span><span>3</span><span> | </span><span>void</span><span> </span><span>ObviousLeak</span><span>() </span><span>{</span><span> </span><span>std::malloc</span><span>(</span><span>4096</span><span>); }</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>|                      </span><span>~~~~~~~~~~~^~~~~~</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>GCC 15.3 对它一言不发。这段代码里 <code>malloc</code> 的返回值直接丢掉，是一次无条件的泄漏，但 <code>-fanalyzer</code> 只抱怨了”返回值没人用”，没有报 <code>-Wanalyzer-malloc-leak</code>。所以静态分析这条路我放弃了，改用运行时自己数：每个 <code>operator new</code> 加一，每个 <code>operator delete</code> 减一。</p></section>
<section><h2>我把”泄漏”变成了一个可以数的数字<a href="#我把泄漏变成了一个可以数的数字"><span>#</span></a></h2><p>替换全局分配函数是 C++ 里少有的、不需要任何插桩框架就能做到的观测手段：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>namespace</span><span> {</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>long</span><span> </span><span>g_new_count</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>4</div></div><div><span>long</span><span> </span><span>g_delete_count</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>5</div></div><div><span>// Keeps the allocation observable, so the compiler cannot elide it.</span></div></div><div><div><div>6</div></div><div><span>volatile</span><span> </span><span>void*</span><span> </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>nullptr</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>void*</span><span> </span><span>CountAlloc</span><span>(std::</span><span>size_t</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>  </span><span>++</span><span>g_new_count</span><span>;</span></div></div><div><div><div>10</div></div><div><span>  </span><span>if</span><span> (</span><span>void*</span><span> </span><span>p</span><span> </span><span>=</span><span> std::</span><span>malloc</span><span>(</span><span>n</span><span>)) </span><span>return</span><span> </span><span>p</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>throw</span><span> std::</span><span>bad_alloc</span><span>();</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>}</span><span>  // namespace</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>void*</span><span> </span><span>operator</span><span> new(std::</span><span>size_t</span><span><span> </span><span>n</span><span>) { </span></span><span>return</span><span> </span><span>CountAlloc</span><span>(</span><span>n</span><span>); }</span></div></div><div><div><div>17</div></div><div><span>void*</span><span> </span><span>operator</span><span> new[](std::</span><span>size_t</span><span><span> </span><span>n</span><span>) { </span></span><span>return</span><span> </span><span>CountAlloc</span><span>(</span><span>n</span><span>); }</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>void</span><span> </span><span>operator</span><span> delete(</span><span>void*</span><span><span> </span><span>p</span><span>) </span></span><span>noexcept</span><span> {</span></div></div><div><div><div>20</div></div><div><span>  </span><span>if</span><span> (</span><span>p</span><span>) {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>++</span><span>g_delete_count</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>std::</span><span>free</span><span>(</span><span>p</span><span>);</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>void</span><span> </span><span>operator</span><span> delete[](</span><span>void*</span><span><span> </span><span>p</span><span>) </span></span><span>noexcept</span><span> {</span></div></div><div><div><div>27</div></div><div><span>  </span><span>if</span><span> (</span><span>p</span><span>) {</span></div></div><div><div><div>28</div></div><div><span>    </span><span>++</span><span>g_delete_count</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>std::</span><span>free</span><span>(</span><span>p</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>void</span><span> </span><span>operator</span><span> delete(</span><span>void*</span><span><span> </span><span>p</span><span>, std::</span></span><span>size_t</span><span>) </span><span>noexcept</span><span> { </span><span>operator</span><span> </span><span>delete</span><span>(</span><span>p</span><span>); }</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>void</span><span> </span><span>operator</span><span> delete[](</span><span>void*</span><span><span> </span><span>p</span><span>, std::</span></span><span>size_t</span><span>) </span><span>noexcept</span><span> {</span></div></div><div><div><div>36</div></div><div><span>  </span><span>operator</span><span> </span><span>delete[]</span><span>(</span><span>p</span><span>);</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>注意最后两行：C++14 之后编译器会优先调用带 <code>size_t</code> 的 sized deallocation。如果只替换不带 size 的版本，计数会漏掉一半，这个坑我踩过一次。</p><p>然后写六个场景，每个都让构造函数或后续调用抛异常：</p>

<table><thead><tr><th>场景</th><th>new</th><th>delete</th><th>泄漏对象</th></tr></thead><tbody><tr><td>手工清理，构造函数抛异常</td><td>2</td><td>1</td><td><strong>1</strong></td></tr><tr><td>RAII，构造函数抛异常</td><td>2</td><td>2</td><td><strong>0</strong></td></tr><tr><td>裸指针成员，构造函数抛异常</td><td>2</td><td>1</td><td><strong>1</strong></td></tr><tr><td>RAII 成员，构造函数抛异常</td><td>2</td><td>2</td><td><strong>0</strong></td></tr><tr><td>手工清理，后续调用失败</td><td>1</td><td>0</td><td><strong>1</strong></td></tr><tr><td>RAII，后续调用失败</td><td>1</td><td>1</td><td><strong>0</strong></td></tr></tbody></table><div><div><div></div><div>这张表是在 <code>-O0</code> 下跑出来的</div></div><div><p>我一开始用 <code>-O2</code> 编译同一份代码，得到的是另一张表。GCC 会把一对不逃逸的 <code>new</code> / <code>delete</code> 直接优化掉。手工清理那一行的第一个 <code>new</code> 就是这么消失的，泄漏数从 1 变成了”看起来没泄漏”。写这篇文章的第一次汇编实验我也栽在这里：两个循环的 <code>operator new</code> 全被消除了，我一度以为编译器把整个循环优化成了闭式求和，确实如此，但那不是我要测的东西。</p></div></div><p>顺带一提，这个计数器的实现方式也解释了”为什么 C++ 里泄漏难查”：分配和释放是两个独立的函数调用，中间隔着任意长的控制流。RAII 做的事情，就是把这两个调用在<strong>同一个作用域</strong>里绑定起来。</p></section>
<section><h2>构造函数抛异常时，析构函数不会运行<a href="#构造函数抛异常时析构函数不会运行"><span>#</span></a></h2><p>上表里最反直觉的是第二行和第三行的对比。它们看起来几乎一样，结果却不同：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Boom</span><span> {};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>Resource</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>char</span><span> </span><span>pad</span><span>[</span><span>64</span><span>];</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>struct</span><span> </span><span>Failing</span><span> {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>Failing</span><span>() { </span><span>throw</span><span> </span><span>Boom</span><span>{}; }</span></div></div><div><div><div>9</div></div><div><span>};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>class</span><span> </span><span>RawHolder</span><span> {</span></div></div><div><div><div>12</div></div><div><span> </span><span>public:</span></div></div><div><div><div>13</div></div><div><span>  </span><span>RawHolder</span><span>() : </span><span>resource_</span><span>(</span><span>new</span><span> </span><span>Resource</span><span>), </span><span>failing_</span><span>(</span><span>new</span><span> </span><span>Failing</span><span>) {}</span></div></div><div><div><div>14</div></div><div><span>  </span><span>~RawHolder</span><span>() {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>delete</span><span> </span><span>failing_</span><span>;</span></div></div><div><div><div>16</div></div><div><span>    </span><span>delete</span><span> </span><span>resource_</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span> </span><span>private:</span></div></div><div><div><div>20</div></div><div><span>  </span><span>Resource</span><span>*</span><span> </span><span>resource_</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>Failing</span><span>*</span><span> </span><span>failing_</span><span>;</span></div></div><div><div><div>22</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>new Failing</code> 抛出异常时，<code>RawHolder</code> 这个对象从来没被构造完成，所以 <code>~RawHolder</code> <strong>不会执行</strong>。<code>resource_</code> 指向的那个 <code>Resource</code> 对象就此失联。</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>new Resource 成功</p></span></div><div><span><p>new Failing 抛异常</p></span></div><div><span><p>RawHolder 构造未完成</p></span></div><div><span><p>已构造的成员按逆序析构</p></span></div><div><span><p>Resource 泄漏</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>new Resource 成功</p></span></div><div><span><p>new Failing 抛异常</p></span></div><div><span><p>RawHolder 构造未完成</p></span></div><div><span><p>已构造的成员按逆序析构</p></span></div><div><span><p>Resource 泄漏</p></span></div>
</div></div></div><p>注意图里那条分支：<strong>已经构造完成的成员</strong>会被销毁。这正是 <code>std::unique_ptr</code> 成员版本安全的原因：<code>resource_</code> 这个 <code>unique_ptr</code> 本身已经构造完成了，栈展开会调用它的析构函数，顺手把 <code>Resource</code> 也删掉。</p><p>所以在构造函数里管理多个资源时，正确写法不是”在构造函数末尾兜底”，而是让每个资源各自拥有一个已经构造完成的所有者：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>RaiiHolder</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>RaiiHolder</span><span>()</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>: </span><span>resource_</span><span><span>(</span><span>std</span><span>::</span></span><span>make_unique</span><span>&lt;</span><span>Resource</span><span>&gt;()),</span></div></div><div><div><div>5</div></div><div><span>        </span><span>failing_</span><span><span>(</span><span>std</span><span>::</span></span><span>make_unique</span><span>&lt;</span><span>Failing</span><span>&gt;()) {}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span> </span><span>private:</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>unique_ptr</span><span>&lt;</span><span>Resource</span><span>&gt; </span><span>resource_</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>unique_ptr</span><span>&lt;</span><span>Failing</span><span>&gt; </span><span>failing_</span><span>;</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不需要写析构函数，也不需要 <code>try</code> / <code>catch</code>。这条规则和”构造函数里不要写裸露的 <code>delete</code>”是同一件事的两种说法。</p></section>
<section><h2>销毁顺序是确定的，不是随机的<a href="#销毁顺序是确定的不是随机的"><span>#</span></a></h2><p>运行下面这几个类，把构造和析构都打印出来：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Tracer</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>explicit</span><span> </span><span>Tracer</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>name</span><span>) : </span></span><span>name_</span><span>(</span><span>name</span><span>) {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>std::</span><span>printf</span><span>(</span><span>"  ctor  </span><span>%s</span><span>\n</span><span>"</span><span>, </span><span>name_</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>  </span><span>~Tracer</span><span>() { std::</span><span>printf</span><span>(</span><span>"  dtor  </span><span>%s</span><span>\n</span><span>"</span><span>, </span><span>name_</span><span>); }</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span> </span><span>private:</span></div></div><div><div><div>9</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>name_</span><span>;</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>struct</span><span> </span><span>Base</span><span> {</span></div></div><div><div><div>13</div></div><div><span>  </span><span>Tracer</span><span> </span><span>tracer</span><span>{</span><span>"Base"</span><span>};</span></div></div><div><div><div>14</div></div><div><span>};</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>struct</span><span> </span><span>MemberA</span><span> {</span></div></div><div><div><div>17</div></div><div><span>  </span><span>Tracer</span><span> </span><span>tracer</span><span>{</span><span>"member a (declared 1st)"</span><span>};</span></div></div><div><div><div>18</div></div><div><span>};</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>struct</span><span> </span><span>MemberB</span><span> {</span></div></div><div><div><div>21</div></div><div><span>  </span><span>Tracer</span><span> </span><span>tracer</span><span>{</span><span>"member b (declared 2nd)"</span><span>};</span></div></div><div><div><div>22</div></div><div><span>};</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>class</span><span> </span><span>Derived</span><span> : </span><span>public</span><span> </span><span>Base</span><span> {</span></div></div><div><div><div>25</div></div><div><span> </span><span>public:</span></div></div><div><div><div>26</div></div><div><span>  </span><span>Derived</span><span>() </span><span>=</span><span> </span><span>default</span><span>;</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span> </span><span>private:</span></div></div><div><div><div>29</div></div><div><span>  </span><span>MemberA</span><span> </span><span>a_</span><span>;</span></div></div><div><div><div>30</div></div><div><span>  </span><span>MemberB</span><span> </span><span>b_</span><span>;</span></div></div><div><div><div>31</div></div><div><span>  </span><span>Tracer</span><span> </span><span>body_</span><span>{</span><span>"Derived ctor body"</span><span>};</span></div></div><div><div><div>32</div></div><div><span>};</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span><span>"-- enter main --</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>37</div></div><div><span>    </span><span>Tracer</span><span> </span><span>first</span><span>{</span><span>"local 1 (outer scope)"</span><span>};</span></div></div><div><div><div>38</div></div><div><span>    </span><span>Tracer</span><span> </span><span>second</span><span>{</span><span>"local 2 (outer scope)"</span><span>};</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span><span>"-- locals gone, now build Derived --</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>41</div></div><div><span>  </span><span>Derived</span><span> </span><span>d</span><span>;</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span><span>"-- leaving main --</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>实际输出：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>--</span><span> </span><span>enter</span><span> </span><span>main</span><span> </span><span>--</span></div></div><div><div><div>2</div></div><div><span>  </span><span>ctor</span><span>  </span><span>local</span><span> </span><span>1</span><span> (outer </span><span>scope</span><span>)</span></div></div><div><div><div>3</div></div><div><span>  </span><span>ctor</span><span>  </span><span>local</span><span> </span><span>2</span><span> (outer </span><span>scope</span><span>)</span></div></div><div><div><div>4</div></div><div><span>  </span><span>dtor</span><span>  </span><span>local</span><span> </span><span>2</span><span> (outer </span><span>scope</span><span>)</span></div></div><div><div><div>5</div></div><div><span>  </span><span>dtor</span><span>  </span><span>local</span><span> </span><span>1</span><span> (outer </span><span>scope</span><span>)</span></div></div><div><div><div>6</div></div><div><span>--</span><span> </span><span>locals</span><span> </span><span>gone,</span><span> </span><span>now</span><span> </span><span>build</span><span> </span><span>Derived</span><span> </span><span>--</span></div></div><div><div><div>7</div></div><div><span>  </span><span>ctor</span><span>  </span><span>Base</span></div></div><div><div><div>8</div></div><div><span>  </span><span>ctor</span><span>  </span><span>member</span><span> </span><span>a</span><span> (declared </span><span>1st</span><span>)</span></div></div><div><div><div>9</div></div><div><span>  </span><span>ctor</span><span>  </span><span>member</span><span> </span><span>b</span><span> (declared </span><span>2nd</span><span>)</span></div></div><div><div><div>10</div></div><div><span>  </span><span>ctor</span><span>  </span><span>Derived</span><span> </span><span>ctor</span><span> </span><span>body</span></div></div><div><div><div>11</div></div><div><span>--</span><span> </span><span>leaving</span><span> </span><span>main</span><span> </span><span>--</span></div></div><div><div><div>12</div></div><div><span>  </span><span>dtor</span><span>  </span><span>Derived</span><span> </span><span>ctor</span><span> </span><span>body</span></div></div><div><div><div>13</div></div><div><span>  </span><span>dtor</span><span>  </span><span>member</span><span> </span><span>b</span><span> (declared </span><span>2nd</span><span>)</span></div></div><div><div><div>14</div></div><div><span>  </span><span>dtor</span><span>  </span><span>member</span><span> </span><span>a</span><span> (declared </span><span>1st</span><span>)</span></div></div><div><div><div>15</div></div><div><span>  </span><span>dtor</span><span>  </span><span>Base</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>三条规则，一次全看到了：</p><ul>
<li>局部对象按构造的逆序销毁；</li>
<li>成员按<strong>声明顺序</strong>构造、按声明逆序销毁，和初始化列表里写的顺序无关；</li>
<li>基类在成员之后销毁。</li>
</ul><p>第三条规则有个实际后果。如果一个成员的析构函数要用到另一个成员：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Session</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>Connection</span><span> </span><span>conn</span><span>;</span></div></div><div><div><div>3</div></div><div><span>  </span><span>Logger</span><span> </span><span>log</span><span>;</span><span>  // log 的析构函数要往 conn 里写日志</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>那么 <code>log</code> 会先于 <code>conn</code> 销毁，写入会落在一个已经关闭的连接上。声明顺序就是依赖顺序：被依赖的对象声明在前面，它就会活得最久。</p><div><div><div></div><div>初始化列表的顺序会被编译器警告</div></div><div><p>初始化列表里写的顺序如果和声明顺序不一致，GCC 会给出 <code>-Wreorder</code> 警告，但析构顺序不会警告。后者只能靠你自己记住。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>reorder.cpp:7:7:</span><span> </span><span>warning:</span><span> </span><span>'Pair::second_'</span><span> </span><span>will</span><span> </span><span>be</span><span> </span><span>initialized</span><span> </span><span>after</span><span> [-Wreorder]</span></div></div><div><div><div>2</div></div><div><span>reorder.cpp:6:7:</span><span> </span><span>warning:</span><span>   </span><span>'int Pair::first_'</span><span> [-Wreorder]</span></div></div><div><div><div>3</div></div><div><span>reorder.cpp:3:3:</span><span> </span><span>warning:</span><span>   </span><span>when</span><span> </span><span>initialized</span><span> </span><span>here</span><span> [-Wreorder]</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div></div></section>
<section><h2>标准库已经写好了一大半 RAII<a href="#标准库已经写好了一大半-raii"><span>#</span></a></h2><p>不需要自己造轮子。日常用到的基本都在标准库里：</p>

<table><thead><tr><th>资源</th><th>RAII 类型</th><th>释放动作</th></tr></thead><tbody><tr><td>单个对象</td><td><code>std::unique_ptr&lt;T&gt;</code></td><td><code>delete</code></td></tr><tr><td>数组</td><td><code>std::unique_ptr&lt;T[]&gt;</code></td><td><code>delete[]</code></td></tr><tr><td>共享所有权</td><td><code>std::shared_ptr&lt;T&gt;</code></td><td>引用计数归零时 <code>delete</code></td></tr><tr><td>互斥锁</td><td><code>std::lock_guard</code> / <code>std::unique_lock</code></td><td><code>unlock</code></td></tr><tr><td>文件流</td><td><code>std::ifstream</code> / <code>std::ofstream</code></td><td><code>close</code></td></tr><tr><td>动态数组</td><td><code>std::vector</code></td><td>释放缓冲区并调用元素析构</td></tr><tr><td>线程</td><td><code>std::jthread</code></td><td><code>request_stop</code> 后 <code>join</code></td></tr><tr><td>任意 C 句柄</td><td><code>std::unique_ptr&lt;T, Deleter&gt;</code></td><td>你自己指定的 <code>Deleter</code></td></tr></tbody></table><p>最后一行最有用。C 库里所有的 <code>fopen</code> / <code>sqlite3_open</code> / <code>SSL_new</code>，都能用自定义删除器包成 RAII（<code>FilePtr</code> 就是上面那个 <code>std::unique_ptr&lt;std::FILE, FileCloser&gt;</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;</span><span>FilePtr</span><span>&gt; </span><span>OpenAll</span><span>() {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>FilePtr</span><span>&gt; </span><span>files</span><span>;</span></div></div><div><div><div>3</div></div><div><span>  </span><span>files</span><span>.</span><span>emplace_back</span><span><span>(</span><span>std</span><span>::</span></span><span>fopen</span><span>(</span><span>"/etc/hostname"</span><span>, </span><span>"r"</span><span>));</span></div></div><div><div><div>4</div></div><div><span>  </span><span>files</span><span>.</span><span>emplace_back</span><span><span>(</span><span>std</span><span>::</span></span><span>fopen</span><span>(</span><span>"/etc/os-release"</span><span>, </span><span>"r"</span><span>));</span></div></div><div><div><div>5</div></div><div><span>  </span><span>return</span><span> </span><span>files</span><span>;</span><span>  // Moved out; if this throws, both are closed.</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运行时输出：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>opened</span><span> </span><span>2</span><span> </span><span>files</span></div></div><div><div><div>2</div></div><div><span>leaving</span><span> </span><span>main</span></div></div><div><div><div>3</div></div><div><span>  </span><span>fclose(0x5f92727d9320</span><span>)</span></div></div><div><div><div>4</div></div><div><span>  </span><span>fclose(0x5f92727d9520</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两次 <code>fclose</code> 都发生在 <code>main</code> 返回之后、进程退出之前。注意 <code>OpenAll()</code> 是按值返回 <code>std::vector&lt;FilePtr&gt;</code> 的，<code>unique_ptr</code> 不可拷贝，这里全靠移动语义。如果第二个 <code>emplace_back</code> 抛异常，第一个 <code>FilePtr</code> 已经是一个构造完成的元素，<code>vector</code> 的析构会把它一起带走。</p><p>锁也值得单独看一眼。用 4 个线程各加 20 万次，然后让一个函数握着锁抛异常：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>BumpAndThrow</span><span>() {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>std::</span><span>lock_guard</span><span>&lt;std::</span><span>mutex</span><span>&gt; </span><span>lock</span><span>(</span><span>g_mutex</span><span>);</span></div></div><div><div><div>3</div></div><div><span>  </span><span>g_counter</span><span> </span><span>+=</span><span> </span><span>1000</span><span>;</span></div></div><div><div><div>4</div></div><div><span>  </span><span>throw</span><span> std::</span><span>runtime_error</span><span>(</span><span>"boom while holding the lock"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>caught:</span><span> </span><span>boom</span><span> </span><span>while</span><span> </span><span>holding</span><span> </span><span>the</span><span> </span><span>lock</span></div></div><div><div><div>2</div></div><div><span>counter</span><span> </span><span>=</span><span> </span><span>801000</span><span> (expected </span><span>801000</span><span>)</span></div></div><div><div><div>3</div></div><div><span>mutex</span><span> </span><span>is</span><span> </span><span>free</span><span> </span><span>after</span><span> </span><span>the</span><span> </span><span>throw</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一行是 <code>g_mutex.try_lock()</code> 试出来的。如果没有 RAII，这个异常会让互斥量永远锁着，下一次加锁就是死锁，而且是在某个与错误原因完全无关的地方卡住。这类 bug 的调试成本远高于它的代码长度。</p></section>
<section><h2>那它到底贵不贵：热路径 55 对 55<a href="#那它到底贵不贵热路径-55-对-55"><span>#</span></a></h2><p>这是我最想知道的。同一段循环，一份用裸指针，一份用 <code>unique_ptr</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>long</span><span> </span><span>g_sink</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>2</div></div><div><span>// volatile keeps the constructor/destructor side effects observable.</span></div></div><div><div><div>3</div></div><div><span>volatile</span><span> </span><span>int</span><span> </span><span>g_live</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>4</div></div><div><span>volatile</span><span> </span><span>void*</span><span> </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>nullptr</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>void</span><span> </span><span>MayThrow</span><span>();</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>class</span><span> </span><span>Widget</span><span> {</span></div></div><div><div><div>9</div></div><div><span> </span><span>public:</span></div></div><div><div><div>10</div></div><div><span>  </span><span>explicit</span><span> </span><span>Widget</span><span>(</span><span>int</span><span><span> </span><span>value</span><span>) : </span></span><span>value_</span><span>(</span><span>value</span><span>) { </span><span>g_live</span><span> </span><span>=</span><span> </span><span>g_live</span><span> </span><span>+</span><span> </span><span>1</span><span>; }</span></div></div><div><div><div>11</div></div><div><span>  </span><span>~Widget</span><span>() { </span><span>g_live</span><span> </span><span>=</span><span> </span><span>g_live</span><span> </span><span>-</span><span> </span><span>1</span><span>; }</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>  </span><span>int</span><span> </span><span>value</span><span>() </span><span>const</span><span> { </span><span>return</span><span> </span><span>value_</span><span>; }</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span> </span><span>private:</span></div></div><div><div><div>16</div></div><div><span>  </span><span>int</span><span> </span><span>value_</span><span>;</span></div></div><div><div><div>17</div></div><div><span>};</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>void</span><span> </span><span>ManualHot</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>20</div></div><div><span>  </span><span>long</span><span> </span><span>sum</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>Widget</span><span>*</span><span> </span><span>w</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Widget</span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>23</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>w</span><span>;</span></div></div><div><div><div>24</div></div><div><span>    </span><span>sum</span><span> </span><span>+=</span><span> </span><span>w</span><span>-&gt;</span><span>value</span><span>();</span></div></div><div><div><div>25</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>nullptr</span><span>;</span></div></div><div><div><div>26</div></div><div><span>    </span><span>delete</span><span> </span><span>w</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>  </span><span>g_sink</span><span> </span><span>=</span><span> </span><span>sum</span><span> </span><span>+</span><span> </span><span>g_live</span><span>;</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>void</span><span> </span><span>RaiiHot</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>32</div></div><div><span>  </span><span>long</span><span> </span><span>sum</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>33</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>auto</span><span> </span><span>w</span><span> </span><span>=</span><span> std::</span><span>make_unique</span><span>&lt;</span><span>Widget</span><span>&gt;(</span><span>i</span><span>);</span></div></div><div><div><div>35</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>w</span><span>.</span><span>get</span><span>();</span></div></div><div><div><div>36</div></div><div><span>    </span><span>sum</span><span> </span><span>+=</span><span> </span><span>w</span><span>-&gt;</span><span>value</span><span>();</span></div></div><div><div><div>37</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>nullptr</span><span>;</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>39</div></div><div><span>  </span><span>g_sink</span><span> </span><span>=</span><span> </span><span>sum</span><span> </span><span>+</span><span> </span><span>g_live</span><span>;</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>g_escape</code> 这个 <code>volatile</code> 全局变量是必须的。第一次写这个实验时我没加它，GCC 把两边的 <code>new</code> / <code>delete</code> 全部消掉了，<code>objdump</code> 里一个 <code>operator new</code> 都找不到，我差点得出”编译器把整个循环算成了等差数列”这种荒谬结论（它确实这么做了，但那不是我要测的）。写入一个 <code>volatile</code> 全局指针之后，分配结果必须真实存在，两个循环才终于可以在同一层面上比较。</p><p><code>-O2</code> 编译，数指令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>objdump</span><span> </span><span>-d</span><span> </span><span>-C</span><span> </span><span>--no-show-raw-insn</span><span> </span><span>codegen.o</span><span> &gt; </span><span>codegen.asm</span></div></div><div><div><div>2</div></div><div><span>$</span><span> </span><span>awk</span><span> </span><span>'/^[0-9a-f]+ &lt;/ { n=$0; sub(/^[^&lt;]*&lt;/,"",n); sub(/&gt;:$/,"",n); next }</span></div></div><div><div><div>3</div></div><div><span><span>       </span></span><span>/^[[:space:]]+[0-9a-f]+:/ { c[n]++ }</span></div></div><div><div><div>4</div></div><div><span><span>       </span></span><span>END { for (k in c) printf "  %-24s %3d instructions\n", k, c[k] }'</span><span> </span><span>codegen.asm</span><span> | </span><span>sort</span></div></div><div><div><div>5</div></div><div><span>  </span><span>ManualHot(int</span><span>)            55 instructions</span></div></div><div><div><div>6</div></div><div><span>  </span><span>ManualUnwind(int</span><span>)         58 instructions</span></div></div><div><div><div>7</div></div><div><span>  </span><span>RaiiHot(int</span><span>)              55 instructions</span></div></div><div><div><div>8</div></div><div><span>  </span><span>RaiiUnwind(int</span><span>)           58 instructions</span></div></div><div><div><div>9</div></div><div><span>  </span><span>RaiiUnwind(int</span><span>) [clone .cold]   8 instructions</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>55 对 55。我把两份反汇编去掉地址和注释后逐行 <code>diff</code>，53 行里唯一的差别是分支目标地址，以及一条 <code>cmp</code> 的操作数顺序（<code>cmp %r12d,%r14d</code> 和 <code>cmp %r14d,%r12d</code> 对后面的 <code>jne</code> 等价）。两者都是 <code>call operator new</code>、几次 <code>volatile</code> 写入、<code>call operator delete</code>、循环。也就是说，在热路径上，RAII 的抽象代价是 <strong>0</strong>。</p><p>那异常路径呢？在获取资源和释放资源之间放一个可能抛异常的调用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>ManualUnwind</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>long</span><span> </span><span>sum</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>3</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Widget</span><span>*</span><span> </span><span>w</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Widget</span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>w</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>sum</span><span> </span><span>+=</span><span> </span><span>w</span><span>-&gt;</span><span>value</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>MayThrow</span><span>();</span><span>  // 这里抛异常</span></div></div><div><div><div>8</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>nullptr</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>delete</span><span> </span><span>w</span><span>;</span><span>    // 到不了</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>  </span><span>g_sink</span><span> </span><span>=</span><span> </span><span>sum</span><span> </span><span>+</span><span> </span><span>g_live</span><span>;</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>void</span><span> </span><span>RaiiUnwind</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>15</div></div><div><span>  </span><span>long</span><span> </span><span>sum</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>16</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>auto</span><span> </span><span>w</span><span> </span><span>=</span><span> std::</span><span>make_unique</span><span>&lt;</span><span>Widget</span><span>&gt;(</span><span>i</span><span>);</span></div></div><div><div><div>18</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>w</span><span>.</span><span>get</span><span>();</span></div></div><div><div><div>19</div></div><div><span>    </span><span>sum</span><span> </span><span>+=</span><span> </span><span>w</span><span>-&gt;</span><span>value</span><span>();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>MayThrow</span><span>();</span><span>  // 这里抛异常</span></div></div><div><div><div>21</div></div><div><span>    </span><span>g_escape</span><span> </span><span>=</span><span> </span><span>nullptr</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>  </span><span>g_sink</span><span> </span><span>=</span><span> </span><span>sum</span><span> </span><span>+</span><span> </span><span>g_live</span><span>;</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>两个函数的主体都是 58 条指令。差别只在别的地方：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span><span> </span><span>manual_only.o</span><span> </span><span>---</span></div></div><div><div><div>2</div></div><div><span>  </span><span>.text</span><span>                 </span><span>180</span><span> </span><span>bytes</span></div></div><div><div><div>3</div></div><div><span>  </span><span>.eh_frame</span><span>              </span><span>72</span><span> </span><span>bytes</span></div></div><div><div><div>4</div></div><div><span>---</span><span> </span><span>raii_only.o</span><span> </span><span>---</span></div></div><div><div><div>5</div></div><div><span>  </span><span>.text</span><span>                 </span><span>196</span><span> </span><span>bytes</span></div></div><div><div><div>6</div></div><div><span>  </span><span>.text.unlikely</span><span>         </span><span>32</span><span> </span><span>bytes</span><span>   &lt;</span><span>-</span><span> </span><span>RAII</span><span> </span><span>特有的</span></div></div><div><div><div>7</div></div><div><span>  </span><span>.gcc_except_table</span><span>      </span><span>21</span><span> </span><span>bytes</span><span>   &lt;</span><span>-</span><span> </span><span>RAII</span><span> </span><span>特有的</span></div></div><div><div><div>8</div></div><div><span>  </span><span>.eh_frame</span><span>             </span><span>120</span><span> </span><span>bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>手工版本里<strong>根本没有</strong> <code>.text.unlikely</code> 和 <code>.gcc_except_table</code> 这两节，因为它压根没有”异常时该清理什么”的概念。RAII 版本多出来的 32 字节冷代码，就是栈展开时执行的着陆垫：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>0000000000000000</span><span> </span><span>&lt;RaiiUnwind</span><span>(</span><span>int</span><span>) [clone </span><span>.cold]&gt;:</span></div></div><div><div><div>2</div></div><div><span>   </span><span>0:</span><span>  </span><span>mov</span><span>    (%r12),%eax</span></div></div><div><div><div>3</div></div><div><span>   </span><span>4:</span><span>  </span><span>mov</span><span>    </span><span>%rbx,%rdi</span></div></div><div><div><div>4</div></div><div><span>   </span><span>7:</span><span>  </span><span>mov</span><span>    </span><span>$0x4</span><span>,%esi</span></div></div><div><div><div>5</div></div><div><span>   </span><span>c:</span><span>  </span><span>sub</span><span>    </span><span>$0x1</span><span>,%eax</span></div></div><div><div><div>6</div></div><div><span>   </span><span>f:</span><span>  </span><span>mov</span><span>    </span><span>%eax,</span><span>(</span><span>%r12</span><span>)</span></div></div><div><div><div>7</div></div><div><span>  </span><span>13:</span><span>  </span><span>call</span><span>   </span><span>...</span><span> </span><span>operator</span><span> </span><span>delete</span><span>(</span><span>void*,</span><span> </span><span>unsigned</span><span> </span><span>long</span><span>)</span></div></div><div><div><div>8</div></div><div><span>  </span><span>18:</span><span>  </span><span>mov</span><span>    </span><span>%r13,%rdi</span></div></div><div><div><div>9</div></div><div><span>  </span><span>1b:</span><span>  </span><span>call</span><span>   </span><span>...</span><span> </span><span>_Unwind_Resume</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前 5 条指令是内联的 <code>~Widget</code>（<code>g_live</code> 减一），然后释放对象，最后 <code>_Unwind_Resume</code> 把异常继续往上抛。8 条指令，外加 21 字节的只读异常表。整个异常安全的成本就是这些：它不在热路径上，不在每次迭代里，只在”真的要展开”的路径上。这也是我理解 RAII 开销的方式 - 它不是”每次调用付一点”，而是”为每条可能失败的路径准备一小段冷代码”。</p><p>（严格地说，这里是 libstdc++ 的 table-based unwinding 才有的性质。如果编译器用了别的展开方案，或者析构函数可能抛异常，代码生成会不一样。我倾向于认为在这个配置下结论是稳的。）</p></section>
<section><h2>分配开销：unique_ptr 1.0x，shared_ptr 1.2x<a href="#分配开销unique_ptr-10xshared_ptr-12x"><span>#</span></a></h2><p>上一节测的是”不额外的指令”。这一节测”会不会更慢”。</p><p>我在 <code>std::vector</code> 里放 20 万个 <code>int</code>，对象的创建和销毁都算进时间里，跑 25 轮取中位数：</p>

<table><thead><tr><th>写法</th><th>min</th><th>中位数</th><th>max</th><th>相对裸指针</th></tr></thead><tbody><tr><td>裸 <code>new</code> / <code>delete</code></td><td>30.4</td><td><strong>38.2</strong></td><td>43.4</td><td>1.00x</td></tr><tr><td><code>std::make_unique&lt;int&gt;</code></td><td>34.3</td><td><strong>40.4</strong></td><td>45.2</td><td>1.06x</td></tr><tr><td><code>std::make_shared&lt;int&gt;</code></td><td>34.9</td><td><strong>46.4</strong></td><td>62.2</td><td>1.22x</td></tr></tbody></table><p>单位是 ns/object。我跑了 3 轮完整的 25 次中位数，<code>unique_ptr</code> 是 0.99x - 1.06x，<code>shared_ptr</code> 是 1.16x - 1.22x。</p><p><code>unique_ptr</code> 和裸指针在同一片噪声里，这个符合预期：它就是一个指针，析构函数是 <code>delete</code>，编译器内联之后没有额外字段。<code>shared_ptr</code> 稳定地贵约 15% - 20%，差价来自控制块和引用计数的原子操作。注意我用的是 <code>const auto&amp;</code> 解引用，没有触碰引用计数；如果按值传 <code>shared_ptr</code>，每传一次就多一次原子增减，那个差距会更大。</p><div><div><div></div><div>这个基准测的是什么</div></div><div><p>时间大头其实是分配器，不是智能指针。20 万个 4 字节的对象，真实成本在 <code>malloc</code> / <code>free</code>。换成 <code>std::pmr</code> 或者按块分配，两者的绝对数字都会变，但比例关系大概不会。</p></div></div></section>
<section><h2>六个真实的坑<a href="#六个真实的坑"><span>#</span></a></h2><p>到这里，RAII 看起来是免费的、万能的。下面是我自己在真实代码里踩过的坑，每一个都有可以复现的失败。</p><section><h3>1. 浅拷贝就是两次 <code>delete</code><a href="#1-浅拷贝就是两次-delete"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Buffer</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>explicit</span><span> </span><span>Buffer</span><span>(std::</span><span>size_t</span><span><span> </span><span>size</span><span>) : </span></span><span>data_</span><span>(</span><span>new</span><span> </span><span>char</span><span>[</span><span>size</span><span>]), </span><span>size_</span><span>(</span><span>size</span><span>) {}</span></div></div><div><div><div>4</div></div><div><span>  </span><span>~Buffer</span><span>() { </span><span>delete[]</span><span> </span><span>data_</span><span>; }</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>// No copy constructor, no copy assignment: the implicit ones are shallow.</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span> </span><span>private:</span></div></div><div><div><div>9</div></div><div><span>  </span><span>char*</span><span> </span><span>data_</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>size_</span><span>;</span></div></div><div><div><div>11</div></div><div><span>};</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>  </span><span>Buffer</span><span> </span><span>a</span><span>(</span><span>16</span><span>);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>Buffer</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>a</span><span>;</span><span>  // Shallow copy of data_.</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译器生成的拷贝构造函数做的事是”逐位拷贝”，于是两个 <code>Buffer</code> 拥有同一个 <code>data_</code>。两个析构函数都要 <code>delete[]</code> 它：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ERROR:</span><span> </span><span>AddressSanitizer:</span><span> </span><span>attempting</span><span> </span><span>double-free</span><span> </span><span>on</span><span> </span><span>0x75412fde0010</span><span> </span><span>in</span><span> </span><span>thread</span><span> </span><span>T0:</span></div></div><div><div><div>2</div></div><div><span>    </span><span>#0 ... in operator delete[](void*)</span></div></div><div><div><div>3</div></div><div><span>    </span><span>#1 ... in Buffer::~Buffer() buffer.cpp:6</span></div></div><div><div><div>4</div></div><div><span>    </span><span>#2 ... in main buffer.cpp:18</span></div></div><div><div><div>5</div></div><div><span>freed</span><span> </span><span>by</span><span> </span><span>thread</span><span> </span><span>T0</span><span> </span><span>here:</span></div></div><div><div><div>6</div></div><div><span>    </span><span>#1 ... in Buffer::~Buffer() buffer.cpp:6</span></div></div><div><div><div>7</div></div><div><span>previously</span><span> </span><span>allocated</span><span> </span><span>by</span><span> </span><span>thread</span><span> </span><span>T0</span><span> </span><span>here:</span></div></div><div><div><div>8</div></div><div><span>    </span><span>#1 ... in Buffer::Buffer(unsigned long) buffer.cpp:5</span></div></div><div><div><div>9</div></div><div><span>SUMMARY:</span><span> </span><span>AddressSanitizer:</span><span> </span><span>double-free</span><span> </span><span>buffer.cpp:6</span><span> </span><span>in</span><span> </span><span>Buffer::~Buffer</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这就是”三法则 / 五法则”的来源：一旦你手写析构函数，拷贝构造、拷贝赋值、移动构造、移动赋值都得一起考虑。<code>unique_ptr</code> 的做法是直接把拷贝构造 <code>= delete</code>，编译期就拦住：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>unique_ptr_copy.cpp:5:12:</span><span> </span><span>error:</span><span> </span><span>use</span><span> </span><span>of</span><span> </span><span>deleted</span><span> </span><span>function</span></div></div><div><div><div>2</div></div><div><span>    </span><span>'std::unique_ptr&lt;_Tp, _Dp&gt;::unique_ptr(const std::unique_ptr&lt;_Tp, _Dp&gt;&amp;)</span></div></div><div><div><div>3</div></div><div><span><span>     </span></span><span>[with _Tp = int; _Dp = std::default_delete&lt;int&gt;]'</span></div></div><div><div><div>4</div></div><div><span>    </span><span>5</span><span> |   </span><span>auto</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>a</span><span>;  </span><span>//</span><span> </span><span>Deleted:</span><span> </span><span>unique_ptr</span><span> </span><span>is</span><span> </span><span>move-only.</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>|            </span><span>^</span></div></div><div><div><div>6</div></div><div><span>note:</span><span> </span><span>declared</span><span> </span><span>here</span></div></div><div><div><div>7</div></div><div><span>  </span><span>543</span><span> |       </span><span>unique_ptr(const</span><span> </span><span>unique_ptr</span><span>&amp;) = delete;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>比运行时 double-free 好得多。</p></section><section><h3>2. 自赋值会读到自己刚释放的内存<a href="#2-自赋值会读到自己刚释放的内存"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Holder</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>Holder</span><span>() : </span><span>payload_</span><span>(</span><span>new</span><span> </span><span>Payload</span><span>) {}</span></div></div><div><div><div>4</div></div><div><span>  </span><span>~Holder</span><span>() { </span><span>delete</span><span> </span><span>payload_</span><span>; }</span></div></div><div><div><div>5</div></div><div><span>  </span><span>Holder</span><span>(</span><span>const</span><span> </span><span>Holder</span><span>&amp;</span><span><span> </span><span>other</span><span>) : </span></span><span>payload_</span><span>(</span><span>new</span><span> </span><span>Payload</span><span>(</span><span>*</span><span>other</span><span>.</span><span>payload_</span><span>)) {}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>  </span><span>Holder</span><span>&amp;</span><span> </span><span>operator</span><span>=(</span><span>const</span><span> </span><span>Holder</span><span>&amp;</span><span><span> </span><span>other</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>    </span><span>delete</span><span> </span><span>payload_</span><span>;</span><span>  // If &amp;other == this, other.payload_ now dangles.</span></div></div><div><div><div>9</div></div><div><span>    </span><span>payload_</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Payload</span><span>(</span><span>*</span><span>other</span><span>.</span><span>payload_</span><span>);</span></div></div><div><div><div>10</div></div><div><span>    </span><span>return</span><span> </span><span>*</span><span>this</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span> </span><span>private:</span></div></div><div><div><div>14</div></div><div><span>  </span><span>Payload</span><span>*</span><span> </span><span>payload_</span><span>;</span></div></div><div><div><div>15</div></div><div><span>};</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>  </span><span>Holder</span><span> </span><span>h</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>h</span><span> </span><span>=</span><span> </span><span>h</span><span>;</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ERROR:</span><span> </span><span>AddressSanitizer:</span><span> </span><span>heap-use-after-free</span><span> </span><span>on</span><span> </span><span>address</span><span> </span><span>0x78e30a9e0040</span></div></div><div><div><div>2</div></div><div><span>READ</span><span> </span><span>of</span><span> </span><span>size</span><span> </span><span>32</span><span> </span><span>at</span><span> </span><span>0x78e30a9e0040</span><span> </span><span>thread</span><span> </span><span>T0</span></div></div><div><div><div>3</div></div><div><span>    </span><span>#0 ... in Holder::operator=(Holder const&amp;) self_assign.cpp:19</span></div></div><div><div><div>4</div></div><div><span>    </span><span>#1 ... in main self_assign.cpp:29</span></div></div><div><div><div>5</div></div><div><span>freed</span><span> </span><span>by</span><span> </span><span>thread</span><span> </span><span>T0</span><span> </span><span>here:</span></div></div><div><div><div>6</div></div><div><span>    </span><span>#1 ... in Holder::operator=(Holder const&amp;) self_assign.cpp:18</span></div></div><div><div><div>7</div></div><div><span>previously</span><span> </span><span>allocated</span><span> </span><span>by</span><span> </span><span>thread</span><span> </span><span>T0</span><span> </span><span>here:</span></div></div><div><div><div>8</div></div><div><span>    </span><span>#1 ... in Holder::Holder() self_assign.cpp:13</span></div></div><div><div><div>9</div></div><div><span>SUMMARY:</span><span> </span><span>AddressSanitizer:</span><span> </span><span>heap-use-after-free</span><span> </span><span>self_assign.cpp:19</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>in Holder::</span><span>operator</span><span><span>=</span><span>(</span></span><span>Holder</span><span> </span><span>const</span><span>&amp;)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>修法是 copy-and-swap：先拷贝出一个临时对象，再和它交换。这样自赋值天然安全，因为交换的是指针，不是被指向的数据。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>Holder</span><span>&amp;</span><span> </span><span>operator</span><span>=(</span><span>Holder</span><span><span> </span><span>other</span><span>) {</span></span><span>  // By value: the copy already exists.</span></div></div><div><div><div>2</div></div><div><span>    </span><span>swap</span><span>(</span><span>*</span><span>this</span><span>, </span><span>other</span><span>);</span><span>              // It dies at the end of the function.</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>*</span><span>this</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>friend</span><span> </span><span>void</span><span> </span><span>swap</span><span>(</span><span>Holder</span><span>&amp;</span><span><span> </span><span>a</span><span>, </span></span><span>Holder</span><span>&amp;</span><span><span> </span><span>b</span><span>) </span></span><span>noexcept</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>using</span><span> std::</span><span>swap</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>swap</span><span>(</span><span>a</span><span>.</span><span>payload_</span><span>, </span><span>b</span><span>.</span><span>payload_</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3. 析构函数抛异常，程序直接 <code>terminate</code><a href="#3-析构函数抛异常程序直接-terminate"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Bad</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>~Bad</span><span>() { </span><span>throw</span><span> std::</span><span>runtime_error</span><span>(</span><span>"boom in destructor"</span><span>); }</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>dtor_throw.cpp:</span><span> </span><span>In</span><span> </span><span>destructor</span><span> </span><span>'Bad::~Bad()':</span></div></div><div><div><div>2</div></div><div><span>dtor_throw.cpp:6:12:</span><span> </span><span>warning:</span><span> </span><span>'throw'</span><span> </span><span>will</span><span> </span><span>always</span><span> </span><span>call</span><span> </span><span>'terminate'</span><span> [-Wterminate]</span></div></div><div><div><div>3</div></div><div><span>    </span><span>6</span><span> |   </span><span>~Bad</span><span>() { </span><span>throw</span><span> </span><span>std::runtime_error</span><span>(</span><span>"boom in destructor"</span><span>); }</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>|            </span><span>^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</span></div></div><div><div><div>5</div></div><div><span>dtor_throw.cpp:6:12:</span><span> </span><span>note:</span><span> </span><span>in</span><span> </span><span>C++11</span><span> </span><span>destructors</span><span> </span><span>default</span><span> </span><span>to</span><span> </span><span>'noexcept'</span></div></div><div><div><div>6</div></div><div><span>terminate</span><span> </span><span>called</span><span> </span><span>after</span><span> </span><span>throwing</span><span> </span><span>an</span><span> </span><span>instance</span><span> </span><span>of</span><span> </span><span>'std::runtime_error'</span></div></div><div><div><div>7</div></div><div><span>  </span><span>what</span><span>():  boom in destructor</span></div></div><div><div><div>8</div></div><div><span>[exit: 134]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++11 起析构函数默认是 <code>noexcept</code> 的，所以在析构函数里抛异常会直接 <code>std::terminate</code>，<code>try</code> / <code>catch</code> 都拦不住。如果你真的需要让析构失败可见，正确的做法是提供一个显式的 <code>close()</code> 方法，把错误返回给调用者，析构函数只做”尽力而为”的清理。</p></section><section><h3>4. 构造函数里管理多个资源<a href="#4-构造函数里管理多个资源"><span>#</span></a></h3><p>已经单独讲过，本质是”构造函数抛异常时，这个对象的析构函数不会运行”。用 RAII 成员，不要用裸指针成员。</p></section><section><h3>5. <code>new[]</code> 和 <code>delete</code> 不匹配<a href="#5-new-和-delete-不匹配"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>int*</span><span> </span><span>p</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>int</span><span>[</span><span>4</span><span>];</span></div></div><div><div><div>3</div></div><div><span>  </span><span>delete</span><span> </span><span>p</span><span>;</span><span>  // new[] must be paired with delete[].</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ERROR:</span><span> </span><span>AddressSanitizer:</span><span> </span><span>alloc-dealloc-mismatch</span><span> (operator </span><span>new</span><span> [] vs operator delete)</span></div></div><div><div><div>2</div></div><div><span>    </span><span>#1 ... in main array_mismatch.cpp:3</span></div></div><div><div><div>3</div></div><div><span>SUMMARY:</span><span> </span><span>AddressSanitizer:</span><span> </span><span>alloc-dealloc-mismatch</span><span> </span><span>array_mismatch.cpp:3</span><span> </span><span>in</span><span> </span><span>main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 <code>std::unique_ptr&lt;int[]&gt;</code> 或者 <code>std::vector&lt;int&gt;</code> 就不会写错。</p></section><section><h3>6. <code>shared_ptr</code> 的循环引用<a href="#6-shared_ptr-的循环引用"><span>#</span></a></h3><p>两个对象互相持有 <code>shared_ptr</code>，引用计数永远不归零，析构函数永远不执行。RAII 在这里帮不了你，因为它压根等不到析构被调用的那一刻。打破环用 <code>std::weak_ptr</code>。这一条我没有单独写实验，因为它太容易复现，而解释起来又太长。</p></section></section>
<section><h2>编译器还没有给我 <code>&lt;scope&gt;</code>，所以我自己写了一个<a href="#编译器还没有给我-scope所以我自己写了一个"><span>#</span></a></h2><p>C++23 的 <code>&lt;scope&gt;</code> 头文件提供了 <code>std::scope_exit</code> 和 <code>std::scope_fail</code>。我试了一下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>g++</span><span> </span><span>-std=c++23</span><span> </span><span>scope.cpp</span><span> </span><span>-o</span><span> </span><span>scope</span></div></div><div><div><div>2</div></div><div><span>scope.cpp:2:10:</span><span> </span><span>fatal</span><span> </span><span>error:</span><span> </span><span>scope:</span><span> </span><span>No</span><span> </span><span>such</span><span> </span><span>file</span><span> </span><span>or</span><span> </span><span>directory</span></div></div><div><div><div>3</div></div><div><span>    </span><span>2</span><span> | </span><span>#include &lt;scope&gt;</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>|          </span><span>^~~~~~~</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>libstdc++ 15.3 还没有实现它，所以现在只能自己写一个。核心也是析构函数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>typename</span><span> </span><span>F</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>class</span><span> </span><span>ScopeGuard</span><span> {</span></div></div><div><div><div>3</div></div><div><span> </span><span>public:</span></div></div><div><div><div>4</div></div><div><span>  </span><span>explicit</span><span> </span><span>ScopeGuard</span><span>(</span><span>F</span><span><span> </span><span>f</span><span>) : </span></span><span>f_</span><span><span>(</span><span>std</span><span>::</span></span><span>move</span><span>(</span><span>f</span><span>)) {}</span></div></div><div><div><div>5</div></div><div><span>  </span><span>ScopeGuard</span><span>(</span><span>ScopeGuard</span><span>&amp;&amp;</span><span><span> </span><span>other</span><span>) </span></span><span>noexcept</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>: </span><span>f_</span><span><span>(</span><span>std</span><span>::</span></span><span>move</span><span>(</span><span>other</span><span>.</span><span>f_</span><span>)), </span><span>active_</span><span><span>(</span><span>std</span><span>::</span></span><span>exchange</span><span>(</span><span>other</span><span>.</span><span>active_</span><span>, </span><span>false</span><span>)) {}</span></div></div><div><div><div>7</div></div><div><span>  </span><span>ScopeGuard</span><span>(</span><span>const</span><span> </span><span>ScopeGuard</span><span>&amp;</span><span>) </span><span>=</span><span> </span><span>delete</span><span>;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>ScopeGuard</span><span>&amp;</span><span> </span><span>operator</span><span>=(</span><span>const</span><span> </span><span>ScopeGuard</span><span>&amp;</span><span>) </span><span>=</span><span> </span><span>delete</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>~ScopeGuard</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span> (</span><span>active_</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>      </span><span>f_</span><span>();</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>  </span><span>void</span><span> </span><span>dismiss</span><span>() </span><span>noexcept</span><span> { </span><span>active_</span><span> </span><span>=</span><span> </span><span>false</span><span>; }</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span> </span><span>private:</span></div></div><div><div><div>18</div></div><div><span>  </span><span>F</span><span> </span><span>f_</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>bool</span><span> </span><span>active_</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>20</div></div><div><span>};</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>template</span><span> &lt;</span><span>typename</span><span> </span><span>F</span><span>&gt;</span></div></div><div><div><div>23</div></div><div><span>ScopeGuard</span><span>&lt;</span><span>F</span><span>&gt; </span><span>MakeScopeGuard</span><span>(</span><span>F</span><span><span> </span><span>f</span><span>) {</span></span></div></div><div><div><div>24</div></div><div><span>  </span><span>return</span><span> </span><span>ScopeGuard</span><span>&lt;</span><span>F</span><span><span>&gt;(</span><span>std</span><span>::</span></span><span>move</span><span>(</span><span>f</span><span>));</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>运行起来：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>body</span><span> </span><span>of</span><span> </span><span>block</span><span> </span><span>1</span></div></div><div><div><div>2</div></div><div><span>  </span><span>guard</span><span> </span><span>1</span><span> </span><span>fired</span></div></div><div><div><div>3</div></div><div><span>  </span><span>body</span><span> </span><span>of</span><span> </span><span>block</span><span> </span><span>2</span></div></div><div><div><div>4</div></div><div><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第二个 block 调用了 <code>dismiss()</code>，所以它的守卫没有触发。<code>ScopeGuard</code> 是 move-only 的，移动时把 <code>active_</code> 交接出去而不是拷贝，否则移动后临时对象的析构会重复触发一次 <code>f_()</code>。这个 <code>std::exchange</code> 和 <code>unique_ptr</code> 移动时清空自己的指针是同一个动作。</p></section>
<section><h2>一些 RAII 不擅长的地方<a href="#一些-raii-不擅长的地方"><span>#</span></a></h2><p>我不想把 RAII 说成万能的。它至少有四个边界：</p><ul>
<li><strong>析构顺序有依赖时，它是隐式的</strong>。成员声明顺序决定了谁活得长，这个约束藏在类的布局里，不看头文件看不出来。</li>
<li><strong>需要报告失败的清理，它做不到</strong>。<code>fclose</code> 会失败，<code>fsync</code> 会失败，但析构函数不能抛异常。这类资源需要显式的 <code>close()</code>。</li>
<li><strong>生命周期长于作用域的场景，它管不了</strong>。缓存、连接池、跨线程共享的对象，需要 <code>shared_ptr</code>、所有权转移，或者干脆把所有权交还给调用者。</li>
<li><strong>深层递归析构可能爆栈</strong>。用 <code>unique_ptr</code> 串起来的链表，析构时会一层层递归。我在 8 MiB 栈上实测：10 万个节点还能撑住，100 万个节点直接段错误（退出码 139）。这时候反而需要一个手写的迭代式清理。</li>
</ul><p>前三条是边界，第四条是陷阱。</p></section>
<section><h2>最后<a href="#最后"><span>#</span></a></h2><p>我把这套实验留下的最实际的结论写在这里：</p><ul>
<li>热路径上，RAII 和手工清理编译出的机器码逐条同构（55 对 55 条指令）。</li>
<li>异常路径上，它额外付出的是 8 条冷指令和一个 21 字节的只读异常表。</li>
<li>分配 + 释放的实测开销，<code>unique_ptr</code> 和裸指针在噪声内，<code>shared_ptr</code> 约 1.2x。</li>
<li>真正的风险不在性能，而在语义：浅拷贝、自赋值、析构函数抛异常、构造函数里的第二个资源。</li>
</ul><p>如果我在 2015 年重新写一遍现在手上的这些代码，我会做的最小改动只有一个：把所有 <code>goto cleanup</code> 和所有在函数末尾手写的 <code>free</code>，换成让编译器替我安排。省下的不是 keystroke，是那条你迟早会漏掉的失败路径。</p><p>现在我要去把项目里那几个 <code>goto cleanup</code> 删掉了。</p></section>
<section><h2>参考<a href="#参考"><span>#</span></a></h2><ul>
<li>C++ Core Guidelines: <a href="https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-raii" target="_blank">R.1 - Manage resources automatically using resource handles and RAII</a></li>
<li><a href="https://google.github.io/styleguide/cppguide.html" target="_blank">Google C++ Style Guide</a></li>
<li>Scott Meyers, <em>Effective C++</em>, Item 13: Use objects to manage resources</li>
<li>cppreference: <a href="https://en.cppreference.com/w/cpp/language/raii" target="_blank">RAII</a></li>
<li>本仓库的姊妹篇：<a href="/posts/cpp-exception/">从错误码到栈展开：我如何理解 C++ 异常</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/configuration-file-formats-for-ai-agents/</id>
      <title type="text">我把同一份 Agent 配置写了 4 遍: JSON、TOML、YAML 与配置文件的语法和选择</title>
      <published>2026-09-13T12:00:00.000Z</published>
      <updated>2026-09-13T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/configuration-file-formats-for-ai-agents/"/>
      <summary type="text">我把同一份包含 16 个叶子值的 Agent 配置分别写成 JSON、TOML、YAML 和 INI，实际解析并比较它们，再解释 JSONC、.env、XML、Schema、覆盖顺序、密钥管理与常见陷阱。</summary>
      <content type="html"><![CDATA[<p>AI Agent 已经开始替我读代码、调用工具、运行命令和修改文件，但它们似乎在配置格式上召开了一次没有邀请用户参加的会议。一个工具要 <code>settings.json</code>，另一个要 <code>config.toml</code>，CI 又塞来一份 YAML，项目规则偏偏写在 <code>AGENTS.md</code>。默认配置通常只能证明程序能启动，真正决定效率和安全边界的模型、权限、沙箱、工具、目录规则与环境变量，最后还是要我自己写。</p>
<p>所以我做了一个很小的配置实验室。我先设计一份包含 <strong>16 个叶子值</strong>的 Agent 配置，再把它等价地写成 JSON、TOML、YAML 和 INI，用真实解析器读回内存，比较它们到底表达了什么。4 份文件合计只有 71 行，却已经足够踩到注释、尾逗号、字符串推断、重复键、日期类型和数组表这些坑。配置文件大概就是这样一种东西: 看起来像便签，实际上更像驾驶舱接线板，少一个引号可能只关掉音乐，也可能打开网络和写权限（多少有点刺激）。</p>
<p>本文以 <a href="https://www.rfc-editor.org/rfc/rfc8259" target="_blank">JSON RFC 8259</a>、<a href="https://toml.io/en/v1.0.0" target="_blank">TOML 1.0.0</a>、<a href="https://yaml.org/spec/1.2.2/" target="_blank">YAML 1.2.2</a> 为主要语法依据。具体 Agent 工具的字段会持续变化，本文不会猜某个工具明天还认不认今天的字段，而是先把所有配置背后的数据模型讲清楚。</p>
<section><h2>配置文件不是配置本身<a href="#配置文件不是配置本身"><span>#</span></a></h2><p>我先区分 4 个经常混在一起的东西:</p><ol>
<li><strong>格式</strong>回答文本怎样变成数据，例如 JSON 的 <code>{}</code>、TOML 的 <code>[table]</code> 和 YAML 的缩进。</li>
<li><strong>Schema</strong> 回答数据应该长什么样，例如 <code>temperature</code> 必须是 <code>0</code> 到 <code>2</code> 之间的数字。</li>
<li><strong>语义</strong>回答字段会做什么，例如 <code>network = false</code> 到底禁止 Agent 联网，还是只禁止它启动的子进程联网。</li>
<li><strong>加载策略</strong>回答多份配置如何合并，例如命令行、项目配置和用户配置谁覆盖谁。</li>
</ol><p>语法正确，只说明 Parser 能读。下面这段 JSON 完全合法:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"temperature"</span><span>: </span><span>9000</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"sandbox_mode"</span><span>: </span><span>"banana"</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它在 Schema 层和业务语义层大概率都不合法。反过来，字段和值全写对了，放错文件路径也等于没有配置。Parser、Validator 和 Loader 是 3 个不同部件，不要指望其中一个替另外两个值夜班。</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>配置文本</p></span></div><div><span><p>Parser 解析语法</p></span></div><div><span><p>内存数据</p></span></div><div><span><p>Schema 验证结构</p></span></div><div><span><p>应用检查语义</p></span></div><div><span><p>多来源合并</p></span></div><div><span><p>Agent 实际行为</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>配置文本</p></span></div><div><span><p>Parser 解析语法</p></span></div><div><span><p>内存数据</p></span></div><div><span><p>Schema 验证结构</p></span></div><div><span><p>应用检查语义</p></span></div><div><span><p>多来源合并</p></span></div><div><span><p>Agent 实际行为</p></span></div>
</div></div></div><p>JSON、TOML 和 YAML 的表面差异很大，但日常配置最终大多落到同一棵树上:</p><ul>
<li><strong>Mapping</strong>: 键到值的映射，也叫 Object、Table、Map 或 Dictionary。</li>
<li><strong>Sequence</strong>: 有顺序的值列表，也叫 Array 或 List。</li>
<li><strong>Scalar</strong>: 字符串、数字、布尔值、空值、日期等单个值。</li>
</ul><p>理解这 3 类节点，比背下 3 张标点符号表有用得多。</p></section>
<section><h2>我先造一份 Agent 配置<a href="#我先造一份-agent-配置"><span>#</span></a></h2><p>我希望这份配置足够小，又能覆盖实际需求:</p><ul>
<li>选择模型和温度。</li>
<li>设置工作目录与包含规则。</li>
<li>控制读、写、网络 3 种权限。</li>
<li>注册 2 个 MCP Server，每个 Server 有命令和参数数组。</li>
</ul><p>它对应的概念结构如下:</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>root</span></div></div><div><div><div>2</div></div><div><span>├── model: string</span></div></div><div><div><div>3</div></div><div><span>├── temperature: number</span></div></div><div><div><div>4</div></div><div><span>├── dry_run: boolean</span></div></div><div><div><div>5</div></div><div><span>├── workspace: string</span></div></div><div><div><div>6</div></div><div><span>├── include: array&lt;string&gt;</span></div></div><div><div><div>7</div></div><div><span>├── permissions: object</span></div></div><div><div><div>8</div></div><div><span>│   ├── read: boolean</span></div></div><div><div><div>9</div></div><div><span>│   ├── write: boolean</span></div></div><div><div><div>10</div></div><div><span>│   └── network: boolean</span></div></div><div><div><div>11</div></div><div><span>└── mcp_servers: array&lt;object&gt;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>├── docs</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>│   ├── command: string</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>│   └── args: array&lt;string&gt;</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>└── repo</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>├── command: string</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>└── args: array&lt;string&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里一共有 16 个叶子值。根节点有对象、数组和标量，已经是一份很典型的 Agent 配置了。</p></section>
<section><h2>JSON: 标点很多，但歧义很少<a href="#json-标点很多但歧义很少"><span>#</span></a></h2><p>JSON 是最小的一门。它只有 6 类值:</p>

<table><thead><tr><th>类型</th><th>示例</th></tr></thead><tbody><tr><td>Object</td><td><code>{"read": true}</code></td></tr><tr><td>Array</td><td><code>["src", "tests"]</code></td></tr><tr><td>String</td><td><code>"reasoning-large"</code></td></tr><tr><td>Number</td><td><code>0.2</code></td></tr><tr><td>Boolean</td><td><code>true</code>、<code>false</code></td></tr><tr><td>Null</td><td><code>null</code></td></tr></tbody></table><p>同一份配置写成 JSON 是这样:</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"model"</span><span>: </span><span>"reasoning-large"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"temperature"</span><span>: </span><span>0.2</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"dry_run"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"workspace"</span><span>: </span><span>"/work/demo"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>"include"</span><span>: [</span><span>"src/**/*.ts"</span><span>, </span><span>"tests/**/*.ts"</span><span>],</span></div></div><div><div><div>7</div></div><div><span>  </span><span>"permissions"</span><span>: {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"read"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"write"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"network"</span><span>: </span><span>false</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>12</div></div><div><span>  </span><span>"mcp_servers"</span><span>: [</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>{</span><span>"name"</span><span>: </span><span>"docs"</span><span>, </span><span>"command"</span><span>: </span><span>"node"</span><span>, </span><span>"args"</span><span>: [</span><span>"server.js"</span><span>, </span><span>"--stdio"</span><span>]},</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>{</span><span>"name"</span><span>: </span><span>"repo"</span><span>, </span><span>"command"</span><span>: </span><span>"python"</span><span>, </span><span>"args"</span><span>: [</span><span>"repo.py"</span><span>]}</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这份文件是 <strong>16 行、396 Bytes</strong>。标点占了不少位置，但每个值的边界都很明确。</p><section><h3>Object、Array 与逗号<a href="#objectarray-与逗号"><span>#</span></a></h3><p>Object 用花括号，成员写成 <code>"key": value</code>。Array 用方括号，元素按顺序排列。成员或元素之间放逗号，最后一个后面不能放逗号:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"tools"</span><span>: [</span><span>"read"</span><span>, </span><span>"write"</span><span>]</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面不是标准 JSON:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"tools"</span><span>: [</span><span>"read"</span><span>, </span><span>"write"</span><span>],</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用 Python 3.13 的 <code>json</code> 模块解析一份带注释和尾逗号的文本，得到的是:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>JSONDecodeError: Expecting property name enclosed in double quotes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这两个限制常让手写配置变得烦，但也让生成器和不同语言的 Parser 更容易达成一致。</p></section><section><h3>Key 和 String 必须使用双引号<a href="#key-和-string-必须使用双引号"><span>#</span></a></h3><p>标准 JSON 的 Object Key 是 String，所以必须加双引号。String 本身也只能使用双引号:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>"model"</span><span>: </span><span>"reasoning-large"</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面 3 种都不是标准 JSON:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>{</span><span>model</span><span>: </span></span><span>"reasoning-large"</span><span>}   </span><span>// 裸 key</span></div></div><div><div><div>2</div></div><div><span>{</span><span>'model'</span><span>: </span><span>'reasoning-large'</span><span>} </span><span>// 单引号</span></div></div><div><div><div>3</div></div><div><span>{</span><span>"model"</span><span>: </span><span>undefined</span><span>}        </span><span>// JSON 没有 undefined</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它们可能是合法 JavaScript，却不是合法 JSON。JSON 源自 JavaScript 对象字面量，但今天二者不是同一种语言。</p></section><section><h3>转义: 反斜杠会再收一次税<a href="#转义-反斜杠会再收一次税"><span>#</span></a></h3><p>JSON String 中常用转义包括:</p>

<table><thead><tr><th>写法</th><th>值中的字符</th></tr></thead><tbody><tr><td><code>\"</code></td><td>双引号</td></tr><tr><td><code>\\</code></td><td>反斜杠</td></tr><tr><td><code>\n</code></td><td>换行</td></tr><tr><td><code>\t</code></td><td>制表符</td></tr><tr><td><code>\u4e2d</code></td><td>Unicode 字符 <code>中</code></td></tr></tbody></table><p>Windows 路径和正则表达式最容易暴露双层解析:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"workspace"</span><span>: </span><span>"C:</span><span>\\</span><span>Users</span><span>\\</span><span>me</span><span>\\</span><span>project"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"pattern"</span><span>: </span><span>"^src</span><span>\\</span><span>/.*</span><span>\\</span><span>.ts$"</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文件里写 <code>\\</code>，Parser 读出来才是一个 <code>\</code>。如果这段字符串随后还要进入正则 Parser，它又会解释一遍。每套 Parser 都像机场安检，再过一层就再开一次箱。</p></section><section><h3>Number 没有 <code>int64</code> 这回事<a href="#number-没有-int64-这回事"><span>#</span></a></h3><p>JSON Grammar 允许负数、小数和指数形式:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"retries"</span><span>: </span><span>3</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"temperature"</span><span>: </span><span>0.2</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"threshold"</span><span>: </span><span>1e-6</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>但 JSON 只定义 Number 的文本语法，不承诺宿主语言使用哪种数值类型。JavaScript 通常把它读成 IEEE 754 双精度 Number，因此超过安全整数范围的 ID 可能丢精度:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"job_id"</span><span>: </span><span>9007199254740993</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>跨语言传递大整数、雪花 ID 或精确 Decimal 时，我会写成字符串，再由应用按明确类型解析:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"job_id"</span><span>: </span><span>"9007199254740993"</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3><code>null</code>、缺失与空字符串不同<a href="#null缺失与空字符串不同"><span>#</span></a></h3><p>这 3 份配置表达不同状态:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>"proxy"</span><span>: </span><span>null</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>"proxy"</span><span>: </span><span>""</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它们通常分别表示未提供、显式置空、提供了空字符串。具体工具可能赋予别的语义，但格式本身没有把三者合并。配置合并时这一区别尤其重要: <code>null</code> 有时会删除继承值，有时会覆盖成空值，有时直接不合法。只能查工具文档或 Schema，不能靠气氛判断。</p></section><section><h3>重复 Key 和成员顺序<a href="#重复-key-和成员顺序"><span>#</span></a></h3><p>RFC 8259 说 Object 中的名称<strong>应该唯一</strong>，因为重复时不同实现可能保留第一个、保留最后一个、全部保留或报错:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"network"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"network"</span><span>: </span><span>true</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>人类可能从上往下读成 “最后一次覆盖”，安全审计却不该建立在 “可能” 上。不要写重复 Key。</p><p>JSON Object 在数据模型上是无序映射。Parser 经常保留源码顺序，但业务逻辑不应该依赖顺序。需要顺序时使用 Array。</p></section><section><h3>JSON 为什么没有注释<a href="#json-为什么没有注释"><span>#</span></a></h3><p>标准 JSON 没有注释。对机器生成、API 交换和锁文件来说，这是优点；对手写的长期配置来说，这很痛苦。常见解决方案有 3 种:</p><ol>
<li>工具明确支持 <strong>JSONC</strong>，也就是带 <code>//</code> 或 <code>/* ... */</code> 注释的 JSON 方言。</li>
<li>用 <code>$comment</code>、<code>description</code> 之类的普通字段，前提是 Schema 允许未知字段。</li>
<li>攬弃 JSON，改用工具支持的 TOML 或 YAML。</li>
</ol><div><div><div></div><div>Warning</div></div><div><p><code>.json</code> 扩展名不等于支持 JSONC。<code>tsconfig.json</code> 等工具接受注释，是对应 Parser 的扩展能力，不是 JSON 标准突然心软了。必须以消费该文件的工具为准。</p></div></div><p>JSONC 也不是 JSON5。JSON5 还可能允许单引号、裸 Key、<code>Infinity</code> 等更多 JavaScript 风格语法。方言名称看起来只差一个字符，能力边界却不同。</p></section><section><h3>我什么时候选 JSON<a href="#我什么时候选-json"><span>#</span></a></h3><p>我会把 JSON 用在这些地方:</p><ul>
<li>工具或 API 强制要求 JSON。</li>
<li>文件主要由程序生成，而不是长期手写。</li>
<li>需要成熟的 JSON Schema、编辑器补全和跨语言互操作。</li>
<li>希望语法保持很小，不接受隐式类型或复杂别名。</li>
</ul><p>对于一份需要大量解释的 Agent 权限配置，纯 JSON 不是我最喜欢的手写格式。对一份 MCP 进程注册表或机器生成的状态文件，它通常很合适。</p></section></section>
<section><h2>TOML: 配置文件开始像配置文件了<a href="#toml-配置文件开始像配置文件了"><span>#</span></a></h2><p>TOML 的目标就是映射到 Hash Table，并让含义尽量明显。它增加了注释、Table、日期时间和更丰富的 String，同时仍然保持相当严格。</p><p>同一份配置写成 TOML:</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span> = </span><span>"reasoning-large"</span></div></div><div><div><div>2</div></div><div><span>temperature</span><span> = </span><span>0.2</span></div></div><div><div><div>3</div></div><div><span>dry_run</span><span> = </span><span>false</span></div></div><div><div><div>4</div></div><div><span>workspace</span><span> = </span><span>"/work/demo"</span></div></div><div><div><div>5</div></div><div><span>include</span><span> = [</span><span>"src/**/*.ts"</span><span>, </span><span>"tests/**/*.ts"</span><span>]</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>[</span><span>permissions</span><span>]</span></div></div><div><div><div>8</div></div><div><span>read</span><span> = </span><span>true</span></div></div><div><div><div>9</div></div><div><span>write</span><span> = </span><span>false</span></div></div><div><div><div>10</div></div><div><span>network</span><span> = </span><span>false</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>[[</span><span>mcp_servers</span><span>]]</span></div></div><div><div><div>13</div></div><div><span>name</span><span> = </span><span>"docs"</span></div></div><div><div><div>14</div></div><div><span>command</span><span> = </span><span>"node"</span></div></div><div><div><div>15</div></div><div><span>args</span><span> = [</span><span>"server.js"</span><span>, </span><span>"--stdio"</span><span>]</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>[[</span><span>mcp_servers</span><span>]]</span></div></div><div><div><div>18</div></div><div><span>name</span><span> = </span><span>"repo"</span></div></div><div><div><div>19</div></div><div><span>command</span><span> = </span><span>"python"</span></div></div><div><div><div>20</div></div><div><span>args</span><span> = [</span><span>"repo.py"</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这份文件是 <strong>20 行、334 Bytes</strong>。它比 JSON 多 4 行，却少了 62 Bytes，主要因为不再重复花括号和大量双引号。</p><section><h3>Key/Value Pair<a href="#keyvalue-pair"><span>#</span></a></h3><p>TOML 最基本的语句是:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>key</span><span> = v</span><span>alue</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>等号左右的空格不影响含义。Bare Key 只能包含 ASCII 字母、数字、下划线和连字符:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span> = </span><span>"reasoning-large"</span></div></div><div><div><div>2</div></div><div><span>max_tokens</span><span> = </span><span>8192</span></div></div><div><div><div>3</div></div><div><span>sandbox-mode</span><span> = </span><span>"workspace-write"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>特殊 Key 要加引号:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"</span><span>server name</span><span>"</span><span> = </span><span>"docs"</span></div></div><div><div><div>2</div></div><div><span>"</span><span>127.0.0.1</span><span>"</span><span> = </span><span>"local"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>TOML 区分大小写，所以 <code>model</code> 和 <code>Model</code> 是两个 Key。为了少给自己制造一种区别，我通常统一使用 <code>snake_case</code>。</p></section><section><h3>注释<a href="#注释"><span>#</span></a></h3><p><code>#</code> 到行尾是注释，String 内部的 <code>#</code> 仍然是普通字符:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span> = </span><span>"reasoning-large"</span><span> </span><span># 用于代码任务</span></div></div><div><div><div>2</div></div><div><span>color</span><span> = </span><span>"#ff8800"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注释是 TOML 很适合手写 Agent 配置的原因。权限字段旁边可以解释为什么关闭，而不是期待 6 个月后的自己心灵感应。</p></section><section><h3>4 种 String<a href="#4-种-string"><span>#</span></a></h3><p>TOML 有 Basic、Literal 以及它们各自的多行形式。</p><p><strong>Basic String</strong> 使用双引号并处理转义:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>message</span><span> = </span><span>"first line</span><span>\n</span><span>second line"</span></div></div><div><div><div>2</div></div><div><span>path</span><span> = </span><span>"C:</span><span>\\</span><span>Users</span><span>\\</span><span>me</span><span>\\</span><span>project"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>Literal String</strong> 使用单引号，不处理反斜杠转义，很适合 Windows 路径和正则:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>path</span><span> = </span><span>'C:\Users\me\project'</span></div></div><div><div><div>2</div></div><div><span>pattern</span><span> = </span><span>'^src/.*\.ts$'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>单引号不是 Shell 的单引号，只是 TOML Literal String 的定界符。</p><p><strong>Multiline Basic String</strong> 使用 3 个双引号:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>instructions</span><span> = </span><span>"""</span></div></div><div><div><div>2</div></div><div><span>Read the repository rules first.</span></div></div><div><div><div>3</div></div><div><span>Run the narrowest relevant check.</span></div></div><div><div><div>4</div></div><div><span>Do not print secrets.</span></div></div><div><div><div>5</div></div><div><span>"""</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>Multiline Literal String</strong> 使用 3 个单引号，内容中的反斜杠仍然保持原样:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>windows_example</span><span> = </span><span>'''</span></div></div><div><div><div>2</div></div><div><span>C:\Users\me\project</span></div></div><div><div><div>3</div></div><div><span>'''</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>多行 String 的首个紧邻换行会被裁掉，结尾换行是否保留要看定界符位置和具体写法。放长 Prompt 时，我会用一个实际 Parser 打印 <code>repr()</code>，不靠目测数换行。</p></section><section><h3>Integer、Float 与可读分隔符<a href="#integerfloat-与可读分隔符"><span>#</span></a></h3><p>TOML 区分 Integer 和 Float，并允许下划线分组数字:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>max_tokens</span><span> = </span><span>128_000</span></div></div><div><div><div>2</div></div><div><span>ratio</span><span> = </span><span>0.25</span></div></div><div><div><div>3</div></div><div><span>threshold</span><span> = </span><span>1e-6</span></div></div><div><div><div>4</div></div><div><span>hex_mask</span><span> = </span><span>0xFF_A0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>布尔值只能小写:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>enabled</span><span> = </span><span>true</span></div></div><div><div><div>2</div></div><div><span>dry_run</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>True</code>、<code>FALSE</code> 都不合法。严格其实挺好，少一种写法就少一种会议议题。</p></section><section><h3>日期与时间是原生类型<a href="#日期与时间是原生类型"><span>#</span></a></h3><p>TOML 比 JSON 多出 Offset Date-Time、Local Date-Time、Local Date 和 Local Time:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>expires_at</span><span> = </span><span>2026-09-13T08:30:00Z</span></div></div><div><div><div>2</div></div><div><span>maintenance_date</span><span> = </span><span>2026-09-13</span></div></div><div><div><div>3</div></div><div><span>quiet_time</span><span> = </span><span>08:30:00</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用 Python 3.13 标准库 <code>tomllib</code> 解析后，3 个值分别变成:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>expires_at       datetime  2026-09-13 08:30:00+00:00</span></div></div><div><div><div>2</div></div><div><span>maintenance_date date      2026-09-13</span></div></div><div><div><div>3</div></div><div><span>quiet_time       time      08:30:00</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是字符串格式化糖。Parser 确实产生了不同宿主类型。需要原样保留时间文本时应该加引号。</p></section><section><h3>Array 与 Inline Table<a href="#array-与-inline-table"><span>#</span></a></h3><p>普通 Array:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include</span><span> = [</span><span>"src/**/*.ts"</span><span>, </span><span>"tests/**/*.ts"</span><span>]</span></div></div><div><div><div>2</div></div><div><span>ports</span><span> = [</span><span>3000</span><span>, </span><span>3001</span><span>, </span><span>3002</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>TOML 1.0 Array 允许混合不同类型，但配置中的同类列表通常更容易验证和消费。Array 可以跨行，并允许最后一个元素后带逗号:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>allowed_tools</span><span> = [</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"read"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"search"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"browser"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Inline Table 适合很小的对象:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>limits</span><span> = { </span><span>requests</span><span> = </span><span>10</span><span>, </span><span>timeout_seconds</span><span> = </span><span>30</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>TOML 1.0 的 Inline Table 要放在一行内，而且最后一项后不能有尾逗号。对象一旦开始增长，我就把它改成正式 Table。</p></section><section><h3>Table 与 Dotted Key<a href="#table-与-dotted-key"><span>#</span></a></h3><p>Table Header 建立嵌套对象:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>permissions</span><span>]</span></div></div><div><div><div>2</div></div><div><span>read</span><span> = </span><span>true</span></div></div><div><div><div>3</div></div><div><span>write</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它等价于:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"permissions"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"read"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"write"</span><span>: </span><span>false</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Dotted Key 可以写同一结构:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>permissions.read</span><span> = </span><span>true</span></div></div><div><div><div>2</div></div><div><span>permissions.write</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再深一层也可以:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mcp.docs.command</span><span> = </span><span>"node"</span></div></div><div><div><div>2</div></div><div><span>mcp.docs.enabled</span><span> = </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>但不要同时用多种写法重复定义同一个 Key。TOML 禁止重复 Key，我实际把下面文本交给 <code>tomllib</code>:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>key</span><span> = </span><span>1</span></div></div><div><div><div>2</div></div><div><span>key</span><span> = </span><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结果是:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>TOMLDecodeError: Cannot overwrite a value (at line 2, column 8)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这比 JSON 的 “各 Parser 自行发挥” 更安全。</p></section><section><h3>Array of Tables: MCP Server 的关键语法<a href="#array-of-tables-mcp-server-的关键语法"><span>#</span></a></h3><p><code>[[mcp_servers]]</code> 每出现一次，就向 <code>mcp_servers</code> Array 追加一个 Table:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[[</span><span>mcp_servers</span><span>]]</span></div></div><div><div><div>2</div></div><div><span>name</span><span> = </span><span>"docs"</span></div></div><div><div><div>3</div></div><div><span>command</span><span> = </span><span>"node"</span></div></div><div><div><div>4</div></div><div><span>args</span><span> = [</span><span>"server.js"</span><span>, </span><span>"--stdio"</span><span>]</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[[</span><span>mcp_servers</span><span>]]</span></div></div><div><div><div>7</div></div><div><span>name</span><span> = </span><span>"repo"</span></div></div><div><div><div>8</div></div><div><span>command</span><span> = </span><span>"python"</span></div></div><div><div><div>9</div></div><div><span>args</span><span> = [</span><span>"repo.py"</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它对应 JSON 的:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"mcp_servers"</span><span>: [</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>4</div></div><div><span>      </span><span>"name"</span><span>: </span><span>"docs"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"command"</span><span>: </span><span>"node"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"args"</span><span>: [</span><span>"server.js"</span><span>, </span><span>"--stdio"</span><span>]</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>9</div></div><div><span>      </span><span>"name"</span><span>: </span><span>"repo"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>      </span><span>"command"</span><span>: </span><span>"python"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>      </span><span>"args"</span><span>: [</span><span>"repo.py"</span><span>]</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个方括号 <code>[x]</code> 是 Table，两个方括号 <code>[[x]]</code> 是 Array of Tables。少写一层方括号，不是少了一点装饰，而是换了数据类型。</p></section><section><h3>Table 之后的归属陷阱<a href="#table-之后的归属陷阱"><span>#</span></a></h3><p>TOML 依赖 Header 改变后续 Key 的归属:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span> = </span><span>"reasoning-large"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>[</span><span>permissions</span><span>]</span></div></div><div><div><div>4</div></div><div><span>read</span><span> = </span><span>true</span></div></div><div><div><div>5</div></div><div><span>write</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>model</code> 在根 Table，<code>read</code> 和 <code>write</code> 在 <code>permissions</code>。如果把 <code>model</code> 放到 Header 后面:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>permissions</span><span>]</span></div></div><div><div><div>2</div></div><div><span>read</span><span> = </span><span>true</span></div></div><div><div><div>3</div></div><div><span>model</span><span> = </span><span>"reasoning-large"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>那么 <code>model</code> 也属于 <code>permissions</code>，不是回到根节点。缩进不改变归属，最近的 Table Header 才改变。TOML 看起来平坦，但 Parser 记得自己当前站在哪个房间。</p></section><section><h3>TOML 没有 <code>null</code><a href="#toml-没有-null"><span>#</span></a></h3><p>TOML 没有通用 Null。可选字段通常直接省略，或者由应用定义一个明确的哨兵字符串。不要擅自写:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>proxy</span><span> = n</span><span>ull</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它不合法。省略和显式清除必须由工具的合并协议另行设计，这也是 TOML 在多层覆盖场景中的一个真实限制。</p></section><section><h3>我什么时候选 TOML<a href="#我什么时候选-toml"><span>#</span></a></h3><p>如果工具允许我自己选择格式，我通常把 TOML 用于:</p><ul>
<li>人类长期维护的应用或 Agent 设置。</li>
<li>结构以 Key/Value 和中等深度 Table 为主。</li>
<li>需要注释、明确类型和严格重复键检查。</li>
<li>包含日期时间、路径、正则或多行 Prompt。</li>
</ul><p>当嵌套非常深、对象数组很多时，TOML 的 Header 会让我在文件里频繁确认当前位置。这时 YAML 或 JSON 可能更直观。</p></section></section>
<section><h2>YAML: 最少的标点，最大的语法表面积<a href="#yaml-最少的标点最大的语法表面积"><span>#</span></a></h2><p>YAML 用缩进表达层次，用 <code>key: value</code> 表达 Mapping，用 <code>- value</code> 表达 Sequence。它看起来最接近人写的提纲，也因此最容易让人忘记它是一门完整的数据序列化语言，而不是 “JSON 去掉括号”。</p><p>同一份配置写成 YAML:</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span>: </span><span>reasoning-large</span></div></div><div><div><div>2</div></div><div><span>temperature</span><span>: </span><span>0.2</span></div></div><div><div><div>3</div></div><div><span>dry_run</span><span>: </span><span>false</span></div></div><div><div><div>4</div></div><div><span>workspace</span><span>: </span><span>/work/demo</span></div></div><div><div><div>5</div></div><div><span>include</span><span>:</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>- </span><span>src/**/*.ts</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>- </span><span>tests/**/*.ts</span></div></div><div><div><div>8</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>9</div></div><div><span>  </span><span>read</span><span>: </span><span>true</span></div></div><div><div><div>10</div></div><div><span>  </span><span>write</span><span>: </span><span>false</span></div></div><div><div><div>11</div></div><div><span>  </span><span>network</span><span>: </span><span>false</span></div></div><div><div><div>12</div></div><div><span>mcp_servers</span><span>:</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>- </span><span>name</span><span>: </span><span>docs</span></div></div><div><div><div>14</div></div><div><span>    </span><span>command</span><span>: </span><span>node</span></div></div><div><div><div>15</div></div><div><span>    </span><span>args</span><span>: [</span><span>server.js</span><span>, </span><span>--stdio</span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>- </span><span>name</span><span>: </span><span>repo</span></div></div><div><div><div>17</div></div><div><span>    </span><span>command</span><span>: </span><span>python</span></div></div><div><div><div>18</div></div><div><span>    </span><span>args</span><span>: [</span><span>repo.py</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这份文件是 <strong>18 行、310 Bytes</strong>，比 JSON 少 86 Bytes，缩短约 <strong>21.7%</strong>。视觉噪声确实最低，但这 86 Bytes 不是免费午餐，它把一部分结构责任交给了空格和类型推断。</p><section><h3>Mapping、Sequence 与缩进<a href="#mappingsequence-与缩进"><span>#</span></a></h3><p>Mapping 是 <code>key: value</code>:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span>: </span><span>reasoning-large</span></div></div><div><div><div>2</div></div><div><span>dry_run</span><span>: </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Sequence 的每项以 <code>- </code> 开头:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tools</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>- </span><span>read</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>- </span><span>search</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>- </span><span>browser</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Sequence Item 也可以是 Mapping:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>servers</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>- </span><span>name</span><span>: </span><span>docs</span></div></div><div><div><div>3</div></div><div><span>    </span><span>command</span><span>: </span><span>node</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>- </span><span>name</span><span>: </span><span>repo</span></div></div><div><div><div>5</div></div><div><span>    </span><span>command</span><span>: </span><span>python</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>缩进表示父子关系。YAML 规范要求缩进使用空格，不能用 Tab:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>read</span><span>: </span><span>true</span></div></div><div><div><div>3</div></div><div><span>  </span><span>write</span><span>: </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>read</code> 和 <code>write</code> 比 <code>permissions</code> 多缩进 2 个空格，所以属于它。2 个空格不是 YAML 的强制宽度，只是一种最常见约定；同一级必须保持一致。</p><p>下面的视觉差异很小，数据结构却不同:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 一个对象，里面有两个字段</span></div></div><div><div><div>2</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>read</span><span>: </span><span>true</span></div></div><div><div><div>4</div></div><div><span>  </span><span>write</span><span>: </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 两个对象组成的数组</span></div></div><div><div><div>2</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>- </span><span>read</span><span>: </span><span>true</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>- </span><span>write</span><span>: </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第 2 份通常不是权限配置想要的结构。横杠不是 Bullet 的装饰，它创建了 Array Item。</p></section><section><h3>Block Style 与 Flow Style<a href="#block-style-与-flow-style"><span>#</span></a></h3><p>YAML 也支持类似 JSON 的 Flow Style:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include</span><span>: [</span><span>src/**/*.ts</span><span>, </span><span>tests/**/*.ts</span><span>]</span></div></div><div><div><div>2</div></div><div><span>permissions</span><span>: {</span><span>read</span><span>: </span><span>true</span><span>, </span><span>write</span><span>: </span><span>false</span><span>, </span><span>network</span><span>: </span><span>false</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这和 Block Style 表达同类数据。我的规则很简单:</p><ul>
<li>小型标量数组可以用 Flow Style。</li>
<li>嵌套对象和对象数组使用 Block Style。</li>
<li>不为了省 2 行把整份 YAML 压成括号迷宫。</li>
</ul><p>YAML 1.2 的目标之一是让 JSON 成为它的严格子集。因此，一份标准 JSON 文本通常也是合法 YAML 1.2 文本。不过，工具实际使用的 YAML Parser、Schema 和扩展版本仍可能不同，不能由这个包含关系推断所有实现行为一致。</p></section><section><h3>Plain、单引号与双引号 Scalar<a href="#plain单引号与双引号-scalar"><span>#</span></a></h3><p>不加引号的是 Plain Scalar:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span>: </span><span>reasoning-large</span></div></div><div><div><div>2</div></div><div><span>workspace</span><span>: </span><span>/work/demo</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>单引号主要按字面保留内容，内部单引号写两次:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pattern</span><span>: </span><span>'^src/.*\.ts$'</span></div></div><div><div><div>2</div></div><div><span>message</span><span>: </span><span>'It</span><span>''</span><span>s ready'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>双引号会处理 <code>\n</code>、<code>\t</code>、<code>\uXXXX</code> 等转义:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>message</span><span>: </span><span>"first line</span><span>\n</span><span>second line"</span></div></div><div><div><div>2</div></div><div><span>path</span><span>: </span><span>"C:</span><span>\\</span><span>Users</span><span>\\</span><span>me</span><span>\\</span><span>project"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>YAML 的 Plain Scalar 方便，但某些开头字符和 <code>: </code>、<code> #</code> 组合具有结构意义。遇到 URL、时间、通配符、正则、冒号后空格或看起来像布尔值和数字的文本，我倾向于加引号:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>url</span><span>: </span><span>"https://example.com/api"</span></div></div><div><div><div>2</div></div><div><span>pattern</span><span>: </span><span>"*.ts"</span></div></div><div><div><div>3</div></div><div><span>answer</span><span>: </span><span>"null"</span></div></div><div><div><div>4</div></div><div><span>version</span><span>: </span><span>"1.0"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的引号不是为了让文件看起来正式，而是为了固定类型和边界。</p></section><section><h3>隐式类型: YAML 最著名的脚趾捕捉器<a href="#隐式类型-yaml-最著名的脚趾捕捉器"><span>#</span></a></h3><p>YAML 会根据 Schema 解析 Plain Scalar。YAML 1.1 中 <code>yes</code>、<code>no</code>、<code>on</code>、<code>off</code> 经常被解析成 Boolean；YAML 1.2 Core Schema 把 Boolean 收窄为大小写组合的 <code>true</code> 和 <code>false</code>。问题是现实工具不一定使用同一规范版本或同一 Schema。</p><p>我用 <code>yq 4.53.2</code> 实际解析了 6 个值:</p>

<table><thead><tr><th>源文本</th><th>解析后的值</th><th>类型</th></tr></thead><tbody><tr><td><code>on</code></td><td><code>"on"</code></td><td>String</td></tr><tr><td><code>yes</code></td><td><code>"yes"</code></td><td>String</td></tr><tr><td><code>null</code></td><td><code>null</code></td><td>Null</td></tr><tr><td><code>2026-09-13</code></td><td><code>"2026-09-13"</code></td><td>String</td></tr><tr><td><code>0123</code></td><td><code>123</code></td><td>Integer</td></tr><tr><td><code>1e3</code></td><td><code>1000</code></td><td>Integer</td></tr></tbody></table><p>这组结果描述的是本次 <code>yq</code> 实验，不是宇宙中所有 YAML Parser 的共同誓言。比如某些 Parser 会把日期构造成 Date 对象，旧版 YAML 1.1 Parser 可能把 <code>on</code> 读成 <code>true</code>。</p><p>跨工具配置里，我会遵循 3 条保守规则:</p><ol>
<li>Boolean 只写 <code>true</code> 和 <code>false</code>。</li>
<li>Null 只写 <code>null</code>，需要字符串时写 <code>"null"</code>。</li>
<li>版本号、ID、端口前导零、日期和类似科学计数法的文本都加引号。</li>
</ol></section><section><h3>Null 有很多写法，但不要炫技<a href="#null-有很多写法但不要炫技"><span>#</span></a></h3><p>YAML Core Schema 可以识别 <code>null</code>、<code>Null</code>、<code>NULL</code> 和 <code>~</code>。空 Value 也可能解析为空节点:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>proxy</span><span>: </span><span>null</span></div></div><div><div><div>2</div></div><div><span>cache</span><span>: </span><span>~</span></div></div><div><div><div>3</div></div><div><span>optional</span><span>:</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它们是否在应用合并时表示 “删除继承值”，仍由应用定义。我只写小写 <code>null</code>，因为清楚比展示语言掌握程度重要。</p></section><section><h3>多行文本: <code>|</code> 与 <code>&gt;</code><a href="#多行文本--与-"><span>#</span></a></h3><p>Agent 配置经常包含 Prompt 或 Instructions，YAML 的 Block Scalar 很方便。</p><p>Literal Style <code>|</code> 保留换行:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>instructions</span><span>: </span><span>|</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>Read repository rules first.</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Run the narrowest relevant check.</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Do not print secrets.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>解析结果近似:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Read repository rules first.\nRun the narrowest relevant check.\nDo not print secrets.\n</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Folded Style <code>&gt;</code> 会把多数普通换行折叠为空格:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>instructions</span><span>: </span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>Read repository rules first.</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Run the narrowest relevant check.</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Do not print secrets.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>解析结果近似:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Read repository rules first. Run the narrowest relevant check. Do not print secrets.\n</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>末尾 Chomping Indicator 控制结尾换行:</p>

<table><thead><tr><th>写法</th><th>末尾行为</th></tr></thead><tbody><tr><td><code>|</code> / <code>&gt;</code></td><td>通常保留 1 个结尾换行</td></tr><tr><td><code>|-</code> / <code>&gt;-</code></td><td>删除结尾换行</td></tr><tr><td><code>|+</code> / <code>&gt;+</code></td><td>保留所有结尾空行</td></tr></tbody></table><p>Prompt 对空格和换行敏感时，<code>|</code> 和 <code>&gt;</code> 不是排版选择，而是数据选择。我通常用 <code>|-</code> 表示 “保留内部行，不额外添加末尾换行”。</p></section><section><h3>Anchor 与 Alias<a href="#anchor-与-alias"><span>#</span></a></h3><p>YAML 可以用 Anchor <code>&amp;name</code> 标记节点，再用 Alias <code>*name</code> 引用同一节点:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>safe_permissions</span><span>: </span><span>&amp;</span><span>safe</span></div></div><div><div><div>2</div></div><div><span>  </span><span>read</span><span>: </span><span>true</span></div></div><div><div><div>3</div></div><div><span>  </span><span>write</span><span>: </span><span>false</span></div></div><div><div><div>4</div></div><div><span>  </span><span>network</span><span>: </span><span>false</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>agents</span><span>:</span></div></div><div><div><div>7</div></div><div><span>  </span><span>reviewer</span><span>:</span></div></div><div><div><div>8</div></div><div><span>    </span><span>permissions</span><span>: </span><span>*</span><span>safe</span></div></div><div><div><div>9</div></div><div><span>  </span><span>researcher</span><span>:</span></div></div><div><div><div>10</div></div><div><span>    </span><span>permissions</span><span>: </span><span>*</span><span>safe</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这能减少重复，但也增加了追踪成本。更微妙的是，常见的 Merge Key <code>&lt;&lt;</code> 并不属于 YAML 1.2 Core Schema 的普通能力，很多工具把它作为额外约定支持:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>researcher</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>&lt;&lt;</span><span>: </span><span>*</span><span>defaults</span></div></div><div><div><div>3</div></div><div><span>  </span><span>network</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>除非目标工具明确支持，我不会在可移植 Agent 配置里依赖 Merge Key。几十行重复配置可能不优雅，但一次静默忽略的权限覆盖更不优雅。</p><p>Alias 还可能让反序列化后的数据成为共享引用，而不是两个完全独立的副本。应用若修改其中一个节点，另一个是否跟着变化取决于加载器和后续转换。配置只是静态输入时影响不大，直接在内存中变更解析结果时要留意。</p></section><section><h3>Tag 与反序列化安全<a href="#tag-与反序列化安全"><span>#</span></a></h3><p>YAML 支持显式 Tag，用来说明节点类型:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>port</span><span>: </span><span>!!int</span><span> </span><span>"4321"</span></div></div><div><div><div>2</div></div><div><span>label</span><span>: </span><span>!!str</span><span> </span><span>4321</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>某些语言生态还支持构造本地对象的自定义 Tag。历史上，不安全加载任意 YAML 可能触发对象构造甚至代码执行。处理不可信 YAML 时，要使用目标库的 Safe Loader，并限制 Alias 数量、文档大小和嵌套深度。</p><p>这不是 YAML 独有的问题。任何支持扩展类型、引用或对象构造的反序列化格式都需要资源与类型边界。只是 YAML 的友好外观很容易让人忘记 Parser 后面可能站着一台对象工厂。</p></section><section><h3>多文档 Stream<a href="#多文档-stream"><span>#</span></a></h3><p>YAML 可以在一个 Stream 中放多份 Document:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>name</span><span>: </span><span>reviewer</span></div></div><div><div><div>3</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>4</div></div><div><span>  </span><span>write</span><span>: </span><span>false</span></div></div><div><div><div>5</div></div><div><span>---</span></div></div><div><div><div>6</div></div><div><span>name</span><span>: </span><span>implementer</span></div></div><div><div><div>7</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>8</div></div><div><span>  </span><span>write</span><span>: </span><span>true</span></div></div><div><div><div>9</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>---</code> 开始新 Document，<code>...</code> 显式结束 Document。很多 Kubernetes 文件使用这种能力，但不是所有 Agent 配置 Loader 都接受多文档。工具只期待一个 Mapping 时，不要因为 YAML 能写就写。</p></section><section><h3>重复 Key<a href="#重复-key"><span>#</span></a></h3><p>YAML Mapping 的 Key 必须唯一，但现实 Parser 对重复 Key 的处理并不统一。有的报错，有的保留最后值:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>network</span><span>: </span><span>false</span></div></div><div><div><div>3</div></div><div><span>  </span><span>network</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这在权限配置里尤其危险。Lint 应当把重复 Key 当错误，而不是把它当覆盖机制。</p></section><section><h3>我什么时候选 YAML<a href="#我什么时候选-yaml"><span>#</span></a></h3><p>我会用 YAML 处理:</p><ul>
<li>人类频繁编辑、嵌套层次明显的配置。</li>
<li>CI Pipeline、部署清单等生态已经统一使用 YAML 的地方。</li>
<li>需要大量多行文本或对象数组。</li>
<li>目标工具提供严格 Schema、编辑器支持和明确 Parser 版本。</li>
</ul><p>如果配置涉及安全权限、隐式类型很多，且没有 Schema 或 Lint，我反而更偏向 TOML 或 JSON。YAML 的简洁来自上下文，机器和人都必须把上下文读对。</p></section></section>
<section><h2>INI: 简单到每个实现都想补一点<a href="#ini-简单到每个实现都想补一点"><span>#</span></a></h2><p>INI 没有一份像 RFC 8259 那样统治所有实现的统一规范。它通常由 Section 和 Key/Value 组成:</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[agent]</span></div></div><div><div><div>2</div></div><div><span>model</span><span><span> </span><span>=</span><span> reasoning-large</span></span></div></div><div><div><div>3</div></div><div><span>temperature</span><span><span> </span><span>=</span><span> 0.2</span></span></div></div><div><div><div>4</div></div><div><span>dry_run</span><span><span> </span><span>=</span><span> false</span></span></div></div><div><div><div>5</div></div><div><span>workspace</span><span><span> </span><span>=</span><span> /work/demo</span></span></div></div><div><div><div>6</div></div><div><span>include</span><span><span> </span><span>=</span><span> src/**/*.ts, tests/**/*.ts</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>[permissions]</span></div></div><div><div><div>9</div></div><div><span>read</span><span><span> </span><span>=</span><span> true</span></span></div></div><div><div><div>10</div></div><div><span>write</span><span><span> </span><span>=</span><span> false</span></span></div></div><div><div><div>11</div></div><div><span>network</span><span><span> </span><span>=</span><span> false</span></span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>[mcp:docs]</span></div></div><div><div><div>14</div></div><div><span>command</span><span><span> </span><span>=</span><span> node</span></span></div></div><div><div><div>15</div></div><div><span>args</span><span><span> </span><span>=</span><span> server.js, --stdio</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>[mcp:repo]</span></div></div><div><div><div>18</div></div><div><span>command</span><span><span> </span><span>=</span><span> python</span></span></div></div><div><div><div>19</div></div><div><span>args</span><span><span> </span><span>=</span><span> repo.py</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这是实验中最短的文件: <strong>19 行、280 Bytes</strong>。但短的原因之一是它没有直接表达 Array 和对象数组，我把它们压成了逗号分隔字符串与命名 Section。应用必须自己约定怎么拆。</p><section><h3>值通常都是 String<a href="#值通常都是-string"><span>#</span></a></h3><p>Python 3.13 的 <code>ConfigParser</code> 读取上面的值后:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>temperature -&gt; '0.2'  type: str</span></div></div><div><div><div>2</div></div><div><span>dry_run     -&gt; 'false' type: str</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>getfloat()</code> 和 <code>getboolean()</code> 可以显式转换，但文件格式本身没有给每个值统一定型。尤其不要这样写:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>bool</span><span>(config[</span><span>"agent"</span><span>][</span><span>"dry_run"</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为 <code>bool("false")</code> 仍然是 <code>True</code>。应该使用 Parser 提供的布尔转换 API，或者自己定义严格词汇表。</p></section><section><h3>Section、Key 大小写和重复项依赖实现<a href="#sectionkey-大小写和重复项依赖实现"><span>#</span></a></h3><p>以 Python <code>ConfigParser</code> 默认行为为例:</p><ul>
<li>Section 名称区分大小写。</li>
<li>Section 内的 Key 不区分大小写，并会规范成小写。</li>
<li><code>#</code> 和 <code>;</code> 可以开始整行注释。</li>
<li><code>=</code> 和 <code>:</code> 都能分隔 Key 与 Value。</li>
<li>读取多份文件时，后读文件的冲突值优先。</li>
</ul><p>换一个 INI Parser，这些细节可能改变。INI 的核心优势是简单，核心风险也是没有足够统一的边界。</p></section><section><h3>嵌套与 Array 都是应用协议<a href="#嵌套与-array-都是应用协议"><span>#</span></a></h3><p>下面 3 种都有人使用:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[permissions]</span></div></div><div><div><div>2</div></div><div><span>read</span><span><span> </span><span>=</span><span> true</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>permissions.read</span><span><span> </span><span>=</span><span> true</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[agent.permissions]</span></div></div><div><div><div>2</div></div><div><span>read</span><span><span> </span><span>=</span><span> true</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>INI 本身没有告诉我们哪一种代表嵌套对象。逗号分隔也无法无歧义表达参数本身包含逗号的 Array:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>args</span><span><span> </span><span>=</span><span> --message, hello, world</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是 3 个参数，还是 <code>--message</code> 加上 <code>hello, world</code> 一个参数？只有应用知道。结构复杂到需要发明转义规则时，INI 已经不再简单，换 TOML、YAML 或 JSON 更省事。</p></section><section><h3>我什么时候选 INI<a href="#我什么时候选-ini"><span>#</span></a></h3><p>INI 适合:</p><ul>
<li>兼容已有工具或 Windows 风格配置。</li>
<li>配置本质上是少量分组后的 String Key/Value。</li>
<li>不需要深层嵌套、复杂 Array 或严格跨语言类型。</li>
</ul><p>它不适合作为我从零设计的复杂 Agent 编排格式。两个 MCP Server 尚且能靠 Section 命名约定表达，等到 Server 还有环境变量、超时、权限和传输方式时，这套约定很快会长成一门没有规范的新语言。</p></section></section>
<section><h2><code>.env</code>: 它解决密钥注入，不解决结构化配置<a href="#env-它解决密钥注入不解决结构化配置"><span>#</span></a></h2><p><code>.env</code> 文件通常是一行一个环境变量:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>AGENT_API_KEY</span><span><span>=</span><span>replace-me</span></span></div></div><div><div><div>2</div></div><div><span>AGENT_BASE_URL</span><span><span>=</span><span>https://api.example.com</span></span></div></div><div><div><div>3</div></div><div><span>AGENT_TIMEOUT_SECONDS</span><span><span>=</span><span>30</span></span></div></div><div><div><div>4</div></div><div><span>AGENT_DEBUG</span><span><span>=</span><span>false</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它最重要的特征不是扩展名，而是最终进入进程环境的 <strong>String 到 String 映射</strong>。<code>30</code> 和 <code>false</code> 仍然是 String，应用必须转换。</p><p><code>.env</code> 生态也不是完全统一的规范。是否支持 <code>export</code>、单双引号、行尾注释、变量展开和多行 Value，取决于具体 Dotenv 实现。下面这些行为不能脱离 Loader 讨论:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export </span><span>AGENT_MODE</span><span><span>=</span><span>review</span></span></div></div><div><div><div>2</div></div><div><span>ROOT</span><span><span>=</span><span>/work/demo</span></span></div></div><div><div><div>3</div></div><div><span>CACHE_DIR</span><span>=</span><span>${</span><span>ROOT</span><span>}</span><span>/.cache</span></div></div><div><div><div>4</div></div><div><span>PASSWORD</span><span>=</span><span>'pa$sword'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>某些 Loader 会展开 <code>${ROOT}</code>，某些不会；某些会接受 <code>export</code>，某些只把它当作 Key 的一部分。Shell 能 <code>source .env</code> 也不代表 Dotenv Parser 与 Shell 语法完全相同。</p><section><h3>不要把 <code>.env</code> 变成小型 YAML<a href="#不要把-env-变成小型-yaml"><span>#</span></a></h3><p>我不会这样塞复杂数据:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>AGENT_PERMISSIONS</span><span><span>=</span><span>{</span></span><span>"read":true,"write"</span><span>:false}</span></div></div><div><div><div>2</div></div><div><span>AGENT_TOOLS</span><span><span>=</span><span>read,search,browser</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一项变成嵌套 JSON，第二项发明逗号数组。现在一次配置要经过 Dotenv Parser、环境变量传递，再经过 JSON 或 CSV Parser。可以工作，但边界和错误信息都变差。</p><p>更清楚的分工是:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># config.toml, 可以提交</span></div></div><div><div><div>2</div></div><div><span>model</span><span> = </span><span>"reasoning-large"</span></div></div><div><div><div>3</div></div><div><span>api_key_env</span><span> = </span><span>"AGENT_API_KEY"</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>[</span><span>permissions</span><span>]</span></div></div><div><div><div>6</div></div><div><span>read</span><span> = </span><span>true</span></div></div><div><div><div>7</div></div><div><span>write</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># .env.local, 不提交</span></div></div><div><div><div>2</div></div><div><span>AGENT_API_KEY</span><span><span>=</span><span>actual-secret-value</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结构、默认值和权限放版本控制中的配置文件；密钥从环境、操作系统 Keychain 或 Secret Manager 注入。配置保存密钥的<strong>名字</strong>，不保存密钥本身。</p><div><div><div></div><div>Caution</div></div><div><p><code>.env</code> 不是保险箱，只是普通文本文件。把它加入 <code>.gitignore</code> 能降低误提交概率，不能防止本机其他进程、备份软件、Shell 历史或日志读取它。生产环境优先使用平台的 Secret Store，并限制 Agent 能读取和输出哪些环境变量。</p></div></div></section></section>
<section><h2>XML: 冗长，但结构和工具链都很成熟<a href="#xml-冗长但结构和工具链都很成熟"><span>#</span></a></h2><p>XML 在新 Agent CLI 中不算主流，但 Maven、MSBuild、IDE、企业集成和许多旧系统仍大量使用。它用 Element、Attribute 和 Text 表达树:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;?</span><span>xml</span><span> version</span><span>=</span><span>"1.0"</span><span> encoding</span><span>=</span><span>"UTF-8"</span><span>?&gt;</span></div></div><div><div><div>2</div></div><div><span>&lt;</span><span>agent</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>&lt;</span><span>model</span><span>&gt;reasoning-large&lt;/</span><span>model</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>&lt;</span><span>temperature</span><span>&gt;0.2&lt;/</span><span>temperature</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>&lt;</span><span>dryRun</span><span>&gt;false&lt;/</span><span>dryRun</span><span>&gt;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>&lt;</span><span>permissions</span><span> </span><span>read</span><span>=</span><span>"true"</span><span> </span><span>write</span><span>=</span><span>"false"</span><span> </span><span>network</span><span>=</span><span>"false"</span><span> /&gt;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>&lt;</span><span>mcpServers</span><span>&gt;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>&lt;</span><span>server</span><span> </span><span>name</span><span>=</span><span>"docs"</span><span> </span><span>command</span><span>=</span><span>"node"</span><span>&gt;</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>&lt;</span><span>arg</span><span>&gt;server.js&lt;/</span><span>arg</span><span>&gt;</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>&lt;</span><span>arg</span><span>&gt;--stdio&lt;/</span><span>arg</span><span>&gt;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>&lt;/</span><span>server</span><span>&gt;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>&lt;/</span><span>mcpServers</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span>&lt;/</span><span>agent</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>XML 的问题不是不能表达，而是同一概念经常既能用 Attribute，也能用 Child Element:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>permission</span><span> </span><span>name</span><span>=</span><span>"network"</span><span> </span><span>enabled</span><span>=</span><span>"false"</span><span> /&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>permission</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>&lt;</span><span>name</span><span>&gt;network&lt;/</span><span>name</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>&lt;</span><span>enabled</span><span>&gt;false&lt;/</span><span>enabled</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span>&lt;/</span><span>permission</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>格式只提供机制，Schema 或应用约定才规定哪一种合法。XML Schema、XPath、Namespace 和成熟 Validator 是它的强项；标签闭合、实体转义和较高的文本体积是代价。</p><p>XML 中 <code>&lt;</code> 和 <code>&amp;</code> 等字符要转义:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>condition</span><span>&gt;tokens </span><span><span>&amp;</span><span>lt</span><span>;</span></span><span> 1000 </span><span><span>&amp;</span><span>amp</span><span>;&amp;</span><span>amp</span><span>;</span></span><span> safe&lt;/</span><span>condition</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>处理不可信 XML 时还要防范外部实体、实体扩展和资源消耗问题。使用禁用外部实体与 DTD 的安全 Parser 配置，不要把任意 XML 交给默认全功能解析器。</p><p>我不会因为 XML 老就把它翻译成 YAML。消费工具原生要求 XML 时，保留 XML 通常最可靠。格式迁移只有在整个读写链路一起迁移时才有意义。</p></section>
<section><h2>JSONC、JSON5 与 JavaScript 配置不是一个东西<a href="#jsoncjson5-与-javascript-配置不是一个东西"><span>#</span></a></h2><p>文件长得像 JSON，不代表它遵守 JSON RFC。</p><section><h3>JSONC<a href="#jsonc"><span>#</span></a></h3><p>JSONC 通常表示允许 JavaScript 风格注释的 JSON:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>// 禁止 Agent 修改文件</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"write"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"network"</span><span>: </span><span>false</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>是否允许尾逗号仍由 Parser 选项决定。微软的 <code>jsonc-parser</code> 就把 <code>allowTrailingComma</code> 做成独立选项，所以 “支持注释” 不能推出 “支持尾逗号”。</p></section><section><h3>JSON5<a href="#json5"><span>#</span></a></h3><p>JSON5 通常进一步允许单引号、裸 Key、尾逗号、十六进制数等:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>model</span><span>: </span><span>'reasoning-large'</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>tools</span><span>: [</span><span>'read'</span><span>, </span><span>'search'</span><span>],</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它更适合人手写，但标准 JSON Parser 不会接受。</p></section><section><h3>JavaScript 或 TypeScript 配置<a href="#javascript-或-typescript-配置"><span>#</span></a></h3><p>可执行配置甚至可以计算值:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>default</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>model</span><span>:</span><span><span> </span><span>process</span><span>.</span></span><span>env</span><span>.</span><span>AGENT_MODEL</span><span> </span><span>??</span><span> </span><span>"reasoning-large"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>timeout</span><span>:</span><span> </span><span>10</span><span> </span><span>*</span><span> </span><span>1000</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>include</span><span>:</span><span> [</span><span>"src"</span><span>, </span><span>...</span><span><span>(</span><span>process</span><span>.</span></span><span>env</span><span>.</span><span>CI</span><span> </span><span>?</span><span> [</span><span>"tests"</span><span>] </span><span>:</span><span> [])],</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>优点是组合、条件、变量和类型系统都现成。代价也非常具体:</p><ul>
<li>加载配置等于执行代码。</li>
<li>静态分析与跨语言消费更困难。</li>
<li>配置结果依赖环境和执行时机。</li>
<li>不可信仓库中的配置可能直接成为代码执行入口。</li>
</ul><p>当工具明确设计成加载代码配置时，我会用它的能力；当数据文件已经够用时，我不会为了少写 4 行复制而引入一台 JavaScript VM。</p></section></section>
<section><h2>Markdown: Agent Instructions 不是结构化 Settings<a href="#markdown-agent-instructions-不是结构化-settings"><span>#</span></a></h2><p><code>AGENTS.md</code>、项目规则和 System Prompt 解决的是自然语言指令，而不是严格的运行时配置。Markdown 很适合表达:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## Verification</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>-</span><span> Read repository rules before editing.</span></span></div></div><div><div><div>4</div></div><div><span><span>-</span><span> Run the narrowest command that exercises the changed behavior.</span></span></div></div><div><div><div>5</div></div><div><span><span>-</span><span> Never print credentials.</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>但它不适合替代:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sandbox_mode</span><span> = </span><span>"workspace-write"</span></div></div><div><div><div>2</div></div><div><span>network_access</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>自然语言里的 “不要联网” 是模型要遵循的指令，Runtime 中的 <code>network_access = false</code> 是执行环境可以强制的能力边界。前者像门上的告示，后者像门锁。对安全属性，我两个都写，但绝不把告示当锁。</p><p>我现在把 Agent 定制分成 4 层:</p>

<table><thead><tr><th>层</th><th>适合的载体</th><th>例子</th></tr></thead><tbody><tr><td>行为指令</td><td>Markdown</td><td>编码风格、工作流、验证要求</td></tr><tr><td>结构化设置</td><td>JSON/TOML/YAML</td><td>模型、工具、超时、权限</td></tr><tr><td>敏感值</td><td>Environment/Secret Store</td><td>API Key、Token、证书口令</td></tr><tr><td>强制策略</td><td>Sandbox/Policy</td><td>可写目录、网络、命令审批</td></tr></tbody></table><p>这种分层避免了两个常见误区: 用自然语言假装权限隔离，以及把几百行团队规范塞进一个转义严重的 JSON String。</p></section>
<section><h2>同一份数据，4 个 Parser 实际读一次<a href="#同一份数据4-个-parser-实际读一次"><span>#</span></a></h2><p>只对着代码块比较很容易产生一种虚假的掌控感，所以我真的解析了它们。实验环境是 Python 3.13.15 与 <code>yq 4.53.2</code>。</p><p>JSON 由 Python 标准库读取:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> json</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>with</span><span> </span><span>open</span><span>(</span><span>"agent.json"</span><span>, </span><span>encoding</span><span>=</span><span>"utf-8"</span><span>) </span><span>as</span><span> </span><span>file</span><span>:</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>json_config </span><span>=</span><span> json.</span><span>load</span><span>(</span><span>file</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>TOML 由 Python 3.11 起进入标准库的 <code>tomllib</code> 读取:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> tomllib</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>with</span><span> </span><span>open</span><span>(</span><span>"agent.toml"</span><span>, </span><span>"rb"</span><span>) </span><span>as</span><span> </span><span>file</span><span>:</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>toml_config </span><span>=</span><span> tomllib.</span><span>load</span><span>(</span><span>file</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>YAML 用 <code>yq</code> 规范化成 JSON 后读取:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>yq</span><span> </span><span>-o=json</span><span> </span><span>agent.yaml</span><span> &gt; </span><span>agent-from-yaml.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我最后比较 3 棵结构化数据树:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>assert</span><span><span> json_config </span><span>==</span><span> toml_config </span><span>==</span><span> yaml_config</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>断言通过。也就是说，396 Bytes 的 JSON、334 Bytes 的 TOML 和 310 Bytes 的 YAML 最终表达了相同的 16 个叶子值。INI 没参加这个等价断言，因为 Array、对象数组和类型转换已经需要应用自定义协议。硬把它也转换成同一棵树，只会证明我写了一个 INI 方言，不会证明 INI 本身有这些语义。</p><p>实验数字汇总:</p>

<table><thead><tr><th>格式</th><th>行数</th><th>UTF-8 Bytes</th><th>相对 JSON</th></tr></thead><tbody><tr><td>JSON</td><td>16</td><td>396</td><td>基准</td></tr><tr><td>TOML</td><td>20</td><td>334</td><td>少 15.7%</td></tr><tr><td>YAML</td><td>18</td><td>310</td><td>少 21.7%</td></tr><tr><td>INI</td><td>19</td><td>280</td><td>少 29.3%，但丢失原生结构</td></tr></tbody></table><p>Bytes 不是选择配置格式的主要指标，毕竟 116 Bytes 还装不下一张很有说服力的猫图。它真正说明的是: 简洁有两种来源，一种是去掉冗余标点，另一种是把语义工作推给应用。前者通常很好，后者要计入复杂度。</p></section>
<section><h2>语法正确之后，还有 Schema<a href="#语法正确之后还有-schema"><span>#</span></a></h2><p>假设这两份 JSON 都能解析:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"model"</span><span>: </span><span>"reasoning-large"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"temperature"</span><span>: </span><span>0.2</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"permissions"</span><span>: {</span><span>"read"</span><span>: </span><span>true</span><span>, </span><span>"write"</span><span>: </span><span>false</span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"model"</span><span>: </span><span>42</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"temperature"</span><span>: </span><span>"cold"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"permissions"</span><span>: [</span><span>"everything"</span><span>]</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Parser 只会说两份都合法。JSON Schema 可以描述应用期待的结构:</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"$schema"</span><span>: </span><span>"https://json-schema.org/draft/2020-12/schema"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"type"</span><span>: </span><span>"object"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"required"</span><span>: [</span><span>"model"</span><span>, </span><span>"permissions"</span><span>],</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"additionalProperties"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>"properties"</span><span>: {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"model"</span><span>: {</span><span>"type"</span><span>: </span><span>"string"</span><span>, </span><span>"minLength"</span><span>: </span><span>1</span><span>},</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"temperature"</span><span>: {</span><span>"type"</span><span>: </span><span>"number"</span><span>, </span><span>"minimum"</span><span>: </span><span>0</span><span>, </span><span>"maximum"</span><span>: </span><span>2</span><span>},</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"permissions"</span><span>: {</span></div></div><div><div><div>10</div></div><div><span>      </span><span>"type"</span><span>: </span><span>"object"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>      </span><span>"required"</span><span>: [</span><span>"read"</span><span>, </span><span>"write"</span><span>, </span><span>"network"</span><span>],</span></div></div><div><div><div>12</div></div><div><span>      </span><span>"additionalProperties"</span><span>: </span><span>false</span><span>,</span></div></div><div><div><div>13</div></div><div><span>      </span><span>"properties"</span><span>: {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>"read"</span><span>: {</span><span>"type"</span><span>: </span><span>"boolean"</span><span>},</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"write"</span><span>: {</span><span>"type"</span><span>: </span><span>"boolean"</span><span>},</span></div></div><div><div><div>16</div></div><div><span>        </span><span>"network"</span><span>: {</span><span>"type"</span><span>: </span><span>"boolean"</span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Schema 能检查:</p><ul>
<li>必填字段是否存在。</li>
<li>Value 类型是否正确。</li>
<li>Number 范围与 String Pattern。</li>
<li>Array Item 的结构。</li>
<li>是否允许未知 Key。</li>
</ul><p>但 Schema 通常不能完整回答这些语义问题:</p><ul>
<li><code>workspace</code> 是否真的存在。</li>
<li><code>command</code> 是否能在当前机器执行。</li>
<li><code>write = true</code> 与当前 Sandbox 是否冲突。</li>
<li>两个 MCP Server 是否用了重复名称。</li>
<li>某个模型是否支持工具调用。</li>
</ul><p>因此完整验证通常分两阶段: Schema 做结构检查，应用代码做语义检查。JSON Schema 本身虽然用 JSON 编写，但也常被拿来验证 YAML，因为 YAML 可以先加载成同样的 Mapping、Sequence 与 Scalar 树。关键是先固定 YAML 的类型解析规则。</p><section><h3><code>additionalProperties</code> 要不要关<a href="#additionalproperties-要不要关"><span>#</span></a></h3><p>把 <code>additionalProperties</code> 设为 <code>false</code> 可以抓住拼写错误:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>permisions</span><span>: </span><span># 少写了一个 s</span></div></div><div><div><div>2</div></div><div><span>  </span><span>write</span><span>: </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>否则错误 Key 可能被静默忽略，用户以为权限已关闭，Agent 则按默认值运行。这对安全字段很危险。</p><p>代价是向前兼容性降低。旧 Validator 遇到新字段会拒绝配置。我会对安全敏感、内部控制的配置使用严格 Schema；对需要插件扩展的区域，则明确留出 <code>extensions</code> 或命名空间，而不是允许任意 Key 混在根节点。</p></section></section>
<section><h2>覆盖顺序比文件格式更容易制造事故<a href="#覆盖顺序比文件格式更容易制造事故"><span>#</span></a></h2><p>现实 Agent 工具通常不只读一份文件。常见来源包括:</p><ol>
<li>内置默认值。</li>
<li>系统或组织策略。</li>
<li>用户级配置。</li>
<li>项目级配置。</li>
<li>本机未提交配置。</li>
<li>环境变量。</li>
<li>命令行参数。</li>
</ol><p>但这个列表不是通用优先级。每个工具可以采用不同顺序，有些来源甚至禁止覆盖某些字段。例如 Codex 当前官方配置参考明确区分用户级 <code>~/.codex/config.toml</code> 与项目级 <code>.codex/config.toml</code>，并限制项目配置覆盖 Provider、通知与遥测等机器本地字段。这个限制来自 Loader 和安全策略，不来自 TOML。</p><section><h3>Scalar 覆盖容易，Mapping 与 Array 合并困难<a href="#scalar-覆盖容易mapping-与-array-合并困难"><span>#</span></a></h3><p>假设用户配置是:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>read</span><span>: </span><span>true</span></div></div><div><div><div>3</div></div><div><span>  </span><span>write</span><span>: </span><span>false</span></div></div><div><div><div>4</div></div><div><span>tools</span><span>:</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>- </span><span>read</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>- </span><span>search</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>项目配置是:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>permissions</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>write</span><span>: </span><span>true</span></div></div><div><div><div>3</div></div><div><span>tools</span><span>:</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>- </span><span>browser</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>至少存在 4 种合理策略:</p><ol>
<li><strong>浅覆盖</strong>: 整个 <code>permissions</code> 被项目对象替换，<code>read</code> 消失。</li>
<li><strong>深合并</strong>: 只把 <code>write</code> 改成 <code>true</code>，保留 <code>read</code>。</li>
<li><strong>Array 替换</strong>: <code>tools</code> 最后只有 <code>browser</code>。</li>
<li><strong>Array 追加</strong>: <code>tools</code> 变成 <code>read</code>、<code>search</code>、<code>browser</code>。</li>
</ol><p>格式没有规定答案。YAML Anchor、TOML Dotted Key 和 JSON Merge Patch 是不同机制，也不能自动代表工具的加载策略。</p><p>我在修改 Agent 配置前会先找 4 个信息:</p><ul>
<li>文件搜索路径。</li>
<li>来源优先级。</li>
<li>Object 是替换还是深合并。</li>
<li>Array 是替换、追加还是按 ID 合并。</li>
</ul><p>找不到官方说明时，我会做一个无害实验: 在低优先级配置中放唯一值，在高优先级配置中只覆盖一个兄弟字段，然后运行工具的 <code>config show</code>、诊断命令或最小启动路径观察最终值。不要拿网络和写权限当实验字段。</p></section></section>
<section><h2>一份实际可维护的 Agent 配置应该怎样分层<a href="#一份实际可维护的-agent-配置应该怎样分层"><span>#</span></a></h2><p>我倾向于下面的目录布局:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>project/</span></div></div><div><div><div>2</div></div><div><span>├── AGENTS.md                 # 团队行为指令，可提交</span></div></div><div><div><div>3</div></div><div><span>├── .agent/</span></div></div><div><div><div>4</div></div><div><span>│   ├── config.toml           # 项目结构化设置，可提交</span></div></div><div><div><div>5</div></div><div><span>│   └── config.local.toml     # 本机覆盖，不提交</span></div></div><div><div><div>6</div></div><div><span>├── .env.example              # 环境变量名称与示例，可提交</span></div></div><div><div><div>7</div></div><div><span>├── .env.local                # 本地密钥，不提交</span></div></div><div><div><div>8</div></div><div><span>└── schemas/</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>└── agent.schema.json     # 如果工具支持自定义 Schema</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文件名只是示意，真实工具要求什么就用什么，不要为了统一外观擅自改名。</p><p>项目配置只放团队可共享且确定的内容:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>model</span><span> = </span><span>"reasoning-large"</span></div></div><div><div><div>2</div></div><div><span>sandbox_mode</span><span> = </span><span>"workspace-write"</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>[</span><span>permissions</span><span>]</span></div></div><div><div><div>5</div></div><div><span>network</span><span> = </span><span>false</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>[</span><span>verification</span><span>]</span></div></div><div><div><div>8</div></div><div><span>commands</span><span> = [</span><span>"pnpm check"</span><span>, </span><span>"pnpm build"</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>本地覆盖放机器路径或个人偏好:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>workspace</span><span> = </span><span>"/home/me/src/project"</span></div></div><div><div><div>2</div></div><div><span>editor</span><span> = </span><span>"nvim"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Secret 只通过名字引用:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>provider</span><span>]</span></div></div><div><div><div>2</div></div><div><span>api_key_env</span><span> = </span><span>"AGENT_API_KEY"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>自然语言规则留在 Markdown:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## Editing</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>-</span><span> Reuse existing repository conventions.</span></span></div></div><div><div><div>4</div></div><div><span><span>-</span><span> Do not modify generated files by hand.</span></span></div></div><div><div><div>5</div></div><div><span><span>-</span><span> Run the command that exercises the changed behavior.</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这 4 类内容更新频率、读者和安全边界都不同。把它们拆开，不是为了多造文件，而是为了让 Git Diff、Secret 扫描、Schema 和 Agent 各自处理擅长的对象。</p></section>
<section><h2>权限配置必须默认拒绝<a href="#权限配置必须默认拒绝"><span>#</span></a></h2><p>Agent 和普通格式化工具不同，它可能执行 Shell、访问网络、读取整个 Home Directory 或修改仓库。配置格式的可读性在这里直接影响安全。</p><p>我会明确写出最小权限，而不是依赖版本升级后可能变化的默认值:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>permissions</span><span>]</span></div></div><div><div><div>2</div></div><div><span>read_workspace</span><span> = </span><span>true</span></div></div><div><div><div>3</div></div><div><span>write_workspace</span><span> = </span><span>false</span></div></div><div><div><div>4</div></div><div><span>read_home</span><span> = </span><span>false</span></div></div><div><div><div>5</div></div><div><span>network</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后只在具体任务需要时扩大:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>profiles</span><span>.</span><span>implementation</span><span>.</span><span>permissions</span><span>]</span></div></div><div><div><div>2</div></div><div><span>write_workspace</span><span> = </span><span>true</span></div></div><div><div><div>3</div></div><div><span>network</span><span> = </span><span>false</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意，这只是概念示例，不是某个具体工具的字段参考。实际 Key、Profile 语法与强制能力必须查工具官方文档。最重要的设计原则是:</p><ul>
<li>权限字段使用 Boolean 或封闭 Enum，不用含糊 String。</li>
<li>未知权限值让启动失败，不静默回退到更宽权限。</li>
<li>项目配置不能放宽组织强制策略。</li>
<li>日志展示最终生效来源，但对 Secret 做脱敏。</li>
<li>权限提升需要显式、局部、可审计。</li>
</ul><p>自然语言 Prompt 可以改善行为，Sandbox 才能限制能力。两者一起用，但不要混为一谈。</p></section>
<section><h2>常见错误，我现在怎样定位<a href="#常见错误我现在怎样定位"><span>#</span></a></h2><section><h3>1. <code>Unexpected token</code> 或 <code>Expected comma</code><a href="#1-unexpected-token-或-expected-comma"><span>#</span></a></h3><p>这通常是 JSON 的引号、逗号或括号错误。先让 Parser 报准确行列:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>jq</span><span> </span><span>empty</span><span> </span><span>config.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不要从文件开头人工数括号。格式化器和 Parser 专门干这个。</p></section><section><h3>2. TOML Key 跑进错误 Table<a href="#2-toml-key-跑进错误-table"><span>#</span></a></h3><p>检查它前面最近的 <code>[table]</code> 或 <code>[[array_of_tables]]</code>。缩进不会把 Key 拉回根节点。可以用 Python 标准库打印解析结果:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>python</span><span> </span><span>-c</span><span> </span><span>'import pathlib,tomllib,pprint; pprint.pp(tomllib.loads(pathlib.Path("config.toml").read_text()))'</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3. YAML <code>mapping values are not allowed here</code><a href="#3-yaml-mapping-values-are-not-allowed-here"><span>#</span></a></h3><p>优先检查:</p><ul>
<li>缩进层级是否一致。</li>
<li>是否混入 Tab。</li>
<li>Plain Scalar 中是否出现 <code>: </code>。</li>
<li>上一行是否漏了 <code>-</code> 或 <code>:</code>。</li>
<li>多行 Block 是否缩进不足。</li>
</ul><p>用 Parser 看结构，而不是只看语法高亮:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>yq</span><span> </span><span>-o=json</span><span> </span><span>config.yaml</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4. 值看起来对，类型却错<a href="#4-值看起来对类型却错"><span>#</span></a></h3><p>打印 Value 及其类型。典型错误包括:</p><ul>
<li>JSON 中把 <code>false</code> 写成 <code>"false"</code>。</li>
<li>YAML 中把版本号解析成 Number。</li>
<li>TOML 日期没加引号，变成 Date。</li>
<li>INI 中把所有 String 当成自动带类型。</li>
</ul></section><section><h3>5. 配置合法但完全没生效<a href="#5-配置合法但完全没生效"><span>#</span></a></h3><p>这通常不是格式问题。检查:</p><ul>
<li>文件名和路径。</li>
<li>当前工作目录。</li>
<li>项目是否被工具标记为可信。</li>
<li>Profile 是否真的选中。</li>
<li>更高优先级来源是否覆盖。</li>
<li>字段是否已弃用或只在特定版本支持。</li>
</ul></section><section><h3>6. Agent 启动后才报 MCP Server 失败<a href="#6-agent-启动后才报-mcp-server-失败"><span>#</span></a></h3><p>配置语法只证明 <code>command</code> 是 String、<code>args</code> 是 Array。还要检查:</p><ul>
<li>Executable 是否存在。</li>
<li>工作目录是否正确。</li>
<li>参数是否被拆成独立 Array Item。</li>
<li>环境变量是否传入。</li>
<li>Transport 是 stdio、HTTP 还是其他方式。</li>
<li>Server 是否向 stdout 打了破坏协议的日志。</li>
</ul><p>这是语义验证，不是 Parser 能提前推断的内容。</p></section></section>
<section><h2>一张够用的格式选择表<a href="#一张够用的格式选择表"><span>#</span></a></h2>

<table><thead><tr><th>需求</th><th>JSON</th><th>TOML</th><th>YAML</th><th>INI</th><th><code>.env</code></th><th>XML</th></tr></thead><tbody><tr><td>跨语言数据交换</td><td>很强</td><td>强</td><td>强</td><td>弱</td><td>弱</td><td>很强</td></tr><tr><td>人工长期维护</td><td>一般</td><td>很强</td><td>强</td><td>简单场景强</td><td>仅少量变量</td><td>一般</td></tr><tr><td>标准注释</td><td>无</td><td>有</td><td>有</td><td>通常有</td><td>实现相关</td><td>有</td></tr><tr><td>原生嵌套</td><td>有</td><td>有</td><td>有</td><td>无统一方式</td><td>无</td><td>有</td></tr><tr><td>原生 Array</td><td>有</td><td>有</td><td>有</td><td>无统一方式</td><td>无</td><td>有</td></tr><tr><td>Null</td><td>有</td><td>无</td><td>有</td><td>无统一方式</td><td>无</td><td>可通过 Schema 约定</td></tr><tr><td>日期时间类型</td><td>无</td><td>有</td><td>Schema/Parser 相关</td><td>无</td><td>无</td><td>Schema 相关</td></tr><tr><td>隐式类型风险</td><td>低</td><td>低</td><td>较高</td><td>值常为 String</td><td>值为 String</td><td>由 Schema 决定</td></tr><tr><td>Schema 工具链</td><td>很强</td><td>可借助应用 Schema</td><td>很强</td><td>分散</td><td>应用自检</td><td>很强</td></tr><tr><td>适合 Secret 本体</td><td>不适合</td><td>不适合</td><td>不适合</td><td>不适合</td><td>仅本地开发</td><td>不适合</td></tr></tbody></table><p>我的实际决策顺序不是 “我喜欢哪种语法”，而是:</p><ol>
<li><strong>消费工具要求什么格式？</strong> 要求 JSON 就不要喂 TOML。</li>
<li><strong>官方示例和 Schema 使用什么版本与方言？</strong> YAML 1.1 和 1.2、JSON 与 JSONC 不能混叫。</li>
<li><strong>文件主要由人写还是机器生成？</strong> 人写偏 TOML/YAML，机器交换偏 JSON。</li>
<li><strong>结构有多深，对象数组有多少？</strong> 平坦配置偏 TOML，深层清单偏 YAML/JSON。</li>
<li><strong>是否需要注释、多行 Prompt 或日期？</strong> 这些会改变选择。</li>
<li><strong>如何验证、合并和保护 Secret？</strong> 没有这一步，格式选得再漂亮也只是一份漂亮的风险。</li>
</ol></section>
<section><h2>我的保守写法<a href="#我的保守写法"><span>#</span></a></h2><p>跨 Agent、跨 Parser 工作时，我尽量使用每种格式中最无聊的子集。</p><section><h3>JSON<a href="#json"><span>#</span></a></h3><ul>
<li>所有 Key 与 String 使用双引号。</li>
<li>不写注释和尾逗号，除非工具明确声明 JSONC 方言。</li>
<li>不写重复 Key，不依赖 Object 顺序。</li>
<li>大整数 ID 写成 String。</li>
<li>区分缺失、<code>null</code> 和空 String。</li>
</ul></section><section><h3>TOML<a href="#toml"><span>#</span></a></h3><ul>
<li>Key 使用统一的 <code>snake_case</code>。</li>
<li>根 Key 放在第一个 Table Header 之前。</li>
<li>用 <code>[table]</code> 表示对象，用 <code>[[table]]</code> 表示对象数组。</li>
<li>路径和正则优先考虑 Literal String。</li>
<li>不重复定义 Key，不用不存在的 <code>null</code>。</li>
</ul></section><section><h3>YAML<a href="#yaml"><span>#</span></a></h3><ul>
<li>每层 2 个空格，禁止 Tab。</li>
<li>Boolean 只写 <code>true</code> 和 <code>false</code>。</li>
<li>日期、版本、ID、通配符和可疑 Scalar 加引号。</li>
<li>多行 Prompt 明确选择 <code>|</code>、<code>&gt;</code> 和 Chomping Indicator。</li>
<li>避免 Merge Key、自定义 Tag 和复杂 Anchor，除非目标工具明确支持。</li>
<li>用 Linter 拒绝重复 Key。</li>
</ul></section><section><h3>INI 与 <code>.env</code><a href="#ini-与-env"><span>#</span></a></h3><ul>
<li>假设所有 Value 都是 String，再显式转换。</li>
<li>不发明深层对象和复杂 Array 协议。</li>
<li>查清注释、引号、展开与覆盖行为属于哪个实现。</li>
<li>Secret 不提交，示例文件只提交变量名称和安全占位值。</li>
</ul></section></section>
<section><h2>我会怎样配置一个新 Agent 工具<a href="#我会怎样配置一个新-agent-工具"><span>#</span></a></h2><p>现在遇到新工具，我不会直接复制一份 300 行 “ultimate config”。我会按下面顺序建立配置:</p><ol>
<li>从官方最小示例启动，确认文件路径、格式版本和当前工具版本。</li>
<li>先配置模型与只读工作区，让工具输出或表现出最终生效值。</li>
<li>加入一个工具或 MCP Server，实际调用一次。</li>
<li>显式关闭网络与写权限，再做一个应当被拒绝的无害操作。</li>
<li>只对确实需要的 Profile 开放局部权限。</li>
<li>把行为规则移到项目 Markdown，把 Secret 移到环境或 Secret Store。</li>
<li>给配置加 Parser 检查、Schema 验证和 Secret 扫描。</li>
<li>升级工具时先读配置变更，再让旧字段悄悄退休。</li>
</ol><p>OpenAI Codex 当前使用 TOML 配置，并把用户级、项目级和 Profile 配置分开；其他工具可能使用 JSON、JSONC、YAML 或自己的组合。这个生态大概不会很快统一。我反而觉得真正可迁移的能力不是背住所有文件名，而是看见任何新格式时，立即问出同一组问题: 这是什么数据树，Scalar 怎样定型，Schema 在哪，来源怎样覆盖，最终权限由谁强制？</p><p>再往前看几年，我怀疑 Agent 配置会越来越像编译系统: 人写一层相对友好的源码，Schema 和 Policy 做静态检查，Loader 合并组织、项目与个人配置，Runtime 再把它编译成一个可审计的能力图。JSON、TOML 和 YAML 大概都不会赢得终局，它们只是不同的源语言。真正重要的产物，是 Agent 最后到底能看什么、能改什么、能调用什么。</p><p>我现在至少不会再因为 YAML 少了 86 Bytes 就宣布它 “更现代” 了。机器已经很聪明，配置文件不必跟着耍聪明。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://www.rfc-editor.org/rfc/rfc8259" target="_blank">RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format</a></li>
<li><a href="https://toml.io/en/v1.0.0" target="_blank">TOML v1.0.0 Specification</a></li>
<li><a href="https://yaml.org/spec/1.2.2/" target="_blank">YAML 1.2.2 Specification</a></li>
<li><a href="https://docs.python.org/3/library/json.html" target="_blank">Python <code>json</code> Documentation</a></li>
<li><a href="https://docs.python.org/3/library/tomllib.html" target="_blank">Python <code>tomllib</code> Documentation</a></li>
<li><a href="https://docs.python.org/3/library/configparser.html" target="_blank">Python <code>configparser</code> Documentation</a></li>
<li><a href="https://json-schema.org/understanding-json-schema/about" target="_blank">JSON Schema: What is a schema?</a></li>
<li><a href="https://github.com/microsoft/node-jsonc-parser" target="_blank">Microsoft <code>jsonc-parser</code></a></li>
<li><a href="https://www.w3.org/TR/xml/" target="_blank">W3C XML 1.0 Fifth Edition</a></li>
<li><a href="https://developers.openai.com/codex/config-reference/" target="_blank">OpenAI Codex Configuration Reference</a></li>
<li><a href="https://github.com/bkeepers/dotenv" target="_blank">Dotenv README</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/regular-expressions-from-syntax-to-automation/</id>
      <title type="text">我不想再盲贴正则表达式了: 从语法到 Scoop、Python、CMake、C++ 和 TypeScript</title>
      <published>2026-09-07T12:00:00.000Z</published>
      <updated>2026-09-07T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/regular-expressions-from-syntax-to-automation/"/>
      <summary type="text">我从一份包含 4 个版本号的发布页开始，逐步写出可解释、可移植、可测试的正则表达式，再把同一套方法放进 Scoop checkver、Python、CMake、C++ 和 JavaScript/TypeScript。</summary>
      <content type="html"><![CDATA[<p>我在日常开发里早就离不开正则表达式了。Scoop 要从非 GitHub 发布页抓版本，Python 脚本要清洗日志，CMake 要从第三方库的 Header 中读版本，TypeScript 又要验证构建产物名称。可我过去的工作流基本是把样本文本扔给 AI，复制一串像线路噪声的字符，再运行一次看看输出对不对。只验证一个成功样本，相当于拿一把只试过一把锁的钥匙去管理整栋楼，多少有点勇。</p>
<p>所以我给自己造了一个很小的正则实验室。我准备了一份同时包含构建日期、预览版、稳定版和依赖版本的文本，再准备 12 个发布物文件名，其中 6 个应该接受，6 个必须拒绝。然后我把同一个问题分别放进 Python、Scoop、CMake、C++ 和 TypeScript，实际运行它们，看看哪些知识可以搬过去，哪些反斜杠会在途中离奇失踪。</p>
<section><h2>正则表达式到底是什么<a href="#正则表达式到底是什么"><span>#</span></a></h2><p>正则表达式不是一段会执行的业务代码，而是一种描述<strong>字符串集合</strong>的小语言。模式 <code>cat</code> 描述所有包含连续字符 <code>cat</code> 的文本；模式 <code>^cat$</code> 则只描述完整等于 <code>cat</code> 的文本。</p><p>我现在会把一次匹配拆成 4 个对象：</p><ol>
<li><strong>目标文本</strong>：发布页、文件名、日志或源码。</li>
<li><strong>模式</strong>：哪些字符可以出现，以什么顺序出现。</li>
<li><strong>操作</strong>：搜索一处、搜索全部、验证全文、提取分组或替换。</li>
<li><strong>方言和宿主字符串</strong>：Python <code>re</code>、JavaScript <code>RegExp</code>、C++ ECMAScript、CMake Regex 和 .NET Regex 并不是同一个引擎，模式还可能先经过 Python、JSON 或 CMake 的字符串解析。</li>
</ol><p>最后一项解释了大量 “明明在线网站能匹配，放进项目就坏了”。很多时候正则本身没错，错的是我拿法语词典去查西班牙语，或者字符串解析器先吃掉了一层反斜杠。</p><p>大多数本文涉及的引擎会从某个位置出发，沿着模式尝试字符，失败时再换分支或回退。我们可以把它想成一个在迷宫里移动的指针：普通字符是直路，<code>|</code> 是岔路，量词是可以重复走的环，回溯则是从死路退回来。这个比喻到这里就够了；真正重要的是，每增加一个含糊分支，引擎可能就多一条要探索的路。</p></section>
<section><h2>先只学 6 组积木<a href="#先只学-6-组积木"><span>#</span></a></h2><p>我不再从一张塞满符号的速查表开始。绝大多数日常模式都由下面 6 组积木拼出来。</p><section><h3>1. 字面字符和转义<a href="#1-字面字符和转义"><span>#</span></a></h3><p>普通字符通常匹配自己：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>release     -&gt; 匹配 release</span></div></div><div><div><div>2</div></div><div><span>v2          -&gt; 匹配 v2</span></div></div><div><div><div>3</div></div><div><span>x64         -&gt; 匹配 x64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这些字符通常有特殊意义：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>. ^ $ * + ? { } [ ] ( ) | \</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>要匹配字面上的点，写 <code>\.</code>，因为裸的 <code>.</code> 通常表示 “除换行外的任意单个字符”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>2.7.3       -&gt; 既能匹配 2.7.3，也可能匹配 2x7y3</span></div></div><div><div><div>2</div></div><div><span>2\.7\.3     -&gt; 只把两个位置当作字面点号</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是美学差异。版本号里的点不转义，模式就比我想象的更宽。</p></section><section><h3>2. 字符类<a href="#2-字符类"><span>#</span></a></h3><p>方括号表示 “从这一组字符中取一个”：</p>

<table><thead><tr><th>模式</th><th>含义</th><th>示例</th></tr></thead><tbody><tr><td><code>[abc]</code></td><td><code>a</code>、<code>b</code>、<code>c</code> 中任意一个</td><td><code>cat</code> 中的 <code>c</code></td></tr><tr><td><code>[a-z]</code></td><td>ASCII 小写字母范围</td><td><code>release</code></td></tr><tr><td><code>[0-9]</code></td><td>一个 ASCII 数字</td><td><code>7</code></td></tr><tr><td><code>[^0-9]</code></td><td>一个非 ASCII 数字字符</td><td><code>v</code></td></tr><tr><td><code>[._-]</code></td><td>点、下划线或连字符之一</td><td><code>2.7-rc</code></td></tr></tbody></table><p>很多引擎还提供快捷形式：</p><ul>
<li><code>\d</code>：数字字符。</li>
<li><code>\w</code>：所谓的 word character。</li>
<li><code>\s</code>：空白字符。</li>
<li>大写版本 <code>\D</code>、<code>\W</code>、<code>\S</code> 表示相反集合。</li>
</ul><p>但快捷形式的 Unicode 语义会随方言变化。Python 3 的 <code>\d</code> 默认可以匹配 Unicode 十进制数字，JavaScript 的 <code>\d</code> 仍然只匹配 ASCII <code>0-9</code>；CMake 的文档则没有定义 <code>\d</code> 这套快捷类。版本号、端口、哈希这类协议字段只允许 ASCII 时，我直接写 <code>[0-9]</code>。它长了 3 个字符，却少了一个跨语言猜谜游戏。</p><div><div><div></div><div>Note</div></div><div><p>字符类只消费一个字符。<code>[abc]+</code> 才是一个或多个；<code>[abc]</code> 也不表示字符串 <code>abc</code>。方括号是候选集合，圆括号才组合一段模式。</p></div></div></section><section><h3>3. 量词<a href="#3-量词"><span>#</span></a></h3><p>量词控制它前面的一个字符、字符类或分组重复多少次：</p>

<table><thead><tr><th>量词</th><th>次数</th><th>例子</th></tr></thead><tbody><tr><td><code>*</code></td><td>0 次或更多</td><td><code>ab*</code> 匹配 <code>a</code>、<code>ab</code>、<code>abbb</code></td></tr><tr><td><code>+</code></td><td>1 次或更多</td><td><code>[0-9]+</code> 匹配一串数字</td></tr><tr><td><code>?</code></td><td>0 次或 1 次</td><td><code>v?2</code> 匹配 <code>2</code> 或 <code>v2</code></td></tr><tr><td><code>{3}</code></td><td>恰好 3 次</td><td><code>[0-9]{3}</code></td></tr><tr><td><code>{2,4}</code></td><td>2 到 4 次</td><td><code>[a-f0-9]{2,4}</code></td></tr><tr><td><code>{2,}</code></td><td>至少 2 次</td><td><code>[0-9]{2,}</code></td></tr></tbody></table><p>量词默认通常是<strong>贪婪</strong>的，它会先吃尽可能多的字符，再在后续匹配失败时回退：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入: &lt;a&gt;one&lt;/a&gt;&lt;b&gt;two&lt;/b&gt;</span></div></div><div><div><div>2</div></div><div><span>模式: &lt;.*&gt;</span></div></div><div><div><div>3</div></div><div><span>结果: &lt;a&gt;one&lt;/a&gt;&lt;b&gt;two&lt;/b&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>给量词加 <code>?</code> 会变成懒惰版本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>模式: &lt;.*?&gt;</span></div></div><div><div><div>2</div></div><div><span>第一处结果: &lt;a&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不过，用 <code>&lt;.*?&gt;</code> 解析 HTML 仍然不是好主意。引号、注释、嵌套标签和脚本内容很快就会把问题升级。能用 HTML Parser 取得节点时，我不会逼正则假装自己是一棵 DOM 树。</p></section><section><h3>4. 锚点和边界<a href="#4-锚点和边界"><span>#</span></a></h3><p>锚点不消费字符，它只断言当前位置：</p>

<table><thead><tr><th>模式</th><th>常见含义</th></tr></thead><tbody><tr><td><code>^</code></td><td>字符串开头；多行模式下也可能是每行开头</td></tr><tr><td><code>$</code></td><td>字符串结尾；多行模式下也可能是每行结尾</td></tr><tr><td><code>\b</code></td><td>单词边界</td></tr><tr><td><code>\B</code></td><td>非单词边界</td></tr></tbody></table><p>假设我要验证发布物名称：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tool-v2.7.3-windows-x64.zip</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不加锚点的模式可能在下面这个校验和文件名内部找到一段成功匹配：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tool-v2.7.3-windows-x64.zip.sha256</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果我的问题是 “字符串里有没有一个发布物名称”，内部匹配没问题。如果问题是 “整个文件名是否合法”，就要用完整匹配 API，或者使用 <code>^...$</code>。搜索和验证不是同一件事。</p></section><section><h3>5. 分组、捕获和或分支<a href="#5-分组捕获和或分支"><span>#</span></a></h3><p>圆括号把多段模式组合起来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(windows|linux)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>|</code> 表示二选一。它的优先级很低，所以：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>^cat|dog$      -&gt; 以 cat 开头，或者以 dog 结尾</span></div></div><div><div><div>2</div></div><div><span>^(cat|dog)$    -&gt; 全文只能是 cat 或 dog</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>普通圆括号同时会保存捕获内容。第 0 组通常是完整匹配，第 1 组开始才是我写的括号：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>模式: ^tool-v([0-9]+\.[0-9]+\.[0-9]+)-(windows|linux)$</span></div></div><div><div><div>2</div></div><div><span>输入: tool-v2.7.3-windows</span></div></div><div><div><div>3</div></div><div><span>组 0: tool-v2.7.3-windows</span></div></div><div><div><div>4</div></div><div><span>组 1: 2.7.3</span></div></div><div><div><div>5</div></div><div><span>组 2: windows</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只想组合但不想捕获时，很多方言支持非捕获分组 <code>(?:...)</code>。需要在代码里取值时，我更喜欢命名分组，因为插入一个新括号不会把 <code>group(3)</code> 悄悄变成 <code>group(4)</code>。遗憾的是命名语法并不统一：</p>

<table><thead><tr><th>环境</th><th>命名捕获</th></tr></thead><tbody><tr><td>Python</td><td><code>(?P&lt;version&gt;...)</code></td></tr><tr><td>JavaScript/TypeScript</td><td><code>(?&lt;version&gt;...)</code></td></tr><tr><td>.NET / Scoop</td><td><code>(?&lt;version&gt;...)</code></td></tr><tr><td>C++ <code>std::regex</code> 默认 ECMAScript</td><td>不支持命名捕获</td></tr><tr><td>CMake Regex</td><td>不支持命名捕获</td></tr></tbody></table><p>这张表值得记住。模式的主要结构可以迁移，方言细节必须重新核对。</p></section><section><h3>6. Lookaround 和反向引用<a href="#6-lookaround-和反向引用"><span>#</span></a></h3><p>Lookaround 检查旁边是否存在某段文本，但不把它放进最终匹配：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[0-9]+(?= MiB)       -&gt; 只取 MiB 前面的数字</span></div></div><div><div><div>2</div></div><div><span>(?&lt;=version=)[0-9.]+ -&gt; 只取 version= 后面的数字</span></div></div><div><div><div>3</div></div><div><span>foo(?!bar)           -&gt; foo 后面不能紧跟 bar</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>反向引用则要求后面再次出现前面捕获过的文本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\b([A-Za-z]+)\s+\1\b</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它能找到 <code>the the</code> 这样的连续重复词。Python 和 JavaScript 还支持按名称引用，但语法仍然不同。</p><p>我把 Lookaround 和反向引用放在后面，不是因为它们不重要，而是因为它们最容易降低可移植性。CMake Regex 不支持 Lookaround，C++ 默认 ECMAScript 方言也不该假设拥有现代 JavaScript 的全部能力。能用捕获分组取得值，或者先定位再做第二步判断时，朴素方案通常更容易搬家。</p></section></section>
<section><h2>把一个版本模式从宽改窄<a href="#把一个版本模式从宽改窄"><span>#</span></a></h2><p>我的实验页面抽取成纯文本后是这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Build date: 2026.09.07</span></div></div><div><div><div>2</div></div><div><span>Preview: v2.8.0-beta.1</span></div></div><div><div><div>3</div></div><div><span>Latest stable: v2.7.3</span></div></div><div><div><div>4</div></div><div><span>Requires Runtime 1.4.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果我只写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[0-9]</span><span>+(?:</span><span>\.</span><span>[0-9]</span><span>+)+</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Python 实际找到了 4 项：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>['2026.09.07', '2.8.0', '2.7.3', '1.4.0']</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>模式没有错，它忠实找出了所有 “由点分隔的数字”。错的是需求只有 7 个字，“从页面提取版本”。页面上有 4 个都像版本的数字时，正则不可能读心。</p><p>我先把需求改写成人话：</p><blockquote><p>找到以 <code>Latest stable:</code> 开头的完整行，允许冒号后有空白和一个 <code>v</code>，捕获恰好 3 段 ASCII 数字组成的稳定版号，不接受任何尾随字符。</p></blockquote><p>然后逐块翻译：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>^Latest stable:                锚定标签和行首</span></div></div><div><div><div>2</div></div><div><span>\s*                            允许 0 个或更多空白</span></div></div><div><div><div>3</div></div><div><span>v                              要求 v</span></div></div><div><div><div>4</div></div><div><span>([0-9]+\.[0-9]+\.[0-9]+)      捕获三段版本号</span></div></div><div><div><div>5</div></div><div><span>$                              到行尾必须结束</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最终模式是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>^Latest stable:\s</span><span>*</span><span>v</span><span>(</span></span><span>[0-9]</span><span>+</span><span>\.</span><span>[0-9]</span><span>+</span><span>\.</span><span>[0-9]</span><span><span>+)</span><span>$</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>加上多行模式后，它从 4 个候选收窄到 1 个，得到 <code>2.7.3</code>。这里不是正则突然变聪明了，而是我终于把上下文也写进了规格。</p><div><div><div></div><div>Important</div></div><div><p>先写清输入契约，再写正则。<code>[\d.]+</code> 甚至允许只有点号的字符串，<code>[0-9]+(?:\.[0-9]+)+</code> 也分不清日期和版本。上下文、锚点和负例比再堆一个字符类更有用。</p></div></div></section>
<section><h2>反斜杠为什么总像在繁殖<a href="#反斜杠为什么总像在繁殖"><span>#</span></a></h2><p>屏幕上看到的模式经常要穿过两层语言：先由宿主语言生成字符串，再由正则引擎解释这个字符串。</p><p>我想交给正则引擎的内容是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[0-9]</span><span>+</span><span>\.</span><span>[0-9]</span><span>+</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在不同宿主里可能写成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Python raw string</span></div></div><div><div><div>2</div></div><div><span><span>pattern </span><span>=</span><span> </span></span><span>r</span><span>"</span><span>[</span><span>0-9</span><span>]+</span><span>\.</span><span>[</span><span>0-9</span><span>]+</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// JavaScript 正则字面量</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>pattern</span><span> </span><span>=</span><span> /</span><span>[0-9]</span><span><span>+</span><span>\.</span></span><span>[0-9]</span><span><span>+</span><span>/</span></span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// RegExp 构造函数还要经过字符串解析</span></div></div><div><div><div>5</div></div><div><span>const</span><span> </span><span>dynamicPattern</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>RegExp</span><span>(</span><span>"[0-9]+</span><span>\\</span><span>.[0-9]+"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// C++ raw string literal</span></div></div><div><div><div>2</div></div><div><span>const</span><span> std::</span><span>regex</span><span> </span><span>pattern</span><span>{</span><span>R"([0-9]+\.[0-9]+)"</span><span>};</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// 普通 C++ 字符串</span></div></div><div><div><div>5</div></div><div><span>const</span><span> std::</span><span>regex</span><span> </span><span>noisy</span><span>{</span><span>"[0-9]+</span><span>\\</span><span>.[0-9]+"</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"regex"</span><span>: </span><span>"[0-9]+</span><span>\\</span><span>.[0-9]+"</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这几行最终送给引擎的是同一个模式。JSON 文件中的 <code>\\</code> 不是正则需要两个反斜杠，而是 JSON 先把它还原为一个 <code>\</code>，正则再把 <code>\.</code> 解释为字面点号。</p><p>我现在调试这类问题时会打印宿主字符串的真实值，而不是盯着源码数斜杠。源码是包装盒，引擎收到的字符串才是货物。</p></section>
<section><h2>Python: 把模式写成可执行规格<a href="#python-把模式写成可执行规格"><span>#</span></a></h2><p>Python 的 <a href="https://docs.python.org/3/library/re.html" target="_blank"><code>re</code> 文档</a>明确建议用 raw string 表示模式。我的完整实验如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> re</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>page </span><span>=</span><span> </span></span><span>"""Build date: 2026.09.07</span></div></div><div><div><div>4</div></div><div><span>Preview: v2.8.0-beta.1</span></div></div><div><div><div>5</div></div><div><span>Latest stable: v2.7.3</span></div></div><div><div><div>6</div></div><div><span>Requires Runtime 1.4.0</span></div></div><div><div><div>7</div></div><div><span>"""</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>loose </span><span>=</span><span> re.</span><span>compile</span><span>(</span></span><span>r</span><span>"</span><span>[</span><span>0-9</span><span>]+</span><span><span>(?:</span><span>\.</span></span><span>[</span><span>0-9</span><span>]+</span><span>)</span><span>+</span><span>"</span><span>)</span></div></div><div><div><div>10</div></div><div><span><span>scoped </span><span>=</span><span> re.</span><span>compile</span><span>(</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>r</span><span>"^Latest stable:\s</span><span>*</span><span><span>v</span><span>(</span></span><span>?P&lt;version&gt;</span><span>[</span><span>0-9</span><span>]+</span><span>\.</span><span>[</span><span>0-9</span><span>]+</span><span>\.</span><span>[</span><span>0-9</span><span>]+</span><span><span>)</span><span>$"</span></span><span>,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>re.</span><span>MULTILINE</span><span>,</span></div></div><div><div><div>13</div></div><div><span>)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>print</span><span>(</span><span>"loose:"</span><span><span>, loose.</span><span>findall</span><span>(page))</span></span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>match </span><span>=</span><span> scoped.</span><span>search</span><span>(page)</span></span></div></div><div><div><div>18</div></div><div><span>if</span><span> match </span><span>is</span><span> </span><span>None</span><span>:</span></div></div><div><div><div>19</div></div><div><span>    </span><span>raise</span><span> </span><span>RuntimeError</span><span>(</span><span>"stable version not found"</span><span>)</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>print</span><span>(</span><span>"stable:"</span><span><span>, match.</span><span>group</span><span>(</span></span><span>"version"</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>loose: ['2026.09.07', '2.8.0', '2.7.3', '1.4.0']</span></div></div><div><div><div>2</div></div><div><span>stable: 2.7.3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里有 5 个常用 API，名字很像，但契约不同：</p>

<table><thead><tr><th>API</th><th>做什么</th><th>我会在何时使用</th></tr></thead><tbody><tr><td><code>re.search()</code></td><td>在任意位置找第一处</td><td>从页面或日志提取一个字段</td></tr><tr><td><code>re.match()</code></td><td>只从字符串开头尝试</td><td>解析有固定前缀的行</td></tr><tr><td><code>re.fullmatch()</code></td><td>整个字符串必须匹配</td><td>校验文件名、ID、版本格式</td></tr><tr><td><code>re.findall()</code></td><td>返回所有匹配或捕获值</td><td>只需要结果列表</td></tr><tr><td><code>re.finditer()</code></td><td>迭代带位置和分组的 Match</td><td>需要每一处的上下文</td></tr><tr><td><code>re.sub()</code></td><td>替换匹配内容</td><td>规范化文本</td></tr></tbody></table><p><code>findall()</code> 有一个容易忽略的行为：没有捕获组时返回完整匹配；有一个捕获组时返回该组；有多个捕获组时返回 Tuple。维护代码时，这种返回形状会因为加一个括号而变化。我需要稳定的详细结果时会用 <code>finditer()</code>，只用于结构的括号则写成 <code>(?:...)</code>。</p><p>替换也可以复用命名组：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>versioned </span><span>=</span><span> </span></span><span>"tool_2.7.3_windows.zip"</span></div></div><div><div><div>2</div></div><div><span><span>normalized </span><span>=</span><span> re.</span><span>sub</span><span>(</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>r</span><span><span>"^</span><span>(</span></span><span>?P&lt;name&gt;</span><span>[</span><span>a-z</span><span>]+</span><span><span>)</span><span>_</span><span>(</span></span><span>?P&lt;version&gt;</span><span>[</span><span>0-9.</span><span>]+</span><span><span>)</span><span>_</span><span>(</span></span><span>?P&lt;os&gt;</span><span>[</span><span>a-z</span><span>]+</span><span><span>)</span><span>\.</span><span>zip$"</span></span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>r</span><span><span>"</span><span>\g</span><span>&lt;name&gt;-v</span><span>\g</span><span>&lt;version&gt;-</span><span>\g</span><span>&lt;os&gt;.zip"</span></span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>versioned,</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div><span>assert</span><span><span> normalized </span><span>==</span><span> </span></span><span>"tool-v2.7.3-windows.zip"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>用 <code>re.VERBOSE</code> 拆掉一行咒语<a href="#用-reverbose-拆掉一行咒语"><span>#</span></a></h3><p>模式一长，我宁愿让它多占几行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>artifact </span><span>=</span><span> re.</span><span>compile</span><span>(</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>r</span><span>"""</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>^tool-v</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>(</span><span>?P&lt;version&gt;</span><span>[</span><span>0-9</span><span>]+</span><span>\.</span><span>[</span><span>0-9</span><span>]+</span><span>\.</span><span>[</span><span>0-9</span><span>]+</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>-</span><span>(</span><span>?P&lt;os&gt;</span><span>windows</span><span>|</span><span><span>linux</span><span>)</span></span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>-</span><span>(</span><span>?P&lt;arch&gt;</span><span>x64</span><span>|</span><span><span>arm64</span><span>)</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>\.</span><span>zip$</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>"""</span><span>,</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>re.</span><span>VERBOSE</span><span>,</span></div></div><div><div><div>10</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>re.VERBOSE</code> 会忽略模式中的大部分空白，并允许 <code>#</code> 注释。字符类里的空白仍有意义，需要匹配字面空格时也要明确写出。它没有改变正则能力，只是把一条压缩后的机器指令恢复成了可以审查的规格。</p></section><section><h3>Flags 不是装饰<a href="#flags-不是装饰"><span>#</span></a></h3><p>我最常用的 Python Flags 是：</p><ul>
<li><code>re.IGNORECASE</code> 或 <code>re.I</code>：忽略大小写。</li>
<li><code>re.MULTILINE</code> 或 <code>re.M</code>：让 <code>^</code> 和 <code>$</code> 作用于每一行。</li>
<li><code>re.DOTALL</code> 或 <code>re.S</code>：让 <code>.</code> 也匹配换行。</li>
<li><code>re.VERBOSE</code> 或 <code>re.X</code>：允许分行和注释。</li>
<li><code>re.ASCII</code> 或 <code>re.A</code>：让 <code>\w</code>、<code>\d</code>、<code>\s</code> 等使用 ASCII 语义。</li>
</ul><p><code>MULTILINE</code> 改的是锚点，<code>DOTALL</code> 改的是点号。二者名字都容易让我脑内自动补全成 “跨行匹配”，但它们控制的是两件不同的事。</p></section></section>
<section><h2>Scoop: 从非 GitHub 页面提取稳定版<a href="#scoop-从非-github-页面提取稳定版"><span>#</span></a></h2><p>GitHub Releases 可以让 Scoop 直接使用 <code>"checkver": "github"</code>。真正逼我写正则的，通常是厂商自己的下载页。</p><p>假设页面源码中有 3 个链接：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>a</span><span> </span><span>href</span><span>=</span><span>"/downloads/tool-v2.8.0-beta.1-windows-x64.zip"</span><span>&gt;Preview&lt;/</span><span>a</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span>&lt;</span><span>a</span><span> </span><span>href</span><span>=</span><span>"/downloads/tool-v2.7.3-windows-x64.zip"</span><span>&gt;Latest stable&lt;/</span><span>a</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span>&lt;</span><span>a</span><span> </span><span>href</span><span>=</span><span>"/runtime/runtime-v1.4.0-windows-x64.zip"</span><span>&gt;Runtime&lt;/</span><span>a</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的 Manifest 可以这样写：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"version"</span><span>: </span><span>"2.7.3"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"homepage"</span><span>: </span><span>"https://vendor.example/tool"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"architecture"</span><span>: {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"url"</span><span>: </span><span>"https://vendor.example/downloads/tool-v2.7.3-windows-x64.zip"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>"hash"</span><span>: </span><span>"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>10</div></div><div><span>  </span><span>"checkver"</span><span>: {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>"url"</span><span>: </span><span>"https://vendor.example/tool/downloads"</span><span>,</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"regex"</span><span>: </span><span>"href=</span><span>\"</span><span>/downloads/tool-v(?&lt;version&gt;[0-9]+</span><span>\\</span><span>.[0-9]+</span><span>\\</span><span>.[0-9]+)-windows-x64</span><span>\\</span><span>.zip</span><span>\"</span><span>"</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>14</div></div><div><span>  </span><span>"autoupdate"</span><span>: {</span></div></div><div><div><div>15</div></div><div><span>    </span><span>"architecture"</span><span>: {</span></div></div><div><div><div>16</div></div><div><span>      </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>17</div></div><div><span>        </span><span>"url"</span><span>: </span><span>"https://vendor.example/downloads/tool-v$version-windows-x64.zip"</span></div></div><div><div><div>18</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里我没有只抓 <code>[\d.]+</code>，而是把路径前缀、产品名、平台、架构和 <code>.zip</code> 后缀都放进模式。预览版因为版本后紧跟 <code>-beta.1</code>，无法满足 <code>-windows</code>；Runtime 又没有正确的路径和产品名。3 个链接只剩 1 个候选。</p><p><code>(?&lt;version&gt;...)</code> 是命名捕获。Scoop 会把它作为版本使用，并能把其他命名组暴露成 <code>$matchName</code> 一类变量。官方的 <a href="https://github.com/ScoopInstaller/Scoop/wiki/App-Manifest-Autoupdate#using-regex-in-checkver" target="_blank">Autoupdate 文档</a>列出了 <code>regex</code>、<code>replace</code>、<code>reverse</code> 以及命名捕获的用法。</p><section><h3>先在 PowerShell 里看清每一组<a href="#先在-powershell-里看清每一组"><span>#</span></a></h3><p>在写入 JSON 之前，我会先绕开 JSON 转义，直接用 PowerShell 验证 .NET Regex：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$page</span><span><span> </span><span>=</span><span> </span></span><span>Get-Content</span><span><span> .\fixture.html </span><span>-</span><span>Raw</span></span></div></div><div><div><div>2</div></div><div><span>$pattern</span><span><span> </span><span>=</span><span> </span></span><span>'href="/downloads/tool-v(?&lt;version&gt;[0-9]+\.[0-9]+\.[0-9]+)-windows-x64\.zip"'</span></div></div><div><div><div>3</div></div><div><span>$match</span><span><span> </span><span>=</span><span> [</span></span><span>regex</span><span>]::Match(</span><span>$page</span><span>, </span><span>$pattern</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>if</span><span><span> (</span><span>-not</span><span> </span></span><span>$match.Success</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>throw</span><span> </span><span>'stable version not found'</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>$match.Value</span></div></div><div><div><div>10</div></div><div><span>$match.Groups</span><span>[</span><span>'version'</span><span>].Value</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我用 PowerShell 7.6.5 运行后得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>href="/downloads/tool-v2.7.3-windows-x64.zip"</span></div></div><div><div><div>2</div></div><div><span>2.7.3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再把模式放进 Manifest，并运行真正的 Scoop 工具：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$checkver</span><span><span> </span><span>=</span><span> </span></span><span>"</span><span>$(</span><span>scoop prefix scoop</span><span>)</span><span>\bin\checkver.ps1"</span></div></div><div><div><div>2</div></div><div><span>&amp; </span><span>$checkver</span><span> tool .\bucket</span></div></div><div><div><div>3</div></div><div><span>&amp; </span><span>$checkver</span><span><span> tool .\bucket </span><span>-</span><span>Update</span></span></div></div><div><div><div>4</div></div><div><span>scoop install .\bucket\tool.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>checkver</code> 成功只证明版本提取成功，<code>-Update</code> 才会检查新 URL 能否生成并下载，最后的本地安装才覆盖解压、哈希和入口。验证链从 “一个 Match” 变成了 “提取 -&gt; 更新 -&gt; 下载 -&gt; 安装”。</p><div><div><div></div><div>Tip</div></div><div><p>页面如果提供稳定 JSON API、XML、RSS 或可预测的目录索引，优先用结构化来源。Scoop <code>checkver</code> 支持 JSONPath 和 XPath。HTML 文案和 CSS Class 是给人和前端看的，通常比机器接口更容易变化。</p></div></div></section><section><h3><code>reverse</code> 不是排序<a href="#reverse-不是排序"><span>#</span></a></h3><p>Scoop 的 <code>"reverse": true</code> 会取最后一个匹配，而不是比较语义版本后取最大值。如果页面按时间倒序排列，默认第一项可能正好是最新；如果按升序排列，最后一项可能正好是最新。但这依赖页面顺序，不是版本排序算法。</p><p>我会先问自己：页面是否承诺顺序？如果没有，我更愿意寻找 <code>Latest stable</code> 上下文或结构化接口，而不是把偶然的 DOM 顺序写进更新逻辑。</p></section></section>
<section><h2>CMake: 从第三方 Header 读取版本<a href="#cmake-从第三方-header-读取版本"><span>#</span></a></h2><p>CMake 自己有一套较小的 Regex 方言。<a href="https://cmake.org/cmake/help/latest/command/string.html#regex-specification" target="_blank"><code>string(REGEX)</code> 文档</a>列出的核心能力包括锚点、点号、字符类、<code>*</code>、<code>+</code>、<code>?</code>、<code>|</code> 和捕获组。它没有列出 <code>\d</code>、<code>{m,n}</code>、非捕获组或 Lookaround，所以我不会把 Python 模式原样粘进去。</p><p>假设一个 vendored 第三方库只在 <code>version.h</code> 暴露版本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#define</span><span> </span><span>MYLIB_VERSION_MAJOR</span><span> </span><span>2</span></div></div><div><div><div>2</div></div><div><span>#define</span><span> </span><span>MYLIB_VERSION_MINOR</span><span> </span><span>7</span></div></div><div><div><div>3</div></div><div><span>#define</span><span> </span><span>MYLIB_VERSION_PATCH</span><span> </span><span>3</span></div></div><div><div><div>4</div></div><div><span>#define</span><span> </span><span>MYLIB_ABI_VERSION</span><span> </span><span>4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我只筛选前三行，再取每行末尾数字：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>file</span><span>(STRINGS </span><span>"${CMAKE_CURRENT_SOURCE_DIR}/vendor/mylib/version.h"</span><span> version_lines</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>REGEX [[^#define MYLIB_VERSION_(MAJOR|MINOR|PATCH) [0-9]+$]])</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>set</span><span>(parts)</span></div></div><div><div><div>5</div></div><div><span>foreach</span><span>(line IN LISTS version_lines)</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>string</span><span>(REGEX MATCH [[([0-9]+)$]] _ </span><span>"${line}"</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>list</span><span>(APPEND parts </span><span>"${CMAKE_MATCH_1}"</span><span>)</span></div></div><div><div><div>8</div></div><div><span>endforeach</span><span>()</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>list</span><span>(JOIN parts </span><span>"."</span><span> MYLIB_VERSION)</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>if</span><span>(</span><span>NOT</span><span> MYLIB_VERSION </span><span>MATCHES</span><span> [[^[0-9]+</span><span>\.</span><span>[0-9]+</span><span>\.</span><span>[0-9]+$]])</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>message</span><span>(FATAL_ERROR </span><span>"Cannot parse mylib version: ${MYLIB_VERSION}"</span><span>)</span></div></div><div><div><div>14</div></div><div><span>endif</span><span>()</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>message</span><span>(STATUS </span><span>"MYLIB_VERSION=${MYLIB_VERSION}"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我实际用 CMake 4.1.6 运行了这段脚本，输出是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- MYLIB_VERSION=2.7.3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>[[...]]</code> 是 CMake Bracket Argument。它不处理反斜杠转义，因此模式里的 <code>\.</code> 可以原样到达正则引擎。若改用普通双引号，某些反斜杠还要经过 CMake 语言解析。CMake 官方文档也用 Bracket Argument 展示如何避免这层噪声。</p><p>这里还有两个边界：</p><ol>
<li><code>CMAKE_MATCH_1</code> 到 <code>CMAKE_MATCH_9</code> 保存捕获组，组数有限，而且没有命名捕获。</li>
<li>CMake 4.1 通过 Policy <code>CMP0186</code> 调整了重复搜索中 <code>^</code> 的行为。跨版本维护时，应该让 <code>cmake_minimum_required()</code> 明确策略范围，并针对项目最低版本验证。</li>
</ol><section><h3>CMake 中不要把 Glob 当 Regex<a href="#cmake-中不要把-glob-当-regex"><span>#</span></a></h3><p><code>file(GLOB)</code> 的 <code>*.cpp</code> 是 Glob，不是 Regex：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>file</span><span>(GLOB sources CONFIGURE_DEPENDS </span><span>"src/*.cpp"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>*</code> 在 Glob 中表示任意字符序列；在 Regex 中，它只重复前一个模式。相似的符号掩盖了不同的语法。顺带一提，CMake 官方仍不建议用 Glob 收集构建系统的源文件，因为新增或删除文件可能让生成系统何时重新配置变得不直观；显式列出源文件通常更稳。</p></section></section>
<section><h2>vcpkg: 正则应该待在哪一层<a href="#vcpkg-正则应该待在哪一层"><span>#</span></a></h2><p>vcpkg Port 的 <code>portfile.cmake</code> 使用 CMake 语言，因此真要匹配文本时，面对的还是 CMake Regex 方言。但我不会让每次构建都抓网页，再用正则猜今天的最新版本。Port 构建需要可复现输入，“最新” 是一个会随时间移动的目标。</p><p>非 GitHub 上游通常使用 <a href="https://learn.microsoft.com/en-us/vcpkg/maintainers/functions/vcpkg_download_distfile" target="_blank"><code>vcpkg_download_distfile()</code></a>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>set</span><span>(VERSION </span><span>"2.7.3"</span><span>)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>vcpkg_download_distfile(ARCHIVE</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>URLS </span><span>"https://vendor.example/releases/mylib-${VERSION}.tar.gz"</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>FILENAME </span><span>"mylib-${VERSION}.tar.gz"</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>SHA512 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>vcpkg_extract_source_archive(</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>SOURCE_PATH</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>ARCHIVE </span><span>"${ARCHIVE}"</span></div></div><div><div><div>12</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的版本和 SHA512 都是固定输入。更新机器人或维护脚本可以在上游页面用正则发现 <code>2.7.4</code>，但它应该生成一次明确变更，重新计算哈希，经过审查后提交。构建阶段只消费已经确定的版本。</p><p>如果源码在 GitHub，优先使用 <a href="https://learn.microsoft.com/en-us/vcpkg/maintainers/functions/vcpkg_from_github" target="_blank"><code>vcpkg_from_github()</code></a>，并把 <code>REF</code> 固定到 Tag 或 Commit；官方文档也明确说 <code>REF</code> 不应是 Branch。正则适合发现候选版本，不适合替代依赖锁定。</p><div><div><div></div><div>Warning</div></div><div><p>不要为了让 CI 继续跑而跳过哈希。正则回答 “哪个文本像版本”，SHA512 回答 “下载到的字节是不是维护者审查过的那一份”。它们解决的是两个完全不同的问题。</p></div></div></section>
<section><h2>C++: <code>std::regex_search</code> 和 <code>std::regex_match</code> 不是一回事<a href="#c-stdregex_search-和-stdregex_match-不是一回事"><span>#</span></a></h2><p>C++11 在 <code>&lt;regex&gt;</code> 中提供了标准正则库。根据 <a href="https://en.cppreference.com/w/cpp/regex.html" target="_blank">cppreference 的正则库总览</a>，<code>std::regex</code> 默认使用修改过的 ECMAScript Grammar，也可以显式选择 Basic、Extended、awk、grep 或 egrep 方言。</p><p>我用完整匹配验证发布物名称：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;regex&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>static</span><span> </span><span>const</span><span> std::</span><span>regex</span><span> </span><span>artifact</span><span>{</span></div></div><div><div><div>7</div></div><div><span>        </span><span>R"(^tool-v([0-9]+\.[0-9]+\.[0-9]+)-(windows|linux)-(x64|arm64)\.zip$)"</span><span>,</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>std::regex::</span><span>ECMAScript</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>const</span><span> std::</span><span>string</span><span> </span><span>filename</span><span> </span><span>=</span><span> </span><span>"tool-v2.7.3-windows-x64.zip"</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>std::</span><span>smatch</span><span> </span><span>match</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>std</span><span>::</span></span><span>regex_match</span><span>(</span><span>filename</span><span>, </span><span>match</span><span>, </span><span>artifact</span><span>)) {</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>std::</span><span>cerr</span><span> </span><span>&lt;&lt;</span><span> </span><span>"invalid artifact name</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>16</div></div><div><span>        </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"version="</span><span> </span><span>&lt;&lt;</span><span> </span><span>match</span><span>[</span><span>1</span><span>]</span></div></div><div><div><div>20</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>" os="</span><span> </span><span>&lt;&lt;</span><span> </span><span>match</span><span>[</span><span>2</span><span>]</span></div></div><div><div><div>21</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>" arch="</span><span> </span><span>&lt;&lt;</span><span> </span><span>match</span><span>[</span><span>3</span><span>] </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我用 GCC 15.3.0 编译运行，得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>version=2.7.3 os=windows arch=x64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>std::regex_match()</code> 要求整个序列匹配，适合校验。<code>std::regex_search()</code> 只要求某个子序列匹配，适合从一段文本提取。即使模式已经有 <code>^</code> 和 <code>$</code>，我仍会选择表达意图的 API，让下一位读代码的人不用先审计两个锚点。</p><p>C++ 标准 Regex 没有命名捕获，所以 <code>match[1]</code>、<code>match[2]</code>、<code>match[3]</code> 的顺序就是接口。只用于结构的括号要尽量少；修改模式时，也要同步检查消费这些下标的代码。</p><p><code>std::regex</code> 构造可能做明显多于字符串比较的工作。我不会在处理每一行的循环里重新构造同一个模式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 不要把固定模式放进热循环</span></div></div><div><div><div>2</div></div><div><span>for</span><span> (</span><span>const</span><span> </span><span>auto&amp;</span><span> </span><span>line</span><span> : </span><span>lines</span><span>) {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>std::</span><span>regex</span><span> </span><span>pattern</span><span>{</span><span>R"(... )"</span><span>};</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>// ...</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>// 构造一次，重复使用</span></div></div><div><div><div>8</div></div><div><span>static</span><span> </span><span>const</span><span> std::</span><span>regex</span><span> </span><span>pattern</span><span>{</span><span>R"(... )"</span><span>};</span></div></div><div><div><div>9</div></div><div><span>for</span><span> (</span><span>const</span><span> </span><span>auto&amp;</span><span> </span><span>line</span><span> : </span><span>lines</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>// std::regex_search(line, pattern)</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果需求只是判断固定前缀、后缀或单个分隔符，<code>starts_with()</code>、<code>ends_with()</code>、<code>find()</code>、<code>from_chars()</code> 往往更直接，也更容易得到稳定性能。正则是工具，不是加入项目后的强制税种。</p></section>
<section><h2>JavaScript/TypeScript: 字面量、全局状态和动态模式<a href="#javascripttypescript-字面量全局状态和动态模式"><span>#</span></a></h2><p>JavaScript 中的正则是对象。<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions" target="_blank">MDN Regular Expressions Guide</a>列出了两种构造方法：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 固定模式，优先使用字面量</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>fixed</span><span> </span><span>=</span><span> /tool-v(</span><span>[0-9]</span><span><span>+</span><span>\.</span></span><span>[0-9]</span><span><span>+</span><span>\.</span></span><span>[0-9]</span><span><span>+</span><span>)/</span></span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// 模式来自运行时数据时使用构造函数</span></div></div><div><div><div>5</div></div><div><span>const</span><span> </span><span>prefix</span><span> </span><span>=</span><span> </span><span>"tool"</span><span>;</span></div></div><div><div><div>6</div></div><div><span>const</span><span> </span><span>dynamic</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>RegExp</span><span>(</span><span>`</span><span>${</span><span>prefix</span><span>}</span><span>-v([0-9]+</span><span>\\</span><span>.[0-9]+</span><span>\\</span><span>.[0-9]+)`</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>构造函数接收字符串，所以 <code>\.</code> 要写成 <code>\\.</code>。如果变量可能包含 <code>.</code>、<code>*</code>、<code>[</code> 等元字符，不能直接插值。现代运行时提供 <code>RegExp.escape()</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>product</span><span> </span><span>=</span><span> </span><span>"tool.next"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>pattern</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>RegExp</span><span>(</span><span>`^</span><span>${</span><span>RegExp</span><span>.</span><span>escape</span><span><span>(</span><span>product</span><span>)</span></span><span>}</span><span>-v([0-9]+</span><span>\\</span><span>.[0-9]+</span><span>\\</span><span>.[0-9]+)$`</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>RegExp.escape()</code> 在较新的 JavaScript Runtime 中可用；项目的 Runtime 和 TypeScript <code>lib</code> 较旧时，要先核对兼容性，不要用手写的不完整替换冒充同等实现。</p><p>命名捕获让 TypeScript 端更清楚：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>page</span><span> </span><span>=</span><span> </span><span>`Build date: 2026.09.07</span></div></div><div><div><div>2</div></div><div><span>Preview: v2.8.0-beta.1</span></div></div><div><div><div>3</div></div><div><span>Latest stable: v2.7.3</span></div></div><div><div><div>4</div></div><div><span>Requires Runtime 1.4.0`</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>const</span><span> </span><span>stable</span><span> </span><span>=</span><span> /</span><span>^</span><span>Latest stable:</span><span>\s</span><span><span>*</span><span>v(?&lt;</span></span><span>version</span><span>&gt;</span><span>[0-9]</span><span><span>+</span><span>\.</span></span><span>[0-9]</span><span><span>+</span><span>\.</span></span><span>[0-9]</span><span><span>+</span><span>)</span></span><span>$</span><span>/</span><span>m</span><span>.</span><span>exec</span><span><span>(</span><span>page</span><span>);</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>if</span><span> (</span><span>!</span><span><span>stable</span><span>?.</span></span><span>groups</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>  </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span>(</span><span>"stable version not found"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>stable</span><span>.</span></span><span>groups</span><span>.</span><span>version</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>2.7.3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>常用 Flags：</p>

<table><thead><tr><th>Flag</th><th>作用</th></tr></thead><tbody><tr><td><code>g</code></td><td>查找全部，并让 Regex 对象维护 <code>lastIndex</code></td></tr><tr><td><code>i</code></td><td>忽略大小写</td></tr><tr><td><code>m</code></td><td>多行锚点</td></tr><tr><td><code>s</code></td><td>点号匹配换行</td></tr><tr><td><code>u</code></td><td>按 Unicode Code Point 解释模式</td></tr><tr><td><code>v</code></td><td>更完整的 Unicode Set 能力，不能和 <code>u</code> 同时使用</td></tr><tr><td><code>y</code></td><td>Sticky，从 <code>lastIndex</code> 指定位置开始匹配</td></tr><tr><td><code>d</code></td><td>为匹配和捕获组生成索引</td></tr></tbody></table><section><h3>带 <code>g</code> 的 <code>test()</code> 有状态<a href="#带-g-的-test-有状态"><span>#</span></a></h3><p>这是 JavaScript 最容易制造幽灵 Bug 的地方之一：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>digits</span><span> </span><span>=</span><span> /</span><span>[0-9]</span><span><span>+</span><span>/</span></span><span>g</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>digits</span><span>.</span></span><span>test</span><span>(</span><span>"v2"</span><span>); </span><span>// true, lastIndex 前进</span></div></div><div><div><div>4</div></div><div><span><span>digits</span><span>.</span></span><span>test</span><span>(</span><span>"v2"</span><span>); </span><span>// false, 从上次位置继续</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个输入，连续调用两次可能得到不同结果，因为 <code>g</code> 或 <code>y</code> 模式会更新 <code>lastIndex</code>。只做布尔校验时，我不用 <code>g</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>digits</span><span> </span><span>=</span><span> /</span><span>[0-9]</span><span><span>+</span><span>/</span></span><span>;</span></div></div><div><div><div>2</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>digits</span><span>.</span></span><span>test</span><span>(</span><span>"v2"</span><span>));</span></div></div><div><div><div>3</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>digits</span><span>.</span></span><span>test</span><span>(</span><span>"v2"</span><span>));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>需要所有匹配及其分组时，<code>matchAll()</code> 更直观：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>text</span><span> </span><span>=</span><span> </span><span>"tool-v2.7.3 and runtime-v1.4.0"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>versions</span><span> </span><span>=</span><span> [</span><span>...</span><span><span>text</span><span>.</span></span><span>matchAll</span><span>(</span><span>/v(?&lt;</span><span>version</span><span>&gt;</span><span>[0-9]</span><span><span>+</span><span>\.</span></span><span>[0-9]</span><span><span>+</span><span>\.</span></span><span>[0-9]</span><span><span>+</span><span>)/</span></span><span>g</span><span>)]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>.</span><span>map</span><span><span>((</span><span>match</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>match</span><span>.</span></span><span>groups</span><span>?.</span><span>version</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span><span>(</span><span>versions</span><span>); </span></span><span>// ["2.7.3", "1.4.0"]</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>同一模式为什么不能到处复制<a href="#同一模式为什么不能到处复制"><span>#</span></a></h2><p>我把本文几个环境放在一起：</p>

<table><thead><tr><th>能力</th><th>Python <code>re</code></th><th>JavaScript</th><th>C++ ECMAScript</th><th>CMake</th><th>.NET / Scoop</th></tr></thead><tbody><tr><td><code>[0-9]</code>、<code>* + ?</code>、捕获组</td><td>是</td><td>是</td><td>是</td><td>是</td><td>是</td></tr><tr><td><code>{m,n}</code></td><td>是</td><td>是</td><td>是</td><td>文档未定义</td><td>是</td></tr><tr><td>非捕获组 <code>(?:...)</code></td><td>是</td><td>是</td><td>是</td><td>否</td><td>是</td></tr><tr><td>命名捕获</td><td><code>(?P&lt;n&gt;...)</code></td><td><code>(?&lt;n&gt;...)</code></td><td>否</td><td>否</td><td><code>(?&lt;n&gt;...)</code></td></tr><tr><td>懒惰量词</td><td>是</td><td>是</td><td>是</td><td>文档未定义</td><td>是</td></tr><tr><td>Lookbehind</td><td>有限制</td><td>现代引擎支持</td><td>不应依赖</td><td>否</td><td>是</td></tr><tr><td>完整匹配 API</td><td><code>fullmatch</code></td><td>用锚点</td><td><code>regex_match</code></td><td>用锚点</td><td>API 或锚点</td></tr></tbody></table><p>“文档未定义” 不等于 “我的机器试了一次没报错，所以可以用”。可移植代码依赖的是目标方言承诺的语法，不是某次实现恰好接受的字符。</p><p>这也解释了在线测试网站的局限。它非常适合高亮匹配和观察分组，但我必须把 Flavor 设成真正的引擎。即便网站上看起来正确，最终测试仍要回到项目使用的 Python、Node.js、C++ 标准库、CMake 或 PowerShell。</p></section>
<section><h2>我现在怎样从零写一个模式<a href="#我现在怎样从零写一个模式"><span>#</span></a></h2><p>我把过去的 “AI 生成 -&gt; 跑一下” 改成下面 7 步。</p><section><h3>1. 先写输入契约<a href="#1-先写输入契约"><span>#</span></a></h3><p>不要写 “提取版本”。要写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>输入是发布页源码。</span></div></div><div><div><div>2</div></div><div><span>只接受 Latest stable 下载链接。</span></div></div><div><div><div>3</div></div><div><span>版本必须是 3 段 ASCII 数字。</span></div></div><div><div><div>4</div></div><div><span>拒绝 Preview、Runtime、校验和链接和其他架构。</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2. 收集正例和负例<a href="#2-收集正例和负例"><span>#</span></a></h3><p>正例告诉模式应该接受什么，负例决定它不会宽到哪里去。我的文件名矩阵是：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>应该接受:</span></div></div><div><div><div>2</div></div><div><span>tool-v2.7.3-windows-x64.zip</span></div></div><div><div><div>3</div></div><div><span>tool-v10.0.0-linux-arm64.zip</span></div></div><div><div><div>4</div></div><div><span>tool-v0.1.0-windows-arm64.zip</span></div></div><div><div><div>5</div></div><div><span>tool-v3.12.5-linux-x64.zip</span></div></div><div><div><div>6</div></div><div><span>tool-v1.0.0-windows-x64.zip</span></div></div><div><div><div>7</div></div><div><span>tool-v99.8.7-linux-arm64.zip</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>必须拒绝:</span></div></div><div><div><div>10</div></div><div><span>tool-v2.7-windows-x64.zip</span></div></div><div><div><div>11</div></div><div><span>tool-v2.7.3-macos-x64.zip</span></div></div><div><div><div>12</div></div><div><span>tool-v2.7.3-windows-riscv64.zip</span></div></div><div><div><div>13</div></div><div><span>prefix-tool-v2.7.3-windows-x64.zip</span></div></div><div><div><div>14</div></div><div><span>tool-v2.7.3-windows-x64.zip.sha256</span></div></div><div><div><div>15</div></div><div><span>tool-v2.7.3.4-linux-x64.zip</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它不是随机样本。6 个负例分别攻击版本段数、OS、架构、前缀、后缀和额外版本段。</p></section><section><h3>3. 从字面骨架开始<a href="#3-从字面骨架开始"><span>#</span></a></h3><p>先写不会变化的部分：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>^tool-v-VERSION-</span><span>(</span><span>windows|linux</span><span>)</span><span>-</span><span>(</span><span>x64|arm64</span><span>)</span></span><span>\.</span><span>zip$</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>再把 <code>VERSION</code> 换成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[0-9]</span><span>+</span><span>\.</span><span>[0-9]</span><span>+</span><span>\.</span><span>[0-9]</span><span>+</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最终得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>^tool-v</span><span>(</span></span><span>[0-9]</span><span>+</span><span>\.</span><span>[0-9]</span><span>+</span><span>\.</span><span>[0-9]</span><span><span>+)</span><span>-</span><span>(</span><span>windows|linux</span><span>)</span><span>-</span><span>(</span><span>x64|arm64</span><span>)</span></span><span>\.</span><span>zip$</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4. 明确我要搜索还是验证<a href="#4-明确我要搜索还是验证"><span>#</span></a></h3><ul>
<li>提取页面字段：Search。</li>
<li>校验完整文件名：Full Match。</li>
<li>收集所有版本：Find All / Iterator。</li>
<li>重写名称：Replace。</li>
</ul><p>不要用一个模糊的 “Match” 代替操作语义。</p></section><section><h3>5. 逐组读取结果<a href="#5-逐组读取结果"><span>#</span></a></h3><p>我至少检查完整匹配、每个捕获组和匹配位置。只打印版本号会藏住模式实际吞掉的前后文本。</p></section><section><h3>6. 在真实 Runtime 建表测试<a href="#6-在真实-runtime-建表测试"><span>#</span></a></h3><p>Python 版本：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> re</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>artifact </span><span>=</span><span> re.</span><span>compile</span><span>(</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>r</span><span><span>"^tool-v</span><span>(</span></span><span>?P&lt;version&gt;</span><span>[</span><span>0-9</span><span>]+</span><span>\.</span><span>[</span><span>0-9</span><span>]+</span><span>\.</span><span>[</span><span>0-9</span><span>]+</span><span><span>)</span><span>-"</span></span></div></div><div><div><div>5</div></div><div><span>    </span><span>r</span><span><span>"</span><span>(</span></span><span>?P&lt;os&gt;</span><span>windows</span><span>|</span><span><span>linux</span><span>)</span><span>-</span><span>(</span></span><span>?P&lt;arch&gt;</span><span>x64</span><span>|</span><span><span>arm64</span><span>)</span><span>\.</span><span>zip$"</span></span></div></div><div><div><div>6</div></div><div><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>cases </span><span>=</span><span> [</span></span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>(</span><span>"tool-v2.7.3-windows-x64.zip"</span><span>, </span><span>True</span><span>),</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>(</span><span>"tool-v10.0.0-linux-arm64.zip"</span><span>, </span><span>True</span><span>),</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>(</span><span>"tool-v2.7-windows-x64.zip"</span><span>, </span><span>False</span><span>),</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>(</span><span>"tool-v2.7.3-macos-x64.zip"</span><span>, </span><span>False</span><span>),</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>(</span><span>"tool-v2.7.3-windows-x64.zip.sha256"</span><span>, </span><span>False</span><span>),</span></div></div><div><div><div>14</div></div><div><span>]</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>for</span><span> text, expected </span><span>in</span><span> cases:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>actual </span><span>=</span><span> artifact.</span><span>fullmatch</span><span>(text) </span><span>is</span><span> </span><span>not</span><span> </span><span>None</span></div></div><div><div><div>18</div></div><div><span>    </span><span>assert</span><span><span> actual </span><span>==</span><span> expected, text</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我在完整实验里运行了 12 个案例，结果是 <code>12/12 passed</code>。同一矩阵也通过了 Node.js 24.19.0 的 TypeScript 实现和 GCC 15.3.0 的 C++ 实现。一次成功输出升级成 3 个 Runtime、12 个边界案例；这才开始像验证，而不是占卜。</p></section><section><h3>7. 最后才压缩或搬进配置<a href="#7-最后才压缩或搬进配置"><span>#</span></a></h3><p>可读版本通过后，再把它放进 JSON、CMake 或一行配置。如果压缩过程引入双重转义，我会同时保留分块注释或测试脚本。正则短不等于简单，往往只代表空白被删了。</p></section></section>
<section><h2>AI 应该怎样参与<a href="#ai-应该怎样参与"><span>#</span></a></h2><p>我没有打算停止让 AI 帮我写正则。变化在于，我不再把输出当答案，而是当候选实现。</p><p>我现在会给 AI 这些材料：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>目标引擎: .NET Regex，由 Scoop checkver 使用</span></div></div><div><div><div>2</div></div><div><span>宿主格式: JSON，所以反斜杠需要 JSON 转义</span></div></div><div><div><div>3</div></div><div><span>要提取: 命名组 version</span></div></div><div><div><div>4</div></div><div><span>正例: ...</span></div></div><div><div><div>5</div></div><div><span>负例: ...</span></div></div><div><div><div>6</div></div><div><span>要求: 解释每一段，不使用 Lookbehind</span></div></div><div><div><div>7</div></div><div><span>输出: 原始 Regex、JSON 字符串、PowerShell 验证脚本</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后我会反问 4 件事：</p><ol>
<li>哪个输入会被意外接受？</li>
<li>哪个方言特性不可移植？</li>
<li>模式交给引擎前的真实字符串是什么？</li>
<li>是否存在不用正则的结构化方案？</li>
</ol><p>AI 很擅长快速枚举语法，但它看不到我没有提供的页面变化，也不会自动知道这段字符串最终经过 JSON、CMake 还是 C++ Raw String。规格和反例仍然在我手里。</p></section>
<section><h2>正则什么时候会变成第二个问题<a href="#正则什么时候会变成第二个问题"><span>#</span></a></h2><p>有些任务本身有明确语法树：HTML、JSON、XML、C++、URL。正则可以在局部定位文本，但通常不该承担完整解析。</p><p>我会在这些情况停下来：</p><ul>
<li>需要处理任意层级嵌套，例如括号或 HTML Element。</li>
<li>输入已有成熟 Parser，例如 JSON、YAML、XML、URL。</li>
<li>模式开始依赖大量 <code>.*?</code> 跨越不受控内容。</li>
<li>每修一个负例就增加一条互相覆盖的 Lookaround。</li>
<li>输入来自不可信用户，而模式有嵌套、含糊的重复。</li>
</ul><p>经典危险形状是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>^</span><span>(</span><span>a</span><span>+)+</span><span>$</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对一长串 <code>a</code> 加一个结尾 <code>!</code>，某些回溯引擎会反复尝试把同一串 <code>a</code> 切成不同分组，组合数迅速增长。这类 ReDoS 不需要模式看起来很长，只需要路径足够含糊。</p><p>实用规则很朴素：避免在重复里再放可以用多种方式消费同一字符的重复；给输入长度设上限；不要让攻击者同时控制模式和超长文本；对外部输入做性能验证。若场景要求线性时间保证，可以考虑 RE2、Rust <code>regex</code> 这类限制部分高级能力以换取更可预测执行时间的引擎。</p></section>
<section><h2>从今天往后<a href="#从今天往后"><span>#</span></a></h2><p>我怀疑正则表达式短期内不会消失。结构化 API 会越来越多，但版本页、日志、构建输出、旧式 Header 和文件名仍然是一大片半结构化文本。模型会继续生成越来越像答案的模式，可真正的进步不是让我更快复制它，而是让我能读出 <code>^</code> 在约束什么、哪个括号在捕获什么、为什么 CMake 不接受 Python 的写法，以及负例会从哪条缝里钻进来。</p><p>这篇文章最后留下的不是一条万能正则，而是一套比较无聊但可靠的循环：写契约，收集正反例，选择方言，分块构造，在真实 Runtime 运行，再接入自动化。现在 AI 仍然可以写第一稿，只是那串线路噪声终于需要接受 Code Review 了。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/understanding-git/</id>
      <title type="text">我会用 Git，但它到底在做什么？从对象数据库到现代工作流</title>
      <published>2026-09-06T12:00:00.000Z</published>
      <updated>2026-09-06T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/understanding-git/"/>
      <summary type="text">我用 Git 2.54.0 建了一个隔离仓库，从 blob、tree、commit、index、ref 和 reflog 拆开 add、commit、branch、merge、rebase、fetch、pull 与 push，再整理一套意图明确、可恢复的现代 Git 命令行。</summary>
      <content type="html"><![CDATA[<p>我会 <code>git add</code>、<code>git commit</code>、<code>git pull</code> 和 <code>git push</code>，但很长时间里，这些命令在我脑中只是一条能让代码抵达 GitHub 的传送带。AI 当然可以替我操作，而且大多数时候会操作得更快；但如果我自己分不清工作区、暂存区、引用和远端跟踪分支，就连 AI 刚刚改写了历史还是只更新了一个指针都无法判断。于是我用 Git 2.54.0 建了一个隔离仓库，从对象数据库开始，把平时敲下的命令逐层拆开。</p>
<p>这不是一张命令速查表。我的目标是先建立模型，再从模型推导命令。Git 自己给出的冷幽默式描述是 “the stupid content tracker”，但它的底层其实相当小：一个内容寻址对象数据库，加上一组会移动的名字，再配一个准备下一次快照的 index。命令很多，核心没有那么多。</p>
<div><div><div><div><span><p>git add</p></span></div><div><span><p>git commit</p></span></div><div><span><p>移动当前分支</p></span></div><div><span></span></div><div><span></span></div><div><span><p>git push</p></span></div><div><span><p>merge 或 rebase</p></span></div><div><span><p>工作区 Working Tree</p></span></div><div><span><p>暂存区 Index</p></span></div><div><span><p>对象数据库 Objects</p></span></div><div><span><p>refs/heads/main</p></span></div><div><span><p>HEAD</p></span></div><div><span><p>git fetch</p></span></div><div><span><p>refs/remotes/origin/main</p></span></div><div><span><p>远端 refs/heads/main</p></span></div>
</div><div><div><span><p>git add</p></span></div><div><span><p>git commit</p></span></div><div><span><p>移动当前分支</p></span></div><div><span></span></div><div><span></span></div><div><span><p>git push</p></span></div><div><span><p>merge 或 rebase</p></span></div><div><span><p>工作区 Working Tree</p></span></div><div><span><p>暂存区 Index</p></span></div><div><span><p>对象数据库 Objects</p></span></div><div><span><p>refs/heads/main</p></span></div><div><span><p>HEAD</p></span></div><div><span><p>git fetch</p></span></div><div><span><p>refs/remotes/origin/main</p></span></div><div><span><p>远端 refs/heads/main</p></span></div>
</div></div></div>
<p>这张图是全文的地图。后面的绝大多数 Git 操作，都只是复制内容、创建不可变对象，或者让某个引用从一个 commit 移到另一个 commit。</p>
<section><h2>先把几个基础概念钉牢<a href="#先把几个基础概念钉牢"><span>#</span></a></h2><p>我以前会说”这个文件已经在 Git 里了”，但这句话至少可能指 4 件不同的事：文件出现在工作目录里、内容进入 index、内容被某个 commit 引用，或者 commit 已经推到远端。Git 的难点有一半来自把这些状态压成了一个模糊的”保存”。</p><section><h3>仓库与工作区不是同一个东西<a href="#仓库与工作区不是同一个东西"><span>#</span></a></h3><p><strong>Repository（仓库）</strong> 主要指 <code>.git</code> 中的对象、引用和配置。<strong>Working tree（工作区或工作树）</strong> 是我在编辑器里看到、编译器会读取的那份文件集合。一个普通项目目录通常把两者放在一起：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pocket-git/          # 工作区根目录</span></div></div><div><div><div>2</div></div><div><span>├── README.md        # 可以直接编辑的文件</span></div></div><div><div><div>3</div></div><div><span>├── src/</span></div></div><div><div><div>4</div></div><div><span>│   └── app.txt</span></div></div><div><div><div>5</div></div><div><span>└── .git/            # 仓库元数据与对象数据库</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>删除工作区文件不等于立即删除历史中的 blob；删除 <code>.git</code> 则会让这个目录失去本地 Git 历史、refs 和 index，只剩普通文件。bare repository 没有普通工作区，服务器端仓库经常采用这种形态。</p><div><div><div></div><div>Note</div></div><div><p>上一段中的”工作区”是 Git working tree，不是 IDE 把多个文件夹组织起来时所说的 workspace。两者可能指向同一目录，也可能完全不是一层概念。</p></div></div></section><section><h3>暂存区不是待上传清单<a href="#暂存区不是待上传清单"><span>#</span></a></h3><p><strong>Index（索引，也叫 staging area 或暂存区）</strong> 保存下一次 commit 准备采用的路径、文件模式和 blob ID。它不是一个只有文件名的勾选列表，也不是尚未 push 的 commit 队列，而是一份接近完整目录快照的二进制数据结构。</p><p>这解释了一个初学时很反直觉的现象：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>printf</span><span> </span><span>'version 1\n'</span><span> &gt; </span><span>app.txt</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>app.txt</span></div></div><div><div><div>3</div></div><div><span>printf</span><span> </span><span>'version 2\n'</span><span> &gt; </span><span>app.txt</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>commit</span><span> </span><span>-m</span><span> </span><span>'record app'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个 commit 收进去的是 <code>version 1</code>，因为 <code>git add</code> 当时把那份内容写入对象数据库并让 index 指向它。之后写入工作区的 <code>version 2</code> 仍未暂存。<code>git add</code> 更准确的理解不是”开始跟踪这个文件”，而是”把这些路径此刻的内容放入下一张快照”。</p></section><section><h3><code>HEAD</code>、branch 与 commit<a href="#headbranch-与-commit"><span>#</span></a></h3><p><strong>Commit</strong> 是不可变对象，记录根 tree、parent、身份、时间和消息。<strong>Branch</strong> 是 <code>refs/heads/...</code> 下会移动的引用。<strong><code>HEAD</code></strong> 表示当前检出位置，通常间接指向某个本地分支。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>HEAD -&gt; refs/heads/main -&gt; commit 0063127 -&gt; parent cd5e714</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>执行 <code>git commit</code> 时，Git 创建新 commit，再让当前分支指向它；旧 commit 没有被修改。执行 <code>git switch topic</code> 时，Git 改变 <code>HEAD</code> 所指分支，并让 index 与工作区尽量匹配目标快照。分支是名字，commit 才是历史节点。</p></section><section><h3>tracked、untracked、modified 与 staged<a href="#trackeduntrackedmodified-与-staged"><span>#</span></a></h3><p>这些词描述的是路径与 3 份状态之间的关系：</p>

<table><thead><tr><th>状态</th><th>含义</th><th>常见下一步</th></tr></thead><tbody><tr><td>untracked</td><td>工作区有这个路径，但 index 与 <code>HEAD</code> 都没有记录它</td><td><code>git add &lt;path&gt;</code> 或写入 <code>.gitignore</code></td></tr><tr><td>unmodified</td><td>工作区、index 与 <code>HEAD</code> 对该路径的内容一致</td><td>不需要动作</td></tr><tr><td>modified</td><td>工作区内容不同于 index</td><td>查看 <code>git diff</code>，再暂存或恢复</td></tr><tr><td>staged</td><td>index 内容不同于 <code>HEAD</code></td><td>查看 <code>git diff --staged</code>，再 commit 或取消暂存</td></tr><tr><td>deleted</td><td>某次比较的一侧已有路径，另一侧没有</td><td>暂存删除、恢复文件或提交删除</td></tr><tr><td>conflicted</td><td>index 中存在尚未解决的高 stage 条目</td><td>解决内容，<code>git add</code> 后继续操作</td></tr></tbody></table><p>一个文件可以同时 modified 与 staged。前面的 <code>MM src/app.txt</code> 就是：index 已准备一个版本，工作区又在它上面继续修改。<code>git status</code> 给状态摘要，<code>git diff</code> 展开具体内容；前者像仪表盘，后者才是示波器。</p></section><section><h3>本地分支、远端跟踪分支与 upstream<a href="#本地分支远端跟踪分支与-upstream"><span>#</span></a></h3><p>这 3 个名字也经常被混成”远端分支”：</p><ul>
<li><code>main</code> 是本地分支，我可以离线提交并移动它；</li>
<li><code>origin/main</code> 是本地保存的 remote-tracking ref，表示上次 fetch 后我看到的远端 <code>main</code>；</li>
<li>服务器上的 <code>refs/heads/main</code> 才是远端此刻真正的分支。</li>
</ul><p><strong>Upstream</strong> 是给本地分支配置的默认比较、pull 和 push 对象。例如 <code>git push -u origin topic</code> 会在发布分支的同时设置 upstream。upstream 不是父 commit，也不意味着本地分支会自动与服务器同步。</p><p>最后把最常见的状态流压成 4 步：</p><div><div><div>远端仓库本地仓库Index工作区远端仓库本地仓库Index工作区我编辑文件git add：写入 blob 并更新 indexgit commit：创建快照并移动本地 branchgit push：发送对象并请求移动远端 ref我
</div><div>远端仓库本地仓库Index工作区远端仓库本地仓库Index工作区我编辑文件git add：写入 blob 并更新 indexgit commit：创建快照并移动本地 branchgit push：发送对象并请求移动远端 ref我
</div></div></div><p>这 4 步可以分开检查，也应该分开理解。</p></section></section>
<section><h2>先说结论：Git 保存的是快照，不是补丁<a href="#先说结论git-保存的是快照不是补丁"><span>#</span></a></h2><p>很多介绍会把 Git 描述成一个逐行保存差异的工具。这对阅读 <code>git diff</code> 很直观，但不适合作为底层模型。</p><p>每次 commit 指向一棵完整目录树。没有变化的文件会继续引用原来的 blob，所以 Git 不需要重复创建内容相同的对象；发生变化的文件得到新的 blob。Git 在对象语义上保存快照，在 packfile 的物理存储里才会为了压缩寻找 delta。换句话说：<strong>快照是数据模型，差异是比较结果和存储优化。</strong></p><p>我把 Git 想成一座仓库：对象是贴着内容指纹的密封箱，commit 是一张指向整批箱子的装箱单，branch 只是贴在某张装箱单上的可移动书签。这个比喻到这里就够了，接下来直接看字节。</p></section>
<section><h2>从零建一个可以解剖的仓库<a href="#从零建一个可以解剖的仓库"><span>#</span></a></h2><p>我不会拿正在工作的项目做实验。先创建临时目录，并显式指定初始分支名：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>repo</span><span><span>=</span><span>$(</span></span><span>mktemp</span><span> </span><span>-d</span><span>)</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>"</span><span>$repo</span><span>"</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>init</span><span> </span><span>--initial-branch=main</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>user.name</span><span> </span><span>Ada</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>user.email</span><span> </span><span>ada@example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的实验版本是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>git version 2.54.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>git init</code> 没有创建任何 commit。它只创建 <code>.git</code> 管理目录。下面是采用默认 <code>files</code> 引用后端的仓库在后续使用中会出现的关键路径；<code>index</code>、<code>refs/remotes</code> 和 <code>logs</code> 并不一定在刚初始化时就存在：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.git/</span></div></div><div><div><div>2</div></div><div><span>├── HEAD</span></div></div><div><div><div>3</div></div><div><span>├── config</span></div></div><div><div><div>4</div></div><div><span>├── index              # 第一次 add 后出现</span></div></div><div><div><div>5</div></div><div><span>├── objects/           # 对象数据库</span></div></div><div><div><div>6</div></div><div><span>├── refs/heads/        # 本地分支引用</span></div></div><div><div><div>7</div></div><div><span>├── refs/remotes/      # fetch 后的远端跟踪引用</span></div></div><div><div><div>8</div></div><div><span>└── logs/              # 启用 reflog 的引用发生更新后出现</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现代脚本应使用 <code>git init --initial-branch=main</code>，而不是依赖某个 Git 版本或用户配置碰巧采用什么默认分支名。长期偏好则可以配置：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>init.defaultBranch</span><span> </span><span>main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Note</div></div><div><p><code>--global</code> 修改当前用户的 Git 配置，不应该不加判断地写进项目安装脚本。项目级策略尽量放在仓库配置、服务端规则或文档中，个人偏好留给个人。</p></div></div></section>
<section><h2>第一个 blob：内容如何变成 ID<a href="#第一个-blob内容如何变成-id"><span>#</span></a></h2><p>写一个 13 字节文件（结尾换行也算 1 字节）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>printf</span><span> </span><span>'# pocket-git\n'</span><span> &gt; </span><span>README.md</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>hash-object</span><span> </span><span>README.md</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>5d87241c4953134b72c3ef069243777aa25121d9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个 40 位 object ID 不是只对文件内容计算 SHA-1。Git 实际哈希的是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>blob 13\0# pocket-git\n</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也就是对象类型、一个空格、十进制长度、NUL 字节，再接原始内容：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> hashlib</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span><span>content </span><span>=</span><span> </span></span><span>b</span><span>"# pocket-git</span><span>\n</span><span>"</span></div></div><div><div><div>4</div></div><div><span><span>raw </span><span>=</span><span> </span></span><span>f</span><span>"blob </span><span>{</span><span>len</span><span>(content)</span><span>}</span><span>\0</span><span>"</span><span><span>.</span><span>encode</span><span>() </span><span>+</span><span> content</span></span></div></div><div><div><div>5</div></div><div><span>print</span><span><span>(hashlib.</span><span>sha1</span><span>(raw).</span><span>hexdigest</span><span>())</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出仍然是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>5d87241c4953134b72c3ef069243777aa25121d9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>git hash-object README.md</code> 默认只计算，不写入数据库；加 <code>-w</code> 才会写。平时不需要直接执行它，因为 <code>git add</code> 会完成这一步。</p><p>Git 2.54.0 新仓库仍默认使用 SHA-1，也支持 <code>git init --object-format=sha256</code>。不过 SHA-1 与 SHA-256 仓库目前不能直接互操作，所以不要为了看起来更现代就擅自切换团队仓库的对象格式。这里的 object ID 首先承担内容寻址和完整性校验；它不是文件加密，也不等于 commit 签名。</p></section>
<section><h2><code>git add</code> 不是”添加文件”，而是更新下一张快照<a href="#git-add-不是添加文件而是更新下一张快照"><span>#</span></a></h2><p>再写一个文件，然后暂存两者：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir</span><span> </span><span>src</span></div></div><div><div><div>2</div></div><div><span>printf</span><span> </span><span>'version 1\n'</span><span> &gt; </span><span>src/app.txt</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>README.md</span><span> </span><span>src/app.txt</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>ls-files</span><span> </span><span>--stage</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>100644 5d87241c4953134b72c3ef069243777aa25121d9 0 README.md</span></div></div><div><div><div>2</div></div><div><span>100644 83baae61804e65cc73a7201a7252750c76066a30 0 src/app.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这就是 index 的可读投影。每行记录文件模式、blob ID、stage 编号和路径。<code>100644</code> 是普通非可执行文件；最后的 <code>0</code> 表示正常条目。普通内容冲突中，同一路径最多还可能出现 stage 1、2、3，分别保存 merge base、ours 和 theirs；删除等冲突类型可能缺少其中某项，而且 rebase 里的 ours 与 theirs 要从重放操作而非原分支名理解。</p><p>所以 <code>git add</code> 这个名字稍微有点误导。它不仅”添加新文件”，还会把已修改文件的<strong>当前内容</strong>写成 blob，并更新 index 中对应路径。之后继续编辑同一个文件，不会自动改变已经暂存的内容。</p></section>
<section><h2>从 index 写出 tree，再创建 commit<a href="#从-index-写出-tree再创建-commit"><span>#</span></a></h2><p>index 还不是 tree 对象，但它是 tree 的施工图。执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>write-tree</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的根 tree ID 是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>208dc736f90acf95e1f43a0a0ae122b906953eef</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>查看它：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>cat-file</span><span> </span><span>-p</span><span> </span><span>208dc736f90acf95e1f43a0a0ae122b906953eef</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>会看到根目录中的 blob 和 <code>src</code> 子 tree。tree 保存名字、模式和下一级对象 ID；blob 只保存内容，不知道自己叫 <code>README.md</code> 还是 <code>copy.md</code>。两个路径内容完全相同，可以指向同一个 blob。</p><p>现在提交。为了让下面的 commit ID 可以复算，我除了使用前面配置的身份，还临时固定 author date 和 committer date：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>GIT_AUTHOR_DATE</span><span>=</span><span>'2026-09-06T12:00:00+08:00'</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>GIT_COMMITTER_DATE</span><span>=</span><span>"</span><span>$GIT_AUTHOR_DATE</span><span>"</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>commit</span><span> </span><span>-m</span><span> </span><span>'feat: build the first snapshot'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我得到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cd5e7140591d1099e8a990fe16f839a4b91be271</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>直接查看对象：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>cat-file</span><span> </span><span>-p</span><span> </span><span>HEAD</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tree 208dc736f90acf95e1f43a0a0ae122b906953eef</span></div></div><div><div><div>2</div></div><div><span>author Ada &lt;ada@example.com&gt; 1788667200 +0800</span></div></div><div><div><div>3</div></div><div><span>committer Ada &lt;ada@example.com&gt; 1788667200 +0800</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>feat: build the first snapshot</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一个 commit 没有 <code>parent</code>。后续普通 commit 有 1 个 parent，merge commit 通常有 2 个或更多 parent。commit 本身不装文件内容，只记录：</p><ul>
<li>根 tree；</li>
<li>parent commit；</li>
<li>author 与 author date；</li>
<li>committer 与 committer date；</li>
<li>commit message；</li>
<li>可选签名等 header。</li>
</ul><p>因此修改 commit message、作者、时间、parent 或 tree 中任意一个字节，都会得到新的 commit ID；它之后的所有子 commit 也必须改写，因为每个子 commit 都记录 parent ID。这就是 <code>rebase</code>、<code>commit --amend</code> 和历史过滤为什么会成串地产生新 ID。</p><p>把这些对象连起来以后，第一张快照实际是一张由 ID 串起的 DAG：</p><div><div><div><div><span></span></div><div><span><p>README.md / 100644</p></span></div><div><span><p>src / 040000</p></span></div><div><span><p>app.txt / 100644</p></span></div><div><span><p>commit cd5e714<br /><br />作者、时间、消息</p></span></div><div><span><p>root tree 208dc73</p></span></div><div><span><p>blob 5d87241<br /><br /># pocket-git</p></span></div><div><span><p>tree src</p></span></div><div><span><p>blob 83baae6<br /><br />version 1</p></span></div>
</div><div><div><span></span></div><div><span><p>README.md / 100644</p></span></div><div><span><p>src / 040000</p></span></div><div><span><p>app.txt / 100644</p></span></div><div><span><p>commit cd5e714<br /><br />作者、时间、消息</p></span></div><div><span><p>root tree 208dc73</p></span></div><div><span><p>blob 5d87241<br /><br /># pocket-git</p></span></div><div><span><p>tree src</p></span></div><div><span><p>blob 83baae6<br /><br />version 1</p></span></div>
</div></div></div><p>路径名活在 tree 的边上，文件内容活在 blob 中，commit 只从根 tree 进入整张图。这也是相同内容能够复用同一个 blob 的原因。</p><p>一次普通 <code>git commit</code> 在概念上做了 4 件事：</p><ol>
<li>从 index 写出 tree 对象；</li>
<li>创建指向该 tree 和当前 parent 的 commit 对象；</li>
<li>把当前 branch ref 移到新 commit；</li>
<li>在已启用的 <code>HEAD</code> 和当前本地分支 reflog 中记录这次移动。</li>
</ol><p>它<strong>不会</strong>自动提交工作区里尚未暂存的修改。<code>git commit -a</code> 只会自动暂存已跟踪文件的修改和删除，仍不会包含未跟踪文件。我更偏好显式 <code>git add -p</code>，因为省掉一次检查通常也顺便省掉了思考（这个优化不太划算）。</p><p>我实际做了一次：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>printf</span><span> </span><span>'version 2\n'</span><span> &gt; </span><span>src/app.txt</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>src/app.txt</span></div></div><div><div><div>3</div></div><div><span>printf</span><span> </span><span>'version 3\n'</span><span> &gt; </span><span>src/app.txt</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>status</span><span> </span><span>--short</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>MM src/app.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>左边的 <code>M</code> 表示 index 相对 <code>HEAD</code> 有修改，右边的 <code>M</code> 表示工作区相对 index 又有修改。同一个路径此时同时存在 3 个版本：</p>

<table><thead><tr><th>位置</th><th>内容</th><th>查看方式</th></tr></thead><tbody><tr><td><code>HEAD</code></td><td><code>version 1</code></td><td><code>git show HEAD:src/app.txt</code></td></tr><tr><td>index</td><td><code>version 2</code></td><td><code>git show :src/app.txt</code></td></tr><tr><td>工作区</td><td><code>version 3</code></td><td><code>cat src/app.txt</code></td></tr></tbody></table><div><div><div><div><span><p>git diff --staged</p></span></div><div><span><p>git diff</p></span></div><div><span><p>HEAD<br /><br />version 1</p></span></div><div><span><p>index<br /><br />version 2</p></span></div><div><span><p>工作区<br /><br />version 3</p></span></div>
</div><div><div><span><p>git diff --staged</p></span></div><div><span><p>git diff</p></span></div><div><span><p>HEAD<br /><br />version 1</p></span></div><div><span><p>index<br /><br />version 2</p></span></div><div><span><p>工作区<br /><br />version 3</p></span></div>
</div></div></div><p>这里的箭头表示比较方向，不表示内容自动流动。它把 <code>MM</code> 的两个 <code>M</code> 直接摊开了：左列比较 <code>HEAD</code> 与 index，右列比较 index 与工作区。</p><p>对应的两次比较也完全不同：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span>                  </span><span># 工作区 vs index</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--staged</span><span>         </span><span># index vs HEAD</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这是我认为最值得形成肌肉记忆的提交循环：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>status</span><span> </span><span>--short</span><span> </span><span>--branch</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>diff</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>--patch</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--staged</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>commit</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>git add --patch</code>（缩写 <code>git add -p</code>）按 hunk 选择内容，比不看差异就 <code>git add .</code> 更接近”设计一个 commit”。如果同一段修改同时修 bug、改命名和格式化，我会先把它们拆成能独立解释的快照。commit 的边界是给未来的我、同事、<code>git bisect</code> 和 AI 阅读的 API。</p><div><div><div></div><div>Warning</div></div><div><p><code>git add .</code> 本身没有错，但它表达的是”暂存当前目录以下所有变化”。在运行前后都看 <code>git status</code> 和 <code>git diff --staged</code>，不要把它当作关闭红点的按钮。</p></div></div></section>
<section><h2><code>git diff</code> 到底在比较什么<a href="#git-diff-到底在比较什么"><span>#</span></a></h2><p><code>diff</code> 是我每天最应该多敲几次的 Git 命令。它不修改状态，只把两个端点之间的变化编码成 patch。真正的第一个问题不是”改了什么”，而是”拿哪两份状态比较”。</p><section><h3>先选对两端<a href="#先选对两端"><span>#</span></a></h3>

<table><thead><tr><th>命令</th><th>旧端</th><th>新端</th><th>我通常用它回答</th></tr></thead><tbody><tr><td><code>git diff</code></td><td>index</td><td>工作区</td><td>我改了但还没暂存什么？</td></tr><tr><td><code>git diff --staged</code></td><td><code>HEAD</code></td><td>index</td><td>下一次 commit 会包含什么？</td></tr><tr><td><code>git diff HEAD</code></td><td><code>HEAD</code></td><td>工作区</td><td>当前全部已跟踪修改是什么？</td></tr><tr><td><code>git diff &lt;A&gt; &lt;B&gt;</code></td><td>commit A</td><td>commit B</td><td>从 A 变成 B 要应用什么 patch？</td></tr><tr><td><code>git diff &lt;A&gt;...&lt;B&gt;</code></td><td>A 与 B 的 merge base</td><td>commit B</td><td>B 分支从分叉点引入了什么？</td></tr><tr><td><code>git show &lt;commit&gt;</code></td><td>普通 commit 的 parent</td><td>该 commit</td><td>这个普通 commit 引入了什么？</td></tr></tbody></table><p>方向很重要。<code>git diff A B</code> 描述的是把 A 变成 B：<code>-</code> 行来自 A，<code>+</code> 行来自 B。交换参数，patch 的正负也会反过来。</p><p><code>git diff A..B</code> 对 <code>diff</code> 来说与 <code>git diff A B</code> 基本等价，只比较两个端点；这里的 <code>..</code> 不具有 <code>git log A..B</code> 那种”可达 commit 集合之差”语义。三点 <code>A...B</code> 才会先求 merge base，再把 merge base 与 B 比较。两个点和三个点长得只差一个点，含义差了一棵分叉历史（非常 Git）。</p><p>只看某些路径时，我会用 <code>--</code> 明确结束 revision 参数：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>HEAD</span><span> </span><span>--</span><span> </span><span>src/app.txt</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>main...topic</span><span> </span><span>--</span><span> </span><span>src/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这也能避免一个路径名恰好像 branch 名时产生歧义。</p></section><section><h3>逐行拆一份 unified diff<a href="#逐行拆一份-unified-diff"><span>#</span></a></h3><p>在前面的实验里，index 保存 <code>version 2</code>，工作区保存 <code>version 3</code>。运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--</span><span> </span><span>src/app.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实际输出是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>diff --git a/src/app.txt b/src/app.txt</span></div></div><div><div><div>2</div></div><div><span>index 1f7a7a4..7170a52 100644</span></div></div><div><div><div>3</div></div><div><span>--- a/src/app.txt</span></div></div><div><div><div>4</div></div><div><span>+++ b/src/app.txt</span></div></div><div><div><div>5</div></div><div><span>@@ -1 +1 @@</span></div></div><div><div><div>6</div></div><div><span>-version 2</span></div></div><div><div><div>7</div></div><div><span>+version 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>从上往下读。</p><section><h4><code>diff --git</code>：一个文件级变化开始了<a href="#diff---git一个文件级变化开始了"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>diff --git a/src/app.txt b/src/app.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一行标记新的 file patch。<code>a/</code> 表示变化前一侧，<code>b/</code> 表示变化后一侧；它们通常不是仓库里真实存在的两个目录。重命名或复制时，两侧路径会不同。</p></section><section><h4><code>index</code>：前后 blob 与文件模式<a href="#index前后-blob-与文件模式"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>index 1f7a7a4..7170a52 100644</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>1f7a7a4</code> 是旧端内容的 blob ID 缩写，对应 <code>version 2</code>；</li>
<li><code>7170a52</code> 是新端内容计算出的 blob ID 缩写，对应 <code>version 3</code>；</li>
<li><code>100644</code> 表示普通非可执行文件。</li>
</ul><p>工作区版本即使还没被 <code>git add</code> 写成 loose object，Git 仍可以根据内容算出它应有的 object ID。文件模式发生变化时，输出通常改为独立的 <code>old mode</code> 和 <code>new mode</code> 行。</p></section><section><h4><code>---</code> 与 <code>+++</code>：旧文件和新文件标签<a href="#----与-旧文件和新文件标签"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>--- a/src/app.txt</span></div></div><div><div><div>2</div></div><div><span>+++ b/src/app.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>---</code> 永远标旧端，<code>+++</code> 永远标新端。这里的 3 个减号和 3 个加号是 header 语法，不表示删除了 3 行或增加了 3 行。新建文件的旧端、删除文件的新端会在这里显示 <code>/dev/null</code>。</p></section><section><h4><code>@@</code>：hunk 在两边的坐标<a href="#hunk-在两边的坐标"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -1 +1 @@</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个 patch 可以包含多个相隔较远的修改块，每一块叫 hunk。一般格式是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -&lt;旧起始行&gt;,&lt;旧行数&gt; +&lt;新起始行&gt;,&lt;新行数&gt; @@ &lt;可选上下文&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-1</code> 表示旧文件从第 1 行开始、共 1 行；<code>+1</code> 对新文件含义相同。行数为 1 时逗号和行数会省略，所以这里不是”缺了一个数字”。例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@@ -10,4 +10,6 @@ function parseConfig()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>表示旧端从第 10 行取 4 行，新端从第 10 行取 6 行。末尾的函数名或章节名只是帮助定位的 hunk context，不参与 patch 内容。</p></section><section><h4>hunk 正文：看每行第一个字符<a href="#hunk-正文看每行第一个字符"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>version 2</span></div></div><div><div><div>2</div></div><div><span>version 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>空格开头：context line，两边都存在，内容没变；</li>
<li><code>-</code> 开头：只在旧端存在，要删除；</li>
<li><code>+</code> 开头：只在新端存在，要加入。</li>
</ul><p>这几个前缀是 patch 数据，不是装饰颜色。终端常把删除染红、增加染绿，但把颜色关掉或把 patch 发给程序后，真正可靠的仍是行首字符。</p><p>若文件最后一行缺少换行，Git 还会输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\ No newline at end of file</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是新增的文件内容，而是说明紧邻的那一侧末行没有 line feed。文本文件看起来完全一样，末尾字节却不同，Git 当然会认真记账（它对字节的原则性比我强得多）。</p></section></section><section><h3>新建、删除、改权限和重命名怎么看<a href="#新建删除改权限和重命名怎么看"><span>#</span></a></h3><p>普通内容修改之外，extended headers 会描述文件级元数据：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>new file mode 100644</span></div></div><div><div><div>2</div></div><div><span>deleted file mode 100644</span></div></div><div><div><div>3</div></div><div><span>old mode 100644</span></div></div><div><div><div>4</div></div><div><span>new mode 100755</span></div></div><div><div><div>5</div></div><div><span>rename from old-name.txt</span></div></div><div><div><div>6</div></div><div><span>rename to new-name.txt</span></div></div><div><div><div>7</div></div><div><span>similarity index 98%</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>100644 -&gt; 100755</code> 通常表示 executable bit 发生变化，不代表文件正文改变。重命名则需要一个重要修正：Git commit 并不存储”rename old to new”操作。Git 保存前后 tree，diff 再把一个删除和一个新增按内容相似度推断为 rename。<code>similarity index 98%</code> 表示 Git 认为 98% 的行没有变化；调整 rename detection 阈值可能得到不同展示，但 commit 对象没有改变。</p><p>想明确启用并设置阈值，可以：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--find-renames=50%</span><span> </span><span>&lt;A&gt;</span><span> </span><span>&lt;B&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>二进制文件默认通常只显示 <code>Binary files ... differ</code>，不会假装把任意字节流解释成文本行。项目可以通过 <code>.gitattributes</code> 配置 textconv 或专用 diff driver，但那改变的是展示，不是对象内容。</p></section><section><h3>适合不同阅读阶段的 diff 视图<a href="#适合不同阅读阶段的-diff-视图"><span>#</span></a></h3><p>完整 patch 不是每次都最合适。我通常从概览逐层放大：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--stat</span><span>                 </span><span># 每个文件的增删规模</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--name-status</span><span>          </span><span># 路径及 M/A/D/R 等状态</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>diff</span><span>                        </span><span># 完整未暂存 patch</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--staged</span><span>               </span><span># 完整待提交 patch</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--word-diff</span><span>            </span><span># 观察行内词级变化</span></div></div><div><div><div>6</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--check</span><span>                </span><span># 检查空白错误与冲突标记</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>--stat</code> 适合先判断变化范围，不能代替阅读 patch；<code>--word-diff</code> 对长段文字和一行内改多个 token 很有用，但它不是常规 unified patch；<code>--check</code> 适合提交前发现 trailing whitespace 等问题。</p><p>只想让脚本判断有无差异时：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--quiet</span><span> </span><span>--exit-code</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>退出码 <code>0</code> 表示没有差异，<code>1</code> 表示存在差异，其他非零值通常表示真正的错误。这里不能用”命令没有打印东西，所以相同”来判断，因为 <code>--quiet</code> 本来就不打印。</p><p>Git 也可以离开仓库比较两个普通路径：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--no-index</span><span> </span><span>old.txt</span><span> </span><span>new.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这时它很像系统 <code>diff -u</code>，仍使用 Git 熟悉的 patch 展示和退出码。至于 <code>--patience</code>、<code>--histogram</code> 等 diff algorithm，它们会改变 hunk 如何对齐，适合默认 Myers 算法给出难读结果时尝试；它们不会改变两端真实内容。</p><div><div><div></div><div>Important</div></div><div><p>审查 commit 前看 <code>git diff --staged</code>，push 前看 branch 间的 diff。<code>git status</code> 只能告诉我”哪个文件处于什么状态”，不能告诉我删掉的条件判断是不是刚好把权限检查也带走了。</p></div></div></section></section>
<section><h2>branch 和 HEAD 原来如此轻<a href="#branch-和-head-原来如此轻"><span>#</span></a></h2><p>第一次提交后，我直接看两个文件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cat</span><span> </span><span>.git/HEAD</span></div></div><div><div><div>2</div></div><div><span>cat</span><span> </span><span>.git/refs/heads/main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出分别是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ref: refs/heads/main</span></div></div><div><div><div>2</div></div><div><span>cd5e7140591d1099e8a990fe16f839a4b91be271</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>main</code> 只是一个指向 commit 的 ref，<code>HEAD</code> 通常又是一个指向当前 branch ref 的符号引用。创建分支几乎不复制数据：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>branch</span><span> </span><span>experiment</span><span> </span><span>cd5e714</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这只是新建 <code>refs/heads/experiment</code>，写入同一个 commit ID。无论仓库有 10 KB 还是 100 GB，创建普通分支都不是复制完整项目。</p><p>第二次提交后，我得到：</p><div><div><div>mainexperimentcd5e7140063127HEAD
</div><div>mainexperimentcd5e7140063127HEAD
</div></div></div><p>此时 <code>main</code> 指向 <code>0063127</code>，<code>experiment</code> 仍指向 <code>cd5e714</code>。所谓”在某个分支上提交”，就是 Git 创建新 commit 后移动 <code>HEAD</code> 间接指向的那个分支。</p><section><h3>detached HEAD 是什么<a href="#detached-head-是什么"><span>#</span></a></h3><p>如果执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>switch</span><span> </span><span>--detach</span><span> </span><span>cd5e714</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>HEAD</code> 会直接保存 commit ID，而不再指向 <code>refs/heads/...</code>。这适合检查历史或做可丢弃实验。此时提交仍然会创建合法 commit，只是没有 branch 名自动跟着它移动。切走后，那些 commit 可能变得不可达，但通常还能暂时从 reflog 找回。</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>refs/heads/main</p></span></div><div><span><p>0063127</p></span></div><div><span><p>cd5e714</p></span></div><div><span><p>普通 HEAD</p></span></div><div><span><p>detached HEAD</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>refs/heads/main</p></span></div><div><span><p>0063127</p></span></div><div><span><p>cd5e714</p></span></div><div><span><p>普通 HEAD</p></span></div><div><span><p>detached HEAD</p></span></div>
</div></div></div><p>普通状态下，<code>HEAD -&gt; branch -&gt; commit</code>；detached 状态则跳过 branch，变成 <code>HEAD -&gt; commit</code>。少掉的正是那个会自动移动、也方便以后寻找的名字。</p><p>如果实验值得保留，在切走前给它一个名字：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>switch</span><span> </span><span>-c</span><span> </span><span>rescued-experiment</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>为什么现在更推荐 <code>switch</code> 和 <code>restore</code><a href="#为什么现在更推荐-switch-和-restore"><span>#</span></a></h2><p>传统 <code>git checkout</code> 同时承担两类工作：</p><ul>
<li>切换分支或进入 detached HEAD；</li>
<li>用 index 或某个 commit 覆盖路径。</li>
</ul><p>同一个命令既移动 <code>HEAD</code>，又可能覆盖文件，意图不够显眼。Git 2.23 在 2019 年加入 <code>git switch</code> 和 <code>git restore</code>，把两组职责拆开。<strong><code>checkout</code> 没有被废弃，也不是错误命令</strong>；现代交互式工作流更推荐语义专一的命令，因为代码审查和终端历史更容易读懂。</p><p>常见映射如下：</p>

<table><thead><tr><th>意图</th><th>现代命令</th><th>传统写法</th></tr></thead><tbody><tr><td>切换已有分支</td><td><code>git switch topic</code></td><td><code>git checkout topic</code></td></tr><tr><td>创建并切换分支</td><td><code>git switch -c topic</code></td><td><code>git checkout -b topic</code></td></tr><tr><td>切回上一个分支</td><td><code>git switch -</code></td><td><code>git checkout -</code></td></tr><tr><td>临时查看 commit</td><td><code>git switch --detach &lt;commit&gt;</code></td><td><code>git checkout &lt;commit&gt;</code></td></tr><tr><td>丢弃工作区路径修改</td><td><code>git restore &lt;path&gt;</code></td><td><code>git checkout -- &lt;path&gt;</code></td></tr><tr><td>取消暂存</td><td><code>git restore --staged &lt;path&gt;</code></td><td><code>git reset HEAD &lt;path&gt;</code></td></tr><tr><td>从某 commit 取回路径</td><td><code>git restore --source=&lt;commit&gt; &lt;path&gt;</code></td><td><code>git checkout &lt;commit&gt; -- &lt;path&gt;</code></td></tr></tbody></table><p>我在前面的 3 版本实验里运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>restore</span><span> </span><span>src/app.txt</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>restore</span><span> </span><span>--staged</span><span> </span><span>src/app.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一条把工作区从 <code>version 3</code> 恢复到 index 的 <code>version 2</code>；第二条把 index 从 <code>version 2</code> 恢复到 <code>HEAD</code> 的 <code>version 1</code>，但工作区仍保留 <code>version 2</code>。这里没有魔法，只是明确指定复制的来源和目的地。</p><div><div><div><div><span><p>git restore --staged</p></span></div><div><span><p>git restore</p></span></div><div><span><p>HEAD<br /><br />version 1</p></span></div><div><span><p>index<br /><br />version 2</p></span></div><div><span><p>工作区<br /><br />version 3</p></span></div>
</div><div><div><span><p>git restore --staged</p></span></div><div><span><p>git restore</p></span></div><div><span><p>HEAD<br /><br />version 1</p></span></div><div><span><p>index<br /><br />version 2</p></span></div><div><span><p>工作区<br /><br />version 3</p></span></div>
</div></div></div><p><code>restore</code> 的危险程度取决于箭头落在哪里：落到工作区就可能覆盖未保存内容，落到 index 则只是重新安排下一次快照。</p><div><div><div></div><div>Caution</div></div><div><p><code>git restore &lt;path&gt;</code> 会覆盖尚未暂存的工作区修改。先运行 <code>git diff -- &lt;path&gt;</code>。如果内容重要但还不适合 commit，可以先建临时分支提交，通常比把唯一副本塞进 stash 更容易找回。</p></div></div></section>
<section><h2>merge、rebase 和 cherry-pick 都在改什么<a href="#mergerebase-和-cherry-pick-都在改什么"><span>#</span></a></h2><p>假设历史分叉成：</p><div><div><div>maintopicOABCD
</div><div>maintopicOABCD
</div></div></div><section><h3>Fast-forward：历史没分叉，只移动指针<a href="#fast-forward历史没分叉只移动指针"><span>#</span></a></h3><p>先看最简单的情况。<code>main</code> 停在 <code>O</code>，<code>topic</code> 从 <code>O</code> 继续提交了 <code>C</code>、<code>D</code>，而 <code>main</code> 自己没有新 commit：</p><p>合并前：</p><div><div><div>maintopicOCD
</div><div>maintopicOCD
</div></div></div><p>合并后：</p><div><div><div>mainOCD
</div><div>mainOCD
</div></div></div><p>两张图中的 <code>O</code>、<code>C</code>、<code>D</code> 是同一批 commit。前一张图里 <code>main</code> 留在 <code>O</code>；后一张只画发生变化的 <code>main</code>，它已经抵达 <code>D</code>。<code>topic</code> 没有移动，仍然指向 <code>D</code>。</p><p><code>O</code> 是 <code>D</code> 的祖先，所以 <code>git merge topic</code> 不必创建 merge commit。Git 只把 <code>main</code> 从 <code>O</code> 移到 <code>D</code>，commit 图一个节点都没增加；这就是 fast-forward，缩写为 ff。之后看到的 <code>pull --ff-only</code> 和 push 检查都在问同一个祖先关系。</p></section><section><h3><code>merge</code>：保留分叉，增加汇合点<a href="#merge保留分叉增加汇合点"><span>#</span></a></h3><p>在 <code>main</code> 上执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>switch</span><span> </span><span>main</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>merge</span><span> </span><span>topic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>若不能 fast-forward，Git 创建一个 merge commit <code>M</code>：</p><div><div><div>maintopicOABCDM
</div><div>maintopicOABCDM
</div></div></div><p><code>M</code> 有两个 parent。历史明确记录两条工作线曾经并行存在。</p><p>我会在需要保留集成边界时使用 merge，例如合入一个完整功能分支。想明确保留这个边界，可以：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>merge</span><span> </span><span>--no-ff</span><span> </span><span>topic</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3><code>rebase</code>：复制 commit，换一组 parent<a href="#rebase复制-commit换一组-parent"><span>#</span></a></h3><p>在 <code>topic</code> 上执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>switch</span><span> </span><span>topic</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>rebase</span><span> </span><span>main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Git 找到共同祖先 <code>O</code>，取出 <code>C</code>、<code>D</code> 引入的补丁，再以 <code>B</code> 为新基底重放，得到新 commit <code>C'</code>、<code>D'</code>：</p><div><div><div>maintopicOABC'D'
</div><div>maintopicOABC'D'
</div></div></div><p><code>C'</code> 与 <code>C</code> 的 parent 不同，所以 ID 必然不同；<code>D'</code> 的 parent 又变了，因此也有新 ID。rebase 不是”移动原 commit”，而是复制出一条语义相似的新历史，再移动分支引用。</p><p>我的规则很简单：</p><ul>
<li>尚未共享的个人分支，可以 rebase、<code>commit --amend</code>、<code>rebase --interactive</code>；</li>
<li>已被其他人基于其开发的公开历史，不随意 rebase；</li>
<li>集成策略服从项目，不把”线性历史”当宗教。</li>
</ul><p>交互式整理常用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>rebase</span><span> </span><span>--interactive</span><span> </span><span>&lt;base&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果评审后只想把修补自动折叠进原 commit：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>commit</span><span> </span><span>--fixup=&lt;commit&gt;</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>rebase</span><span> </span><span>--interactive</span><span> </span><span>--autosquash</span><span> </span><span>&lt;base&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>rebase 前后想确认 patch series 是否意外变化：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>range-diff</span><span> </span><span>&lt;old-base&gt;..&lt;old-tip&gt;</span><span> </span><span>&lt;new-base&gt;..&lt;new-tip&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3><code>cherry-pick</code>：复制选定的 commit<a href="#cherry-pick复制选定的-commit"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>cherry-pick</span><span> </span><span>&lt;commit&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它把某个 commit 引入的变化应用到当前 <code>HEAD</code>，再创建一个新 commit。因为 parent 通常不同，新 ID 也不同。适合把一个独立修复移到维护分支，不适合长期用来手工同步两条本应合并的分支。</p><p>冲突不是文件坏了，而是 Git 无法自动决定 3 方合并结果。先看：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>status</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>diff</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>解决每个文件后暂存，再继续对应操作：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>&lt;resolved-path&gt;</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>merge</span><span> </span><span>--continue</span><span>       </span><span># merge 场景</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>rebase</span><span> </span><span>--continue</span><span>      </span><span># rebase 场景</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>cherry-pick</span><span> </span><span>--continue</span><span> </span><span># cherry-pick 场景</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不想继续时，使用相应的 <code>--abort</code>，不要边慌边 <code>reset --hard</code>。</p></section></section>
<section><h2>远端不是云端魔法，只是另一个仓库<a href="#远端不是云端魔法只是另一个仓库"><span>#</span></a></h2><p><code>origin</code> 只是远端的默认昵称，不是 Git 的保留关键字。查看配置：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>remote</span><span> </span><span>--verbose</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>remote</span><span> </span><span>get-url</span><span> </span><span>origin</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个本地仓库通常同时有：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>refs/heads/main                 # 我的本地分支</span></div></div><div><div><div>2</div></div><div><span>refs/remotes/origin/main        # 我上次 fetch 后看到的远端状态</span></div></div><div><div><div>3</div></div><div><span>远端仓库的 refs/heads/main       # 远端此刻真正的分支</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>后两者不是同一个东西。<code>origin/main</code> 是本地的远端跟踪引用，它不会凭空知道服务器刚刚发生了什么。</p><div><div><div><div><span><p>远端仓库 origin</p></span></div><div><span><p>本地仓库 .git</p></span></div><div><span><p>fetch 缺少的对象</p></span></div><div><span><p>fetch 更新观察值</p></span></div><div><span><p>merge 或 rebase</p></span></div><div><span><p>push 缺少的对象</p></span></div><div><span><p>push 请求更新 ref</p></span></div><div><span><p>objects</p></span></div><div><span><p>refs/heads/main<br /><br />本地 main</p></span></div><div><span><p>refs/remotes/origin/main<br /><br />上次 fetch 的观察值</p></span></div><div><span><p>objects</p></span></div><div><span><p>refs/heads/main<br /><br />远端此刻的 main</p></span></div>
</div><div><div><span><p>远端仓库 origin</p></span></div><div><span><p>本地仓库 .git</p></span></div><div><span><p>fetch 缺少的对象</p></span></div><div><span><p>fetch 更新观察值</p></span></div><div><span><p>merge 或 rebase</p></span></div><div><span><p>push 缺少的对象</p></span></div><div><span><p>push 请求更新 ref</p></span></div><div><span><p>objects</p></span></div><div><span><p>refs/heads/main<br /><br />本地 main</p></span></div><div><span><p>refs/remotes/origin/main<br /><br />上次 fetch 的观察值</p></span></div><div><span><p>objects</p></span></div><div><span><p>refs/heads/main<br /><br />远端此刻的 main</p></span></div>
</div></div></div><p>关键是不把 <code>origin/main</code> 画到服务器里。它住在我的 <code>.git</code> 中，是一次成功 fetch 留下的本地观察值；网络断开以后，它当然不会自己刷新。</p><section><h3><code>fetch</code>：下载对象，更新远端跟踪引用<a href="#fetch下载对象更新远端跟踪引用"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>fetch</span><span> </span><span>origin</span><span> </span><span>--prune</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它与远端协商本地缺少的对象，接收 packfile，再按 refspec 更新 <code>refs/remotes/origin/*</code>。<code>--prune</code> 删除远端已经不存在的远端跟踪引用，但不会删除我的本地分支。</p><p>fetch 通常不改当前工作区、index 或本地 <code>main</code>，所以它是理解远端变化的好分界点：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>log</span><span> </span><span>--oneline</span><span> </span><span>--graph</span><span> </span><span>--decorate</span><span> </span><span>--all</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>log</span><span> </span><span>--left-right</span><span> </span><span>--cherry-pick</span><span> </span><span>main...origin/main</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>main...origin/main</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3><code>pull</code>：先 fetch，再选择一种集成策略<a href="#pull先-fetch再选择一种集成策略"><span>#</span></a></h3><p><code>git pull</code> 不是与 fetch 并列的下载协议。它先 fetch，然后把上游分支集成进当前分支。真正需要决定的是第二步。</p><div><div><div><div><span><p>1. fetch</p></span></div><div><span></span></div><div><span><p>ff-only</p></span></div><div><span><p>rebase</p></span></div><div><span><p>no-rebase</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>远端 upstream</p></span></div><div><span><p>本地 origin/main</p></span></div><div><span>2. 如何集成？</span></div><div><span><p>只允许移动 main</p></span></div><div><span><p>复制本地 commit 到新基底</p></span></div><div><span><p>merge，必要时创建 M</p></span></div><div><span><p>本地 main</p></span></div>
</div><div><div><span><p>1. fetch</p></span></div><div><span></span></div><div><span><p>ff-only</p></span></div><div><span><p>rebase</p></span></div><div><span><p>no-rebase</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>远端 upstream</p></span></div><div><span><p>本地 origin/main</p></span></div><div><span>2. 如何集成？</span></div><div><span><p>只允许移动 main</p></span></div><div><span><p>复制本地 commit 到新基底</p></span></div><div><span><p>merge，必要时创建 M</p></span></div><div><span><p>本地 main</p></span></div>
</div></div></div><p>所以 pull 不是第四种历史操作。它只是把 fetch 与某一种已有的集成操作串起来。</p><p>我不喜欢让一个无参数 <code>git pull</code> 隐藏策略。现代、安全而可读的写法是明确选择：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>pull</span><span> </span><span>--ff-only</span><span>   </span><span># 只允许快进，分叉就停下</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>pull</span><span> </span><span>--rebase</span><span>    </span><span># fetch 后把本地 commit 重放到上游</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>pull</span><span> </span><span>--no-rebase</span><span> </span><span># fetch 后 merge</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对于还在建立心智模型的人，我更推荐拆成两步：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>fetch</span><span> </span><span>origin</span><span> </span><span>--prune</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>log</span><span> </span><span>--oneline</span><span> </span><span>--graph</span><span> </span><span>--decorate</span><span> </span><span>--all</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>rebase</span><span> </span><span>origin/main</span><span>  </span><span># 或 git merge origin/main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这样每个命令只做一类事，出错时也知道哪一层出了问题。Git 版本之间默认 pull 策略可能变化，项目策略也不同；显式参数比背默认值可靠。</p><p>长期配置应选自己和团队真正采用的策略，例如：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>pull.ff</span><span> </span><span>only</span></div></div><div><div><div>2</div></div><div><span># 或</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>pull.rebase</span><span> </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不要把两条都照抄。<code>ff-only</code> 在本地与上游分叉时停止；<code>rebase</code> 会重写本地尚未发布的 commit。它们表达不同政策。</p></section><section><h3><code>push</code>：上传缺少的对象，请远端移动 ref<a href="#push上传缺少的对象请远端移动-ref"><span>#</span></a></h3><p>第一次发布分支：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>push</span><span> </span><span>--set-upstream</span><span> </span><span>origin</span><span> </span><span>topic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>--set-upstream</code>（缩写 <code>-u</code>）建立本地 <code>topic</code> 与上游分支的关系。以后在 <code>push.default=simple</code> 的常见配置下，可以直接：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>push</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>push 分成两件事：</p><ol>
<li>把远端缺少的可达对象发送过去；</li>
<li>请求远端把某个 ref 从旧 ID 更新到新 ID。</li>
</ol><p>普通 push 默认要求更新是 fast-forward，也就是远端旧 tip 必须是新 tip 的祖先。否则 Git 拒绝 non-fast-forward，因为直接移动 ref 可能让别人已发布的 commit 失去分支入口。</p><div><div><div><div><span></span></div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span></span></div><div><span><p>push：远端旧 tip R -&gt; 本地新 tip N</p></span></div><div><span><p>R 是 N 的祖先吗？</p></span></div><div><span><p>fast-forward<br /><br />允许把远端 ref 移到 N</p></span></div><div><span><p>non-fast-forward<br /><br />默认拒绝</p></span></div><div><span><p>先 fetch 并集成远端历史<br /><br />或确认后使用 force-with-lease</p></span></div>
</div><div><div><span></span></div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span></span></div><div><span><p>push：远端旧 tip R -&gt; 本地新 tip N</p></span></div><div><span><p>R 是 N 的祖先吗？</p></span></div><div><span><p>fast-forward<br /><br />允许把远端 ref 移到 N</p></span></div><div><span><p>non-fast-forward<br /><br />默认拒绝</p></span></div><div><span><p>先 fetch 并集成远端历史<br /><br />或确认后使用 force-with-lease</p></span></div>
</div></div></div><p>服务器检查的是可达关系，不是比较谁的时间戳更新。如果远端 tip 不是本地 tip 的祖先，直接移动 ref 就可能把一段别人仍需要的历史甩出分支可达范围。</p><p>如果我确实 rebase 了<strong>自己的</strong>远端分支，需要改写它，我使用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>push</span><span> </span><span>--force-with-lease</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它比 <code>--force</code> 多一个租约条件：远端 ref 必须仍是我预期的值，否则拒绝覆盖。需要最严格地表达预期值时，可以写：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>push</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>--force-with-lease=refs/heads/topic:&lt;expected-old-oid&gt;</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>origin</span><span> </span><span>topic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>裸 <code>--force-with-lease</code> 通常以本地远端跟踪引用作为预期值；后台自动 fetch 可能更新这个依据，所以它不是绝对保险。<code>--force</code> 则直接关闭这层保护，可能丢掉远端 commit。我不会把 <code>push -f</code> 做成 shell alias，这种便利与拔掉烟雾报警器差不多。</p></section></section>
<section><h2>撤销操作：先问要改哪一层<a href="#撤销操作先问要改哪一层"><span>#</span></a></h2><p>Git 最让人害怕的地方通常不是保存，而是撤销。只要先回答两个问题，命令会清楚很多：</p><ol>
<li>要改工作区、index、branch ref，还是追加一个反向 commit？</li>
<li>目标历史是否已经共享？</li>
</ol><section><h3>常用决策表<a href="#常用决策表"><span>#</span></a></h3>

<table><thead><tr><th>意图</th><th>推荐命令</th><th>改动层</th><th>是否改写 branch 历史</th></tr></thead><tbody><tr><td>丢弃未暂存的路径修改</td><td><code>git restore &lt;path&gt;</code></td><td>工作区</td><td>否</td></tr><tr><td>取消暂存并保留工作区</td><td><code>git restore --staged &lt;path&gt;</code></td><td>index</td><td>否</td></tr><tr><td>从某快照恢复路径</td><td><code>git restore --source=&lt;rev&gt; &lt;path&gt;</code></td><td>工作区</td><td>否</td></tr><tr><td>修正最后一次未发布提交</td><td><code>git commit --amend</code></td><td>commit + ref</td><td>是</td></tr><tr><td>撤销已发布提交</td><td><code>git revert &lt;commit&gt;</code></td><td>新增反向 commit</td><td>否</td></tr><tr><td>本地分支退回但保留暂存</td><td><code>git reset --soft &lt;rev&gt;</code></td><td>ref</td><td>是</td></tr><tr><td>本地分支退回并重置 index</td><td><code>git reset --mixed &lt;rev&gt;</code></td><td>ref + index</td><td>是</td></tr><tr><td>本地分支、index、工作区全退回</td><td><code>git reset --hard &lt;rev&gt;</code></td><td>三者</td><td>是，并丢工作区修改</td></tr><tr><td>找回刚刚移丢的 commit</td><td><code>git reflog</code></td><td>只查看日志</td><td>否</td></tr></tbody></table><p><code>restore</code> 面向路径，<code>reset</code> 主要移动当前 branch 并按模式决定是否同步 index 和工作区，<code>revert</code> 则创建新 commit。名字相似，历史兼容性完全不同。</p></section><section><h3><code>revert</code> 为什么适合公开历史<a href="#revert-为什么适合公开历史"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>revert</span><span> </span><span>&lt;bad-commit&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它不删除坏 commit，而是计算反向变化并创建一个新 commit。所有协作者仍共享同一条已有历史，只需继续 fast-forward。对于已经推送到公共分支的错误，这通常是最无聊也最安全的方案。</p></section><section><h3>reflog：引用移动的本地黑匣子<a href="#reflog引用移动的本地黑匣子"><span>#</span></a></h3><p>我做了一个故意的事故：先记住第二个 commit <code>0063127</code>，再把 <code>main</code> 硬重置回第一个 commit：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>reset</span><span> </span><span>--hard</span><span> </span><span>cd5e714</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>git log</code> 看不到第二个 commit 了，但 <code>git reflog --oneline</code> 仍显示：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cd5e714 HEAD@{0}: reset: moving to cd5e714...</span></div></div><div><div><div>2</div></div><div><span>0063127 HEAD@{1}: commit: feat: advance the app</span></div></div><div><div><div>3</div></div><div><span>cd5e714 HEAD@{2}: commit (initial): feat: build the first snapshot</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>于是我恢复：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>reset</span><span> </span><span>--hard</span><span> </span><span>0063127</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>main</code> 又回到了第二个 commit。reflog 记录启用了日志的 refs 与 <code>HEAD</code> 的近期移动；普通非裸仓库通常会记录 <code>HEAD</code> 和本地分支，但不是所有引用都必然有 reflog。它不会 push 到远端，也不是永久备份；条目会过期，不可达对象最终可能被垃圾回收。但在误操作之后，第一反应应该是停止继续改写，运行 <code>git reflog</code>，给目标 commit 新建分支：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>branch</span><span> </span><span>rescue</span><span> </span><span>0063127</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条更稳妥的恢复路径最终得到下面这张历史图：</p><div><div><div>mainrescuecd5e7140063127
</div><div>mainrescuecd5e7140063127
</div></div></div><p><code>main</code> 仍停在 <code>cd5e714</code>，而新建的 <code>rescue</code> 重新指向 <code>0063127</code>。这张图描述的是恢复后的 commit DAG，不是重新执行 commit 的过程：<code>git branch rescue 0063127</code> 只补回一个引用，没有创建 <code>0063127</code>。</p><p>只要对象还在，名字丢了通常比内容丢了容易修。</p><div><div><div></div><div>Note</div></div><div><p>图里的 reflog 是恢复窗口，不是永久引用或远端备份。不要把”现在还能找回”理解成”永远不会被回收”。</p></div></div></section></section>
<section><h2>Git 为什么不会为每个版本保存一整份大文件<a href="#git-为什么不会为每个版本保存一整份大文件"><span>#</span></a></h2><p>为了观察物理存储，我创建了一个 255000 字节文本文件，提交后只追加 15 字节，再提交一次。两个 blob 的逻辑大小分别是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>255000 bytes</span></div></div><div><div><div>2</div></div><div><span>255015 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在运行 <code>git gc</code> 前，实验仓库有 15 个 loose objects，zlib 压缩后合计 44641 字节。运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>gc</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>verify-pack</span><span> </span><span>-v</span><span> </span><span>.git/objects/pack/</span><span>*</span><span>.idx</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>packfile 变成 14239 字节，索引为 1492 字节。<code>verify-pack</code> 显示较新的 255015 字节 blob 在 pack 中占 13080 字节，旧版本则被表示成深度 1 的 delta，数据只有 15 字节（加上对象编码后占 26 字节）。仅比较原 44641 字节 loose objects 与 14239 字节 pack，主体存储缩小约 68.1%。</p><p>这解释了两个看似矛盾的事实：</p><ul>
<li>Git 的逻辑模型是完整快照；</li>
<li>Git 的物理 packfile 可以跨相似对象做 delta compression。</li>
</ul><div><div><div><div><span><p>packfile：一种物理编码</p></span></div><div><span><p>逻辑模型：两个完整 blob</p></span></div><div><span></span></div><div><span><p>ID 不变</p></span></div><div><span><p>ID 不变</p></span></div><div><span><p>blob A<br /><br />255000 bytes</p></span></div><div><span><p>blob B<br /><br />255015 bytes</p></span></div><div><span><p>保存 blob B 的基底</p></span></div><div><span><p>blob A 的 delta<br /><br />数据 15 bytes</p></span></div>
</div><div><div><span><p>packfile：一种物理编码</p></span></div><div><span><p>逻辑模型：两个完整 blob</p></span></div><div><span></span></div><div><span><p>ID 不变</p></span></div><div><span><p>ID 不变</p></span></div><div><span><p>blob A<br /><br />255000 bytes</p></span></div><div><span><p>blob B<br /><br />255015 bytes</p></span></div><div><span><p>保存 blob B 的基底</p></span></div><div><span><p>blob A 的 delta<br /><br />数据 15 bytes</p></span></div>
</div></div></div><p>左边回答”对象是什么”，右边回答”这次 pack 如何省空间”。重新打包可以更换基底和 delta 链，却不能改动对象按完整内容计算出的 ID。</p><p>对象的 ID 仍由规范化的完整内容决定，不由”它是哪个对象的 delta”决定。Git 可以随时重新打包而不改变 commit ID。<code>git gc</code>、push 和服务端维护可能触发打包；日常不需要手工管理每个 pack。</p><p>我最后运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>fsck</span><span> </span><span>--full</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>命令没有输出并以 0 退出，表示这份小仓库的对象连通性和有效性检查通过。</p></section>
<section><h2>clone 到底复制了什么<a href="#clone-到底复制了什么"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>&lt;url&gt;</span><span> </span><span>project</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>概念上它做了这些事：</p><ol>
<li>创建本地仓库；</li>
<li>配置默认远端 <code>origin</code>；</li>
<li>与远端协商并下载所需对象；</li>
<li>建立远端跟踪引用；</li>
<li>创建并检出默认本地分支。</li>
</ol><div><div><div>工作区本地 .git远端仓库Git 客户端工作区本地 .git远端仓库Git 客户端我git clone URL project创建仓库并配置 origin协商 refs 与缺少的对象发送 packfile 与远端 ref 值建立 origin/main 与本地 main检出默认分支快照我
</div><div>工作区本地 .git远端仓库Git 客户端工作区本地 .git远端仓库Git 客户端我git clone URL project创建仓库并配置 origin协商 refs 与缺少的对象发送 packfile 与远端 ref 值建立 origin/main 与本地 main检出默认分支快照我
</div></div></div><p>clone 之后出现的是一个能独立工作的本地仓库，而不是远端文件夹的网络映射。<code>origin</code> 只是把这个新仓库与来源仓库重新连起来的配置。</p><p>Git 是分布式版本控制系统，因为普通 clone 通常拥有项目历史和对象数据库，不只是服务器工作区的一个薄视图。离线时我仍能 log、diff、branch、commit、merge 和 rebase；只有与别的仓库交换对象和 refs 时才需要网络。</p><p>大仓库可以选择浅克隆、部分克隆或稀疏检出，但三者解决不同问题：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>--depth=1</span><span> </span><span>&lt;url&gt;</span><span>           </span><span># 截断历史深度</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>--filter=blob:none</span><span> </span><span>&lt;url&gt;</span><span>  </span><span># 按需获取 blob</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>sparse-checkout</span><span> </span><span>set</span><span> </span><span>src</span><span> </span><span>docs</span><span>    </span><span># 限制工作区展开路径</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不要把它们统称为”只下载一部分代码”。浅克隆改变可用历史，partial clone 延迟对象获取，sparse-checkout 主要改变工作区呈现。</p></section>
<section><h2>多任务开发：<code>worktree</code> 比反复 stash 更直接<a href="#多任务开发worktree-比反复-stash-更直接"><span>#</span></a></h2><p>当我正在 <code>feature</code> 分支写到一半，突然要修 <code>main</code> 上的线上 bug，传统动作常常是 stash、switch、修复、切回、pop，然后祈祷 stash 冲突别来凑热闹。</p><p>更清楚的方式是附加工作树：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>worktree</span><span> </span><span>add</span><span> </span><span>-b</span><span> </span><span>hotfix</span><span> </span><span>../project-hotfix</span><span> </span><span>main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个仓库对象数据库现在对应两个工作目录：原目录继续停在 <code>feature</code>，新目录检出 <code>hotfix</code>。两边有各自的 <code>HEAD</code> 和 index，共享 objects 和大部分 refs。</p><div><div><div><div><span><p>project-hotfix/：附加 worktree</p></span></div><div><span><p>project/：主 worktree</p></span></div><div><span><p>共享的 common Git dir</p></span></div><div><span><p>checkout / restore</p></span></div><div><span><p>add</p></span></div><div><span><p>checkout / restore</p></span></div><div><span><p>add</p></span></div><div><span></span></div><div><span></span></div><div><span><p>读写共享对象</p></span></div><div><span><p>读写共享对象</p></span></div><div><span><p>objects<br /><br />全部 commit、tree、blob</p></span></div><div><span><p>refs/heads/feature</p></span></div><div><span><p>refs/heads/hotfix</p></span></div><div><span><p>HEAD</p></span></div><div><span><p>独立 index</p></span></div><div><span><p>工作目录<br /><br />未完成的 feature 修改</p></span></div><div><span><p>HEAD</p></span></div><div><span><p>独立 index</p></span></div><div><span><p>工作目录<br /><br />hotfix 修改</p></span></div>
</div><div><div><span><p>project-hotfix/：附加 worktree</p></span></div><div><span><p>project/：主 worktree</p></span></div><div><span><p>共享的 common Git dir</p></span></div><div><span><p>checkout / restore</p></span></div><div><span><p>add</p></span></div><div><span><p>checkout / restore</p></span></div><div><span><p>add</p></span></div><div><span></span></div><div><span></span></div><div><span><p>读写共享对象</p></span></div><div><span><p>读写共享对象</p></span></div><div><span><p>objects<br /><br />全部 commit、tree、blob</p></span></div><div><span><p>refs/heads/feature</p></span></div><div><span><p>refs/heads/hotfix</p></span></div><div><span><p>HEAD</p></span></div><div><span><p>独立 index</p></span></div><div><span><p>工作目录<br /><br />未完成的 feature 修改</p></span></div><div><span><p>HEAD</p></span></div><div><span><p>独立 index</p></span></div><div><span><p>工作目录<br /><br />hotfix 修改</p></span></div>
</div></div></div><p><code>worktree</code> 不是再 clone 一次。昂贵的对象数据库和大部分 refs 只保留一份；每个工作树只增加自己的工作目录、<code>HEAD</code>、index 与少量管理状态。于是两个任务可以同时脏、同时运行构建，却不会争夺同一份 checkout。</p><p>同一个本地 branch 默认不能同时检出到两个 worktree。这层保护避免两个目录都试图移动同一个 branch ref，却给人一种自己互不相关的错觉。</p><p>完成后再移除附加工作树：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>worktree</span><span> </span><span>remove</span><span> </span><span>../project-hotfix</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这对并行运行测试、比较两个版本，以及让多个 AI agent 各自在独立分支工作尤其有用。它不是复制两份完整仓库，也避免一个工作区被不同任务来回覆盖。</p><p><code>git stash</code> 仍适合很短的临时收纳：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>stash</span><span> </span><span>push</span><span> </span><span>--include-untracked</span><span> </span><span>-m</span><span> </span><span>'wip: parser experiment'</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>stash</span><span> </span><span>list</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>stash</span><span> </span><span>show</span><span> </span><span>--patch</span><span> </span><span>stash@{</span><span>0</span><span>}</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>stash</span><span> </span><span>pop</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>但 stash 本质上仍由 commit-like 对象和 refs 管理，不是神秘抽屉。我会给它消息，并尽快处理；需要跨天保存或交给别人时，临时分支上的普通 commit 更可见、更可审查。</p></section>
<section><h2>一套我现在愿意长期使用的命令行<a href="#一套我现在愿意长期使用的命令行"><span>#</span></a></h2><section><h3>初始配置<a href="#初始配置"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>user.name</span><span> </span><span>'Your Name'</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>user.email</span><span> </span><span>'you@example.com'</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>init.defaultBranch</span><span> </span><span>main</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>fetch.prune</span><span> </span><span>true</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>merge.conflictStyle</span><span> </span><span>zdiff3</span></div></div><div><div><div>6</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>rerere.enabled</span><span> </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里前两项进入 commit 身份；<code>fetch.prune</code> 清理失效的远端跟踪引用；<code>zdiff3</code> 在冲突标记中显示 base，帮助判断双方分别改了什么；<code>rerere</code> 记录冲突解决结果，在相同冲突再次出现时复用。签名、凭据管理、换行符和 pull 策略依平台与团队而定，不适合复制一份”万能配置”。</p><p>检查配置来源比只看最终值更有用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--list</span><span> </span><span>--show-origin</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>每日开始<a href="#每日开始"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>switch</span><span> </span><span>main</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>fetch</span><span> </span><span>origin</span><span> </span><span>--prune</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>pull</span><span> </span><span>--ff-only</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>switch</span><span> </span><span>-c</span><span> </span><span>feature/readable-name</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果项目要求 rebase 工作流，我会显式采用它，而不是同时配置互相冲突的习惯。</p></section><section><h3>制作原子 commit<a href="#制作原子-commit"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>status</span><span> </span><span>--short</span><span> </span><span>--branch</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>diff</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>--patch</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--staged</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>commit</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>commit message 解释变化的意图和原因。标题能独立读懂，正文写约束、权衡和行为变化；不要把 <code>update files</code> 这种只能证明键盘工作过的句子留给历史。</p></section><section><h3>同步并复核<a href="#同步并复核"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>fetch</span><span> </span><span>origin</span><span> </span><span>--prune</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>rebase</span><span> </span><span>origin/main</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>range-diff</span><span> </span><span>origin/main...@{upstream}</span><span> </span><span>origin/main...HEAD</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>push</span><span> </span><span>--set-upstream</span><span> </span><span>origin</span><span> </span><span>HEAD</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>@{upstream}</code> 需要当前分支已经配置 upstream；首次发布前没有它。<code>HEAD</code> 作为 push 源表示当前分支 tip，但目标命名和权限仍由 refspec、配置和服务端规则决定。团队若使用 merge，则换成对应流程，不要为了套模板偷偷改写历史。</p></section><section><h3>阅读历史<a href="#阅读历史"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>log</span><span> </span><span>--oneline</span><span> </span><span>--graph</span><span> </span><span>--decorate</span><span> </span><span>--all</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>log</span><span> </span><span>--first-parent</span><span> </span><span>main</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>show</span><span> </span><span>&lt;commit&gt;</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>&lt;a&gt;..&lt;b&gt;</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>blame</span><span> </span><span>-L</span><span> </span><span>&lt;start&gt;,&lt;end&gt;</span><span> </span><span>--</span><span> </span><span>&lt;path&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>blame</code> 回答”这一行最后由哪个 commit 引入”，不是”该怪谁”。拿到 commit 后继续 <code>git show</code> 和阅读上下文，通常才有答案。</p></section><section><h3>定位引入 bug 的 commit<a href="#定位引入-bug-的-commit"><span>#</span></a></h3><p>当我有一个可重复判定好坏的命令时，<code>bisect</code> 通常能在 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 个 commit 中用大约 <span><span>log⁡2N\log_2 N</span><span><span><span></span><span><span>lo<span>g</span></span><span><span><span><span><span><span></span><span><span>2</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>N</span></span></span></span> 次判断定位边界。在线性、每个候选都可测试的理想历史中，1024 个候选 commit 约需 10 轮，而不是从头读 1024 次 diff；复杂 merge DAG 和被跳过的 commit 会增加轮数，甚至让 Git 无法唯一定位。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>bisect</span><span> </span><span>start</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>bisect</span><span> </span><span>bad</span><span> </span><span>HEAD</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>bisect</span><span> </span><span>good</span><span> </span><span>&lt;known-good&gt;</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>bisect</span><span> </span><span>run</span><span> </span><span>./reproduce.sh</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>bisect</span><span> </span><span>reset</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>reproduce.sh</code> 必须以 0 表示 good，1 到 127（除 125）表示 bad，125 表示无法测试。这个脚本值得认真写，因为错误的 oracle 会高效地把我带到错误答案，计算机在这方面一如既往地非常配合。</p></section></section>
<section><h2>哪些旧习惯该换，哪些只是需要理解<a href="#哪些旧习惯该换哪些只是需要理解"><span>#</span></a></h2>

<table><thead><tr><th>旧习惯或高风险写法</th><th>我现在使用</th><th>原因</th></tr></thead><tbody><tr><td><code>git checkout &lt;branch&gt;</code></td><td><code>git switch &lt;branch&gt;</code></td><td>明确表达切分支</td></tr><tr><td><code>git checkout -- &lt;path&gt;</code></td><td><code>git restore &lt;path&gt;</code></td><td>明确表达覆盖工作区路径</td></tr><tr><td><code>git reset HEAD &lt;path&gt;</code></td><td><code>git restore --staged &lt;path&gt;</code></td><td>明确表达取消暂存</td></tr><tr><td>无参数 <code>git pull</code></td><td>显式 <code>--ff-only</code>、<code>--rebase</code> 或 <code>--no-rebase</code></td><td>明确集成策略</td></tr><tr><td><code>git push --force</code></td><td><code>git push --force-with-lease</code></td><td>避免覆盖意外出现的远端更新</td></tr><tr><td><code>git filter-branch</code></td><td><code>git filter-repo</code> 等专用工具</td><td>官方文档明确警告其安全和性能陷阱</td></tr><tr><td>用 stash 在任务间来回切</td><td><code>git worktree add</code></td><td>两个工作目录保持独立 index 与工作状态</td></tr><tr><td>一次 <code>git add .</code> 后盲提交</td><td><code>add -p</code> + <code>diff --staged</code></td><td>设计并验证 commit 边界</td></tr></tbody></table><p>这里最重要的修正是：<strong>旧不等于废弃，现代也不等于更短。</strong> <code>checkout</code>、<code>reset</code> 和 merge 都仍然是合法且必要的 Git 概念。我要淘汰的是含糊意图和无保护的危险默认，而不是为了追新把能工作的命令全部换皮。</p><p><code>git filter-branch</code> 是少数官方文档直接写着”不推荐使用”的例子；文档建议改用 <code>git filter-repo</code> 等替代工具。反过来，<code>git checkout</code> 并没有这种弃用声明。准确区分”职责过载，所以有更清晰的新命令”和”官方明确不推荐”，比传播一张真假混合的现代化清单更重要。</p></section>
<section><h2>把 AI 放回正确的位置<a href="#把-ai-放回正确的位置"><span>#</span></a></h2><p>理解这些内部结构后，我并不打算少用 AI。恰恰相反，我可以给出更准确、也更可验证的任务：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>请先展示工作区与 index 的 diff，只暂存解析器修复相关 hunk；</span></div></div><div><div><div>2</div></div><div><span>创建一个原子 commit；fetch 后不要直接 pull；</span></div></div><div><div><div>3</div></div><div><span>比较当前分支与 origin/main，再告诉我应 merge 还是 rebase；</span></div></div><div><div><div>4</div></div><div><span>禁止 force，确需改写个人远端分支时使用 force-with-lease。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这比”帮我把代码传上去”多了一点字，却把数据损失、历史策略和 commit 边界都变成了显式约束。AI 能替我执行命令，但它不能替我承担仓库策略。至少目前如此，我怀疑以后也仍然如此：工具会越来越善于推断意图，而真正稀缺的东西会变成<strong>能否清楚表达意图，并检查结果是否符合约束</strong>。</p><p>如果未来的 Git UI 只剩一个”完成我的修改”按钮，底层大概率仍要回答今天这些问题：下一张快照是什么，哪条引用应该移动，是否允许改写已经共享的历史，远端当前值是否仍符合预期。按钮可以消失，状态机不会。</p><p>我现在仍会让 AI 帮我 rebase、拆 commit、找回 reflog 里的对象，甚至写那段无聊的 bisect oracle。但当它说”已完成”时，我至少知道该看 <code>status</code>、<code>diff --staged</code>、<code>log --graph</code> 和 refs，而不是只看终端最后一行是不是绿色。很好，接下来终于可以放心地让机器替我敲命令了。</p></section>
<section><h2>继续拆下去<a href="#继续拆下去"><span>#</span></a></h2><ul>
<li><a href="https://git-scm.com/book/en/v2/Git-Internals-Git-Objects" target="_blank">Pro Git: Git Internals - Git Objects</a></li>
<li><a href="https://git-scm.com/book/en/v2/Git-Internals-Git-References" target="_blank">Pro Git: Git Internals - Git References</a></li>
<li><a href="https://git-scm.com/book/en/v2/Git-Internals-Packfiles" target="_blank">Pro Git: Git Internals - Packfiles</a></li>
<li><a href="https://git-scm.com/docs/git-switch" target="_blank"><code>git switch</code> 官方文档</a></li>
<li><a href="https://git-scm.com/docs/git-restore" target="_blank"><code>git restore</code> 官方文档</a></li>
<li><a href="https://git-scm.com/docs/git-pull" target="_blank"><code>git pull</code> 官方文档</a></li>
<li><a href="https://git-scm.com/docs/git-push" target="_blank"><code>git push</code> 官方文档</a></li>
<li><a href="https://git-scm.com/docs/git-reflog" target="_blank"><code>git reflog</code> 官方文档</a></li>
<li><a href="https://git-scm.com/docs/git-worktree" target="_blank"><code>git worktree</code> 官方文档</a></li>
<li><a href="https://git-scm.com/docs/git-diff" target="_blank"><code>git diff</code> 官方文档</a></li>
<li><a href="https://git-scm.com/docs/diff-generate-patch" target="_blank">Git patch 输出格式</a></li>
<li><a href="https://git-scm.com/docs/git-filter-branch" target="_blank"><code>git filter-branch</code> 的官方警告</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/understanding-qemu/</id>
      <title type="text">一台不存在的计算机：QEMU 工作原理与实践</title>
      <published>2026-09-04T14:00:00.000Z</published>
      <updated>2026-09-04T14:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/understanding-qemu/"/>
      <summary type="text">我用 QEMU 10.2.4 从零启动一个 512 字节 x86 扇区，运行 992 字节 AArch64 ELF，再沿着 machine、TCG/KVM、设备与后端、VirtIO、QMP、GDB、磁盘、网络和 ARM virt 拆开完整系统模拟链。</summary>
      <content type="html"><![CDATA[<p>写完 <a href="/posts/understanding-kconfig/">Kconfig</a> 和 <a href="/posts/understanding-kbuild/">Kbuild</a> 之后，我已经能配置、编译并加载一个 Linux 内核模块，却发现实验台中央还摆着一块巨大的黑盒：我会复制一行 <code>qemu-system-x86_64</code> 命令，但不知道它究竟造了哪台机器、CPU 在哪里执行、磁盘为何既是文件又是设备，也不知道按下 <code>Ctrl+A X</code> 时到底在和谁说话。于是我写了一个 512 字节启动扇区，把它放进 QEMU 10.2.4，又从 QMP、Device Tree 和一份 992 字节的 AArch64 ELF 反向拆开这台并不存在的计算机。</p>
<div><div><div><div><span></span></div><div><span><p>TCG</p></span></div><div><span><p>KVM</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Guest 指令</p></span></div><div><span><p>加速器</p></span></div><div><span><p>翻译成 Host 指令</p></span></div><div><span><p>让 Host CPU 执行</p></span></div><div><span><p>Machine 与内存映射</p></span></div><div><span><p>设备前端</p></span></div><div><span><p>块、网络、字符后端</p></span></div><div><span><p>固件或直接内核启动</p></span></div><div><span><p>HMP / QMP / GDB</p></span></div>
</div><div><div><span></span></div><div><span><p>TCG</p></span></div><div><span><p>KVM</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Guest 指令</p></span></div><div><span><p>加速器</p></span></div><div><span><p>翻译成 Host 指令</p></span></div><div><span><p>让 Host CPU 执行</p></span></div><div><span><p>Machine 与内存映射</p></span></div><div><span><p>设备前端</p></span></div><div><span><p>块、网络、字符后端</p></span></div><div><span><p>固件或直接内核启动</p></span></div><div><span><p>HMP / QMP / GDB</p></span></div>
</div></div></div>
<p>这张图是本文的地图。QEMU 不是单纯的”虚拟 CPU”，也不是一个好看的虚拟机窗口。它把指令执行、整机拓扑、设备模型、宿主机后端和控制接口拼成一个进程。命令行之所以长，只是因为我们在一行文本里描述了一台计算机。</p>
<section><h2>先把名字拆开<a href="#先把名字拆开"><span>#</span></a></h2><p>QEMU 最常见的 3 组程序做的是不同事情：</p>

<table><thead><tr><th>程序</th><th>输入</th><th>模拟范围</th><th>典型用途</th></tr></thead><tbody><tr><td><code>qemu-system-x86_64</code>、<code>qemu-system-aarch64</code></td><td>固件、内核、磁盘和设备参数</td><td>CPU、内存、总线、中断控制器、设备等整机</td><td>启动操作系统、固件和裸机程序</td></tr><tr><td><code>qemu-aarch64</code>、<code>qemu-riscv64</code></td><td>某个目标架构的用户态 ELF</td><td>指令集、系统调用、信号和线程接口</td><td>在 x86_64 Linux 上运行或测试异构 Linux 程序</td></tr><tr><td><code>qemu-img</code></td><td>磁盘镜像</td><td>不运行 Guest</td><td>创建、检查、转换、比较镜像</td></tr></tbody></table><p>另外 3 个名字经常与它混在一起：</p><ul>
<li><strong>KVM</strong> 是 Linux 内核里的虚拟化接口。它让兼容架构的 Guest 指令直接在硬件虚拟化模式中执行，但不负责给我造 UART、网卡和磁盘。</li>
<li><strong>QEMU</strong> 可以只用 TCG 完成跨架构模拟，也可以调用 KVM 执行 vCPU，同时继续提供设备模型和进程生命周期。</li>
<li><strong>libvirt</strong> 是更高一层的管理 API。它探测能力、生成 QEMU 参数并管理网络、存储和生命周期；<code>virsh</code> 和 virt-manager 通常站在这一层。</li>
</ul><p>所以”QEMU 还是 KVM”不是一个很好的二选一。常见的 Linux 虚拟机恰好是 <strong>QEMU + KVM</strong>：KVM 跑 CPU，QEMU 造其余机器。只写 <code>qemu-system-x86_64</code> 而不指定加速器时，QEMU 默认使用 TCG；安装了 KVM 并不等于这条命令自动用上它。</p><p>本文固定在以下实验环境：</p>

<table><thead><tr><th>项目</th><th>实验值</th></tr></thead><tbody><tr><td>Host</td><td>x86_64 Linux</td></tr><tr><td>QEMU</td><td><code>10.2.4</code></td></tr><tr><td>GNU Binutils</td><td><code>2.46</code></td></tr><tr><td>Guest 实验 1</td><td>16-bit x86 裸机启动扇区</td></tr><tr><td>Guest 实验 2</td><td>AArch64 Linux 用户态静态 ELF</td></tr><tr><td>加速器</td><td>TCG 与 KVM 均实际启动</td></tr></tbody></table><p>先问正在使用的二进制，而不是问搜索引擎记得哪个版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>--version</span></div></div><div><div><div>2</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-accel</span><span> </span><span>help</span></div></div><div><div><div>3</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-machine</span><span> </span><span>help</span></div></div><div><div><div>4</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-cpu</span><span> </span><span>help</span></div></div><div><div><div>5</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-device</span><span> </span><span>help</span></div></div><div><div><div>6</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-nic</span><span> </span><span>help</span></div></div><div><div><div>7</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-display</span><span> </span><span>help</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在我的 QEMU 10.2.4 构建中，x86_64 的 <code>-machine help</code> 打印 43 行 machine 类型，<code>-device help</code> 列出 455 个设备模型；AArch64 的 machine 列表则有 113 行。这里包含版本化 machine 和别名，不能直接当成 113 块不同开发板，但它已经很好地解释了为什么背命令行是个坏主意。QEMU 自己就是可查询的硬件目录。</p></section>
<section><h2>一条命令其实在描述什么？<a href="#一条命令其实在描述什么"><span>#</span></a></h2><p>我会把 system emulator 的参数按 7 层阅读：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-$ARCH \</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>[machine] [cpu] [accelerator] \</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>[devices] [backends] [interfaces] [boot]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>层</th><th>回答的问题</th><th>常见参数</th></tr></thead><tbody><tr><td>Machine</td><td>这是什么板子，地址图和默认设备是什么？</td><td><code>-machine</code>、<code>-m</code>、<code>-smp</code></td></tr><tr><td>CPU</td><td>Guest 看见哪种 CPU 和哪些 feature？</td><td><code>-cpu</code></td></tr><tr><td>Accelerator</td><td>指令由 TCG 翻译还是交给 Hypervisor？</td><td><code>-accel</code></td></tr><tr><td>Device</td><td>Guest 驱动看见什么硬件？</td><td><code>-device</code></td></tr><tr><td>Backend</td><td>设备的数据通向 Host 哪里？</td><td><code>-blockdev</code>、<code>-netdev</code>、<code>-chardev</code></td></tr><tr><td>Interface</td><td>人和管理程序怎样观察、控制和调试？</td><td><code>-display</code>、<code>-serial</code>、<code>-qmp</code>、<code>-gdb</code></td></tr><tr><td>Boot</td><td>第一条 Guest 代码来自哪里？</td><td><code>-bios</code>、<code>-kernel</code>、<code>-initrd</code>、<code>-append</code>、<code>-drive</code></td></tr></tbody></table><p>这里最有用的分界是 <strong>device 与 backend</strong>。<code>virtio-net-pci</code> 是 Guest 能枚举并由驱动绑定的 PCI 设备；<code>user</code>、<code>tap</code> 或 <code>passt</code> 是 Host 侧真正收发包的后端。<code>virtio-blk-pci</code> 是 Guest 磁盘控制器；raw 文件、qcow2 文件或 Host 块设备是后端。前者像插座，后者像插座后面的电线，只是这个比喻用一次就够了。</p></section>
<section><h2>我先启动 512 字节，而不是一个发行版<a href="#我先启动-512-字节而不是一个发行版"><span>#</span></a></h2><p>如果一上来就启动 Debian，屏幕上会同时出现固件、引导器、内核、initramfs、systemd 和虚拟硬件。每一层都可能成功地遮住上一层。为了只观察 QEMU，我写了一个 BIOS 启动扇区：初始化 COM1，输出一行字，然后向测试退出设备写一个值。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.code16</span></div></div><div><div><div>2</div></div><div><span>.global _start</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>_start:</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>cli</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>xorw %</span><span>ax</span><span>, %</span><span>ax</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>movw %</span><span>ax</span><span>, %</span><span>ds</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>movw %</span><span>ax</span><span>, %</span><span>ss</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>movw </span><span>$0x7c00</span><span>, %</span><span>sp</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span> <span> </span></span><span># COM1: 115200 baud, 8N1, FIFO on.</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3f9</span><span>, %</span><span>dx</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>xorb %</span><span>al</span><span>, %</span><span>al</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3fb</span><span>, %</span><span>dx</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>movb </span><span>$0x80</span><span>, %</span><span>al</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3f8</span><span>, %</span><span>dx</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>movb </span><span>$0x01</span><span>, %</span><span>al</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3f9</span><span>, %</span><span>dx</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>xorb %</span><span>al</span><span>, %</span><span>al</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3fb</span><span>, %</span><span>dx</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>movb </span><span>$0x03</span><span>, %</span><span>al</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3fa</span><span>, %</span><span>dx</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>movb </span><span>$0xc7</span><span>, %</span><span>al</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3fc</span><span>, %</span><span>dx</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>movb </span><span>$0x0b</span><span>, %</span><span>al</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>movw </span><span>$message</span><span>, %</span><span>si</span></div></div><div><div><div>35</div></div><div><span>print:</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>lodsb</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>testb %</span><span>al</span><span>, %</span><span>al</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>jz</span><span> done</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>movb %</span><span>al</span><span>, %</span><span>ah</span></div></div><div><div><div>40</div></div><div><span>wait_tx:</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3fd</span><span>, %</span><span>dx</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>inb %</span><span>dx</span><span>, %</span><span>al</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>testb </span><span>$0x20</span><span>, %</span><span>al</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>jz</span><span> wait_tx</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>movb %</span><span>ah</span><span>, %</span><span>al</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>movw </span><span>$0x3f8</span><span>, %</span><span>dx</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>jmp</span><span> print</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div><span>done:</span></div></div><div><div><div>51</div></div><div><span><span>  </span></span><span>movw </span><span>$0xf4</span><span>, %</span><span>dx</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>movb </span><span>$0x10</span><span>, %</span><span>al</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>outb %</span><span>al</span><span>, %</span><span>dx</span></div></div><div><div><div>54</div></div><div><span><span>  </span></span><span>hlt</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>message:</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>.asciz "hello from sector </span><span>0</span><span>\r\n"</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>.org </span><span>510</span></div></div><div><div><div>60</div></div><div><span>.</span><span>word</span><span> </span><span>0xaa55</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>构建时我先链接 ELF，让 <code>message</code> 得到以 <code>0x7c00</code> 为基准的正确地址，再只抽取 <code>.text</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>as</span><span> </span><span>--32</span><span> </span><span>-o</span><span> </span><span>boot.o</span><span> </span><span>boot.S</span></div></div><div><div><div>2</div></div><div><span>ld</span><span> </span><span>-m</span><span> </span><span>elf_i386</span><span> </span><span>-Ttext</span><span> </span><span>0x7c00</span><span> </span><span>-o</span><span> </span><span>boot.elf</span><span> </span><span>boot.o</span></div></div><div><div><div>3</div></div><div><span>objcopy</span><span> </span><span>-O</span><span> </span><span>binary</span><span> </span><span>--only-section=.text</span><span> </span><span>boot.elf</span><span> </span><span>boot.bin</span></div></div><div><div><div>4</div></div><div><span>stat</span><span> </span><span>--format=</span><span>'%n %s bytes'</span><span> </span><span>boot.bin</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出刚好是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>boot.bin 512 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后 16 字节为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>0x55 0xaa</code> 位于偏移 510，BIOS 才把它当成可启动扇区。现在启动：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>pc,accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-drive</span><span> </span><span>file=boot.bin,format=raw,if=floppy</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-display</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-serial</span><span> </span><span>stdio</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-monitor</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-no-reboot</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-device</span><span> </span><span>isa-debug-exit,iobase=0xf4,iosize=</span><span>0x04</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实际输出只有一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hello from sector 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>进程退出码为 <code>33</code>。这不是随机事故。<code>isa-debug-exit</code> 收到 <code>0x10</code> 后按 <code>(value &lt;&lt; 1) | 1</code> 生成退出码，所以得到 <span><span>0x10×2+1=330x10 \times 2 + 1 = 33</span><span><span><span></span><span>0</span><span>x</span><span>10</span><span></span><span>×</span><span></span></span><span><span></span><span>2</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span><span></span><span>=</span><span></span></span><span><span></span><span>33</span></span></span></span>。这个设备不是现实 PC 上的标准零件，而是我明确加进实验机的测试出口。Guest 写一个 I/O port，QEMU 的设备回调收到访问并结束 Host 进程。512 字节代码已经跨过了 CPU、I/O 地址空间、UART、字符后端和进程生命周期 5 层。</p><section><h3>从复位向量到 <code>0x7c00</code><a href="#从复位向量到-0x7c00"><span>#</span></a></h3><p>这条路径仍有固件参与：</p><div><div><div>Host16550A UARTGuest RAM虚拟软盘SeaBIOSx86 vCPUHost16550A UARTGuest RAM虚拟软盘SeaBIOSx86 vCPU从复位向量开始执行读取第 0 扇区放到物理地址 0x7c00跳到 0x7c00OUT 到 COM1写入 stdio chardev
</div><div>Host16550A UARTGuest RAM虚拟软盘SeaBIOSx86 vCPUHost16550A UARTGuest RAM虚拟软盘SeaBIOSx86 vCPU从复位向量开始执行读取第 0 扇区放到物理地址 0x7c00跳到 0x7c00OUT 到 COM1写入 stdio chardev
</div></div></div><p><code>boot.bin</code> 不是 Host 进程可直接执行的 ELF。SeaBIOS 经虚拟软盘控制器读它，检查签名并复制到 Guest RAM。我的 <code>outb</code> 也没有直接碰 Host 的物理串口；QEMU 捕获 Guest I/O 指令，把访问路由到 16550A 设备模型，再由 <code>-serial stdio</code> 连接终端。</p><p>我把 <code>-accel tcg</code> 换成 <code>-accel kvm</code> 后，同一个扇区也输出同一行并以 33 退出。可观察的 machine contract 没变，变化的是 vCPU 执行引擎。这正是加速器应该有的边界。</p></section></section>
<section><h2>TCG 到底模拟了什么？<a href="#tcg-到底模拟了什么"><span>#</span></a></h2><p>TCG 是 Tiny Code Generator。粗略但有用的数据流是：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>分支、异常、I/O、失效</p></span></div><div><span><p>Guest 指令</p></span></div><div><span><p>Guest 前端解码</p></span></div><div><span><p>TCG 中间操作</p></span></div><div><span><p>Host 后端生成代码</p></span></div><div><span><p>Translation Block 缓存</p></span></div><div><span><p>Host CPU 执行</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>分支、异常、I/O、失效</p></span></div><div><span><p>Guest 指令</p></span></div><div><span><p>Guest 前端解码</p></span></div><div><span><p>TCG 中间操作</p></span></div><div><span><p>Host 后端生成代码</p></span></div><div><span><p>Translation Block 缓存</p></span></div><div><span><p>Host CPU 执行</p></span></div>
</div></div></div><p>QEMU 通常以 translation block 为单位翻译一段 Guest 指令，而不是每执行一条就启动一次解释器。已经生成的 Host 代码可以缓存和复用；控制流、异常、自修改代码、MMIO 等事件再让执行返回 QEMU。具体边界是实现细节，不能把 TB 简化成 C 语言 basic block 的永久一一映射。</p><p>TCG 的价值不是”比硬件快”，而是 <strong>Host 与 Guest 架构可以不同</strong>。x86_64 Host 能运行 AArch64、RISC-V、MIPS 等 Guest，也能提供较强的可观察性、确定性相关选项和软件断点能力。代价是翻译、状态同步和设备模拟都有成本。</p><p>常用写法：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-aarch64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>virt,accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-cpu</span><span> </span><span>max</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-smp</span><span> </span><span>4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-cpu max</code> 表示 TCG 能提供的丰富功能集合，不代表某颗真实 SoC。若目标是复现 Cortex-A57 的行为，就应选 <code>-cpu cortex-a57</code>，并接受这个模型实现了什么、不实现什么。</p><p><code>-accel tcg,thread=multi</code> 允许支持的目标为多个 vCPU 使用多个 TCG 线程。它不保证 4 个 Guest vCPU 就得到 4 倍性能；锁竞争、设备路径和 Guest 工作负载都在场。<code>one-insn-per-tb=on</code> 则故意把每个 TB 缩到一条指令，适合分析日志，不适合假装性能还正常（这次真的不能两全）。</p></section>
<section><h2>KVM 为什么快，又为什么仍然需要 QEMU？<a href="#kvm-为什么快又为什么仍然需要-qemu"><span>#</span></a></h2><p>当 Host 与 Guest 架构兼容、CPU 支持硬件虚拟化且 <code>/dev/kvm</code> 可用时，可以写：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>q35</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-accel</span><span> </span><span>kvm</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-cpu</span><span> </span><span>host</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-m</span><span> </span><span>2G</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-smp</span><span> </span><span>4</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>QEMU 通过 KVM API 创建 VM 和 vCPU，把 Guest RAM 注册给内核，然后让 vCPU 进入硬件虚拟化执行。普通 Guest 指令不必先翻译成 Host 指令；遇到需要 Host 处理的事件，例如某些 I/O、MMIO 或停机状态，KVM 退出到 QEMU，QEMU 再运行对应设备逻辑。</p><div><div><div>QEMU device modelHost CPU/dev/kvmQEMUQEMU device modelHost CPU/dev/kvmQEMU创建 VM、vCPU、映射 Guest RAMKVM_RUN进入 Guest modeMMIO/PIO 等 VM exit返回 exit 原因执行设备访问返回结果再次 KVM_RUN
</div><div>QEMU device modelHost CPU/dev/kvmQEMUQEMU device modelHost CPU/dev/kvmQEMU创建 VM、vCPU、映射 Guest RAMKVM_RUN进入 Guest modeMMIO/PIO 等 VM exit返回 exit 原因执行设备访问返回结果再次 KVM_RUN
</div></div></div><p><code>-cpu host</code> 暴露 Host CPU 的大量特性，通常适合单机开发和性能实验，却会把 VM 绑得更紧。需要跨不同 Host live migration 时，应选择双方共同支持的命名 CPU model 和 feature 集，而不是默认假设另一台机器也长着同一颗 CPU。</p><p>我不会用 512 字节启动扇区比较 TCG 与 KVM 性能。这个 workload 的 Guest 只输出 21 个字符，QEMU 进程启动和固件执行占据了绝大多数时间；测出来的主要是噪声，不是 CPU 加速比。要比较加速器，至少应固定 machine、CPU feature、vCPU 数、Guest 镜像和 workload，再分别测 Guest 内部 CPU 时间、I/O 吞吐与 Host 消耗。</p><p>也不要把 KVM 当成安全魔法。QEMU 的设备模型仍然处理 Guest 提交的数据，QEMU 官方文档明确指出 TCG 不是安全边界；测试不可信镜像时仍应使用非特权账户、最小设备集合、受控网络和宿主机沙箱策略。</p></section>
<section><h2>Machine 不是一个无关紧要的名字<a href="#machine-不是一个无关紧要的名字"><span>#</span></a></h2><p>Machine type 决定主板级拓扑：地址空间、总线、芯片组、中断控制器、固件接口和一组默认设备。CPU 只是板子上的一个节点。</p><p>x86_64 常见两族是：</p><ul>
<li><code>pc-i440fx-*</code>：较老的 i440FX + PIIX PC 平台，兼容面广。</li>
<li><code>pc-q35-*</code>：较现代的 Q35 + ICH9 平台，原生 PCIe 拓扑更符合新设备。</li>
</ul><p>我的 QEMU 10.2.4 默认项是 <code>pc-i440fx-10.2</code>，而 <code>pc</code> 是指向当前默认版本的别名。用于一次性内核实验时，别名很方便；用于长期保存或迁移的 VM 时，应记录版本化 machine，例如 <code>pc-q35-10.2</code>。升级 QEMU 后，<code>q35</code> 别名可能指向新兼容级别，而版本化名字存在的目的正是固定 Guest 可见的硬件 contract。</p><p>ARM 更能暴露这个事实。x86 PC 长期有相对统一的启动和硬件传统；ARM 镜像往往绑定到具体 SoC、UART 地址、中断控制器和 Device Tree。<code>qemu-system-aarch64</code> 因此没有默认 machine，必须写：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-aarch64</span><span> </span><span>-machine</span><span> </span><span>virt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>virt</code> 不是某块真实开发板。它是专门为虚拟化设计的通用平台，提供可配置 CPU、RAM、PL011、GIC、PCI/PCIe、VirtIO 和生成的 Device Tree。若目的是学习 Linux、U-Boot 和驱动框架，它通常比假装某块量产板更干净；若目的是复现真实板卡寄存器和启动 ROM，就必须检查 <code>-machine help</code> 是否真的包含那块板。没有 machine model 时，一份真实固件不会因为加上 <code>-dtb board.dtb</code> 就突然能运行。DTB 描述硬件，不负责实现硬件。</p></section>
<section><h2>我让 QEMU 自己交出 ARM 硬件图<a href="#我让-qemu-自己交出-arm-硬件图"><span>#</span></a></h2><p>QEMU 可以把 <code>virt</code> machine 生成的 DTB 写出来：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-aarch64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>virt,dumpdtb=virt.dtb</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-cpu</span><span> </span><span>cortex-a57</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-m</span><span> </span><span>256M</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-display</span><span> </span><span>none</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>dtc</span><span> </span><span>-I</span><span> </span><span>dtb</span><span> </span><span>-O</span><span> </span><span>dts</span><span> </span><span>-o</span><span> </span><span>virt.dts</span><span> </span><span>virt.dtb</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>QEMU 写出的 <code>virt.dtb</code> 是一个预留到 <code>1,048,576</code> 字节的 blob。反编译后，关键节点包括：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>memory@40000000 {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>reg = &lt;0x00 0x40000000 0x00 0x10000000&gt;;</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>device_type = "memory";</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>pl011@9000000 {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>interrupts = &lt;0x00 0x01 0x04&gt;;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>reg = &lt;0x00 0x9000000 0x00 0x1000&gt;;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>compatible = "arm,pl011", "arm,primecell";</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>virtio_mmio@a000000 {</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>interrupts = &lt;0x00 0x10 0x01&gt;;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>reg = &lt;0x00 0xa000000 0x00 0x200&gt;;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>compatible = "virtio,mmio";</span></div></div><div><div><div>16</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>实验中的 <code>-m 256M</code> 变成从 <code>0x40000000</code> 开始、长度 <code>0x10000000</code> 的 RAM。串口是 <code>0x09000000</code> 的 PL011，<code>chosen.stdout-path</code> 也指向它。DTB 还预留了从 <code>0x0a000000</code> 到 <code>0x0a003e00</code> 的 32 个 VirtIO MMIO slot，每个间隔 <code>0x200</code>。命令行不再是一串咒语，它已经变成内核实际消费的硬件描述。</p><p>因此 ARM64 串口启动参数通常是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>console=ttyAMA0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而上一篇 x86_64 实验使用的是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>console=ttyS0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是 Linux 随机换了名字。两个 machine 分别提供 PL011 和 16550A 风格 UART，对应不同驱动与设备节点。</p></section>
<section><h2>固件启动、磁盘启动和直接内核启动<a href="#固件启动磁盘启动和直接内核启动"><span>#</span></a></h2><p>QEMU 可以从不同层进入 Guest。它们不是同一条路径的不同拼写。</p><section><h3>1. 固件加磁盘<a href="#1-固件加磁盘"><span>#</span></a></h3><p>典型 PC VM：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>q35,accel=kvm</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-m</span><span> </span><span>2G</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-drive</span><span> </span><span>file=system.qcow2,format=qcow2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>QEMU 启动 BIOS 或 UEFI 固件，固件枚举设备并选择启动项，随后进入磁盘里的引导器，再由引导器加载内核。适合安装和运行完整发行版，也最接近通用机器的启动链。</p><p>UEFI 的代码镜像通常只读，变量存储则需要单独的可写副本。不要让多个 VM 直接共享同一个可写变量镜像；Secure Boot key、BootOrder 和固件设置都在里面。</p></section><section><h3>2. <code>-bios</code> 启动固件或裸机镜像<a href="#2--bios-启动固件或裸机镜像"><span>#</span></a></h3><p>对 ARM <code>virt</code> 上的 U-Boot v2026.07，官方文档给出的非安全启动是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>qemu_arm64_defconfig</span></div></div><div><div><div>2</div></div><div><span>make</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>qemu-system-aarch64</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-machine</span><span> </span><span>virt</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-cpu</span><span> </span><span>cortex-a57</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-nographic</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-bios</span><span> </span><span>u-boot.bin</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里 QEMU 把 U-Boot 放进模拟 flash，从地址 <code>0x0</code> 执行，并把生成的 DTB 放在 RAM 开头。若要复现 EL3、TF-A 与 OP-TEE，则启动链和镜像组合会继续增加；不能拿一个非安全 <code>u-boot.bin</code> 命令假装已经验证 secure world。</p></section><section><h3>3. <code>-kernel</code> 直接内核启动<a href="#3--kernel-直接内核启动"><span>#</span></a></h3><p>上一篇文章中，我用下面的命令启动自己构建的 Linux 6.18：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-cpu</span><span> </span><span>max</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-m</span><span> </span><span>256M</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-smp</span><span> </span><span>1</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-nographic</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-no-reboot</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-kernel</span><span> </span><span>arch/x86/boot/bzImage</span><span> </span><span>\</span></div></div><div><div><div>9</div></div><div><span>  </span><span>-initrd</span><span> </span><span>initramfs.cpio</span><span> </span><span>\</span></div></div><div><div><div>10</div></div><div><span>  </span><span>-append</span><span> </span><span>"console=ttyS0 rdinit=/init panic=-1"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它绕过 Guest 磁盘上的 GRUB 和 root filesystem 查找，直接把 kernel、initramfs 和 command line 交给 QEMU 的直接 Linux boot 路径。它很适合内核迭代：不必每次把 <code>bzImage</code> 复制进磁盘分区，也不必调试一个与当前问题无关的 bootloader。</p><p>ARM64 对应的实验骨架是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-aarch64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>virt,accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-cpu</span><span> </span><span>cortex-a57</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-m</span><span> </span><span>512M</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-smp</span><span> </span><span>2</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-nographic</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-no-reboot</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-kernel</span><span> </span><span>arch/arm64/boot/Image</span><span> </span><span>\</span></div></div><div><div><div>9</div></div><div><span>  </span><span>-initrd</span><span> </span><span>initramfs.cpio</span><span> </span><span>\</span></div></div><div><div><div>10</div></div><div><span>  </span><span>-append</span><span> </span><span>"console=ttyAMA0 rdinit=/init panic=-1"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内核必须真的包含 <code>virt</code> 所需的 CPU、GIC、PL011、timer、PCI 或 VirtIO 支持。QEMU 能造出设备，不会替 Linux 打开 Kconfig。于是前两篇文章与这一篇形成了闭环：Kconfig 允许驱动存在，Kbuild 把驱动放进镜像，QEMU machine 决定启动后是否真有匹配的硬件。</p></section></section>
<section><h2><code>-nographic</code> 不等于 <code>-display none</code><a href="#-nographic-不等于--display-none"><span>#</span></a></h2><p>这两个选项看起来接近，行为并不相同：</p>

<table><thead><tr><th>选项</th><th>图形窗口</th><th>串口</th><th>Monitor</th></tr></thead><tbody><tr><td><code>-display none</code></td><td>不创建显示输出</td><td>不自动重定向</td><td>不自动重定向</td></tr><tr><td><code>-nographic</code></td><td>禁用图形输出</td><td>通常把串口接到 stdio</td><td>将 Monitor 与串口复用到 stdio</td></tr><tr><td><code>-serial stdio -monitor none -display none</code></td><td>无</td><td>只把串口接到 stdio</td><td>禁用 HMP</td></tr></tbody></table><p>我在自动实验中偏爱第三种显式写法，因为日志里只有 Guest 串口，不会意外把 <code>(qemu)</code> Monitor prompt 混进去。交互调试时 <code>-nographic</code> 很方便：</p><ul>
<li><code>Ctrl+A H</code> 查看快捷键。</li>
<li><code>Ctrl+A C</code> 在串口与 HMP Monitor 之间切换。</li>
<li><code>Ctrl+A X</code> 退出 QEMU。</li>
</ul><p>这些是 QEMU chardev multiplexer 的转义，不是 Guest 收到的 <code>Ctrl+A</code>。要把真的 <code>Ctrl+A</code> 送给 Guest，需要按两次 <code>Ctrl+A</code>。</p><p><code>console=ttyS0</code> 或 <code>console=ttyAMA0</code> 同样不可省略。QEMU 把虚拟串口接到终端，只解决 Host 端；Linux command line 还要告诉内核把 console 写到哪个 Guest 设备。两边必须在同一根虚拟电线上见面。</p></section>
<section><h2>设备前端与后端必须成对阅读<a href="#设备前端与后端必须成对阅读"><span>#</span></a></h2><p>考虑网络：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-netdev</span><span> </span><span>user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>-device </span><span>virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>virtio-net-pci</code> 是 Guest 枚举到的网卡。</li>
<li><code>netdev=net0</code> 把它连到 ID 为 <code>net0</code> 的 Host backend。</li>
<li><code>user</code> 选择无特权用户态网络栈。</li>
<li><code>hostfwd</code> 把 Host <code>127.0.0.1:2222</code> 转到 Guest TCP 22。</li>
</ul><p>只写 <code>-netdev</code> 没有 Guest 网卡，Guest 无从发送；只写带 <code>netdev=net0</code> 的 <code>-device</code> 而没有同名 backend，QEMU 会在启动时拒绝这个悬空引用。ID 是这张图里的接线标签。</p><p>存储也一样。下面的显式 block graph 经 QEMU 10.2.4 启动验证：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>q35,accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-nodefaults</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-display</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-S</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-blockdev</span><span> </span><span>driver=file,node-name=file0,filename=disk.qcow2</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-blockdev</span><span> </span><span>driver=qcow2,node-name=disk0,file=file0</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-device</span><span> </span><span>virtio-blk-pci,drive=disk0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>数据路径为：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Guest virtio-blk driver</p></span></div><div><span><p>virtio-blk-pci device</p></span></div><div><span><p>qcow2 block node: disk0</p></span></div><div><span><p>file block node: file0</p></span></div><div><span><p>Host disk.qcow2</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Guest virtio-blk driver</p></span></div><div><span><p>virtio-blk-pci device</p></span></div><div><span><p>qcow2 block node: disk0</p></span></div><div><span><p>file block node: file0</p></span></div><div><span><p>Host disk.qcow2</p></span></div>
</div></div></div><p><code>-drive file=...,format=qcow2,if=virtio</code> 是方便的组合式旧入口；<code>-blockdev</code> 加 <code>-device</code> 更啰嗦，却把格式层、文件层和 Guest 设备层分开。要做 overlay、镜像过滤、throttle 或 live block operation 时，显式图更容易审查。</p><p><code>-nodefaults</code> 会移除 machine 的常见默认设备。它适合可重复实验，但不是”删掉全部硬件”；machine 本身不可缺少的控制器和平台对象仍然存在。搭一台最小 VM 时，我会加它，然后只添加实验需要的串口、磁盘和网卡。</p></section>
<section><h2>raw 与 qcow2 不是”大文件与小文件”<a href="#raw-与-qcow2-不是大文件与小文件"><span>#</span></a></h2><p>我创建了两个虚拟容量均为 1 GiB 的空镜像：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-img</span><span> </span><span>create</span><span> </span><span>-f</span><span> </span><span>raw</span><span> </span><span>disk.raw</span><span> </span><span>1G</span></div></div><div><div><div>2</div></div><div><span>qemu-img</span><span> </span><span>create</span><span> </span><span>-f</span><span> </span><span>qcow2</span><span> </span><span>disk.qcow2</span><span> </span><span>1G</span></div></div><div><div><div>3</div></div><div><span>qemu-img</span><span> </span><span>info</span><span> </span><span>--output=json</span><span> </span><span>disk.qcow2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在支持 sparse file 的 Host filesystem 上，结果是：</p>

<table><thead><tr><th>镜像</th><th>Guest 虚拟容量</th><th>Host 文件逻辑长度</th><th>实际分配空间</th></tr></thead><tbody><tr><td>raw</td><td><code>1,073,741,824</code> B</td><td><code>1,073,741,824</code> B</td><td><code>4,096</code> B</td></tr><tr><td>qcow2</td><td><code>1,073,741,824</code> B</td><td><code>196,624</code> B</td><td><code>200,704</code> B</td></tr></tbody></table><p>空 raw 在这里反而只分配 4 KiB，因为它是 sparse file；qcow2 需要约 196 KiB 元数据。于是”qcow2 一定更省空间”在第 0 秒就是错的。两者真正的差别是格式与能力：</p>

<table><thead><tr><th>维度</th><th>raw</th><th>qcow2</th></tr></thead><tbody><tr><td>布局</td><td>扇区偏移接近直接映射</td><td>L1/L2 table、cluster、refcount 等元数据</td></tr><tr><td>稀疏</td><td>依赖 Host filesystem</td><td>格式原生按 cluster 分配</td></tr><tr><td>backing file</td><td>无</td><td>有</td></tr><tr><td>internal snapshot</td><td>无</td><td>有</td></tr><tr><td>压缩/加密能力</td><td>格式本身无</td><td>支持特定模式</td></tr><tr><td>可移植和恢复</td><td>简单</td><td>需要理解 qcow2 元数据</td></tr></tbody></table><p>我又创建了 overlay：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-img</span><span> </span><span>create</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-f</span><span> </span><span>qcow2</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-F</span><span> </span><span>qcow2</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-b</span><span> </span><span>"</span><span>$PWD</span><span>/disk.qcow2"</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>overlay.qcow2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它的虚拟容量仍是 1 GiB，初始实际分配 <code>200,704</code> 字节。读取未覆盖 cluster 时沿 backing chain 去 base，写入则进入 overlay。这个机制非常适合”一份只读基础镜像 + 多个实验分支”。</p><div><div><div></div><div>Backing chain 是数据结构，不是文件命名约定</div></div><div><p>移动、修改或删除 base 可能让 overlay 读到错误数据或直接打不开。创建时显式写 <code>-F</code>，用 <code>qemu-img info --backing-chain</code> 检查链，并在分发前用 <code>qemu-img convert</code> 或 <code>rebase</code> 按计划扁平化。不要在不理解 <code>rebase -u</code> 的情况下用它改元数据指针。</p></div></div><p>QEMU 里至少有 3 种容易都被叫作”快照”的东西：</p><ol>
<li><code>-snapshot</code>：写入临时 overlay，QEMU 退出后通常丢弃。本质上是一次性运行模式。</li>
<li>qcow2 internal snapshot：用 <code>savevm</code>、<code>loadvm</code>、<code>delvm</code> 保存磁盘与 VM state，受设备支持限制。</li>
<li>external snapshot/overlay：新 qcow2 文件以旧镜像为 backing file，链条显式存在，常用于管理系统。</li>
</ol><p>快照不是备份。Guest filesystem 正在写入时抓到的镜像可能只是 crash-consistent；数据库一致性还需要 Guest 协作、freeze 或应用层 protocol。</p></section>
<section><h2>VirtIO 为什么不是”模拟一块更快的真网卡”？<a href="#virtio-为什么不是模拟一块更快的真网卡"><span>#</span></a></h2><p>QEMU 可以模拟 e1000、AHCI、NVMe 等现实设备，Guest 使用现实硬件驱动。这对兼容性和固件测试很重要，但每次寄存器访问与硬件语义都要模拟。</p><p>VirtIO 选择另一条路：它定义一种专为虚拟环境设计的标准设备 contract。Guest 驱动与 device 通过 virtqueue 交换 descriptor，批量描述 buffer，再用 notification 告知对方。它不需要假装自己是某块 20 年前的网卡。</p><div><div><div>QEMU/vhost backendVirtqueue in Guest RAMGuest driverQEMU/vhost backendVirtqueue in Guest RAMGuest driver写 descriptor 与 available ringnotification读取 buffer 并处理 I/O更新 used ringinterrupt 或轮询可见
</div><div>QEMU/vhost backendVirtqueue in Guest RAMGuest driverQEMU/vhost backendVirtqueue in Guest RAMGuest driver写 descriptor 与 available ringnotification读取 buffer 并处理 I/O更新 used ringinterrupt 或轮询可见
</div></div></div><p>这里还要区分 <strong>设备类型</strong> 与 <strong>transport</strong>：</p><ul>
<li><code>virtio-blk</code>、<code>virtio-net</code>、<code>virtio-rng</code> 描述设备功能。</li>
<li><code>virtio-*-pci</code> 经 PCI capability 暴露。</li>
<li><code>virtio-*-device</code> 在某些 machine 上经 VirtIO MMIO 或其他 bus 暴露。</li>
<li>split virtqueue 与 packed virtqueue 描述 ring layout，不是两种磁盘格式。</li>
</ul><p>使用 VirtIO 的前提是 Guest 有对应驱动。initramfs 想从 <code>/dev/vda</code> 挂载 root 时，<code>CONFIG_VIRTIO_BLK</code>、transport 和必要的 PCI 支持必须内建，不能全做成等 root filesystem 挂载后才加载的模块。这又回到了 Kconfig，只是现在我们终于知道那个选项在为哪块虚拟硬件服务。</p><p><code>vhost</code> 还能把部分 VirtIO 数据面移出 QEMU 主循环，例如 <code>vhost-net</code> 借助内核处理网络数据，<code>vhost-user</code> 则通过 Unix socket 连接独立 userspace backend。控制面、feature negotiation、内存共享和安全边界会变复杂，不能只因为名字里有 <code>vhost</code> 就默认打开。</p></section>
<section><h2>网络：先用 user，理解后再上 TAP<a href="#网络先用-user理解后再上-tap"><span>#</span></a></h2><p>最省事的无特权配置是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-netdev</span><span> </span><span>user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>-device </span><span>virtio-net-pci,netdev=net0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>QEMU user-mode network 的默认拓扑通常是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Guest 10.0.2.15</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>-&gt; gateway 10.0.2.2</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>-&gt; DNS 10.0.2.3</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>-&gt; Host/Internet</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Guest 位于 NAT 后面，Host 或局域网不能凭空主动连接它，所以 SSH 需要 <code>hostfwd</code>。绑定 <code>127.0.0.1</code> 比省略地址更保守，避免意外把实验 SSH 暴露到所有 Host 接口。</p><p>ICMP 是一个常见误判点。user backend 对外部 <code>ping</code> 的支持受 Host 与配置限制，<code>ping</code> 失败不能单独证明 TCP/DNS 坏了。先在 Guest 检查地址、route、DNS，再用实际要验证的 TCP/UDP 流量测试。</p><p>TAP 则在 Host 创建二层接口：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-netdev</span><span> </span><span>tap,id=net0,ifname=tap0,script=no,downscript=no</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>-device </span><span>virtio-net-pci,netdev=net0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它适合桥接到 Host bridge、构建多 VM 二层网络或做抓包实验，但创建和配置 TAP/bridge 通常需要额外权限。网络连不通时应分别检查：</p><ol>
<li>Guest NIC 驱动是否绑定。</li>
<li>Guest link、地址和 route 是否正确。</li>
<li>QEMU device 是否连到正确 <code>netdev</code> ID。</li>
<li>TAP 是否 UP 并加入预期 bridge。</li>
<li>Host forwarding、防火墙和 NAT 是否允许流量。</li>
</ol><p><code>passt</code> 是另一种无特权 backend。它作为独立 daemon 工作，通常比内置 user-mode stack 提供更完整的 IPv6、性能和隔离特性。选择 backend 是网络拓扑与安全决策，不是 VirtIO 驱动决策。</p></section>
<section><h2>QEMU user mode：只模拟一个进程<a href="#qemu-user-mode只模拟一个进程"><span>#</span></a></h2><p>为了确认 <code>qemu-aarch64</code> 和 system emulator 的边界，我写了一个不用 libc 的 AArch64 程序：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.global _start</span></div></div><div><div><div>2</div></div><div><span>.text</span></div></div><div><div><div>3</div></div><div><span>_start:</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>mov</span><span> x0, #</span><span>1</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>adr x1, message</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>mov</span><span> x2, #message_end - message</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>mov</span><span> x8, #</span><span>64</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>svc #</span><span>0</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>mov</span><span> x0, #</span><span>7</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>mov</span><span> x8, #</span><span>93</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>svc #</span><span>0</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>message:</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>.ascii "hello from AArch64 user mode\n"</span></div></div><div><div><div>16</div></div><div><span>message_end:</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>交叉链接出一个 <code>992</code> 字节静态 ELF：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>aarch64-linux-gnu-gcc</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-nostdlib</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-static</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-Wl,--build-id=none</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-o</span><span> </span><span>hello-aarch64</span><span> </span><span>hello-aarch64.S</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>file</span><span> </span><span>hello-aarch64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>file</code> 确认它是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ELF 64-bit LSB executable, ARM aarch64, statically linked</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>x86_64 Host 不能原生执行这份指令，但 user emulator 可以：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-aarch64</span><span> </span><span>-strace</span><span> </span><span>./hello-aarch64</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我实际看到：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>write(1,0x400098,29)hello from AArch64 user mode</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>= 29</span></div></div><div><div><div>3</div></div><div><span>exit(7)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>进程退出码正是 7。这里没有 AArch64 BIOS、GIC、PL011、磁盘或第二个 Linux kernel。QEMU 翻译 AArch64 用户指令，拦截 <code>svc</code>，把 Guest ABI 的 <code>write</code> 和 <code>exit</code> 参数转换后交给 Host Linux kernel。</p><p>动态链接程序还需要目标架构的 loader 与 shared libraries：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-aarch64</span><span> </span><span>-L</span><span> </span><span>/path/to/aarch64-sysroot</span><span> </span><span>./app</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-L</code> 指向 Guest ABI sysroot，不是随便一个包含 <code>.so</code> 的目录。混用错误架构、错误 dynamic linker 路径或不兼容 glibc 时，问题发生在 ABI 与文件系统视图，不在虚拟网卡，因为这里根本没有网卡。</p><p>user mode 适合交叉编译后的命令行程序测试、CI 和编译器工作；涉及 kernel driver、boot flow、真实 MMIO 或完整网络 namespace 语义时，应回到 system emulation。</p></section>
<section><h2>HMP、QMP 和 Guest Agent 是 3 个平面<a href="#hmpqmp-和-guest-agent-是-3-个平面"><span>#</span></a></h2><section><h3>HMP：给人使用的 Monitor<a href="#hmp给人使用的-monitor"><span>#</span></a></h3><p>HMP 是 <code>(qemu)</code> prompt 后面的交互命令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(qemu) info status</span></div></div><div><div><div>2</div></div><div><span>(qemu) info registers</span></div></div><div><div><div>3</div></div><div><span>(qemu) info pci</span></div></div><div><div><div>4</div></div><div><span>(qemu) info block</span></div></div><div><div><div>5</div></div><div><span>(qemu) stop</span></div></div><div><div><div>6</div></div><div><span>(qemu) cont</span></div></div><div><div><div>7</div></div><div><span>(qemu) quit</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它适合临时观察。输出面向人，会随版本调整，不应让生产脚本靠正则解析。</p></section><section><h3>QMP：给程序使用的控制 API<a href="#qmp给程序使用的控制-api"><span>#</span></a></h3><p>QMP 使用以 CRLF 分隔的 JSON object。连接建立后，server 先发送 greeting；client 必须完成 <code>qmp_capabilities</code> negotiation，随后才能发普通命令。下面是我实际运行的最小 Unix socket client 的核心：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> json</span></div></div><div><div><div>2</div></div><div><span>import</span><span> socket</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>sock </span><span>=</span><span> socket.</span><span>socket</span><span>(socket.</span><span>AF_UNIX</span><span>, socket.</span><span>SOCK_STREAM</span><span>)</span></span></div></div><div><div><div>5</div></div><div><span><span>sock.</span><span>connect</span><span>(</span></span><span>"/tmp/qmp.sock"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>stream </span><span>=</span><span> sock.</span><span>makefile</span><span>(</span></span><span>"rwb"</span><span>, </span><span>buffering</span><span>=</span><span>0</span><span>)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>print</span><span><span>(json.</span><span>loads</span><span>(stream.</span><span>readline</span><span>()))  </span></span><span># greeting</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>for</span><span> request </span><span>in</span><span> [</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>{</span><span>"execute"</span><span>: </span><span>"qmp_capabilities"</span><span>, </span><span>"id"</span><span>: </span><span>1</span><span>},</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>{</span><span>"execute"</span><span>: </span><span>"query-status"</span><span>, </span><span>"id"</span><span>: </span><span>2</span><span>},</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>{</span><span>"execute"</span><span>: </span><span>"query-kvm"</span><span>, </span><span>"id"</span><span>: </span><span>3</span><span>},</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>{</span><span>"execute"</span><span>: </span><span>"query-cpus-fast"</span><span>, </span><span>"id"</span><span>: </span><span>4</span><span>},</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>{</span><span>"execute"</span><span>: </span><span>"quit"</span><span>, </span><span>"id"</span><span>: </span><span>5</span><span>},</span></div></div><div><div><div>16</div></div><div><span>]:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>stream.</span><span>write</span><span>((json.</span><span>dumps</span><span>(request) </span><span>+</span><span> </span><span>"</span><span>\r\n</span><span>"</span><span><span>).</span><span>encode</span><span>())</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>while</span><span> </span><span>True</span><span>:</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>response </span><span>=</span><span> json.</span><span>loads</span><span>(stream.</span><span>readline</span><span>())</span></div></div><div><div><div>20</div></div><div><span>        </span><span>if</span><span><span> response.</span><span>get</span><span>(</span></span><span>"id"</span><span><span>) </span><span>==</span><span> request[</span></span><span>"id"</span><span>]:</span></div></div><div><div><div>21</div></div><div><span>            </span><span>print</span><span>(response)</span></div></div><div><div><div>22</div></div><div><span>            </span><span>break</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>QEMU 由以下参数启动并停在第一条 Guest 指令之前：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>pc,accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-m</span><span> </span><span>64M</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-S</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-display</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-serial</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-monitor</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-qmp</span><span> </span><span>unix:/tmp/qmp.sock,server=on,wait=off</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关键响应是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span><span>"return"</span><span>:{</span><span>"status"</span><span>:</span><span>"prelaunch"</span><span>,</span><span>"running"</span><span>:</span><span>false</span><span>},</span><span>"id"</span><span>:</span><span>2</span><span>}</span></div></div><div><div><div>2</div></div><div><span>{</span><span>"return"</span><span>:{</span><span>"enabled"</span><span>:</span><span>false</span><span>,</span><span>"present"</span><span>:</span><span>true</span><span>},</span><span>"id"</span><span>:</span><span>3</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>present=true</code> 表示该 QEMU binary 与环境存在 KVM 能力，<code>enabled=false</code> 则准确反映这台 VM 明确选择了 TCG。<code>-S</code> 让状态停在 <code>prelaunch</code>。我喜欢这种接口：没有从彩色日志猜心情，只有结构化状态。</p><p>QMP response 的 <code>id</code> 对应 request；event 则可能异步到来。客户端不能假设读到的下一行一定是刚才命令的 response，必须按 <code>id</code> 配对，并单独处理 <code>event</code>。生产代码还要处理 partial read、断连、schema/version 差异和 error object。</p></section><section><h3>Guest Agent：进入 Guest OS 的通道<a href="#guest-agent进入-guest-os-的通道"><span>#</span></a></h3><p>QEMU Guest Agent 是 Guest 内运行的进程，常通过 virtio-serial 与 Host 管理层通信。它能报告 Guest IP、freeze filesystem、关机或执行受支持操作。QMP 能问 QEMU “vCPU 是否暂停”，却不能凭空知道 Guest 里的 ext4 是否 freeze；QGA 正是跨过这条边界。</p><p>不要把 QMP 或 GDB socket 暴露到不可信网络。它们通常没有应用层认证；能控制 VM 或调试 Guest 的接口本质上就是高权限接口。Unix socket 加严格文件权限是本地实验的稳妥默认值。</p></section></section>
<section><h2>用 GDB 在第一条指令前拦住机器<a href="#用-gdb-在第一条指令前拦住机器"><span>#</span></a></h2><p>QEMU 内置 gdbstub。最短入口是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-S</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-s</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-kernel</span><span> </span><span>arch/x86/boot/bzImage</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-append</span><span> </span><span>"console=ttyS0"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>-S</code>：创建 machine，但不启动 vCPU。</li>
<li><code>-s</code>：等价于 <code>-gdb tcp::1234</code>。</li>
</ul><p>另一个终端连接：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>gdb</span><span> </span><span>vmlinux</span></div></div><div><div><div>2</div></div><div><span>(</span><span>gdb</span><span>) </span><span>target</span><span> </span><span>remote</span><span> </span><span>:1234</span></div></div><div><div><div>3</div></div><div><span>(</span><span>gdb</span><span>) </span><span>hbreak</span><span> </span><span>start_kernel</span></div></div><div><div><div>4</div></div><div><span>(</span><span>gdb</span><span>) </span><span>continue</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调内核时应给 GDB <code>vmlinux</code>，不是压缩后的 <code>bzImage</code>。<code>vmlinux</code> 保留 ELF section 与 symbol；QEMU 启动的是可引导压缩镜像，两者承担不同角色。若 KASLR 让 runtime address 与 symbol address 不一致，可以在实验 command line 加 <code>nokaslr</code>，或者正确处理 relocation，而不是把断点下在希望它出现的地址。</p><p>调 16-bit 启动扇区时，连接后可用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>set architecture i8086</span></div></div><div><div><div>2</div></div><div><span>x/10i $cs*16+$eip</span></div></div><div><div><div>3</div></div><div><span>info registers</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>TCG 下 QEMU 可提供大量 hardware breakpoint/watchpoint；KVM 的能力取决于 accelerator 实现与硬件。user-mode TCG 还不支持 watchpoint。调试 multicore machine 时，每个 vCPU 会作为 GDB thread 暴露，多 cluster machine 还可能需要 <code>target extended-remote</code> 与多个 inferior。</p><p>为避免固定 TCP port 和误暴露，我更喜欢 Unix socket：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-chardev</span><span> </span><span>socket,path=/tmp/gdb.sock,server=on,wait=off,id=gdb0</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>-gdb </span><span>chardev:gdb0</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>-S</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>GDB 9.0 及以上可以：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>target remote /tmp/gdb.sock</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>日志、trace 和可重复执行<a href="#日志trace-和可重复执行"><span>#</span></a></h2><p>串口日志、QEMU debug log 与 trace event 是 3 类不同证据：</p><ul>
<li><code>-serial file:guest.log</code> 记录 Guest 经 UART 输出的内容。</li>
<li><code>-d ... -D qemu.log</code> 记录 QEMU 的执行或设备 debug 信息。</li>
<li><code>-trace ...</code> 订阅代码中定义的 trace event。</li>
</ul><p>先查询支持项：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-d</span><span> </span><span>help</span></div></div><div><div><div>2</div></div><div><span>qemu-system-x86_64</span><span> </span><span>-trace</span><span> </span><span>help</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调早期启动常见写法：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-d</span><span> </span><span>guest_errors,int</span><span> </span><span>-D</span><span> </span><span>qemu.log</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-d in_asm</code> 可以看到翻译的 Guest instruction block，但完整 Linux boot 会快速生成巨大日志。先缩小复现、使用 <code>-S</code> 和 breakpoint，或只在目标阶段打开日志。把几 GiB <code>in_asm</code> 当作理解系统的方法，很像为了找一根针而给整个草垛做逐原子 CT，技术上努力，方向上稍微有点可疑。</p><p>需要可重放的 TCG 实验时，QEMU 还有 instruction counting 与 record/replay 机制，但它们会约束设备、时钟和加速器。<code>-icount</code> 的虚拟时间不等于真实 wall-clock 性能；不要把确定性工具产生的时间数字拿去做 VM 性能榜。</p><p>更深入的动态分析可以使用 TCG plugin，按 instruction、basic block 或 memory access 订阅事件。plugin 能观察非常细的执行，却不应被理解成可以随意修改 Guest state 的通用 hook。先用 QMP、GDB 和现成 trace event，把 plugin 留给确实需要指令级统计的问题。</p></section>
<section><h2>时钟、SMP 和”偶尔失败”<a href="#时钟smp-和偶尔失败"><span>#</span></a></h2><p>VM 里至少同时存在几种时间：Host wall clock、QEMU virtual clock、Guest wall clock、Guest monotonic clock，以及设备 timer。暂停 VM、Host 休眠、过载或迁移都会让它们以不同方式变化。</p><p><code>-smp</code> 也不仅是一个数量：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-smp</span><span> </span><span>cpus=4,sockets=1,cores=2,threads=</span><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Guest 会看见拓扑，调度器与软件许可可能据此作决定。若只写 <code>-smp 4</code>，QEMU 会根据 machine 默认规则补齐 topology；需要可复现实验时应把真正相关的拓扑写全。</p><p>并发 bug 在 TCG 单线程、TCG 多线程和 KVM 下可能表现不同。TCG 没复现不等于真实硬件没有 bug，KVM 复现也不等于 QEMU 设备模型必然有错。先确定问题落在 Guest 软件、虚拟 CPU、设备 contract 还是 Host backend，再更换加速器做交叉验证。</p></section>
<section><h2>一套可重复的内核实验命令<a href="#一套可重复的内核实验命令"><span>#</span></a></h2><p>我现在不会从一条不断追加参数的历史命令开始，而是明确命名每一层：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-name</span><span> </span><span>pocket-kernel</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-machine</span><span> </span><span>pc-q35-10.2,accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-cpu</span><span> </span><span>max</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-m</span><span> </span><span>256M</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-smp</span><span> </span><span>cpus=1,sockets=1,cores=1,threads=</span><span>1</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-nodefaults</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-no-reboot</span><span> </span><span>\</span></div></div><div><div><div>9</div></div><div><span>  </span><span>-display</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>10</div></div><div><span>  </span><span>-chardev</span><span> </span><span>stdio,id=serial0</span><span> </span><span>\</span></div></div><div><div><div>11</div></div><div><span>  </span><span>-device</span><span> </span><span>isa-serial,chardev=serial0</span><span> </span><span>\</span></div></div><div><div><div>12</div></div><div><span>  </span><span>-monitor</span><span> </span><span>none</span><span> </span><span>\</span></div></div><div><div><div>13</div></div><div><span>  </span><span>-qmp</span><span> </span><span>unix:/tmp/pocket-kernel.qmp,server=on,wait=off</span><span> </span><span>\</span></div></div><div><div><div>14</div></div><div><span>  </span><span>-chardev</span><span> </span><span>socket,path=/tmp/pocket-kernel.gdb,server=on,wait=off,id=gdb0</span><span> </span><span>\</span></div></div><div><div><div>15</div></div><div><span>  </span><span>-gdb</span><span> </span><span>chardev:gdb0</span><span> </span><span>\</span></div></div><div><div><div>16</div></div><div><span>  </span><span>-kernel</span><span> </span><span>arch/x86/boot/bzImage</span><span> </span><span>\</span></div></div><div><div><div>17</div></div><div><span>  </span><span>-initrd</span><span> </span><span>initramfs.cpio</span><span> </span><span>\</span></div></div><div><div><div>18</div></div><div><span>  </span><span>-append</span><span> </span><span>"console=ttyS0 rdinit=/init panic=-1 nokaslr"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>-nodefaults</code> 移除了常见默认外设，所以我显式创建 <code>isa-serial</code>，再通过 ID 为 <code>serial0</code> 的 chardev 接到 stdio。最小化的目标是让依赖可见，不是参加参数最短比赛。</p><p>用于网络和持久 rootfs 时再加：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-blockdev</span><span> </span><span>driver=file,node-name=root-file,filename=root.qcow2</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>-blockdev </span><span>driver=qcow2,node-name=root,read-only=off,file=root-file</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>-device </span><span>virtio-blk-pci,drive=root</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>-netdev </span><span>user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>-device </span><span>virtio-net-pci,netdev=net0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>对应内核至少要检查：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>CONFIG_SERIAL_8250=y</span></div></div><div><div><div>2</div></div><div><span>CONFIG_SERIAL_8250_CONSOLE=y</span></div></div><div><div><div>3</div></div><div><span>CONFIG_VIRTIO=y</span></div></div><div><div><div>4</div></div><div><span>CONFIG_VIRTIO_PCI=y</span></div></div><div><div><div>5</div></div><div><span>CONFIG_VIRTIO_BLK=y</span></div></div><div><div><div>6</div></div><div><span>CONFIG_VIRTIO_NET=y</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>具体符号仍以目标 Linux 版本和 machine 为准。root 设备还要在 <code>-append</code> 中给出正确 <code>root=/dev/vda...</code>、filesystem driver 和等待策略。看到 <code>VFS: Unable to mount root fs</code> 时，我会先核对 4 件事：Guest 设备类型、transport driver、block/filesystem driver 是否内建、root path 是否与实际分区一致。</p></section>
<section><h2>常见失败怎样沿层次定位？<a href="#常见失败怎样沿层次定位"><span>#</span></a></h2><section><h3>QEMU 启动前就报 property 或 bus 错误<a href="#qemu-启动前就报-property-或-bus-错误"><span>#</span></a></h3><p>这是 Host 侧 machine graph 没建起来。检查 <code>-device TYPE,help</code>、ID 拼写、bus 名、machine 是否提供该 bus，以及当前 QEMU build 是否包含设备。Guest kernel 尚未执行，不要先改 Kconfig。</p></section><section><h3>屏幕全黑或终端没有日志<a href="#屏幕全黑或终端没有日志"><span>#</span></a></h3><p>分别检查显示与 console：</p><ol>
<li><code>-display none</code> 是否把唯一图形输出关闭。</li>
<li><code>-serial stdio</code> 或 <code>-nographic</code> 是否连接正确 UART。</li>
<li>kernel command line 是否使用 <code>ttyS0</code>、<code>ttyAMA0</code> 等正确设备。</li>
<li>UART driver 和 console support 是否内建。</li>
<li>使用 <code>-S</code> 时是否忘了从 GDB 或 QMP <code>cont</code>。</li>
</ol></section><section><h3><code>No bootable device</code><a href="#no-bootable-device"><span>#</span></a></h3><p>固件没有找到可启动介质。检查镜像内容、boot signature/EFI executable、<code>bootindex</code>、磁盘 interface 与 firmware 能否驱动它。一个包含 ext4 的文件不自动等于含有 bootloader 的 boot disk。</p></section><section><h3>内核看不见磁盘或网卡<a href="#内核看不见磁盘或网卡"><span>#</span></a></h3><p>先用 QMP/HMP 确认 device 真的存在并连接 backend，再看 Guest <code>dmesg</code>、PCI/Device Tree 枚举和驱动配置。VirtIO PCI 与 VirtIO MMIO 需要不同 transport；设备存在但没有 driver 与设备根本没创建，会得到相似的”看不见”，修复层却完全不同。</p></section><section><h3><code>Could not access KVM kernel module</code><a href="#could-not-access-kvm-kernel-module"><span>#</span></a></h3><p>检查 <code>/dev/kvm</code>、Host CPU virtualization、内核模块、权限和容器 device passthrough。需要兼容 fallback 时可写：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-accel</span><span> </span><span>kvm:tcg</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>但性能实验不应静默 fallback。用 QMP <code>query-kvm</code> 记录 <code>enabled</code>，否则以为自己测了 KVM，实际跑了一夜 TCG，也算一种很昂贵的类型错误。</p></section><section><h3>SSH 端口转发连不上<a href="#ssh-端口转发连不上"><span>#</span></a></h3><p>检查 Host port 是否被占用、<code>hostfwd</code> 的绑定地址、Guest sshd 是否监听、Guest firewall 和地址配置。QEMU user networking 默认不接受任意外部入站，端口转发是明确开的洞。</p></section><section><h3>镜像提示 format probing 警告<a href="#镜像提示-format-probing-警告"><span>#</span></a></h3><p>显式写 <code>format=raw</code> 或 <code>format=qcow2</code>。格式自动探测不仅让行为含糊，对 raw 镜像还可能因安全限制阻止对第 0 block 的写入。命令行里知道格式，就把它写出来。</p></section></section>
<section><h2>安全边界不能靠”它只是虚拟机”<a href="#安全边界不能靠它只是虚拟机"><span>#</span></a></h2><p>我会保留这些默认纪律：</p><ul>
<li>用普通用户运行 QEMU，不给整个进程 root。</li>
<li>不可信 Guest 只配置需要的设备与 backend。</li>
<li>QMP、HMP、GDB 和 migration socket 使用 Unix socket 或绑定 loopback，并限制权限。</li>
<li>不随意添加 Host 目录共享、USB passthrough、VFIO、<code>/dev/mem</code> 或可写物理块设备。</li>
<li>user networking 的 <code>hostfwd</code> 显式绑定 <code>127.0.0.1</code>。</li>
<li>对 base image 只读，实验写入 overlay。</li>
<li>semihosting 只给可信裸机代码使用，因为它可以绕过 Guest/Host 隔离访问 Host 资源。</li>
<li>记录 QEMU version、machine version、CPU model、firmware、镜像 hash 和完整参数。</li>
</ul><p>VFIO passthrough 能减少设备模拟并接近原生性能，但它把 IOMMU group、DMA 隔离、reset semantics 和 Host driver ownership 带进系统。它不是”再加一个 <code>-device</code>”级别的学习步骤。我会先把纯模拟设备与 VirtIO 路径弄清，再碰 passthrough。</p></section>
<section><h2>我最终保留的心智模型<a href="#我最终保留的心智模型"><span>#</span></a></h2><p>现在我把 QEMU 看成 5 张互相连接的图：</p><ol>
<li><strong>执行图</strong>：Guest ISA 经 TCG 或 KVM 前进。</li>
<li><strong>machine 图</strong>：CPU、RAM、bus、中断控制器和设备占据确定位置。</li>
<li><strong>I/O 图</strong>：Guest device 经 block、network、character backend 连接 Host。</li>
<li><strong>启动图</strong>：reset 后进入 firmware、bootloader 或 direct kernel boot。</li>
<li><strong>控制图</strong>：人用 HMP，程序用 QMP，调试器用 gdbstub，Guest Agent 报告 OS 内部状态。</li>
</ol><p>512 字节启动扇区只用了 1 个 vCPU、1 个 UART、1 个软盘和 1 个测试退出设备，却已经证明了这 5 张图如何接起来。随后 992 字节 AArch64 ELF 又拿掉整台 machine，只留下指令翻译与 syscall translation。最后，<code>virt</code> 的 1 MiB DTB 把 ARM machine 的 RAM、PL011、GIC、PCI 和 32 个 VirtIO MMIO slot 全部摊在文本里。QEMU 没有变简单，但黑盒变成了几张可以逐边检查的图。</p><p>接下来我最想做的是把上一篇 <code>pocket_sensor</code> 从一个靠 <code>insmod</code> 自说自话的模块，改成真正的 platform driver：在 ARM <code>virt</code> 上增加一个小型 MMIO 设备模型，让 QEMU 生成对应 Device Tree 节点，再观察 Linux 从 <code>compatible</code> 匹配到 <code>probe()</code>、中断与 <code>remove()</code>。到那一步，Kconfig、Kbuild、Device Tree、driver core 和 QEMU device model 会在同一条可执行链上相遇。</p><p>很好。我终于不再把 <code>-nographic</code> 当作启动虚拟机的咒语了。它只是把一根虚拟串口线接到了我的终端，剩下 455 个设备还在后面安静地等着制造新问题。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://gitlab.com/qemu-project/qemu/-/blob/v10.2.4/docs/system/introduction.rst" target="_blank">QEMU 10.2.4: System Emulation Introduction</a></li>
<li><a href="https://www.qemu.org/docs/master/system/invocation.html" target="_blank">QEMU: System Invocation</a></li>
<li><a href="https://www.qemu.org/docs/master/system/images.html" target="_blank">QEMU: Disk Images</a></li>
<li><a href="https://www.qemu.org/docs/master/system/devices/net.html" target="_blank">QEMU: Network Emulation</a></li>
<li><a href="https://www.qemu.org/docs/master/user/main.html" target="_blank">QEMU: User Space Emulator</a></li>
<li><a href="https://www.qemu.org/docs/master/system/gdb.html" target="_blank">QEMU: GDB Usage</a></li>
<li><a href="https://www.qemu.org/docs/master/interop/qmp-spec.html" target="_blank">QEMU Machine Protocol Specification</a></li>
<li><a href="https://www.qemu.org/docs/master/interop/qemu-qmp-ref.html" target="_blank">QEMU Machine Protocol Reference</a></li>
<li><a href="https://www.qemu.org/docs/master/system/target-arm.html" target="_blank">QEMU: Arm System Emulator</a></li>
<li><a href="https://gitlab.com/qemu-project/qemu/-/blob/v10.2.4/hw/misc/debugexit.c" target="_blank">QEMU 10.2.4: isa-debug-exit implementation</a></li>
<li><a href="https://docs.oasis-open.org/virtio/virtio/v1.3/virtio-v1.3.html" target="_blank">OASIS VirtIO 1.3 Specification</a></li>
<li><a href="https://docs.u-boot.org/en/v2026.07/board/emulation/qemu-arm.html" target="_blank">U-Boot v2026.07: QEMU ARM</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/understanding-kbuild/</id>
      <title type="text">我追了一遍 Kbuild: 从一行 obj-m 到可以加载的 Linux 内核模块</title>
      <published>2026-09-04T12:00:00.000Z</published>
      <updated>2026-09-04T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/understanding-kbuild/"/>
      <summary type="text">我用 Linux 6.18 写了一个双文件外部模块，追踪 obj-m、scripts/Makefile.build、.cmd、modpost 和最终 .ko，再把它装进 QEMU 并迁移回内核树验证 n、m、y。</summary>
      <content type="html"><![CDATA[<p><a href="/posts/understanding-kconfig/">上一篇文章</a>停在一行很小的 Makefile 上：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_POCKET_SENSOR</span><span>)</span><span><span> </span><span>+=</span><span> pocket_sensor.o</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>Kconfig 已经算出 <code>CONFIG_POCKET_SENSOR=m</code>，但这一行怎么变成一个能让内核执行的 <code>.ko</code>？我从 Linux v6.18 拉出这根线，写了一个双文件模块，编译它，拆开中间产物，再把它塞进 QEMU 里的 Linux 6.18。最后我真的看到了 <code>module_init()</code> 和 <code>module_exit()</code> 各执行一次。很好，配电柜之后，我们终于开始追电线了。</p>
<div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Kconfig<br /><br />CONFIG_POCKET_SENSOR=m</p></span></div><div><span><p>auto.conf 与 obj-m<br /><br />选择模块路径</p></span></div><div><span><p>scripts/Makefile.build<br /><br />编译 core.o 和 stats.o</p></span></div><div><span><p>ld -r<br /><br />合成 pocket_sensor.o</p></span></div><div><span><p>modpost 与 modfinal<br /><br />生成 pocket_sensor.ko</p></span></div><div><span><p>finit_module()<br /><br />执行 pocket_sensor_init()</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Kconfig<br /><br />CONFIG_POCKET_SENSOR=m</p></span></div><div><span><p>auto.conf 与 obj-m<br /><br />选择模块路径</p></span></div><div><span><p>scripts/Makefile.build<br /><br />编译 core.o 和 stats.o</p></span></div><div><span><p>ld -r<br /><br />合成 pocket_sensor.o</p></span></div><div><span><p>modpost 与 modfinal<br /><br />生成 pocket_sensor.ko</p></span></div><div><span><p>finit_module()<br /><br />执行 pocket_sensor_init()</p></span></div>
</div></div></div>
<p>本文只讨论 Linux Kbuild。U-Boot 也继承了 Kbuild 的许多写法，但它没有 Linux 这条可加载模块链，最终镜像路径也不同。这里不把两种系统勉强揉成一种。</p>
<section><h2>先把实验钉在具体版本上<a href="#先把实验钉在具体版本上"><span>#</span></a></h2><p>我的实验环境如下：</p>

<table><thead><tr><th>项目</th><th>实验值</th></tr></thead><tbody><tr><td>Linux 源码</td><td><code>v6.18</code></td></tr><tr><td>架构</td><td><code>x86_64</code></td></tr><tr><td>GNU Make</td><td><code>4.4.1</code></td></tr><tr><td>GCC</td><td><code>15.2.0</code></td></tr><tr><td>GNU Binutils</td><td><code>2.46</code></td></tr><tr><td>kmod</td><td><code>31</code></td></tr><tr><td>QEMU</td><td><code>10.2.4</code></td></tr><tr><td>BusyBox</td><td><code>1.37.0</code></td></tr></tbody></table><p>内核和外部模块统一使用 GCC 15.2.0 与 GNU Binutils 2.46。正式构建也应复用目标内核的工具链、配置和构建目录，因为 Linux 不向外部模块提供稳定的内核内 ABI。</p><p>我用下面的源码固定分析基准：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>--depth</span><span> </span><span>1</span><span> </span><span>--branch</span><span> </span><span>v6.18</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>https://github.com/torvalds/linux.git</span><span> </span><span>linux-6.18</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Linux 6.18 是这里的机制快照。后续版本可能移动规则、拆分生成文件或改变命令行，所以文中的源码链接都锁在 <code>v6.18</code> 标签。</p></section>
<section><h2>先做出一个真的 <code>.ko</code><a href="#先做出一个真的-ko"><span>#</span></a></h2>

<table><thead><tr><th>时机</th><th>可观察行为</th></tr></thead><tbody><tr><td>加载</td><td>读取 <code>label</code> 和 <code>samples</code> 两个模块参数</td></tr><tr><td>运行</td><td><code>stats.c</code> 分配并初始化一小段样本数组</td></tr><tr><td>卸载</td><td>释放数组并打印样本数</td></tr></tbody></table><p>目录只有 5 个输入文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pocket-sensor/</span></div></div><div><div><div>2</div></div><div><span>├── Kbuild</span></div></div><div><div><div>3</div></div><div><span>├── Makefile</span></div></div><div><div><div>4</div></div><div><span>├── core.c</span></div></div><div><div><div>5</div></div><div><span>├── stats.c</span></div></div><div><div><div>6</div></div><div><span>└── pocket_sensor.h</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>core.c</code> 负责模块生命周期和参数：</p><div><div><div><figure><figcaption><span>GPL-2.0</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;linux/init.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;linux/module.h&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>"pocket_sensor.h"</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>static</span><span> </span><span>char</span><span> </span><span>*</span><span>label </span><span>=</span><span> </span><span>"desk"</span><span>;</span></div></div><div><div><div>7</div></div><div><span>module_param</span><span><span>(</span><span>label</span><span>,</span><span> charp</span><span>,</span><span> </span></span><span><span>0</span><span>444</span></span><span>);</span></div></div><div><div><div>8</div></div><div><span>MODULE_PARM_DESC</span><span><span>(</span><span>label</span><span>,</span><span> </span></span><span>"Sensor label"</span><span>);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>static</span><span> </span><span>unsigned</span><span> </span><span>int</span><span> samples </span><span>=</span><span> </span><span>4</span><span>;</span></div></div><div><div><div>11</div></div><div><span>module_param</span><span><span>(</span><span>samples</span><span>,</span><span> </span></span><span>uint</span><span><span>,</span><span> </span></span><span><span>0</span><span>444</span></span><span>);</span></div></div><div><div><div>12</div></div><div><span>MODULE_PARM_DESC</span><span><span>(</span><span>samples</span><span>,</span><span> </span></span><span>"Number of synthetic samples"</span><span>);</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>static</span><span> </span><span>int</span><span> __init </span><span>pocket_sensor_init</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>15</div></div><div><span>{</span></div></div><div><div><div>16</div></div><div><span>  </span><span>int</span><span> ret;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>ret </span><span>=</span><span> </span><span>pocket_stats_start</span><span>(samples);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>if</span><span> (ret)</span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span> ret;</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>  </span><span>pr_info</span><span>(</span><span>"pocket_sensor: loaded label=</span><span>%s</span><span> samples=</span><span>%u</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>label, samples);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>static</span><span> </span><span>void</span><span> __exit </span><span>pocket_sensor_exit</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>28</div></div><div><span>{</span></div></div><div><div><div>29</div></div><div><span>  </span><span>pr_info</span><span>(</span><span>"pocket_sensor: unloaded samples=</span><span>%u</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>30</div></div><div><span>    </span><span>pocket_stats_stop</span><span>());</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>module_init</span><span><span>(</span><span>pocket_sensor_init</span><span>);</span></span></div></div><div><div><div>34</div></div><div><span>module_exit</span><span><span>(</span><span>pocket_sensor_exit</span><span>);</span></span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>MODULE_AUTHOR</span><span>(</span><span>"bfmhno3"</span><span>);</span></div></div><div><div><div>37</div></div><div><span>MODULE_DESCRIPTION</span><span>(</span><span>"Small Kbuild experiment"</span><span>);</span></div></div><div><div><div>38</div></div><div><span>MODULE_LICENSE</span><span>(</span><span>"GPL"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>stats.c</code> 提供第二个编译单元：</p><div><div><div><figure><figcaption><span>GPL-2.0</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;linux/errno.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;linux/slab.h&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;linux/types.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>"pocket_sensor.h"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>static</span><span> u32 </span><span>*</span><span>values;</span></div></div><div><div><div>8</div></div><div><span>static</span><span> </span><span>unsigned</span><span> </span><span>int</span><span> value_count;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>int</span><span> </span><span>pocket_stats_start</span><span>(</span><span>unsigned</span><span> </span><span>int</span><span><span> </span><span>count</span><span>)</span></span></div></div><div><div><div>11</div></div><div><span>{</span></div></div><div><div><div>12</div></div><div><span>  </span><span>unsigned</span><span> </span><span>int</span><span> i;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>count </span><span>||</span><span> count </span><span>&gt;</span><span> </span><span>4096</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>EINVAL;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>values </span><span>=</span><span> </span><span>kcalloc</span><span>(count, </span><span>sizeof</span><span>(</span><span>*</span><span>values), GFP_KERNEL);</span></div></div><div><div><div>18</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>values)</span></div></div><div><div><div>19</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>ENOMEM;</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>  </span><span>for</span><span> (i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> count; i</span><span>++</span><span>)</span></div></div><div><div><div>22</div></div><div><span>    </span><span>values</span><span>[i] </span><span>=</span><span> i;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>value_count </span><span>=</span><span> count;</span></div></div><div><div><div>24</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>unsigned</span><span> </span><span>int</span><span> </span><span>pocket_stats_stop</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>28</div></div><div><span>{</span></div></div><div><div><div>29</div></div><div><span>  </span><span>unsigned</span><span> </span><span>int</span><span> count </span><span>=</span><span> value_count;</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>  </span><span>kfree</span><span>(values);</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>values </span><span>=</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>value_count </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>34</div></div><div><span>  </span><span>return</span><span> count;</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>头文件只是两个编译单元之间的接口：</p><div><figure><figcaption><span>GPL-2.0</span></figcaption><pre><code><div><div><div>1</div></div><div><span>#ifndef</span><span> </span><span>POCKET_SENSOR_H</span></div></div><div><div><div>2</div></div><div><span>#define</span><span> </span><span>POCKET_SENSOR_H</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>pocket_stats_start</span><span>(</span><span>unsigned</span><span> </span><span>int</span><span><span> </span><span>count</span><span>);</span></span></div></div><div><div><div>5</div></div><div><span>unsigned</span><span> </span><span>int</span><span> </span><span>pocket_stats_stop</span><span>(</span><span>void</span><span>);</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>真正交给 Kbuild 的文件只有两行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-m</span><span><span> </span><span>+=</span><span> pocket_sensor.o</span></span></div></div><div><div><div>2</div></div><div><span>pocket_sensor-y</span><span><span> </span><span>:=</span><span> core.o stats.o</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一行声明最终模块，第二行声明组成这个模块的对象。<code>pocket_sensor-y</code> 里的 <code>y</code> 不是说整个模块内建；它表示这些对象无条件组成 <code>pocket_sensor.o</code>。最终走内建还是模块路径，仍由外层的 <code>obj-y</code> 或 <code>obj-m</code> 决定。</p><p>为了让目录里直接执行 <code>make</code>，我另外放一个很薄的包装 Makefile：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>KDIR</span><span><span> </span><span>?=</span><span> /path/to/linux-6.18-build</span></span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>.PHONY</span><span><span>:</span><span> all clean</span></span></div></div><div><div><div>4</div></div><div><span>all</span><span>:</span></div></div><div><div><div>5</div></div><div><span>  </span><span>$(</span><span>MAKE</span><span>)</span><span> -C </span><span>$(</span><span>KDIR</span><span>)</span><span> M=</span><span>$(</span><span>CURDIR</span><span>)</span><span> modules</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>clean</span><span>:</span></div></div><div><div><div>8</div></div><div><span>  </span><span>$(</span><span>MAKE</span><span>)</span><span> -C </span><span>$(</span><span>KDIR</span><span>)</span><span> M=</span><span>$(</span><span>CURDIR</span><span>)</span><span> clean</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>KDIR</code> 指向已经配置并完成构建的 Linux 6.18 输出目录。只运行 <code>modules_prepare</code> 可以补齐很多生成头和脚本，但在 <code>CONFIG_MODVERSIONS=y</code> 时，它不会生成 <code>Module.symvers</code>。我这次完整构建了实验内核和 <code>modules</code> 目标，避免把缺失符号版本伪装成模块问题。</p><p>第一次构建输出是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ make</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>CC [M]  core.o</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>CC [M]  stats.o</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>LD [M]  pocket_sensor.o</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>MODPOST Module.symvers</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>CC [M]  pocket_sensor.mod.o</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>CC [M]  .module-common.o</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>LD [M]  pocket_sensor.ko</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>干净构建耗时 <code>1.78</code> 秒，立刻再构建一次耗时 <code>0.89</code> 秒，第二次没有执行任何 <code>CC</code>、<code>LD</code> 或 <code>MODPOST</code>。这里的绝对时间包含启动多层 Make 的固定成本，不适合拿去跑编译器排行榜；有趣的是工作量从 7 个构建动作降到了 0。</p><p>5 个输入文件旁边出现了 19 个生成文件：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>core.o</span></div></div><div><div><div>2</div></div><div><span>stats.o</span></div></div><div><div><div>3</div></div><div><span>pocket_sensor.mod</span></div></div><div><div><div>4</div></div><div><span>pocket_sensor.o</span></div></div><div><div><div>5</div></div><div><span>modules.order</span></div></div><div><div><div>6</div></div><div><span>Module.symvers</span></div></div><div><div><div>7</div></div><div><span>pocket_sensor.mod.c</span></div></div><div><div><div>8</div></div><div><span>pocket_sensor.mod.o</span></div></div><div><div><div>9</div></div><div><span>.module-common.o</span></div></div><div><div><div>10</div></div><div><span>pocket_sensor.ko</span></div></div><div><div><div>11</div></div><div><span>.core.o.cmd</span></div></div><div><div><div>12</div></div><div><span>.stats.o.cmd</span></div></div><div><div><div>13</div></div><div><span>.pocket_sensor.mod.cmd</span></div></div><div><div><div>14</div></div><div><span>.pocket_sensor.o.cmd</span></div></div><div><div><div>15</div></div><div><span>.pocket_sensor.mod.o.cmd</span></div></div><div><div><div>16</div></div><div><span>.pocket_sensor.ko.cmd</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>此外还有为 <code>modules.order</code>、<code>Module.symvers</code> 和 <code>.module-common.o</code> 保存命令的 3 个隐藏 <code>.cmd</code> 文件。连同上面的 16 项，总数是 19。Kbuild 很像一条留下完整货运单的装配线：<code>.ko</code> 是箱子，旁边这些小文件告诉它箱子为什么需要重新装。</p><p>成品大小为 <code>6944</code> 字节：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>core.o               4064 bytes</span></div></div><div><div><div>2</div></div><div><span>stats.o              1608 bytes</span></div></div><div><div><div>3</div></div><div><span>pocket_sensor.o      5136 bytes</span></div></div><div><div><div>4</div></div><div><span>pocket_sensor.mod.o  2448 bytes</span></div></div><div><div><div>5</div></div><div><span>pocket_sensor.ko     6944 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我还编译了一个只有单个 <code>.c</code> 的最小模块。它生成 <code>4552</code> 字节的 <code>.ko</code> 和 15 个派生文件；双文件实验生成 19 个派生文件。两个模块行为不同，所以 <code>2392</code> 字节的体积差不能全算作”多文件税”。真正由复合模块新增的关键动作，是把两个局部 <code>.o</code> 用 <code>ld -r</code> 合成最终的 <code>pocket_sensor.o</code>。</p></section>
<section><h2><code>make -C ... M=... modules</code> 为什么像走了两遍？<a href="#make--c--m-modules-为什么像走了两遍"><span>#</span></a></h2><p>命令是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-C</span><span> </span><span>"</span><span>$KDIR</span><span>"</span><span> </span><span>M="</span><span>$PWD</span><span>"</span><span> </span><span>modules</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>部分</th><th>作用</th></tr></thead><tbody><tr><td><code>-C "$KDIR"</code></td><td>进入目标内核构建目录；这里必须有 <code>.config</code>、生成头、构建工具和 <code>Module.symvers</code></td></tr><tr><td><code>M="$PWD"</code></td><td>声明外部模块，并给出源码目录的绝对路径</td></tr><tr><td><code>modules</code></td><td>选择模块构建目标；它也是外部模块的默认目标</td></tr><tr><td>外层 Makefile</td><td>把调用送进内核 Kbuild，不自行拼接内核编译参数</td></tr></tbody></table><p>如果把包装逻辑和 Kbuild 声明写在同一个 Makefile，传统写法会检查 <code>KERNELRELEASE</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ifneq</span><span> (</span><span>$(</span><span>KERNELRELEASE</span><span>)</span><span>,)</span></div></div><div><div><div>2</div></div><div><span>obj-m</span><span><span> </span><span>+=</span><span> pocket_sensor.o</span></span></div></div><div><div><div>3</div></div><div><span>pocket_sensor-y</span><span><span> </span><span>:=</span><span> core.o stats.o</span></span></div></div><div><div><div>4</div></div><div><span>else</span></div></div><div><div><div>5</div></div><div><span>KDIR</span><span><span> </span><span>?=</span><span> /path/to/linux-6.18-build</span></span></div></div><div><div><div>6</div></div><div><span>all</span><span>:</span></div></div><div><div><div>7</div></div><div><span>  </span><span>$(</span><span>MAKE</span><span>)</span><span> -C </span><span>$(</span><span>KDIR</span><span>)</span><span> M=</span><span>$(</span><span>CURDIR</span><span>)</span><span> modules</span></div></div><div><div><div>8</div></div><div><span>endif</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一次读取时，普通 Make 看见 <code>all</code> 并调用内核。第二次进入 Kbuild 后，<code>KERNELRELEASE</code> 已经存在，于是同一文件只暴露 <code>obj-m</code>。我选择独立的 <code>Kbuild</code> 文件后，不再需要这个分支；Kbuild 会优先读取 <code>Kbuild</code>，找不到才读取 <code>Makefile</code>。</p><div><div><div>内核 Kbuild外层 Makefile用户内核 Kbuild外层 Makefile用户makemake -C KDIR M=CURDIR modules设置 KERNELRELEASE读取 Kbuild 或 Makefile 的内核部分生成 pocket_sensor.ko
</div><div>内核 Kbuild外层 Makefile用户内核 Kbuild外层 Makefile用户makemake -C KDIR M=CURDIR modules设置 KERNELRELEASE读取 Kbuild 或 Makefile 的内核部分生成 pocket_sensor.ko
</div></div></div><p>Linux 6.13 以后还支持 <code>-f</code> 入口，它不要求先切换工作目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-f</span><span> </span><span>"</span><span>$KDIR</span><span>/Makefile"</span><span> </span><span>M="</span><span>$PWD</span><span>"</span><span> </span><span>modules</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两种入口最终进入同一套规则。这里的”两遍”不是 GNU Make 神秘地重复编译，而是包装层和内核构建层各读一次自己负责的描述。</p><section><h3><code>src</code>、<code>obj</code>、<code>srctree</code>、<code>objtree</code><a href="#srcobjsrctreeobjtree"><span>#</span></a></h3>

<table><thead><tr><th>变量</th><th>指向</th></tr></thead><tbody><tr><td><code>srctree</code></td><td>内核源码树根目录</td></tr><tr><td><code>objtree</code></td><td>内核输出树根目录；非分离构建时通常等于 <code>srctree</code></td></tr><tr><td><code>src</code></td><td>当前 Kbuild 目录的源码路径</td></tr><tr><td><code>obj</code></td><td>当前 Kbuild 目录的输出路径或处理位置</td></tr></tbody></table><p>构建内核时，<code>O=/path/to/out</code> 选择 <code>objtree</code>。它不等于”把外部模块输出放到这里”。Linux 6.18 为外部模块单独提供 <code>MO=/path/to/module-out</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-C</span><span> </span><span>"</span><span>$KDIR</span><span>"</span><span> </span><span>M="</span><span>$PWD</span><span>"</span><span> </span><span>MO="</span><span>$PWD</span><span>/out"</span><span> </span><span>modules</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以我会把边界记成：<code>O=</code> 管内核输出树，<code>M=</code> 指外部模块源码，<code>MO=</code> 可选地管外部模块输出。3 个路径混在一起时，很多看似缺头文件的问题其实只是拿错了构建目录。</p></section></section>
<section><h2>一行 <code>obj-m</code> 怎样进入 <code>scripts/Makefile.build</code>？<a href="#一行-obj-m-怎样进入-scriptsmakefilebuild"><span>#</span></a></h2><p>上一篇文章已经追到这个入口：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>build</span><span><span> </span><span>:=</span><span> -f </span></span><span>$(</span><span>srctree</span><span>)</span><span>/scripts/Makefile.build obj</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内核随后用近似下面的子 Make 调用处理一个目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-f</span><span> </span><span>scripts/Makefile.build</span><span> </span><span>obj=&lt;current-directory&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Linux 6.18 的 <code>scripts/Makefile.build</code> 先清空 <code>obj-y</code>、<code>obj-m</code>、flags 等局部变量，再按顺序读入配置和规则：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>scripts/Makefile.build</p></span></div><div><span><p>include/config/auto.conf</p></span></div><div><span><p>scripts/Kbuild.include</p></span></div><div><span><p>scripts/Makefile.compiler</p></span></div><div><span><p>当前目录 Kbuild 或 Makefile</p></span></div><div><span><p>scripts/Makefile.lib</p></span></div><div><span><p>展开 obj-y、obj-m 和复合对象</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>scripts/Makefile.build</p></span></div><div><span><p>include/config/auto.conf</p></span></div><div><span><p>scripts/Kbuild.include</p></span></div><div><span><p>scripts/Makefile.compiler</p></span></div><div><span><p>当前目录 Kbuild 或 Makefile</p></span></div><div><span><p>scripts/Makefile.lib</p></span></div><div><span><p>展开 obj-y、obj-m 和复合对象</p></span></div>
</div></div></div><p>这解释了为什么一个目录中的 Kbuild 可以非常短。公共文件已经定义了如何编译 <code>.c</code>、怎样保存命令、如何归档 <code>built-in.a</code>、怎样生成 <code>modules.order</code>。目录文件只声明输入图，不重复写编译器菜谱。</p><p>对这两行输入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-m</span><span><span> </span><span>+=</span><span> pocket_sensor.o</span></span></div></div><div><div><div>2</div></div><div><span>pocket_sensor-y</span><span><span> </span><span>:=</span><span> core.o stats.o</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Makefile.build</code> 算出几组直接影响实验的集合：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-m       = pocket_sensor.o</span></div></div><div><div><div>2</div></div><div><span>multi-obj-m = pocket_sensor.o</span></div></div><div><div><div>3</div></div><div><span>real-obj-m  = core.o stats.o</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>multi-obj-m</code> 是需要部分链接的复合模块。<code>real-obj-m</code> 是真正从源码生成的叶子对象。<code>scripts/Makefile.lib</code> 再通过 <code>part-of-module</code> 给叶子编译追加 <code>-DMODULE</code>、<code>KBUILD_MODNAME</code> 等 flags。我的 <code>V=1</code> 输出中，<code>core.c</code> 的命令确实带有：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-DMODULE</span></div></div><div><div><div>2</div></div><div><span>-DKBUILD_BASENAME='"core"'</span></div></div><div><div><div>3</div></div><div><span>-DKBUILD_MODNAME='"pocket_sensor"'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Kbuild 先生成一个内容只有两行的响应文件 <code>pocket_sensor.mod</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>./core.o</span></div></div><div><div><div>2</div></div><div><span>./stats.o</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后执行实质上的：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ld</span><span> </span><span>-r</span><span> </span><span>-o</span><span> </span><span>pocket_sensor.o</span><span> </span><span>@pocket_sensor.mod</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-r</code> 产生仍可重定位的合并对象，不把它链接成用户空间可执行文件。此时 <code>core.o</code> 对 <code>pocket_stats_start()</code> 的未解析引用由同一次部分链接中的 <code>stats.o</code> 满足。这个阶段像把两个零件焊成一个仍带接口的总成，内核符号的插头还留在外面，稍后加载器再接。</p><p><code>obj-y</code> 和 <code>obj-m</code> 也可能错误地同时列出同一个对象。Linux 6.18 明确执行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-m</span><span><span> </span><span>:=</span><span> </span></span><span>$(</span><span>filter-out</span><span> $(</span><span>obj-y</span><span>),$(</span><span>obj-m</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也就是内建版本优先，模块副本被移除。Kbuild 不会为同一个目标同时制造一份内建对象和一份 <code>.ko</code>，然后让我们猜哪份算数。</p><p>完整的模块前半段现在变成：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>core.c</p></span></div><div><span><p>core.o</p></span></div><div><span><p>stats.c</p></span></div><div><span><p>stats.o</p></span></div><div><span><p>ld -r</p></span></div><div><span><p>pocket_sensor.o</p></span></div><div><span><p>modpost</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>core.c</p></span></div><div><span><p>core.o</p></span></div><div><span><p>stats.c</p></span></div><div><span><p>stats.o</p></span></div><div><span><p>ld -r</p></span></div><div><span><p>pocket_sensor.o</p></span></div><div><span><p>modpost</p></span></div>
</div></div></div></section>
<section><h2><code>.cmd</code> 才是增量构建的记忆<a href="#cmd-才是增量构建的记忆"><span>#</span></a></h2>

<table><thead><tr><th>判断</th><th>Kbuild 使用的依据</th></tr></thead><tbody><tr><td>编译命令是否改变</td><td><code>.cmd</code> 中保存的上一次完整命令</td></tr><tr><td>输入是否改变</td><td>源文件、头文件和被引用的 <code>CONFIG_*</code> 标记依赖</td></tr></tbody></table><p><code>core.o</code> 构建后，<code>.core.o.cmd</code> 开头保存完整命令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>savedcmd_core.o</span><span><span> </span><span>:=</span><span> gcc ... -DMODULE ... -c -o core.o core.c</span></span></div></div><div><div><div>2</div></div><div><span>source_core.o</span><span><span> </span><span>:=</span><span> core.c</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>后面是一长串真实依赖。例如 <code>core.c</code> 包含的 <code>linux/module.h</code> 使用了模块开关，因此文件中出现：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$(</span><span>wildcard</span><span> include/config/MODULES)</span></div></div><div><div><div>2</div></div><div><span>$(</span><span>wildcard</span><span> include/config/MODULE_UNLOAD)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>规则链是：</p><div><div><div><div><span></span></div><div><span><p>否</p></span></div><div><span><p>是</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>if_changed_dep</p></span></div><div><span><p>目标、依赖或命令变化？</p></span></div><div><span><p>跳过编译</p></span></div><div><span><p>cmd_and_fixdep</p></span></div><div><span><p>执行 GCC 编译命令</p></span></div><div><span><p>生成临时 .d 依赖文件</p></span></div><div><span><p>scripts/basic/fixdep</p></span></div><div><span><p>写入 .core.o.cmd</p></span></div><div><span><p>删除临时 .d</p></span></div>
</div><div><div><span></span></div><div><span><p>否</p></span></div><div><span><p>是</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>if_changed_dep</p></span></div><div><span><p>目标、依赖或命令变化？</p></span></div><div><span><p>跳过编译</p></span></div><div><span><p>cmd_and_fixdep</p></span></div><div><span><p>执行 GCC 编译命令</p></span></div><div><span><p>生成临时 .d 依赖文件</p></span></div><div><span><p>scripts/basic/fixdep</p></span></div><div><span><p>写入 .core.o.cmd</p></span></div><div><span><p>删除临时 .d</p></span></div>
</div></div></div><p><code>if_changed_dep</code> 会在目标缺失、普通依赖更新或保存的命令变化时重建。<code>fixdep</code> 还会扫描依赖文件里出现的 <code>CONFIG_FOO</code>，把它转成 <code>include/config/FOO</code> 标记依赖。它并不完整解析 C；Linux 6.18 的源码注释甚至直说，注释中出现的 <code>CONFIG_FOO</code> 也会被抓到。多编一次可以接受，漏编一次不行。</p><p>我实际做了 4 轮增量实验：</p>

<table><thead><tr><th>操作</th><th>重新执行的主要步骤</th></tr></thead><tbody><tr><td>不改任何文件</td><td>无 <code>CC</code>、<code>LD</code>、<code>MODPOST</code></td></tr><tr><td>只改 <code>core.c</code> 中的普通注释</td><td><code>core.o</code>、复合链接、<code>modpost</code>、最终链接</td></tr><tr><td>修改编译 flags</td><td><code>core.o</code> 和 <code>stats.o</code>、复合链接、<code>modpost</code>、最终链接</td></tr><tr><td>修改两者共同包含的 <code>pocket_sensor.h</code></td><td>两个叶子对象及后续链全部更新</td></tr></tbody></table><p>配置依赖不需要额外造一个与模块无关的开关。后面的树内迁移直接用 <code>CONFIG_POCKET_SENSOR</code> 和 <code>CONFIG_POCKET_SENSOR_STATS</code> 验证：切换 <code>n/m/y</code> 后，Kbuild 会选择不编译、生成 <code>.ko</code> 或进入 <code>vmlinux</code>。目标内核配置或构建目录改变后，外部模块则应对匹配的新构建树做干净重建，不要复用旧 <code>.o</code>。</p><div><div><div></div><div>Important</div></div><div><p><code>.cmd</code> 是派生状态，不是稳定接口。需要诊断时可以读它，不能把项目逻辑建立在它的具体文本格式上。</p></div></div></section>
<section><h2><code>modpost</code> 不是又一个链接器<a href="#modpost-不是又一个链接器"><span>#</span></a></h2><p>第一阶段结束时，我们已经有 <code>pocket_sensor.o</code>、<code>pocket_sensor.mod</code> 和 <code>modules.order</code>。<code>modules.order</code> 在这个实验里只有：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pocket_sensor.o</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它记录 Kbuild 发现模块的顺序。随后 <code>scripts/Makefile.modpost</code> 调用主机程序 <code>scripts/mod/modpost</code>。<code>modpost</code> 主要做静态审查和元数据生成：</p>

<table><thead><tr><th>阶段</th><th><code>modpost</code> 的工作</th></tr></thead><tbody><tr><td>符号扫描</td><td>扫描导入、导出、<code>EXPORT_SYMBOL()</code> 和命名空间关系</td></tr><tr><td>符号检查</td><td>对照内核 <code>Module.symvers</code> 检查未解析符号</td></tr><tr><td>版本处理</td><td>在启用 <code>CONFIG_MODVERSIONS</code> 时处理符号 CRC</td></tr><tr><td>静态检查</td><td>执行 section mismatch 等检查</td></tr><tr><td>代码生成</td><td>为每个模块生成 <code>&lt;module&gt;.mod.c</code></td></tr><tr><td>符号输出</td><td>为当前构建写出 <code>Module.symvers</code></td></tr></tbody></table><p>这里有一个很好用的失败实验。我最初只构建了 <code>bzImage</code>，没有构建内核的 <code>modules</code> 目标，于是目标输出树中缺少 <code>Module.symvers</code>。<code>modpost</code> 没有含糊地吐出一个坏 <code>.ko</code>，而是列出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>WARNING: .../Module.symvers is missing.</span></div></div><div><div><div>2</div></div><div><span>ERROR: modpost: "param_ops_uint" [pocket_sensor.ko] undefined!</span></div></div><div><div><div>3</div></div><div><span>ERROR: modpost: "__kmalloc_noprof" [pocket_sensor.ko] undefined!</span></div></div><div><div><div>4</div></div><div><span>ERROR: modpost: "kfree" [pocket_sensor.ko] undefined!</span></div></div><div><div><div>5</div></div><div><span>ERROR: modpost: "_printk" [pocket_sensor.ko] undefined!</span></div></div><div><div><div>6</div></div><div><span>ERROR: modpost: "param_ops_charp" [pocket_sensor.ko] undefined!</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运行内核的 <code>make modules</code> 生成 <code>Module.symvers</code> 后，同一个模块通过。这正是 <code>modpost</code> 的价值：在 QEMU 启动前就指出内核符号账本不完整。</p><p>Linux 6.18 为我的模块生成了很短的 <code>pocket_sensor.mod.c</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;linux/module.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;linux/export-internal.h&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;linux/compiler.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>MODULE_INFO</span><span><span>(</span><span>name</span><span>,</span><span> KBUILD_MODNAME</span><span>);</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>__visible </span><span>struct</span><span> module __this_module</span></div></div><div><div><div>8</div></div><div><span>__section</span><span>(</span><span>".gnu.linkonce.this_module"</span><span>) </span><span>=</span><span> {</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>.name </span><span>=</span><span> KBUILD_MODNAME,</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>.init </span><span>=</span><span> init_module,</span></div></div><div><div><div>11</div></div><div><span>#ifdef</span><span> </span><span>CONFIG_MODULE_UNLOAD</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>.exit </span><span>=</span><span> cleanup_module,</span></div></div><div><div><div>13</div></div><div><span>#endif</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>.arch </span><span>=</span><span> MODULE_ARCH_INIT,</span></div></div><div><div><div>15</div></div><div><span>};</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>MODULE_INFO</span><span><span>(</span><span>depends</span><span>,</span><span> </span></span><span>""</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这段代码把模块名、入口、退出函数和依赖写成内核加载器认识的结构。它被编译为 <code>pocket_sensor.mod.o</code>。</p><p>旧文章常把 <code>vermagic</code> 也指向 <code>&lt;module&gt;.mod.c</code>。Linux 6.18 已经把公共部分放进 <code>scripts/module-common.c</code>，编译为 <code>.module-common.o</code>；其中包含：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>MODULE_INFO</span><span><span>(</span><span>vermagic</span><span>,</span><span> VERMAGIC_STRING</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以最终输入比常见简图多一个对象：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>ld -r</p></span></div><div><span><p>pocket_sensor.o<br /><br />模块代码</p></span></div><div><span><p>scripts/Makefile.modfinal</p></span></div><div><span><p>pocket_sensor.mod.o<br /><br />入口、退出与依赖</p></span></div><div><span><p>.module-common.o<br /><br />vermagic 等公共元数据</p></span></div><div><span><p>scripts/module.lds</p></span></div><div><span><p>pocket_sensor.ko</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>ld -r</p></span></div><div><span><p>pocket_sensor.o<br /><br />模块代码</p></span></div><div><span><p>scripts/Makefile.modfinal</p></span></div><div><span><p>pocket_sensor.mod.o<br /><br />入口、退出与依赖</p></span></div><div><span><p>.module-common.o<br /><br />vermagic 等公共元数据</p></span></div><div><span><p>scripts/module.lds</p></span></div><div><span><p>pocket_sensor.ko</p></span></div>
</div></div></div><p>版本锁定在这里立刻有了价值。机制没变，但元数据从哪一个生成文件进入 <code>.ko</code> 已经变了。</p></section>
<section><h2><code>.ko</code> 仍然是一个可重定位 ELF<a href="#ko-仍然是一个可重定位-elf"><span>#</span></a></h2><p><code>scripts/Makefile.modfinal</code> 最终执行近似下面的链接：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ld</span><span> </span><span>-r</span><span> </span><span>-T</span><span> </span><span>scripts/module.lds</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-o</span><span> </span><span>pocket_sensor.ko</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>pocket_sensor.o</span><span> </span><span>pocket_sensor.mod.o</span><span> </span><span>.module-common.o</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>readelf -h pocket_sensor.ko</code> 给出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Class:   ELF64</span></div></div><div><div><div>2</div></div><div><span>Data:    little endian</span></div></div><div><div><div>3</div></div><div><span>Type:    REL (Relocatable file)</span></div></div><div><div><div>4</div></div><div><span>Machine: Advanced Micro Devices X86-64</span></div></div><div><div><div>5</div></div><div><span>Entry:   0x0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它没有用户程序那种固定入口地址，也还没有落到最终运行地址。<code>readelf -SW</code> 在 6944 字节中找到了 31 个 section，其中最关键的是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.text</span></div></div><div><div><div>2</div></div><div><span>.init.text</span></div></div><div><div><div>3</div></div><div><span>.exit.text</span></div></div><div><div><div>4</div></div><div><span>.modinfo</span></div></div><div><div><div>5</div></div><div><span>__param</span></div></div><div><div><div>6</div></div><div><span>.gnu.linkonce.this_module</span></div></div><div><div><div>7</div></div><div><span>.rela.text</span></div></div><div><div><div>8</div></div><div><span>.rela.init.text</span></div></div><div><div><div>9</div></div><div><span>.rela.exit.text</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>__init</code> 把初始化函数放进 <code>.init.text</code>，<code>__exit</code> 把退出函数放进 <code>.exit.text</code>。<code>module_param()</code> 产生 <code>__param</code> 数据，<code>MODULE_*</code> 宏向 <code>.modinfo</code> 填字符串，链接器保留 relocation section 等待内核处理。</p><p><code>readelf -Ws</code> 还能看到尚未解析的内核符号：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>UND param_ops_uint</span></div></div><div><div><div>2</div></div><div><span>UND __kmalloc_noprof</span></div></div><div><div><div>3</div></div><div><span>UND kfree</span></div></div><div><div><div>4</div></div><div><span>UND _printk</span></div></div><div><div><div>5</div></div><div><span>UND param_ops_charp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是链接失败留下的垃圾。模块必须调用当前内核提供的分配器、日志和参数实现，所以 <code>.ko</code> 有意保留这些外部引用。<code>modpost</code> 已用 <code>Module.symvers</code> 检查它们是否合法，内核加载器稍后给它们填入运行地址。</p><p><code>modinfo</code> 则把 <code>.modinfo</code> 翻译成人容易读的结果：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>filename:    pocket_sensor.ko</span></div></div><div><div><div>2</div></div><div><span>license:     GPL</span></div></div><div><div><div>3</div></div><div><span>description: Small Kbuild experiment</span></div></div><div><div><div>4</div></div><div><span>author:      bfmhno3</span></div></div><div><div><div>5</div></div><div><span>depends:</span></div></div><div><div><div>6</div></div><div><span>name:        pocket_sensor</span></div></div><div><div><div>7</div></div><div><span>vermagic:    6.18.0 mod_unload</span></div></div><div><div><div>8</div></div><div><span>parm:        label:Sensor label (charp)</span></div></div><div><div><div>9</div></div><div><span>parm:        samples:Number of synthetic samples (uint)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>到这里，<code>.ko</code> 已经不神秘了。它是一个附带模块元数据、参数描述、未解析内核符号和重定位记录的 ELF relocatable object。</p></section>
<section><h2>我把它真的装进了 Linux 6.18<a href="#我把它真的装进了-linux-618"><span>#</span></a></h2><p>我没有向宿主机加载实验模块。宿主机是工作环境，不是祭坛。我构建了一个精简的 x86_64 Linux 6.18、一个 <code>1,422,848</code> 字节的 initramfs，然后用 QEMU 启动：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>qemu-system-x86_64</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-machine</span><span> </span><span>accel=tcg</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-cpu</span><span> </span><span>max</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-m</span><span> </span><span>256M</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-smp</span><span> </span><span>1</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-nographic</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-no-reboot</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-kernel</span><span> </span><span>arch/x86/boot/bzImage</span><span> </span><span>\</span></div></div><div><div><div>9</div></div><div><span>  </span><span>-initrd</span><span> </span><span>initramfs.cpio</span><span> </span><span>\</span></div></div><div><div><div>10</div></div><div><span>  </span><span>-append</span><span> </span><span>"console=ttyS0 rdinit=/init panic=-1"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>/init</code> 挂载 <code>/proc</code> 和 <code>/sys</code>，然后执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>insmod</span><span> </span><span>/pocket_sensor.ko</span><span> </span><span>label=qemu</span><span> </span><span>samples=</span><span>7</span></div></div><div><div><div>2</div></div><div><span>cat</span><span> </span><span>/sys/module/pocket_sensor/parameters/label</span></div></div><div><div><div>3</div></div><div><span>cat</span><span> </span><span>/sys/module/pocket_sensor/parameters/samples</span></div></div><div><div><div>4</div></div><div><span>rmmod</span><span> </span><span>pocket_sensor</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>串口日志是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>=== pocket_sensor experiment ===</span></div></div><div><div><div>2</div></div><div><span>pocket_sensor: loading out-of-tree module taints kernel.</span></div></div><div><div><div>3</div></div><div><span>pocket_sensor: loaded label=qemu samples=7</span></div></div><div><div><div>4</div></div><div><span>label=qemu</span></div></div><div><div><div>5</div></div><div><span>samples=7</span></div></div><div><div><div>6</div></div><div><span>pocket_sensor: unloaded samples=7</span></div></div><div><div><div>7</div></div><div><span>=== experiment complete ===</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个参数真的进入 <code>/sys/module/pocket_sensor/parameters/</code>，<code>stats.c</code> 分配的 7 个槽位在卸载时释放，初始化和退出日志各出现一次。外部模块 taint 也符合预期；它表示内核装入了树外代码，不表示实验失败。</p><section><h3>从 <code>insmod</code> 到 <code>module_init()</code><a href="#从-insmod-到-module_init"><span>#</span></a></h3><p><code>insmod</code> 很薄。现代 kmod 通常打开模块文件并调用 <code>finit_module()</code>；内核也保留接收用户缓冲区的 <code>init_module()</code> syscall。Linux 6.18 的核心路径在 <code>kernel/module/main.c</code>：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>insmod pocket_sensor.ko</p></span></div><div><span><p>finit_module()</p></span></div><div><span><p>load_module()</p></span></div><div><span><p>module_sig_check()<br /><br />签名与策略</p></span></div><div><span><p>ELF、modinfo 与 vermagic 检查</p></span></div><div><span><p>layout_and_allocate()<br /><br />布局并分配内存</p></span></div><div><span><p>simplify_symbols()<br /><br />解析内核符号</p></span></div><div><span><p>apply_relocations()<br /><br />应用重定位</p></span></div><div><span><p>解析模块参数并建立 sysfs</p></span></div><div><span><p>do_init_module()</p></span></div><div><span><p>pocket_sensor_init()</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>insmod pocket_sensor.ko</p></span></div><div><span><p>finit_module()</p></span></div><div><span><p>load_module()</p></span></div><div><span><p>module_sig_check()<br /><br />签名与策略</p></span></div><div><span><p>ELF、modinfo 与 vermagic 检查</p></span></div><div><span><p>layout_and_allocate()<br /><br />布局并分配内存</p></span></div><div><span><p>simplify_symbols()<br /><br />解析内核符号</p></span></div><div><span><p>apply_relocations()<br /><br />应用重定位</p></span></div><div><span><p>解析模块参数并建立 sysfs</p></span></div><div><span><p>do_init_module()</p></span></div><div><span><p>pocket_sensor_init()</p></span></div>
</div></div></div><p><code>load_module()</code> 先检查签名，再验证 ELF 和 <code>vermagic</code>，计算各 section 布局并分配模块内存。<code>simplify_symbols()</code> 遍历 <code>SHN_UNDEF</code> 符号，把 <code>_printk</code>、<code>kfree</code> 等名字解析成内核或其他已加载模块中的地址。<code>apply_relocations()</code> 再按架构处理 <code>.rela.*</code>，把地址写进刚分配的代码和数据。</p><p>参数解析发生在入口函数之前。因此 <code>label=qemu samples=7</code> 已经写入静态变量，<code>pocket_sensor_init()</code> 一开始就能读。内核还根据 <code>__param</code> 建立 sysfs 属性。最后 <code>do_init_module()</code> 调用 <code>.mod.c</code> 中记录的 <code>init_module</code>，也就是宏展开后的 <code>pocket_sensor_init()</code>。</p><p><code>insmod</code> 只加载指定文件，不主动解决依赖。真实驱动有依赖时通常用 <code>modprobe</code>，让它根据模块索引先加载依赖；但无论用户空间怎么安排文件，最终都进入上面的内核加载路径。</p></section></section>
<section><h2>4 类错误分别在说什么？<a href="#4-类错误分别在说什么"><span>#</span></a></h2><p>模块错误经常只在终端留下一句短话，真正上下文在内核日志。我的排查顺序是先看 <code>dmesg</code>，再把错误放回加载链。</p>

<table><thead><tr><th>用户空间错误</th><th>加载阶段</th><th>首要检查</th></tr></thead><tbody><tr><td><code>Invalid module format</code></td><td>ELF、<code>vermagic</code> 或符号版本检查</td><td>架构、目标构建目录、内核配置和 <code>dmesg</code></td></tr><tr><td><code>Unknown symbol</code></td><td>符号解析</td><td>依赖模块、导出符号、命名空间和 <code>Module.symvers</code></td></tr><tr><td><code>Operation not permitted</code></td><td>权限或安全策略</td><td><code>CAP_SYS_MODULE</code>、<code>modules_disabled</code>、LSM 和 lockdown</td></tr><tr><td><code>Required key not available</code></td><td>模块签名验证</td><td>签名、可信 keyring 和强制签名策略</td></tr></tbody></table><section><h3><code>Invalid module format</code><a href="#invalid-module-format"><span>#</span></a></h3><p>它通常对应 <code>ENOEXEC</code>，但原因不只一个。常见情况包括：</p><ul>
<li><code>.ko</code> 的架构或 ELF 结构不符合当前内核。</li>
<li><code>vermagic</code> 不匹配。</li>
<li>启用 <code>CONFIG_MODVERSIONS</code> 后，关键符号 CRC 不匹配。</li>
<li>生成 <code>struct module</code> 的构建配置与运行内核不一致。</li>
</ul><p>Linux 6.18 的 <code>check_modinfo()</code> 会比较模块和内核的 magic，并在日志中打印两边字符串。正确修复是用运行内核对应的构建目录和配置重新编译，不是删掉 <code>vermagic</code> 或使用强制加载把检查蒙住。</p></section><section><h3><code>Unknown symbol</code><a href="#unknown-symbol"><span>#</span></a></h3><p><code>simplify_symbols()</code> 无法为某个 <code>SHN_UNDEF</code> 找到可用的导出符号。可能是依赖模块没有加载、目标内核没导出该符号、命名空间导入缺失，或外部模块调用了不该调用的内部接口。</p><p>先用 <code>modinfo</code> 查看依赖，再检查 <code>Module.symvers</code> 和目标内核配置。若 <code>modpost</code> 已经警告 unresolved symbol，不要期待运行时突然心软。</p></section><section><h3><code>Operation not permitted</code><a href="#operation-not-permitted"><span>#</span></a></h3><p>加载入口先执行 <code>may_init_module()</code>。Linux 6.18 在调用者缺少 <code>CAP_SYS_MODULE</code> 或 <code>modules_disabled=1</code> 时返回 <code>EPERM</code>；LSM、lockdown 和容器策略也可能拒绝加载。<code>sudo</code> 只能解决普通权限，不会自动推翻 Secure Boot lockdown 或宿主机给容器划下的边界。</p></section><section><h3><code>Required key not available</code> 或 <code>Key was rejected by service</code><a href="#required-key-not-available-或-key-was-rejected-by-service"><span>#</span></a></h3><p>启用强制模块签名后，<code>module_sig_check()</code> 会验证附在模块末尾的 PKCS#7 签名。模块未签名、算法不支持或密钥不在可信 keyring 中时，Linux 6.18 的强制路径返回 <code>EKEYREJECTED</code>，用户空间显示的短句随 errno 翻译和加载路径而异。正确路径是用目标系统信任的密钥签名并部署证书，不是关闭检查。</p><div><div><div></div><div>Warning</div></div><div><p>强制加载、伪造版本字符串和关闭签名策略会把”构建产物不匹配”变成”内核正在执行未经验证的代码”。它们不是调试捷径，而是在移除护栏。</p></div></div></section></section>
<section><h2>把 <code>obj-m</code> 换成 <code>obj-y</code> 会发生什么？<a href="#把-obj-m-换成-obj-y-会发生什么"><span>#</span></a></h2><p>模块路径现在完整了，但上一篇真正留下的问题还有一半：<code>CONFIG_FOO=y</code> 去了哪里？</p><p>对树内目录，父级 Makefile 写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_POCKET_SENSOR</span><span>)</span><span><span> </span><span>+=</span><span> pocket_sensor/</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当值为 <code>y</code>，<code>scripts/Makefile.build</code> 递归进入目录，并把子目录的内建对象装进：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>drivers/misc/pocket_sensor/built-in.a</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Linux 6.18 使用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ar cDPrST built-in.a ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其中 <code>T</code> 创建 thin archive。它主要保存成员路径，不复制每个 <code>.o</code> 的完整内容。我的归档由两个成员组成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>drivers/misc/pocket_sensor/core.o</span></div></div><div><div><div>2</div></div><div><span>drivers/misc/pocket_sensor/stats.o</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>目录层级继续向上汇聚：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>core.o + stats.o</p></span></div><div><span><p>pocket_sensor/built-in.a</p></span></div><div><span><p>drivers/misc/built-in.a</p></span></div><div><span><p>drivers/built-in.a</p></span></div><div><span><p>根 built-in.a</p></span></div><div><span><p>vmlinux.a</p></span></div><div><span><p>vmlinux.o</p></span></div><div><span><p>scripts/link-vmlinux.sh</p></span></div><div><span><p>vmlinux</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>core.o + stats.o</p></span></div><div><span><p>pocket_sensor/built-in.a</p></span></div><div><span><p>drivers/misc/built-in.a</p></span></div><div><span><p>drivers/built-in.a</p></span></div><div><span><p>根 built-in.a</p></span></div><div><span><p>vmlinux.a</p></span></div><div><span><p>vmlinux.o</p></span></div><div><span><p>scripts/link-vmlinux.sh</p></span></div><div><span><p>vmlinux</p></span></div>
</div></div></div><p>顶层 Makefile 用 <code>KBUILD_VMLINUX_OBJS</code> 生成 <code>vmlinux.a</code>，<code>scripts/Makefile.vmlinux_o</code> 通过 <code>--whole-archive vmlinux.a</code> 生成 <code>vmlinux.o</code>，<code>scripts/link-vmlinux.sh</code> 再完成架构相关的最终链接。<code>y</code> 状态不会生成或更新 <code>pocket_sensor.ko</code>；若刚从 <code>m</code> 切到 <code>y</code>，输出树里可能仍留着旧 <code>.ko</code>，它只是陈旧产物，<code>make clean</code> 才会移除。</p><p><code>modules.builtin</code> 中仍会记录一个逻辑路径：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>kernel/drivers/misc/pocket_sensor/pocket_sensor.ko</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它告诉 <code>modprobe</code> 这个模块名已经内建，不表示磁盘上真的生成了那份 <code>.ko</code>。我的 <code>System.map</code> 则能找到 <code>pocket_sensor_init</code>、<code>pocket_stats_start</code> 和对应 initcall，证明代码已经进入 <code>vmlinux</code>。</p><section><h3>内建时 <code>module_init()</code> 变成 initcall<a href="#内建时-module_init-变成-initcall"><span>#</span></a></h3>

<table><thead><tr><th>构建形态</th><th><code>module_init()</code> 的结果</th></tr></thead><tbody><tr><td>可加载模块</td><td>提供 <code>.mod.c</code> 所引用的 <code>init_module</code> 入口</td></tr><tr><td>内建代码</td><td>映射到 <code>device_initcall()</code>，把函数指针放进 initcall section</td></tr></tbody></table><p>Linux 按 initcall level 执行；同一 level 内的次序受链接顺序影响。<code>obj-y</code> 的排列因此不只是审美问题。两个驱动若偷偷依赖初始化先后，调换 Makefile 中对象顺序就可能改变启动行为。更好的设计仍是显式表达依赖，而不是拿链接顺序当隐藏 API。</p><p><code>module_exit()</code> 对可卸载模块是 <code>rmmod</code> 路径。内建代码正常运行期间不会被卸载，所以退出函数没有对应的日常生命周期意义；我的 <code>System.map</code> 仍保留了 <code>pocket_sensor_exit</code>，但内核不会把它当作可卸载模块的退出回调来调用。不要因为同一份源码能用 <code>y</code> 和 <code>m</code> 编译，就假设两种生命周期完全对称。</p></section></section>
<section><h2>我把外部模块迁回了内核树<a href="#我把外部模块迁回了内核树"><span>#</span></a></h2><p>实验目录改为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>drivers/misc/pocket_sensor/</span></div></div><div><div><div>2</div></div><div><span>├── Kconfig</span></div></div><div><div><div>3</div></div><div><span>├── Makefile</span></div></div><div><div><div>4</div></div><div><span>├── core.c</span></div></div><div><div><div>5</div></div><div><span>├── stats.c</span></div></div><div><div><div>6</div></div><div><span>└── pocket_sensor.h</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>父级 <code>drivers/misc/Kconfig</code> 加一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>source "drivers/misc/pocket_sensor/Kconfig"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>配置文件为：</p><div><figure><figcaption><span>GPL-2.0-only</span></figcaption><pre><code><div><div><div>1</div></div><div><span>config POCKET_SENSOR</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>tristate "Pocket sensor experiment"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>help</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>Build the pocket sensor used to trace the Kbuild pipeline.</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>config POCKET_SENSOR_STATS</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>bool "Pocket sensor statistics"</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>depends on POCKET_SENSOR</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>default y</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>help</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>Add the synthetic sample storage used by the experiment.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>父级 <code>drivers/misc/Makefile</code> 决定是否进入目录：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_POCKET_SENSOR</span><span>)</span><span><span> </span><span>+=</span><span> pocket_sensor/</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>子目录 Makefile 决定总成和可选零件：</p><div><figure><figcaption><span>GPL-2.0-only</span></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_POCKET_SENSOR</span><span>)</span><span><span> </span><span>+=</span><span> pocket_sensor.o</span></span></div></div><div><div><div>2</div></div><div><span>pocket_sensor-y</span><span><span> </span><span>:=</span><span> core.o</span></span></div></div><div><div><div>3</div></div><div><span>pocket_sensor-</span><span>$(</span><span>CONFIG_POCKET_SENSOR_STATS</span><span>)</span><span><span> </span><span>+=</span><span> stats.o</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当 statistics 关闭时，头文件提供不分配内存的 inline 实现；当它打开时，<code>stats.o</code> 进入复合对象。这样配置和链接图一致，不会让 <code>core.o</code> 留下无人提供的函数。</p><p>我在同一个 Linux 6.18 输出树里验证了 3 种状态：</p>

<table><thead><tr><th>配置</th><th>实际构建结果</th></tr></thead><tbody><tr><td><code>CONFIG_POCKET_SENSOR=n</code></td><td>没有创建 <code>drivers/misc/pocket_sensor/</code> 输出目录</td></tr><tr><td><code>CONFIG_POCKET_SENSOR=m</code></td><td>编译 <code>core.o</code>、<code>stats.o</code>，部分链接并生成 <code>pocket_sensor.ko</code></td></tr><tr><td><code>CONFIG_POCKET_SENSOR=y</code></td><td>两个对象进入 thin <code>built-in.a</code>，随后进入 <code>vmlinux</code></td></tr></tbody></table><p><code>y</code> 状态的增量构建日志把路径完整打印出来：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>CC      drivers/misc/pocket_sensor/core.o</span></div></div><div><div><div>2</div></div><div><span>CC      drivers/misc/pocket_sensor/stats.o</span></div></div><div><div><div>3</div></div><div><span>AR      drivers/misc/pocket_sensor/built-in.a</span></div></div><div><div><div>4</div></div><div><span>AR      drivers/misc/built-in.a</span></div></div><div><div><div>5</div></div><div><span>AR      drivers/built-in.a</span></div></div><div><div><div>6</div></div><div><span>AR      built-in.a</span></div></div><div><div><div>7</div></div><div><span>AR      vmlinux.a</span></div></div><div><div><div>8</div></div><div><span>LD      vmlinux.o</span></div></div><div><div><div>9</div></div><div><span>MODPOST vmlinux.symvers</span></div></div><div><div><div>10</div></div><div><span>LD      vmlinux.unstripped</span></div></div><div><div><div>11</div></div><div><span>OBJCOPY vmlinux</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这也把两篇文章真正接上了：</p><div><div><div><div><span></span></div><div><span><p>n</p></span></div><div><span><p>m</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>y</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Kconfig</p></span></div><div><span><p>CONFIG_POCKET_SENSOR</p></span></div><div><span><p>不进入构建</p></span></div><div><span><p>obj-m</p></span></div><div><span><p>pocket_sensor.o</p></span></div><div><span><p>modpost</p></span></div><div><span><p>pocket_sensor.ko</p></span></div><div><span><p>obj-y</p></span></div><div><span><p>built-in.a</p></span></div><div><span><p>vmlinux.a</p></span></div><div><span><p>vmlinux</p></span></div>
</div><div><div><span></span></div><div><span><p>n</p></span></div><div><span><p>m</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>y</p></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Kconfig</p></span></div><div><span><p>CONFIG_POCKET_SENSOR</p></span></div><div><span><p>不进入构建</p></span></div><div><span><p>obj-m</p></span></div><div><span><p>pocket_sensor.o</p></span></div><div><span><p>modpost</p></span></div><div><span><p>pocket_sensor.ko</p></span></div><div><span><p>obj-y</p></span></div><div><span><p>built-in.a</p></span></div><div><span><p>vmlinux.a</p></span></div><div><span><p>vmlinux</p></span></div>
</div></div></div></section>
<section><h2>外部模块和树内模块的维护边界<a href="#外部模块和树内模块的维护边界"><span>#</span></a></h2><p>两条路径共享 Kbuild 语法和大量编译规则，但维护契约不同。</p>

<table><thead><tr><th>维度</th><th>外部模块</th><th>树内模块</th></tr></thead><tbody><tr><td>适合场景</td><td>快速验证、尚未上游的厂商驱动、独立发布或 DKMS</td><td>与内核同步演进、接入子系统测试、准备上游或长期维护产品内核</td></tr><tr><td>版本关系</td><td>内核内部 API 和 ABI 变化后，可能需要重编或修改源码</td><td>接口与调用方可以在同一次内核提交中原子修改</td></tr><tr><td>构建要求</td><td>构建目录、<code>.config</code>、<code>Module.symvers</code> 和工具链必须匹配目标内核</td><td>由同一内核源码树、配置和 Kbuild 图统一构建</td></tr><tr><td>安全与交付</td><td>需要单独处理安装、依赖索引、Secure Boot 签名和密钥部署</td><td>跟随内核镜像或模块安装流程交付</td></tr><tr><td>集成成本</td><td>初期低，但升级兼容工作由模块维护者承担</td><td>初期要补齐 Kconfig、测试、文档和子系统审查</td></tr></tbody></table><p>DKMS 解决的是自动重编与安装流程，不会把不稳定的内核 ABI 变稳定。</p><p>树内也不是把目录复制到 <code>drivers/</code> 就结束。正式驱动还要遵守子系统接口、代码风格、<code>MAINTAINERS</code>、文档与设备绑定规范，并写出准确的 Kconfig 依赖。我的 <code>pocket_sensor</code> 故意没有 platform bus、Device Tree、I2C 或真实寄存器访问；伪造硬件只会让 Kbuild 主线被一堆无意义的驱动样板淹没。</p></section>
<section><h2>ARM64 工程只多哪一层？<a href="#arm64-工程只多哪一层"><span>#</span></a></h2><p>Kbuild 数据流不因架构改变。交叉构建 ARM64 外部模块时，通常在内核配置、内核构建和模块构建中保持同一组变量：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>O="</span><span>$KOUT</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>ARCH=arm64</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>CROSS_COMPILE=aarch64-linux-gnu-</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>my_board_defconfig</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>make</span><span> </span><span>O="</span><span>$KOUT</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>ARCH=arm64</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>CROSS_COMPILE=aarch64-linux-gnu-</span><span> </span><span>\</span></div></div><div><div><div>9</div></div><div><span>  </span><span>-j</span><span>"$(</span><span>nproc</span><span>)"</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>make</span><span> </span><span>-C</span><span> </span><span>"</span><span>$KOUT</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>12</div></div><div><span>  </span><span>M="</span><span>$PWD</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>13</div></div><div><span>  </span><span>ARCH=arm64</span><span> </span><span>\</span></div></div><div><div><div>14</div></div><div><span>  </span><span>CROSS_COMPILE=aarch64-linux-gnu-</span><span> </span><span>\</span></div></div><div><div><div>15</div></div><div><span>  </span><span>modules</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>新增的是目标架构、交叉工具链和真实板卡的启动链，不是第二套 <code>obj-m -&gt; modpost -&gt; .ko</code> 机制。加载时仍要保证运行内核就是这份配置和符号账本对应的内核。</p></section>
<section><h2>继续往下追<a href="#继续往下追"><span>#</span></a></h2><p>这次我从 2 行 Kbuild 得到了一个 <code>6944</code> 字节的 ELF，在 QEMU 中给它传入 2 个参数，观察它分配 7 个样本槽位，再完整卸载。沿途最让我意外的不是链接器，而是那些小账本：<code>.cmd</code> 记住命令和依赖，<code>pocket_sensor.mod</code> 记住复合对象，<code>modules.order</code> 记住模块顺序，<code>Module.symvers</code> 记住内核可见符号，<code>.mod.c</code> 把入口写回 C。</p><p>如果继续向下走，下一站应该是真实总线和设备模型：让同一个模块不再凭 <code>insmod</code> 自说自话，而是由 Device Tree 描述硬件，由 platform 或 I2C core 完成匹配，再看 probe、defer 和 remove 如何与 Kbuild 产物相遇。到那时 <code>.ko</code> 已经不是神秘黑盒，只是驱动生命周期被装进 ELF 后的运输形态。</p><p>很好。我现在可以停止盯着 <code>obj-m</code> 猜 Make 的心情，开始给那块并不存在的传感器寻找一块真正的板子了。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/Documentation/kbuild/modules.rst" target="_blank">Linux 6.18: Building External Modules</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/Documentation/kbuild/makefiles.rst" target="_blank">Linux 6.18: Linux Kernel Makefiles</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/Kbuild.include" target="_blank">Linux 6.18: scripts/Kbuild.include</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/Makefile.build" target="_blank">Linux 6.18: scripts/Makefile.build</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/Makefile.lib" target="_blank">Linux 6.18: scripts/Makefile.lib</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/Makefile.modpost" target="_blank">Linux 6.18: scripts/Makefile.modpost</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/Makefile.modfinal" target="_blank">Linux 6.18: scripts/Makefile.modfinal</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/module-common.c" target="_blank">Linux 6.18: scripts/module-common.c</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/link-vmlinux.sh" target="_blank">Linux 6.18: scripts/link-vmlinux.sh</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/kernel/module/main.c" target="_blank">Linux 6.18: kernel/module/main.c</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/Documentation/admin-guide/module-signing.rst" target="_blank">Linux 6.18: Module signing facility</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/understanding-kconfig/</id>
      <title type="text">我拆开了 Kconfig: 从一个选项到 U-Boot 和 Linux 的条件编译</title>
      <published>2026-09-02T12:00:00.000Z</published>
      <updated>2026-09-02T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/understanding-kconfig/"/>
      <summary type="text">我用 Linux 6.18 的 Kconfig 前端搭了一个 30 行的实验板级配置，再沿着 Kconfig、defconfig、.config、auto.conf、autoconf.h 和 Kbuild 追踪 U-Boot 与 Linux 到底如何裁剪代码。</summary>
      <content type="html"><![CDATA[<p>我最近需要自己裁剪并构建 U-Boot 和 Linux Kernel。两个项目都让我先选一个 <code>defconfig</code>，再面对 <code>menuconfig</code> 里几千个选项，最后得到一片 <code>CONFIG_FOO=y</code>。我会用这些命令，却不知道中间发生了什么。于是我取出 Linux 6.18 的 Kconfig 实现，写了一棵只有 7 个符号的配置树，并一路追到 Makefile 和 C 预处理器。30 行 Kconfig 之后，这套系统终于不再像一个蓝色背景的许愿池。</p>
<p>本文以 <strong>Linux 6.18</strong> 和 <strong>U-Boot v2026.07</strong> 为准。Linux 6.18 是 2025 年 11 月 30 日发布的 longterm 内核，预计维护到 2028 年 12 月；U-Boot v2026.07 则是写作时适合新项目参考的稳定版本。Kconfig 的核心模型很稳定，但具体符号、默认值和 U-Boot 的兼容生成步骤会随版本变化，所以我会把机制和某个板子的配置内容分开。</p>
<section><h2>先纠正一个名字<a href="#先纠正一个名字"><span>#</span></a></h2><p>Kconfig 不是完整的构建系统。</p><p>我更愿意把整个过程分成 3 层：</p><ol>
<li><strong>Kconfig</strong> 描述有哪些配置符号、它们的类型、默认值和约束，并求出一份合法配置。</li>
<li><strong>Kbuild/Makefile</strong> 读取配置，决定哪些目录和 <code>.o</code> 文件参加编译，以及传递哪些编译选项。</li>
<li><strong>编译器和链接器</strong> 根据宏继续裁剪 C 代码，最后生成内核或 U-Boot 镜像。</li>
</ol><p>Kconfig 像配电柜：它决定哪些回路通电，但并不亲自制造电器。<code>make menuconfig</code> 只是配电柜的一块操作面板，不是配电柜本身，更不是发电厂。</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Kconfig 文件</p></span></div><div><span><p>conf/mconf 等前端</p></span></div><div><span><p>defconfig 或旧 .config</p></span></div><div><span><p>完整 .config</p></span></div><div><span><p>include/config/auto.conf</p></span></div><div><span><p>include/generated/autoconf.h</p></span></div><div><span><p>Kbuild/Makefile 选择目标文件</p></span></div><div><span><p>C 预处理器选择代码</p></span></div><div><span><p>编译与链接</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Kconfig 文件</p></span></div><div><span><p>conf/mconf 等前端</p></span></div><div><span><p>defconfig 或旧 .config</p></span></div><div><span><p>完整 .config</p></span></div><div><span><p>include/config/auto.conf</p></span></div><div><span><p>include/generated/autoconf.h</p></span></div><div><span><p>Kbuild/Makefile 选择目标文件</p></span></div><div><span><p>C 预处理器选择代码</p></span></div><div><span><p>编译与链接</p></span></div>
</div></div></div><p>这张图是本文最重要的地图。后面的每个文件都只是在这条数据流上承担一个角色。</p></section>
<section><h2>我先造一个口袋大小的配置系统<a href="#我先造一个口袋大小的配置系统"><span>#</span></a></h2><p>我使用 Linux 6.18 自带的 <code>scripts/kconfig/</code>，因为 U-Boot 的 Kconfig 语言源自 Linux，而 Linux 版本还保留了 <code>m</code> 这个最容易暴露三值逻辑的状态。先取得源码：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>--depth</span><span> </span><span>1</span><span> </span><span>--branch</span><span> </span><span>v6.18</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>https://github.com/torvalds/linux.git</span><span> </span><span>linux-6.18</span></div></div><div><div><div>3</div></div><div><span>cd</span><span> </span><span>linux-6.18</span></div></div><div><div><div>4</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>lab/drivers</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Kconfig 前端需要主机上的 C 编译器、GNU Make、Flex 和 Bison。<code>menuconfig</code> 还需要 ncurses 开发文件。这里只运行非交互的 <code>alldefconfig</code>，所以先不碰 ncurses。</p><p>根配置 <code>lab/Kconfig</code> 如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mainmenu "Pocket Board Configuration"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>config MODULES</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>bool "Enable loadable modules"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>modules</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>default y</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>menu "Board"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>config SOC_ALPHA</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>bool "Alpha SoC"</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>default y</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>config HAS_I2C</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>bool</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>default y if SOC_ALPHA</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>config LOG_LEVEL</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>int "Log level"</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>range 0 4</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>default 2</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>endmenu</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>source "lab/drivers/Kconfig"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>驱动配置放在 <code>lab/drivers/Kconfig</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>menu "Drivers"</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>config SENSOR</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>tristate "Temperature sensor"</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>depends on HAS_I2C</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>default m</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>config SENSOR_DEBUG</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>bool "Verbose sensor debugging"</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>depends on SENSOR</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>config APP</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>bool "Sensor application"</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>select SENSOR_CORE</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>config SENSOR_CORE</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>bool</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>depends on HAS_I2C</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>endmenu</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>现在让 Linux 的构建入口编译 Kconfig 前端，并用所有符号的默认值求解一次：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>O="</span><span>$PWD</span><span>/out"</span><span> </span><span>KBUILD_KCONFIG=lab/Kconfig</span><span> </span><span>alldefconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>O=</code> 把生成物放进 <code>out/</code>，<code>KBUILD_KCONFIG</code> 把入口从内核根目录的 <code>Kconfig</code> 换成我的实验文件。第一次运行还会把 <code>conf.c</code>、解析器和词法分析器编译成主机程序 <code>out/scripts/kconfig/conf</code>。换句话说，配置 Linux 之前，Linux 先在我的电脑上编译了一个小型配置求解器（构建系统也要先构建自己，乌龟下面还是乌龟）。</p><p>我得到的 <code>out/.config</code> 核心部分是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>CONFIG_MODULES=y</span></div></div><div><div><div>2</div></div><div><span>CONFIG_SOC_ALPHA=y</span></div></div><div><div><div>3</div></div><div><span>CONFIG_HAS_I2C=y</span></div></div><div><div><div>4</div></div><div><span>CONFIG_LOG_LEVEL=2</span></div></div><div><div><div>5</div></div><div><span>CONFIG_SENSOR=m</span></div></div><div><div><div>6</div></div><div><span># CONFIG_SENSOR_DEBUG is not set</span></div></div><div><div><div>7</div></div><div><span># CONFIG_APP is not set</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里已经发生了几件值得追踪的事：</p><ul>
<li>我没有直接设置 <code>HAS_I2C</code>，它从 <code>SOC_ALPHA=y</code> 的条件默认值得到 <code>y</code>。</li>
<li><code>SENSOR</code> 的上限允许到 <code>y</code>，默认值把它设成 <code>m</code>。</li>
<li><code>SENSOR_DEBUG</code> 没有显式 <code>default</code>，所以默认为 <code>n</code>。</li>
<li><code>APP</code> 同样默认为 <code>n</code>，因此它还没有触发 <code>select</code>。</li>
</ul><p>Kconfig 文件不是按顺序执行的 Shell 脚本。解析器先收集符号与关系，再计算值、可见性和取值上下限。把它当成一个受约束的配置数据库，比把它当成一串赋值语句准确得多。</p></section>
<section><h2>一个 <code>config</code> 到底定义了什么？<a href="#一个-config-到底定义了什么"><span>#</span></a></h2><p>最常见的条目是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>config SENSOR</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>tristate "Temperature sensor"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>depends on HAS_I2C</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>default m</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>help</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>Build support for the temperature sensor.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>SENSOR</code> 是符号名。写入配置文件时，前端通常给它加上 <code>CONFIG_</code> 前缀，于是得到 <code>CONFIG_SENSOR</code>。Kconfig 源码中的依赖表达式仍然写 <code>SENSOR</code>，不要写 <code>CONFIG_SENSOR</code>。</p><p>一个符号常见的属性包括：</p>

<table><thead><tr><th>属性</th><th>回答的问题</th></tr></thead><tbody><tr><td><code>bool</code> / <code>tristate</code> / <code>string</code> / <code>int</code> / <code>hex</code></td><td>值属于什么类型？</td></tr><tr><td><code>prompt</code> 或类型后的字符串</td><td>用户能否在界面中看到并修改它？</td></tr><tr><td><code>default</code></td><td>用户没有给值时，从哪里开始？</td></tr><tr><td><code>depends on</code></td><td>这个符号允许取到多大的值？</td></tr><tr><td><code>select</code></td><td>当前符号启用时，强制抬高哪个符号的下限？</td></tr><tr><td><code>imply</code></td><td>当前符号启用时，弱建议哪个符号？</td></tr><tr><td><code>range</code></td><td>整数或十六进制值允许在哪个区间？</td></tr><tr><td><code>help</code></td><td>用户在配置界面请求帮助时看到什么？</td></tr></tbody></table><p>类型后的提示文字其实是 <code>prompt</code> 的简写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>bool "Alpha SoC"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>等价于：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>bool</span></div></div><div><div><div>2</div></div><div><span>prompt "Alpha SoC"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这解释了一个常见现象：<strong>符号存在，不代表它会出现在 <code>menuconfig</code>。</strong> <code>HAS_I2C</code> 和 <code>SENSOR_CORE</code> 没有 prompt，它们是内部符号。用户不能直接操作它们，只能让 <code>default</code>、<code>select</code> 等关系计算出值。编辑 <code>.config</code> 强塞一个值也不会把内部符号变成公开开关；下一次解析时，Kconfig 仍按它的定义处理。</p><p><code>menu</code>、<code>comment</code> 和 <code>mainmenu</code> 主要组织界面。<code>source</code> 把另一份 Kconfig 文件读入同一棵配置树。它们不会因为排版漂亮就自动让任何 <code>.c</code> 文件参加编译。</p></section>
<section><h2><code>n</code>、<code>m</code>、<code>y</code> 不是 3 个字符串<a href="#nmy-不是-3-个字符串"><span>#</span></a></h2><p>Linux 的 <code>tristate</code> 使用三值逻辑：</p>

<table><thead><tr><th>文本</th><th>数值</th><th>含义</th></tr></thead><tbody><tr><td><code>n</code></td><td>0</td><td>不构建</td></tr><tr><td><code>m</code></td><td>1</td><td>构建为可加载模块</td></tr><tr><td><code>y</code></td><td>2</td><td>内建</td></tr></tbody></table><p>表达式中的 <code>&amp;&amp;</code> 取较小值，<code>||</code> 取较大值，<code>!</code> 则做 <span><span>2−x2-x</span><span><span><span></span><span>2</span><span></span><span>−</span><span></span></span><span><span></span><span>x</span></span></span></span>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m &amp;&amp; y = m</span></div></div><div><div><div>2</div></div><div><span>m || n = m</span></div></div><div><div><div>3</div></div><div><span>!m = m</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是普通布尔代数中随手多塞了一个字母。它让依赖能够表达一个关键约束：内建代码不能依赖只在运行时才加载的模块。例如 <code>FOO=y</code> 且 <code>BAR=m</code> 时，<code>FOO depends on BAR</code> 会把 <code>FOO</code> 的上限压到 <code>m</code>，因此 <code>FOO</code> 不能保持 <code>y</code>。</p><p><code>modules</code> 属性指定哪一个符号负责开启模块状态。Linux 使用它让大量 <code>tristate</code> 真正拥有 <code>m</code>。U-Boot v2026.07 没有 Linux 那样的可加载内核模块体系，也没有定义全局 <code>MODULES</code> 符号。它的树中仍能看到一些 <code>tristate</code> 声明，但日常板级裁剪应把 U-Boot 配置理解为主要在 <code>n/y</code> 之间选择，不要看到 <code>tristate</code> 就期待生成 <code>.ko</code>。</p></section>
<section><h2><code>depends on</code> 是上限，不是自动开关<a href="#depends-on-是上限不是自动开关"><span>#</span></a></h2><p>假设：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>config SENSOR</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>tristate "Temperature sensor"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>depends on HAS_I2C</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当 <code>HAS_I2C=n</code> 时，<code>SENSOR</code> 的最大值是 <code>n</code>，界面也会把它隐藏或禁用。当 <code>HAS_I2C=y</code> 时，Kconfig 只是允许 <code>SENSOR</code> 取 <code>n</code>、<code>m</code> 或 <code>y</code>，并不会替用户启用它。</p><p>所以 <code>depends on</code> 的读法不是：</p><blockquote><p>选择 SENSOR 时，顺便打开 HAS_I2C。</p></blockquote><p>而是：</p><blockquote><p>只有 HAS_I2C 足够可用时，SENSOR 才允许达到相应状态。</p></blockquote><p>菜单也会传递依赖：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>menu "I2C drivers"</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>depends on HAS_I2C</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>config SENSOR_A</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>tristate "Sensor A"</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>config SENSOR_B</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>tristate "Sensor B"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>endmenu</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里 <code>SENSOR_A</code> 和 <code>SENSOR_B</code> 都继承 <code>HAS_I2C</code> 依赖。<code>if ... endif</code> 也会把表达式附加到内部条目。它们不只是界面折叠工具。</p></section>
<section><h2><code>default</code> 只在用户没有决定时说话<a href="#default-只在用户没有决定时说话"><span>#</span></a></h2><p>Kconfig 允许一个符号出现多个 <code>default</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>config BUFFER_SIZE</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>int "Buffer size"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>default 4096 if ARCH_64BIT</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>default 1024</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果多个默认值的条件都成立，<strong>源码中第一个可见的默认值生效</strong>。但默认值只在用户没有提供值时使用。用户已经通过配置界面或已有 <code>.config</code> 选过 <code>2048</code>，<code>default 4096</code> 不会每次构建都把它夺回来。</p><p>这也解释了为什么新增配置通常保持 <code>default n</code>。旧配置升级到新源码时，<code>make olddefconfig</code> 会替新符号采用默认值；如果每个新驱动都 <code>default y</code>，一次版本升级就会把内核悄悄养胖。Linux 文档只为保留旧行为、纯粹打开下级菜单的 gatekeeper，以及少数用户普遍预期的基础设施等情况建议 <code>default y</code>。</p></section>
<section><h2><code>select</code> 很方便，也很锋利<a href="#select-很方便也很锋利"><span>#</span></a></h2><p>实验中的 <code>APP</code> 写成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>config APP</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>bool "Sensor application"</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>select SENSOR_CORE</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>config SENSOR_CORE</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>bool</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>depends on HAS_I2C</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我又准备了一份故意不合法的输入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># CONFIG_SOC_ALPHA is not set</span></div></div><div><div><div>2</div></div><div><span>CONFIG_APP=y</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>此时 <code>HAS_I2C=n</code>。我把它交给同一个 Linux 6.18 <code>conf</code>，得到：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>WARNING: unmet direct dependencies detected for SENSOR_CORE</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>Depends on [n]: HAS_I2C [=n]</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>Selected by [y]:</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>- APP [=y]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>select</code> 强制抬高目标符号的值，<strong>不会沿着目标符号的 <code>depends on</code> 继续解决依赖</strong>。于是 <code>APP</code> 可以把 <code>SENSOR_CORE</code> 推到 <code>y</code>，同时留下 <code>HAS_I2C=n</code> 这个自相矛盾的组合。</p><p>这不是求解器忘了帮忙，而是 <code>select</code> 的定义。Linux 文档明确建议谨慎使用它，最好只选择没有 prompt、也没有依赖的内部符号。若用户应该理解并控制前置条件，使用 <code>depends on</code>；若只是希望另一个功能通常随当前功能开启，但仍允许用户关闭，考虑 <code>imply</code>。</p><p>一个无聊但安全的重构通常是把能力和实现拆开：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>config HAVE_SENSOR_CORE</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>bool</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>config SENSOR_CORE</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>bool "Sensor core"</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>depends on HAVE_SENSOR_CORE &amp;&amp; HAS_I2C</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>平台可以 <code>select HAVE_SENSOR_CORE</code>，却不绕过 <code>SENSOR_CORE</code> 自己的运行条件。多写一个内部符号，比得到一份能通过配置但不能链接的组合便宜得多。</p></section>
<section><h2><code>defconfig</code>、<code>.config</code> 到底谁是谁？<a href="#defconfigconfig-到底谁是谁"><span>#</span></a></h2><p>这是我以前最容易混淆的部分。</p><section><h3><code>Kconfig</code> 是规则库<a href="#kconfig-是规则库"><span>#</span></a></h3><p>散布在源码树中的 <code>Kconfig</code> 文件定义全部符号和关系。Linux 6.18 的 Git 索引中有 1881 个文件名包含 <code>Kconfig</code>，U-Boot v2026.07 中有 1098 个。单靠一个平铺的配置文件维护这种规模，大概会迅速演化成考古学。</p></section><section><h3><code>defconfig</code> 是最小起点<a href="#defconfig-是最小起点"><span>#</span></a></h3><p>Linux 的架构默认配置通常位于：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>arch/&lt;arch&gt;/configs/&lt;name&gt;_defconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>U-Boot 的板级默认配置位于：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>configs/&lt;board&gt;_defconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它们通常只保存偏离 Kconfig 默认值的决定，不是完整快照。我实际运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>O=/tmp/uboot-out</span><span> </span><span>sandbox_defconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>U-Boot v2026.07 的 <code>configs/sandbox_defconfig</code> 中有 392 行已启用的 <code>CONFIG_</code> 赋值，展开后的 <code>/tmp/uboot-out/.config</code> 有 1125 行已启用赋值，约为 <strong>2.87 倍</strong>。剩余值来自默认值、架构选择、依赖和反向依赖。</p><div><div><div></div><div>Important</div></div><div><p>不要把 <code>defconfig</code> 直接复制成 <code>.config</code> 后立刻编译，也不要假设它已经列出每个关闭项。应该通过 <code>make &lt;board&gt;_defconfig</code> 或对应配置目标，让当前源码树重新求解完整配置。</p></div></div></section><section><h3><code>.config</code> 是当前构建的完整用户配置<a href="#config-是当前构建的完整用户配置"><span>#</span></a></h3><p><code>.config</code> 是某个源码版本、架构和用户选择组合后的工作状态。它比 <code>defconfig</code> 完整，也更容易在版本升级后变旧。</p><p>常见的关闭项写作：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># CONFIG_SENSOR_DEBUG is not set</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是普通注释，而是 Kconfig 保存 <code>n</code> 的标准形式。对于 Makefile 消费的 <code>auto.conf</code> 和 C 代码消费的 <code>autoconf.h</code>，关闭符号通常直接缺席。</p></section><section><h3><code>savedefconfig</code> 做反向压缩<a href="#savedefconfig-做反向压缩"><span>#</span></a></h3><p>完成配置后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>savedefconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Kconfig 会在源码树或输出树中生成名为 <code>defconfig</code> 的最小配置，只保留相对当前默认规则有必要记录的选择。U-Boot 新板级配置的常见工作流是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>my_board_defconfig</span></div></div><div><div><div>2</div></div><div><span>make</span><span> </span><span>menuconfig</span></div></div><div><div><div>3</div></div><div><span>make</span><span> </span><span>savedefconfig</span></div></div><div><div><div>4</div></div><div><span>cp</span><span> </span><span>defconfig</span><span> </span><span>configs/my_board_defconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>提交前应重新从保存的 defconfig 展开一次，确认它确实复现预期配置。默认值会变化，所以 <code>savedefconfig</code> 是相对于<strong>当前 Kconfig 规则</strong>做的压缩，不是跨所有未来版本的数学最小集。</p></section></section>
<section><h2>一次配置命令实际经过什么？<a href="#一次配置命令实际经过什么"><span>#</span></a></h2><p>以 U-Boot 为例：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>O=build</span><span> </span><span>my_board_defconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可以拆成下面几步：</p><ol>
<li>顶层 Makefile 识别 <code>&lt;board&gt;_defconfig</code> 配置目标。</li>
<li>构建主机程序 <code>scripts/kconfig/conf</code>。这里用的是主机编译器 <code>HOSTCC</code>，不是给目标板生成代码的交叉编译器。</li>
<li><code>conf</code> 从根 <code>Kconfig</code> 开始，递归解析 <code>source</code> 引入的文件，建立符号、菜单和依赖图。</li>
<li><code>conf</code> 读取 <code>configs/my_board_defconfig</code> 作为输入，补齐默认值并施加约束。</li>
<li>完整结果写入 <code>build/.config</code>。</li>
<li>后续同步生成 Makefile 和 C 代码需要的配置文件。</li>
</ol><p><code>menuconfig</code>、<code>nconfig</code>、<code>xconfig</code> 和 <code>gconfig</code> 共享同一套解析与求值核心，只是前端不同：</p>

<table><thead><tr><th>目标</th><th>交互方式</th></tr></thead><tbody><tr><td><code>config</code></td><td>逐行询问</td></tr><tr><td><code>menuconfig</code></td><td>ncurses 菜单</td></tr><tr><td><code>nconfig</code></td><td>另一套 ncurses 界面</td></tr><tr><td><code>xconfig</code></td><td>Qt 图形界面</td></tr><tr><td><code>oldconfig</code></td><td>读取旧配置，并逐个询问新符号</td></tr><tr><td><code>olddefconfig</code></td><td>读取旧配置，新符号直接采用默认值</td></tr><tr><td><code>alldefconfig</code></td><td>从所有默认值生成新配置</td></tr><tr><td><code>allnoconfig</code></td><td>尽量全部设为 <code>n</code>，仍受强制关系约束</td></tr><tr><td><code>savedefconfig</code></td><td>把当前配置压缩成最小 defconfig</td></tr><tr><td><code>listnewconfig</code></td><td>列出旧配置中没有的新符号</td></tr></tbody></table><p><code>menuconfig</code> 并不拥有特殊的求值能力。它只是让人类更容易编辑同一张配置图。自动化构建通常更偏爱 <code>olddefconfig</code>，因为 CI 不太擅长在蓝色菜单里按空格键（至少目前如此）。</p></section>
<section><h2>配置怎样进入 Makefile？<a href="#配置怎样进入-makefile"><span>#</span></a></h2><p>同步配置后，Linux 和 U-Boot 都会生成：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include/config/auto.conf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内容类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>CONFIG_HAS_I2C</span><span><span>=</span><span>y</span></span></div></div><div><div><div>2</div></div><div><span>CONFIG_LOG_LEVEL</span><span><span>=</span><span>3</span></span></div></div><div><div><div>3</div></div><div><span>CONFIG_SENSOR</span><span><span>=</span><span>y</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>顶层构建系统把它当作 Makefile 片段包含进来。子目录 Makefile 就可以写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_SENSOR</span><span>)</span><span><span> </span><span>+=</span><span> sensor.o</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>展开过程非常直接：</p><ul>
<li><code>CONFIG_SENSOR=y</code> 得到 <code>obj-y += sensor.o</code>，对象内建。</li>
<li><code>CONFIG_SENSOR=m</code> 得到 <code>obj-m += sensor.o</code>，Linux 把对象送入模块构建路径。</li>
<li><code>CONFIG_SENSOR</code> 未定义时得到 <code>obj- += sensor.o</code>，正常的 <code>obj-y</code> / <code>obj-m</code> 收集逻辑不会选择它。</li>
</ul><p>目录也能用同样方式裁剪：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_HAS_I2C</span><span>)</span><span><span> </span><span>+=</span><span> i2c/</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以关闭一个驱动最彻底的效果不是给整份 C 文件套上 <code>#if 0</code>，而是让它根本不进入编译输入。编译器少工作，链接器也不必期待稍后用 section garbage collection 擦屁股。</p><p>U-Boot 还要处理 SPL、TPL、VPL 等不同阶段，因此源码中常见：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_</span><span>$(</span><span>PHASE_</span><span>)</span><span>ADC</span><span>)</span><span><span> </span><span>+=</span><span> adc-uclass.o</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>PHASE_</code> 会让构建阶段选择 <code>CONFIG_ADC</code>、<code>CONFIG_SPL_ADC</code>、<code>CONFIG_TPL_ADC</code> 等对应符号。裁剪 U-Boot 时必须问清楚一个功能属于完整 U-Boot 还是早期阶段；只关 <code>CONFIG_FOO</code>，不代表 <code>CONFIG_SPL_FOO</code> 自动消失。</p><section><h3>Linux 中谁消费 <code>obj-y</code> 和 <code>obj-m</code>？<a href="#linux-中谁消费-obj-y-和-obj-m"><span>#</span></a></h3><p>上面的表达式还留下了一个问题：谁读取这些变量并把它们变成编译动作？在 Linux 6.18 中，关键入口叫 <code>scripts/Makefile.build</code>。<code>scripts/Kbuild.include</code> 先定义一段缩写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>build</span><span><span> </span><span>:=</span><span> -f </span></span><span>$(</span><span>srctree</span><span>)</span><span>/scripts/Makefile.build obj</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因此顶层 Makefile 中的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$(</span><span>Q</span><span>)$(</span><span>MAKE</span><span>)</span><span> </span><span>$(</span><span>build</span><span>)</span><span>=drivers/i2c</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>大致等价于再次调用 Make，并指定当前处理的目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>-f</span><span> </span><span>scripts/Makefile.build</span><span> </span><span>obj=drivers/i2c</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>scripts/Makefile.build</code> 是逐目录工作的通用引擎。它先清空 <code>obj-y</code>、<code>obj-m</code>、<code>lib-y</code>、编译 flags 等变量，避免环境中的同名变量混进来；然后读取 <code>include/config/auto.conf</code>、公共 Kbuild 规则、编译器规则、当前目录的 <code>Kbuild</code> 或 <code>Makefile</code>，最后再读取 <code>scripts/Makefile.lib</code>。如果同一目录同时存在 <code>Kbuild</code> 和 <code>Makefile</code>，Kbuild 基础设施优先使用 <code>Kbuild</code>。</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>顶层 Makefile</p></span></div><div><span><p>make build=目录</p></span></div><div><span><p>scripts/Makefile.build</p></span></div><div><span><p>当前目录 Kbuild 或 Makefile</p></span></div><div><span><p>obj-y 与 obj-m</p></span></div><div><span><p>built-in.a 与 modules.order</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>顶层 Makefile</p></span></div><div><span><p>make build=目录</p></span></div><div><span><p>scripts/Makefile.build</p></span></div><div><span><p>当前目录 Kbuild 或 Makefile</p></span></div><div><span><p>obj-y 与 obj-m</p></span></div><div><span><p>built-in.a 与 modules.order</p></span></div>
</div></div></div><p>读取当前目录规则后，<code>scripts/Makefile.build</code> 会展开复合对象，找出要递归进入的子目录，并把 <code>obj-y</code> 中的子目录映射为对应的 <code>built-in.a</code>。启用模块构建时，它也会沿 <code>obj-m</code> 收集模块目标并生成 <code>modules.order</code>。例如 <code>drivers/i2c/</code> 不是由顶层 Makefile 硬编码每一个源文件，而是同一套引擎带着不同的 <code>obj</code> 参数逐层下钻。</p><p>这就是本文需要抵达的 Kbuild 边界：Kconfig 求出 <code>CONFIG_FOO</code>，<code>auto.conf</code> 把值交给 Make，目录中的 Kbuild 文件把它展开成 <code>obj-y</code> 或 <code>obj-m</code>，<code>scripts/Makefile.build</code> 再把列表变成当前目录的编译和归档目标。至于 <code>.c</code> 如何变成 <code>.o</code>、多文件模块如何合并、<code>modpost</code> 怎样生成模块元数据，以及 <code>built-in.a</code> 最后如何进入 <code>vmlinux</code>，那已经是另一条足够写满一篇文章的数据流。</p><p>U-Boot 也继承了 Kbuild 风格，并维护自己的 <code>scripts/Makefile.build</code>，但它的镜像阶段、SPL/TPL/VPL 和兼容配置让后续链接路径与 Linux 不完全相同。这里借 Linux 6.18 看清通用接口，不把两套最终链接过程强行说成一回事。</p></section></section>
<section><h2>配置怎样进入 C 代码？<a href="#配置怎样进入-c-代码"><span>#</span></a></h2><p>另一份关键生成物是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include/generated/autoconf.h</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的实验配置生成了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#define</span><span> </span><span>CONFIG_LOG_LEVEL</span><span> </span><span>3</span></div></div><div><div><div>2</div></div><div><span>#define</span><span> </span><span>CONFIG_HAS_I2C</span><span> </span><span>1</span></div></div><div><div><div>3</div></div><div><span>#define</span><span> </span><span>CONFIG_APP</span><span> </span><span>1</span></div></div><div><div><div>4</div></div><div><span>#define</span><span> </span><span>CONFIG_MODULES</span><span> </span><span>1</span></div></div><div><div><div>5</div></div><div><span>#define</span><span> </span><span>CONFIG_SOC_ALPHA</span><span> </span><span>1</span></div></div><div><div><div>6</div></div><div><span>#define</span><span> </span><span>CONFIG_SENSOR_CORE</span><span> </span><span>1</span></div></div><div><div><div>7</div></div><div><span>#define</span><span> </span><span>CONFIG_SENSOR</span><span> </span><span>1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C 代码因此可以做更细粒度的选择：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#if</span><span> </span><span>IS_ENABLED</span><span>(</span><span>CONFIG_SENSOR</span><span>)</span></div></div><div><div><div>2</div></div><div><span>register_sensor</span><span>();</span></div></div><div><div><div>3</div></div><div><span>#endif</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Linux 中优先使用项目提供的 <code>IS_ENABLED()</code>、<code>IS_BUILTIN()</code>、<code>IS_MODULE()</code> 等宏，而不是把所有判断都写成裸 <code>#ifdef</code>。原因之一是模块状态：<code>CONFIG_SENSOR=m</code> 通常表现为 <code>CONFIG_SENSOR_MODULE=1</code>，并不等同于 <code>#define CONFIG_SENSOR 1</code>。<code>IS_ENABLED(CONFIG_SENSOR)</code> 能同时覆盖内建和模块两种启用状态。</p><p>对于数值和字符串，生成结果保留相应值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#define</span><span> </span><span>CONFIG_LOG_LEVEL</span><span> </span><span>3</span></div></div><div><div><div>2</div></div><div><span>#define</span><span> </span><span>CONFIG_LOCALVERSION</span><span> </span><span>"-lab"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Warning</div></div><div><p>不要手改 <code>include/config/auto.conf</code> 或 <code>include/generated/autoconf.h</code>。它们是派生文件，下一次同步就会重写。要改变配置，修改 Kconfig、defconfig 或通过受支持的配置前端修改 <code>.config</code>。</p></div></div><p>Linux 6.18 已把 <code>syncconfig</code> 标为内部实现细节。正常用户运行 <code>make</code> 或配置目标即可，顶层 Makefile 会在 <code>.config</code> 或 Kconfig 输入变化时更新这些生成物。手工调用 <code>make syncconfig</code> 通常是在绕过正确入口。</p></section>
<section><h2>U-Boot 为什么看起来还多一层？<a href="#u-boot-为什么看起来还多一层"><span>#</span></a></h2><p>U-Boot 从 v2014.10-rc1 开始用 Kconfig 替代旧的板级配置体系，但 v2026.07 仍保留一些兼容生成步骤。除了与 Linux 相同的 <code>.config</code>、<code>include/config/auto.conf</code> 和 <code>include/generated/autoconf.h</code>，它还可能生成或使用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include/config.h</span></div></div><div><div><div>2</div></div><div><span>include/autoconf.mk</span></div></div><div><div><div>3</div></div><div><span>spl/include/autoconf.mk</span></div></div><div><div><div>4</div></div><div><span>tpl/include/autoconf.mk</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>板级代码中也仍能看到 <code>include/configs/&lt;board&gt;.h</code>。因此在 U-Boot 中遇到一个 <code>CONFIG_</code> 或 <code>CFG_</code> 宏时，不能只在 Kconfig 文件里搜索一次就宣布它不存在。要区分：</p><ul>
<li>Kconfig 管理并生成的 <code>CONFIG_*</code>。</li>
<li>旧板级头文件或其他构建步骤提供的兼容配置。</li>
<li><code>CFG_*</code> 这类不一定由 Kconfig 管理的编译期常量。</li>
<li>Device Tree 描述的硬件实例与运行时数据。</li>
</ul><p>新配置应优先进入 Kconfig，而不是继续往板级头文件塞宏。旧路径存在是迁移历史，不是新代码需要复制的设计模式。</p></section>
<section><h2>裁剪一个真实功能时，我现在怎么做？<a href="#裁剪一个真实功能时我现在怎么做"><span>#</span></a></h2><p>假设我要在 U-Boot 中去掉某个以太网驱动，或者在 Linux 中加入某个 I2C 控制器。我会沿着同一条链倒着走。</p><section><h3>1. 从符号定义开始<a href="#1-从符号定义开始"><span>#</span></a></h3><p>在 <code>menuconfig</code> 中按 <code>/</code> 搜索符号名，查看：</p><ul>
<li>定义位置。</li>
<li>类型和 prompt。</li>
<li><code>depends on</code>。</li>
<li>被哪些符号 <code>select</code>。</li>
<li>当前值为何被限制。</li>
</ul><p>如果界面显示：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Symbol: FOO [=n]</span></div></div><div><div><div>2</div></div><div><span>Depends on: BAR [=n] &amp;&amp; ARCH_BAZ [=y]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>真正阻止 <code>FOO</code> 的是 <code>BAR=n</code>。反复按空格不会改变这个上限。</p></section><section><h3>2. 查 Kbuild/Makefile 消费点<a href="#2-查-kbuildmakefile-消费点"><span>#</span></a></h3><p>寻找类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>obj-</span><span>$(</span><span>CONFIG_FOO</span><span>)</span><span><span> </span><span>+=</span><span> foo.o</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>确认符号控制的是单个对象、整个目录，还是一组公共代码。有时 UI 中一个开关只控制上层框架，具体驱动还有第二个符号。</p></section><section><h3>3. 查 C 代码中的条件<a href="#3-查-c-代码中的条件"><span>#</span></a></h3><p>查看 <code>IS_ENABLED(CONFIG_FOO)</code>、<code>#ifdef CONFIG_FOO</code> 和该符号影响的数据结构。一个符号可能既控制对象是否编译，也控制已编译文件中的某段代码。</p></section><section><h3>4. 检查阶段和架构变体<a href="#4-检查阶段和架构变体"><span>#</span></a></h3><p>U-Boot 要额外查看 <code>CONFIG_SPL_FOO</code>、<code>CONFIG_TPL_FOO</code> 和 <code>CONFIG_VPL_FOO</code>。Linux 则要留意 <code>FOO=y</code> 与 <code>FOO=m</code> 的差别，以及目标架构的 Kconfig 是否为它追加了依赖或默认值。</p></section><section><h3>5. 从干净默认配置重放<a href="#5-从干净默认配置重放"><span>#</span></a></h3><p>U-Boot：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>O=build</span><span> </span><span>my_board_defconfig</span></div></div><div><div><div>2</div></div><div><span>make</span><span> </span><span>O=build</span><span> </span><span>menuconfig</span></div></div><div><div><div>3</div></div><div><span>make</span><span> </span><span>O=build</span><span> </span><span>savedefconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Linux：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>O=build</span><span> </span><span>ARCH=arm64</span><span> </span><span>my_defconfig</span></div></div><div><div><div>2</div></div><div><span>make</span><span> </span><span>O=build</span><span> </span><span>ARCH=arm64</span><span> </span><span>menuconfig</span></div></div><div><div><div>3</div></div><div><span>make</span><span> </span><span>O=build</span><span> </span><span>ARCH=arm64</span><span> </span><span>savedefconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>交叉编译时再为实际构建设置正确的 <code>CROSS_COMPILE</code>。<code>O=</code>、<code>ARCH</code> 和 <code>CROSS_COMPILE</code> 应在同一输出目录的配置与构建过程中保持一致，避免拿一份架构 A 的 <code>.config</code> 去喂架构 B。</p></section><section><h3>6. 比较语义差异，不比较整份噪声<a href="#6-比较语义差异不比较整份噪声"><span>#</span></a></h3><p>Linux 提供：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scripts/diffconfig</span><span> </span><span>old.config</span><span> </span><span>new.config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它比直接 <code>diff</code> 两份完整 <code>.config</code> 更适合查看符号变化。配置升级时还可以使用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>listnewconfig</span></div></div><div><div><div>2</div></div><div><span>make</span><span> </span><span>olddefconfig</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前者告诉我出现了哪些新问题，后者按默认值回答它们。需要审查安全、启动或尺寸影响时，我会先看 <code>listnewconfig</code>，而不是让新符号在日志里无声飘过。</p></section></section>
<section><h2>几个容易制造幽灵问题的操作<a href="#几个容易制造幽灵问题的操作"><span>#</span></a></h2><section><h3>直接编辑 <code>.config</code> 后不重新求解<a href="#直接编辑-config-后不重新求解"><span>#</span></a></h3><p>手改 <code>.config</code> 可以用于快速实验，但输入仍受依赖约束。应随后运行 <code>olddefconfig</code> 或受支持的配置目标。否则文件里写着 <code>y</code>，求值结果、生成头和实际构建可能并不支持这个愿望。</p></section><section><h3>把 <code>select</code> 当作递归安装依赖<a href="#把-select-当作递归安装依赖"><span>#</span></a></h3><p>它不会递归满足目标的依赖。警告已经很直白，修 Kconfig 关系，不要忽视警告。</p></section><section><h3>在 defconfig 中记录所有默认值<a href="#在-defconfig-中记录所有默认值"><span>#</span></a></h3><p>这样得到的是一份巨大快照。默认值改变时，它会把旧决定钉死，也让代码审查看不出板级真正选择了什么。让 <code>savedefconfig</code> 做压缩。</p></section><section><h3>只看 <code>menuconfig</code> 中是否可见<a href="#只看-menuconfig-中是否可见"><span>#</span></a></h3><p>无 prompt 的内部符号不会出现；依赖为 <code>n</code> 的条目也可能隐藏。搜索符号并查看定义，比滚动菜单可靠。</p></section><section><h3>认为 Kconfig 会自动裁剪任何东西<a href="#认为-kconfig-会自动裁剪任何东西"><span>#</span></a></h3><p>如果新增了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>config FOO</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>bool "Foo support"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>却没有在 Makefile 或 C 代码中消费 <code>CONFIG_FOO</code>，切换它不会改变二进制的一个 bit。Kconfig 只产生决定，构建规则和源码必须执行决定。</p></section></section>
<section><h2>我最终保留的心智模型<a href="#我最终保留的心智模型"><span>#</span></a></h2><p>我现在把 Kconfig 看成一个有 4 类边的符号图：</p><ul>
<li><code>depends on</code> 压低取值上限。</li>
<li><code>select</code> 强制抬高取值下限，但不替目标检查依赖。</li>
<li><code>imply</code> 提供可被用户拒绝的弱下限。</li>
<li><code>default</code> 只在用户没有作出决定时给出起点。</li>
</ul><p><code>defconfig</code> 是送进图中的稀疏输入，<code>.config</code> 是当前版本求解后的工作状态，<code>auto.conf</code> 和 <code>autoconf.h</code> 是分别交给 Make 与 C 的机器接口。随后 <code>obj-$(CONFIG_FOO)</code> 决定文件是否进场，<code>IS_ENABLED(CONFIG_FOO)</code> 决定文件内部哪段代码存活。</p><p>这套设计从 1881 份 Linux Kconfig 文件扩展到 U-Boot 的 1098 份，靠的不是某个神奇菜单，而是把配置声明、求值、构建选择和源码条件分开。未来我怀疑更多固件会把硬件描述继续推向 Device Tree，把编译期能力留给 Kconfig；但只要我们仍要为固定板卡在镜像尺寸、启动链和驱动集合之间做取舍，这张符号图就不会消失。</p><p>接下来最好的练习不是继续读语法表。选自己板子的一个驱动，从它的 <code>config FOO</code> 追到 <code>obj-$(CONFIG_FOO)</code>，再追到 <code>autoconf.h</code> 和最终镜像。追完一个真实符号，蓝色菜单就只是 UI 了。很好，我终于可以回去裁内核，而不是在 <code>menuconfig</code> 里用空格键进行嵌入式占卜。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/Documentation/kbuild/kconfig-language.rst" target="_blank">Linux 6.18 Kconfig Language</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/Documentation/kbuild/kconfig.rst" target="_blank">Linux 6.18 Configuration targets and editors</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/kconfig/Makefile" target="_blank">Linux 6.18 scripts/kconfig/Makefile</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/scripts/Makefile.build" target="_blank">Linux 6.18 scripts/Makefile.build</a></li>
<li><a href="https://github.com/torvalds/linux/blob/v6.18/Documentation/kbuild/makefiles.rst" target="_blank">Linux 6.18 Linux Kernel Makefiles</a></li>
<li><a href="https://www.kernel.org/releases.html" target="_blank">Linux Kernel active releases</a></li>
<li><a href="https://docs.u-boot.org/en/v2026.07/develop/kconfig.html" target="_blank">U-Boot v2026.07: Kconfig in U-Boot</a></li>
<li><a href="https://docs.u-boot.org/en/v2026.07/build/gcc.html" target="_blank">U-Boot v2026.07: Building with GCC</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/cpp-exception/</id>
      <title type="text">从错误码到栈展开: 我如何理解 C++ 异常</title>
      <published>2026-09-01T09:39:28.000Z</published>
      <updated>2026-09-01T09:39:28.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/cpp-exception/"/>
      <summary type="text">我从嵌入式 C 的错误码出发，用一个可运行的栈展开实验串起 C++ 异常、RAII、捕获边界、noexcept、异常安全，以及 std::expected 的适用边界。</summary>
      <content type="html"><![CDATA[<blockquote><p>“异常不是一种错误返回值。它是一种非局部的控制流。”</p></blockquote>
<p>我以前写嵌入式 C 时，错误处理基本是一条熟悉的流水线：</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> result </span><span>=</span><span> </span><span>read_sensor</span><span>(</span><span>&amp;</span><span><span>value</span><span>);</span></span></div></div><div><div><div>2</div></div><div><span>if</span><span> (result </span><span>!=</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>return</span><span> result;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>函数返回错误码，调用者检查错误码，然后继续向上传递。一路上每层都写一次 <code>if</code>，最终总有某一层真正处理它 - 或者某一层忘记检查，程序带着错误继续运行。后者当然非常有趣，只是通常不会在你方便调试的时候发生。</p>
<p>C++ 异常做的事情，本质上是把”逐层手动返回错误”改成了”自动向上寻找能够处理错误的位置”。</p>
<p>但这句话里最关键的不是”自动”，而是”能够处理”。</p>
<section><h2>异常最基本的语法<a href="#异常最基本的语法"><span>#</span></a></h2><p>抛出异常使用 <code>throw</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>double</span><span> </span><span>divide</span><span>(</span><span>double</span><span><span> </span><span>a</span><span>, </span></span><span>double</span><span><span> </span><span>b</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>if</span><span> (</span><span>b</span><span> </span><span>==</span><span> </span><span>0.0</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>throw</span><span> std::</span><span>invalid_argument</span><span>(</span><span>"division by zero"</span><span>);</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>return</span><span> </span><span>a</span><span> </span><span>/</span><span> </span><span>b</span><span>;</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调用者使用 <code>try</code> 和 <code>catch</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>try</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>double</span><span> </span><span>result</span><span> </span><span>=</span><span> </span><span>divide</span><span>(</span><span>10.0</span><span>, </span><span>0.0</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>result</span><span> </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>4</div></div><div><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>invalid_argument</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>std::</span><span>cerr</span><span> </span><span>&lt;&lt;</span><span> </span><span>"参数错误: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>error</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>几个基本规则：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>try</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>// 可能抛出异常的代码</span></div></div><div><div><div>3</div></div><div><span>} </span><span>catch</span><span> (</span><span>const</span><span> </span><span>SpecificException</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>// 处理特定异常</span></div></div><div><div><div>5</div></div><div><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>// 处理其他标准异常</span></div></div><div><div><div>7</div></div><div><span>} </span><span>catch</span><span> (...) {</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>// 处理所有其他异常</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>捕获顺序</div></div><div><p><code>catch</code> 按从上到下的顺序匹配，因此具体类型应放在前面，基类放在后面；<code>catch (...)</code> 只能放在最后。</p></div></div><p>异常一般按值抛出、按 <code>const</code> 引用捕获：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>throw</span><span> </span><span>MyException</span><span>{</span><span>"motor stalled"</span><span>};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>catch</span><span> (</span><span>const</span><span> </span><span>MyException</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>// ...</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不要按值捕获：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>catch</span><span> (std::</span><span>exception</span><span> </span><span>error</span><span>) {</span><span>  // 不推荐</span></div></div><div><div><div>2</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这可能发生对象切片，丢失派生异常中的信息。</p></section>
<section><h2>抛出以后发生了什么？<a href="#抛出以后发生了什么"><span>#</span></a></h2><p>假设调用链是：</p><div><div><div><div><span><p>调用</p></span></div><div><span><p>调用</p></span></div><div><span><p>调用</p></span></div><div><span><p>main()</p></span></div><div><span><p>initialize_system()</p></span></div><div><span><p>load_configuration()</p></span></div><div><span><p>read_file()</p></span></div>
</div><div><div><span><p>调用</p></span></div><div><span><p>调用</p></span></div><div><span><p>调用</p></span></div><div><span><p>main()</p></span></div><div><span><p>initialize_system()</p></span></div><div><span><p>load_configuration()</p></span></div><div><span><p>read_file()</p></span></div>
</div></div></div><p>如果 <code>read_file()</code> 抛出异常，它不会像错误码那样正常返回。C++ 会沿调用栈向上寻找匹配的 <code>catch</code>：</p><div><div><div><div><span><p>开始栈展开</p></span></div><div><span><p>继续传播</p></span></div><div><span><p>找到匹配的处理器</p></span></div><div><span><p>read_file(): 抛出异常</p></span></div><div><span><p>退出 load_configuration()</p></span></div><div><span><p>退出 initialize_system()</p></span></div><div><span><p>main(): catch</p></span></div>
</div><div><div><span><p>开始栈展开</p></span></div><div><span><p>继续传播</p></span></div><div><span><p>找到匹配的处理器</p></span></div><div><span><p>read_file(): 抛出异常</p></span></div><div><span><p>退出 load_configuration()</p></span></div><div><span><p>退出 initialize_system()</p></span></div><div><span><p>main(): catch</p></span></div>
</div></div></div><p>这个过程叫做栈展开，stack unwinding。</p><p>在展开过程中，已经构造完成的局部对象会被自动销毁：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>process</span><span>() {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>data</span><span>(</span><span>1000</span><span>);</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>std::</span><span>lock_guard</span><span>&lt;std::</span><span>mutex</span><span>&gt; </span><span>lock</span><span>(</span><span>mutex</span><span>);</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>  </span><span>perform_operation</span><span>();</span><span>  // 抛出异常</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>即使 <code>perform_operation()</code> 抛出异常：</p><ul>
<li><code>lock</code> 的析构函数仍然执行，互斥锁被释放；</li>
<li><code>data</code> 的析构函数仍然执行，内存被释放。</li>
</ul><div><div><div></div><div>RAII 是异常安全的地基</div></div><div><p>资源放进对象里，清理由析构函数负责，异常只负责改变控制流。不要让 <code>catch</code> 承担本应属于对象生命周期的清理工作。</p></div></div><section><h3>我跑了一个最小的栈展开实验<a href="#我跑了一个最小的栈展开实验"><span>#</span></a></h3><p>只看规则还是有点抽象，所以我写了一个 34 行的小程序，让 3 个局部对象把构造和析构顺序打印出来，然后在最深处抛出 <code>std::runtime_error</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;stdexcept&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>class</span><span> </span><span>Trace</span><span> {</span></div></div><div><div><div>6</div></div><div><span> </span><span>public:</span></div></div><div><div><div>7</div></div><div><span>  </span><span>explicit</span><span> </span><span>Trace</span><span>(std::</span><span>string_view</span><span><span> </span><span>name</span><span>) : </span></span><span>name_</span><span>(</span><span>name</span><span>) {</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"construct "</span><span> </span><span>&lt;&lt;</span><span> </span><span>name_</span><span> </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>  </span><span>~Trace</span><span>() { std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"destroy "</span><span> </span><span>&lt;&lt;</span><span> </span><span>name_</span><span> </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>; }</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span> </span><span>private:</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>std::</span><span>string_view</span><span> </span><span>name_</span><span>;</span></div></div><div><div><div>15</div></div><div><span>};</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>void</span><span> </span><span>read_file</span><span>() {</span></div></div><div><div><div>18</div></div><div><span>  </span><span>Trace</span><span> </span><span>trace</span><span>{</span><span>"read_file"</span><span>};</span></div></div><div><div><div>19</div></div><div><span>  </span><span>throw</span><span> std::</span><span>runtime_error</span><span>{</span><span>"disk error"</span><span>};</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>void</span><span> </span><span>load_configuration</span><span>() {</span></div></div><div><div><div>23</div></div><div><span>  </span><span>Trace</span><span> </span><span>trace</span><span>{</span><span>"load_configuration"</span><span>};</span></div></div><div><div><div>24</div></div><div><span>  </span><span>read_file</span><span>();</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>28</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>29</div></div><div><span>    </span><span>Trace</span><span> </span><span>trace</span><span>{</span><span>"main"</span><span>};</span></div></div><div><div><div>30</div></div><div><span>    </span><span>load_configuration</span><span>();</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>runtime_error</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"catch runtime_error: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>error</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我用 GCC 15.3 和 <code>-std=c++23 -Wall -Wextra -Wpedantic -Werror</code> 编译，实际输出是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>construct</span><span> </span><span>main</span></div></div><div><div><div>2</div></div><div><span>construct</span><span> </span><span>load_configuration</span></div></div><div><div><div>3</div></div><div><span>construct</span><span> </span><span>read_file</span></div></div><div><div><div>4</div></div><div><span>destroy</span><span> </span><span>read_file</span></div></div><div><div><div>5</div></div><div><span>destroy</span><span> </span><span>load_configuration</span></div></div><div><div><div>6</div></div><div><span>destroy</span><span> </span><span>main</span></div></div><div><div><div>7</div></div><div><span>catch</span><span> </span><span>runtime_error:</span><span> </span><span>disk</span><span> </span><span>error</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>3 个对象按构造的严格逆序销毁，最后才进入 1 个匹配的处理器。异常像按下了调用栈的倒带键：控制流向上走，析构函数沿路把资源收回来。若把同一条 3 层调用链改成 C 风格错误码，每一层都要检查并继续返回；这里中间两层没有错误转发代码，只保留了真正处理错误的边界。</p><p>因此，现代 C++ 中通常不应该这样清理资源：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Resource</span><span>*</span><span> </span><span>resource</span><span> </span><span>=</span><span> </span><span>acquire</span><span>();</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>try</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>use</span><span>(</span><span>resource</span><span>);</span></div></div><div><div><div>5</div></div><div><span>} </span><span>catch</span><span> (...) {</span></div></div><div><div><div>6</div></div><div><span>  </span><span>release</span><span>(</span><span>resource</span><span>);</span></div></div><div><div><div>7</div></div><div><span>  </span><span>throw</span><span>;</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>release</span><span>(</span><span>resource</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>应该把资源包装成对象：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>auto</span><span> </span><span>resource</span><span> </span><span>=</span><span> </span><span>make_resource</span><span>();</span></div></div><div><div><div>2</div></div><div><span>use</span><span>(</span><span>resource</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>资源对象的析构函数负责清理。这样正常返回、提前返回和抛出异常都走同一套清理逻辑。</p></section></section>
<section><h2><code>throw;</code> 和 <code>throw e;</code> 不一样<a href="#throw-和-throw-e-不一样"><span>#</span></a></h2><p>捕获后重新抛出当前异常：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>log_error</span><span>(</span><span>error</span><span>.</span><span>what</span><span>());</span></div></div><div><div><div>3</div></div><div><span>  </span><span>throw</span><span>;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里应当使用没有参数的 <code>throw;</code>。它重新抛出当前正在处理的异常，保留原始异常对象及其动态类型。</p><p>不要写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>throw</span><span> </span><span>error</span><span>;</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这会用表达式 <code>e</code> 的静态类型 <code>std::exception</code> 初始化一个新的异常对象，因此派生部分会被切掉。</p></section>
<section><h2>自定义异常<a href="#自定义异常"><span>#</span></a></h2><p>自定义异常通常继承 <code>std::runtime_error</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>ConfigurationError</span><span> : </span><span>public</span><span> std::</span><span>runtime_error</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>using</span><span> std::runtime_error::</span><span>runtime_error</span><span>;</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>使用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>load_configuration</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>configuration_exists</span><span>()) {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>throw</span><span> </span><span>ConfigurationError</span><span>{</span><span>"configuration file is missing"</span><span>};</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>捕获：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>catch</span><span> (</span><span>const</span><span> </span><span>ConfigurationError</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>std::</span><span>cerr</span><span> </span><span>&lt;&lt;</span><span> </span><span>"配置错误: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>error</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不要为每一种小错误都创建一个异常类。异常类型的价值在于让调用者能够采取不同措施，而不仅仅是让类名看起来很完整。</p><p>如果两个错误的处理策略完全一样，它们很可能不需要两个异常类型。</p></section>
<section><h2>到底什么时候应该抛异常？<a href="#到底什么时候应该抛异常"><span>#</span></a></h2><p>我认为最有用的判断标准是：</p><div><div><div></div><div>判断是否应该抛出</div></div><div><p>当前函数无法完成它承诺的操作，而且调用者不太可能在紧邻的位置用普通分支处理这个失败，此时异常通常是自然的接口。</p></div></div><p>例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Configuration</span><span> </span><span>load_configuration</span><span>(</span><span>const</span><span> std::filesystem::</span><span>path</span><span>&amp;</span><span><span> </span><span>path</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个函数承诺返回一个有效配置。如果文件损坏，函数无法兑现承诺。此时抛异常很自然：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Configuration</span><span> </span><span>load_configuration</span><span>(</span><span>const</span><span> std::filesystem::</span><span>path</span><span>&amp;</span><span><span> </span><span>path</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>std::</span><span>ifstream</span><span> </span><span>input</span><span>(</span><span>path</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>input</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>throw</span><span> </span><span>ConfigurationError</span><span>{</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"cannot open configuration: "</span><span> </span><span>+</span><span> </span><span>path</span><span>.</span><span>string</span><span>()};</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>  </span><span>return</span><span> </span><span>parse_configuration</span><span>(</span><span>input</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>特别适合异常的情况包括：</p><ul>
<li>构造函数无法建立对象不变量；</li>
<li>初始化过程失败，程序无法继续；</li>
<li>文件、数据库或系统资源出现罕见故障；</li>
<li>某个深层函数无法处理错误，需要跨越多层调用栈；</li>
<li>函数通常成功，而失败属于异常路径。</li>
</ul><p>构造函数尤其适合异常，因为构造函数没有返回值可以装错误码：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>SerialPort</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>explicit</span><span> </span><span>SerialPort</span><span>(</span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>device</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>// 约定 open_device 成功时返回空 error_code。</span></div></div><div><div><div>5</div></div><div><span>    </span><span>if</span><span> (</span><span>const</span><span> std::</span><span>error_code</span><span> </span><span>error</span><span> </span><span>=</span><span> </span><span>open_device</span><span>(</span><span>device</span><span>)) {</span></div></div><div><div><div>6</div></div><div><span>      </span><span>throw</span><span> std::</span><span>system_error</span><span>{</span><span>error</span><span>,</span></div></div><div><div><div>7</div></div><div><span>                              </span><span>"cannot open serial port "</span><span> </span><span>+</span><span> </span><span>device</span><span>};</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>要么构造出一个有效的 <code>SerialPort</code>，要么构造失败并抛出。不会产生一个”半死不活、需要调用 <code>is_valid()</code> 才敢使用”的对象。</p></section>
<section><h2>什么情况不应该抛异常？<a href="#什么情况不应该抛异常"><span>#</span></a></h2><p>异常不适合表达正常、频繁、可预期的业务分支。</p><p>例如查找元素：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>User</span><span>*</span><span> </span><span>find_user</span><span>(</span><span>UserId</span><span><span> </span><span>id</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>“没有找到用户”可能完全正常，因此返回空指针或 <code>std::optional</code> 更合适：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>optional</span><span>&lt;</span><span>User</span><span>&gt; </span><span>find_user</span><span>(</span><span>UserId</span><span><span> </span><span>id</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>类似情况还有：</p><ul>
<li>队列当前为空；</li>
<li>非阻塞读取暂时没有数据；</li>
<li>用户输入不符合格式；</li>
<li>网络操作需要重试；</li>
<li>传感器偶尔超时，而超时是协议的一部分；</li>
<li>解析器试探某种语法但没有匹配；</li>
<li>高频控制循环中的状态变化。</li>
</ul><p>在 C++23 中，还可以使用 <code>std::expected&lt;T, E&gt;</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>enum</span><span> </span><span>class</span><span> </span><span>SensorError</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>kTimeout</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>kDisconnected</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>kChecksumError</span><span>,</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>std::</span><span>expected</span><span>&lt;</span><span>Reading</span><span>, </span><span>SensorError</span><span>&gt; </span><span>read_sensor</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>调用者显式处理：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>auto</span><span> </span><span>result</span><span> </span><span>=</span><span> </span><span>read_sensor</span><span>();</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>if</span><span> (</span><span>!</span><span>result</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span> (</span><span>result</span><span>.</span><span>error</span><span>() </span><span>==</span><span> SensorError::</span><span>kTimeout</span><span>) {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>retry</span><span>();</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>  </span><span>return</span><span>;</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>process</span><span>(</span><span>*</span><span>result</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我通常这样区分：</p>

<table><thead><tr><th>情况</th><th>更合适的机制</th></tr></thead><tbody><tr><td>调用者经常立即处理失败</td><td><code>optional</code>、<code>expected</code>、错误码</td></tr><tr><td>失败罕见，需要跨越多层调用</td><td>异常</td></tr><tr><td>硬实时路径，需要可预测延迟</td><td>错误码或 <code>expected</code></td></tr><tr><td>构造函数无法建立有效对象</td><td>异常</td></tr><tr><td>查询”有没有”</td><td><code>optional</code></td></tr><tr><td>可恢复的输入错误或状态不匹配</td><td><code>expected</code>、错误码</td></tr><tr><td>编程错误或不变量被破坏</td><td>断言、契约检查或终止</td></tr><tr><td>内存不足、文件系统故障等罕见故障</td><td>通常可以使用异常</td></tr></tbody></table><p>异常不是”更高级的错误码”。它们适合不同形状的控制流。</p></section>
<section><h2>应该在哪里捕获？<a href="#应该在哪里捕获"><span>#</span></a></h2><p>这是异常设计中最重要的问题。答案不是”尽量靠近抛出点”，而是：</p><blockquote><p>在能够采取有意义行动的层级捕获。</p></blockquote><p>所谓有意义行动，大致只有几种：</p><ul>
<li>重试操作；</li>
<li>使用备用方案；</li>
<li>请求用户提供新的输入；</li>
<li>把底层错误翻译成当前层的领域错误；</li>
<li>回滚当前事务；</li>
<li>结束当前请求，但保持整个服务继续运行；</li>
<li>在程序最外层记录错误并安全退出。</li>
</ul><p>如果某一层什么都做不了，就不应该为了”看起来处理了错误”而捕获。</p><p>这种代码几乎没有价值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>try</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>load_configuration</span><span>();</span></div></div><div><div><div>3</div></div><div><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>throw</span><span>;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它只是把异常接住，然后原样扔回去。除非这里需要补充上下文，否则让异常自然传播即可。</p><section><h3>一个实际的分层例子<a href="#一个实际的分层例子"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>string</span><span> </span><span>read_file</span><span>(</span><span>const</span><span> std::filesystem::</span><span>path</span><span>&amp;</span><span><span> </span><span>path</span><span>);</span></span></div></div><div><div><div>2</div></div><div><span>Configuration</span><span> </span><span>load_configuration</span><span>(</span><span>const</span><span> std::filesystem::</span><span>path</span><span>&amp;</span><span><span> </span><span>path</span><span>);</span></span></div></div><div><div><div>3</div></div><div><span>void</span><span> </span><span>initialize_application</span><span>();</span></div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>底层文件函数报告它真正知道的事情：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>string</span><span> </span><span>read_file</span><span>(</span><span>const</span><span> std::filesystem::</span><span>path</span><span>&amp;</span><span><span> </span><span>path</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>std::</span><span>ifstream</span><span> </span><span>input</span><span>(</span><span>path</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>input</span><span>) {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>// 不从 errno 猜测 iostream 失败的原因。</span></div></div><div><div><div>6</div></div><div><span>    </span><span>throw</span><span> std::ios_base::</span><span>failure</span><span>{</span><span>"cannot open "</span><span> </span><span>+</span><span> </span><span>path</span><span>.</span><span>string</span><span>()};</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>  </span><span>return</span><span> {std::</span><span>istreambuf_iterator</span><span>&lt;char&gt;</span><span>{</span><span>input</span><span>},</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>std::</span><span>istreambuf_iterator</span><span>&lt;char&gt;</span><span>{}};</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>配置层知道这个文件对业务意味着什么，因此可以翻译异常：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Configuration</span><span> </span><span>load_configuration</span><span>(</span><span>const</span><span> std::filesystem::</span><span>path</span><span>&amp;</span><span><span> </span><span>path</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>return</span><span> </span><span>parse_configuration</span><span>(</span><span>read_file</span><span>(</span><span>path</span><span>));</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>system_error</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>std::</span><span>throw_with_nested</span><span>(</span><span>ConfigurationError</span><span>{</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"failed to load application configuration"</span><span>});</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最外层决定如何向用户呈现：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>auto</span><span> </span><span>config</span><span> </span><span>=</span><span> </span><span>load_configuration</span><span>(</span><span>"app.conf"</span><span>);</span></div></div><div><div><div>4</div></div><div><span>    </span><span>run_application</span><span>(</span><span>config</span><span>);</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> </span><span>EXIT_SUCCESS</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> </span><span>ConfigurationError</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>std::</span><span>cerr</span><span> </span><span>&lt;&lt;</span><span> </span><span>"程序无法启动: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>error</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span> </span><span>EXIT_FAILURE</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>std::</span><span>cerr</span><span> </span><span>&lt;&lt;</span><span> </span><span>"未处理的错误: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>error</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> </span><span>EXIT_FAILURE</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意这里的职责分工：</p><ul>
<li>底层发现并描述技术故障；</li>
<li>中间层在必要时翻译语义；</li>
<li>顶层决定程序是重试、降级还是退出。</li>
</ul><p>这有点像消防报警：传感器负责报告烟雾，楼层控制器负责判断区域，最终由消防系统决定疏散。让烟雾传感器自己决定是否撤离整栋楼，职责多少有点超纲。</p></section></section>
<section><h2>捕获异常不等于”解决异常”<a href="#捕获异常不等于解决异常"><span>#</span></a></h2><p>这是一个非常常见的困惑。</p><p>假设串口打开失败：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>throw</span><span> </span><span>SerialPortError</span><span>{</span><span>"COM3 is unavailable"</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>捕获以后，你不一定能”修好 COM3”。处理异常可能只是：</p><ul>
<li>尝试另一个串口；</li>
<li>等待一秒后重试；</li>
<li>提示用户检查连接；</li>
<li>禁用相关功能；</li>
<li>记录错误并结束程序。</li>
</ul><p>“处理”指的是把系统带到一个定义良好的状态，而不是必须让原操作成功。</p><p>如果当前层做不到这一点，就继续传播。</p></section>
<section><h2><code>try</code> 块中的代码是不是应该尽量少？<a href="#try-块中的代码是不是应该尽量少"><span>#</span></a></h2><p>不是因为 <code>try</code> 天生按行收费，而是因为语义。</p><p>在 GCC、Clang 和 MSVC 常见的表驱动异常实现中，未抛出异常的正常路径通常不需要像错误码那样在每一层增加显式分支。代价主要转移到了异常处理元数据、二进制体积和真正抛出时的工作；这是一种常见实现策略，不是 C++ 标准给出的性能保证。</p><div><div><div></div><div>性能模型</div></div><div><ul>
<li><strong>没有抛出</strong>：常见表驱动实现中的路径成本通常很低，但异常处理元数据和二进制体积并非免费。</li>
<li><strong>真正抛出</strong>：运行时需要构造异常对象、查找处理器、展开调用栈并执行析构函数，成本较高且延迟不容易预测。</li>
</ul></div></div><p><code>try</code> 里面有十行还是一百行，并不是主要性能因素。</p><p>不过，过大的 <code>try</code> 块会造成另一个问题：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>try</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>load_config</span><span>();</span></div></div><div><div><div>3</div></div><div><span>  </span><span>connect_database</span><span>();</span></div></div><div><div><div>4</div></div><div><span>  </span><span>start_network</span><span>();</span></div></div><div><div><div>5</div></div><div><span>  </span><span>initialize_ui</span><span>();</span></div></div><div><div><div>6</div></div><div><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>// 到底哪一步失败了？</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里捕获得太宽，错误上下文容易变模糊。可以让各层的异常信息足够明确，或者按恢复策略划分边界。</p><p>所以更准确的原则是：</p><div><div><div></div><div>划定 <code>try</code> 的边界</div></div><div><p><code>try</code> 块应当覆盖共享同一种恢复策略的操作，而不是机械地追求行数少。</p></div></div></section>
<section><h2>嵌入式项目为什么经常禁用异常？<a href="#嵌入式项目为什么经常禁用异常"><span>#</span></a></h2><p>在嵌入式和硬实时系统里，避免异常往往是合理的。原因可能包括：</p><ul>
<li>栈展开延迟难以严格界定；</li>
<li>异常处理会增加代码体积和展开表；</li>
<li>工具链或运行库支持不完整；</li>
<li>项目禁止动态内存分配；</li>
<li>中断服务函数不能允许非局部控制流；</li>
<li>安全规范要求所有控制路径显式可分析；</li>
<li>项目直接使用了 <code>-fno-exceptions</code>。</li>
</ul><div><div><div></div><div>硬实时路径</div></div><div><p>平均速度在硬实时控制循环里意义不大，最坏情况延迟才是约束。异常展开的最坏延迟通常难以严格界定，因此这里更适合错误码或 <code>std::expected</code>。</p></div></div><p>因此我不会提出”现代 C++ 就应该全面使用异常”这种宗教口号。对于嵌入式代码，下面这种组合通常很实际：</p><ul>
<li>中断、驱动、实时循环：错误码或 <code>expected</code>；</li>
<li>上层非实时业务逻辑：根据项目约束决定是否允许异常；</li>
<li>初始化阶段：如果允许异常，异常往往很好用；</li>
<li>跨 C ABI 边界：绝不让异常逃出；</li>
<li>析构函数：通常不得抛出异常。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>extern</span><span> </span><span>"C"</span><span> </span><span>int</span><span> </span><span>initialize_device</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>initialize_cpp_subsystem</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>log_error</span><span>(</span><span>error</span><span>.</span><span>what</span><span>());</span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>异常在 C++ 边界内结束，转换为 C 能理解的错误码。</p></section>
<section><h2><code>noexcept</code> 是一份承诺<a href="#noexcept-是一份承诺"><span>#</span></a></h2><p>可以声明某个函数不允许异常逃出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>stop_motor</span><span>() </span><span>noexcept</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><code>noexcept</code> 是接口承诺</div></div><div><p>如果异常从 <code>noexcept</code> 函数逃出，程序会调用 <code>std::terminate()</code>，不会继续寻找外层 <code>catch</code>。不要为了性能随手添加 <code>noexcept</code>；它表示函数绝不会通过异常报告失败。</p></div></div><p>析构函数通常隐式为 <code>noexcept</code>；但如果它的基类或成员析构函数可能抛出，这个结论也会跟着改变。如果异常从非抛出的析构函数逃出，程序会调用 <code>std::terminate()</code>；若此时本来就在栈展开，结果同样是直接终止。</p><p>所以析构函数负责释放资源，但不应该通过异常报告清理失败：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Connection::</span><span>~</span><span>Connection</span><span>() </span><span>noexcept</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>close_without_throwing</span><span>();</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>异常安全比”有没有 catch”更重要<a href="#异常安全比有没有-catch更重要"><span>#</span></a></h2><p>一个函数抛出异常后，对象和程序应该处于什么状态？</p><p>C++ 通常讨论三种保证：</p><ol>
<li>基本保证：没有资源泄漏，对象仍然有效，但状态可能改变。</li>
<li>强保证：操作失败后，状态和调用前完全一致，类似事务回滚。</li>
<li>不抛保证：函数保证不抛异常。</li>
</ol><p>例如，先构造新状态，成功后再替换旧状态：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span><span> </span><span>Document</span><span>::</span></span><span>set_content</span><span>(std::</span><span>string</span><span><span> </span><span>new_content</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>validate</span><span>(</span><span>new_content</span><span>);</span><span>       // 可能抛出，尚未修改原对象</span></div></div><div><div><div>3</div></div><div><span>  </span><span>content_</span><span>.</span><span>swap</span><span>(</span><span>new_content</span><span>);</span><span>  // C++17 起 std::string::swap 为 noexcept</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>验证失败时，原内容没有被修改；交换成功后，旧内容由参数 <code>new_content</code> 在离开函数时销毁。这里给出强异常保证，而不是仅仅”接近”它。</p><p>相比之下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>void</span><span> </span><span>update</span><span>() {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>state_</span><span>.</span><span>clear</span><span>();</span></div></div><div><div><div>3</div></div><div><span>  </span><span>state_</span><span>.</span><span>load</span><span>();</span><span>  // 如果这里抛出，旧状态已经丢失</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里即使外层正确捕获异常，对象也可能已经处于不理想的状态。</p><p>所以异常设计的真正难点不是 <code>try/catch</code> 语法，而是：</p><div><div><div></div><div>异常安全的核心问题</div></div><div><p>操作在任意一步失败时，系统是否仍然保持有效状态？<code>catch</code> 只能决定后续控制流，无法自动修复已经破坏的对象不变量。</p></div></div></section>
<section><h2>我最终会采用的规则<a href="#我最终会采用的规则"><span>#</span></a></h2><p>如果我要从嵌入式 C 迁移到现代 C++，我会先使用下面这套相对保守的规则：</p><ul>
<li>预期内、频繁发生、调用者立即处理的失败，使用 <code>std::expected</code>、<code>std::optional</code> 或错误码。</li>
<li>罕见且无法在本地处理、需要跨越多层调用的失败，可以抛异常。</li>
<li>构造函数无法建立有效对象时，抛异常。</li>
<li>只在能够重试、降级、翻译、回滚或终止当前任务的边界捕获。</li>
<li>不要每层都捕获并重新抛出。</li>
<li>异常按值抛出，按 <code>const</code> 引用捕获。</li>
<li>重新抛出使用 <code>throw;</code>。</li>
<li>用 RAII 管理资源，不要依赖 <code>catch</code> 手动释放。</li>
<li>析构函数、C 接口和实时路径不允许异常逃出。</li>
<li>不用异常表示普通分支，也不用错误码掩盖真正无法继续的故障。</li>
<li>不担心 <code>try</code> 块里有几行代码；担心的是异常是否频繁发生，以及恢复边界是否清晰。</li>
</ul><p>最简洁的判断方式可能是：</p><div><div><div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span><p>能</p></span></div><div><span><p>不能</p></span></div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span><p>失败是否是正常结果?</p></span></div><div><span><p>使用 optional / expected / 错误码</p></span></div><div><span><p>当前函数能处理吗?</p></span></div><div><span><p>本地处理</p></span></div><div><span><p>是否需要跨越多层调用栈?</p></span></div><div><span><p>抛出异常</p></span></div><div><span><p>优先考虑 expected</p></span></div>
</div><div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span><p>能</p></span></div><div><span><p>不能</p></span></div><div><span><p>是</p></span></div><div><span><p>否</p></span></div><div><span><p>失败是否是正常结果?</p></span></div><div><span><p>使用 optional / expected / 错误码</p></span></div><div><span><p>当前函数能处理吗?</p></span></div><div><span><p>本地处理</p></span></div><div><span><p>是否需要跨越多层调用栈?</p></span></div><div><span><p>抛出异常</p></span></div><div><span><p>优先考虑 expected</p></span></div>
</div></div></div><p>异常最适合的角色不是代替所有错误码，而是处理那些”这里已经无法继续，但更高层仍然知道该怎么办”的失败。</p><p>底层负责准确报告，中层负责维持不变量，边界层负责恢复或终止。至于每个函数都写一个 <code>try/catch</code> - 大可不必，那只是把错误码时代的 <code>if (error)</code> 换了一件稍微昂贵一点的外套。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/grep-ripgrep-search-guide/</id>
      <title type="text">我终于学会了 grep，然后换成了 ripgrep</title>
      <published>2026-08-31T07:00:00.000Z</published>
      <updated>2026-08-31T07:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/grep-ripgrep-search-guide/"/>
      <summary type="text">我一直在复制 grep 命令，却没有真正理解它。这次我从单个日志文件开始，弄清模式、正则、管道和退出状态，再把同一套搜索迁移到更适合代码仓库的 ripgrep，并做了一次 126 MiB 的实测。</summary>
      <content type="html"><![CDATA[<p>我每天都能看到 <code>grep</code>：查日志时有它，读脚本时有它，甚至 AI 给出的命令里也经常有它。但我对它的理解长期停留在 “把一段神秘字符粘到管道后面”。这多少有点尴尬，所以我建了一个很小的实验目录，从一份 5 行日志开始，最后又让 <code>grep</code> 和 <code>ripgrep</code> 扫了 4096 个文件。搜索工具嘛，最好还是搜点东西再谈。</p>
<p><code>grep</code> 的名字来自早期 Unix 编辑器 <code>ed</code> 中的命令 <code>g/re/p</code>，大意是全局寻找正则表达式并打印匹配行。名字已经半个世纪了，但它今天仍然在做同一件事：<strong>读取文本，寻找匹配模式，输出匹配的行。</strong> 它像一把文本世界里的筛子，标准输入或文件从上面倒进去，符合模式的行从下面落出来。这个比喻到这里就够了，后面我们直接看命令。</p>
<section><h2>先做一份可以搜索的日志<a href="#先做一份可以搜索的日志"><span>#</span></a></h2><p>我创建了一个 <code>app.log</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>INFO boot</span></div></div><div><div><div>2</div></div><div><span>WARN cache slow</span></div></div><div><div><div>3</div></div><div><span>ERROR database timeout</span></div></div><div><div><div>4</div></div><div><span>INFO retry</span></div></div><div><div><div>5</div></div><div><span>ERROR recovered</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最小的 <code>grep</code> 由模式和文件组成：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>'ERROR'</span><span> </span><span>app.log</span></div></div><div><div><div>2</div></div><div><span>ERROR</span><span> </span><span>database</span><span> </span><span>timeout</span></div></div><div><div><div>3</div></div><div><span>ERROR</span><span> </span><span>recovered</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它的通用形状是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>grep</span><span> [选项] </span><span>'模式'</span><span> [文件...]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我现在会默认用单引号包住模式。不是 <code>grep</code> 强制要求，而是 shell 也认识 <code>*</code>、<code>$</code>、<code>[</code> 等字符。加引号等于先告诉 shell “这段别碰，完整交给 grep”，可以少掉一类很无聊的错误。</p></section>
<section><h2>grep 到底匹配什么<a href="#grep-到底匹配什么"><span>#</span></a></h2><p><code>grep</code> 默认按<strong>行</strong>工作。只要模式匹配一行中的任意部分，它就输出整行，而不是只输出命中的几个字符。根据 <a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/grep.html" target="_blank">POSIX grep 规范</a>，默认模式是基本正则表达式，也就是 BRE (Basic Regular Expression)。</p><p>先看几个我最常用的模式：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 以 ERROR 开头</span></div></div><div><div><div>2</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>'^ERROR'</span><span> </span><span>app.log</span></div></div><div><div><div>3</div></div><div><span>ERROR</span><span> </span><span>database</span><span> </span><span>timeout</span></div></div><div><div><div>4</div></div><div><span>ERROR</span><span> </span><span>recovered</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span># 以 timeout 结尾</span></div></div><div><div><div>7</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>'timeout$'</span><span> </span><span>app.log</span></div></div><div><div><div>8</div></div><div><span>ERROR</span><span> </span><span>database</span><span> </span><span>timeout</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span># 匹配 WARN 或 ERROR，-E 启用扩展正则表达式</span></div></div><div><div><div>11</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>-E</span><span> </span><span>'WARN|ERROR'</span><span> </span><span>app.log</span></div></div><div><div><div>12</div></div><div><span>WARN</span><span> </span><span>cache</span><span> </span><span>slow</span></div></div><div><div><div>13</div></div><div><span>ERROR</span><span> </span><span>database</span><span> </span><span>timeout</span></div></div><div><div><div>14</div></div><div><span>ERROR</span><span> </span><span>recovered</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>^</code> 锚定行首，<code>$</code> 锚定行尾，<code>|</code> 表示二选一。这里最容易踩的坑是：默认 BRE 和 <code>grep -E</code> 使用的 ERE (Extended Regular Expression) 语法并不完全一样。我如果需要 <code>|</code>、<code>+</code>、<code>?</code> 或分组，通常直接写 <code>grep -E</code>，而不是开始和反斜杠进行某种古老仪式。</p><p>如果我根本不需要正则表达式，只想搜索字面文本，就用 <code>-F</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>-F</span><span> </span><span>'ERROR database'</span><span> </span><span>app.log</span></div></div><div><div><div>2</div></div><div><span>ERROR</span><span> </span><span>database</span><span> </span><span>timeout</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这在搜索包含 <code>.</code>、<code>[</code>、<code>*</code> 的配置或代码时尤其省心。<code>-F</code> 的意思不是 “更弱的 grep”，而是 “不要解释特殊字符”。</p></section>
<section><h2>我真正会记住的选项<a href="#我真正会记住的选项"><span>#</span></a></h2><p><code>grep --help</code> 很长，但日常使用不需要背完整本手册。下面这组已经覆盖了我大部分需求：</p>

<table><thead><tr><th>选项</th><th>作用</th><th>示例</th></tr></thead><tbody><tr><td><code>-n</code></td><td>显示行号</td><td><code>grep -n 'ERROR' app.log</code></td></tr><tr><td><code>-i</code></td><td>忽略大小写</td><td><code>grep -i 'error' app.log</code></td></tr><tr><td><code>-v</code></td><td>反选不匹配的行</td><td><code>grep -v '^INFO' app.log</code></td></tr><tr><td><code>-c</code></td><td>统计匹配行数</td><td><code>grep -c 'ERROR' app.log</code></td></tr><tr><td><code>-l</code></td><td>只输出包含匹配的文件名</td><td><code>grep -l 'ERROR' *.log</code></td></tr><tr><td><code>-w</code></td><td>匹配完整单词</td><td><code>grep -w 'ERROR' app.log</code></td></tr><tr><td><code>-x</code></td><td>匹配完整一行</td><td><code>grep -x 'INFO retry' app.log</code></td></tr><tr><td><code>-A 2</code></td><td>输出命中行及后 2 行</td><td><code>grep -A 2 'timeout' app.log</code></td></tr><tr><td><code>-B 2</code></td><td>输出命中行及前 2 行</td><td><code>grep -B 2 'timeout' app.log</code></td></tr><tr><td><code>-C 2</code></td><td>输出命中行前后各 2 行</td><td><code>grep -C 2 'timeout' app.log</code></td></tr><tr><td><code>-E</code></td><td>使用扩展正则表达式</td><td>`grep -E ‘WARN</td></tr><tr><td><code>-F</code></td><td>按字面字符串匹配</td><td><code>grep -F 'a[b]' file.txt</code></td></tr></tbody></table><p>例如带上行号后，我的 5 行日志给出：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>-n</span><span> </span><span>-E</span><span> </span><span>'WARN|ERROR'</span><span> </span><span>app.log</span></div></div><div><div><div>2</div></div><div><span>2:WARN</span><span> </span><span>cache</span><span> </span><span>slow</span></div></div><div><div><div>3</div></div><div><span>3:ERROR</span><span> </span><span>database</span><span> </span><span>timeout</span></div></div><div><div><div>4</div></div><div><span>5:ERROR</span><span> </span><span>recovered</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>统计也确实是 2 行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>-c</span><span> </span><span>'ERROR'</span><span> </span><span>app.log</span></div></div><div><div><div>2</div></div><div><span>2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意，<code>-c</code> 统计的是<strong>匹配行数</strong>，不是一行内出现了多少次模式。这个区别在脚本里很容易偷偷制造 bug。</p></section>
<section><h2>grep 为什么总出现在管道里<a href="#grep-为什么总出现在管道里"><span>#</span></a></h2><p>如果没有文件参数，<code>grep</code> 就从标准输入读取。于是任何能输出文本的命令都能接到它前面：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 查找进程列表中的 ssh</span></div></div><div><div><div>2</div></div><div><span>ps</span><span> </span><span>aux</span><span> | </span><span>grep</span><span> </span><span>'[s]sh'</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 只保留日志中的错误</span></div></div><div><div><div>5</div></div><div><span>journalctl</span><span> </span><span>-u</span><span> </span><span>nginx</span><span> | </span><span>grep</span><span> </span><span>-i</span><span> </span><span>'error'</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 在命令输出中寻找某个配置项</span></div></div><div><div><div>8</div></div><div><span>some-command</span><span> </span><span>--verbose</span><span> | </span><span>grep</span><span> </span><span>-F</span><span> </span><span>'cache_dir='</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一条里的 <code>[s]sh</code> 是个老而实用的小技巧：它能匹配 <code>ssh</code>，但 <code>grep</code> 自己的命令行中出现的是 <code>[s]sh</code>，因此不会把自己也匹配出来。不过，如果系统提供 <code>pgrep</code>，按进程名或属性找进程通常更直接。能用结构化工具时，不必强迫文本筛子兼职数据库。</p><p><code>grep</code> 还通过退出状态告诉脚本发生了什么：</p><ul>
<li><code>0</code>：至少匹配到一行。</li>
<li><code>1</code>：没有匹配。</li>
<li>大于 <code>1</code>：发生错误。</li>
</ul><p>所以检查是否存在时，我会使用 <code>-q</code>，而不是先输出再扔掉：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span> </span><span>grep</span><span> </span><span>-q</span><span> </span><span>'ERROR'</span><span> </span><span>app.log</span><span>; </span><span>then</span></div></div><div><div><div>2</div></div><div><span>    </span><span>echo</span><span> </span><span>'found an error'</span></div></div><div><div><div>3</div></div><div><span>fi</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>“没有匹配” 是正常结果，不是工具坏了。理解这 3 种状态后，<code>grep</code> 才真正从交互命令变成了可靠的脚本部件。</p></section>
<section><h2>从单个文件走进整个目录<a href="#从单个文件走进整个目录"><span>#</span></a></h2><p>GNU grep 和很多现代实现支持递归搜索：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>grep</span><span> </span><span>-R</span><span> </span><span>-n</span><span> </span><span>--include=</span><span>'*.ts'</span><span> </span><span>--exclude-dir=node_modules</span><span> </span><span>'TODO'</span><span> </span><span>src</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里 <code>-R</code> 递归遍历目录，<code>--include</code> 只保留 TypeScript 文件，<code>--exclude-dir</code> 排除依赖目录。它能工作，但我必须逐项告诉 <code>grep</code> 什么值得搜索。项目一大，命令就开始长出越来越多的过滤条件。</p><p>这也是我开始理解 <code>ripgrep</code> 的位置。它并不是发明了另一种 “找字符串” 的方式，而是把<strong>递归搜索代码仓库</strong>设成了默认场景。</p><div><div><div></div><div>Note</div></div><div><p><code>grep</code> 存在 POSIX、GNU、BSD 以及其他实现，不同系统的扩展选项可能不同。<code>-E</code>、<code>-F</code>、<code>-i</code>、<code>-n</code>、<code>-q</code>、<code>-v</code> 等核心选项有 POSIX 定义；<code>-R</code>、<code>--include</code> 这样的能力应以本机 <code>man grep</code> 或 <code>grep --help</code> 为准。写可移植脚本时，我不会假设所有机器都运行 GNU grep。</p></div></div></section>
<section><h2>ripgrep：把代码仓库当成默认环境<a href="#ripgrep把代码仓库当成默认环境"><span>#</span></a></h2><p><code>ripgrep</code> 的可执行文件名是 <code>rg</code>。它同样逐行搜索模式，但直接递归搜索当前目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># grep 需要显式递归和目录</span></div></div><div><div><div>2</div></div><div><span>grep</span><span> </span><span>-R</span><span> </span><span>-n</span><span> </span><span>'TODO'</span><span> </span><span>.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># rg 默认递归搜索当前目录</span></div></div><div><div><div>5</div></div><div><span>rg</span><span> </span><span>'TODO'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 Ubuntu 或 Debian、Fedora、Arch Linux 和 macOS 上，可以分别安装：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>apt</span><span> </span><span>install</span><span> </span><span>ripgrep</span></div></div><div><div><div>2</div></div><div><span>sudo</span><span> </span><span>dnf</span><span> </span><span>install</span><span> </span><span>ripgrep</span></div></div><div><div><div>3</div></div><div><span>sudo</span><span> </span><span>pacman</span><span> </span><span>-S</span><span> </span><span>ripgrep</span></div></div><div><div><div>4</div></div><div><span>brew</span><span> </span><span>install</span><span> </span><span>ripgrep</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Windows 可以使用 Scoop：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop install ripgrep</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>安装后先确认实际版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>rg</span><span> </span><span>--version</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>rg</code> 最重要的特性可能不是速度，而是它默认不搜索什么。根据 <a href="https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering" target="_blank">ripgrep 官方指南</a>，它默认跳过：</p><ul>
<li><code>.gitignore</code>、<code>.ignore</code> 和 <code>.rgignore</code> 排除的路径。</li>
<li>隐藏文件和隐藏目录。</li>
<li>检测到的二进制文件。</li>
<li>符号链接指向的内容。</li>
</ul><p>我在一个 Git 仓库里做了 3 文件的小实验：<code>visible.txt</code>、被 <code>.gitignore</code> 排除的 <code>ignored.log</code>，以及隐藏文件 <code>.hidden.txt</code>。三个文件都只有一行 <code>SECRET</code>。为了不让 <code>grep</code> 顺手钻进 <code>.git</code>，我显式排除了它：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>grep</span><span> </span><span>-R</span><span> </span><span>-n</span><span> </span><span>--exclude-dir=.git</span><span> </span><span>'SECRET'</span><span> </span><span>.</span></div></div><div><div><div>2</div></div><div><span>./.hidden.txt:1:SECRET</span><span> </span><span>hidden</span></div></div><div><div><div>3</div></div><div><span>./visible.txt:1:SECRET</span><span> </span><span>visible</span></div></div><div><div><div>4</div></div><div><span>./ignored.log:1:SECRET</span><span> </span><span>ignored</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>$</span><span> </span><span>rg</span><span> </span><span>-n</span><span> </span><span>'SECRET'</span><span> </span><span>.</span></div></div><div><div><div>7</div></div><div><span>./visible.txt:1:SECRET</span><span> </span><span>visible</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个目录里，<code>grep</code> 找到 3 行，<code>rg</code> 默认只给我 1 行。<code>rg</code> 不是漏搜了，它执行了更适合代码仓库的过滤策略。</p><p>需要掀开过滤器时，也有明确的开关：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 搜索被 ignore 规则排除的路径</span></div></div><div><div><div>2</div></div><div><span>rg</span><span> </span><span>--no-ignore</span><span> </span><span>'SECRET'</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 同时搜索隐藏文件</span></div></div><div><div><div>5</div></div><div><span>rg</span><span> </span><span>--hidden</span><span> </span><span>--no-ignore</span><span> </span><span>'SECRET'</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># -u 逐级解除限制：-u 忽略 ignore，-uu 包含隐藏文件，-uuu 还包含二进制文件</span></div></div><div><div><div>8</div></div><div><span>rg</span><span> </span><span>-uuu</span><span> </span><span>'SECRET'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-uuu</code> 很好记，但我不会把它设成默认别名。二进制内容可能把控制字符直接送进终端，而且 <code>node_modules</code>、构建产物和 <code>.git</code> 往往正是噪声的主要来源。只有当 “我明明知道它在这里” 时，我才逐级加 <code>u</code>。</p></section>
<section><h2>rg 中最顺手的几种搜索<a href="#rg-中最顺手的几种搜索"><span>#</span></a></h2><p>多数 <code>grep</code> 习惯可以直接迁移：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>rg</span><span> </span><span>-i</span><span> </span><span>'error'</span><span>                 </span><span># 忽略大小写</span></div></div><div><div><div>2</div></div><div><span>rg</span><span> </span><span>-F</span><span> </span><span>'array[index]'</span><span>          </span><span># 字面字符串</span></div></div><div><div><div>3</div></div><div><span>rg</span><span> </span><span>-w</span><span> </span><span>'Result'</span><span>                </span><span># 完整单词</span></div></div><div><div><div>4</div></div><div><span>rg</span><span> </span><span>-C</span><span> </span><span>3</span><span> </span><span>'panic'</span><span>               </span><span># 前后各 3 行上下文</span></div></div><div><div><div>5</div></div><div><span>rg</span><span> </span><span>-l</span><span> </span><span>'TODO|FIXME'</span><span>            </span><span># 只列出文件名</span></div></div><div><div><div>6</div></div><div><span>rg</span><span> </span><span>-c</span><span> </span><span>'TODO'</span><span>                  </span><span># 按文件统计匹配行数</span></div></div><div><div><div>7</div></div><div><span>rg</span><span> </span><span>-q</span><span> </span><span>'needle'</span><span> </span><span>path/to/file</span><span>   </span><span># 静默检查，依靠退出状态</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>针对仓库筛选文件时，glob 比一长串目录参数更舒服：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>rg</span><span> </span><span>'fetchData'</span><span> </span><span>-g</span><span> </span><span>'*.ts'</span><span> </span><span>-g</span><span> </span><span>'*.tsx'</span></div></div><div><div><div>2</div></div><div><span>rg</span><span> </span><span>'password'</span><span> </span><span>-g</span><span> </span><span>'!dist/**'</span><span> </span><span>-g</span><span> </span><span>'!*.min.js'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-g</code> 可以重复使用，前面的 <code>!</code> 表示排除。记得给 glob 加单引号，否则 shell 可能在 <code>rg</code> 看到它之前就先展开。</p><p><code>rg</code> 还内置文件类型：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>rg</span><span> </span><span>-tpy</span><span> </span><span>'def main'</span><span>       </span><span># 只搜索 Python</span></div></div><div><div><div>2</div></div><div><span>rg</span><span> </span><span>-tts</span><span> </span><span>'fetchData'</span><span>      </span><span># 只搜索 TypeScript</span></div></div><div><div><div>3</div></div><div><span>rg</span><span> </span><span>-Tjs</span><span> </span><span>'TODO'</span><span>           </span><span># 排除 JavaScript</span></div></div><div><div><div>4</div></div><div><span>rg</span><span> </span><span>--type-list</span><span>           </span><span># 查看当前支持的类型</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的文件类型本质上是一组扩展名和 glob，不是在做语法分析。如果我需要寻找真正的函数调用、声明或引用，AST 或语言服务器才是对的工具。正则表达式看起来像一把锤子，而代码仓库里碰巧到处都是长得像钉子的字符串，仅此而已。</p></section>
<section><h2>两套正则表达式并不完全相同<a href="#两套正则表达式并不完全相同"><span>#</span></a></h2><p><code>grep</code> 默认使用 BRE，<code>grep -E</code> 使用 ERE。<code>rg</code> 默认使用 Rust 的 regex 引擎，支持 Unicode，并保证线性时间搜索，但默认不支持反向引用和 look-around。简单的搜索通常可以直接对应：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>grep</span><span> </span><span>-E</span><span> </span><span>'WARN|ERROR'</span><span> </span><span>app.log</span></div></div><div><div><div>2</div></div><div><span>rg</span><span> </span><span>'WARN|ERROR'</span><span> </span><span>app.log</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果确实需要 look-around 或反向引用，并且当前 <code>rg</code> 构建包含 PCRE2，可以使用 <code>-P</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>rg</span><span> </span><span>-P</span><span> </span><span>'ERROR(?= database)'</span><span> </span><span>app.log</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我不会因为 PCRE2 更 “强” 就默认打开它。大多数代码搜索只需要字面量、字符类、分支和重复；默认引擎更可预测，也更不容易写出性能悬崖。复杂正则如果已经需要一杯咖啡才能读懂，通常也到了该换脚本或结构化工具的时候。</p></section>
<section><h2>我测了一次速度，但不准备宣布宇宙真理<a href="#我测了一次速度但不准备宣布宇宙真理"><span>#</span></a></h2><p>为了不只复述 “ripgrep 很快”，我生成了 4096 个文本文件，并让其中每 16 个文件有 1 个包含固定字符串 <code>needle-search-marker</code>。当时我在 Python 会话中直接执行实验；下面把同一段生成逻辑整理成可独立运行的 <code>generate_benchmark.py</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env python3</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>from</span><span> pathlib </span><span>import</span><span> Path</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>ROOT</span><span> </span><span>=</span><span> </span><span>Path</span><span>(</span></span><span>"bench"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>FILE_COUNT</span><span> </span><span>=</span><span> </span></span><span>4096</span></div></div><div><div><div>7</div></div><div><span><span>MATCH_INTERVAL</span><span> </span><span>=</span><span> </span></span><span>16</span></div></div><div><div><div>8</div></div><div><span><span>PATTERN</span><span> </span><span>=</span><span> </span></span><span>b</span><span>"needle-search-marker</span><span>\n</span><span>"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>if</span><span><span> </span><span>ROOT</span><span>.</span><span>exists</span><span>():</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>raise</span><span> </span><span>SystemExit</span><span>(</span><span>"bench/ already exists; remove it before regenerating"</span><span>)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>ROOT</span><span>.</span><span>mkdir</span><span>()</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>line </span><span>=</span><span> (</span></span><span>"alpha beta gamma delta epsilon 0123456789 "</span><span><span> </span><span>*</span><span> </span></span><span>16</span><span><span> </span><span>+</span><span> </span></span><span>"</span><span>\n</span><span>"</span><span><span>).</span><span>encode</span><span>()</span></span></div></div><div><div><div>16</div></div><div><span><span>plain_payload </span><span>=</span><span> line </span><span>*</span><span> </span></span><span>48</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>for</span><span> index </span><span>in</span><span> </span><span>range</span><span><span>(</span><span>FILE_COUNT</span><span>):</span></span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>payload </span><span>=</span><span> plain_payload</span></div></div><div><div><div>20</div></div><div><span>    </span><span>if</span><span><span> index </span><span>%</span><span> </span><span>MATCH_INTERVAL</span><span> </span><span>==</span><span> </span></span><span>0</span><span>:</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>payload </span><span>+=</span><span> </span><span>PATTERN</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>(</span><span>ROOT</span><span> </span><span>/</span><span> </span><span>f</span><span>"file-</span><span>{</span><span>index</span><span>:04d</span><span>}</span><span>.txt"</span><span><span>).</span><span>write_bytes</span><span>(payload)</span></span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span><span>total_bytes </span><span>=</span><span> </span></span><span>sum</span><span><span>(path.</span><span>stat</span><span>().st_size </span></span><span>for</span><span> path </span><span>in</span><span><span> </span><span>ROOT</span><span>.</span><span>iterdir</span><span>())</span></span></div></div><div><div><div>25</div></div><div><span><span>matching_files </span><span>=</span><span> </span><span>FILE_COUNT</span><span> </span><span>//</span><span> </span><span>MATCH_INTERVAL</span></span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>print</span><span>(</span><span>f</span><span>"files: </span><span>{</span><span>FILE_COUNT</span><span>}</span><span>"</span><span>)</span></div></div><div><div><div>28</div></div><div><span>print</span><span>(</span><span>f</span><span>"size: </span><span>{</span><span>total_bytes</span><span>}</span><span> bytes (</span><span>{</span><span><span>total_bytes </span><span>/</span><span> </span></span><span>1024</span><span><span> </span><span>/</span><span> </span></span><span>1024</span><span>:.2f</span><span>}</span><span> MiB)"</span><span>)</span></div></div><div><div><div>29</div></div><div><span>print</span><span>(</span><span>f</span><span>"matching files: </span><span>{</span><span>matching_files</span><span>}</span><span>"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这组参数最终生成 4096 个文件，总大小为 132322560 bytes，也就是 126.19 MiB，其中 256 个文件包含目标字符串。接下来的两组命令都会搜索这份固定语料。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>python</span><span> </span><span>generate_benchmark.py</span></div></div><div><div><div>2</div></div><div><span>files:</span><span> </span><span>4096</span></div></div><div><div><div>3</div></div><div><span>size:</span><span> </span><span>132322560</span><span> </span><span>bytes</span><span> (126.19 </span><span>MiB</span><span>)</span></div></div><div><div><div>4</div></div><div><span>matching</span><span> </span><span>files:</span><span> </span><span>256</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>下面再把同一次实验的计时逻辑整理成 <code>benchmark.py</code>。它先让每个工具预热 1 次，使语料进入系统缓存，然后正式运行 9 次。<code>time.perf_counter()</code> 测量的是从创建子进程到进程退出的墙钟时间，命令输出则送到 <code>/dev/null</code>，避免终端渲染成为主要开销。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env python3</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>from</span><span> pathlib </span><span>import</span><span> Path</span></div></div><div><div><div>4</div></div><div><span>import</span><span> statistics</span></div></div><div><div><div>5</div></div><div><span>import</span><span> subprocess</span></div></div><div><div><div>6</div></div><div><span>import</span><span> time</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>ROOT</span><span> </span><span>=</span><span> </span><span>Path</span><span>(</span></span><span>"bench"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>PATTERN</span><span> </span><span>=</span><span> </span></span><span>"needle-search-marker"</span></div></div><div><div><div>10</div></div><div><span><span>RUNS</span><span> </span><span>=</span><span> </span></span><span>9</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>if</span><span> </span><span>not</span><span><span> </span><span>ROOT</span><span>.</span><span>is_dir</span><span>():</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>raise</span><span> </span><span>SystemExit</span><span>(</span><span>"bench/ does not exist; run generate_benchmark.py first"</span><span>)</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>commands </span><span>=</span><span> {</span></span></div></div><div><div><div>16</div></div><div><span>    </span><span>"grep"</span><span>: [</span><span>"grep"</span><span>, </span><span>"-R"</span><span>, </span><span>"-l"</span><span>, </span><span>"-F"</span><span><span>, </span><span>PATTERN</span><span>, </span></span><span>str</span><span><span>(</span><span>ROOT</span><span>)],</span></span></div></div><div><div><div>17</div></div><div><span>    </span><span>"rg"</span><span>: [</span><span>"rg"</span><span>, </span><span>"-l"</span><span>, </span><span>"-F"</span><span><span>, </span><span>PATTERN</span><span>, </span></span><span>str</span><span><span>(</span><span>ROOT</span><span>)],</span></span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>timings </span><span>=</span><span> {}</span></span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>for</span><span> command </span><span>in</span><span><span> commands.</span><span>values</span><span>():</span></span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>subprocess.</span><span>run</span><span>(command, </span><span>stdout</span><span><span>=</span><span>subprocess.</span><span>DEVNULL</span><span>, </span></span><span>check</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>for</span><span> name, command </span><span>in</span><span><span> commands.</span><span>items</span><span>():</span></span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>samples </span><span>=</span><span> []</span></div></div><div><div><div>27</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span><span>(</span><span>RUNS</span><span>):</span></span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>started </span><span>=</span><span> time.</span><span>perf_counter</span><span>()</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>subprocess.</span><span>run</span><span>(command, </span><span>stdout</span><span><span>=</span><span>subprocess.</span><span>DEVNULL</span><span>, </span></span><span>check</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>samples.</span><span>append</span><span>((time.</span><span>perf_counter</span><span>() </span><span>-</span><span> started) </span><span>*</span><span> </span><span>1000</span><span>)</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>timings[name] </span><span>=</span><span> samples</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>for</span><span> name, samples </span><span>in</span><span><span> timings.</span><span>items</span><span>():</span></span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>formatted </span><span>=</span><span> </span><span>", "</span><span><span>.</span><span>join</span><span>(</span></span><span>f</span><span>"</span><span>{</span><span>sample</span><span>:.2f</span><span>}</span><span>"</span><span> </span><span>for</span><span> sample </span><span>in</span><span> samples)</span></div></div><div><div><div>35</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span>: </span><span>{</span><span>formatted</span><span>}</span><span> ms"</span><span>)</span></div></div><div><div><div>36</div></div><div><span>    </span><span>print</span><span>(</span><span>f</span><span>"</span><span>{</span><span>name</span><span>}</span><span> median: </span><span>{</span><span><span>statistics.</span><span>median</span><span>(samples)</span></span><span>:.2f</span><span>}</span><span> ms"</span><span>)</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span><span>ratio </span><span>=</span><span> statistics.</span><span>median</span><span>(timings[</span></span><span>"grep"</span><span><span>]) </span><span>/</span><span> statistics.</span><span>median</span><span>(timings[</span></span><span>"rg"</span><span>])</span></div></div><div><div><div>39</div></div><div><span>print</span><span>(</span><span>f</span><span>"grep / rg: </span><span>{</span><span>ratio</span><span>:.2f</span><span>}</span><span>x"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这份计时脚本通过 <code>subprocess.run()</code> 直接启动外部可执行文件，不经过 shell；在我的环境中，它解析到 GNU grep 3.12 和 ripgrep 15.1.0。因此，下面的数据确实来自 GNU grep 3.12，而不是其他 <code>grep</code> 实现。9 次原始数据按脚本记录的执行顺序列出，没有挑选或删除离群值：</p>

<table><thead><tr><th>次数</th><th><code>grep</code></th><th><code>rg</code></th></tr></thead><tbody><tr><td>1</td><td>44.67 ms</td><td>13.90 ms</td></tr><tr><td>2</td><td>45. 36ms</td><td>14.66 ms</td></tr><tr><td>3</td><td>43.01 ms</td><td>14.96 ms</td></tr><tr><td>4</td><td>42.65 ms</td><td>15.72 ms</td></tr><tr><td>5</td><td>44.68 ms</td><td>15.91 ms</td></tr><tr><td>6</td><td>42.72 ms</td><td>11.92 ms</td></tr><tr><td>7</td><td>44.74 ms</td><td>15.04 ms</td></tr><tr><td>8</td><td>43.28 ms</td><td>13.66 ms</td></tr><tr><td>9</td><td>44.01 ms</td><td>14.59 ms</td></tr><tr><td><strong>中位数</strong></td><td><strong>44.01 ms</strong></td><td><strong>14.66 ms</strong></td></tr></tbody></table><p>两个命令实际分别是：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>grep</span><span> </span><span>-R</span><span> </span><span>-l</span><span> </span><span>-F</span><span> </span><span>'needle-search-marker'</span><span> </span><span>bench/</span></div></div><div><div><div>2</div></div><div><span>rg</span><span> </span><span>-l</span><span> </span><span>-F</span><span> </span><span>'needle-search-marker'</span><span> </span><span>bench/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因此，中位时间和相对值为：</p>

<table><thead><tr><th>工具</th><th>中位时间</th><th>相对值</th></tr></thead><tbody><tr><td><code>grep</code></td><td>44.01 ms</td><td>3.00x</td></tr><tr><td><code>rg</code></td><td>14.66 ms</td><td>1.00x</td></tr></tbody></table><p>这次 <code>rg</code> 大约快 3 倍。数字很具体，结论却必须克制：它只描述这台机器、这两个版本、热缓存、固定字符串和 4096 个文件。换成单个大文件、高匹配率、不同正则、机械硬盘或另一个 <code>grep</code> 实现，比例都可能变化。<code>ripgrep</code> 项目的 <a href="https://github.com/BurntSushi/ripgrep#quick-examples-comparing-tools" target="_blank">官方 README</a> 也明确提醒，单个 benchmark 永远不够。</p><p>我反而更在意前一个实验：在包含忽略文件和隐藏文件的目录中，默认输出从 3 行降到了 1 行。少掉的 2 行不是性能指标，而是我原本就不想看的噪声。这种默认行为每天都会影响搜索体验，44 ms 和 15 ms 则未必。</p></section>
<section><h2>我现在怎么选<a href="#我现在怎么选"><span>#</span></a></h2><p>我的规则最后变得很简单：</p><ul>
<li><strong>脚本要跨陌生的 Unix 环境运行</strong>：优先使用 POSIX <code>grep</code> 的核心选项。它普遍存在，而且契约有标准。</li>
<li><strong>搜索单个文件或管道输出</strong>：<code>grep</code> 已经足够直接，尤其是系统上不一定安装 <code>rg</code> 时。</li>
<li><strong>交互式搜索代码仓库</strong>：优先使用 <code>rg</code>。递归、ignore、隐藏文件过滤、glob 和文件类型都围绕这个场景设计。</li>
<li><strong>搜索结果疑似被过滤</strong>：先试 <code>rg -u</code>、<code>rg --hidden</code> 或 <code>rg --debug</code>，不要立刻断言文件不存在。</li>
<li><strong>需要理解代码结构</strong>：停止继续堆正则，换语言服务器、AST 搜索或 IDE 引用查找。</li>
</ul><p><code>grep</code> 没有因为 <code>ripgrep</code> 出现就过时。它更像 shell 世界里的基础协议，而 <code>rg</code> 是针对现代代码仓库重新安排过默认值的高性能客户端。我大概会继续在脚本里写 <code>grep</code>，在终端里敲 <code>rg</code>。至少下一次看到 <code>grep -Eiv</code> 时，我不再需要假装那是一串不可分割的魔法咒语了。</p></section>
<section><h2>继续查阅<a href="#继续查阅"><span>#</span></a></h2><ul>
<li><a href="https://pubs.opengroup.org/onlinepubs/9799919799/utilities/grep.html" target="_blank">POSIX grep 规范</a></li>
<li><a href="https://www.gnu.org/software/grep/manual/grep.html" target="_blank">GNU grep 3.12 手册</a></li>
<li><a href="https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md" target="_blank">ripgrep User Guide</a></li>
<li><a href="https://github.com/BurntSushi/ripgrep/blob/master/README.md" target="_blank">ripgrep README 与性能说明</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/vcpkg-modern-cmake-http-client/</id>
      <title type="text">从零学习 vcpkg: 用现代 CMake 做一个 HTTP CLI</title>
      <published>2026-08-30T12:00:00.000Z</published>
      <updated>2026-08-30T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/vcpkg-modern-cmake-http-client/"/>
      <summary type="text">我用 vcpkg、现代 CMake、CPR、CLI11 和 spdlog 做了一个可运行的命令行 HTTP 客户端，并沿着清单、baseline、triplet、toolchain 和 imported target 把 C/C++ 依赖管理串起来。</summary>
      <content type="html"><![CDATA[<p>我过去写的嵌入式项目主要使用 C，依赖通常是芯片厂商 SDK、几份直接放进仓库的源码，以及一些自己维护的驱动，所以我很少认真面对 C/C++ 包管理。Python 有 pip，JavaScript/TypeScript 有 npm 和 pnpm，Rust 有 Cargo；C++ 则把编译器、构建系统和包管理器拆成了三件事。于是我决定不再只背 <code>vcpkg install</code>，而是用 vcpkg、现代 CMake 和 3 个第三方库做一个真的能发出请求的 HTTP CLI，然后看看每一层到底负责什么。</p>
<p>这个小程序叫 <code>vhttp</code>。它支持 GET、POST、自定义 Header、超时和详细日志。CLI11 解析命令行，CPR 发送 HTTP 请求，spdlog 记录状态。3 个直接依赖最后展开成 10 个需要安装的 port，刚好足以看到包管理器开始工作，又没有膨胀成另一个 curl（我只是来学 vcpkg，不是来重写互联网的）。</p>
<section><h2>先把 3 个角色分开<a href="#先把-3-个角色分开"><span>#</span></a></h2><p>C/C++ 依赖管理容易混乱，通常不是因为命令太多，而是因为几种工具同时出现在终端里：</p><ul>
<li><strong>编译器</strong>：GCC、Clang 或 MSVC，把源文件变成目标文件，并参与链接。</li>
<li><strong>CMake</strong>：构建系统生成器，描述 target、源文件、编译选项和链接关系，再生成 Ninja、Makefile 或 Visual Studio 工程。</li>
<li><strong>vcpkg</strong>：包管理器，解析依赖图，取得源码和 port 配方，按目标平台构建并安装库，再把它们交给 CMake 查找。</li>
</ul><p>我把 vcpkg 想成仓库管理员，把 CMake 想成装配清单。仓库管理员负责把零件放到正确货架，装配清单负责说明 <code>vhttp</code> 需要哪些零件以及如何连接。二者会合作，但不是同一个东西。</p><p>vcpkg 也不等于一个只下载预编译二进制的商店。它的 registry 主要保存 port 的版本信息、补丁和构建配方，很多库会在本机按 triplet 编译。第一次安装 OpenSSL 或 Boost 时看到编译器忙起来，不是 vcpkg 突然兼职写代码，而是它正在按配方生产当前平台需要的产物。</p></section>
<section><h2>安装 vcpkg<a href="#安装-vcpkg"><span>#</span></a></h2><p>官方的通用安装方式是克隆仓库并运行 bootstrap 脚本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>https://github.com/microsoft/vcpkg.git</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>vcpkg</span></div></div><div><div><div>3</div></div><div><span>./bootstrap-vcpkg.sh</span><span> </span><span>-disableMetrics</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Windows PowerShell 对应：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>git clone https:</span><span>//</span></span><span>github.com</span><span><span>/</span><span>microsoft</span><span>/</span><span>vcpkg.git</span></span></div></div><div><div><div>2</div></div><div><span>cd vcpkg</span></div></div><div><div><div>3</div></div><div><span>.\</span><span>bootstrap-vcpkg.bat</span><span><span> </span><span>-</span><span>disableMetrics</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>bootstrap 会准备 <code>vcpkg</code> 可执行文件。然后设置 <code>VCPKG_ROOT</code>，并把可执行文件加入 <code>PATH</code>。Linux 或 macOS 可以写入自己的 shell 配置：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>VCPKG_ROOT</span><span>=</span><span>"</span><span>$HOME</span><span>/tools/vcpkg"</span></div></div><div><div><div>2</div></div><div><span>export</span><span> </span><span>PATH</span><span>=</span><span>"</span><span>$VCPKG_ROOT</span><span>:</span><span>$PATH</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>PowerShell 当前会话可以这样设置：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$env:VCPKG_ROOT</span><span><span> </span><span>=</span><span> </span></span><span>"C:\tools\vcpkg"</span></div></div><div><div><div>2</div></div><div><span>$env:PATH</span><span><span> </span><span>=</span><span> </span></span><span><span>"</span><span>$</span></span><span>env:VCPKG_ROOT</span><span><span>;</span><span>$</span></span><span>env:PATH</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后检查实际入口，而不是假设 bootstrap 已经替我们处理了所有环境变量：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$ vcpkg version</span></div></div><div><div><div>2</div></div><div><span>vcpkg package management program version 2026-07-27-98d7cb0cf1f4686a3e43aa5672b6230c1d56bce8</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>版本会变化。这里真正重要的是命令可以运行，并且 <code>VCPKG_ROOT</code> 指向包含 <code>scripts/buildsystems/vcpkg.cmake</code> 的那份 vcpkg。</p></section>
<section><h2>Classic mode 和 Manifest mode<a href="#classic-mode-和-manifest-mode"><span>#</span></a></h2><p>vcpkg 有两种工作模式：</p>

<table><thead><tr><th>模式</th><th>依赖声明</th><th>安装位置</th><th>适合场景</th></tr></thead><tbody><tr><td>Classic mode</td><td>主要靠命令安装</td><td><code>$VCPKG_ROOT/installed</code></td><td>临时试用、旧项目</td></tr><tr><td>Manifest mode</td><td>项目提交 <code>vcpkg.json</code></td><td>每个项目自己的安装树</td><td>正式项目、版本控制、CI</td></tr></tbody></table><p>Classic mode 的体验接近系统级仓库：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>vcpkg</span><span> </span><span>install</span><span> </span><span>fmt</span></div></div><div><div><div>2</div></div><div><span>vcpkg</span><span> </span><span>install</span><span> </span><span>fmt:x64-windows</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这很适合快速试一个库，但依赖关系存在于开发者的机器里。换一台机器后，项目本身没有完整声明告诉我们该装什么。</p><p>Manifest mode 把直接依赖写进 <code>vcpkg.json</code>，并为每个项目维护独立安装树。它也支持 baseline、版本约束、override 和自定义 registry。对新项目，我会直接使用 Manifest mode。把依赖写进仓库，比把它们记在脑内某个没有备份的 YAML 文件里可靠得多。</p></section>
<section><h2>建立 <code>vhttp</code><a href="#建立-vhttp"><span>#</span></a></h2><p>先创建目录并初始化应用清单：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir</span><span> </span><span>vhttp</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>vhttp</span></div></div><div><div><div>3</div></div><div><span>vcpkg</span><span> </span><span>new</span><span> </span><span>--application</span></div></div><div><div><div>4</div></div><div><span>vcpkg</span><span> </span><span>add</span><span> </span><span>port</span><span> </span><span>cli11</span></div></div><div><div><div>5</div></div><div><span>vcpkg</span><span> </span><span>add</span><span> </span><span>port</span><span> </span><span>cpr</span></div></div><div><div><div>6</div></div><div><span>vcpkg</span><span> </span><span>add</span><span> </span><span>port</span><span> </span><span>spdlog</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>vcpkg new --application</code> 会创建项目清单和 registry 配置，<code>vcpkg add port</code> 则把直接依赖写进清单。项目最终结构如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>vhttp/</span></div></div><div><div><div>2</div></div><div><span>├── CMakeLists.txt</span></div></div><div><div><div>3</div></div><div><span>├── CMakePresets.json</span></div></div><div><div><div>4</div></div><div><span>├── src/</span></div></div><div><div><div>5</div></div><div><span>│   └── main.cpp</span></div></div><div><div><div>6</div></div><div><span>├── vcpkg-configuration.json</span></div></div><div><div><div>7</div></div><div><span>└── vcpkg.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我还会忽略本机构建产物：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/build/</span></div></div><div><div><div>2</div></div><div><span>/vcpkg_installed/</span></div></div><div><div><div>3</div></div><div><span>CMakeUserPresets.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>vcpkg.json</code> 很短：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"name"</span><span>: </span><span>"vhttp"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"version-semver"</span><span>: </span><span>"0.1.0"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"dependencies"</span><span>: [</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"cli11"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"cpr"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>"spdlog"</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它只声明<strong>直接依赖</strong>。CPR 需要 curl，curl 的 HTTPS 支持需要 OpenSSL 和 zlib，spdlog 默认又需要 fmt；这些传递依赖由 vcpkg 解图，不应该为了复制一次安装输出就全部手写进 <code>dependencies</code>。</p><p><code>vcpkg-configuration.json</code> 保存 registry 和 baseline：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"default-registry"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"kind"</span><span>: </span><span>"git"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"baseline"</span><span>: </span><span>"1390a3d90e08a7901a7781d31332a662102cd5cc"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"repository"</span><span>: </span><span>"https://github.com/microsoft/vcpkg"</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>baseline 是 vcpkg registry 的一个 Git commit。它给整张依赖图一个确定的版本地板，让两台机器不要因为各自恰好在不同日期安装，就悄悄选到不同 port 版本。上面的 commit 是我这次实验实际使用的快照；新项目应该保留 <code>vcpkg new</code> 为自己生成的值，而不是机械复制它。</p><p>需要主动更新依赖视野时，可以运行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>vcpkg</span><span> </span><span>x-update-baseline</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>更新 baseline 是一个明确的依赖升级动作，应该连同构建结果一起审查和提交。它不是每次构建前都要念一遍的咒语。</p><div><div><div></div><div>Note</div></div><div><p>baseline 并不等于传统意义上的 lockfile。它确定 registry 快照；<code>version&gt;=</code> 表达最低版本，<code>overrides</code> 可以强制精确版本。大多数应用先固定 baseline，只在确有需要时增加版本约束或 override，会比给每个传递依赖手工钉死版本更容易维护。</p></div></div></section>
<section><h2>用 CMake Presets 接上 vcpkg<a href="#用-cmake-presets-接上-vcpkg"><span>#</span></a></h2><p>vcpkg 通过一个 CMake toolchain 文件接入配置过程。关键点是：CMake 在执行第一次 <code>project()</code> 时就会确定工具链，所以 <code>CMAKE_TOOLCHAIN_FILE</code> 必须在那之前可见。</p><p>我不把本机的绝对路径写进 <code>CMakeLists.txt</code>，而是提交一份 <code>CMakePresets.json</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"version"</span><span>: </span><span>6</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"configurePresets"</span><span>: [</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>5</div></div><div><span>      </span><span>"name"</span><span>: </span><span>"dev"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>      </span><span>"displayName"</span><span>: </span><span>"Development build with vcpkg"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>      </span><span>"generator"</span><span>: </span><span>"Ninja"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>      </span><span>"binaryDir"</span><span>: </span><span>"${sourceDir}/build/${presetName}"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>      </span><span>"cacheVariables"</span><span>: {</span></div></div><div><div><div>10</div></div><div><span>        </span><span>"CMAKE_BUILD_TYPE"</span><span>: </span><span>"Debug"</span><span>,</span></div></div><div><div><div>11</div></div><div><span>        </span><span>"CMAKE_EXPORT_COMPILE_COMMANDS"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"CMAKE_TOOLCHAIN_FILE"</span><span>: </span><span>"$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"</span></div></div><div><div><div>13</div></div><div><span><span>      </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>16</div></div><div><span>  </span><span>"buildPresets"</span><span>: [</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>18</div></div><div><span>      </span><span>"name"</span><span>: </span><span>"dev"</span><span>,</span></div></div><div><div><div>19</div></div><div><span>      </span><span>"configurePreset"</span><span>: </span><span>"dev"</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>]</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Preset 固定了生成器、构建目录和共享配置，但通过环境变量取得每台机器自己的 vcpkg 路径。团队可以提交这份文件；如果某人不想全局设置 <code>VCPKG_ROOT</code>，也可以在不提交的 <code>CMakeUserPresets.json</code> 中继承 <code>dev</code> 并设置本机环境。</p><p>配置时，vcpkg toolchain 会发现项目根目录中的 manifest，并自动执行安装：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cmake</span><span> </span><span>--preset</span><span> </span><span>dev</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我第一次配置时声明了 3 个直接依赖，vcpkg 实际列出了 10 个 port：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cli11</span></div></div><div><div><div>2</div></div><div><span>cpr</span></div></div><div><div><div>3</div></div><div><span>curl</span></div></div><div><div><div>4</div></div><div><span>fmt</span></div></div><div><div><div>5</div></div><div><span>openssl</span></div></div><div><div><div>6</div></div><div><span>spdlog</span></div></div><div><div><div>7</div></div><div><span>vcpkg-cmake</span></div></div><div><div><div>8</div></div><div><span>vcpkg-cmake-config</span></div></div><div><div><div>9</div></div><div><span>vcpkg-cmake-get-vars</span></div></div><div><div><div>10</div></div><div><span>zlib</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>从 3 个名字展开到 10 个构建节点，这就是手工下载压缩包、复制 include 路径和寻找 <code>.a</code>/<code>.lib</code> 文件开始变得不太可爱的地方。Manifest mode 会把这棵安装树放进当前构建目录的 <code>vcpkg_installed</code>，不会污染另一个项目。</p></section>
<section><h2>现代 CMake 只围绕 target 工作<a href="#现代-cmake-只围绕-target-工作"><span>#</span></a></h2><p>根目录的 <code>CMakeLists.txt</code> 如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cmake_minimum_required</span><span>(VERSION 3.25)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>project</span><span>(vhttp VERSION 0.1.0 LANGUAGES CXX)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>find_package</span><span>(CLI11 CONFIG REQUIRED)</span></div></div><div><div><div>6</div></div><div><span>find_package</span><span>(cpr CONFIG REQUIRED)</span></div></div><div><div><div>7</div></div><div><span>find_package</span><span>(spdlog CONFIG REQUIRED)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>add_executable</span><span>(vhttp)</span></div></div><div><div><div>10</div></div><div><span>target_sources</span><span>(vhttp PRIVATE src/main.cpp)</span></div></div><div><div><div>11</div></div><div><span>target_compile_features</span><span>(vhttp PRIVATE cxx_std_20)</span></div></div><div><div><div>12</div></div><div><span>target_link_libraries</span><span>(vhttp</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>PRIVATE</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>CLI11::CLI11</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>cpr::cpr</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>spdlog::spdlog</span></div></div><div><div><div>17</div></div><div><span>)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>target_compile_options</span><span>(vhttp</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>PRIVATE</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>$&lt;$&lt;CXX_COMPILER_ID:</span><span>MSVC</span><span>&gt;:/W4&gt;</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>$&lt;$&lt;CXX_COMPILER_ID:</span><span>MSVC</span><span>&gt;:/permissive-&gt;</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>$&lt;$&lt;</span><span>NOT</span><span>:$&lt;CXX_COMPILER_ID:</span><span>MSVC</span><span>&gt;&gt;:-Wall&gt;</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>$&lt;$&lt;</span><span>NOT</span><span>:$&lt;CXX_COMPILER_ID:</span><span>MSVC</span><span>&gt;&gt;:-Wextra&gt;</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>$&lt;$&lt;</span><span>NOT</span><span>:$&lt;CXX_COMPILER_ID:</span><span>MSVC</span><span>&gt;&gt;:-Wpedantic&gt;</span></div></div><div><div><div>26</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里没有全局 <code>include_directories()</code>，没有硬编码 <code>lib</code> 路径，也没有 <code>link_directories()</code>。<code>find_package(... CONFIG REQUIRED)</code> 找到库导出的 CMake package config，<code>CLI11::CLI11</code>、<code>cpr::cpr</code> 和 <code>spdlog::spdlog</code> 则是 imported target。</p><p>这些 target 自带 include 目录、编译定义和传递链接要求。<code>vhttp</code> 只需要声明”我以 <code>PRIVATE</code> 方式链接它们”。这就是现代 CMake 最重要的习惯：让 target 携带 usage requirements，而不是把一堆全局状态泼到整个目录上。</p><p><code>target_compile_features(vhttp PRIVATE cxx_std_20)</code> 也比手工拼 <code>-std=c++20</code> 更可移植。警告选项确实因编译器而不同，所以我用 generator expression 在生成阶段选择 MSVC 或 GCC/Clang 的参数。</p></section>
<section><h2>写一个真的 HTTP 客户端<a href="#写一个真的-http-客户端"><span>#</span></a></h2><p><code>src/main.cpp</code> 的完整实现如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;algorithm&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;cctype&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;stdexcept&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>&lt;CLI/CLI.hpp&gt;</span></div></div><div><div><div>9</div></div><div><span>#include</span><span> </span><span>&lt;cpr/cpr.h&gt;</span></div></div><div><div><div>10</div></div><div><span>#include</span><span> </span><span>&lt;spdlog/spdlog.h&gt;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>namespace</span><span> {</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>cpr::</span><span>Header</span><span> </span><span>ParseHeaders</span><span>(</span><span>const</span><span> std::</span><span>vector</span><span>&lt;std::</span><span>string</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>raw_headers</span><span>) {</span></span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>cpr::</span><span>Header</span><span> </span><span>headers</span><span>;</span></div></div><div><div><div>16</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>auto&amp;</span><span> </span><span>raw</span><span> : </span><span>raw_headers</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>colon</span><span> </span><span>=</span><span> </span><span>raw</span><span>.</span><span>find</span><span>(</span><span>':'</span><span>);</span></div></div><div><div><div>18</div></div><div><span>    </span><span>if</span><span> (</span><span>colon</span><span> </span><span>==</span><span> std::string::</span><span>npos</span><span> </span><span>||</span><span> </span><span>colon</span><span> </span><span>==</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>19</div></div><div><span>      </span><span>throw</span><span> std::</span><span>runtime_error</span><span>(</span><span>"invalid header, expected 'Name: Value': "</span><span> </span><span>+</span></div></div><div><div><div>20</div></div><div><span>                               </span><span>raw</span><span>);</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>    </span><span>auto</span><span> </span><span>value_start</span><span> </span><span>=</span><span> </span><span>colon</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>24</div></div><div><span>    </span><span>while</span><span> (</span><span>value_start</span><span> </span><span>&lt;</span><span> </span><span>raw</span><span>.</span><span>size</span><span>() </span><span>&amp;&amp;</span><span> </span><span>raw</span><span>[</span><span>value_start</span><span>] </span><span>==</span><span> </span><span>' '</span><span>) {</span></div></div><div><div><div>25</div></div><div><span>      </span><span>++</span><span>value_start</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>    </span><span>headers</span><span>.</span><span>emplace</span><span>(</span><span>raw</span><span>.</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>colon</span><span>), </span><span>raw</span><span>.</span><span>substr</span><span>(</span><span>value_start</span><span>));</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>  </span><span>return</span><span> </span><span>headers</span><span>;</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>std::</span><span>string</span><span> </span><span>ToUpper</span><span>(std::</span><span>string</span><span><span> </span><span>value</span><span>) {</span></span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>std::</span><span>ranges</span><span>::</span><span>transform</span><span>(</span><span>value</span><span>, </span><span>value</span><span>.</span><span>begin</span><span>(), [](</span><span>unsigned</span><span> </span><span>char</span><span><span> </span><span>ch</span><span>) {</span></span></div></div><div><div><div>34</div></div><div><span>    </span><span>return</span><span> </span><span><span>static_cast</span><span>&lt;char&gt;</span></span><span><span>(</span><span>std</span><span>::</span></span><span>toupper</span><span>(</span><span>ch</span><span>));</span></div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>36</div></div><div><span>  </span><span>return</span><span> </span><span>value</span><span>;</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div><div><div><div>38</div></div><div>
</div></div><div><div><div>39</div></div><div><span>}</span><span>  // namespace</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>int</span><span><span> </span><span>argc</span><span>, </span></span><span>char*</span><span><span> </span><span>argv</span><span>[]) {</span></span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>CLI::</span><span>App</span><span> </span><span>app</span><span>{</span><span>"A tiny HTTP client built with vcpkg"</span><span>};</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>url</span><span>;</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>method</span><span>{</span><span>"GET"</span><span>};</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>body</span><span>;</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>string</span><span>&gt; </span><span>raw_headers</span><span>;</span></div></div><div><div><div>48</div></div><div><span>  </span><span>int</span><span> </span><span>timeout_ms</span><span>{</span><span>5000</span><span>};</span></div></div><div><div><div>49</div></div><div><span>  </span><span>bool</span><span> </span><span>verbose</span><span>{</span><span>false</span><span>};</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>  </span><span>app</span><span>.</span><span>add_option</span><span>(</span><span>"url"</span><span>, </span><span>url</span><span>, </span><span>"Request URL"</span><span>)-&gt;</span><span>required</span><span>();</span></div></div><div><div><div>52</div></div><div><span>  </span><span>app</span><span>.</span><span>add_option</span><span>(</span><span>"-X,--request"</span><span>, </span><span>method</span><span>, </span><span>"HTTP method: GET or POST"</span><span>);</span></div></div><div><div><div>53</div></div><div><span>  </span><span>app</span><span>.</span><span>add_option</span><span>(</span><span>"-H,--header"</span><span>, </span><span>raw_headers</span><span>, </span><span>"Request header, repeatable"</span><span>);</span></div></div><div><div><div>54</div></div><div><span>  </span><span>app</span><span>.</span><span>add_option</span><span>(</span><span>"-d,--data"</span><span>, </span><span>body</span><span>, </span><span>"POST request body"</span><span>);</span></div></div><div><div><div>55</div></div><div><span>  </span><span>app</span><span>.</span><span>add_option</span><span>(</span><span>"--timeout"</span><span>, </span><span>timeout_ms</span><span>, </span><span>"Timeout in milliseconds"</span><span>)</span></div></div><div><div><div>56</div></div><div><span>      </span><span>-&gt;</span><span>check</span><span>(CLI::</span><span>PositiveNumber</span><span>);</span></div></div><div><div><div>57</div></div><div><span>  </span><span>app</span><span>.</span><span>add_flag</span><span>(</span><span>"-v,--verbose"</span><span>, </span><span>verbose</span><span>, </span><span>"Enable debug logs"</span><span>);</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>  </span><span>CLI11_PARSE</span><span>(</span><span>app</span><span>, </span><span>argc</span><span>, </span><span>argv</span><span>);</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span>  </span><span>method</span><span> </span><span>=</span><span> </span><span>ToUpper</span><span>(</span><span>method</span><span>);</span></div></div><div><div><div>62</div></div><div><span>  </span><span>if</span><span> (</span><span>method</span><span> </span><span>!=</span><span> </span><span>"GET"</span><span> </span><span>&amp;&amp;</span><span> </span><span>method</span><span> </span><span>!=</span><span> </span><span>"POST"</span><span>) {</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span>spdlog::</span><span>error</span><span>(</span><span>"unsupported method: {}"</span><span>, </span><span>method</span><span>);</span></div></div><div><div><div>64</div></div><div><span>    </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>65</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>spdlog::</span><span>set_level</span><span>(</span><span>verbose</span><span> </span><span>?</span><span> spdlog::level::</span><span>debug</span><span> </span><span>:</span><span> spdlog::level::</span><span>info</span><span>);</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>spdlog::</span><span>debug</span><span>(</span><span>"{} {} (timeout={} ms)"</span><span>, </span><span>method</span><span>, </span><span>url</span><span>, </span><span>timeout_ms</span><span>);</span></div></div><div><div><div>70</div></div><div>
</div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>cpr::</span><span>Session</span><span> </span><span>session</span><span>;</span></div></div><div><div><div>72</div></div><div><span>    </span><span>session</span><span>.</span><span>SetUrl</span><span>(cpr::</span><span>Url</span><span>{</span><span>url</span><span>});</span></div></div><div><div><div>73</div></div><div><span>    </span><span>session</span><span>.</span><span>SetHeader</span><span>(</span><span>ParseHeaders</span><span>(</span><span>raw_headers</span><span>));</span></div></div><div><div><div>74</div></div><div><span>    </span><span>session</span><span>.</span><span>SetTimeout</span><span>(cpr::</span><span>Timeout</span><span>{</span><span>timeout_ms</span><span>});</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span><span>    </span></span><span>cpr::</span><span>Response</span><span> </span><span>response</span><span>;</span></div></div><div><div><div>77</div></div><div><span>    </span><span>if</span><span> (</span><span>method</span><span> </span><span>==</span><span> </span><span>"POST"</span><span>) {</span></div></div><div><div><div>78</div></div><div><span>      </span><span>session</span><span>.</span><span>SetBody</span><span>(cpr::</span><span>Body</span><span>{</span><span>body</span><span>});</span></div></div><div><div><div>79</div></div><div><span>      </span><span>response</span><span> </span><span>=</span><span> </span><span>session</span><span>.</span><span>Post</span><span>();</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>81</div></div><div><span>      </span><span>response</span><span> </span><span>=</span><span> </span><span>session</span><span>.</span><span>Get</span><span>();</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>    </span><span>if</span><span> (</span><span>response</span><span>.</span><span>error</span><span>.</span><span>code</span><span> </span><span>!=</span><span> cpr::ErrorCode::</span><span>OK</span><span>) {</span></div></div><div><div><div>85</div></div><div><span><span>      </span></span><span>spdlog::</span><span>error</span><span>(</span><span>"request failed: {}"</span><span>, </span><span>response</span><span>.</span><span>error</span><span>.</span><span>message</span><span>);</span></div></div><div><div><div>86</div></div><div><span>      </span><span>return</span><span> </span><span>2</span><span>;</span></div></div><div><div><div>87</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>88</div></div><div>
</div></div><div><div><div>89</div></div><div><span><span>    </span></span><span>spdlog::</span><span>info</span><span>(</span><span>"HTTP {} in {:.0f} ms, {} bytes"</span><span>, </span><span>response</span><span>.</span><span>status_code</span><span>,</span></div></div><div><div><div>90</div></div><div><span>                 </span><span>response</span><span>.</span><span>elapsed</span><span> </span><span>*</span><span> </span><span>1000.0</span><span>, </span><span>response</span><span>.</span><span>text</span><span>.</span><span>size</span><span>());</span></div></div><div><div><div>91</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>response</span><span>.</span><span>text</span><span>;</span></div></div><div><div><div>92</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>response</span><span>.</span><span>text</span><span>.</span><span>ends_with</span><span>(</span><span>'</span><span>\n</span><span>'</span><span>)) {</span></div></div><div><div><div>93</div></div><div><span><span>      </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span></div></div><div><div><div>94</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>95</div></div><div>
</div></div><div><div><div>96</div></div><div><span>    </span><span>return</span><span> </span><span>response</span><span>.</span><span>status_code</span><span> </span><span>&gt;=</span><span> </span><span>400</span><span> </span><span>?</span><span> </span><span>3</span><span> </span><span>:</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>97</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>exception</span><span>&amp;</span><span> </span><span>error</span><span>) {</span></div></div><div><div><div>98</div></div><div><span><span>    </span></span><span>spdlog::</span><span>error</span><span>(</span><span>"{}"</span><span>, </span><span>error</span><span>.</span><span>what</span><span>());</span></div></div><div><div><div>99</div></div><div><span>    </span><span>return</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>100</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>101</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里有一个刻意的接口设计：spdlog 把诊断信息写到标准错误，响应 body 写到标准输出。因此可以把 body 继续交给其他程序，而日志不会混进数据流：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>vhttp</span><span> </span><span>https://example.com/api</span><span> | </span><span>jq</span><span> </span><span>.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>jq</code> 是一个处理 JSON 的命令行工具；管道符 <code>|</code> 会把 <code>vhttp</code> 写到标准输出的响应 body 交给 <code>jq</code>，最后的 <code>.</code> 表示读取并格式化整个 JSON 值。例如服务返回一行紧凑 JSON 时，<code>jq .</code> 会把它缩进为更容易阅读的多行结构。<code>jq</code> 不是 <code>vhttp</code> 的依赖，需要按操作系统单独安装；如果不需要格式化 JSON，直接运行 <code>vhttp https://example.com/api</code> 即可。</p><p>返回码也区分了几类失败：参数或 Header 错误返回 1，传输错误返回 2，HTTP 4xx/5xx 返回 3。HTTP 404 和 TCP 连接失败不是一回事，CLI 最好不要把它们都压成一个含糊的 <code>false</code>。</p></section>
<section><h2>配置、构建和运行<a href="#配置构建和运行"><span>#</span></a></h2><p>有了 configure preset 和 build preset，基本循环只有两条命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cmake</span><span> </span><span>--preset</span><span> </span><span>dev</span></div></div><div><div><div>2</div></div><div><span>cmake</span><span> </span><span>--build</span><span> </span><span>--preset</span><span> </span><span>dev</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的实际构建只有 2 个 Ninja step：编译 <code>main.cpp</code>，然后链接 <code>vhttp</code>。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>[1/2] Building CXX object CMakeFiles/vhttp.dir/src/main.cpp.o</span></div></div><div><div><div>2</div></div><div><span>[2/2] Linking CXX executable vhttp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>为了避免把外部网络状态混进验证，我使用 Python 3 标准库自带的 <code>http.server</code> 在本机启动了一个静态文件服务器。它不需要额外安装 Web 框架，只要系统中的 <code>python3</code> 命令可用即可。</p><p>我先在项目根目录创建测试目录和响应文件。<code>printf</code> 不会额外改写内容，因此这个文件恰好是 17 bytes：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>http-fixture</span></div></div><div><div><div>2</div></div><div><span>printf</span><span> </span><span>'hello from vhttp\n'</span><span> &gt; </span><span>http-fixture/hello.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后打开第一个终端，在项目根目录启动服务器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>python3</span><span> </span><span>-m</span><span> </span><span>http.server</span><span> </span><span>18080</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>    </span><span>--bind</span><span> </span><span>127.0.0.1</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>    </span><span>--directory</span><span> </span><span>http-fixture</span></div></div><div><div><div>4</div></div><div><span>Serving</span><span> </span><span>HTTP</span><span> </span><span>on</span><span> </span><span>127.0.0.1</span><span> </span><span>port</span><span> </span><span>18080</span><span> (http://127.0.0.1:18080/) ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>-m http.server</code> 让 Python 运行标准库中的静态 HTTP server 模块。</li>
<li><code>18080</code> 是监听端口。</li>
<li><code>--bind 127.0.0.1</code> 只允许本机访问，避免把临时测试服务暴露到局域网。</li>
<li><code>--directory http-fixture</code> 把刚才创建的目录作为网站根目录。</li>
</ul><p>这条命令会占用当前终端并持续运行。验证结束后按 <kbd>Ctrl</kbd> + <kbd>C</kbd> 停止服务。Windows 如果安装了 Python Launcher，也可以把命令开头的 <code>python3</code> 换成 <code>py</code>。</p><p>接着打开第二个终端，仍然进入 <code>vhttp</code> 项目根目录，再运行客户端。URL 中的 <code>/hello.txt</code> 对应 <code>http-fixture/hello.txt</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$</span><span> </span><span>./build/dev/vhttp</span><span> </span><span>-v</span><span> </span><span>http://127.0.0.1:18080/hello.txt</span></div></div><div><div><div>2</div></div><div><span>[debug] GET http://127.0.0.1:18080/hello.txt (</span><span>timeout</span><span>=</span><span>5000</span><span> </span><span>ms</span><span>)</span></div></div><div><div><div>3</div></div><div><span>[info] HTTP 200 in 76 ms, 17 bytes</span></div></div><div><div><div>4</div></div><div><span>hello</span><span> </span><span>from</span><span> </span><span>vhttp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>POST 和重复 Header 使用相同的可执行文件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>./build/dev/vhttp</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-X</span><span> </span><span>POST</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-H</span><span> </span><span>"Content-Type: application/json"</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-H</span><span> </span><span>"Accept: application/json"</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-d</span><span> </span><span>'{"message":"hello"}'</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>https://httpbin.org/post</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个外部 URL 只用于演示命令形式，服务可用性和响应时间不属于程序的确定性保证。</p></section>
<section><h2>日常需要哪些 vcpkg 操作<a href="#日常需要哪些-vcpkg-操作"><span>#</span></a></h2><p>我最后留下的是一组很短的操作表：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 搜索 registry 中的 port</span></div></div><div><div><div>2</div></div><div><span>vcpkg</span><span> </span><span>search</span><span> </span><span>cpr</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 向 manifest 添加直接依赖</span></div></div><div><div><div>5</div></div><div><span>vcpkg</span><span> </span><span>add</span><span> </span><span>port</span><span> </span><span>cpr</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 按当前 manifest 手动安装</span></div></div><div><div><div>8</div></div><div><span>vcpkg</span><span> </span><span>install</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span># 更新 registry baseline</span></div></div><div><div><div>11</div></div><div><span>vcpkg</span><span> </span><span>x-update-baseline</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span># 查看 triplet 帮助</span></div></div><div><div><div>14</div></div><div><span>vcpkg</span><span> </span><span>help</span><span> </span><span>triplet</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在 Manifest mode 中，移除依赖的干净做法是从 <code>vcpkg.json</code> 的 <code>dependencies</code> 删除它，再重新配置或运行 <code>vcpkg install</code>，让声明成为唯一事实来源。不要一边保留 manifest 声明，一边手工删除安装树里的文件，那会把”当前状态”和”期望状态”重新拆开。</p><p>triplet 描述目标架构、操作系统、运行库和链接方式。常见默认值包括 <code>x64-windows</code>、<code>x64-linux</code> 和 <code>x64-osx</code>。如果需要 Windows 静态链接，可以在 preset 的 <code>cacheVariables</code> 中明确设置：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"VCPKG_TARGET_TRIPLET"</span><span>: </span><span>"x64-windows-static"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>改变 triplet、编译器或重要 vcpkg 配置后，我会使用新的 build 目录或删除旧 CMake cache 再配置。CMake cache 记住了第一次配置的工具链；试图在同一个目录里把整套 ABI 偷梁换柱，通常只会得到一段很有教育意义的错误日志。</p><p>vcpkg 默认还会使用 binary cache。第一次从源码构建后，它可以按 ABI hash 复用已构建包；CI 或团队也能通过 <code>VCPKG_BINARY_SOURCES</code> 配置共享文件、HTTP、NuGet 等后端。对这个小项目，本地默认 cache 已经够用。等构建 OpenSSL 的次数开始按团队人数相乘时，再配置共享 cache，收益会非常具体。</p></section>
<section><h2>我实际踩到的坑<a href="#我实际踩到的坑"><span>#</span></a></h2><p><strong><code>find_package()</code> 找不到库。</strong> 最常见原因不是库名拼错，而是配置阶段根本没有加载 vcpkg toolchain。检查 <code>VCPKG_ROOT</code>、preset 展开结果和 build 目录，不要靠全局 <code>include_directories()</code> 把错误暂时盖住。</p><p><strong>下载失败不等于编译失败。</strong> 我第一次拉取 CPR 和 fmt 源码时遇到 <code>curl error 56</code>。重新下载后，vcpkg 继续使用已经完成的 port 和 binary cache，不需要从 0 重建全部 10 个节点。网络、port 构建和项目编译是 3 个阶段，先看失败发生在哪一层。</p><p><strong>不要猜 CMake target 名。</strong> port 安装完成后，vcpkg 会打印 usage，例如 <code>find_package(cpr CONFIG REQUIRED)</code> 和 <code>target_link_libraries(... cpr::cpr)</code>。包名、CMake package 名和 target 名不保证大小写或拼写完全一致，安装输出和 port 文档比直觉可靠。</p><p><strong>不要把 toolchain 路径硬编码进项目。</strong> 本机的 <code>C:\tools\vcpkg</code> 对另一台机器没有意义。共享 preset 引用 <code>$env{VCPKG_ROOT}</code>，本机差异放环境变量或 <code>CMakeUserPresets.json</code>，边界会清楚很多。</p></section>
<section><h2>再往前一步<a href="#再往前一步"><span>#</span></a></h2><p>这次实验最有价值的产物其实不是 100 多行 C++，而是一条可以重复的链路：<code>vcpkg.json</code> 声明依赖，baseline 固定 registry 视图，triplet 描述目标 ABI，vcpkg toolchain 把安装树暴露给 CMake，imported target 再把 usage requirements 传给 <code>vhttp</code>。</p><p>我以后写桌面工具、网络服务或需要第三方库的嵌入式上位机时，大概率会继续沿用这套结构。项目变大后，可以增加自定义 triplet、manifest feature、overlay port 和共享 binary cache，但核心仍然是这 5 个文件和两条 CMake 命令。C++ 仍然没有突然长出一个随语言发行的 Cargo，不过现在至少依赖不再散落在下载目录和项目属性对话框里。很好，我可以继续写 C++ 了。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://learn.microsoft.com/en-us/vcpkg/get_started/get-started" target="_blank">vcpkg 官方入门: Install and use packages with CMake</a></li>
<li><a href="https://learn.microsoft.com/en-us/vcpkg/concepts/manifest-mode" target="_blank">vcpkg Manifest mode</a></li>
<li><a href="https://learn.microsoft.com/en-us/vcpkg/users/buildsystems/cmake-integration" target="_blank">vcpkg 与 CMake 集成</a></li>
<li><a href="https://learn.microsoft.com/en-us/vcpkg/users/versioning" target="_blank">vcpkg Versioning reference</a></li>
<li><a href="https://learn.microsoft.com/en-us/vcpkg/concepts/triplets" target="_blank">vcpkg Triplets</a></li>
<li><a href="https://github.com/libcpr/cpr" target="_blank">CPR</a></li>
<li><a href="https://github.com/CLIUtils/CLI11" target="_blank">CLI11</a></li>
<li><a href="https://github.com/gabime/spdlog" target="_blank">spdlog</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/npm-pnpm-tooling-guide/</id>
      <title type="text">npm 与 pnpm 安装的到底是什么？从 JavaScript 包到原生模块</title>
      <published>2026-08-28T12:00:00.000Z</published>
      <updated>2026-08-28T12:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/npm-pnpm-tooling-guide/"/>
      <summary type="text">我原以为 npm 和 pnpm 安装的只是 JavaScript 或 TypeScript 包，直到遇到 sharp 这类包含原生模块和平台产物的依赖。本文在 WSL2 中动手实验，沿着 registry、lockfile、store、node_modules 和 CLI 入口把这件事弄清楚。</summary>
      <content type="html"><![CDATA[<p>我一开始也以为 npm 和 pnpm 安装的就是 JavaScript 或 TypeScript 包，最多再附带几段配置文件。直到我见到的包越来越多，尤其是 <code>sharp</code> 这类会带上原生 <code>.node</code> 模块、预编译二进制和平台依赖的包，这个模型终于不够用了。于是我在 WSL2 里建了一个隔离项目，沿着 registry、tarball、lockfile、store 和 <code>node_modules</code> 一路观察：我们安装的到底是什么，最后又是谁把它变成了一个可以执行的命令。</p>
<p>本文以 WSL2 中的 Ubuntu 22.04 LTS 为例，命令行提示符统一使用虚构的 <code>user@wsl</code>。路径中的用户名、主机名、硬件、IP、Windows 路径和日志路径均已省略；示例版本和下载数量也会随时间变化，不要把它们当成自己的环境事实。</p>
<p>我会先建立一份隔离实验，再回头解释每个文件和命令为什么出现。这样比背安装咒语可靠得多：包管理器就像仓库管理员，lockfile 是出入库账本，<code>node_modules</code> 则是当前项目真正拿来工作的装配区。</p>
<section><h2>npm、pnpm、Node.js 和 nvm 分别是什么<a href="#npmpnpmnodejs-和-nvm-分别是什么"><span>#</span></a></h2><ul>
<li><strong>Node.js</strong> 是 JavaScript 的运行时，负责执行 JavaScript 程序。</li>
<li><strong>npm</strong> 是 Node.js 生态的包管理器，通常随 Node.js 一起安装。它负责下载依赖、维护 <code>package.json</code> 和 <code>package-lock.json</code>，也能执行项目脚本。</li>
<li><strong>pnpm</strong> 是独立的 Node.js 包管理器。它与 npm 使用同一个 npm registry 生态，但命令、锁文件和 <code>node_modules</code> 的组织方式有所不同。</li>
<li><strong>nvm</strong> 是 Node.js 版本管理器，用来安装和切换 Node.js 版本。它不是包管理器，也不负责安装项目依赖。</li>
<li><strong>Corepack</strong> 是包管理器入口和版本准备工具。它可以根据项目声明准备指定版本的 pnpm 或 Yarn，但它不是 npm，也不负责安装项目依赖。</li>
</ul><section><h3>npm 和 pnpm 怎么选<a href="#npm-和-pnpm-怎么选"><span>#</span></a></h3><p>两者都能创建项目、安装依赖和执行脚本，但默认行为不同：</p>

<table><thead><tr><th>对比项</th><th>npm</th><th>pnpm</th></tr></thead><tbody><tr><td>通常来源</td><td>随 Node.js 发行版提供</td><td>独立安装或由 Corepack 准备</td></tr><tr><td>锁文件</td><td><code>package-lock.json</code></td><td><code>pnpm-lock.yaml</code></td></tr><tr><td>默认依赖布局</td><td>直接组织在项目 <code>node_modules</code> 中</td><td>共享 store，加 virtual store 和链接</td></tr><tr><td>磁盘复用</td><td>依赖具体版本和配置</td><td>共享内容寻址 store，通常更节省空间</td></tr><tr><td>依赖声明约束</td><td>相对宽松</td><td>链接布局更容易暴露未声明依赖</td></tr><tr><td>添加依赖</td><td><code>npm install &lt;pkg&gt;</code></td><td><code>pnpm add &lt;pkg&gt;</code></td></tr></tbody></table><p>推荐规则很简单：<strong>新项目优先考虑 pnpm，已有项目必须遵循项目声明。</strong> 如果项目已经提交了 <code>package-lock.json</code>，通常使用 npm；如果项目已经提交了 <code>pnpm-lock.yaml</code>，就使用 pnpm。不要在同一个项目里交替执行 <code>npm install</code> 和 <code>pnpm install</code>，这样会产生两套锁文件和难以解释的依赖变化。</p><p>pnpm 并不是所有场景的唯一答案。只想快速开始一个小项目，或者参与明确要求 npm 的项目时，npm 更直接。选择的关键不是“哪个命令更酷”，而是项目是否有明确的包管理器契约，以及团队是否统一使用它。</p></section></section>
<section><h2>在 WSL2 中准备 Node.js<a href="#在-wsl2-中准备-nodejs"><span>#</span></a></h2><p>进入 Ubuntu 后，可以先确认系统和 shell。以下输出只保留与教程有关的部分：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>uname</span><span> </span><span>-sr</span></div></div><div><div><div>2</div></div><div><span>Linux</span><span> </span><span>6.x.x-microsoft-standard-WSL2</span></div></div><div><div><div>3</div></div><div><span>user@wsl:~$</span><span> </span><span>bash</span><span> </span><span>--version</span><span> | </span><span>head</span><span> </span><span>-n</span><span> </span><span>1</span></div></div><div><div><div>4</div></div><div><span>GNU</span><span> </span><span>bash,</span><span> </span><span>version</span><span> </span><span>5.1.16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>使用 nvm 安装 Node.js。安装远程脚本前应先阅读来源和内容，因为 <code>curl | bash</code> 会直接执行下载的脚本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>curl</span><span> </span><span>-o-</span><span> </span><span>https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh</span><span> | </span><span>bash</span></div></div><div><div><div>2</div></div><div><span>=</span><span>&gt; </span><span>Downloading</span><span> </span><span>nvm</span><span> </span><span>from</span><span> </span><span>git</span><span> </span><span>to</span><span> </span><span>'$HOME/.nvm'</span></div></div><div><div><div>3</div></div><div><span>=</span><span>&gt; </span><span>Appending</span><span> </span><span>nvm</span><span> </span><span>source</span><span> </span><span>string</span><span> </span><span>to</span><span> </span><span>'$HOME/.bashrc'</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当前 shell 立即加载 nvm：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>.</span><span> </span><span>"</span><span>$HOME</span><span>/.nvm/nvm.sh"</span></div></div><div><div><div>2</div></div><div><span>user@wsl:~$</span><span> </span><span>nvm</span><span> </span><span>install</span><span> </span><span>24</span></div></div><div><div><div>3</div></div><div><span>Downloading</span><span> </span><span>and</span><span> </span><span>installing</span><span> </span><span>node</span><span> </span><span>v24.20.0...</span></div></div><div><div><div>4</div></div><div><span>Computing</span><span> </span><span>checksum</span><span> </span><span>with</span><span> </span><span>sha256sum</span></div></div><div><div><div>5</div></div><div><span>Checksums</span><span> </span><span>matched!</span></div></div><div><div><div>6</div></div><div><span>Now</span><span> </span><span>using</span><span> </span><span>node</span><span> </span><span>v24.20.0</span><span> (npm </span><span>v11.19.0</span><span>)</span></div></div><div><div><div>7</div></div><div><span>Creating</span><span> </span><span>default</span><span> </span><span>alias:</span><span> </span><span>default</span><span> -&gt; </span><span>24</span><span> (-&gt; </span><span>v24.20.0</span><span> </span><span>*</span><span>)</span></div></div><div><div><div>8</div></div><div><span>user@wsl:~$</span><span> </span><span>node</span><span> </span><span>--version</span></div></div><div><div><div>9</div></div><div><span>v24.20.0</span></div></div><div><div><div>10</div></div><div><span>user@wsl:~$</span><span> </span><span>npm</span><span> </span><span>--version</span></div></div><div><div><div>11</div></div><div><span>11.19.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里的 <code>npm</code> 已经随 Node.js 一起提供。此时可以使用 npm，但还没有因为安装 Node.js 就自动得到 pnpm。</p></section>
<section><h2>Corepack 到底做什么<a href="#corepack-到底做什么"><span>#</span></a></h2><p>Corepack 最容易被误解成“另一个包管理器”。更准确的分层是：</p><ol>
<li>Node.js 执行 JavaScript。</li>
<li>npm 是一个包管理器，通常随 Node.js 分发。</li>
<li>pnpm 和 Yarn 是其他包管理器。</li>
<li>Corepack 是包管理器版本管理器，为 pnpm、Yarn 等准备版本并提供转发入口。</li>
</ol><p>Corepack 解决的问题是：<strong>应该运行哪个版本的包管理器</strong>。它不解决：<strong>项目应该安装哪些依赖</strong>。后一个问题仍由 <code>pnpm install</code>、<code>pnpm add</code>、<code>npm install</code> 等命令负责。</p><p>这里有一个容易随 Node.js 版本变化的现实：不能假设每个 Node.js 发行版都内置 Corepack。检查命令是否存在；如果不存在，按项目文档安装 pnpm，或者使用 pnpm 官方提供的安装方式。无论采用哪种方式，最后都要检查实际运行的 <code>pnpm --version</code>，而不是只检查安装命令是否成功。</p><section><h3><code>corepack enable pnpm</code> 发生了什么<a href="#corepack-enable-pnpm-发生了什么"><span>#</span></a></h3><p>在提供 Corepack 的 Node.js 环境中执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>corepack</span><span> </span><span>--version</span></div></div><div><div><div>2</div></div><div><span>0.35.0</span></div></div><div><div><div>3</div></div><div><span>user@wsl:~$</span><span> </span><span>corepack</span><span> </span><span>enable</span><span> </span><span>pnpm</span></div></div><div><div><div>4</div></div><div><span>user@wsl:~$</span><span> </span><span>type</span><span> </span><span>-a</span><span> </span><span>pnpm</span></div></div><div><div><div>5</div></div><div><span>pnpm</span><span> </span><span>is</span><span> </span><span>.../bin/pnpm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>corepack enable pnpm</code> 通常会创建或启用一个名为 <code>pnpm</code> 的 shim。shim 不是完整的 pnpm，它更像一个转接头：用户执行 <code>pnpm</code> 时，shim 把请求交给 Corepack；Corepack 再准备合适版本的 pnpm 并转发参数。</p><p>因此第一次执行 <code>pnpm</code> 时，可能看到下载确认：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>pnpm</span><span> </span><span>--version</span></div></div><div><div><div>2</div></div><div><span>! </span><span>Corepack</span><span> </span><span>is</span><span> </span><span>about</span><span> </span><span>to</span><span> </span><span>download</span><span> </span><span>https://registry.npmjs.org/pnpm/-/pnpm-11.24.0.tgz</span></div></div><div><div><div>3</div></div><div><span>? Do you want to continue? [Y/n] y</span></div></div><div><div><div>4</div></div><div><span>11.24.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这次下载的是 <strong>pnpm 自己</strong>，不是项目依赖。确认下载后，Corepack 保存或复用这个包，之后继续把 <code>pnpm</code> 命令交给它执行。</p></section><section><h3><code>packageManager</code> 和 Corepack 的关系<a href="#packagemanager-和-corepack-的关系"><span>#</span></a></h3><p>项目可以在 <code>package.json</code> 中写：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"packageManager"</span><span>: </span><span>"pnpm@11.22.0"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个字段是项目声明，意思是“处理这个项目时使用 pnpm 11.22.0”。它不是依赖声明，也不会把 <code>pnpm</code> 写入项目的 <code>dependencies</code>。</p><p>如果项目的 <code>.npmrc</code> 还包含：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>manage-package-manager-versions</span><span><span> </span><span>=</span><span> true</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>那么 pnpm 可以根据 <code>packageManager</code> 字段参与包管理器版本管理。可以把两者理解为：</p><ul>
<li><code>packageManager</code>：项目声明应该使用什么工具和版本。</li>
<li><code>manage-package-manager-versions</code>：pnpm 是否允许按这项声明管理工具版本。</li>
<li>Corepack shim：让命令能够进入版本准备和转发流程。</li>
<li><code>pnpm install</code>：真正安装项目依赖。</li>
</ul><p>这个配置键是 pnpm 的设置，不是 npm 的通用配置。若同一目录还会运行 npm，npm 可能提示 <code>Unknown project config</code>；这不是安装失败，但说明不应把 pnpm 专属选项当成跨工具协议。项目最好只保留实际使用的包管理器配置，并把团队约定写进项目文档或自动检查中。</p><p>检查四个版本时，命令和输出应分别理解：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>node</span><span> </span><span>--version</span></div></div><div><div><div>2</div></div><div><span>v24.20.0</span></div></div><div><div><div>3</div></div><div><span>user@wsl:~$</span><span> </span><span>npm</span><span> </span><span>--version</span></div></div><div><div><div>4</div></div><div><span>11.19.0</span></div></div><div><div><div>5</div></div><div><span>user@wsl:~$</span><span> </span><span>corepack</span><span> </span><span>--version</span></div></div><div><div><div>6</div></div><div><span>0.35.0</span></div></div><div><div><div>7</div></div><div><span>user@wsl:~$</span><span> </span><span>pnpm</span><span> </span><span>--version</span></div></div><div><div><div>8</div></div><div><span>11.24.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果某个 Node.js 发行版没有提供 Corepack，<code>corepack</code> 命令可能不存在。这时应按照项目文档安装 pnpm，最后仍然用 <code>pnpm --version</code> 检查实际运行版本。全局安装 pnpm、Corepack 提供命令入口、<code>pnpm add</code> 安装项目依赖，是三个不同动作。</p><div><div><div></div><div>Warning</div></div><div><p>不要把 <code>corepack enable</code>、全局安装 pnpm 和 <code>pnpm add</code> 混成一个动作。前两者处理“命令从哪里来、运行哪个版本”，最后一个才修改当前项目的依赖。</p></div></div></section></section>
<section><h2>用隔离项目观察安装过程<a href="#用隔离项目观察安装过程"><span>#</span></a></h2><p>不要为了学习包管理器直接修改重要项目。创建临时目录后，在其中安装几个形态不同的包：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>tmp_dir=</span><span>$(</span><span>mktemp</span><span> </span><span>-d</span><span>)</span></div></div><div><div><div>2</div></div><div><span>user@wsl:~$</span><span> </span><span>echo</span><span> </span><span>"</span><span>$tmp_dir</span><span>"</span></div></div><div><div><div>3</div></div><div><span>/tmp/tmp.xxxxxxxx</span></div></div><div><div><div>4</div></div><div><span>user@wsl:~$</span><span> </span><span>cd</span><span> </span><span>"</span><span>$tmp_dir</span><span>"</span></div></div><div><div><div>5</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>init</span></div></div><div><div><div>6</div></div><div><span>Wrote</span><span> </span><span>to</span><span> </span><span>/tmp/tmp.xxxxxxxx/package.json</span></div></div><div><div><div>7</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>add</span><span> </span><span>lodash</span><span> </span><span>typescript</span><span> </span><span>sharp</span></div></div><div><div><div>8</div></div><div><span>Packages:</span><span> </span><span>+9</span></div></div><div><div><div>9</div></div><div><span>Progress:</span><span> </span><span>resolved</span><span> </span><span>54,</span><span> </span><span>reused</span><span> </span><span>0,</span><span> </span><span>downloaded</span><span> </span><span>12,</span><span> </span><span>added</span><span> </span><span>9,</span><span> </span><span>done</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span> </span><span>dependencies:</span></div></div><div><div><div>12</div></div><div><span>+</span><span> </span><span>lodash</span><span> </span><span>4.18.1</span></div></div><div><div><div>13</div></div><div><span>+</span><span> </span><span>sharp</span><span> </span><span>0.35.4</span></div></div><div><div><div>14</div></div><div><span>+</span><span> </span><span>typescript</span><span> </span><span>7.0.2</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>Done</span><span> </span><span>in</span><span> </span><span>6.5s</span><span> </span><span>using</span><span> </span><span>pnpm</span><span> </span><span>v11.24.0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里的版本和下载数量会变化。重要的是动作：<code>pnpm init</code> 创建 <code>package.json</code>，<code>pnpm add</code> 修改依赖声明、解析依赖并创建安装目录。</p><p>查看生成的声明：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>cat</span><span> </span><span>package.json</span></div></div><div><div><div>2</div></div><div><span>{</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"name"</span><span>:</span><span> </span><span>"tmp-project",</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"version"</span><span>:</span><span> </span><span>"1.0.0",</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"packageManager"</span><span>:</span><span> </span><span>"pnpm@11.24.0",</span></div></div><div><div><div>6</div></div><div><span>  </span><span>"type"</span><span>:</span><span> </span><span>"module",</span></div></div><div><div><div>7</div></div><div><span>  </span><span>"dependencies"</span><span>:</span><span> </span><span>{</span></div></div><div><div><div>8</div></div><div><span>    </span><span>"lodash"</span><span>:</span><span> </span><span>"^4.18.1",</span></div></div><div><div><div>9</div></div><div><span>    </span><span>"sharp"</span><span>:</span><span> </span><span>"^0.35.4",</span></div></div><div><div><div>10</div></div><div><span>    </span><span>"typescript"</span><span>:</span><span> </span><span>"^7.0.2"</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pnpm init</code> 在不同版本中生成的字段可能不同。不要把临时项目的名字、版本或格式当成固定模板。</p><section><h3>用 npm 做一次对照<a href="#用-npm-做一次对照"><span>#</span></a></h3><p>我会再开一个目录，只安装一个开发工具。这样实验不会把 npm 和 pnpm 的文件混在一起：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:~$</span><span> </span><span>npm_dir=</span><span>$(</span><span>mktemp</span><span> </span><span>-d</span><span>)</span></div></div><div><div><div>2</div></div><div><span>user@wsl:~$</span><span> </span><span>cd</span><span> </span><span>"</span><span>$npm_dir</span><span>"</span></div></div><div><div><div>3</div></div><div><span>user@wsl:/tmp/tmp.yyyyyyyy$</span><span> </span><span>npm</span><span> </span><span>init</span><span> </span><span>-y</span></div></div><div><div><div>4</div></div><div><span>Wrote</span><span> </span><span>to</span><span> </span><span>/tmp/tmp.yyyyyyyy/package.json</span></div></div><div><div><div>5</div></div><div><span>user@wsl:/tmp/tmp.yyyyyyyy$</span><span> </span><span>npm</span><span> </span><span>install</span><span> </span><span>--save-dev</span><span> </span><span>typescript</span></div></div><div><div><div>6</div></div><div><span>added</span><span> </span><span>1</span><span> </span><span>package,</span><span> </span><span>and</span><span> </span><span>audited</span><span> </span><span>2</span><span> </span><span>packages</span><span> </span><span>in</span><span> </span><span>1s</span></div></div><div><div><div>7</div></div><div><span>user@wsl:/tmp/tmp.yyyyyyyy$</span><span> </span><span>npm</span><span> </span><span>exec</span><span> </span><span>tsc</span><span> </span><span>--</span><span> </span><span>--version</span></div></div><div><div><div>8</div></div><div><span>Version</span><span> </span><span>7.0.2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这个目录会得到 <code>package-lock.json</code>，而不是 <code>pnpm-lock.yaml</code>。<code>npm exec</code> 和 <code>pnpm exec</code> 都优先使用项目本地的 CLI，但两者不会共享锁文件格式，也不应该在同一个项目里轮流负责更新依赖。这个对照实验的价值不在于哪一个命令少敲两个字符，而在于把“同一个 registry 生态”和“不同的项目安装实现”分开观察。</p></section></section>
<section><h2>包不是语言<a href="#包不是语言"><span>#</span></a></h2><p>registry 发布的是带元数据的 tarball，而不是“一个 JavaScript 文件”。包中可以包含：</p><ul>
<li>JavaScript 或 TypeScript 源码</li>
<li>WASM 文件</li>
<li>原生 <code>.node</code> 模块</li>
<li>预编译二进制</li>
<li>CLI 入口</li>
<li>其他包的运行时或平台依赖</li>
</ul><p>实验中的三个包分别说明了不同情况：</p><ul>
<li><code>lodash</code> 主要作为可导入的 JavaScript API 使用。</li>
<li><code>typescript</code> 提供 API，也通过 <code>bin</code> 字段提供 <code>tsc</code> 命令。</li>
<li><code>sharp</code> 会根据操作系统和 CPU 解析平台相关实现，安装时可能出现 <code>@img/*</code> 等平台包。</li>
</ul><p>查询 registry 元数据：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>view</span><span> </span><span>lodash</span><span> </span><span>dist.tarball</span></div></div><div><div><div>2</div></div><div><span>https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz</span></div></div><div><div><div>3</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>view</span><span> </span><span>typescript</span><span> </span><span>bin</span><span> </span><span>--json</span></div></div><div><div><div>4</div></div><div><span>{</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"tsc"</span><span>:</span><span> </span><span>"bin/tsc"</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>bin</code> 字段表示包声明了命令入口。安装后，包管理器会在项目中创建 <code>node_modules/.bin/tsc</code> 的链接或 shim：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>exec</span><span> </span><span>tsc</span><span> </span><span>--version</span></div></div><div><div><div>2</div></div><div><span>Version</span><span> </span><span>7.0.2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>tsc</code> 不是 shell 自带命令，也不是独立安装的系统程序。它来自 TypeScript 包的 <code>bin</code> 声明，最终由 Node.js 执行。判断一个包能做什么，应查看它的 <code>exports</code>、<code>main</code>、<code>bin</code>、平台依赖和安装脚本，而不是只看包名。</p></section>
<section><h2>一次安装改变了哪些东西<a href="#一次安装改变了哪些东西"><span>#</span></a></h2><p>可以把项目依赖分成三层：</p><ol>
<li><strong>声明层</strong>：<code>package.json</code> 中的 <code>dependencies</code> 和 <code>devDependencies</code>。</li>
<li><strong>解析层</strong>：lockfile 中的实际版本、间接依赖、peer dependency 和完整性信息。</li>
<li><strong>落盘层</strong>：store、virtual store、<code>node_modules</code> 链接和 <code>.bin</code> 入口。</li>
</ol><p>运行 <code>pnpm add lodash</code> 时，大致发生四步：</p><ol>
<li>读取项目边界和 registry 配置。</li>
<li>查询包元数据，按照 semver 范围选择版本并写入 lockfile。</li>
<li>下载 tarball，校验后写入内容寻址 store。</li>
<li>在项目中创建依赖链接和命令入口。</li>
</ol><p>用命令查看这些层：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>root</span></div></div><div><div><div>2</div></div><div><span>/tmp/tmp.xxxxxxxx/node_modules</span></div></div><div><div><div>3</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>store</span><span> </span><span>path</span></div></div><div><div><div>4</div></div><div><span>&lt;home-directory&gt;/.local/share/pnpm/store/v11</span></div></div><div><div><div>5</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>realpath</span><span> </span><span>node_modules/lodash</span></div></div><div><div><div>6</div></div><div><span>/tmp/tmp.xxxxxxxx/node_modules/.pnpm/lodash@4.18.1/node_modules/lodash</span></div></div><div><div><div>7</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>why</span><span> </span><span>sharp</span></div></div><div><div><div>8</div></div><div><span>sharp@0.35.4</span></div></div><div><div><div>9</div></div><div><span>└──</span><span> </span><span>tmp-project@1.0.0</span><span> (dependencies)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>pnpm 的 store 像共享零件仓，<code>node_modules/.pnpm</code> 像当前项目的装配区，顶层 <code>node_modules/lodash</code> 是给 Node.js 看的入口。多个项目可以复用 store，但项目自己的 <code>package.json</code>、lockfile、链接和 peer 组合仍然独立。</p><p>npm 的目标相同，但默认的依赖布局不同。应用代码应依赖包公开的 <code>exports</code> 或 <code>main</code>，不要依赖某个包管理器偶然生成的内部路径。</p><p><code>^1.2.3</code> 是版本范围，不是唯一版本。它允许一定范围内的更新；lockfile 则记录这次实际选择的版本。<code>pnpm install --frozen-lockfile</code> 会拒绝在安装时偷偷修改 lockfile，适合 CI 和新机器：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/path/to/project$</span><span> </span><span>pnpm</span><span> </span><span>install</span><span> </span><span>--frozen-lockfile</span></div></div><div><div><div>2</div></div><div><span>Lockfile</span><span> </span><span>is</span><span> </span><span>up</span><span> </span><span>to</span><span> </span><span>date,</span><span> </span><span>resolution</span><span> </span><span>step</span><span> </span><span>is</span><span> </span><span>skipped</span></div></div><div><div><div>3</div></div><div><span>Already</span><span> </span><span>up</span><span> </span><span>to</span><span> </span><span>date</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>npm 项目通常使用 <code>npm ci</code> 达到类似的“按已有锁文件安装”效果。<code>pnpm install</code> 和 <code>pnpm update</code> 也不是一回事：前者按当前声明安装，后者主动寻找允许范围内的新版本。</p></section>
<section><h2>命令为什么能运行<a href="#命令为什么能运行"><span>#</span></a></h2><p>普通 shell 通过 <code>PATH</code> 查找命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>command</span><span> </span><span>-v</span><span> </span><span>node</span></div></div><div><div><div>2</div></div><div><span>&lt;node-installation&gt;/bin/node</span></div></div><div><div><div>3</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>type</span><span> </span><span>-a</span><span> </span><span>pnpm</span></div></div><div><div><div>4</div></div><div><span>pnpm</span><span> </span><span>is</span><span> </span><span>&lt;node-installation&gt;/bin/pnpm</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>执行项目脚本时，npm 和 pnpm 会临时把项目的 <code>node_modules/.bin</code> 放到脚本环境的 PATH 前面。因此，安装 TypeScript 后可以在 <code>package.json</code> 中定义：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"scripts"</span><span>: {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"type-check"</span><span>: </span><span>"tsc --version"</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>普通 shell 不会自动注入这层 PATH。可以通过以下两种入口明确执行同一个本地 <code>tsc</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/path/to/project$</span><span> </span><span>pnpm</span><span> </span><span>exec</span><span> </span><span>tsc</span><span> </span><span>--version</span></div></div><div><div><div>2</div></div><div><span>Version</span><span> </span><span>7.0.2</span></div></div><div><div><div>3</div></div><div><span>user@wsl:/path/to/project$</span><span> </span><span>node_modules/.bin/tsc</span><span> </span><span>--version</span></div></div><div><div><div>4</div></div><div><span>Version</span><span> </span><span>7.0.2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四种常见入口可以这样区分：</p><ol>
<li><code>node src/index.mjs</code>：Node.js 直接运行文件。</li>
<li><code>pnpm exec tsc --version</code>：从当前项目依赖中寻找并运行 <code>tsc</code>。</li>
<li><code>pnpm run dev</code>：读取 <code>package.json</code> 的 <code>scripts</code>，并注入本地 <code>.bin</code> PATH。</li>
<li>脚本启动 Node.js、shell 或其他子进程：子进程继承脚本环境。</li>
</ol><section><h3><code>npm run</code>、<code>pnpm run</code> 和直接执行<a href="#npm-runpnpm-run-和直接执行"><span>#</span></a></h3><p><code>npm run build</code> 与 <code>pnpm run build</code> 都会读取 <code>package.json</code> 的 <code>scripts</code> 字段，并把项目的 <code>node_modules/.bin</code> 放到脚本环境的 <code>PATH</code> 前面。脚本因此可以直接调用本地安装的 <code>tsc</code>、<code>astro</code> 或 <code>vite</code>，不需要用户把它们全局安装一份。</p><p>我通常把入口分成三类：</p><ol>
<li><code>node src/index.mjs</code>：Node.js 直接运行文件，完全绕开包管理器脚本。</li>
<li><code>pnpm exec tsc --version</code>：从当前项目依赖中寻找并运行本地 CLI。</li>
<li><code>pnpm run type-check</code>：读取脚本定义，并注入 <code>.bin</code> PATH；npm 项目对应 <code>npm run type-check</code>。</li>
</ol><p><code>pnpm dlx</code>、<code>npx</code> 和 <code>npm exec</code> 适合一次性获取并执行 CLI。它们的便利之处也是风险所在：命令可能触发一次临时下载，且不会把工具记录为项目依赖。需要可复现的工具，应写入 <code>devDependencies</code>，再通过脚本或 <code>pnpm exec</code> 使用。</p></section></section>
<section><h2>registry、换源和网络故障<a href="#registry换源和网络故障"><span>#</span></a></h2><p>registry 是提供包元数据和 tarball 的 HTTP 服务。它可以是 npm 默认 registry、第三方镜像、公司私有 registry 或代理缓存。切换 registry 只改变查询和下载的来源，不改变项目依赖目录，也不会自动修复错误的版本范围。</p><p>我会先检查当前配置，而不是直接删除缓存：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>config</span><span> </span><span>get</span><span> </span><span>registry</span></div></div><div><div><div>2</div></div><div><span>https://registry.npmjs.org/</span></div></div><div><div><div>3</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>npm</span><span> </span><span>config</span><span> </span><span>get</span><span> </span><span>registry</span></div></div><div><div><div>4</div></div><div><span>https://registry.npmjs.org/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只验证一次时，使用命令行参数，不改变配置文件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>view</span><span> </span><span>lodash</span><span> </span><span>version</span><span> </span><span>--registry=https://registry.npmjs.org/</span></div></div><div><div><div>2</div></div><div><span>4.18.1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>项目级 <code>.npmrc</code> 适合项目配置，用户级配置适合个人默认值。认证信息不要写死在仓库中：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//registry.example.invalid/:</span><span>_authToken</span><span><span>=</span><span>${NPM_TOKEN}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>遇到 <code>ERR_PNPM_NO_MATCHING_VERSION</code>，先检查包名、registry 和可用版本，再检查 semver、lockfile 和平台限制。遇到网络超时或证书错误，依次检查 registry、DNS、代理、系统时间和 CA。不要把清空整个缓存当成万能修复。</p></section>
<section><h2>audit 与生命周期脚本<a href="#audit-与生命周期脚本"><span>#</span></a></h2><p><code>audit</code> 会把依赖树中的包名和版本与已知漏洞公告进行匹配。它不审查业务代码，也不能证明没有报告的包绝对安全：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>audit</span></div></div><div><div><div>2</div></div><div><span>No</span><span> </span><span>known</span><span> </span><span>vulnerabilities</span><span> </span><span>found</span></div></div><div><div><div>3</div></div><div><span>user@wsl:/tmp/tmp.xxxxxxxx$</span><span> </span><span>pnpm</span><span> </span><span>audit</span><span> </span><span>--prod</span></div></div><div><div><div>4</div></div><div><span>No</span><span> </span><span>known</span><span> </span><span>vulnerabilities</span><span> </span><span>found</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>安装依赖本身也可能执行代码。<code>preinstall</code>、<code>install</code>、<code>postinstall</code> 和 <code>prepare</code> 可能编译 native addon、下载平台产物、生成代码，甚至执行任意脚本。安装前应审查包来源和生命周期脚本；在 CI 中可以评估 <code>--ignore-scripts</code> 是否适合项目，但不要把关闭安全检查当成第一步修复。</p><p>如果项目的 <code>package.json</code> 声明了 <code>devEngines.packageManager</code>，却使用了错误的包管理器，npm 可能报：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>npm error code EBADDEVENGINES</span></div></div><div><div><div>2</div></div><div><span>npm error Invalid name "pnpm" does not match "npm"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这表示当前 npm 不符合项目要求，不是一次漏洞扫描结果。正确做法是切换到项目声明的 pnpm，而不是删除检查或绕过它。</p></section>
<section><h2>从零建立可迁移环境<a href="#从零建立可迁移环境"><span>#</span></a></h2><p>新机器可以按以下顺序操作：</p><ol>
<li>在 WSL2 中准备 Ubuntu 和 shell。</li>
<li>使用 nvm 安装满足项目 <code>engines</code> 的 Node.js，并确认 shell 启动文件已加载 nvm。</li>
<li>检查 <code>node --version</code> 和 <code>npm --version</code>。</li>
<li>启用或安装项目规定的 pnpm，并检查 <code>corepack --version</code>（若存在）和 <code>pnpm --version</code>。</li>
<li>进入项目根目录，先阅读 <code>package.json</code> 的 <code>packageManager</code>、<code>engines</code> 和 lockfile 类型。</li>
<li>执行项目规定的锁定安装：pnpm 项目使用 <code>pnpm install --frozen-lockfile</code>，npm 项目通常使用 <code>npm ci</code>。</li>
<li>按项目文档执行开发、检查、构建和预览命令。</li>
</ol><p>如果项目位于 <code>/mnt/c</code> 或其他 Windows 挂载路径，安装和大量小文件访问可能明显慢于 Linux 文件系统中的 <code>~/projects</code>。这是 I/O 边界的代价，不是 pnpm store 损坏；我通常把源码和 store 放在 WSL2 的 Linux 文件系统里，只通过编辑器或 Git 与 Windows 侧协作。</p><p>最小排错表：</p>

<table><thead><tr><th>症状</th><th>先查什么</th><th>最小修复</th><th>如何确认</th></tr></thead><tbody><tr><td><code>command not found</code></td><td><code>command -v pnpm</code>、<code>echo $PATH</code></td><td>修正 PATH 或 Corepack 入口</td><td><code>pnpm --version</code></td></tr><tr><td><code>ERR_PNPM_NO_MATCHING_VERSION</code></td><td>包名、registry、可用版本</td><td>修正版本范围或 registry</td><td><code>pnpm view &lt;pkg&gt; versions</code></td></tr><tr><td>registry 超时或证书错误</td><td>registry、代理、系统时间</td><td>修正源、DNS、代理或 CA</td><td>单次 <code>pnpm view</code> 成功</td></tr><tr><td>lockfile 不同步</td><td><code>git diff package.json pnpm-lock.yaml</code></td><td>使用正确管理器更新锁文件</td><td><code>pnpm install --frozen-lockfile</code></td></tr><tr><td>Node 不满足 engines</td><td><code>node --version</code></td><td>切换 Node.js 版本</td><td>版本满足 <code>engines</code></td></tr><tr><td>native 模块安装失败</td><td><code>pnpm why &lt;pkg&gt;</code>、平台包和安装日志</td><td>修复平台依赖或编译链</td><td>实际导入模块</td></tr><tr><td>脚本找不到本地 CLI</td><td><code>pnpm exec &lt;cmd&gt;</code>、脚本 PATH</td><td>使用项目脚本或 <code>pnpm exec</code></td><td><code>&lt;cmd&gt; --version</code></td></tr><tr><td><code>EBADDEVENGINES</code></td><td><code>packageManager</code>、当前包管理器</td><td>使用项目声明的管理器</td><td><code>pnpm --version</code></td></tr></tbody></table><p>整条路径可以概括为：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Shell PATH</p></span></div><div><span><p>npm 或 pnpm</p></span></div><div><span><p>registry 元数据</p></span></div><div><span><p>tarball</p></span></div><div><span><p>pnpm store</p></span></div><div><span><p>virtual store</p></span></div><div><span><p>node_modules 链接</p></span></div><div><span><p>.bin shim</p></span></div><div><span><p>Node.js 进程</p></span></div><div><span><p>package.json scripts</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>Shell PATH</p></span></div><div><span><p>npm 或 pnpm</p></span></div><div><span><p>registry 元数据</p></span></div><div><span><p>tarball</p></span></div><div><span><p>pnpm store</p></span></div><div><span><p>virtual store</p></span></div><div><span><p>node_modules 链接</p></span></div><div><span><p>.bin shim</p></span></div><div><span><p>Node.js 进程</p></span></div><div><span><p>package.json scripts</p></span></div>
</div></div></div><p>真正值得记住的不是一串安装咒语，而是这条边界链：包从哪个 registry 来，版本解析记录在哪里，文件落在哪一层，命令由哪个入口转给哪个 Node.js。顺着这条链排查，npm 和 pnpm 的差异就会变成可以观察和验证的实现选择。未来包管理器大概还会继续把“下载一个包”包装成更多自动化，但 lockfile、脚本和运行时边界不会凭空消失；理解它们，至少能让下一次 <code>command not found</code> 变成一个可定位的实验，而不是一次祈祷。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://docs.npmjs.com/cli/v11/configuring-npm/package-json" target="_blank">npm package.json 文档</a></li>
<li><a href="https://docs.npmjs.com/cli/v11/using-npm/scripts" target="_blank">npm scripts 文档</a></li>
<li><a href="https://docs.npmjs.com/auditing-package-dependencies-for-security-vulnerabilities" target="_blank">npm audit 文档</a></li>
<li><a href="https://pnpm.io/cli/add" target="_blank">pnpm add 文档</a></li>
<li><a href="https://pnpm.io/package_json" target="_blank">pnpm package.json 文档</a></li>
<li><a href="https://pnpm.io/cli/store" target="_blank">pnpm store 文档</a></li>
<li><a href="https://pnpm.io/settings/node-modules" target="_blank">pnpm Node-Modules 设置</a></li>
<li><a href="https://nodejs.org/api/corepack.html" target="_blank">Node.js Corepack 文档</a></li>
<li><a href="https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager" target="_blank">Node.js npm 入门指南</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/cudy-tr3000-immortalwrt-flashing-guide/</id>
      <title type="text">Cudy TR3000 256 MB 刷入 ImmortalWrt：一次完整的图形化流程</title>
      <published>2026-08-27T01:00:00.000Z</published>
      <updated>2026-08-27T01:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/cudy-tr3000-immortalwrt-flashing-guide/"/>
      <summary type="text">记录 Cudy TR3000 256 MB V1 从原厂固件、签名中间固件到 ImmortalWrt 25.12.1 和自定义镜像的完整图形化刷机流程，并介绍官方 TFTP 恢复方法。</summary>
      <content type="html"><![CDATA[<div><div><div></div><div>刷机风险提示</div></div><div><p>以下风险提示来自 Cudy 官方京东自营店，具体情况请以官方最新说明为准：</p><ol>
<li>刷机后遇到的相关问题，需您自主寻求解决方案，店铺不提供技术支持。</li>
<li>刷机后不支持退货退款，保修期内最多支持两次因刷机产生的售后。</li>
<li>保留权益：除刷机产生的售后外，其他质量问题在保修期内仍可享受正常售后。</li>
</ol><p>本文只记录我这台 <strong>Cudy TR3000 256 MB V1</strong> 的实际操作。刷机有风险，请确认自己能接受设备无法启动、需要恢复固件，甚至失去部分售后支持的结果。官方政策和设备支持范围可能变化，购买和操作前请以 Cudy 官方说明为准。</p></div></div>
<p>我在 2025 年 9 月 1 日买过一台 Cudy TR3000 256 MB V1，刷入 ImmortalWrt 后用了接近一年，网络稳定性和日常体验都还不错。2026 年 8 月 24 日，机缘巧合之下，我又买了一台同型号设备，于是决定把这次从原厂系统到 ImmortalWrt，再到自定义镜像和官方恢复的完整过程记录下来。</p>
<p>这次我没有拆机，也没有进入 UART 或 U-Boot。真正做的事情基本都在浏览器里完成，唯一稍微像“网络工程”的部分是最后用 Tftpd64 通过 TFTP 把官方固件送回路由器。整个过程更像一条网页里的接力赛：原厂网页先交棒给中间固件，中间固件再交棒给 ImmortalWrt，最后我还让它跑了一圈自定义构建。</p>
<section><h2>先确认：这是 256 MB 版本<a href="#先确认这是-256-mb-版本"><span>#</span></a></h2><p>这台设备背面标签右上角明确写着 <strong>256 MB 闪存版</strong>，产品型号是 <code>TR3000</code>，管理地址是 <code>192.168.10.1</code>。</p><p></p><figure><img src="/assets/images/cudy-tr3000/2026-08-24-device-label.jpg" alt="2026 年购买的 Cudy TR3000 256 MB V1 原始背面标签照片" /><figcaption>2026 年购买的 Cudy TR3000 256 MB V1 原始背面标签照片</figcaption></figure><p></p><p>这台设备的 SN 前缀是 <code>S0001825</code>。去年那台设备的 SN 前缀则是 <code>TR3000</code>。两台机器标签形式不同，但都明确标注了 256 MB 闪存版本。本文只记录这两台 256 MB V1 设备，不把其他批次的 SN 规则推广到本设备之外。</p><p></p><figure><img src="/assets/images/cudy-tr3000/2025-09-01-device-label.jpg" alt="2025 年购买的 Cudy TR3000 256 MB V1 原始背面标签照片" /><figcaption>2025 年购买的 Cudy TR3000 256 MB V1 原始背面标签照片</figcaption></figure><p></p><div><div><div></div><div>Important</div></div><div><p>文章中的文件名、界面和分区选择只适用于明确标注为 <strong>Cudy TR3000 256 MB V1</strong> 的设备。不要只凭 SN 猜测硬件版本，应该先确认机身标签上的容量标识。</p></div></div></section>
<section><h2>第一次开机：先把原厂网页走完<a href="#第一次开机先把原厂网页走完"><span>#</span></a></h2><p>我建议全程使用网线，把电脑连接到路由器的 LAN 口。开始前关闭电脑上的其他网络连接，避免电脑通过 Wi-Fi 或另一块网卡走错路由。刷写过程中保持 USB-C 供电稳定，不要碰电源线。</p><p>打开：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>http://192.168.10.1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第一次登录时，原厂页面会要求创建管理员密码。我这里设置成了 <code>00000000</code>，只是为了演示方便；实际使用请设置一个强密码，并在发布截图前自行处理其中的敏感信息。</p><p></p><figure><img src="/assets/images/cudy-tr3000/01-create-admin-password.jpg" alt="原厂首次登录时创建管理员密码的页面" /><figcaption>原厂首次登录时创建管理员密码的页面</figcaption></figure><p></p><p>设置密码后，系统会进入快速设置引导。我按下面的顺序一路点击“下一步”：</p><ol>
<li>选择模式。</li>
<li>选择时区。</li>
<li>配置 WAN。</li>
<li>配置无线网络。</li>
<li>查看概要。</li>
<li>点击“保存&amp;应用”。</li>
</ol><p></p><figure><img src="/assets/images/cudy-tr3000/02-choose-mode.jpg" alt="原厂快速设置中的模式选择页面" /><figcaption>原厂快速设置中的模式选择页面</figcaption></figure><p></p><p></p><figure><img src="/assets/images/cudy-tr3000/03-choose-timezone.jpg" alt="原厂快速设置中的时区选择页面" /><figcaption>原厂快速设置中的时区选择页面</figcaption></figure><p></p><p></p><figure><img src="/assets/images/cudy-tr3000/04-configure-wan.jpg" alt="原厂快速设置中的 WAN 配置页面" /><figcaption>原厂快速设置中的 WAN 配置页面</figcaption></figure><p></p><p></p><figure><img src="/assets/images/cudy-tr3000/05-configure-wireless.jpg" alt="原厂快速设置中的无线配置页面" /><figcaption>原厂快速设置中的无线配置页面</figcaption></figure><p></p><p></p><figure><img src="/assets/images/cudy-tr3000/06-setup-summary.jpg" alt="原厂快速设置的概要页面" /><figcaption>原厂快速设置的概要页面</figcaption></figure><p></p><p>完成设置后，进入原厂系统界面。页面底部的浅灰色文字包含版权信息、硬件版本、系统版本、Cudy 官网和售后邮件地址。再进入“系统”，可以看到更完整的系统信息，两处内容能够互相对应。</p><p></p><figure><img src="/assets/images/cudy-tr3000/07-cudy-system-page.jpg" alt="原厂系统主页，底部显示硬件版本和系统版本信息" /><figcaption>原厂系统主页，底部显示硬件版本和系统版本信息</figcaption></figure><p></p><p></p><figure><img src="/assets/images/cudy-tr3000/08-cudy-system-information.jpg" alt="原厂系统信息页面" /><figcaption>原厂系统信息页面</figcaption></figure><p></p></section>
<section><h2>第一步：刷入签名中间固件<a href="#第一步刷入签名中间固件"><span>#</span></a></h2><p>原厂升级入口会检查固件签名。直接把 ImmortalWrt 镜像上传到原厂网页，结果就是报错，这并不奇怪。Cudy 官方提供了一个签名中间固件，先用它绕过原厂网页这一层限制，再从中间系统安装我们真正想用的系统。</p><p>我使用的是 Cudy 官方 Google Drive 文件夹中的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>TR3000 256MB Flash V1 (not for TR3000 V1)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>其中的文件名是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cudy_tr3000-256mb-v1-sysupgrade.bin</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>官方入口：<a href="https://www.cudy.com/zh-CN/blogs/faq/openwrt-software-download" target="_blank">Cudy OpenWrt 软件和中间固件下载</a>。页面底部提供了 <a href="https://drive.google.com/drive/folders/1BKVarlwlNxf7uJUtRhuMGUqeCa5KpMnj" target="_blank">Google Drive 中间固件文件夹</a>，进入 <code>TR3000 256MB Flash V1 (not for TR3000 V1)</code> 文件夹后下载对应文件。下载后先把文件保存好，不要等到网页已经打开时才临时寻找。</p><p>如果直接上传未签名或不匹配的文件，原厂页面会显示错误。</p><p></p><figure><img src="/assets/images/cudy-tr3000/09-signature-upload-error.jpg" alt="原厂网页上传不匹配固件时的报错页面" /><figcaption>原厂网页上传不匹配固件时的报错页面</figcaption></figure><p></p><p>进入“基本设置”，选择“固件升级”。</p><p></p><figure><img src="/assets/images/cudy-tr3000/10-cudy-firmware-upgrade.jpg" alt="原厂基本设置中的固件升级入口" /><figcaption>原厂基本设置中的固件升级入口</figcaption></figure><p></p><p>点击上传，选择 <code>cudy_tr3000-256mb-v1-sysupgrade.bin</code>。</p><p></p><figure><img src="/assets/images/cudy-tr3000/11-upload-intermediate-firmware.jpg" alt="原厂网页上传 256 MB 签名中间固件" /><figcaption>原厂网页上传 256 MB 签名中间固件</figcaption></figure><p></p><p>点击“继续”后，页面会弹出刷写进度。此时不要关闭页面、拔掉网线或断开电源。</p><p></p><figure><img src="/assets/images/cudy-tr3000/12-intermediate-flashing.jpg" alt="签名中间固件正在刷写" /><figcaption>签名中间固件正在刷写</figcaption></figure><p></p><p>刷写完成后，路由器会自动重启。</p><p></p><figure><img src="/assets/images/cudy-tr3000/13-intermediate-rebooting.jpg" alt="签名中间固件刷写完成，路由器正在重启" /><figcaption>签名中间固件刷写完成，路由器正在重启</figcaption></figure><p></p><p>这个过程中指示灯会变红。等到路由器完成重启，指示灯恢复白色，再继续下一步。</p></section>
<section><h2>第二步：进入中间系统<a href="#第二步进入中间系统"><span>#</span></a></h2><p>原厂系统的地址是 <code>192.168.10.1</code>，中间系统启动后地址变为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>http://192.168.1.1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>中间系统第一次登录时默认没有密码，直接点击“登录”即可。</p><p></p><figure><img src="/assets/images/cudy-tr3000/14-intermediate-login.jpg" alt="重启后进入中间固件的登录页面" /><figcaption>重启后进入中间固件的登录页面</figcaption></figure><p></p><p>登录后可以确认当前系统已经从 Cudy 原厂系统变成了 OpenWrt 中间固件。</p><p></p><figure><img src="/assets/images/cudy-tr3000/15-intermediate-system-page.jpg" alt="中间固件的系统主页" /><figcaption>中间固件的系统主页</figcaption></figure><p></p><p>这里是一个很容易误判的节点：现在还没有完成 ImmortalWrt 安装，只是完成了从原厂签名系统到 OpenWrt 中间系统的过渡。</p></section>
<section><h2>第三步：刷入 ImmortalWrt 25.12.1<a href="#第三步刷入-immortalwrt-25121"><span>#</span></a></h2><p>我使用 ImmortalWrt Firmware Selector 的固定地址选择设备：</p><p><a href="https://firmware-selector.immortalwrt.org/?version=25.12.1&amp;target=mediatek%2Ffilogic&amp;id=cudy_tr3000-256mb-v1" target="_blank">https://firmware-selector.immortalwrt.org/?version=25.12.1&amp;target=mediatek%2Ffilogic&amp;id=cudy_tr3000-256mb-v1</a></p><p>页面中的设备 profile 必须是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>cudy_tr3000-256mb-v1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/cudy-tr3000/16-immortalwrt-firmware-selector.jpg" alt="ImmortalWrt Firmware Selector 中的 256 MB 设备 profile" /><figcaption>ImmortalWrt Firmware Selector 中的 256 MB 设备 profile</figcaption></figure><p></p><p>在下载选项中选择 <strong>SYSUPGRADE</strong>，不要选择下面的 <strong>KERNEL</strong>。我下载到的文件名是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>immortalwrt-25.12.1-mediatek-filogic-cudy_tr3000-256mb-v1-squashfs-sysupgrade.bin</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>回到中间固件管理页面，进入“系统”，选择“备份与升级”。</p><p></p><figure><img src="/assets/images/cudy-tr3000/17-openwrt-flash-menu.jpg" alt="中间固件中进入备份与升级的点击顺序" /><figcaption>中间固件中进入备份与升级的点击顺序</figcaption></figure><p></p><p>点击最下方的“刷写固件…”，上传刚才下载的 ImmortalWrt <code>sysupgrade</code> 文件。</p><p></p><figure><img src="/assets/images/cudy-tr3000/18-upload-immortalwrt-firmware.jpg" alt="上传 ImmortalWrt 25.12.1 sysupgrade 固件" /><figcaption>上传 ImmortalWrt 25.12.1 sysupgrade 固件</figcaption></figure><p></p><p>上传后取消勾选“保留当前配置”，再点击“继续”。我选择清除旧配置，是因为中间固件和目标系统之间并不存在值得冒险保留的配置兼容性。</p><p></p><figure><img src="/assets/images/cudy-tr3000/19-immortalwrt-flashing.jpg" alt="ImmortalWrt 固件正在刷写" /><figcaption>ImmortalWrt 固件正在刷写</figcaption></figure><p></p><p>刷写时指示灯会变红并闪烁。等到指示灯变白，路由器完成自动重启，就可以重新进入 ImmortalWrt。</p><p></p><figure><img src="/assets/images/cudy-tr3000/20-immortalwrt-system-page.jpg" alt="重启后进入 ImmortalWrt 系统界面" /><figcaption>重启后进入 ImmortalWrt 系统界面</figcaption></figure><p></p></section>
<section><h2>首次配置和软件包<a href="#首次配置和软件包"><span>#</span></a></h2><p>ImmortalWrt 初次进入时，会询问是否开启“值守式系统升级”。它的含义是让系统自动检查并执行符合条件的系统升级，减少手动维护工作，但自动升级也意味着升级时机和配置变化不再完全由用户控制。</p><p>我在演示手动升级，所以选择“否，禁用检查”。如果这是长期运行的设备，请根据自己的备份习惯和风险接受程度决定，不要把“自动”两个字直接等同于“更安全”。</p><p>刚刷好的系统功能比较基础，但额外软件可以从 LuCI 图形界面安装。点击顶部“系统”，再进入“软件包”。</p><p></p><figure><img src="/assets/images/cudy-tr3000/21-package-menu.jpg" alt="ImmortalWrt 系统菜单中的软件包入口" /><figcaption>ImmortalWrt 系统菜单中的软件包入口</figcaption></figure><p></p><p>进入软件包页面后，先点击“更新列表…”，再在左侧“过滤器”中输入软件名，在结果列表中选择安装。</p><p></p><figure><img src="/assets/images/cudy-tr3000/22-package-page.jpg" alt="ImmortalWrt 软件包管理页面" /><figcaption>ImmortalWrt 软件包管理页面</figcaption></figure><p></p><p>这里有一个值得记住的细节：安装和卸载软件包不会直接修改只读系统镜像。系统会在原有镜像上叠加一个 overlay，配置和额外文件通常进入这一层。好处是系统镜像和运行时修改分离，代价是 overlay 空间不是无限的。</p></section>
<section><h2>第四步：申请预装软件的自定义镜像<a href="#第四步申请预装软件的自定义镜像"><span>#</span></a></h2><p>直接在系统里安装软件已经够用，但我还想记录一次完整的自定义构建流程，于是回到同一个 Firmware Selector 页面，点击“自定义预安装软件包和/或首次启动脚本”。</p><p>页面里已经包含一组基础软件包。我保留这些基础包，只在后面追加自己需要的内容。我的校园网使用深澜认证，所以增加了 <code>bitsrunlogin-go</code>；为了改善界面，又增加了 Argon 主题：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>bitsrunlogin-go luci-app-bitsrunlogin-go luci-i18n-bitsrunlogin-go-zh-cn</span></div></div><div><div><div>2</div></div><div><span>luci-theme-argon luci-i18n-argon-config-zh-cn luci-app-argon-config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>完整的软件包配置如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>apk-openssl autocore base-files block-mount bridger ca-bundle default-settings-chn dnsmasq-full dropbear firewall4 fitblk fstools kmod-crypto-hw-safexcel kmod-gpio-button-hotplug kmod-leds-gpio kmod-nf-nathelper kmod-nft-offload libc libgcc libustream-openssl logd luci mtd netifd nftables odhcp6c odhcpd-ipv6only ppp ppp-mod-pppoe procd-ujail uboot-envtools uci uclient-fetch urandom-seed urngd wpad-openssl kmod-usb3 kmod-mt7915e kmod-mt7981-firmware mt7981-wo-firmware automount bitsrunlogin-go luci-app-bitsrunlogin-go luci-i18n-bitsrunlogin-go-zh-cn luci-theme-argon luci-i18n-argon-config-zh-cn luci-app-argon-config</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/cudy-tr3000/23-custom-build-options.jpg" alt="ImmortalWrt 自定义预安装软件和首次启动脚本页面" /><figcaption>ImmortalWrt 自定义预安装软件和首次启动脚本页面</figcaption></figure><p></p><p>我没有自定义首次启动脚本的需求，所以脚本区域保持为空。配置完成后点击“请求构建”。页面会显示请求位于构建队列中。</p><p></p><figure><img src="/assets/images/cudy-tr3000/24-build-queued.jpg" alt="ImmortalWrt 自定义镜像请求位于构建队列中" /><figcaption>ImmortalWrt 自定义镜像请求位于构建队列中</figcaption></figure><p></p><p>等待构建完成后，页面会出现下载入口。</p><p></p><figure><img src="/assets/images/cudy-tr3000/25-build-succeeded.jpg" alt="ImmortalWrt 自定义镜像构建成功" /><figcaption>ImmortalWrt 自定义镜像构建成功</figcaption></figure><p></p><p>我下载到的文件名类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>immortalwrt-25.12.1-17179a901f8d-mediatek-filogic-cudy_tr3000-256mb-v1-squashfs-sysupgrade.bin</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实际文件名应以页面当天生成的结果为准，不要手动猜测或改写中间的构建标识。</p></section>
<section><h2>第五步：刷入自定义镜像<a href="#第五步刷入自定义镜像"><span>#</span></a></h2><p>自定义镜像下载完成后，再次进入 ImmortalWrt 的管理页面。系统一般可以通过下面两个地址访问：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>http://192.168.1.1</span></div></div><div><div><div>2</div></div><div><span>http://immortalwrt.lan</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>先登录管理界面。</p><p></p><figure><img src="/assets/images/cudy-tr3000/26-custom-image-login.jpg" alt="自定义 ImmortalWrt 镜像的登录界面" /><figcaption>自定义 ImmortalWrt 镜像的登录界面</figcaption></figure><p></p><p>进入系统后确认当前仍然是 ImmortalWrt。</p><p></p><figure><img src="/assets/images/cudy-tr3000/27-custom-image-system-page.jpg" alt="自定义镜像进入后的系统界面" /><figcaption>自定义镜像进入后的系统界面</figcaption></figure><p></p><p>按照前面相同的路径进入“系统 → 备份与升级”，上传自定义 <code>sysupgrade</code> 文件。</p><p></p><figure><img src="/assets/images/cudy-tr3000/28-upload-custom-image.jpg" alt="上传 ImmortalWrt 自定义 sysupgrade 镜像" /><figcaption>上传 ImmortalWrt 自定义 sysupgrade 镜像</figcaption></figure><p></p><p>同样取消勾选“保留设置并继续使用当前配置”，点击“继续”，等待路由器完成重启。这样做的目的仍然是避免把上一套镜像的配置直接带进新的软件包集合。</p></section>
<section><h2>第六步：用 TFTP 恢复 Cudy 官方固件<a href="#第六步用-tftp-恢复-cudy-官方固件"><span>#</span></a></h2><p>TFTP 可以理解成一个非常朴素的局域网文件传输方式：路由器启动时主动向电脑请求一个固定名字的文件。它不像网页上传那样由浏览器发起，而是由路由器的 bootloader 在恢复模式中发起请求。</p><p>这次恢复使用 Cudy 官方固件，不是把 ImmortalWrt 通过 TFTP 刷回去。根据 <a href="https://www.cudy.com/zh-cn/blogs/faq/how-to-recovery-the-cudy-router-from-openwrt-firmware-to-cudy-official-firmware" target="_blank">Cudy 官方恢复说明</a>，我使用 Tftpd64 提供恢复文件。</p><section><h3>准备官方恢复镜像<a href="#准备官方恢复镜像"><span>#</span></a></h3><p>从 <a href="https://www.cudy.com/zh-cn/pages/download-center/tr3000-1-0" target="_blank">Cudy TR3000 官方下载页面</a>下载官方固件。本文操作时看到的最新版本是 <code>2.5.21</code>，文件名为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>m_upgrade_TR3000_256MB_Flash-R103-2.5.21-20260708-181324-sysupgrade_81571.zip</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>解压后找到 <code>.bin</code> 文件，将它重命名为：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>recovery.bin</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把它放在：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>E:\Documents\cudy_tr3000-256mb_v1\official</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>实际路径可以不同，但后面的 Tftpd64 根目录必须指向这个文件所在的目录。</p></section><section><h3>配置电脑网卡和防火墙<a href="#配置电脑网卡和防火墙"><span>#</span></a></h3><p>用网线连接路由器 LAN 口和电脑。在 Windows 网卡 IPv4 设置中手动填写：</p>

<table><thead><tr><th>项目</th><th>值</th></tr></thead><tbody><tr><td>IP 地址</td><td><code>192.168.1.88</code></td></tr><tr><td>子网掩码</td><td><code>255.255.255.0</code></td></tr><tr><td>默认网关</td><td>留空</td></tr><tr><td>DNS</td><td>留空</td></tr></tbody></table><p></p><figure><img src="/assets/images/cudy-tr3000/29-tftp-static-ip.jpg" alt="Windows 网卡 IPv4 静态地址配置为 192.168.1.88" /><figcaption>Windows 网卡 IPv4 静态地址配置为 192.168.1.88</figcaption></figure><p></p><p>为了让 TFTP 请求不被 Windows 拦截，我临时关闭了专用网络和公用网络防火墙。这里只建议在恢复期间短暂关闭，恢复完成后立即重新开启。</p><p></p><figure><img src="/assets/images/cudy-tr3000/30-disable-firewall.jpg" alt="Windows 安全中心中临时关闭专用网络防火墙" /><figcaption>Windows 安全中心中临时关闭专用网络防火墙</figcaption></figure><p></p></section><section><h3>配置 Tftpd64<a href="#配置-tftpd64"><span>#</span></a></h3><p>Tftpd64 可以从 <a href="https://github.com/PJO2/tftpd64/releases" target="_blank">GitHub Releases</a> 下载。如果使用 Scoop，也可以安装 extras 仓库里的 <code>tftpd</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop install tftpd</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>打开 Tftpd64 后，设置正确的 Current Directory，也就是包含 <code>recovery.bin</code> 的目录，再选择电脑当前使用的 <code>192.168.1.88</code> 网卡。</p><p></p><figure><img src="/assets/images/cudy-tr3000/31-tftpd-settings.jpg" alt="Tftpd64 的根目录和网卡配置界面" /><figcaption>Tftpd64 的根目录和网卡配置界面</figcaption></figure><p></p><p>点击“Show Dir”，确认目录列表中确实出现 <code>recovery.bin</code>。如果这里看不到文件，先修正目录，不要继续操作路由器。</p><p></p><figure><img src="/assets/images/cudy-tr3000/32-tftpd-show-directory.jpg" alt="Tftpd64 Show Dir 显示 recovery.bin" /><figcaption>Tftpd64 Show Dir 显示 recovery.bin</figcaption></figure><p></p></section><section><h3>触发官方恢复<a href="#触发官方恢复"><span>#</span></a></h3><ol>
<li>关闭路由器电源。</li>
<li>按住路由器侧面的 RESET 键不松手。</li>
<li>插入 USB-C 电源。</li>
<li>等待 Tftpd64 界面出现文件传输。</li>
<li>确认开始传输后再松开 RESET 键。</li>
</ol><p></p><figure><img src="/assets/images/cudy-tr3000/33-tftpd-transfer.jpg" alt="Tftpd64 正在向路由器传输 recovery.bin" /><figcaption>Tftpd64 正在向路由器传输 recovery.bin</figcaption></figure><p></p><p>TFTP 传输完成不代表恢复已经完成。bootloader 还需要校验镜像，并把内容写入 Flash。传输结束后不要断电，等待路由器自动重启。我这次观察到通常在一分钟以内完成重启。</p><p>恢复完成后，指示灯常亮红色。把电脑网卡 IPv4 改回自动获取，并重新开启 Windows 专用网络和公用网络防火墙，然后访问：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>http://192.168.10.1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p></p><figure><img src="/assets/images/cudy-tr3000/34-official-firmware-page.jpg" alt="恢复完成后进入 Cudy 官方固件页面，版本为 2.5.21" /><figcaption>恢复完成后进入 Cudy 官方固件页面，版本为 2.5.21</figcaption></figure><p></p><p>页面底部显示固件版本已经回到 <code>2.5.21</code>，这就是我判断官方固件恢复成功的依据。</p></section></section>
<section><h2>我最后留下的检查点<a href="#我最后留下的检查点"><span>#</span></a></h2><p>这次流程没有命令行，也没有改动 bootloader。我的检查点只有几个：</p><ul>
<li>标签明确写着 <code>256 MB 闪存版</code>。</li>
<li>原厂系统地址是 <code>192.168.10.1</code>。</li>
<li>中间固件启动后地址变成 <code>192.168.1.1</code>。</li>
<li>中间固件使用 <code>cudy_tr3000-256mb-v1-sysupgrade.bin</code>。</li>
<li>ImmortalWrt Selector 使用 <code>cudy_tr3000-256mb-v1</code>。</li>
<li>目标镜像选择 <code>SYSUPGRADE</code>，不是 <code>KERNEL</code>。</li>
<li>刷写 ImmortalWrt 时取消保留旧配置。</li>
<li>TFTP 恢复使用官方 R103 固件，并重命名为 <code>recovery.bin</code>。</li>
<li>TFTP 传输完成后继续等待，不能立即断电。</li>
<li>恢复成功后，官方固件页面显示版本 <code>2.5.21</code>。</li>
</ul><p>从 2025 年 9 月的第一台，到 2026 年 8 月的第二台，我对这个小路由器的结论没有变：硬件本身并不神秘，真正容易出错的是文件分支和等待时间。选错一次文件，或者在传输完成后过早拔电，都会把一个图形化流程变成维修流程。前者是判断题，后者是耐心题，路由器对此一视同仁。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ol>
<li><a href="https://www.cudy.com/zh-CN/blogs/faq/openwrt-software-download" target="_blank">Cudy 官方 OpenWrt 软件和中间固件下载说明</a>，256 MB 中间固件下载入口。</li>
<li><a href="https://www.cudy.com/zh-cn/pages/download-center/tr3000-1-0" target="_blank">Cudy TR3000 官方下载页面</a>，官方固件和恢复镜像。</li>
<li><a href="https://www.cudy.com/zh-cn/blogs/faq/how-to-recovery-the-cudy-router-from-openwrt-firmware-to-cudy-official-firmware" target="_blank">Cudy 官方从 OpenWrt 恢复官方固件说明</a>，TFTP 恢复流程。</li>
<li><a href="https://firmware-selector.immortalwrt.org/?version=25.12.1&amp;target=mediatek%2Ffilogic&amp;id=cudy_tr3000-256mb-v1" target="_blank">ImmortalWrt Firmware Selector</a>，选择 <code>cudy_tr3000-256mb-v1</code> 和 <code>SYSUPGRADE</code> 镜像。</li>
<li><a href="https://github.com/PJO2/tftpd64/releases" target="_blank">Tftpd64 Releases</a>，Windows TFTP 服务端下载地址。</li>
<li><a href="https://openwrt.org/toh/cudy/tr3000" target="_blank">OpenWrt Wiki：Cudy TR3000</a>，设备支持背景和原厂恢复风险说明。</li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/gdb-debugging-guide/</id>
      <title type="text">从暂停到理解：GDB 调试完整实践</title>
      <published>2026-08-25T12:00:00.000Z</published>
      <updated>2026-09-30T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/gdb-debugging-guide/"/>
      <summary type="text">我用一个带有逻辑错误、递归、线程、异常、fork 和崩溃路径的 C++20 小程序，在 NixOS 上从零搭环境，把每条 GDB 命令的真实输出贴出来，并逐段解释这些输出到底在说什么。</summary>
      <content type="html"><![CDATA[<p>我一直觉得 GDB 很重要，但以前真正遇到崩溃时，第一反应还是加几行 <code>std::cout</code>。后来我认真试了几次 GDB，发现卡住我的其实不是命令，而是输出：<code>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:25</code> 里哪一段是什么意思？<code>#1  0x0000555555556324 in ...</code> 的那个地址是谁的地址？为什么 <code>watch counter</code> 明明设置成功了，线程改了它却一次都没停？</p>
<p>所以这篇文章换了一种写法。我写了一个故意有错误的小程序，在 NixOS 上用一个 <code>flake.nix</code> 固定工具链，然后把一次完整调试过程中的每条命令和它的真实输出原样贴出来，再逐段解释输出的格式。你照着做的时候，可以把自己的终端和文章并排放着对照：地址、PID、LWP 这类运行时数字大概率和我不一样，但输出的”形状”应该完全一致。</p>
<p>我的环境是 NixOS 26.05、x86-64、GNU GDB 17.2、GCC 15.3.0。所有 GDB 会话都用 <code>gdb -q -nx</code> 启动：<code>-q</code> 去掉版权横幅，<code>-nx</code> 不读取 <code>~/.gdbinit</code>。后者很重要，我自己的 <code>~/.gdbinit</code> 里装了 gdb-dashboard，如果不加 <code>-nx</code>，你看到的会是一整屏仪表盘，而不是下面这些文本。</p>
<section><h2>准备环境：NixOS 上的 flake.nix<a href="#准备环境nixos-上的-flakenix"><span>#</span></a></h2><p>如果你用的是 Debian、Arch 这类发行版，装上 <code>gdb</code> 和 <code>g++</code> 就可以直接跳到下一节。NixOS 上我更喜欢给实验目录放一个 <code>flake.nix</code>，这样工具链版本、调试符号和编译参数都是确定的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>description</span><span> = </span><span>"GDB + C++20 debugging playground"</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>  </span><span>inputs</span><span>.</span><span>nixpkgs</span><span>.</span><span>url</span><span> = </span><span>"github:NixOS/nixpkgs/nixos-unstable"</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>  </span><span>outputs</span><span><span> = { </span><span>nixpkgs</span><span>, ... }:</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>let</span></div></div><div><div><div>8</div></div><div><span>      </span><span>systems</span><span> = [ </span><span>"x86_64-linux"</span><span> </span><span>"aarch64-linux"</span><span> ];</span></div></div><div><div><div>9</div></div><div><span>      </span><span>forEachSystem</span><span><span> = </span><span>nixpkgs</span><span>.</span><span>lib</span><span>.</span><span>genAttrs</span><span> </span><span>systems</span><span>;</span></span></div></div><div><div><div>10</div></div><div><span>    </span><span>in</span><span> {</span></div></div><div><div><div>11</div></div><div><span>      </span><span>devShells</span><span><span> = </span><span>forEachSystem</span><span> (</span><span>system</span><span>:</span></span></div></div><div><div><div>12</div></div><div><span>        </span><span>let</span><span> </span><span>pkgs</span><span><span> = </span><span>nixpkgs</span><span>.</span><span>legacyPackages</span><span>.</span></span><span>${</span><span>system</span><span>}</span><span>;</span></div></div><div><div><div>13</div></div><div><span>        </span><span>in</span><span> {</span></div></div><div><div><div>14</div></div><div><span>          </span><span>default</span><span><span> = </span><span>pkgs</span><span>.</span><span>mkShell</span><span> {</span></span></div></div><div><div><div>15</div></div><div><span>            </span><span># mkShell 自带 stdenv 的 gcc/g++、objdump 和 nm，这里只需要补上 gdb 和 gdbserver。</span></div></div><div><div><div>16</div></div><div><span>            </span><span>packages</span><span><span> = [ </span><span>pkgs</span><span>.</span><span>gdb</span><span> ];</span></span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>            </span><span># 关闭 cc-wrapper 默认注入的加固参数（栈保护、FORTIFY、调用后清零寄存器等），</span></div></div><div><div><div>19</div></div><div><span>            </span><span># 让反汇编更接近普通发行版上 GCC 的输出。</span></div></div><div><div><div>20</div></div><div><span>            </span><span>hardeningDisable</span><span> = [ </span><span>"all"</span><span> ];</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>            </span><span># Nixpkgs 的 gdb 打过补丁，会从这个变量寻找分离的调试符号，这里给 glibc 用。</span></div></div><div><div><div>23</div></div><div><span>            </span><span>NIX_DEBUG_INFO_DIRS</span><span> = </span><span>"</span><span>${</span><span><span>pkgs</span><span>.</span><span>glibc</span><span>.</span><span>debug</span></span><span>}</span><span>/lib/debug"</span><span>;</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>            </span><span>shellHook</span><span> = </span><span>''</span></div></div><div><div><div>26</div></div><div><span><span>              </span></span><span>g++ --version | head -n 1</span></div></div><div><div><div>27</div></div><div><span><span>              </span></span><span>gdb --version | head -n 1</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>''</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>          </span></span><span>};</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里有三处和普通发行版不一样的地方，每一处我都踩过或者验证过。</p><p><strong>第一，<code>hardeningDisable</code>。</strong> Nixpkgs 的 <code>g++</code> 不是裸的 GCC，而是一个 cc-wrapper 脚本，它会在你的参数前面悄悄插入一串加固参数。去掉 <code>hardeningDisable</code> 之后，用 <code>NIX_DEBUG=1</code> 可以看到 wrapper 实际做了什么：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ NIX_DEBUG=1 g++ -std=c++20 -g3 -O0 -c gdb_demo.cpp -o /dev/null 2&gt;&amp;1 | sed -n '/extra flags before/,/extra flags after/p'</span></div></div><div><div><div>2</div></div><div><span>extra flags before to /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/bin/g++:</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>-fPIC</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>-fstack-clash-protection</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>-O2</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>-U_FORTIFY_SOURCE</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>-Wformat</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>-Wformat-security</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>-Werror=format-security</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>-fzero-call-used-regs=used-gpr</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>-fstrict-flex-arrays=1</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>-fstack-protector-strong</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>--param</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>ssp-buffer-size=4</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>-fno-strict-overflow</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>-fno-omit-frame-pointer</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>-mno-omit-leaf-frame-pointer</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>-mtls-dialect=gnu2</span></div></div><div><div><div>20</div></div><div><span>original flags to /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/bin/g++:</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>-std=c++20</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>-g3</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>-O0</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>-c</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>gdb_demo.cpp</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>-o</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>/dev/null</span></div></div><div><div><div>28</div></div><div><span>extra flags after to /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/bin/g++:</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>original flags</code> 才是我写的那一行，<code>extra flags before</code> 全是 wrapper 加的。有意思的是里面还有一个 <code>-O2</code>，好在它排在我的 <code>-O0</code> 前面，GCC 以最后一个 <code>-O</code> 为准，所以优化级别没有被覆盖。但 <code>-fstack-protector-strong</code>、<code>-fzero-call-used-regs=used-gpr</code> 这些会实实在在地改变生成的指令：函数进出时多出栈 canary 的写入和检查，返回前多出清零寄存器的指令。对日常开发这是好事，对一篇要逐条读反汇编的文章就是噪声，所以我把它全关了。关掉之后再看同一条命令，<code>extra flags before</code> 里只剩 <code>-fno-omit-frame-pointer</code>、<code>-mno-omit-leaf-frame-pointer</code> 和 <code>-mtls-dialect=gnu2</code> 这三个和加固无关的默认参数。</p><p><strong>第二，<code>NIX_DEBUG_INFO_DIRS</code>。</strong> 普通发行版上 glibc 的调试符号通常装在 <code>/usr/lib/debug</code>，NixOS 没有这个目录。Nixpkgs 里的 glibc 把调试符号拆到了单独的 <code>debug</code> 输出，而 Nixpkgs 的 gdb 打了一个补丁：如果环境变量 <code>NIX_DEBUG_INFO_DIRS</code> 存在，就用它作为 <code>debug-file-directory</code>。不设置时，<code>info sharedlibrary</code> 里每个库都带着 <code>(*)</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) info sharedlibrary</span></div></div><div><div><div>2</div></div><div><span>From                To                  Syms Read   Shared Object Library</span></div></div><div><div><div>3</div></div><div><span>0x00007ffff7fc4000  0x00007ffff7fff000  Yes (*)     /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2</span></div></div><div><div><div>4</div></div><div><span>0x00007ffff7c00000  0x00007ffff7e83000  Yes (*)     /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>5</div></div><div><span>0x00007ffff7ec2000  0x00007ffff7fba000  Yes (*)     /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libm.so.6</span></div></div><div><div><div>6</div></div><div><span>0x00007ffff7e95000  0x00007ffff7ec2000  Yes (*)     /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libgcc_s.so.1</span></div></div><div><div><div>7</div></div><div><span>0x00007ffff7800000  0x00007ffff7a09000  Yes (*)     /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6</span></div></div><div><div><div>8</div></div><div><span>(*): Shared library is missing debugging information.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一行就是 <code>(*)</code> 的图例：这个库缺少调试信息。设置之后，glibc 家族的 <code>ld-linux</code>、<code>libm</code>、<code>libc</code> 都不再带 <code>(*)</code>，只剩 GCC 运行时的 <code>libstdc++</code> 和 <code>libgcc_s</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) show debug-file-directory</span></div></div><div><div><div>2</div></div><div><span>The directory where separate debug symbols are searched for is "/nix/store/fk98h17sza0xxz9iw0fi5b2nn1h4c0pm-glibc-2.42-84-debug/lib/debug".</span></div></div><div><div><div>3</div></div><div><span>(gdb) info sharedlibrary</span></div></div><div><div><div>4</div></div><div><span>From                To                  Syms Read   Shared Object Library</span></div></div><div><div><div>5</div></div><div><span>0x00007ffff7fc4000  0x00007ffff7fff000  Yes         /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2</span></div></div><div><div><div>6</div></div><div><span>0x00007ffff7c00000  0x00007ffff7e83000  Yes (*)     /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>7</div></div><div><span>0x00007ffff7ec2000  0x00007ffff7fba000  Yes         /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libm.so.6</span></div></div><div><div><div>8</div></div><div><span>0x00007ffff7e95000  0x00007ffff7ec2000  Yes (*)     /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libgcc_s.so.1</span></div></div><div><div><div>9</div></div><div><span>0x00007ffff7800000  0x00007ffff7a09000  Yes         /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6</span></div></div><div><div><div>10</div></div><div><span>(*): Shared library is missing debugging information.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>区别在调用栈里更明显。同样是主线程卡在 <code>join()</code> 里，没有 glibc 调试符号时是这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) thread 1</span></div></div><div><div><div>2</div></div><div><span>[Switching to thread 1 (Thread 0x7ffff7e90780 (LWP 138338))]</span></div></div><div><div><div>3</div></div><div><span>#0  0x00007ffff78a5a22 in __syscall_cancel_arch () from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6</span></div></div><div><div><div>4</div></div><div><span>(gdb) bt</span></div></div><div><div><div>5</div></div><div><span>#0  0x00007ffff78a5a22 in __syscall_cancel_arch () from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6</span></div></div><div><div><div>6</div></div><div><span>#1  0x00007ffff789912c in __internal_syscall_cancel () from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6</span></div></div><div><div><div>7</div></div><div><span>#2  0x00007ffff78998ac in __futex_abstimed_wait_common () from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6</span></div></div><div><div><div>8</div></div><div><span>#3  0x00007ffff789ec8c in __pthread_clockjoin_ex () from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6</span></div></div><div><div><div>9</div></div><div><span>#4  0x00007ffff7cf2f1b in std::thread::join() () from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>10</div></div><div><span>#5  0x00005555555565af in main (argc=1, argv=0x7fffffffb868) at gdb_demo.cpp:36</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>有了之后，每一帧都有参数和 glibc 源文件位置：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) thread 1</span></div></div><div><div><div>2</div></div><div><span>[Switching to thread 1 (Thread 0x7ffff7e90780 (LWP 165718))]</span></div></div><div><div><div>3</div></div><div><span>#0  __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>4</div></div><div><span>warning: 56  ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S: No such file or directory</span></div></div><div><div><div>5</div></div><div><span>(gdb) bt</span></div></div><div><div><div>6</div></div><div><span>#0  __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>7</div></div><div><span>#1  0x00007ffff789912c in __internal_syscall_cancel (a1=&lt;optimized out&gt;, a2=&lt;optimized out&gt;, a3=&lt;optimized out&gt;, a4=&lt;optimized out&gt;, a5=a5@entry=0,</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>a6=a6@entry=4294967295, nr=202) at cancellation.c:49</span></div></div><div><div><div>9</div></div><div><span>#2  0x00007ffff78998ac in __futex_abstimed_wait_common64 (private=128, futex_word=0x7ffff77ff990, expected=&lt;optimized out&gt;, op=&lt;optimized out&gt;, abstime=0x0,</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>cancel=true) at futex-internal.c:57</span></div></div><div><div><div>11</div></div><div><span>#3  __futex_abstimed_wait_common (futex_word=futex_word@entry=0x7ffff77ff990, expected=&lt;optimized out&gt;, clockid=clockid@entry=0, abstime=abstime@entry=0x0,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>private=private@entry=128, cancel=cancel@entry=true) at futex-internal.c:87</span></div></div><div><div><div>13</div></div><div><span>#4  0x00007ffff789993f in __GI___futex_abstimed_wait_cancelable64 (futex_word=futex_word@entry=0x7ffff77ff990, expected=&lt;optimized out&gt;,</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>clockid=clockid@entry=0, abstime=abstime@entry=0x0, private=private@entry=128) at futex-internal.c:139</span></div></div><div><div><div>15</div></div><div><span>#5  0x00007ffff789ec8c in __pthread_clockjoin_ex (threadid=140737345746624, thread_return=0x0, clockid=0, abstime=0x0, block=&lt;optimized out&gt;)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>at pthread_join_common.c:108</span></div></div><div><div><div>17</div></div><div><span>#6  0x00007ffff7cf2f1b in std::thread::join() () from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>18</div></div><div><span>#7  0x00005555555565af in main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:36</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>那行 <code>warning: 56	../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S: No such file or directory</code> 不是错误。调试符号里记录了”这条指令来自 glibc 源码的第 56 行”，但我没有下载 glibc 源码，GDB 只是如实告诉我它找不到文件。函数名、参数和行号都已经可用了。</p><p><strong>第三，libstdc++ 的 pretty printer。</strong> 后面你会看到 GDB 把 <code>std::vector</code> 打印成 <code>std::vector of length 4, capacity 4 = {3, 5, 7, 9}</code>，这不是 GDB 内置的能力，而是 libstdc++ 附带的一段 Python 脚本。GDB 只会从 <code>auto-load safe-path</code> 里的目录自动加载脚本，Nixpkgs 在构建 gdb 时把对应 GCC 的库目录写进了这个默认值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>2</div></div><div><span>(gdb) start</span></div></div><div><div><div>3</div></div><div><span>Temporary breakpoint 1 at 0x23ab: file gdb_demo.cpp, line 25.</span></div></div><div><div><div>4</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>5</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>6</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>Temporary breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:25</span></div></div><div><div><div>9</div></div><div><span>25      const std::string mode = argc &gt; 1 ? argv[1] : "";</span></div></div><div><div><div>10</div></div><div><span>(gdb) info auto-load python-scripts</span></div></div><div><div><div>11</div></div><div><span>Loaded  Script</span></div></div><div><div><div>12</div></div><div><span>Yes     /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6.0.34-gdb.py</span></div></div><div><div><div>13</div></div><div><span>(gdb) show auto-load safe-path</span></div></div><div><div><div>14</div></div><div><span>List of directories from which it is safe to auto-load files is $debugdir:$datadir/auto-load:/nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Loaded Yes</code> 说明脚本已经生效。我猜如果你用的 GCC 和 gdb 来自不同的 nixpkgs 版本，store 路径对不上，这里就会变成 <code>No</code>，GDB 会打印一条 <code>auto-loading has been declined</code> 警告，<code>vector</code> 也会退化成原始的结构体。同一个 flake 里取 <code>pkgs.gdb</code> 和 mkShell 自带的 GCC，可以避免这个问题。</p><p>把 <code>flake.nix</code> 放进实验目录之后还有一个小坑。如果这个目录是一个 Git 仓库，flake 必须先被 Git 跟踪，否则 Nix 看不到它：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ nix develop</span></div></div><div><div><div>2</div></div><div><span>error: Path 'flake.nix' in the repository "/tmp/gdb-h2" is not tracked by Git.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>       </span></span><span>To make it visible to Nix, run:</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>git -C "/tmp/gdb-h2" add "flake.nix"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>git add flake.nix</code> 之后再进入，shellHook 会打印两个版本号：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ git add flake.nix</span></div></div><div><div><div>2</div></div><div><span>$ nix develop</span></div></div><div><div><div>3</div></div><div><span>warning: Git tree '/tmp/gdb-h2' is dirty</span></div></div><div><div><div>4</div></div><div><span>g++ (GCC) 15.3.0</span></div></div><div><div><div>5</div></div><div><span>GNU gdb (GDB) 17.2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Git tree is dirty</code> 只是提醒工作区有未提交的修改，可以忽略。第一次进入会顺便生成 <code>flake.lock</code>，把 nixpkgs 固定到某个具体提交。下面所有命令都在这个 shell 里执行。</p></section>
<section><h2>实验对象：一个故意写错的 C++ 程序<a href="#实验对象一个故意写错的-c-程序"><span>#</span></a></h2><p>我把下面的文件保存为 <code>gdb_demo.cpp</code>。文章里提到的所有行号都指这份文件，所以请原样复制，不要调整空行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;sys/wait.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;unistd.h&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;mutex&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>&lt;thread&gt;</span></div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>int</span><span> </span><span>recursive_sum</span><span>(</span><span>const</span><span> std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>values</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>index</span><span>) {</span></span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> (</span><span>index</span><span> </span><span>==</span><span> </span><span>values</span><span>.</span><span>size</span><span>()) {</span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span> </span><span>values</span><span>[</span><span>index</span><span>] </span><span>+</span><span> </span><span>recursive_sum</span><span>(</span><span>values</span><span>, </span><span>index</span><span> </span><span>+</span><span> </span><span>1</span><span>);</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>void</span><span> </span><span>worker</span><span>(</span><span>int&amp;</span><span><span> </span><span>counter</span><span>, std::</span></span><span>mutex</span><span>&amp;</span><span><span> </span><span>mutex</span><span>) {</span></span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>std::</span><span>lock_guard</span><span>&lt;std::</span><span>mutex</span><span>&gt; </span><span>lock</span><span>(</span><span>mutex</span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>++</span><span>counter</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>int</span><span><span> </span><span>argc</span><span>, </span></span><span>char*</span><span><span> </span><span>argv</span><span>[]) {</span></span></div></div><div><div><div>25</div></div><div><span>    </span><span>const</span><span> std::</span><span>string</span><span> </span><span>mode</span><span> </span><span>=</span><span> </span><span>argc</span><span> </span><span>&gt;</span><span> </span><span>1</span><span> </span><span>?</span><span> </span><span>argv</span><span>[</span><span>1</span><span>] </span><span>:</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>values</span><span>{</span><span>3</span><span>, </span><span>5</span><span>, </span><span>7</span><span>, </span><span>9</span><span>};</span></div></div><div><div><div>27</div></div><div><span>    </span><span>const</span><span> </span><span>int</span><span> </span><span>total</span><span> </span><span>=</span><span> </span><span>recursive_sum</span><span>(</span><span>values</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>const</span><span> </span><span>double</span><span> </span><span>average</span><span> </span><span>=</span><span> </span><span><span>static_cast</span><span>&lt;double&gt;</span></span><span>(</span><span>total</span><span>) </span><span>/</span><span> (</span><span>values</span><span>.</span><span>size</span><span>() </span><span>-</span><span> </span><span>1</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"total="</span><span> </span><span>&lt;&lt;</span><span> </span><span>total</span><span> </span><span>&lt;&lt;</span><span> </span><span>", average="</span><span> </span><span>&lt;&lt;</span><span> </span><span>average</span></div></div><div><div><div>30</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>", expected_average=6</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>    </span><span>int</span><span> </span><span>counter</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>std::</span><span>mutex</span><span> </span><span>mutex</span><span>;</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>std::</span><span>thread</span><span> </span><span>first</span><span>(</span><span>worker</span><span>, std::</span><span>ref</span><span>(</span><span>counter</span><span>), std::</span><span>ref</span><span>(</span><span>mutex</span><span>));</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>std::</span><span>thread</span><span> </span><span>second</span><span>(</span><span>worker</span><span>, std::</span><span>ref</span><span>(</span><span>counter</span><span>), std::</span><span>ref</span><span>(</span><span>mutex</span><span>));</span></div></div><div><div><div>36</div></div><div><span>    </span><span>first</span><span>.</span><span>join</span><span>();</span></div></div><div><div><div>37</div></div><div><span>    </span><span>second</span><span>.</span><span>join</span><span>();</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"counter="</span><span> </span><span>&lt;&lt;</span><span> </span><span>counter</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>    </span><span>if</span><span> (</span><span>mode</span><span> </span><span>==</span><span> </span><span>"--throw"</span><span>) {</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>at</span><span>(</span><span>values</span><span>.</span><span>size</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>    </span><span>if</span><span> (</span><span>mode</span><span> </span><span>==</span><span> </span><span>"--fork"</span><span>) {</span></div></div><div><div><div>45</div></div><div><span>        </span><span>const</span><span> </span><span>pid_t</span><span> </span><span>pid</span><span> </span><span>=</span><span> </span><span>fork</span><span>();</span></div></div><div><div><div>46</div></div><div><span>        </span><span>if</span><span> (</span><span>pid</span><span> </span><span>==</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"child: pid="</span><span> </span><span>&lt;&lt;</span><span> </span><span>getpid</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>48</div></div><div><span>            </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span>        </span><span>waitpid</span><span>(</span><span>pid</span><span>, </span><span>nullptr</span><span>, </span><span>0</span><span>);</span></div></div><div><div><div>51</div></div><div><span><span>        </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"parent: child "</span><span> </span><span>&lt;&lt;</span><span> </span><span>pid</span><span> </span><span>&lt;&lt;</span><span> </span><span>" exited</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>    </span><span>if</span><span> (</span><span>mode</span><span> </span><span>==</span><span> </span><span>"--crash"</span><span>) {</span></div></div><div><div><div>55</div></div><div><span>        </span><span>int*</span><span> </span><span>pointer</span><span> </span><span>=</span><span> </span><span>nullptr</span><span>;</span></div></div><div><div><div>56</div></div><div><span><span>        </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"about to dereference a null pointer</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>        </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>*</span><span>pointer</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>58</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>它有五条值得观察的路径：<code>recursive_sum</code> 递归求和；两个线程在互斥锁保护下各把 <code>counter</code> 加 1000 次；<code>--throw</code> 用 <code>values.at(4)</code> 触发越界异常；<code>--fork</code> 创建一个子进程；<code>--crash</code> 解引用空指针。</p><p>编译命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>g++</span><span> </span><span>-std=c++20</span><span> </span><span>-g3</span><span> </span><span>-O0</span><span> </span><span>-fno-omit-frame-pointer</span><span> </span><span>-Wall</span><span> </span><span>-Wextra</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>    </span><span>-o</span><span> </span><span>gdb_demo</span><span> </span><span>gdb_demo.cpp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-g3</code> 把源码行号、变量位置、类型，甚至宏定义都写进可执行文件；<code>-O0</code> 关闭优化，让每一行源码都老老实实对应一段指令；<code>-fno-omit-frame-pointer</code> 保留 <code>rbp</code> 帧指针，后面读 <code>info frame</code> 和反汇编时会更直观。</p><p>在终端里依次运行四种模式：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>3</div></div><div><span>counter=2000</span></div></div><div><div><div>4</div></div><div><span>$ ./gdb_demo --crash</span></div></div><div><div><div>5</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>6</div></div><div><span>counter=2000</span></div></div><div><div><div>7</div></div><div><span>about to dereference a null pointer</span></div></div><div><div><div>8</div></div><div><span>Segmentation fault         (core dumped) ./gdb_demo --crash</span></div></div><div><div><div>9</div></div><div><span>$ echo $?</span></div></div><div><div><div>10</div></div><div><span>139</span></div></div><div><div><div>11</div></div><div><span>$ ./gdb_demo --throw</span></div></div><div><div><div>12</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>13</div></div><div><span>counter=2000</span></div></div><div><div><div>14</div></div><div><span>terminate called after throwing an instance of 'std::out_of_range'</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>what():  vector::_M_range_check: __n (which is 4) &gt;= this-&gt;size() (which is 4)</span></div></div><div><div><div>16</div></div><div><span>Aborted                    (core dumped) ./gdb_demo --throw</span></div></div><div><div><div>17</div></div><div><span>$ ./gdb_demo --fork</span></div></div><div><div><div>18</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>19</div></div><div><span>counter=2000</span></div></div><div><div><div>20</div></div><div><span>child: pid=170023</span></div></div><div><div><div>21</div></div><div><span>parent: child 170023 exited</span></div></div><div><div><div>22</div></div><div><span>$ ./gdb_demo --crash | cat</span></div></div><div><div><div>23</div></div><div><span>$ nm gdb_demo_stripped</span></div></div><div><div><div>24</div></div><div><span>nm: gdb_demo_stripped: no symbols</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>3 + 5 + 7 + 9 = 24</code>，两个线程各加 1000 次，<code>counter=2000</code> 也对。但平均值打印的是 <code>8</code>，而我写在输出里的期望值是 <code>6</code>。这就是第一个 bug：程序没有崩溃，编译器也没有警告，但状态已经错了。GDB 最擅长的恰恰是这种问题。</p><p>退出码 <code>139</code> 等于 <code>128 + 11</code>，11 是 <code>SIGSEGV</code> 的编号。<code>(core dumped)</code> 说明 systemd-coredump 已经把现场保存了下来，后面会用到。</p><p>最后一条 <code>./gdb_demo --crash | cat</code> 什么都没打印，这不是复制错误。当 <code>stdout</code> 连到终端时它是行缓冲的，每个 <code>\n</code> 都会刷新；连到管道时变成全缓冲，要攒满一块才写出去。程序在刷新之前就崩溃了，缓冲区里的三行输出跟着进程一起消失。后面用 GDB 的批处理模式时会再撞见它一次。</p></section>
<section><h2>第一次会话：逐行读懂 GDB 在说什么<a href="#第一次会话逐行读懂-gdb-在说什么"><span>#</span></a></h2><p>先看一次不加 <code>-q</code> 的启动，知道那段横幅长什么样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>GNU gdb (GDB) 17.2</span></div></div><div><div><div>3</div></div><div><span>Copyright (C) 2025 Free Software Foundation, Inc.</span></div></div><div><div><div>4</div></div><div><span>License GPLv3+: GNU GPL version 3 or later &lt;http://gnu.org/licenses/gpl.html&gt;</span></div></div><div><div><div>5</div></div><div><span>This is free software: you are free to change and redistribute it.</span></div></div><div><div><div>6</div></div><div><span>There is NO WARRANTY, to the extent permitted by law.</span></div></div><div><div><div>7</div></div><div><span>Type "show copying" and "show warranty" for details.</span></div></div><div><div><div>8</div></div><div><span>This GDB was configured as "x86_64-unknown-linux-gnu".</span></div></div><div><div><div>9</div></div><div><span>Type "show configuration" for configuration details.</span></div></div><div><div><div>10</div></div><div><span>For bug reporting instructions, please see:</span></div></div><div><div><div>11</div></div><div><span>&lt;https://www.gnu.org/software/gdb/bugs/&gt;.</span></div></div><div><div><div>12</div></div><div><span>Find the GDB manual and other documentation resources online at:</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>&lt;http://www.gnu.org/software/gdb/documentation/&gt;.</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>For help, type "help".</span></div></div><div><div><div>16</div></div><div><span>Type "apropos word" to search for commands related to "word"...</span></div></div><div><div><div>17</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>前面是版权和帮助信息，真正有用的只有最后两行。<code>Reading symbols from ./gdb_demo...</code> 表示 GDB 在读取可执行文件里的调试信息；如果这里后面跟着 <code>(No debugging symbols found in ./gdb_demo)</code>，说明编译时没有加 <code>-g</code>，后面的源码级调试基本都用不了。<code>(gdb)</code> 是 GDB 的提示符，从这里开始输入的是 GDB 命令，不是 Shell 命令。</p><p>从现在开始我都用 <code>gdb -q -nx</code>。下面是第一次完整会话，我把它拆成几段来读。</p><section><h3>设置断点<a href="#设置断点"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break main</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x23ab: file gdb_demo.cpp, line 25.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一行要按字段读：</p><ul>
<li><code>Breakpoint 1</code>：断点编号。后面的 <code>delete 1</code>、<code>disable 1</code> 都用这个编号。</li>
<li><code>at 0x23ab</code>：断点所在的指令地址。注意它很小，因为程序还没有运行，这只是函数在可执行文件里的偏移量。</li>
<li><code>file gdb_demo.cpp, line 25.</code>：GDB 把这个地址映射回的源码位置。</li>
</ul><p>我让它停在 <code>main</code>，它却报告第 25 行而不是第 24 行的函数签名。这是因为 GDB 会跳过函数序言（保存 <code>rbp</code>、开辟栈空间的那几条指令），直接停在第一行真正的用户代码上。</p></section><section><h3>启动程序<a href="#启动程序"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) run</span></div></div><div><div><div>2</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>3</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>4</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:25</span></div></div><div><div><div>7</div></div><div><span>25      const std::string mode = argc &gt; 1 ? argv[1] : "";</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>Starting program: /tmp/gdb-post/gdb_demo</code>：GDB fork 出一个子进程并执行目标程序。我的实验目录是 <code>/tmp/gdb-post</code>，你的路径会不一样。</li>
<li><code>[Thread debugging using libthread_db enabled]</code> 和下一行：GDB 加载了 glibc 附带的 <code>libthread_db</code>，用来理解线程。在 NixOS 上它来自 <code>/nix/store/...-glibc-.../lib</code>，只要这两行出现，线程调试就是正常的。</li>
<li><code>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:25</code>：命中了 1 号断点，停在 <code>main</code> 函数里，括号里是函数参数的当前值，最后是文件和行号。<code>argv</code> 是一个栈地址，它会随环境变量的总长度变化，所以你看到的数字几乎肯定和我不同。</li>
<li><code>25	    const std::string mode = ...</code>：行号、一个 Tab，然后是源码。</li>
</ul><p>这里有一个我一开始完全理解错的点：<strong>GDB 显示出来的这一行还没有执行</strong>。它表示”下一步将要执行第 25 行”。此时 <code>mode</code> 还没有被构造。</p></section><section><h3>单步和打印<a href="#单步和打印"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) next</span></div></div><div><div><div>2</div></div><div><span>26      std::vector&lt;int&gt; values{3, 5, 7, 9};</span></div></div><div><div><div>3</div></div><div><span>(gdb) next</span></div></div><div><div><div>4</div></div><div><span>27      const int total = recursive_sum(values, 0);</span></div></div><div><div><div>5</div></div><div><span>(gdb) print values</span></div></div><div><div><div>6</div></div><div><span>$1 = std::vector of length 4, capacity 4 = {3, 5, 7, 9}</span></div></div><div><div><div>7</div></div><div><span>(gdb) next</span></div></div><div><div><div>8</div></div><div><span>28      const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>9</div></div><div><span>(gdb) print total</span></div></div><div><div><div>10</div></div><div><span>$2 = 24</span></div></div><div><div><div>11</div></div><div><span>(gdb) print average</span></div></div><div><div><div>12</div></div><div><span>$3 = 6.9533558069247378e-310</span></div></div><div><div><div>13</div></div><div><span>(gdb) next</span></div></div><div><div><div>14</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>15</div></div><div><span>(gdb) print average</span></div></div><div><div><div>16</div></div><div><span>$4 = 8</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>next</code> 执行当前行，然后打印下一个将要执行的行。执行两次之后停在第 27 行，此时第 26 行已经执行完，<code>values</code> 已经构造好了，所以可以 <code>print values</code>。</p><p><code>$1 = std::vector of length 4, capacity 4 = {3, 5, 7, 9}</code> 也要拆开读：</p><ul>
<li><code>$1</code> 是 GDB 的值历史编号。每次 <code>print</code> 的结果都会存成 <code>$1</code>、<code>$2</code>、<code>$3</code>……，后面可以当变量用。</li>
<li><code>std::vector of length 4, capacity 4 = {...}</code> 是 libstdc++ pretty printer 的输出格式：长度、容量、元素。后面我会用 <code>print/r</code> 看看它下面的原始结构体。</li>
</ul><p>接着停在第 28 行时打印 <code>total</code> 得到 <code>24</code>，说明递归求和是对的。然后打印 <code>average</code> 得到 <code>6.9533558069247378e-310</code>：这不是 GDB 出错，而是第 28 行还没执行，<code>average</code> 所在的栈内存里还是上一个使用者留下的残留字节，按 <code>double</code> 解释就成了一个极小的数。再 <code>next</code> 一次，<code>average</code> 变成了 <code>8</code>。</p><p>到这里第一个 bug 已经定位了：<code>total</code> 是对的，<code>average</code> 在第 28 行被算成了 <code>8</code>，问题只能在 <code>/ (values.size() - 1)</code> 里。</p></section><section><h3>跨行语句与程序输出<a href="#跨行语句与程序输出"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) next</span></div></div><div><div><div>2</div></div><div><span>30                &lt;&lt; ", expected_average=6\n";</span></div></div><div><div><div>3</div></div><div><span>(gdb) next</span></div></div><div><div><div>4</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>5</div></div><div><span>32      int counter = 0;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>第 29 到 30 行是一条跨两行的语句，所以 <code>next</code> 会先停在第 30 行。直到这条语句真正执行完，<code>total=24, average=8, expected_average=6</code> 才出现在屏幕上。程序自己的输出和 GDB 的输出混在同一个终端里，没有任何前缀区分，这一点要习惯。</p></section><section><h3>运行到结束<a href="#运行到结束"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) continue</span></div></div><div><div><div>2</div></div><div><span>Continuing.</span></div></div><div><div><div>3</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 154515)]</span></div></div><div><div><div>4</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 154515) exited]</span></div></div><div><div><div>5</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 154516)]</span></div></div><div><div><div>6</div></div><div><span>counter=2000</span></div></div><div><div><div>7</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 154516) exited]</span></div></div><div><div><div>8</div></div><div><span>[Inferior 1 (process 154466) exited normally]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>Continuing.</code>：放行，直到下一个断点、信号或进程退出。</li>
<li><code>[New Thread 0x7ffff77ff6c0 (LWP 154515)]</code>：程序创建了一个线程。<code>0x7ffff77ff6c0</code> 是 <code>pthread_t</code> 的值，在 glibc 里它是线程控制块的地址；<code>LWP 154515</code> 是内核里的线程 ID，也就是 <code>ps -L</code> 或 <code>top -H</code> 里看到的那个数字。</li>
<li><code>[Thread ... exited]</code>：线程退出。两个线程的创建和退出顺序每次运行都可能不同。</li>
<li><code>[Inferior 1 (process 154466) exited normally]</code>：inferior 是 GDB 对”被调试进程”的称呼，<code>exited normally</code> 表示退出码为 0。如果退出码非 0，会显示成 <code>exited with code 01</code> 这样的形式。</li>
</ul><p>如果进程还活着时输入 <code>quit</code>，GDB 会确认一次：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) quit</span></div></div><div><div><div>2</div></div><div><span>A debugging session is active.</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Inferior 1 [process 131334] will be killed.</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Quit anyway? (y or n)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输入 <code>y</code> 就会杀掉被调试进程并退出。</p></section></section>
<section><h2>断点：先决定在哪里停<a href="#断点先决定在哪里停"><span>#</span></a></h2><p>我调试时通常先问自己：程序应该在哪些位置停下来？下面这组命令一次性设置了五种断点，然后查看、禁用、删除它们：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break main</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x23ab: file gdb_demo.cpp, line 25.</span></div></div><div><div><div>5</div></div><div><span>(gdb) break recursive_sum</span></div></div><div><div><div>6</div></div><div><span>Breakpoint 2 at 0x22da: file gdb_demo.cpp, line 11.</span></div></div><div><div><div>7</div></div><div><span>(gdb) break gdb_demo.cpp:14 if index == 2</span></div></div><div><div><div>8</div></div><div><span>Breakpoint 3 at 0x22f8: file gdb_demo.cpp, line 14.</span></div></div><div><div><div>9</div></div><div><span>(gdb) tbreak gdb_demo.cpp:38</span></div></div><div><div><div>10</div></div><div><span>Temporary breakpoint 4 at 0x25be: file gdb_demo.cpp, line 38.</span></div></div><div><div><div>11</div></div><div><span>(gdb) rbreak ^worker</span></div></div><div><div><div>12</div></div><div><span>Breakpoint 5 at 0x233c: file gdb_demo.cpp, line 18.</span></div></div><div><div><div>13</div></div><div><span>void worker(int&amp;, std::mutex&amp;);</span></div></div><div><div><div>14</div></div><div><span>Successfully created breakpoint 5.</span></div></div><div><div><div>15</div></div><div><span>(gdb) info breakpoints</span></div></div><div><div><div>16</div></div><div><span>Num     Type           Disp Enb Address            What</span></div></div><div><div><div>17</div></div><div><span>1       breakpoint     keep y   0x00000000000023ab in main(int, char**) at gdb_demo.cpp:25</span></div></div><div><div><div>18</div></div><div><span>2       breakpoint     keep y   0x00000000000022da in recursive_sum(std::vector&lt;int, std::allocator&lt;int&gt; &gt; const&amp;, unsigned long) at gdb_demo.cpp:11</span></div></div><div><div><div>19</div></div><div><span>3       breakpoint     keep y   0x00000000000022f8 in recursive_sum(std::vector&lt;int, std::allocator&lt;int&gt; &gt; const&amp;, unsigned long) at gdb_demo.cpp:14</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>stop only if index == 2</span></div></div><div><div><div>21</div></div><div><span>4       breakpoint     del  y   0x00000000000025be in main(int, char**) at gdb_demo.cpp:38</span></div></div><div><div><div>22</div></div><div><span>5       breakpoint     keep y   0x000000000000233c in worker(int&amp;, std::mutex&amp;) at gdb_demo.cpp:18</span></div></div><div><div><div>23</div></div><div><span>(gdb) disable 2 4</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>五种写法分别是：按函数名、按函数名、按”文件&lt;行号&gt;”加条件、临时断点、正则表达式。<code>rbreak ^worker</code> 会给所有名字匹配 <code>^worker</code> 的函数下断点，并把匹配到的函数签名 <code>void worker(int&amp;, std::mutex&amp;);</code> 打印出来，让你确认它没有误伤别的函数。</p><p><code>info breakpoints</code> 是一张表，列的含义是：</p><ul>
<li><code>Num</code>：断点编号。</li>
<li><code>Type</code>：<code>breakpoint</code> 是普通断点，后面还会看到 <code>hw watchpoint</code> 和 <code>catchpoint</code>。</li>
<li><code>Disp</code>：命中后的处置方式。<code>keep</code> 表示保留，<code>del</code> 表示命中一次就删除，这就是 <code>tbreak</code> 的”临时”。</li>
<li><code>Enb</code>：是否启用，<code>y</code> 或 <code>n</code>。</li>
<li><code>Address</code>：指令地址。</li>
<li><code>What</code>：完整的函数签名和源码位置。C++ 的函数会显示带参数类型的全名，<code>recursive_sum(std::vector&lt;int, std::allocator&lt;int&gt; &gt; const&amp;, unsigned long)</code> 就是 <code>recursive_sum</code> 展开模板和 <code>std::size_t</code> 之后的样子。</li>
<li><code>stop only if index == 2</code>：条件断点的条件，单独缩进一行。</li>
</ul><p><code>disable 2 4</code> 暂时关掉 2 号和 4 号断点，但保留它们的编号和条件。然后运行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) run</span></div></div><div><div><div>2</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>3</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>4</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:25</span></div></div><div><div><div>7</div></div><div><span>25      const std::string mode = argc &gt; 1 ? argv[1] : "";</span></div></div><div><div><div>8</div></div><div><span>(gdb) continue</span></div></div><div><div><div>9</div></div><div><span>Continuing.</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Breakpoint 3, recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=2) at gdb_demo.cpp:14</span></div></div><div><div><div>12</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>13</div></div><div><span>(gdb) print index</span></div></div><div><div><div>14</div></div><div><span>$1 = 2</span></div></div><div><div><div>15</div></div><div><span>(gdb) info breakpoints</span></div></div><div><div><div>16</div></div><div><span>Num     Type           Disp Enb Address            What</span></div></div><div><div><div>17</div></div><div><span>1       breakpoint     keep y   0x00005555555563ab in main(int, char**) at gdb_demo.cpp:25</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>breakpoint already hit 1 time</span></div></div><div><div><div>19</div></div><div><span>2       breakpoint     keep n   0x00005555555562da in recursive_sum(std::vector&lt;int, std::allocator&lt;int&gt; &gt; const&amp;, unsigned long) at gdb_demo.cpp:11</span></div></div><div><div><div>20</div></div><div><span>3       breakpoint     keep y   0x00005555555562f8 in recursive_sum(std::vector&lt;int, std::allocator&lt;int&gt; &gt; const&amp;, unsigned long) at gdb_demo.cpp:14</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>stop only if index == 2</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>breakpoint already hit 1 time</span></div></div><div><div><div>23</div></div><div><span>4       breakpoint     del  n   0x00005555555565be in main(int, char**) at gdb_demo.cpp:38</span></div></div><div><div><div>24</div></div><div><span>5       breakpoint     keep y   0x000055555555633c in worker(int&amp;, std::mutex&amp;) at gdb_demo.cpp:18</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>条件断点只在 <code>index=2</code> 那一层递归停下，前面 <code>index=0</code> 和 <code>index=1</code> 的两次经过都被 GDB 在后台判断后自动放行了。</p><p>再看第二次 <code>info breakpoints</code>，有两处变化。第一，<code>Address</code> 从 <code>0x00000000000023ab</code> 变成了 <code>0x00005555555563ab</code>：程序是 PIE（位置无关可执行文件），运行时被加载到 <code>0x555555554000</code>，<code>0x555555554000 + 0x23ab = 0x5555555563ab</code>。GDB 默认关闭了地址空间随机化（<code>show disable-randomization</code> 是 <code>on</code>），所以每次运行的基址都一样，这也是为什么你的地址很可能和我的一模一样，而栈地址却不一样。第二，多出了 <code>breakpoint already hit 1 time</code>，命中计数。2 号和 4 号的 <code>Enb</code> 是 <code>n</code>。</p><p>接着删除条件断点，继续运行到线程里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) delete 3</span></div></div><div><div><div>2</div></div><div><span>(gdb) continue</span></div></div><div><div><div>3</div></div><div><span>Continuing.</span></div></div><div><div><div>4</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>5</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 154927)]</span></div></div><div><div><div>6</div></div><div><span>[Switching to Thread 0x7ffff77ff6c0 (LWP 154927)]</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>Thread 2 "gdb_demo" hit Breakpoint 5, worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:18</span></div></div><div><div><div>9</div></div><div><span>18      for (int i = 0; i &lt; 1000; ++i) {</span></div></div><div><div><div>10</div></div><div><span>(gdb) continue</span></div></div><div><div><div>11</div></div><div><span>Continuing.</span></div></div><div><div><div>12</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 154928)]</span></div></div><div><div><div>13</div></div><div><span>[Switching to Thread 0x7ffff6ffe6c0 (LWP 154928)]</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>Thread 3 "gdb_demo" hit Breakpoint 5, worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:18</span></div></div><div><div><div>16</div></div><div><span>18      for (int i = 0; i &lt; 1000; ++i) {</span></div></div><div><div><div>17</div></div><div><span>(gdb) info breakpoints</span></div></div><div><div><div>18</div></div><div><span>Num     Type           Disp Enb Address            What</span></div></div><div><div><div>19</div></div><div><span>1       breakpoint     keep y   0x00005555555563ab in main(int, char**) at gdb_demo.cpp:25</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>breakpoint already hit 1 time</span></div></div><div><div><div>21</div></div><div><span>2       breakpoint     keep n   0x00005555555562da in recursive_sum(std::vector&lt;int, std::allocator&lt;int&gt; &gt; const&amp;, unsigned long) at gdb_demo.cpp:11</span></div></div><div><div><div>22</div></div><div><span>4       breakpoint     del  n   0x00005555555565be in main(int, char**) at gdb_demo.cpp:38</span></div></div><div><div><div>23</div></div><div><span>5       breakpoint     keep y   0x000055555555633c in worker(int&amp;, std::mutex&amp;) at gdb_demo.cpp:18</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>breakpoint already hit 2 times</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>线程命中断点时的格式和主线程略有不同：</p><ul>
<li><code>[Switching to Thread 0x7ffff77ff6c0 (LWP 154927)]</code>：GDB 把”当前线程”切换到了命中断点的那个线程。</li>
<li><code>Thread 2 "gdb_demo" hit Breakpoint 5</code>：<code>Thread 2</code> 是 GDB 自己给线程分配的编号（主线程是 1），不是 LWP；<code>"gdb_demo"</code> 是线程名，新线程默认继承进程名。</li>
<li><code>counter=@0x7fffffffb60c: 0</code>：<code>counter</code> 是引用，GDB 用 <code>@地址: 值</code> 的格式同时显示它指向哪里和当前值。</li>
<li><code>mutex=...</code>：参数没有被打印，不是出错。GDB 默认的 <code>print frame-arguments</code> 是 <code>scalars</code>，在停止行和调用栈里只展开标量参数，结构体和类一律折叠成 <code>...</code>。前面 <code>recursive_sum</code> 的 <code>values=std::vector of length 4, capacity 4 = {...}</code> 也是同一条规则，只是 pretty printer 额外给出了长度和容量。</li>
</ul><p>两个线程分别命中一次，所以 5 号断点显示 <code>already hit 2 times</code>。</p><section><h3>还不存在的函数：pending 断点<a href="#还不存在的函数pending-断点"><span>#</span></a></h3><p>如果断点要打在一个共享库里，而库还没被加载，GDB 会先问你：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break execute_native_thread_routine</span></div></div><div><div><div>4</div></div><div><span>Function "execute_native_thread_routine" not defined.</span></div></div><div><div><div>5</div></div><div><span>Make breakpoint pending on future shared library load? (y or [n]) y</span></div></div><div><div><div>6</div></div><div><span>Breakpoint 1 (execute_native_thread_routine) pending.</span></div></div><div><div><div>7</div></div><div><span>(gdb) info breakpoints</span></div></div><div><div><div>8</div></div><div><span>Num     Type           Disp Enb Address    What</span></div></div><div><div><div>9</div></div><div><span>1       breakpoint     keep y   &lt;PENDING&gt;  execute_native_thread_routine</span></div></div><div><div><div>10</div></div><div><span>(gdb) run</span></div></div><div><div><div>11</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>12</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>13</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>14</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>15</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 155304)]</span></div></div><div><div><div>16</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 155305)]</span></div></div><div><div><div>17</div></div><div><span>[Switching to Thread 0x7ffff77ff6c0 (LWP 155304)]</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>Thread 2 "gdb_demo" hit Breakpoint 1, 0x00007ffff7cf2e90 in execute_native_thread_routine ()</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>21</div></div><div><span>(gdb) info breakpoints</span></div></div><div><div><div>22</div></div><div><span>Num     Type           Disp Enb Address            What</span></div></div><div><div><div>23</div></div><div><span>1       breakpoint     keep y   0x00007ffff7cf2e90 &lt;execute_native_thread_routine&gt;</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>breakpoint already hit 1 time</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>execute_native_thread_routine</code> 是 libstdc++ 里每个 <code>std::thread</code> 的入口函数。程序启动前 libstdc++ 还没加载，GDB 找不到这个符号，所以询问是否设置 pending 断点。回答 <code>y</code> 之后，<code>Address</code> 列显示 <code>&lt;PENDING&gt;</code>；运行后库被加载，断点被解析到真实地址 <code>0x00007ffff7cf2e90</code>。</p><p>命中时的停止行和之前也不一样：<code>0x00007ffff7cf2e90 in execute_native_thread_routine () from /nix/store/...libstdc++.so.6</code>。函数名后面是空括号，最后是 <code>from 库路径</code> 而不是 <code>at 文件:行号</code>，这就是”没有调试信息”的样子：GDB 只知道符号名，不知道参数和源码。前面讲 flake 时提到 <code>libstdc++</code> 依然带着 <code>(*)</code>，指的就是这个。</p></section></section>
<section><h2>控制执行：不止 next 和 continue<a href="#控制执行不止-next-和-continue"><span>#</span></a></h2><p><code>next</code> 和 <code>continue</code> 之外，还有几个命令可以更精确地控制程序跑到哪里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) start</span></div></div><div><div><div>4</div></div><div><span>Temporary breakpoint 1 at 0x23ab: file gdb_demo.cpp, line 25.</span></div></div><div><div><div>5</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>6</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>7</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>Temporary breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:25</span></div></div><div><div><div>10</div></div><div><span>25      const std::string mode = argc &gt; 1 ? argv[1] : "";</span></div></div><div><div><div>11</div></div><div><span>(gdb) advance 29</span></div></div><div><div><div>12</div></div><div><span>main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:29</span></div></div><div><div><div>13</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>14</div></div><div><span>(gdb) until 32</span></div></div><div><div><div>15</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>16</div></div><div><span>main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:32</span></div></div><div><div><div>17</div></div><div><span>32      int counter = 0;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>start</code> 等于”在 <code>main</code> 上设一个临时断点再 <code>run</code>”，所以输出里是 <code>Temporary breakpoint 1</code>。</li>
<li><code>advance 29</code> 运行到第 29 行。它的停止行开头没有 <code>Breakpoint N,</code>，因为这不是断点命中，只是到达了目标位置。</li>
<li><code>until 32</code> 在这里的效果和 <code>advance</code> 类似；它真正的用途是在循环末尾使用时直接跑完整个循环，而不是一圈一圈地 <code>next</code>。中途第 29 到 30 行执行完毕，所以那行输出出现在中间。</li>
</ul><p><code>finish</code>、<code>return</code> 和 <code>jump</code> 更激进，我用它们做一个反事实实验：如果 <code>index=2</code> 那一层递归直接返回 100，会发生什么？</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break recursive_sum if index == 2</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x22da: file gdb_demo.cpp, line 11.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Breakpoint 1, recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=2) at gdb_demo.cpp:11</span></div></div><div><div><div>11</div></div><div><span>11      if (index == values.size()) {</span></div></div><div><div><div>12</div></div><div><span>(gdb) return 100</span></div></div><div><div><div>13</div></div><div><span>Make recursive_sum(std::vector&lt;int, std::allocator&lt;int&gt; &gt; const&amp;, unsigned long) return now? (y or n) y</span></div></div><div><div><div>14</div></div><div><span>#0  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=1) at gdb_demo.cpp:14</span></div></div><div><div><div>15</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>16</div></div><div><span>(gdb) finish</span></div></div><div><div><div>17</div></div><div><span>Run till exit from #0  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=1) at gdb_demo.cpp:14</span></div></div><div><div><div>18</div></div><div><span>0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=0) at gdb_demo.cpp:14</span></div></div><div><div><div>19</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>20</div></div><div><span>Value returned is $1 = 105</span></div></div><div><div><div>21</div></div><div><span>(gdb) finish</span></div></div><div><div><div>22</div></div><div><span>Run till exit from #0  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=0) at gdb_demo.cpp:14</span></div></div><div><div><div>23</div></div><div><span>0x000055555555642c in main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:27</span></div></div><div><div><div>24</div></div><div><span>27      const int total = recursive_sum(values, 0);</span></div></div><div><div><div>25</div></div><div><span>Value returned is $2 = 108</span></div></div><div><div><div>26</div></div><div><span>(gdb) next</span></div></div><div><div><div>27</div></div><div><span>28      const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>28</div></div><div><span>(gdb) print total</span></div></div><div><div><div>29</div></div><div><span>$3 = 108</span></div></div><div><div><div>30</div></div><div><span>(gdb) jump 32</span></div></div><div><div><div>31</div></div><div><span>Continuing at 0x5555555564f4.</span></div></div><div><div><div>32</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 156151)]</span></div></div><div><div><div>33</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 156152)]</span></div></div><div><div><div>34</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 156151) exited]</span></div></div><div><div><div>35</div></div><div><span>counter=2000</span></div></div><div><div><div>36</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 156152) exited]</span></div></div><div><div><div>37</div></div><div><span>[Inferior 1 (process 156113) exited normally]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐段读：</p><ul>
<li><code>return 100</code> 会先确认，因为它会强制弹出当前栈帧，函数剩下的代码不再执行。确认后 GDB 打印新的当前帧 <code>#0  0x0000555555556324 in recursive_sum (..., index=1)</code>，这是调用者那一层。注意行首多了一个地址，后面讲栈帧时会解释。</li>
<li><code>finish</code> 运行到当前函数返回。<code>Run till exit from #0 ...</code> 说明从哪一帧开始，接下来一行是返回到的位置，最后 <code>Value returned is $1 = 105</code> 是返回值，同样进入了值历史。<code>105 = 5 + 100</code>，<code>108 = 3 + 105</code>。</li>
<li>回到 <code>main</code> 后 <code>print total</code> 得到 <code>108</code>，被伪造的返回值一路传了上来。</li>
<li><code>jump 32</code> 让程序从第 32 行继续执行，<code>Continuing at 0x5555555564f4.</code> 是第 32 行对应的地址。第 28 到 30 行被整个跳过了，所以这次运行的输出里根本没有 <code>total=...</code> 那一行。</li>
</ul><p><code>return</code> 和 <code>jump</code> 是实验工具，不是修复手段。跳过构造函数、析构函数或者加锁操作，很容易把程序带进一个真实执行中永远不会出现的状态。</p><p>还有一个 <code>starti</code>，它停在进程的第一条机器指令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) starti</span></div></div><div><div><div>4</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Program stopped.</span></div></div><div><div><div>7</div></div><div><span>0x00007ffff7fe3e00 in _start () from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2</span></div></div><div><div><div>8</div></div><div><span>(gdb) x/i $pc</span></div></div><div><div><div>9</div></div><div><span>=&gt; 0x7ffff7fe3e00 &lt;_start&gt;:  mov    %rsp,%rdi</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Program stopped.</code> 没有断点编号，因为这是 <code>starti</code> 本身的停止。第一条指令不在 <code>main</code> 里，也不在 <code>gdb_demo</code> 里，而是在动态链接器 <code>ld-linux-x86-64.so.2</code> 的 <code>_start</code>。<code>x/i $pc</code> 的格式后面讲内存时再展开。</p></section>
<section><h2>栈帧：递归时我到底在看谁<a href="#栈帧递归时我到底在看谁"><span>#</span></a></h2><p>这一节我把断点设在第 12 行 <code>return 0;</code>，也就是递归最深处：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break gdb_demo.cpp:12</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x22f1: file gdb_demo.cpp, line 12.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Breakpoint 1, recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=4) at gdb_demo.cpp:12</span></div></div><div><div><div>11</div></div><div><span>12          return 0;</span></div></div><div><div><div>12</div></div><div><span>(gdb) bt</span></div></div><div><div><div>13</div></div><div><span>#0  recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=4) at gdb_demo.cpp:12</span></div></div><div><div><div>14</div></div><div><span>#1  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=3) at gdb_demo.cpp:14</span></div></div><div><div><div>15</div></div><div><span>#2  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=2) at gdb_demo.cpp:14</span></div></div><div><div><div>16</div></div><div><span>#3  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=1) at gdb_demo.cpp:14</span></div></div><div><div><div>17</div></div><div><span>#4  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=0) at gdb_demo.cpp:14</span></div></div><div><div><div>18</div></div><div><span>#5  0x000055555555642c in main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:27</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>bt</code> 是 <code>backtrace</code> 的缩写，每一行是一个栈帧，从当前帧往调用者方向排列：</p><ul>
<li><code>#0</code>、<code>#1</code>……：帧编号。<code>#0</code> 是当前正在执行的函数，编号越大越靠近 <code>main</code>。</li>
<li><code>0x0000555555556324</code>：这一帧的 pc，也就是这一帧”停在哪条指令”。对 <code>#1</code> 到 <code>#5</code> 来说，它是返回地址：被调用的函数返回后将从这里继续。</li>
<li><code>in recursive_sum (...)</code>：函数名和参数。</li>
<li><code>at gdb_demo.cpp:14</code>：pc 对应的源码位置。</li>
</ul><p><code>#0</code> 前面没有地址，是因为当前 pc 恰好是第 12 行的第一条指令，GDB 认为直接显示行号就够了。<code>#1</code> 到 <code>#4</code> 的地址完全相同，因为它们都是同一条 <code>call recursive_sum</code> 指令之后的那条指令，只是属于不同层的递归。4 个元素加上终止那一层，一共 5 层 <code>recursive_sum</code>，再加上 <code>main</code>，正好 6 帧。</p><p>切换到某一帧再看：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) frame 2</span></div></div><div><div><div>2</div></div><div><span>#2  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=2) at gdb_demo.cpp:14</span></div></div><div><div><div>3</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>4</div></div><div><span>(gdb) print index</span></div></div><div><div><div>5</div></div><div><span>$1 = 2</span></div></div><div><div><div>6</div></div><div><span>(gdb) info args</span></div></div><div><div><div>7</div></div><div><span>values = std::vector of length 4, capacity 4 = {3, 5, 7, 9}</span></div></div><div><div><div>8</div></div><div><span>index = 2</span></div></div><div><div><div>9</div></div><div><span>(gdb) info locals</span></div></div><div><div><div>10</div></div><div><span>No locals.</span></div></div><div><div><div>11</div></div><div><span>(gdb) up</span></div></div><div><div><div>12</div></div><div><span>#3  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=1) at gdb_demo.cpp:14</span></div></div><div><div><div>13</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>14</div></div><div><span>(gdb) down</span></div></div><div><div><div>15</div></div><div><span>#2  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=2) at gdb_demo.cpp:14</span></div></div><div><div><div>16</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>frame 2</code> 切换到 2 号帧，打印这一帧的摘要和源码行。之后的 <code>print index</code> 读取的是 <strong>当前选中帧</strong> 里的 <code>index</code>，所以得到 <code>2</code>，而不是程序实际停住的那一帧里的 <code>4</code>。<code>info args</code> 打印当前帧的参数，这里没有折叠，<code>values</code> 完整展开；<code>info locals</code> 打印局部变量，<code>recursive_sum</code> 没有局部变量，所以是 <code>No locals.</code>。<code>up</code> 往调用者方向走一帧，<code>down</code> 往回走。</p><p><code>info frame</code> 的输出最密集：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) info frame</span></div></div><div><div><div>2</div></div><div><span>Stack level 2, frame at 0x7fffffffb550:</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>rip = 0x555555556324 in recursive_sum (gdb_demo.cpp:14); saved rip = 0x555555556324</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>called by frame at 0x7fffffffb580, caller of frame at 0x7fffffffb520</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>source language c++.</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>Arglist at 0x7fffffffb540, args: values=std::vector of length 4, capacity 4 = {...}, index=2</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>Locals at 0x7fffffffb540, Previous frame's sp is 0x7fffffffb550</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>Saved registers:</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>rbx at 0x7fffffffb538, rbp at 0x7fffffffb540, rip at 0x7fffffffb548</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>Stack level 2</code>：帧编号。</li>
<li><code>frame at 0x7fffffffb550</code>：这一帧的 CFA（canonical frame address），可以理解为调用这个函数之前的栈指针值。每一帧有唯一的 CFA。</li>
<li><code>rip = 0x555555556324 in recursive_sum (gdb_demo.cpp:14)</code>：这一帧的 pc。</li>
<li><code>saved rip = 0x555555556324</code>：这一帧返回后要跳到的地址。因为它的调用者也是 <code>recursive_sum</code>，所以两者相同。</li>
<li><code>called by frame at ...</code>、<code>caller of frame at ...</code>：上一帧和下一帧的 CFA。</li>
<li><code>Arglist at</code>、<code>Locals at</code>：参数和局部变量相对的基址，在 <code>-O0</code> 下就是 <code>rbp</code>。</li>
<li><code>Saved registers</code>：这一帧保存了哪些寄存器以及保存在哪里。<code>rip at 0x7fffffffb548</code> 就是返回地址在栈上的位置。</li>
</ul><p>最后回到 0 号帧，连续 <code>finish</code> 三次，看返回值怎样一层层累加：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) frame 0</span></div></div><div><div><div>2</div></div><div><span>#0  recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=4) at gdb_demo.cpp:12</span></div></div><div><div><div>3</div></div><div><span>12          return 0;</span></div></div><div><div><div>4</div></div><div><span>(gdb) finish</span></div></div><div><div><div>5</div></div><div><span>Run till exit from #0  recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=4) at gdb_demo.cpp:12</span></div></div><div><div><div>6</div></div><div><span>0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=3) at gdb_demo.cpp:14</span></div></div><div><div><div>7</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>8</div></div><div><span>Value returned is $2 = 0</span></div></div><div><div><div>9</div></div><div><span>(gdb) finish</span></div></div><div><div><div>10</div></div><div><span>Run till exit from #0  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=3) at gdb_demo.cpp:14</span></div></div><div><div><div>11</div></div><div><span>0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=2) at gdb_demo.cpp:14</span></div></div><div><div><div>12</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>13</div></div><div><span>Value returned is $3 = 9</span></div></div><div><div><div>14</div></div><div><span>(gdb) finish</span></div></div><div><div><div>15</div></div><div><span>Run till exit from #0  0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=2) at gdb_demo.cpp:14</span></div></div><div><div><div>16</div></div><div><span>0x0000555555556324 in recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=1) at gdb_demo.cpp:14</span></div></div><div><div><div>17</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>18</div></div><div><span>Value returned is $4 = 16</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>0</code>、<code>9 = 9 + 0</code>、<code>16 = 7 + 9</code>。每次 <code>finish</code> 之后停在调用者那一行的中间，所以停止行前面都带着地址。</p></section>
<section><h2>数据：让 GDB 替我问问题<a href="#数据让-gdb-替我问问题"><span>#</span></a></h2><p>停在第 29 行，<code>total</code> 和 <code>average</code> 都已经算好了：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break 29</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x248d: file gdb_demo.cpp, line 29.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:29</span></div></div><div><div><div>11</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>12</div></div><div><span>(gdb) print total</span></div></div><div><div><div>13</div></div><div><span>$1 = 24</span></div></div><div><div><div>14</div></div><div><span>(gdb) print average</span></div></div><div><div><div>15</div></div><div><span>$2 = 8</span></div></div><div><div><div>16</div></div><div><span>(gdb) print $</span></div></div><div><div><div>17</div></div><div><span>$3 = 8</span></div></div><div><div><div>18</div></div><div><span>(gdb) print $1 + 1</span></div></div><div><div><div>19</div></div><div><span>$4 = 25</span></div></div><div><div><div>20</div></div><div><span>(gdb) print/x total</span></div></div><div><div><div>21</div></div><div><span>$5 = 0x18</span></div></div><div><div><div>22</div></div><div><span>(gdb) print/t total</span></div></div><div><div><div>23</div></div><div><span>$6 = 11000</span></div></div><div><div><div>24</div></div><div><span>(gdb) whatis values</span></div></div><div><div><div>25</div></div><div><span>type = std::vector&lt;int&gt;</span></div></div><div><div><div>26</div></div><div><span>(gdb) whatis values[0]</span></div></div><div><div><div>27</div></div><div><span>type = int</span></div></div><div><div><div>28</div></div><div><span>(gdb) ptype recursive_sum</span></div></div><div><div><div>29</div></div><div><span>type = int (const std::vector&lt;int&gt; &amp;, std::size_t)</span></div></div><div><div><div>30</div></div><div><span>(gdb) print values[2]</span></div></div><div><div><div>31</div></div><div><span>$7 = 7</span></div></div><div><div><div>32</div></div><div><span>(gdb) print values.size()</span></div></div><div><div><div>33</div></div><div><span>$8 = 4</span></div></div><div><div><div>34</div></div><div><span>(gdb) print values.data()</span></div></div><div><div><div>35</div></div><div><span>Cannot evaluate function -- may be inlined</span></div></div><div><div><div>36</div></div><div><span>(gdb) print (double) total / values.size()</span></div></div><div><div><div>37</div></div><div><span>$9 = 6</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>print $</code> 取最近一次的值，<code>print $1 + 1</code> 用值历史做运算，结果依然进入历史，成为 <code>$4</code>。</li>
<li><code>print/x</code> 和 <code>print/t</code> 是输出格式，<code>/x</code> 十六进制，<code>/t</code> 二进制，<code>/d</code> 十进制，<code>/c</code> 字符。<code>24</code> 是 <code>0x18</code>，也是 <code>11000</code>。</li>
<li><code>whatis</code> 给出表达式的类型名；<code>ptype</code> 会展开类型，对函数就是完整的签名。对 <code>std::vector</code> 用 <code>ptype</code> 会打印出整个类定义，几百行，一般我用 <code>whatis</code>。</li>
<li><code>print values[2]</code> 和 <code>print values.size()</code> 可以工作，<code>print values.data()</code> 却报 <code>Cannot evaluate function -- may be inlined</code>。这个报错信息有点误导。真正的原因是 <code>data()</code> 是一个模板成员函数，我的程序从来没有调用过它，编译器就根本没有生成这个函数的代码，GDB 没有东西可调用。<code>operator[]</code> 和 <code>size()</code> 在程序里用过，所以它们存在。</li>
<li><code>print (double) total / values.size()</code> 得到 <code>6</code>，这是我对正确公式的一次试算，结果和 <code>expected_average</code> 一致。</li>
</ul><p>在 GDB 里调用函数要谨慎。<code>print some_function()</code> 会真的在被调试进程里执行这个函数，它可能修改全局状态、申请内存或者拿锁。我优先读取变量和内存，只在确认函数没有副作用时才调用它。</p><p><code>display</code> 让某个表达式在每次停下时自动打印：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) display total</span></div></div><div><div><div>2</div></div><div><span>1: total = 24</span></div></div><div><div><div>3</div></div><div><span>(gdb) display/x counter</span></div></div><div><div><div>4</div></div><div><span>2: /x counter = 0xffffffff</span></div></div><div><div><div>5</div></div><div><span>(gdb) next</span></div></div><div><div><div>6</div></div><div><span>30                &lt;&lt; ", expected_average=6\n";</span></div></div><div><div><div>7</div></div><div><span>1: total = 24</span></div></div><div><div><div>8</div></div><div><span>2: /x counter = 0xffffffff</span></div></div><div><div><div>9</div></div><div><span>(gdb) info display</span></div></div><div><div><div>10</div></div><div><span>Auto-display expressions now in effect:</span></div></div><div><div><div>11</div></div><div><span>Num Enb Expression</span></div></div><div><div><div>12</div></div><div><span>1:   y  total</span></div></div><div><div><div>13</div></div><div><span>2:   y  /x counter</span></div></div><div><div><div>14</div></div><div><span>(gdb) undisplay 2</span></div></div><div><div><div>15</div></div><div><span>(gdb) next</span></div></div><div><div><div>16</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>17</div></div><div><span>32      int counter = 0;</span></div></div><div><div><div>18</div></div><div><span>1: total = 24</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>1: total = 24</code> 开头的 <code>1:</code> 是 display 编号，<code>/x counter</code> 会把格式也显示出来。此时 <code>counter</code> 还没初始化，<code>0xffffffff</code> 是栈上的残留值。<code>info display</code> 列出所有自动显示项，<code>undisplay 2</code> 删除 2 号。</p><section><h3>pretty printer 下面是什么<a href="#pretty-printer-下面是什么"><span>#</span></a></h3><p>pretty printer 很方便，但有时我想知道对象的真实布局。停在第 34 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break 34</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x2518: file gdb_demo.cpp, line 34.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:34</span></div></div><div><div><div>12</div></div><div><span>34      std::thread first(worker, std::ref(counter), std::ref(mutex));</span></div></div><div><div><div>13</div></div><div><span>(gdb) print/r values</span></div></div><div><div><div>14</div></div><div><span>$1 = {&lt;std::_Vector_base&lt;int, std::allocator&lt;int&gt; &gt;&gt; = {</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>_M_impl = {&lt;std::allocator&lt;int&gt;&gt; = {&lt;std::__new_allocator&lt;int&gt;&gt; = {&lt;No data fields&gt;}, &lt;No data fields&gt;}, &lt;std::_Vector_base&lt;int, std::allocator&lt;int&gt; &gt;::_Vector_impl_data&gt; = {_M_start = 0x55555556f320, _M_finish = 0x55555556f330, _M_end_of_storage = 0x55555556f330}, &lt;No data fields&gt;}}, &lt;No data fields&gt;}</span></div></div><div><div><div>16</div></div><div><span>(gdb) print &amp;values[0]</span></div></div><div><div><div>17</div></div><div><span>$2 = (int *) 0x55555556f320</span></div></div><div><div><div>18</div></div><div><span>(gdb) print values[0]@4</span></div></div><div><div><div>19</div></div><div><span>$3 = {3, 5, 7, 9}</span></div></div><div><div><div>20</div></div><div><span>(gdb) print mutex</span></div></div><div><div><div>21</div></div><div><span>$4 = {&lt;std::__mutex_base&gt; = {_M_mutex = {__data = {__lock = 0, __count = 0, __owner = 0, __nusers = 0, __kind = 0, __spins = 0, __elision = 0, __list = {</span></div></div><div><div><div>22</div></div><div><span><span>          </span></span><span>__prev = 0x0, __next = 0x0}}, __size = '\000' &lt;repeats 39 times&gt;, __align = 0}}, &lt;No data fields&gt;}</span></div></div><div><div><div>23</div></div><div><span>(gdb) set print pretty on</span></div></div><div><div><div>24</div></div><div><span>(gdb) print mutex</span></div></div><div><div><div>25</div></div><div><span>$5 = {</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>&lt;std::__mutex_base&gt; = {</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>_M_mutex = {</span></div></div><div><div><div>28</div></div><div><span><span>      </span></span><span>__data = {</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>__lock = 0,</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>__count = 0,</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>__owner = 0,</span></div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>__nusers = 0,</span></div></div><div><div><div>33</div></div><div><span><span>        </span></span><span>__kind = 0,</span></div></div><div><div><div>34</div></div><div><span><span>        </span></span><span>__spins = 0,</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>__elision = 0,</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>__list = {</span></div></div><div><div><div>37</div></div><div><span><span>          </span></span><span>__prev = 0x0,</span></div></div><div><div><div>38</div></div><div><span><span>          </span></span><span>__next = 0x0</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>      </span></span><span>},</span></div></div><div><div><div>41</div></div><div><span><span>      </span></span><span>__size = '\000' &lt;repeats 39 times&gt;,</span></div></div><div><div><div>42</div></div><div><span><span>      </span></span><span>__align = 0</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>}, &lt;No data fields&gt;}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>print/r</code> 的 <code>r</code> 是 raw，绕过 pretty printer。原来 <code>std::vector&lt;int&gt;</code> 的本体只有三个指针：<code>_M_start</code> 指向第一个元素，<code>_M_finish</code> 指向最后一个元素之后，<code>_M_end_of_storage</code> 指向已分配内存的末尾。<code>0x...f330 - 0x...f320 = 16</code> 字节，正好 4 个 <code>int</code>，这就是 <code>length 4, capacity 4</code> 的来源。尖括号 <code>&lt;std::_Vector_base&lt;...&gt;&gt;</code> 表示基类子对象，<code>&lt;No data fields&gt;</code> 表示空基类。</p><p><code>print values[0]@4</code> 里的 <code>@</code> 是 GDB 特有的运算符：从左边这个对象开始，把连续 4 个同类型元素当成数组打印。面对一个裸指针或者 C 数组时它非常有用。</p><p><code>std::mutex</code> 没有 pretty printer，默认挤在一行里很难读。<code>set print pretty on</code> 之后，每个字段单独一行并且有缩进。这个 <code>mutex</code> 里全是 0，因为第 33 行已经构造过它，而且此时还没有人加锁。</p></section></section>
<section><h2>内存、寄存器与汇编<a href="#内存寄存器与汇编"><span>#</span></a></h2><p><code>x</code> 命令直接读内存，格式是 <code>x/nfu 地址</code>：<code>n</code> 是数量，<code>f</code> 是格式（<code>d</code> 十进制、<code>x</code> 十六进制、<code>s</code> 字符串、<code>i</code> 指令），<code>u</code> 是单位（<code>b</code> 1 字节、<code>h</code> 2 字节、<code>w</code> 4 字节、<code>g</code> 8 字节）。接着上面的会话：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) x/4dw &amp;values[0]</span></div></div><div><div><div>2</div></div><div><span>0x55555556f320:  3  5  7  9</span></div></div><div><div><div>3</div></div><div><span>(gdb) x/16xb &amp;values[0]</span></div></div><div><div><div>4</div></div><div><span>0x55555556f320:  0x03  0x00  0x00  0x00  0x05  0x00  0x00  0x00</span></div></div><div><div><div>5</div></div><div><span>0x55555556f328:  0x07  0x00  0x00  0x00  0x09  0x00  0x00  0x00</span></div></div><div><div><div>6</div></div><div><span>(gdb) x/2xg &amp;values[0]</span></div></div><div><div><div>7</div></div><div><span>0x55555556f320:  0x0000000500000003  0x0000000900000007</span></div></div><div><div><div>8</div></div><div><span>(gdb) x/s argv[0]</span></div></div><div><div><div>9</div></div><div><span>0x7fffffffbf28:  "/tmp/gdb-post/gdb_demo"</span></div></div><div><div><div>10</div></div><div><span>(gdb) x/3i $pc</span></div></div><div><div><div>11</div></div><div><span>=&gt; 0x555555556518 &lt;main(int, char**)+404&gt;:  lea    -0xf0(%rbp),%rax</span></div></div><div><div><div>12</div></div><div><span><span>   </span></span><span>0x55555555651f &lt;main(int, char**)+411&gt;:  mov    %rax,%rdi</span></div></div><div><div><div>13</div></div><div><span><span>   </span></span><span>0x555555556522 &lt;main(int, char**)+414&gt;:  call   0x555555557463 &lt;_ZSt3refISt5mutexESt17reference_wrapperIT_ERS2_&gt;</span></div></div><div><div><div>14</div></div><div><span>(gdb) print $pc</span></div></div><div><div><div>15</div></div><div><span>$6 = (void (*)(void)) 0x555555556518 &lt;main(int, char**)+404&gt;</span></div></div><div><div><div>16</div></div><div><span>(gdb) info registers rip rsp rbp</span></div></div><div><div><div>17</div></div><div><span>rip            0x555555556518      0x555555556518 &lt;main(int, char**)+404&gt;</span></div></div><div><div><div>18</div></div><div><span>rsp            0x7fffffffb5b0      0x7fffffffb5b0</span></div></div><div><div><div>19</div></div><div><span>rbp            0x7fffffffb6d0      0x7fffffffb6d0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>每一行输出都是 <code>地址:</code> 加上从这个地址开始的若干个值，用 Tab 分隔。</p><ul>
<li><code>x/4dw</code> 按 4 字节十进制读 4 个单元，就是 <code>3 5 7 9</code>。</li>
<li><code>x/16xb</code> 按字节读 16 个。<code>3</code> 被存成 <code>0x03 0x00 0x00 0x00</code>，低位字节在低地址，这是 x86-64 的小端序。一行显示 8 个字节，所以第二行从 <code>0x...f328</code> 开始。</li>
<li><code>x/2xg</code> 按 8 字节读。同样 8 个字节被当成一个整数时，小端序让高地址的 <code>5</code> 跑到了高位：<code>0x0000000500000003</code>。三种视角看的是完全相同的 16 个字节。</li>
<li><code>x/s argv[0]</code> 把地址当成 C 字符串读，直到遇到 <code>\0</code>。</li>
<li><code>x/3i $pc</code> 反汇编 3 条指令。<code>=&gt;</code> 标出当前 pc，<code>&lt;main(int, char**)+404&gt;</code> 是”这条指令距离 <code>main</code> 起点 404 字节”。这里用的是 GDB 默认的 AT&amp;T 语法：源操作数在前，目标在后，寄存器带 <code>%</code> 前缀，<code>-0xf0(%rbp)</code> 表示地址 <code>rbp - 0xf0</code>。<code>call</code> 后面那个长长的 <code>_ZSt3ref...</code> 是 <code>std::ref&lt;std::mutex&gt;</code> 被 mangle 之后的符号名。</li>
<li><code>$pc</code> 是 GDB 对程序计数器的通用名字，x86-64 上等于 <code>$rip</code>。</li>
<li><code>info registers</code> 的三列是：寄存器名、十六进制原始值、“自然”格式。对 <code>rip</code> 来说自然格式是符号加偏移，对 <code>rsp</code> 这类通用寄存器就是再显示一次数值。</li>
</ul><p>整个函数的反汇编用 <code>disassemble /s</code>，它把源码行和指令交错排列。我在 <code>recursive_sum</code> 入口处执行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break recursive_sum</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x22da: file gdb_demo.cpp, line 11.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Breakpoint 1, recursive_sum (values=std::vector of length 4, capacity 4 = {...}, index=0) at gdb_demo.cpp:11</span></div></div><div><div><div>11</div></div><div><span>11      if (index == values.size()) {</span></div></div><div><div><div>12</div></div><div><span>(gdb) disassemble /s recursive_sum</span></div></div><div><div><div>13</div></div><div><span>Dump of assembler code for function _Z13recursive_sumRKSt6vectorIiSaIiEEm:</span></div></div><div><div><div>14</div></div><div><span>gdb_demo.cpp:</span></div></div><div><div><div>15</div></div><div><span>10  int recursive_sum(const std::vector&lt;int&gt;&amp; values, std::size_t index) {</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>0x00005555555562c9 &lt;+0&gt;:  push   %rbp</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>0x00005555555562ca &lt;+1&gt;:  mov    %rsp,%rbp</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>0x00005555555562cd &lt;+4&gt;:  push   %rbx</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>0x00005555555562ce &lt;+5&gt;:  sub    $0x18,%rsp</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>0x00005555555562d2 &lt;+9&gt;:  mov    %rdi,-0x18(%rbp)</span></div></div><div><div><div>21</div></div><div><span><span>   </span></span><span>0x00005555555562d6 &lt;+13&gt;:  mov    %rsi,-0x20(%rbp)</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>11      if (index == values.size()) {</span></div></div><div><div><div>24</div></div><div><span>=&gt; 0x00005555555562da &lt;+17&gt;:  mov    -0x18(%rbp),%rax</span></div></div><div><div><div>25</div></div><div><span><span>   </span></span><span>0x00005555555562de &lt;+21&gt;:  mov    %rax,%rdi</span></div></div><div><div><div>26</div></div><div><span><span>   </span></span><span>0x00005555555562e1 &lt;+24&gt;:  call   0x555555556fae &lt;_ZNKSt6vectorIiSaIiEE4sizeEv&gt;</span></div></div><div><div><div>27</div></div><div><span><span>   </span></span><span>0x00005555555562e6 &lt;+29&gt;:  cmp    %rax,-0x20(%rbp)</span></div></div><div><div><div>28</div></div><div><span><span>   </span></span><span>0x00005555555562ea &lt;+33&gt;:  sete   %al</span></div></div><div><div><div>29</div></div><div><span><span>   </span></span><span>0x00005555555562ed &lt;+36&gt;:  test   %al,%al</span></div></div><div><div><div>30</div></div><div><span><span>   </span></span><span>0x00005555555562ef &lt;+38&gt;:  je     0x5555555562f8 &lt;_Z13recursive_sumRKSt6vectorIiSaIiEEm+47&gt;</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>12          return 0;</span></div></div><div><div><div>33</div></div><div><span><span>   </span></span><span>0x00005555555562f1 &lt;+40&gt;:  mov    $0x0,%eax</span></div></div><div><div><div>34</div></div><div><span><span>   </span></span><span>0x00005555555562f6 &lt;+45&gt;:  jmp    0x555555556326 &lt;_Z13recursive_sumRKSt6vectorIiSaIiEEm+93&gt;</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>13      }</span></div></div><div><div><div>37</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>38</div></div><div><span><span>   </span></span><span>0x00005555555562f8 &lt;+47&gt;:  mov    -0x20(%rbp),%rdx</span></div></div><div><div><div>39</div></div><div><span><span>   </span></span><span>0x00005555555562fc &lt;+51&gt;:  mov    -0x18(%rbp),%rax</span></div></div><div><div><div>40</div></div><div><span><span>   </span></span><span>0x0000555555556300 &lt;+55&gt;:  mov    %rdx,%rsi</span></div></div><div><div><div>41</div></div><div><span><span>   </span></span><span>0x0000555555556303 &lt;+58&gt;:  mov    %rax,%rdi</span></div></div><div><div><div>42</div></div><div><span><span>   </span></span><span>0x0000555555556306 &lt;+61&gt;:  call   0x555555557186 &lt;_ZNKSt6vectorIiSaIiEEixEm&gt;</span></div></div><div><div><div>43</div></div><div><span><span>   </span></span><span>0x000055555555630b &lt;+66&gt;:  mov    (%rax),%ebx</span></div></div><div><div><div>44</div></div><div><span><span>   </span></span><span>0x000055555555630d &lt;+68&gt;:  mov    -0x20(%rbp),%rax</span></div></div><div><div><div>45</div></div><div><span><span>   </span></span><span>0x0000555555556311 &lt;+72&gt;:  lea    0x1(%rax),%rdx</span></div></div><div><div><div>46</div></div><div><span><span>   </span></span><span>0x0000555555556315 &lt;+76&gt;:  mov    -0x18(%rbp),%rax</span></div></div><div><div><div>47</div></div><div><span><span>   </span></span><span>0x0000555555556319 &lt;+80&gt;:  mov    %rdx,%rsi</span></div></div><div><div><div>48</div></div><div><span><span>   </span></span><span>0x000055555555631c &lt;+83&gt;:  mov    %rax,%rdi</span></div></div><div><div><div>49</div></div><div><span><span>   </span></span><span>0x000055555555631f &lt;+86&gt;:  call   0x5555555562c9 &lt;_Z13recursive_sumRKSt6vectorIiSaIiEEm&gt;</span></div></div><div><div><div>50</div></div><div><span><span>   </span></span><span>0x0000555555556324 &lt;+91&gt;:  add    %ebx,%eax</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>15  }</span></div></div><div><div><div>53</div></div><div><span><span>   </span></span><span>0x0000555555556326 &lt;+93&gt;:  mov    -0x8(%rbp),%rbx</span></div></div><div><div><div>54</div></div><div><span><span>   </span></span><span>0x000055555555632a &lt;+97&gt;:  leave</span></div></div><div><div><div>55</div></div><div><span><span>   </span></span><span>0x000055555555632b &lt;+98&gt;:  ret</span></div></div><div><div><div>56</div></div><div><span>End of assembler dump.</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>读法和 <code>x/i</code> 一样，只是按源码行分了组：<code>10	int recursive_sum(...)</code> 下面的 6 条指令是函数序言，保存 <code>rbp</code> 和 <code>rbx</code>，开辟 <code>0x18</code> 字节栈空间，再把两个参数 <code>rdi</code>、<code>rsi</code> 存到栈上。<code>=&gt;</code> 指向第 11 行的第一条指令，这正是断点所在的位置。</p><p>这段反汇编顺便解开了前面的一个谜题。第 14 行的 <code>call 0x5555555562c9 &lt;_Z13recursive_sum...&gt;</code> 是递归调用本身，紧跟在它后面的是 <code>0x0000555555556324 &lt;+91&gt;:	add %ebx,%eax</code>。<code>0x555555556324</code> 就是前面 <code>bt</code> 里 <code>#1</code> 到 <code>#4</code> 共用的那个返回地址：递归返回后，把 <code>values[index]</code>（之前存进了 <code>ebx</code>）加到返回值 <code>eax</code> 上。</p><p><code>stepi</code> 和 <code>nexti</code> 是指令级的单步，区别和 <code>step</code>、<code>next</code> 一样，<code>nexti</code> 不会进入 <code>call</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) stepi</span></div></div><div><div><div>2</div></div><div><span>0x00005555555562de  11      if (index == values.size()) {</span></div></div><div><div><div>3</div></div><div><span>(gdb) stepi</span></div></div><div><div><div>4</div></div><div><span>0x00005555555562e1  11      if (index == values.size()) {</span></div></div><div><div><div>5</div></div><div><span>(gdb) x/i $pc</span></div></div><div><div><div>6</div></div><div><span>=&gt; 0x5555555562e1 &lt;_Z13recursive_sumRKSt6vectorIiSaIiEEm+24&gt;:  call   0x555555556fae &lt;_ZNKSt6vectorIiSaIiEE4sizeEv&gt;</span></div></div><div><div><div>7</div></div><div><span>(gdb) nexti</span></div></div><div><div><div>8</div></div><div><span>0x00005555555562e6  11      if (index == values.size()) {</span></div></div><div><div><div>9</div></div><div><span>(gdb) info registers rdi rsi</span></div></div><div><div><div>10</div></div><div><span>rdi            0x7fffffffb610      140737488336400</span></div></div><div><div><div>11</div></div><div><span>rsi            0x0                 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>停止行变成了 <code>0x00005555555562de	11	...</code>：地址、Tab、行号、源码。行首出现地址，说明 pc 在第 11 行的中间而不是开头。<code>nexti</code> 越过了对 <code>size()</code> 的调用。<code>info registers rdi rsi</code> 显示两个参数寄存器：按照 System V x86-64 调用约定，第一个参数放 <code>rdi</code>，第二个放 <code>rsi</code>，所以 <code>rdi</code> 是 <code>values</code> 的地址（引用在底层就是指针），<code>rsi = 0</code> 就是 <code>index</code>。</p></section>
<section><h2>改变状态：做一个反事实实验<a href="#改变状态做一个反事实实验"><span>#</span></a></h2><p>GDB 不仅能看，还能改。我想验证：如果 <code>average</code> 算对了，后面的输出会不会正确？</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break 29</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x248d: file gdb_demo.cpp, line 29.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:29</span></div></div><div><div><div>11</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>12</div></div><div><span>(gdb) print average</span></div></div><div><div><div>13</div></div><div><span>$1 = 8</span></div></div><div><div><div>14</div></div><div><span>(gdb) set variable average = 6</span></div></div><div><div><div>15</div></div><div><span>(gdb) print average</span></div></div><div><div><div>16</div></div><div><span>$2 = 6</span></div></div><div><div><div>17</div></div><div><span>(gdb) set $expected = (double) total / values.size()</span></div></div><div><div><div>18</div></div><div><span>(gdb) print $expected</span></div></div><div><div><div>19</div></div><div><span>$3 = 6</span></div></div><div><div><div>20</div></div><div><span>(gdb) print average == $expected</span></div></div><div><div><div>21</div></div><div><span>$4 = true</span></div></div><div><div><div>22</div></div><div><span>(gdb) next</span></div></div><div><div><div>23</div></div><div><span>30                &lt;&lt; ", expected_average=6\n";</span></div></div><div><div><div>24</div></div><div><span>(gdb) next</span></div></div><div><div><div>25</div></div><div><span>total=24, average=6, expected_average=6</span></div></div><div><div><div>26</div></div><div><span>32      int counter = 0;</span></div></div><div><div><div>27</div></div><div><span>(gdb) continue</span></div></div><div><div><div>28</div></div><div><span>Continuing.</span></div></div><div><div><div>29</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 158570)]</span></div></div><div><div><div>30</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 158570) exited]</span></div></div><div><div><div>31</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 158571)]</span></div></div><div><div><div>32</div></div><div><span>counter=2000</span></div></div><div><div><div>33</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 158571) exited]</span></div></div><div><div><div>34</div></div><div><span>[Inferior 1 (process 158515) exited normally]</span></div></div><div><div><div>35</div></div><div><span>(gdb) print $_exitcode</span></div></div><div><div><div>36</div></div><div><span>$5 = 0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>set variable average = 6</code> 直接改写栈上的变量。<code>average</code> 在源码里是 <code>const</code>，但 <code>const</code> 只是编译期的约束，<code>-O0</code> 下它依然老老实实地住在栈上，GDB 照样可以写。</li>
<li><code>set $expected = ...</code> 创建一个 GDB 自己的 convenience variable，它以 <code>$</code> 开头，只存在于 GDB 里，不占用被调试程序的内存。</li>
<li><code>print average == $expected</code> 得到 <code>true</code>。</li>
<li>继续执行后，程序打印出 <code>average=6</code>。</li>
<li><code>$_exitcode</code> 是 GDB 内置的便利变量，保存最近一次退出码。</li>
</ul><p>这次实验说明，只要 <code>average</code> 的值对了，后面没有其他问题。但这不是修复：真正的修复要改源码并重新编译，被 GDB 改过的进程状态不能当测试结果。</p></section>
<section><h2>watchpoint 与线程<a href="#watchpoint-与线程"><span>#</span></a></h2><p>断点问的是”程序什么时候走到这里”，watchpoint 问的是”这块内存什么时候被改了”。我想知道 <code>counter</code> 从 0 变成 2000 的过程，于是在线程创建之前设置一个 watchpoint：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break 34</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x2518: file gdb_demo.cpp, line 34.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:34</span></div></div><div><div><div>12</div></div><div><span>34      std::thread first(worker, std::ref(counter), std::ref(mutex));</span></div></div><div><div><div>13</div></div><div><span>(gdb) watch counter</span></div></div><div><div><div>14</div></div><div><span>Hardware watchpoint 2: counter</span></div></div><div><div><div>15</div></div><div><span>(gdb) continue</span></div></div><div><div><div>16</div></div><div><span>Continuing.</span></div></div><div><div><div>17</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 158939)]</span></div></div><div><div><div>18</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 158940)]</span></div></div><div><div><div>19</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 158939) exited]</span></div></div><div><div><div>20</div></div><div><span>counter=2000</span></div></div><div><div><div>21</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 158940) exited]</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>Watchpoint 2 deleted because the program has left the block in</span></div></div><div><div><div>24</div></div><div><span>which its expression is valid.</span></div></div><div><div><div>25</div></div><div><span>__libc_start_call_main (main=main@entry=0x555555556384 &lt;main(int, char**)&gt;, argc=argc@entry=1, argv=argv@entry=0x7fffffffb7f8)</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>at ../sysdeps/nptl/libc_start_call_main.h:74</span></div></div><div><div><div>27</div></div><div><span>warning: 74  ../sysdeps/nptl/libc_start_call_main.h: No such file or directory</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Hardware watchpoint 2: counter</code> 说明 GDB 用 CPU 的调试寄存器实现了这个 watchpoint，不需要单步模拟。然而线程把 <code>counter</code> 改了 2000 次，GDB 一次都没有停，直到 <code>main</code> 返回才告诉我 <code>Watchpoint 2 deleted because the program has left the block in which its expression is valid.</code></p><p>我第一次看到这个输出时相当困惑。原因在于 <code>watch counter</code> 监视的是表达式 <code>counter</code>，而这个表达式只在 <code>main</code> 的这一帧里有意义。GDB 把 watchpoint 和 <code>main</code> 的栈帧绑定在一起。据我观察，工作线程写入时触发的事件，因为发生在另一个线程的调用栈上，没有被报告出来。<code>main</code> 返回之后，这一帧消失，watchpoint 就被自动删除了。</p><p>解决办法是 <code>watch -l</code>，也就是 <code>-location</code>：先把表达式求值成一个地址，然后只监视这个地址，不再关心作用域：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break 34</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x2518: file gdb_demo.cpp, line 34.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:34</span></div></div><div><div><div>12</div></div><div><span>34      std::thread first(worker, std::ref(counter), std::ref(mutex));</span></div></div><div><div><div>13</div></div><div><span>(gdb) watch -l counter</span></div></div><div><div><div>14</div></div><div><span>Hardware watchpoint 2: -location counter</span></div></div><div><div><div>15</div></div><div><span>(gdb) continue</span></div></div><div><div><div>16</div></div><div><span>Continuing.</span></div></div><div><div><div>17</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 159309)]</span></div></div><div><div><div>18</div></div><div><span>[Switching to Thread 0x7ffff77ff6c0 (LWP 159309)]</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>Thread 2 "gdb_demo" hit Hardware watchpoint 2: -location counter</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>Old value = 0</span></div></div><div><div><div>23</div></div><div><span>New value = 1</span></div></div><div><div><div>24</div></div><div><span>worker (counter=@0x7fffffffb60c: 1, mutex=...) at gdb_demo.cpp:21</span></div></div><div><div><div>25</div></div><div><span>21      }</span></div></div><div><div><div>26</div></div><div><span>(gdb) info threads</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>Id   Target Id                                     Frame</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>1    Thread 0x7ffff7e90780 (LWP 159290) "gdb_demo" __GI___clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:62</span></div></div><div><div><div>29</div></div><div><span>* 2    Thread 0x7ffff77ff6c0 (LWP 159309) "gdb_demo" worker (counter=@0x7fffffffb60c: 1, mutex=...) at gdb_demo.cpp:21</span></div></div><div><div><div>30</div></div><div><span>(gdb) continue</span></div></div><div><div><div>31</div></div><div><span>Continuing.</span></div></div><div><div><div>32</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 159310)]</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>Thread 2 "gdb_demo" hit Hardware watchpoint 2: -location counter</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>Old value = 1</span></div></div><div><div><div>37</div></div><div><span>New value = 2</span></div></div><div><div><div>38</div></div><div><span>worker (counter=@0x7fffffffb60c: 2, mutex=...) at gdb_demo.cpp:21</span></div></div><div><div><div>39</div></div><div><span>21      }</span></div></div><div><div><div>40</div></div><div><span>(gdb) info watchpoints</span></div></div><div><div><div>41</div></div><div><span>Num     Type           Disp Enb Address            What</span></div></div><div><div><div>42</div></div><div><span>2       hw watchpoint  keep y                      -location counter</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>breakpoint already hit 2 times</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这次立刻就停了：</p><ul>
<li><code>Thread 2 "gdb_demo" hit Hardware watchpoint 2: -location counter</code>：哪个线程、哪个 watchpoint。</li>
<li><code>Old value = 0</code> / <code>New value = 1</code>：写入前后的值。</li>
<li><code>worker (...) at gdb_demo.cpp:21</code>：停止位置。注意是第 21 行而不是执行 <code>++counter</code> 的第 20 行。硬件 watchpoint 在写入指令执行 <strong>之后</strong> 才触发，此时 pc 已经走到了下一条指令，而它属于第 21 行。</li>
</ul><p><code>info threads</code> 是一张线程表：</p><ul>
<li>第一列的 <code>*</code> 标记当前线程。</li>
<li><code>Id</code> 是 GDB 线程编号。</li>
<li><code>Target Id</code> 是 <code>pthread_t</code>、LWP 和线程名。</li>
<li><code>Frame</code> 是这个线程当前所在的帧。1 号主线程此刻停在 <code>__GI___clone3</code>，它正在创建第二个线程。</li>
</ul><p>每次 <code>++counter</code> 都停一次太慢了，加一个条件，让它在 1500 时再停：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) delete 2</span></div></div><div><div><div>2</div></div><div><span>(gdb) watch -l counter if counter == 1500</span></div></div><div><div><div>3</div></div><div><span>Hardware watchpoint 3: -location counter</span></div></div><div><div><div>4</div></div><div><span>(gdb) continue</span></div></div><div><div><div>5</div></div><div><span>Continuing.</span></div></div><div><div><div>6</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 159309) exited]</span></div></div><div><div><div>7</div></div><div><span>[Switching to Thread 0x7ffff6ffe6c0 (LWP 159310)]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>Thread 3 "gdb_demo" hit Hardware watchpoint 3: -location counter</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Old value = 1499</span></div></div><div><div><div>12</div></div><div><span>New value = 1500</span></div></div><div><div><div>13</div></div><div><span>worker (counter=@0x7fffffffb60c: 1500, mutex=...) at gdb_demo.cpp:21</span></div></div><div><div><div>14</div></div><div><span>21      }</span></div></div><div><div><div>15</div></div><div><span>(gdb) print i</span></div></div><div><div><div>16</div></div><div><span>$1 = 499</span></div></div><div><div><div>17</div></div><div><span>(gdb) info threads</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>Id   Target Id                                     Frame</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>1    Thread 0x7ffff7e90780 (LWP 159290) "gdb_demo" __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>20</div></div><div><span>* 3    Thread 0x7ffff6ffe6c0 (LWP 159310) "gdb_demo" worker (counter=@0x7fffffffb60c: 1500, mutex=...) at gdb_demo.cpp:21</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这次命中的是 3 号线程，而且 <code>i = 499</code>。换句话说，3 号线程自己加了 500 次，另外 1000 次来自 2 号线程，后者此时已经退出了（<code>[Thread 0x7ffff77ff6c0 (LWP 159309) exited]</code>）。互斥锁保证了总数正确，但不保证两个线程的交替顺序。</p><p>主线程这时在做什么？</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>(gdb) thread 1</span></div></div><div><div><div>2</div></div><div><span>[Switching to thread 1 (Thread 0x7ffff7e90780 (LWP 159290))]</span></div></div><div><div><div>3</div></div><div><span>#0  __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>4</div></div><div><span>warning: 56  ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S: No such file or directory</span></div></div><div><div><div>5</div></div><div><span>(gdb) bt</span></div></div><div><div><div>6</div></div><div><span>#0  __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>7</div></div><div><span>#1  0x00007ffff789912c in __internal_syscall_cancel (a1=&lt;optimized out&gt;, a2=&lt;optimized out&gt;, a3=&lt;optimized out&gt;, a4=&lt;optimized out&gt;, a5=a5@entry=0,</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>a6=a6@entry=4294967295, nr=202) at cancellation.c:49</span></div></div><div><div><div>9</div></div><div><span>#2  0x00007ffff78998ac in __futex_abstimed_wait_common64 (private=128, futex_word=0x7ffff6ffe990, expected=&lt;optimized out&gt;, op=&lt;optimized out&gt;, abstime=0x0,</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>cancel=true) at futex-internal.c:57</span></div></div><div><div><div>11</div></div><div><span>#3  __futex_abstimed_wait_common (futex_word=futex_word@entry=0x7ffff6ffe990, expected=&lt;optimized out&gt;, clockid=clockid@entry=0, abstime=abstime@entry=0x0,</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>private=private@entry=128, cancel=cancel@entry=true) at futex-internal.c:87</span></div></div><div><div><div>13</div></div><div><span>#4  0x00007ffff789993f in __GI___futex_abstimed_wait_cancelable64 (futex_word=futex_word@entry=0x7ffff6ffe990, expected=&lt;optimized out&gt;,</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>clockid=clockid@entry=0, abstime=abstime@entry=0x0, private=private@entry=128) at futex-internal.c:139</span></div></div><div><div><div>15</div></div><div><span>#5  0x00007ffff789ec8c in __pthread_clockjoin_ex (threadid=140737337353920, thread_return=0x0, clockid=0, abstime=0x0, block=&lt;optimized out&gt;)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>at pthread_join_common.c:108</span></div></div><div><div><div>17</div></div><div><span>#6  0x00007ffff7cf2f1b in std::thread::join() () from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>18</div></div><div><span>#7  0x00005555555565be in main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:37</span></div></div><div><div><div>19</div></div><div><span>(gdb) thread apply all bt</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>Thread 3 (Thread 0x7ffff6ffe6c0 (LWP 159310) "gdb_demo"):</span></div></div><div><div><div>22</div></div><div><span>#0  worker (counter=@0x7fffffffb60c: 1500, mutex=...) at gdb_demo.cpp:21</span></div></div><div><div><div>23</div></div><div><span>#1  0x000055555555889a in std::__invoke_impl&lt;void, void (*)(int&amp;, std::mutex&amp;), std::reference_wrapper&lt;int&gt;, std::reference_wrapper&lt;std::mutex&gt; &gt; (__f=@0x55555556f8d8: 0x55555555632c &lt;worker(int&amp;, std::mutex&amp;)&gt;) at /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/invoke.h:63</span></div></div><div><div><div>24</div></div><div><span>#2  0x00005555555587f0 in std::__invoke&lt;void (*)(int&amp;, std::mutex&amp;), std::reference_wrapper&lt;int&gt;, std::reference_wrapper&lt;std::mutex&gt; &gt; (__fn=@0x55555556f8d8: 0x55555555632c &lt;worker(int&amp;, std::mutex&amp;)&gt;) at /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/invoke.h:98</span></div></div><div><div><div>25</div></div><div><span>#3  0x000055555555873d in std::thread::_Invoker&lt;std::tuple&lt;void (*)(int&amp;, std::mutex&amp;), std::reference_wrapper&lt;int&gt;, std::reference_wrapper&lt;std::mutex&gt; &gt; &gt;::_M_invoke&lt;0ul, 1ul, 2ul&gt; (this=0x55555556f8c8) at /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/std_thread.h:303</span></div></div><div><div><div>26</div></div><div><span>#4  0x00005555555586da in std::thread::_Invoker&lt;std::tuple&lt;void (*)(int&amp;, std::mutex&amp;), std::reference_wrapper&lt;int&gt;, std::reference_wrapper&lt;std::mutex&gt; &gt; &gt;::operator() (this=0x55555556f8c8) at /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/std_thread.h:310</span></div></div><div><div><div>27</div></div><div><span>#5  0x00005555555586be in std::thread::_State_impl&lt;std::thread::_Invoker&lt;std::tuple&lt;void (*)(int&amp;, std::mutex&amp;), std::reference_wrapper&lt;int&gt;, std::reference_wrapper&lt;std::mutex&gt; &gt; &gt; &gt;::_M_run (this=0x55555556f8c0) at /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/std_thread.h:255</span></div></div><div><div><div>28</div></div><div><span>#6  0x00007ffff7cf2ea4 in execute_native_thread_routine () from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>29</div></div><div><span>#7  0x00007ffff789ce73 in start_thread (arg=&lt;optimized out&gt;) at pthread_create.c:448</span></div></div><div><div><div>30</div></div><div><span>#8  0x00007ffff79242bc in __GI___clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>Thread 1 (Thread 0x7ffff7e90780 (LWP 159290) "gdb_demo"):</span></div></div><div><div><div>33</div></div><div><span>#0  __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>34</div></div><div><span>#1  0x00007ffff789912c in __internal_syscall_cancel (a1=&lt;optimized out&gt;, a2=&lt;optimized out&gt;, a3=&lt;optimized out&gt;, a4=&lt;optimized out&gt;, a5=a5@entry=0, a6=a6@entry=4294967295, nr=202) at cancellation.c:49</span></div></div><div><div><div>35</div></div><div><span>#2  0x00007ffff78998ac in __futex_abstimed_wait_common64 (private=128, futex_word=0x7ffff6ffe990, expected=&lt;optimized out&gt;, op=&lt;optimized out&gt;, abstime=0x0, cancel=true) at futex-internal.c:57</span></div></div><div><div><div>36</div></div><div><span>#3  __futex_abstimed_wait_common (futex_word=futex_word@entry=0x7ffff6ffe990, expected=&lt;optimized out&gt;, clockid=clockid@entry=0, abstime=abstime@entry=0x0, private=private@entry=128, cancel=cancel@entry=true) at futex-internal.c:87</span></div></div><div><div><div>37</div></div><div><span>#4  0x00007ffff789993f in __GI___futex_abstimed_wait_cancelable64 (futex_word=futex_word@entry=0x7ffff6ffe990, expected=&lt;optimized out&gt;, clockid=clockid@entry=0, abstime=abstime@entry=0x0, private=private@entry=128) at futex-internal.c:139</span></div></div><div><div><div>38</div></div><div><span>#5  0x00007ffff789ec8c in __pthread_clockjoin_ex (threadid=140737337353920, thread_return=0x0, clockid=0, abstime=0x0, block=&lt;optimized out&gt;) at pthread_join_common.c:108</span></div></div><div><div><div>39</div></div><div><span>#6  0x00007ffff7cf2f1b in std::thread::join() () from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>40</div></div><div><span>#7  0x00005555555565be in main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:37</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>thread 1</code> 切换到主线程，它停在第 37 行的 <code>second.join()</code> 里，一路调到 futex 等待。<code>thread apply all bt</code> 对所有线程执行 <code>bt</code>，每个线程前面有一行 <code>Thread N (...)</code> 标题，这是我遇到死锁或者卡住时第一个执行的命令。3 号线程的栈里那一串 <code>std::__invoke_impl</code>、<code>std::thread::_Invoker</code> 是 <code>std::thread</code> 把你的函数包装起来调用的过程，真正的起点在 <code>#7 start_thread</code> 和 <code>#8 __GI___clone3</code>。</p><section><h3>next 时其他线程在干什么<a href="#next-时其他线程在干什么"><span>#</span></a></h3><p>GDB 默认工作在 all-stop 模式：任何一个线程停下，所有线程都停下；但当你 <code>next</code> 时，所有线程都会被放行。这会产生一些反直觉的输出：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break worker</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x233c: file gdb_demo.cpp, line 18.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 159730)]</span></div></div><div><div><div>11</div></div><div><span>[Switching to Thread 0x7ffff77ff6c0 (LWP 159730)]</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>Thread 2 "gdb_demo" hit Breakpoint 1, worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:18</span></div></div><div><div><div>14</div></div><div><span>18      for (int i = 0; i &lt; 1000; ++i) {</span></div></div><div><div><div>15</div></div><div><span>(gdb) next</span></div></div><div><div><div>16</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 159731)]</span></div></div><div><div><div>17</div></div><div><span>[Switching to Thread 0x7ffff6ffe6c0 (LWP 159731)]</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>Thread 3 "gdb_demo" hit Breakpoint 1, worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:18</span></div></div><div><div><div>20</div></div><div><span>18      for (int i = 0; i &lt; 1000; ++i) {</span></div></div><div><div><div>21</div></div><div><span>(gdb) next</span></div></div><div><div><div>22</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 159730) exited]</span></div></div><div><div><div>23</div></div><div><span>19          std::lock_guard&lt;std::mutex&gt; lock(mutex);</span></div></div><div><div><div>24</div></div><div><span>(gdb) info threads</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>Id   Target Id                                     Frame</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>1    Thread 0x7ffff7e90780 (LWP 159727) "gdb_demo" __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>27</div></div><div><span>* 3    Thread 0x7ffff6ffe6c0 (LWP 159731) "gdb_demo" worker (counter=@0x7fffffffb60c: 1000, mutex=...) at gdb_demo.cpp:19</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我在 2 号线程里 <code>next</code>，结果停下的却是 3 号线程，因为它在我单步期间命中了同一个断点，GDB 自动切了过去。更夸张的是第二次 <code>next</code>：在 3 号线程从第 18 行走到第 19 行的这一小段时间里，2 号线程跑完了整整 1000 次循环并退出，<code>counter</code> 已经是 <code>1000</code>。</p><p>如果我只想专心看一个线程，可以打开 scheduler locking：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break worker</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x233c: file gdb_demo.cpp, line 18.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 160100)]</span></div></div><div><div><div>11</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 160101)]</span></div></div><div><div><div>12</div></div><div><span>[Switching to Thread 0x7ffff77ff6c0 (LWP 160100)]</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>Thread 2 "gdb_demo" hit Breakpoint 1, worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:18</span></div></div><div><div><div>15</div></div><div><span>18      for (int i = 0; i &lt; 1000; ++i) {</span></div></div><div><div><div>16</div></div><div><span>(gdb) set scheduler-locking step</span></div></div><div><div><div>17</div></div><div><span>(gdb) next</span></div></div><div><div><div>18</div></div><div><span>19          std::lock_guard&lt;std::mutex&gt; lock(mutex);</span></div></div><div><div><div>19</div></div><div><span>(gdb) next</span></div></div><div><div><div>20</div></div><div><span>20          ++counter;</span></div></div><div><div><div>21</div></div><div><span>(gdb) info threads</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>Id   Target Id                                     Frame</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>1    Thread 0x7ffff7e90780 (LWP 160097) "gdb_demo" __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>24</div></div><div><span>* 2    Thread 0x7ffff77ff6c0 (LWP 160100) "gdb_demo" worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:20</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>3    Thread 0x7ffff6ffe6c0 (LWP 160101) "gdb_demo" worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:18</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>set scheduler-locking step</code> 的意思是：<code>step</code> 和 <code>next</code> 期间只让当前线程运行，<code>continue</code> 时照常放行所有线程。这次两次 <code>next</code> 都留在 2 号线程里，<code>info threads</code> 显示 3 号线程还停在第 18 行，<code>counter</code> 仍然是 <code>0</code>。</p><p>普通断点和 watchpoint 只能告诉我某个线程在某个时刻做了什么，不能证明程序没有数据竞争。如果要调查竞争，我会用 ThreadSanitizer 这类专门工具，而不是在 GDB 里碰运气。</p></section></section>
<section><h2>崩溃、异常、信号与系统调用<a href="#崩溃异常信号与系统调用"><span>#</span></a></h2><section><h3>段错误<a href="#段错误"><span>#</span></a></h3><p><code>--args</code> 让 GDB 把后面的所有内容当成程序和它的参数，省得进入 GDB 后再 <code>set args</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx --args ./gdb_demo --crash</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) show args</span></div></div><div><div><div>4</div></div><div><span>Argument list to give program being debugged when it is started is "--crash".</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo --crash</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 160478)]</span></div></div><div><div><div>11</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 160478) exited]</span></div></div><div><div><div>12</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 160479)]</span></div></div><div><div><div>13</div></div><div><span>counter=2000</span></div></div><div><div><div>14</div></div><div><span>about to dereference a null pointer</span></div></div><div><div><div>15</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 160479) exited]</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>Thread 1 "gdb_demo" received signal SIGSEGV, Segmentation fault.</span></div></div><div><div><div>18</div></div><div><span>0x000055555555677b in main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:57</span></div></div><div><div><div>19</div></div><div><span>57          std::cout &lt;&lt; *pointer &lt;&lt; "\n";</span></div></div><div><div><div>20</div></div><div><span>(gdb) bt</span></div></div><div><div><div>21</div></div><div><span>#0  0x000055555555677b in main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:57</span></div></div><div><div><div>22</div></div><div><span>(gdb) print pointer</span></div></div><div><div><div>23</div></div><div><span>$1 = (int *) 0x0</span></div></div><div><div><div>24</div></div><div><span>(gdb) print *pointer</span></div></div><div><div><div>25</div></div><div><span>Cannot access memory at address 0x0</span></div></div><div><div><div>26</div></div><div><span>(gdb) x/i $pc</span></div></div><div><div><div>27</div></div><div><span>=&gt; 0x55555555677b &lt;main(int, char**)+1015&gt;:  mov    (%rax),%eax</span></div></div><div><div><div>28</div></div><div><span>(gdb) info registers rax</span></div></div><div><div><div>29</div></div><div><span>rax            0x0                 0</span></div></div><div><div><div>30</div></div><div><span>(gdb) info locals</span></div></div><div><div><div>31</div></div><div><span>pointer = 0x0</span></div></div><div><div><div>32</div></div><div><span>mode = "--crash"</span></div></div><div><div><div>33</div></div><div><span>values = std::vector of length 4, capacity 4 = {3, 5, 7, 9}</span></div></div><div><div><div>34</div></div><div><span>total = 24</span></div></div><div><div><div>35</div></div><div><span>average = 8</span></div></div><div><div><div>36</div></div><div><span>counter = 2000</span></div></div><div><div><div>37</div></div><div><span>mutex = {&lt;std::__mutex_base&gt; = {_M_mutex = {__data = {__lock = 0, __count = 0, __owner = 0, __nusers = 0, __kind = 0, __spins = 0, __elision = 0, __list = {</span></div></div><div><div><div>38</div></div><div><span><span>          </span></span><span>__prev = 0x0, __next = 0x0}}, __size = '\000' &lt;repeats 39 times&gt;, __align = 0}}, &lt;No data fields&gt;}</span></div></div><div><div><div>39</div></div><div><span>first = {_M_id = {_M_thread = 0}}</span></div></div><div><div><div>40</div></div><div><span>second = {_M_id = {_M_thread = 0}}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>show args</code> 确认参数已经带上。</li>
<li>没有设置任何断点，程序一路跑到崩溃。<code>Thread 1 "gdb_demo" received signal SIGSEGV, Segmentation fault.</code> 说明 GDB 截获了内核发给程序的信号，在程序被杀死之前停住了它。</li>
<li>下一行 <code>0x000055555555677b in main (...) at gdb_demo.cpp:57</code> 前面带地址，说明崩溃发生在第 57 行中间的某条指令上。</li>
<li><code>print pointer</code> 的输出 <code>(int *) 0x0</code> 先给类型，再给值。指针类型的值总是这样显示。</li>
<li><code>print *pointer</code> 得到 <code>Cannot access memory at address 0x0</code>，GDB 自己去读这个地址也失败了。</li>
<li><code>x/i $pc</code> 显示出错的那条指令 <code>mov (%rax),%eax</code>：把 <code>rax</code> 指向的内存读进 <code>eax</code>。<code>info registers rax</code> 显示 <code>rax</code> 是 0。从源码到指令到寄存器，三层证据都指向同一个结论：空指针解引用。</li>
<li><code>info locals</code> 列出 <code>main</code> 的所有局部变量。<code>first</code> 和 <code>second</code> 的 <code>_M_thread = 0</code> 表示这两个 <code>std::thread</code> 已经 <code>join</code> 过，不再关联任何线程。</li>
</ul><p>最后一条 <code>generate-core-file</code> 把当前进程的完整内存映像保存成文件。两条 <code>Memory read failed</code> 警告我没有深究，我猜是读不了的特殊映射：<code>0xffffffffff600000</code> 是内核的 vsyscall 页，另一块 1 MiB 的区域看起来像是已退出线程留下的不可读保护区。它们不影响后面的分析。</p></section><section><h3>core 文件：离开现场之后<a href="#core-文件离开现场之后"><span>#</span></a></h3><p>core 文件可以在任何时候重新加载，程序不需要再运行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo core.gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>[New LWP 160475]</span></div></div><div><div><div>4</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>5</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>6</div></div><div><span>Core was generated by `/tmp/gdb-post/gdb_demo --crash'.</span></div></div><div><div><div>7</div></div><div><span>Program terminated with signal SIGSEGV, Segmentation fault.</span></div></div><div><div><div>8</div></div><div><span>#0  0x000055555555677b in main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:57</span></div></div><div><div><div>9</div></div><div><span>57          std::cout &lt;&lt; *pointer &lt;&lt; "\n";</span></div></div><div><div><div>10</div></div><div><span>(gdb) bt</span></div></div><div><div><div>11</div></div><div><span>#0  0x000055555555677b in main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:57</span></div></div><div><div><div>12</div></div><div><span>(gdb) print pointer</span></div></div><div><div><div>13</div></div><div><span>$1 = (int *) 0x0</span></div></div><div><div><div>14</div></div><div><span>(gdb) print values</span></div></div><div><div><div>15</div></div><div><span>$2 = std::vector of length 4, capacity 4 = {3, 5, 7, 9}</span></div></div><div><div><div>16</div></div><div><span>(gdb) x/4gx $sp</span></div></div><div><div><div>17</div></div><div><span>0x7fffffffb5b0:  0x1716151413121110  0x4038000000000000</span></div></div><div><div><div>18</div></div><div><span>0x7fffffffb5c0:  0x00007fffffffb7f8  0x00000002f78ab596</span></div></div><div><div><div>19</div></div><div><span>(gdb) print *(double *)($sp + 8)</span></div></div><div><div><div>20</div></div><div><span>$3 = 24</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>加载时 GDB 会先告诉你这个 core 是怎样产生的：<code>Core was generated by ...</code> 是当时的命令行，<code>Program terminated with signal SIGSEGV</code> 是致死信号，然后直接显示崩溃时的帧。变量、寄存器、内存都还在，但你不能 <code>next</code> 或 <code>continue</code>，因为已经没有活着的进程了。</p><p><code>x/4gx $sp</code> 读取栈顶 32 个字节。第二个值 <code>0x4038000000000000</code> 很有意思：按 IEEE 754 双精度解释，指数部分 <code>0x403</code> 是 1027，减去偏置 1023 得 4，尾数是 1.5，<code>1.5 * 2^4 = 24</code>。<code>print *(double *)($sp + 8)</code> 证实了它就是 <code>24</code>。我推测这是第 28 行 <code>static_cast&lt;double&gt;(total)</code> 留下的临时值，编译器把它存在了栈上。</p><p>core 文件里的地址只对生成它的那个可执行文件有意义。如果重新编译过 <code>gdb_demo</code> 再去加载旧 core，GDB 读到的行号和变量位置很可能是错的，而且它不一定会警告你。</p><p>NixOS 默认启用 systemd-coredump，所以前面在 Shell 里崩溃的那两次，core 早就被系统收走了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ coredumpctl list --no-pager --since "2026-09-30 00:25:00" gdb_demo</span></div></div><div><div><div>2</div></div><div><span>TIME                           PID  UID GID SIG     COREFILE EXE                     SIZE</span></div></div><div><div><div>3</div></div><div><span>Wed 2026-09-30 00:25:11 CST 167935 1000 100 SIGSEGV present  /tmp/gdb-post/gdb_demo   57K</span></div></div><div><div><div>4</div></div><div><span>Wed 2026-09-30 00:25:11 CST 167944 1000 100 SIGABRT present  /tmp/gdb-post/gdb_demo 57.2K</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>SIG</code> 列是信号，<code>COREFILE</code> 为 <code>present</code> 说明文件还在。<code>coredumpctl debug</code> 会找到对应的 core，直接用 GDB 打开它，<code>--debugger-arguments</code> 把参数传给 GDB：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ coredumpctl debug --debugger-arguments='-q -nx' 167935</span></div></div><div><div><div>2</div></div><div><span><span>           </span></span><span>PID: 167935 (gdb_demo)</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>Signal: 11 (SEGV)</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Command Line: ./gdb_demo --crash</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>Executable: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>6</div></div><div><span><span>       </span></span><span>Message: Process 167935 (gdb_demo) of user 1000 dumped core.</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>Module /tmp/gdb-post/gdb_demo without build-id.</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>Module /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>Module libgcc_s.so.1 without build-id.</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>Module libstdc++.so.6 without build-id.</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>Stack trace of thread 167935:</span></div></div><div><div><div>13</div></div><div><span><span>                </span></span><span>#0  0x000055555555677b n/a (/tmp/gdb-post/gdb_demo + 0x277b)</span></div></div><div><div><div>14</div></div><div><span><span>                </span></span><span>#1  0x00007ffff782b285 __libc_start_call_main (libc.so.6 + 0x2b285)</span></div></div><div><div><div>15</div></div><div><span><span>                </span></span><span>#2  0x00007ffff782b338 __libc_start_main@@GLIBC_2.34 (libc.so.6 + 0x2b338)</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>#3  0x0000555555556205 n/a (/tmp/gdb-post/gdb_demo + 0x2205)</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>ELF object binary architecture: AMD x86-64</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>Reading symbols from /tmp/gdb-post/gdb_demo...</span></div></div><div><div><div>20</div></div><div><span>[New LWP 167935]</span></div></div><div><div><div>21</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>22</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>23</div></div><div><span>Core was generated by `./gdb_demo --crash'.</span></div></div><div><div><div>24</div></div><div><span>Program terminated with signal SIGSEGV, Segmentation fault.</span></div></div><div><div><div>25</div></div><div><span>#0  0x000055555555677b in main (argc=2, argv=0x7fffffffb568) at gdb_demo.cpp:57</span></div></div><div><div><div>26</div></div><div><span>57          std::cout &lt;&lt; *pointer &lt;&lt; "\n";</span></div></div><div><div><div>27</div></div><div><span>(gdb) bt</span></div></div><div><div><div>28</div></div><div><span>#0  0x000055555555677b in main (argc=2, argv=0x7fffffffb568) at gdb_demo.cpp:57</span></div></div><div><div><div>29</div></div><div><span>(gdb) print pointer</span></div></div><div><div><div>30</div></div><div><span>$1 = (int *) 0x0</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我省略了几行机器相关的元数据。<code>Message</code> 里的 <code>Stack trace of thread</code> 是 systemd-coredump 自己做的简易栈回溯，它不读 DWARF，所以 <code>main</code> 显示成 <code>n/a (/tmp/gdb-post/gdb_demo + 0x277b)</code>，<code>0x277b</code> 正是 <code>0x55555555677b</code> 减去加载基址。后面进入 GDB 之后，一切又回到了熟悉的格式。</p></section><section><h3>C++ 异常<a href="#c-异常"><span>#</span></a></h3><p><code>--throw</code> 路径里，<code>values.at(4)</code> 会抛出 <code>std::out_of_range</code>。如果没人捕获，程序会在 <code>std::terminate</code> 里 <code>abort</code>，那时栈已经展开了一部分，现场不太好看。更好的做法是在抛出的那一刻停下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx --args ./gdb_demo --throw</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) catch throw</span></div></div><div><div><div>4</div></div><div><span>Catchpoint 1 (throw)</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo --throw</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 161253)]</span></div></div><div><div><div>11</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 161253) exited]</span></div></div><div><div><div>12</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 161254)]</span></div></div><div><div><div>13</div></div><div><span>counter=2000</span></div></div><div><div><div>14</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 161254) exited]</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>Thread 1 "gdb_demo" hit Catchpoint 1 (exception thrown), 0x00007ffff7cc5590 in __cxa_throw ()</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>18</div></div><div><span>(gdb) bt</span></div></div><div><div><div>19</div></div><div><span>#0  0x00007ffff7cc5590 in __cxa_throw () from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>20</div></div><div><span>#1  0x00007ffff7cb5dd6 in std::__throw_out_of_range_fmt(char const*, ...) () from /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6</span></div></div><div><div><div>21</div></div><div><span>#2  0x0000555555557cdd in std::vector&lt;int, std::allocator&lt;int&gt; &gt;::_M_range_check (this=0x7fffffffb610, __n=4)</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>at /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/stl_vector.h:1293</span></div></div><div><div><div>23</div></div><div><span>#3  0x0000555555557619 in std::vector&lt;int, std::allocator&lt;int&gt; &gt;::at (this=0x7fffffffb610, __n=4)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>at /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/stl_vector.h:1315</span></div></div><div><div><div>25</div></div><div><span>#4  0x0000555555556640 in main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:41</span></div></div><div><div><div>26</div></div><div><span>(gdb) frame 4</span></div></div><div><div><div>27</div></div><div><span>#4  0x0000555555556640 in main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:41</span></div></div><div><div><div>28</div></div><div><span>41          std::cout &lt;&lt; values.at(values.size()) &lt;&lt; "\n";</span></div></div><div><div><div>29</div></div><div><span>(gdb) print values.size()</span></div></div><div><div><div>30</div></div><div><span>$1 = 4</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>Catchpoint 1 (throw)</code> 是一种新的断点类型：catchpoint，它捕获的是事件，而不是位置。</li>
<li>命中时停在 <code>__cxa_throw ()</code>，这是 libstdc++ 里所有 <code>throw</code> 最终都会调用的函数。<code>from ...libstdc++.so.6</code> 表示这里没有源码。</li>
<li><code>bt</code> 从下往上读：<code>main</code> 的第 41 行调用了 <code>vector::at(__n=4)</code>，<code>at</code> 调用 <code>_M_range_check</code>，后者调用 <code>__throw_out_of_range_fmt</code>，最后是 <code>__cxa_throw</code>。标准库头文件里的帧有源码位置，指向 <code>/nix/store/...-gcc-15.3.0/include/c++/15.3.0/bits/stl_vector.h</code>；编译进 <code>libstdc++.so</code> 里的帧没有。</li>
<li><code>frame 4</code> 跳回 <code>main</code>，这时可以读 <code>values</code>，确认 <code>size()</code> 是 4，而我访问的下标也是 4。</li>
</ul></section><section><h3>系统调用和信号<a href="#系统调用和信号"><span>#</span></a></h3><p><code>catch syscall</code> 在程序进入和离开系统调用时停下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) catch syscall write</span></div></div><div><div><div>4</div></div><div><span>Catchpoint 1 (syscall 'write' [1])</span></div></div><div><div><div>5</div></div><div><span>(gdb) info signals SIGSEGV</span></div></div><div><div><div>6</div></div><div><span>Signal        Stop  Print  Pass to program  Description</span></div></div><div><div><div>7</div></div><div><span>SIGSEGV       Yes  Yes  Yes    Segmentation fault</span></div></div><div><div><div>8</div></div><div><span>(gdb) handle SIGSEGV nostop noprint</span></div></div><div><div><div>9</div></div><div><span>Signal        Stop  Print  Pass to program  Description</span></div></div><div><div><div>10</div></div><div><span>SIGSEGV       No  No  Yes    Segmentation fault</span></div></div><div><div><div>11</div></div><div><span>(gdb) handle SIGSEGV stop print</span></div></div><div><div><div>12</div></div><div><span>Signal        Stop  Print  Pass to program  Description</span></div></div><div><div><div>13</div></div><div><span>SIGSEGV       Yes  Yes  Yes    Segmentation fault</span></div></div><div><div><div>14</div></div><div><span>(gdb) set environment MODE=debug</span></div></div><div><div><div>15</div></div><div><span>(gdb) show environment MODE</span></div></div><div><div><div>16</div></div><div><span>MODE = debug</span></div></div><div><div><div>17</div></div><div><span>(gdb) run</span></div></div><div><div><div>18</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>19</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>20</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>Catchpoint 1 (call to syscall write), __internal_syscall_cancel (a1=1, a2=93824992342848, a3=40, a4=a4@entry=0, a5=a5@entry=0, a6=a6@entry=0, nr=1)</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>at cancellation.c:44</span></div></div><div><div><div>24</div></div><div><span>warning: 44  cancellation.c: No such file or directory</span></div></div><div><div><div>25</div></div><div><span>(gdb) bt 3</span></div></div><div><div><div>26</div></div><div><span>#0  __internal_syscall_cancel (a1=1, a2=93824992342848, a3=40, a4=a4@entry=0, a5=a5@entry=0, a6=a6@entry=0, nr=1) at cancellation.c:44</span></div></div><div><div><div>27</div></div><div><span>#1  0x00007ffff78991a4 in __syscall_cancel (a1=&lt;optimized out&gt;, a2=&lt;optimized out&gt;, a3=&lt;optimized out&gt;, a4=a4@entry=0, a5=a5@entry=0, a6=a6@entry=0, nr=1)</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>at cancellation.c:75</span></div></div><div><div><div>29</div></div><div><span>#2  0x00007ffff791758e in __GI___libc_write (fd=&lt;optimized out&gt;, buf=&lt;optimized out&gt;, nbytes=&lt;optimized out&gt;) at ../sysdeps/unix/sysv/linux/write.c:26</span></div></div><div><div><div>30</div></div><div><span>(More stack frames follow...)</span></div></div><div><div><div>31</div></div><div><span>(gdb) continue</span></div></div><div><div><div>32</div></div><div><span>Continuing.</span></div></div><div><div><div>33</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>Catchpoint 1 (returned from syscall write), __internal_syscall_cancel (a1=1, a2=93824992342848, a3=40, a4=a4@entry=0, a5=a5@entry=0, a6=a6@entry=0, nr=1)</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>at cancellation.c:44</span></div></div><div><div><div>37</div></div><div><span>44  in cancellation.c</span></div></div><div><div><div>38</div></div><div><span>(gdb) continue</span></div></div><div><div><div>39</div></div><div><span>Continuing.</span></div></div><div><div><div>40</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 161664)]</span></div></div><div><div><div>41</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 161665)]</span></div></div><div><div><div>42</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 161664) exited]</span></div></div><div><div><div>43</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 161665) exited]</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>Thread 1 "gdb_demo" hit Catchpoint 1 (call to syscall write), __syscall_cancel_arch () at ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S:56</span></div></div><div><div><div>46</div></div><div><span>warning: 56  ../sysdeps/unix/sysv/linux/x86_64/syscall_cancel.S: No such file or directory</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>一口气看几件事：</p><ul>
<li><code>Catchpoint 1 (syscall 'write' [1])</code>：<code>[1]</code> 是 <code>write</code> 在 x86-64 上的系统调用号。</li>
<li><code>info signals SIGSEGV</code> 和 <code>handle</code> 的输出是同一张表：<code>Stop</code> 表示信号到达时 GDB 是否暂停，<code>Print</code> 表示是否打印一条消息，<code>Pass to program</code> 表示是否把信号继续交给程序。我先把 <code>SIGSEGV</code> 设成不停不打印，再改回来，只是为了展示输出。实际中更常见的是 <code>handle SIGPIPE nostop noprint pass</code>，给那些故意忽略 <code>SIGPIPE</code> 的网络程序用。</li>
<li><code>set environment MODE=debug</code> 设置被调试程序的环境变量，只在下一次 <code>run</code> 时生效。我的程序没有读它，这里只是展示格式。</li>
<li>第一次命中是 <code>call to syscall write</code>。有了 glibc 的调试符号，参数一目了然：<code>nr=1</code> 是系统调用号，<code>a1=1</code> 是文件描述符 stdout，<code>a3=40</code> 是字节数。<code>"total=24, average=8, expected_average=6\n"</code> 正好 40 个字节，就是第一行输出。</li>
<li><code>bt 3</code> 只打印最内层 3 帧，剩下的用 <code>(More stack frames follow...)</code> 提示。</li>
<li><code>continue</code> 之后，这一行真正写到了终端，然后 GDB 在 <code>returned from syscall write</code> 再停一次。每个系统调用都会停两次：进入和返回。</li>
<li>第二个 <code>write</code> 是 <code>counter=2000</code>，它还没写出去，所以屏幕上暂时看不到。</li>
</ul></section></section>
<section><h2>fork：一个会话，两个进程<a href="#fork一个会话两个进程"><span>#</span></a></h2><p>GDB 默认在 <code>fork</code> 之后继续跟踪父进程，子进程自由运行。<code>--fork</code> 路径里我想停在子进程的第 47 行：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx --args ./gdb_demo --fork</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) show follow-fork-mode</span></div></div><div><div><div>4</div></div><div><span>Debugger response to a program call of fork or vfork is "parent".</span></div></div><div><div><div>5</div></div><div><span>(gdb) set follow-fork-mode child</span></div></div><div><div><div>6</div></div><div><span>(gdb) set detach-on-fork off</span></div></div><div><div><div>7</div></div><div><span>(gdb) break 47</span></div></div><div><div><div>8</div></div><div><span>Breakpoint 1 at 0x2697: file gdb_demo.cpp, line 47.</span></div></div><div><div><div>9</div></div><div><span>(gdb) run</span></div></div><div><div><div>10</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo --fork</span></div></div><div><div><div>11</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>12</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>13</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>14</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 162029)]</span></div></div><div><div><div>15</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 162029) exited]</span></div></div><div><div><div>16</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 162030)]</span></div></div><div><div><div>17</div></div><div><span>counter=2000</span></div></div><div><div><div>18</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 162030) exited]</span></div></div><div><div><div>19</div></div><div><span>[Attaching after Thread 0x7ffff7e90780 (LWP 162026) fork to child process 162031]</span></div></div><div><div><div>20</div></div><div><span>[New inferior 2 (process 162031)]</span></div></div><div><div><div>21</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>22</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>23</div></div><div><span>[Switching to Thread 0x7ffff7e90780 (LWP 162031)]</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>Thread 2.1 "gdb_demo" hit Breakpoint 1.2, main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:47</span></div></div><div><div><div>26</div></div><div><span>47              std::cout &lt;&lt; "child: pid=" &lt;&lt; getpid() &lt;&lt; "\n";</span></div></div><div><div><div>27</div></div><div><span>(gdb) info inferiors</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>Num  Description       Connection           Executable</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>1    process 162026    1 (native)           /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>30</div></div><div><span>* 2    process 162031    1 (native)           /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>31</div></div><div><span>(gdb) print pid</span></div></div><div><div><div>32</div></div><div><span>$1 = 0</span></div></div><div><div><div>33</div></div><div><span>(gdb) inferior 1</span></div></div><div><div><div>34</div></div><div><span>[Switching to inferior 1 [process 162026] (/tmp/gdb-post/gdb_demo)]</span></div></div><div><div><div>35</div></div><div><span>[Switching to thread 1.1 (Thread 0x7ffff7e90780 (LWP 162026))]</span></div></div><div><div><div>36</div></div><div><span>#0  arch_fork (ctid=0x7ffff7e90a50) at ../sysdeps/unix/sysv/linux/arch-fork.h:50</span></div></div><div><div><div>37</div></div><div><span>warning: 50  ../sysdeps/unix/sysv/linux/arch-fork.h: No such file or directory</span></div></div><div><div><div>38</div></div><div><span>(gdb) bt 2</span></div></div><div><div><div>39</div></div><div><span>#0  arch_fork (ctid=0x7ffff7e90a50) at ../sysdeps/unix/sysv/linux/arch-fork.h:50</span></div></div><div><div><div>40</div></div><div><span>#1  __GI__Fork () at ../sysdeps/nptl/_Fork.c:33</span></div></div><div><div><div>41</div></div><div><span>(More stack frames follow...)</span></div></div><div><div><div>42</div></div><div><span>(gdb) inferior 2</span></div></div><div><div><div>43</div></div><div><span>[Switching to inferior 2 [process 162031] (/tmp/gdb-post/gdb_demo)]</span></div></div><div><div><div>44</div></div><div><span>[Switching to thread 2.1 (Thread 0x7ffff7e90780 (LWP 162031))]</span></div></div><div><div><div>45</div></div><div><span>#0  main (argc=2, argv=0x7fffffffb7f8) at gdb_demo.cpp:47</span></div></div><div><div><div>46</div></div><div><span>47              std::cout &lt;&lt; "child: pid=" &lt;&lt; getpid() &lt;&lt; "\n";</span></div></div><div><div><div>47</div></div><div><span>(gdb) continue</span></div></div><div><div><div>48</div></div><div><span>Continuing.</span></div></div><div><div><div>49</div></div><div><span>child: pid=162031</span></div></div><div><div><div>50</div></div><div><span>[Inferior 2 (process 162031) exited normally]</span></div></div><div><div><div>51</div></div><div><span>(gdb) info inferiors</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>Num  Description       Connection           Executable</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>1    process 162026    1 (native)           /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>54</div></div><div><span>* 2    &lt;null&gt;                                 /tmp/gdb-post/gdb_demo</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>set follow-fork-mode child</code> 让 GDB 跟随子进程；<code>set detach-on-fork off</code> 让父进程也保留在 GDB 的控制下，而不是被放走。</li>
<li><code>[Attaching after Thread ... fork to child process 162031]</code> 和 <code>[New inferior 2 (process 162031)]</code>：GDB 为子进程创建了第二个 inferior。</li>
<li><code>Thread 2.1 "gdb_demo" hit Breakpoint 1.2</code>：多 inferior 时，编号变成两级。<code>2.1</code> 是 2 号 inferior 的 1 号线程；<code>1.2</code> 是 1 号断点的第 2 个位置，因为同一个断点在两个进程里各有一份。</li>
<li><code>info inferiors</code> 的 <code>*</code> 标记当前 inferior。</li>
<li><code>print pid</code> 得到 <code>0</code>，这正是子进程里 <code>fork()</code> 的返回值。</li>
<li><code>inferior 1</code> 切回父进程，它停在 <code>arch_fork</code> 里，<code>fork()</code> 还没有返回。</li>
<li>子进程 <code>continue</code> 后正常退出，<code>info inferiors</code> 里 2 号的描述变成 <code>&lt;null&gt;</code>。父进程依然停在 <code>fork</code> 里，要回到 <code>inferior 1</code> 再 <code>continue</code> 才会继续。</li>
</ul><p>顺便说一个跟 GDB 无关但我踩到的坑。如果把 <code>--fork</code> 的输出接到管道，前两行会出现两次：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ./gdb_demo --fork</span></div></div><div><div><div>2</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>3</div></div><div><span>counter=2000</span></div></div><div><div><div>4</div></div><div><span>child: pid=169434</span></div></div><div><div><div>5</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>6</div></div><div><span>counter=2000</span></div></div><div><div><div>7</div></div><div><span>parent: child 169434 exited</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>原因和前面 <code>--crash | cat</code> 一样：管道让 <code>stdout</code> 变成全缓冲，<code>fork</code> 时前两行还在缓冲区里，子进程复制了一份，于是父子进程退出时各刷了一次（hah，这大概是我见过最朴素的”状态被复制”演示）。</p></section>
<section><h2>远程调试：gdbserver<a href="#远程调试gdbserver"><span>#</span></a></h2><p>远程调试时，目标机只跑一个很小的 <code>gdbserver</code>，符号解析、源码显示都在主机的 GDB 里完成。我在同一台机器上开两个终端模拟这个过程。目标端：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdbserver localhost:1234 ./gdb_demo --crash</span></div></div><div><div><div>2</div></div><div><span>Process ./gdb_demo created; pid = 166992</span></div></div><div><div><div>3</div></div><div><span>Listening on port 1234</span></div></div><div><div><div>4</div></div><div><span>Remote debugging from host ::1, port 47264</span></div></div><div><div><div>5</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>6</div></div><div><span>counter=2000</span></div></div><div><div><div>7</div></div><div><span>about to dereference a null pointer</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>主机端：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) target remote localhost:1234</span></div></div><div><div><div>4</div></div><div><span>Remote debugging using localhost:1234</span></div></div><div><div><div>5</div></div><div><span>Reading /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2 from remote target...</span></div></div><div><div><div>6</div></div><div><span>warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally instead.</span></div></div><div><div><div>7</div></div><div><span>Reading /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2 from remote target...</span></div></div><div><div><div>8</div></div><div><span>Reading symbols from target:/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2...</span></div></div><div><div><div>9</div></div><div><span>Reading symbols from /nix/store/fk98h17sza0xxz9iw0fi5b2nn1h4c0pm-glibc-2.42-84-debug/lib/debug/.build-id/cd/a09b1b1c97f3bc480e67118d1239e751f6144c.debug...</span></div></div><div><div><div>10</div></div><div><span>0x00007ffff7fe3e00 in _start () from target:/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2</span></div></div><div><div><div>11</div></div><div><span>(gdb) break 57</span></div></div><div><div><div>12</div></div><div><span>Breakpoint 1 at 0x555555556777: file gdb_demo.cpp, line 57.</span></div></div><div><div><div>13</div></div><div><span>(gdb) continue</span></div></div><div><div><div>14</div></div><div><span>Continuing.</span></div></div><div><div><div>15</div></div><div><span>Reading /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libstdc++.so.6 from remote target...</span></div></div><div><div><div>16</div></div><div><span>Reading /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libm.so.6 from remote target...</span></div></div><div><div><div>17</div></div><div><span>Reading /nix/store/604gsr59rj7dzd0nrhp143rpvf7gyiaz-gcc-15.3.0-lib/lib/libgcc_s.so.1 from remote target...</span></div></div><div><div><div>18</div></div><div><span>Reading /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libc.so.6 from remote target...</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>Breakpoint 1, main (argc=2, argv=0x7fffffffb828) at gdb_demo.cpp:57</span></div></div><div><div><div>21</div></div><div><span>57          std::cout &lt;&lt; *pointer &lt;&lt; "\n";</span></div></div><div><div><div>22</div></div><div><span>(gdb) print pointer</span></div></div><div><div><div>23</div></div><div><span>$1 = (int *) 0x0</span></div></div><div><div><div>24</div></div><div><span>(gdb) continue</span></div></div><div><div><div>25</div></div><div><span>Continuing.</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>Program received signal SIGSEGV, Segmentation fault.</span></div></div><div><div><div>28</div></div><div><span>0x000055555555677b in main (argc=2, argv=0x7fffffffb828) at gdb_demo.cpp:57</span></div></div><div><div><div>29</div></div><div><span>57          std::cout &lt;&lt; *pointer &lt;&lt; "\n";</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>gdbserver</code> 启动进程后先把它停在第一条指令，打印 <code>Listening on port 1234</code>，然后等待连接。主机连上后，它打印 <code>Remote debugging from host ::1, port 47264</code>。</li>
<li>主机端连接后停在 <code>_start ()</code>，和前面 <code>starti</code> 看到的位置一样。</li>
<li><code>Reading ... from remote target...</code> 表示 GDB 在通过 gdbserver 的协议把目标机上的共享库一块块传过来读取符号。那条 warning 已经说了：这很慢，建议用 <code>set sysroot</code> 读取本地文件。</li>
<li>程序的标准输出出现在 <strong>目标端</strong> 的终端里，主机端只能看到 GDB 自己的输出。</li>
<li>这次远程会话里没有出现 <code>[New Thread ...]</code> 和 <code>libthread_db</code> 的消息，线程由 gdbserver 在目标端跟踪。</li>
</ul><p>在 NixOS 上有一个方便之处：两台机器如果部署的是同一个系统闭包，库在两边的 <code>/nix/store</code> 路径完全相同，这时可以直接 <code>set sysroot /</code>，让 GDB 从本地读：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) set sysroot /</span></div></div><div><div><div>4</div></div><div><span>(gdb) target remote localhost:1234</span></div></div><div><div><div>5</div></div><div><span>Remote debugging using localhost:1234</span></div></div><div><div><div>6</div></div><div><span>Reading symbols from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2...</span></div></div><div><div><div>7</div></div><div><span>Reading symbols from /nix/store/fk98h17sza0xxz9iw0fi5b2nn1h4c0pm-glibc-2.42-84-debug/lib/debug/.build-id/cd/a09b1b1c97f3bc480e67118d1239e751f6144c.debug...</span></div></div><div><div><div>8</div></div><div><span>0x00007ffff7fe3e00 in _start () from /nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/ld-linux-x86-64.so.2</span></div></div><div><div><div>9</div></div><div><span>(gdb) continue</span></div></div><div><div><div>10</div></div><div><span>Continuing.</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>Program received signal SIGSEGV, Segmentation fault.</span></div></div><div><div><div>13</div></div><div><span>0x000055555555677b in main (argc=2, argv=0x7fffffffb828) at gdb_demo.cpp:57</span></div></div><div><div><div>14</div></div><div><span>57          std::cout &lt;&lt; *pointer &lt;&lt; "\n";</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这次没有 <code>Reading ... from remote target</code>，也没有那条 warning。如果两边的库不一样，这样做会让 GDB 用错误的符号解释目标进程，所以这只适合我确信两边一致的情况。</p><p>gdbserver 监听的端口没有任何认证，任何能连上这个端口的人都可以控制被调试进程。我这里只绑定了 <code>localhost</code>，真实使用时我也会尽量通过 SSH 隧道转发，而不是直接暴露在网络上。</p></section>
<section><h2>优化构建：当源码和指令不再一一对应<a href="#优化构建当源码和指令不再一一对应"><span>#</span></a></h2><p>用同样的代码编一个 <code>-O2</code> 版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>g++</span><span> </span><span>-std=c++20</span><span> </span><span>-g3</span><span> </span><span>-O2</span><span> </span><span>-fno-omit-frame-pointer</span><span> </span><span>-Wall</span><span> </span><span>-Wextra</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>    </span><span>-o</span><span> </span><span>gdb_demo_O2</span><span> </span><span>gdb_demo.cpp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo_O2</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo_O2...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break recursive_sum</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x26b0: file /nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/stl_vector.h, line 1117.</span></div></div><div><div><div>5</div></div><div><span>(gdb) break main</span></div></div><div><div><div>6</div></div><div><span>Breakpoint 2 at 0x2200: file gdb_demo.cpp, line 25.</span></div></div><div><div><div>7</div></div><div><span>(gdb) run</span></div></div><div><div><div>8</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo_O2</span></div></div><div><div><div>9</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>10</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>Breakpoint 2, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:25</span></div></div><div><div><div>13</div></div><div><span>25      const std::string mode = argc &gt; 1 ? argv[1] : "";</span></div></div><div><div><div>14</div></div><div><span>(gdb) next</span></div></div><div><div><div>15</div></div><div><span>26      std::vector&lt;int&gt; values{3, 5, 7, 9};</span></div></div><div><div><div>16</div></div><div><span>(gdb) next</span></div></div><div><div><div>17</div></div><div><span>25      const std::string mode = argc &gt; 1 ? argv[1] : "";</span></div></div><div><div><div>18</div></div><div><span>(gdb) next</span></div></div><div><div><div>19</div></div><div><span>26      std::vector&lt;int&gt; values{3, 5, 7, 9};</span></div></div><div><div><div>20</div></div><div><span>(gdb) next</span></div></div><div><div><div>21</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>22</div></div><div><span>(gdb) info locals</span></div></div><div><div><div>23</div></div><div><span>mode = ""</span></div></div><div><div><div>24</div></div><div><span>values = std::vector of length 4, capacity 4 = {3, 5, 7, 9}</span></div></div><div><div><div>25</div></div><div><span>total = 24</span></div></div><div><div><div>26</div></div><div><span>average = 8</span></div></div><div><div><div>27</div></div><div><span>counter = -1</span></div></div><div><div><div>28</div></div><div><span>mutex = {&lt;std::__mutex_base&gt; = {_M_mutex = {__data = {__lock = -18808, __count = 32767, __owner = 73728, __nusers = 0, __kind = -18792, __spins = 32767,</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>__elision = 0, __list = {__prev = 0x12000, __next = 0x1}},</span></div></div><div><div><div>30</div></div><div><span><span>      </span></span><span>__size = "\210\266\377\377\377\177\000\000\000 \001\000\000\000\000\000\230\266\377\377\377\177\000\000\000 \001\000\000\000\000\000\001\000\000\000\000\000\000", __align = 140737488336520}}, &lt;No data fields&gt;}</span></div></div><div><div><div>31</div></div><div><span>first = {_M_id = {_M_thread = 208896}}</span></div></div><div><div><div>32</div></div><div><span>second = {_M_id = {_M_thread = 13463046263307902464}}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里有三个意外。</p><p>第一，<code>break recursive_sum</code> 报告的位置不在 <code>gdb_demo.cpp</code>，而在 <code>stl_vector.h</code> 第 1117 行，那是 <code>size()</code> 的定义。<code>-O2</code> 把 <code>values.size()</code> 内联进了 <code>recursive_sum</code>，而且把它的指令排到了函数最前面，所以函数的第一条指令”属于”头文件。</p><p>第二，这个断点永远不会命中。我用 <code>objdump</code> 查了一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ objdump -d -C gdb_demo_O2 | grep -c 'call.*recursive_sum'</span></div></div><div><div><div>2</div></div><div><span>0</span></div></div><div><div><div>3</div></div><div><span>$ objdump -d --no-show-raw-insn -C gdb_demo_O2 | grep -B1 -A2 'mov    $0x18,%esi'</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>22c7:  call   20e0 &lt;std::basic_ostream&lt;char, std::char_traits&lt;char&gt; &gt;&amp; std::operator&lt;&lt; &lt;std::char_traits&lt;char&gt; &gt;(std::basic_ostream&lt;char, std::char_traits&lt;char&gt; &gt;&amp;, char const*)@plt&gt;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>22cc:  mov    $0x18,%esi</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>22d1:  mov    %rax,%rdi</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>22d4:  call   2160 &lt;std::ostream::operator&lt;&lt;(int)@plt&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>整个程序里没有任何一条 <code>call recursive_sum</code>。GCC 在编译期就把 <code>recursive_sum({3, 5, 7, 9}, 0)</code> 算出来了，<code>mov $0x18,%esi</code> 直接把常数 <code>24</code> 传给了 <code>operator&lt;&lt;(int)</code>。<code>recursive_sum</code> 的独立版本依然被生成出来，因为它是一个外部可见的函数，但 <code>main</code> 不再需要它。</p><p>第三，<code>next</code> 在第 25 和 26 行之间来回跳：25、26、25、26、29。编译器把两行代码的指令交错排列了，GDB 每次都如实报告”当前指令属于哪一行”。第 27、28 行直接消失，因为它们已经变成了常数。</p><p><code>info locals</code> 在这里还能读到 <code>total = 24</code>、<code>average = 8</code>，这是 DWARF 记录了”这个变量的值是常数 24”，而不是它真的存在某个地方。</p><p><code>recursive_sum</code> 本身的反汇编也很值得一看：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo_O2</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo_O2...</span></div></div><div><div><div>3</div></div><div><span>(gdb) disassemble /s recursive_sum</span></div></div><div><div><div>4</div></div><div><span>Dump of assembler code for function _Z13recursive_sumRKSt6vectorIiSaIiEEm:</span></div></div><div><div><div>5</div></div><div><span>/nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/stl_vector.h:</span></div></div><div><div><div>6</div></div><div><span>1117        size() const _GLIBCXX_NOEXCEPT</span></div></div><div><div><div>7</div></div><div><span><span>   </span></span><span>0x00000000000026b0 &lt;+0&gt;:  mov    (%rdi),%rdx</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>0x00000000000026b3 &lt;+3&gt;:  mov    0x8(%rdi),%rcx</span></div></div><div><div><div>9</div></div><div><span><span>   </span></span><span>0x00000000000026b7 &lt;+7&gt;:  sub    %rdx,%rcx</span></div></div><div><div><div>10</div></div><div><span><span>   </span></span><span>0x00000000000026ba &lt;+10&gt;:  mov    %rcx,%rax</span></div></div><div><div><div>11</div></div><div><span><span>   </span></span><span>0x00000000000026bd &lt;+13&gt;:  sar    $0x2,%rax</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>gdb_demo.cpp:</span></div></div><div><div><div>14</div></div><div><span>11      if (index == values.size()) {</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>0x00000000000026c1 &lt;+17&gt;:  cmp    %rax,%rsi</span></div></div><div><div><div>16</div></div><div><span><span>   </span></span><span>0x00000000000026c4 &lt;+20&gt;:  je     0x26e0 &lt;_Z13recursive_sumRKSt6vectorIiSaIiEEm+48&gt;</span></div></div><div><div><div>17</div></div><div><span><span>   </span></span><span>0x00000000000026c6 &lt;+22&gt;:  lea    (%rdx,%rsi,4),%rax</span></div></div><div><div><div>18</div></div><div><span><span>   </span></span><span>0x00000000000026ca &lt;+26&gt;:  add    %rdx,%rcx</span></div></div><div><div><div>19</div></div><div><span><span>   </span></span><span>0x00000000000026cd &lt;+29&gt;:  xor    %edx,%edx</span></div></div><div><div><div>20</div></div><div><span><span>   </span></span><span>0x00000000000026cf &lt;+31&gt;:  nop</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>13      }</span></div></div><div><div><div>23</div></div><div><span>14      return values[index] + recursive_sum(values, index + 1);</span></div></div><div><div><div>24</div></div><div><span><span>   </span></span><span>0x00000000000026d0 &lt;+32&gt;:  add    (%rax),%edx</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>/nix/store/8sjgd7q3mdks1rb7rbcv0sallhvrhai5-gcc-15.3.0/include/c++/15.3.0/bits/stl_vector.h:</span></div></div><div><div><div>27</div></div><div><span>1117        size() const _GLIBCXX_NOEXCEPT</span></div></div><div><div><div>28</div></div><div><span><span>   </span></span><span>0x00000000000026d2 &lt;+34&gt;:  add    $0x4,%rax</span></div></div><div><div><div>29</div></div><div><span><span>   </span></span><span>0x00000000000026d6 &lt;+38&gt;:  cmp    %rcx,%rax</span></div></div><div><div><div>30</div></div><div><span><span>   </span></span><span>0x00000000000026d9 &lt;+41&gt;:  jne    0x26d0 &lt;_Z13recursive_sumRKSt6vectorIiSaIiEEm+32&gt;</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>gdb_demo.cpp:</span></div></div><div><div><div>33</div></div><div><span>15  }</span></div></div><div><div><div>34</div></div><div><span><span>   </span></span><span>0x00000000000026db &lt;+43&gt;:  mov    %edx,%eax</span></div></div><div><div><div>35</div></div><div><span><span>   </span></span><span>0x00000000000026dd &lt;+45&gt;:  ret</span></div></div><div><div><div>36</div></div><div><span><span>   </span></span><span>0x00000000000026de &lt;+46&gt;:  xchg   %ax,%ax</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>11      if (index == values.size()) {</span></div></div><div><div><div>39</div></div><div><span><span>   </span></span><span>0x00000000000026e0 &lt;+48&gt;:  xor    %edx,%edx</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div><span>12          return 0;</span></div></div><div><div><div>42</div></div><div><span><span>   </span></span><span>0x00000000000026e2 &lt;+50&gt;:  mov    %edx,%eax</span></div></div><div><div><div>43</div></div><div><span><span>   </span></span><span>0x00000000000026e4 &lt;+52&gt;:  ret</span></div></div><div><div><div>44</div></div><div><span>End of assembler dump.</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>&lt;+32&gt;</code> 到 <code>&lt;+41&gt;</code> 是一个循环：<code>add (%rax),%edx</code> 累加当前元素，<code>add $0x4,%rax</code> 前进 4 个字节，<code>cmp</code> 和 <code>jne</code> 判断是否到达末尾。GCC 把尾部递归改写成了循环，一共 53 字节，<code>-O0</code> 版本是 99 字节，而且递归深度从 5 层变成了 0 层。源码行号在这段反汇编里被切成了好几块，<code>size()</code> 的行号 1117 出现了两次，这就是为什么在优化代码里单步会显得乱跳。</p><p>要看到经典的 <code>&lt;optimized out&gt;</code>，得去 <code>worker</code> 里：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo_O2</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo_O2...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break worker</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x2660: file gdb_demo.cpp, line 18.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo_O2</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 163471)]</span></div></div><div><div><div>11</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 163472)]</span></div></div><div><div><div>12</div></div><div><span>[Switching to Thread 0x7ffff77ff6c0 (LWP 163471)]</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>Thread 2 "gdb_demo_O2" hit Breakpoint 1, worker (counter=@0x7fffffffb60c: 0, mutex=...) at gdb_demo.cpp:18</span></div></div><div><div><div>15</div></div><div><span>18      for (int i = 0; i &lt; 1000; ++i) {</span></div></div><div><div><div>16</div></div><div><span>(gdb) set scheduler-locking step</span></div></div><div><div><div>17</div></div><div><span>(gdb) next</span></div></div><div><div><div>18</div></div><div><span>19          std::lock_guard&lt;std::mutex&gt; lock(mutex);</span></div></div><div><div><div>19</div></div><div><span>(gdb) next</span></div></div><div><div><div>20</div></div><div><span>20          ++counter;</span></div></div><div><div><div>21</div></div><div><span>(gdb) info locals</span></div></div><div><div><div>22</div></div><div><span>lock = {_M_device = @0x7fffffffb670}</span></div></div><div><div><div>23</div></div><div><span>i = &lt;optimized out&gt;</span></div></div><div><div><div>24</div></div><div><span>(gdb) print i</span></div></div><div><div><div>25</div></div><div><span>$1 = &lt;optimized out&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>循环变量 <code>i</code> 被优化成了一个寄存器里的计数器，甚至可能被改写成倒数计数，DWARF 在这个位置无法描述它的值，GDB 只能显示 <code>&lt;optimized out&gt;</code>。这不是 GDB 丢了数据，而是编译器已经改变了程序在机器层面的形状。</p><p>我的流程是：先用 <code>-O0 -g3</code> 复现和定位逻辑错误，再用 <code>-O2 -g3</code> 确认优化后的行为。如果问题只在优化版本里出现，我会保存 core，直接看寄存器和反汇编，而不是为了让变量显示得漂亮一点就退回 <code>-O0</code>，那样很可能把问题本身也一起”修”掉了。</p></section>
<section><h2>没有调试信息时<a href="#没有调试信息时"><span>#</span></a></h2><p>再编两个版本：不加 <code>-g</code> 的，以及 <code>strip</code> 过的：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>g++</span><span> </span><span>-std=c++20</span><span> </span><span>-O0</span><span> </span><span>-o</span><span> </span><span>gdb_demo_nodebug</span><span> </span><span>gdb_demo.cpp</span></div></div><div><div><div>2</div></div><div><span>cp</span><span> </span><span>gdb_demo_nodebug</span><span> </span><span>gdb_demo_stripped</span></div></div><div><div><div>3</div></div><div><span>strip</span><span> </span><span>gdb_demo_stripped</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ls -l gdb_demo gdb_demo_nodebug gdb_demo_stripped</span></div></div><div><div><div>2</div></div><div><span>-rwxr-xr-x 1 bfmhno3 users 417648 Sep 30 00:00 gdb_demo</span></div></div><div><div><div>3</div></div><div><span>-rwxr-xr-x 1 bfmhno3 users  55760 Sep 30 00:18 gdb_demo_nodebug</span></div></div><div><div><div>4</div></div><div><span>-rwxr-xr-x 1 bfmhno3 users  35280 Sep 30 00:18 gdb_demo_stripped</span></div></div><div><div><div>5</div></div><div><span>$ nm gdb_demo_stripped</span></div></div><div><div><div>6</div></div><div><span>/etc/profiles/per-user/bfmhno3/bin/bash: line 1: nm: 未找到命令</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-g3</code> 版本是 417648 字节，不带调试信息的是 55760 字节，差了大约 7.5 倍，多出来的全是 DWARF。<code>strip</code> 又去掉了符号表，剩下 35280 字节。</p><p>没有 <code>-g</code> 时 GDB 依然能用，只是降级到符号级：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo_nodebug</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo_nodebug...</span></div></div><div><div><div>3</div></div><div><span>(No debugging symbols found in ./gdb_demo_nodebug)</span></div></div><div><div><div>4</div></div><div><span>(gdb) break main</span></div></div><div><div><div>5</div></div><div><span>Breakpoint 1 at 0x2396</span></div></div><div><div><div>6</div></div><div><span>(gdb) run</span></div></div><div><div><div>7</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo_nodebug</span></div></div><div><div><div>8</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>9</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Breakpoint 1, 0x0000555555556396 in main ()</span></div></div><div><div><div>12</div></div><div><span>(gdb) info locals</span></div></div><div><div><div>13</div></div><div><span>No symbol table info available.</span></div></div><div><div><div>14</div></div><div><span>(gdb) print total</span></div></div><div><div><div>15</div></div><div><span>No symbol "total" in current context.</span></div></div><div><div><div>16</div></div><div><span>(gdb) next</span></div></div><div><div><div>17</div></div><div><span>Single stepping until exit from function main,</span></div></div><div><div><div>18</div></div><div><span>which has no line number information.</span></div></div><div><div><div>19</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>20</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 166296)]</span></div></div><div><div><div>21</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 166296) exited]</span></div></div><div><div><div>22</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 166297)]</span></div></div><div><div><div>23</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 166297) exited]</span></div></div><div><div><div>24</div></div><div><span>counter=2000</span></div></div><div><div><div>25</div></div><div><span>__libc_start_call_main (main=main@entry=0x555555556384 &lt;main&gt;, argc=argc@entry=1, argv=argv@entry=0x7fffffffb7e8) at ../sysdeps/nptl/libc_start_call_main.h:74</span></div></div><div><div><div>26</div></div><div><span>warning: 74  ../sysdeps/nptl/libc_start_call_main.h: No such file or directory</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>Reading symbols</code> 后面跟着 <code>(No debugging symbols found in ...)</code>，这是第一个信号。<code>break main</code> 仍然成功，因为 <code>main</code> 在 ELF 符号表里，但输出只有地址，没有 <code>file ..., line ...</code>。停止行是 <code>0x0000555555556396 in main ()</code>：有地址，空括号，没有源码。局部变量、<code>total</code> 都不存在，<code>next</code> 找不到行号信息，只能一直运行到 <code>main</code> 返回。</p><p><code>strip</code> 过的版本连 <code>main</code> 这个名字都没有了：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo_stripped</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo_stripped...</span></div></div><div><div><div>3</div></div><div><span>(No debugging symbols found in ./gdb_demo_stripped)</span></div></div><div><div><div>4</div></div><div><span>(gdb) break main</span></div></div><div><div><div>5</div></div><div><span>Function "main" not defined.</span></div></div><div><div><div>6</div></div><div><span>Make breakpoint pending on future shared library load? (y or [n]) n</span></div></div><div><div><div>7</div></div><div><span>(gdb) info functions recursive</span></div></div><div><div><div>8</div></div><div><span>All functions matching regular expression "recursive":</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Function "main" not defined.</code> 之后 GDB 以为它可能在某个还没加载的共享库里，所以询问是否设置 pending 断点，我回答了 <code>n</code>。这时只能按地址下断点、读寄存器和反汇编，工作量会大很多。发布版本通常是 strip 过的，这也是为什么值得把调试符号单独保存下来，就像 Nixpkgs 对 glibc 做的那样。</p></section>
<section><h2>TUI：同一屏里看源码<a href="#tui同一屏里看源码"><span>#</span></a></h2><p><code>layout src</code> 把终端分成源码窗口和命令窗口。下面是我停在第 28 行、执行一次 <code>next</code> 之后的屏幕，窗口宽 100 列：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>┌─gdb_demo.cpp─────────────────────────────────────────────────────────────────────────────────────┐</span></div></div><div><div><div>2</div></div><div><span>│       22 }                                                                                       │</span></div></div><div><div><div>3</div></div><div><span>│       23                                                                                         │</span></div></div><div><div><div>4</div></div><div><span>│       24 int main(int argc, char* argv[]) {                                                      │</span></div></div><div><div><div>5</div></div><div><span>│       25     const std::string mode = argc &gt; 1 ? argv[1] : "";                                   │</span></div></div><div><div><div>6</div></div><div><span>│       26     std::vector&lt;int&gt; values{3, 5, 7, 9};                                                │</span></div></div><div><div><div>7</div></div><div><span>│       27     const int total = recursive_sum(values, 0);                                         │</span></div></div><div><div><div>8</div></div><div><span>│B+     28     const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);            │</span></div></div><div><div><div>9</div></div><div><span>│  &gt;    29     std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average                           │</span></div></div><div><div><div>10</div></div><div><span>│       30               &lt;&lt; ", expected_average=6\n";                                              │</span></div></div><div><div><div>11</div></div><div><span>│       31                                                                                         │</span></div></div><div><div><div>12</div></div><div><span>│       32     int counter = 0;                                                                    │</span></div></div><div><div><div>13</div></div><div><span>│       33     std::mutex mutex;                                                                   │</span></div></div><div><div><div>14</div></div><div><span>│       34     std::thread first(worker, std::ref(counter), std::ref(mutex));                      │</span></div></div><div><div><div>15</div></div><div><span>│       35     std::thread second(worker, std::ref(counter), std::ref(mutex));                     │</span></div></div><div><div><div>16</div></div><div><span>└──────────────────────────────────────────────────────────────────────────────────────────────────┘</span></div></div><div><div><div>17</div></div><div><span>multi-thre Thread 0x7ffff7e907 (src) In: main                              L29   PC: 0x55555555648d</span></div></div><div><div><div>18</div></div><div><span>(gdb) next</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>B+</code> 是断点标记：<code>B</code> 表示这个断点至少命中过一次（从没命中过是小写 <code>b</code>），<code>+</code> 表示它处于启用状态（禁用是 <code>-</code>）。</li>
<li><code>&gt;</code> 标出下一条将要执行的行。</li>
<li>底部状态栏依次是：目标类型（<code>multi-thre</code> 是被截断的 <code>multi-threaded process</code>）、当前线程、当前布局 <code>(src)</code>、当前函数、行号 <code>L29</code>，以及当前 pc。</li>
</ul><p><code>layout asm</code> 显示反汇编，<code>layout split</code> 同时显示源码和反汇编，<code>layout regs</code> 在上方加一个寄存器窗口。TUI 模式下方向键会滚动源码窗口，而不是翻命令历史，<code>focus cmd</code> 可以把焦点切回命令窗口；<code>Ctrl-x a</code> 退出 TUI。窗口布局只影响显示，不改变被调试程序。</p></section>
<section><h2>命令脚本与批处理<a href="#命令脚本与批处理"><span>#</span></a></h2><p>重复的调试步骤可以写进文件。我的 <code>debug.gdb</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>set pagination off</span></div></div><div><div><div>2</div></div><div><span>set print pretty on</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>break recursive_sum</span></div></div><div><div><div>5</div></div><div><span>commands</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>silent</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>printf "recursive_sum(index=%lu), values[index]=%d\n", index, index &lt; values.size() ? values[index] : -1</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>continue</span></div></div><div><div><div>9</div></div><div><span>end</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>break 29</span></div></div><div><div><div>12</div></div><div><span>commands</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>printf "total=%d average=%g\n", total, average</span></div></div><div><div><div>14</div></div><div><span>end</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>define btall</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>thread apply all bt</span></div></div><div><div><div>18</div></div><div><span>end</span></div></div><div><div><div>19</div></div><div><span>document btall</span></div></div><div><div><div>20</div></div><div><span>Print the backtrace of every thread.</span></div></div><div><div><div>21</div></div><div><span>end</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>run</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>commands ... end</code> 给最近定义的断点挂一段命令，每次命中时执行。第一段以 <code>silent</code> 开头，所以命中时不会打印平时那两行停止信息；最后的 <code>continue</code> 让它自动放行。</li>
<li><code>printf</code> 的用法和 C 一样，参数是 GDB 表达式。<code>index &lt; values.size() ? values[index] : -1</code> 避免在最后一层越界读取。</li>
<li>第二个断点的命令没有 <code>continue</code>，所以会停在那里等我。</li>
<li><code>define</code> 定义一个新命令，<code>document</code> 给它写帮助文本。</li>
</ul><p>用 <code>-x</code> 加载：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx -x debug.gdb ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>Breakpoint 1 at 0x22da: file gdb_demo.cpp, line 11.</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 2 at 0x248d: file gdb_demo.cpp, line 29.</span></div></div><div><div><div>5</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>6</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>7</div></div><div><span>recursive_sum(index=0), values[index]=3</span></div></div><div><div><div>8</div></div><div><span>recursive_sum(index=1), values[index]=5</span></div></div><div><div><div>9</div></div><div><span>recursive_sum(index=2), values[index]=7</span></div></div><div><div><div>10</div></div><div><span>recursive_sum(index=3), values[index]=9</span></div></div><div><div><div>11</div></div><div><span>recursive_sum(index=4), values[index]=-1</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>Breakpoint 2, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:29</span></div></div><div><div><div>14</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>15</div></div><div><span>total=24 average=8</span></div></div><div><div><div>16</div></div><div><span>(gdb) help btall</span></div></div><div><div><div>17</div></div><div><span>Print the backtrace of every thread.</span></div></div><div><div><div>18</div></div><div><span>(gdb) btall</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>Thread 1 (Thread 0x7ffff7e90780 (LWP 164565) "gdb_demo"):</span></div></div><div><div><div>21</div></div><div><span>#0  main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:29</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>5 次递归调用各打印一行，<code>index=4</code> 那一层是终止条件，打印 <code>-1</code>。然后停在第 29 行，执行了挂在上面的 <code>printf</code>。注意输出里没有 <code>Starting program:</code>，从脚本里执行 <code>run</code> 时 GDB 会省略这一行。我在交互提示符下再调用自定义命令 <code>help btall</code> 和 <code>btall</code>，它们和内置命令用起来没有区别。</p><p>通用设置可以放进 <code>~/.gdbinit</code>，但我会把项目相关的断点放在项目目录里的脚本里，用 <code>-x</code> 显式加载。GDB 默认不会自动执行当前目录下的 <code>.gdbinit</code>，这是出于安全考虑：一个恶意仓库可以借此在你的机器上运行任意命令。</p><p>在 CI 或者脚本里，<code>-batch</code> 让 GDB 执行完 <code>-ex</code> 命令后直接退出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx -batch -ex run -ex bt --args ./gdb_demo --crash 2&gt;&amp;1 | cat</span></div></div><div><div><div>2</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>3</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>4</div></div><div><span>[New Thread 0x7ffff77ff6c0 (LWP 169488)]</span></div></div><div><div><div>5</div></div><div><span>[Thread 0x7ffff77ff6c0 (LWP 169488) exited]</span></div></div><div><div><div>6</div></div><div><span>[New Thread 0x7ffff6ffe6c0 (LWP 169489)]</span></div></div><div><div><div>7</div></div><div><span>[Thread 0x7ffff6ffe6c0 (LWP 169489) exited]</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>Thread 1 "gdb_demo" received signal SIGSEGV, Segmentation fault.</span></div></div><div><div><div>10</div></div><div><span>0x000055555555677b in main (argc=2, argv=0x7fffffffb928) at gdb_demo.cpp:57</span></div></div><div><div><div>11</div></div><div><span>57          std::cout &lt;&lt; *pointer &lt;&lt; "\n";</span></div></div><div><div><div>12</div></div><div><span>#0  0x000055555555677b in main (argc=2, argv=0x7fffffffb928) at gdb_demo.cpp:57</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出里有栈回溯，但程序自己的那三行 <code>total=...</code>、<code>counter=2000</code>、<code>about to dereference ...</code> 全都不见了。和前面的 <code>| cat</code> 是同一个原因：程序的 <code>stdout</code> 接到了管道上，崩溃时缓冲区还没刷新。在批处理里抓崩溃现场时，程序输出不可信，要看的是 GDB 打印的栈。</p></section>
<section><h2>反向执行：谁写了这个值<a href="#反向执行谁写了这个值"><span>#</span></a></h2><p><code>record full</code> 会记录之后每一条指令对寄存器和内存的修改，于是可以倒着执行。我用它回答一个具体的问题：<code>average</code> 是在哪条指令被写成 <code>8</code> 的？</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break 27</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x2418: file gdb_demo.cpp, line 27.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:27</span></div></div><div><div><div>11</div></div><div><span>27      const int total = recursive_sum(values, 0);</span></div></div><div><div><div>12</div></div><div><span>(gdb) record full</span></div></div><div><div><div>13</div></div><div><span>(gdb) next</span></div></div><div><div><div>14</div></div><div><span>28      const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>15</div></div><div><span>(gdb) next</span></div></div><div><div><div>16</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>17</div></div><div><span>(gdb) print average</span></div></div><div><div><div>18</div></div><div><span>$1 = 8</span></div></div><div><div><div>19</div></div><div><span>(gdb) watch average</span></div></div><div><div><div>20</div></div><div><span>Hardware watchpoint 2: average</span></div></div><div><div><div>21</div></div><div><span>(gdb) reverse-continue</span></div></div><div><div><div>22</div></div><div><span>Continuing.</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>Hardware watchpoint 2: average</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>Old value = 8</span></div></div><div><div><div>27</div></div><div><span>New value = 6.9533558069247378e-310</span></div></div><div><div><div>28</div></div><div><span>0x0000555555556488 in main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:28</span></div></div><div><div><div>29</div></div><div><span>28      const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>30</div></div><div><span>(gdb) x/i $pc</span></div></div><div><div><div>31</div></div><div><span>=&gt; 0x555555556488 &lt;main(int, char**)+260&gt;:  movsd  %xmm1,-0x30(%rbp)</span></div></div><div><div><div>32</div></div><div><span>(gdb) info record</span></div></div><div><div><div>33</div></div><div><span>Active record target: record-full</span></div></div><div><div><div>34</div></div><div><span>Replay mode:</span></div></div><div><div><div>35</div></div><div><span>Lowest recorded instruction number is 1.</span></div></div><div><div><div>36</div></div><div><span>Current instruction number is 391.</span></div></div><div><div><div>37</div></div><div><span>Highest recorded instruction number is 392.</span></div></div><div><div><div>38</div></div><div><span>Log contains 392 instructions.</span></div></div><div><div><div>39</div></div><div><span>Max logged instructions is 200000.</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>record full</code> 本身没有输出，但从这里开始的每一步都会被记录。</li>
<li>两次 <code>next</code> 之后 <code>average</code> 是 <code>8</code>。在它上面设 watchpoint，然后 <code>reverse-continue</code>。</li>
<li>倒着执行时 <code>Old value</code> 和 <code>New value</code> 也是按倒退的方向说的：倒退之前是 <code>8</code>，撤销那次写入之后变回了栈上的残留值。</li>
<li>停止位置是 <code>0x0000555555556488</code>，<code>x/i $pc</code> 显示那条指令是 <code>movsd %xmm1,-0x30(%rbp)</code>：把 <code>xmm1</code> 里算好的双精度结果写到 <code>average</code> 在栈上的位置。</li>
<li><code>info record</code> 显示这两行源码一共记录了 392 条指令，当前停在第 391 条。<code>Replay mode</code> 表示此刻是在回放记录，而不是在真实执行。</li>
</ul><p><code>record full</code> 的代价是每条指令都要被 GDB 截获，而且它不认识所有系统调用。在第 34 行创建线程之前打开它：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q -nx ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>(gdb) break 34</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x2518: file gdb_demo.cpp, line 34.</span></div></div><div><div><div>5</div></div><div><span>(gdb) run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>8</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libthread_db.so.1".</span></div></div><div><div><div>9</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7f8) at gdb_demo.cpp:34</span></div></div><div><div><div>12</div></div><div><span>34      std::thread first(worker, std::ref(counter), std::ref(mutex));</span></div></div><div><div><div>13</div></div><div><span>(gdb) record full</span></div></div><div><div><div>14</div></div><div><span>(gdb) next</span></div></div><div><div><div>15</div></div><div><span>Process record and replay target doesn't support syscall number 435</span></div></div><div><div><div>16</div></div><div><span>Process record: failed to record execution log.</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>Program stopped.</span></div></div><div><div><div>19</div></div><div><span>__GI___clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:60</span></div></div><div><div><div>20</div></div><div><span>warning: 60  ../sysdeps/unix/sysv/linux/x86_64/clone3.S: No such file or directory</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>syscall number 435</code> 是 <code>clone3</code>，glibc 用它创建线程，而 GDB 17.2 的 process record 不支持它。所以在这个 demo 里，反向执行只能用在单线程的那一段。需要对多线程程序做可靠的录制回放时，我会去看 rr，它是专门为此设计的工具。</p></section>
<section><h2>修复<a href="#修复"><span>#</span></a></h2><p>回到第一个 bug。GDB 帮我确认了：<code>total</code> 是对的，<code>average</code> 在第 28 行被算错了，而 <code>total / values.size()</code> 能得到期望的 <code>6</code>。修复只需要改一行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>2</div></div><div><span>const double average = static_cast&lt;double&gt;(total) / values.size();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>重新编译并运行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ./gdb_demo_fixed</span></div></div><div><div><div>2</div></div><div><span>total=24, average=6, expected_average=6</span></div></div><div><div><div>3</div></div><div><span>counter=2000</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>另外两个错误路径是我故意留下的，<code>--crash</code> 的修复就是不要解引用空指针，<code>--throw</code> 的修复是不要访问 <code>values.at(values.size())</code>。</p></section>
<section><h2>常见输出速查<a href="#常见输出速查"><span>#</span></a></h2><p>最后把这一路遇到的输出整理成一张表，下次看到它们时可以直接对照：</p>

<table><thead><tr><th>输出</th><th>含义</th><th>下一步</th></tr></thead><tbody><tr><td><code>(No debugging symbols found in ...)</code></td><td>编译时没有 <code>-g</code></td><td>重新用 <code>-g3 -O0</code> 编译</td></tr><tr><td><code>Function "main" not defined.</code></td><td>可执行文件被 strip 过</td><td>找到对应的调试符号，或按地址下断点</td></tr><tr><td><code>Breakpoint 1 (xxx) pending.</code></td><td>符号还没加载，可能在共享库里</td><td>运行后用 <code>info breakpoints</code> 确认已解析</td></tr><tr><td><code>Breakpoint 1, main (...) at file:line</code></td><td>命中断点，显示的这一行尚未执行</td><td><code>next</code>、<code>step</code>、<code>print</code></td></tr><tr><td><code>0x... in func (...) at file:line</code></td><td>pc 在这一行的中间</td><td>正常，通常出现在返回或信号之后</td></tr><tr><td><code>0x... in func () from /path/lib.so</code></td><td>这一帧没有调试信息</td><td><code>frame N</code> 跳回自己的代码</td></tr><tr><td><code>$N = ...</code></td><td>值历史，可以用 <code>$N</code> 引用</td><td></td></tr><tr><td><code>x = @0x...: 5</code></td><td>引用，<code>@</code> 后是它指向的地址</td><td></td></tr><tr><td><code>mutex=...</code></td><td>帧参数被折叠</td><td><code>info args</code> 或 <code>print mutex</code></td></tr><tr><td><code>&lt;optimized out&gt;</code></td><td>优化后该位置无法恢复变量的值</td><td>用 <code>-O0</code> 复现，或读寄存器和反汇编</td></tr><tr><td><code>Cannot access memory at address 0x0</code></td><td>地址无效</td><td><code>x/i $pc</code>、<code>info registers</code></td></tr><tr><td><code>Cannot evaluate function -- may be inlined</code></td><td>函数被内联或根本没有生成</td><td>直接读成员或用 <code>@</code> 运算符</td></tr><tr><td><code>Watchpoint N deleted because the program has left the block</code></td><td>watchpoint 的作用域已结束</td><td>用 <code>watch -l</code> 监视地址</td></tr><tr><td><code>Old value = ... New value = ...</code></td><td>watchpoint 触发，停在写入之后</td><td><code>bt</code> 看是谁写的</td></tr><tr><td><code>[Switching to Thread ...]</code></td><td>当前线程被切换</td><td><code>info threads</code></td></tr><tr><td><code>Thread 2.1 ... Breakpoint 1.2</code></td><td>多 inferior 时的两级编号</td><td><code>info inferiors</code></td></tr><tr><td><code>(*): Shared library is missing debugging information.</code></td><td>共享库没有调试符号</td><td>NixOS 上设置 <code>NIX_DEBUG_INFO_DIRS</code></td></tr><tr><td><code>warning: 56	xxx.S: No such file or directory</code></td><td>有调试符号但没有源码</td><td>可以忽略，或用 <code>directory</code> 指定源码</td></tr><tr><td><code>Process record ... doesn't support syscall number N</code></td><td>录制遇到不支持的系统调用</td><td>缩小录制范围，或换用 rr</td></tr></tbody></table></section>
<section><h2>我以后会怎样使用 GDB<a href="#我以后会怎样使用-gdb"><span>#</span></a></h2><p>这次实验里，错误的平均值从输出里的 <code>8</code> 一路追到了第 28 行的一条 <code>movsd</code> 指令；两个线程把 <code>counter</code> 从 <code>0</code> 加到 <code>2000</code> 的过程，被一个 <code>watch -l</code> 拆成了 <code>1000 + 500 + 500</code>；空指针崩溃则被源码、指令和寄存器三层证据同时指认。真正改变我习惯的不是记住了多少命令，而是终于能读懂 GDB 在每一步说了什么：哪一行已经执行、哪一行还没执行，这个地址是谁的地址，那个 <code>...</code> 为什么是 <code>...</code>。</p><p>写这篇文章时让我最意外的是 <code>-O2</code> 那一节。我原本以为会看到满屏的 <code>&lt;optimized out&gt;</code>，结果编译器直接把整个递归算成了一个常数 <code>0x18</code>，函数一次都没有被调用。源码和机器指令之间的距离，已经比我脑子里的模型远得多。</p><p>再往前看，这个距离大概只会继续变长：编译器更激进，LTO 跨文件内联，异步运行时把一个函数切成好几个状态机片段，越来越多的代码也不是人一行一行写出来的。调试器也许会被接到更聪明的前端上，但我怀疑底层问题不会变：暂停一个进程，然后把寄存器和内存里的字节翻译回人能理解的名字和行号。能读懂这层翻译，就能在任何前端失灵的时候退回来自己看。</p><p>现在我可以把 <code>values.size() - 1</code> 改回来，重新编译，然后去吃点东西。GDB 不会替我修 bug，它只是非常耐心地证明我确实写错了。</p></section>
<section><h2>题外话：我平时用的 gdb-dashboard<a href="#题外话我平时用的-gdb-dashboard"><span>#</span></a></h2><p>前面所有会话我都加了 <code>-nx</code>，因为我想让你看到 GDB 最原始的输出。但我日常调试时并不这样用。我的 <code>~/.gdbinit</code> 由 home-manager 生成，里面加载了 <a href="https://github.com/cyrus-and/gdb-dashboard" target="_blank">gdb-dashboard</a>：每次程序停下，它都会在终端里重画一整屏状态，源码、汇编、寄存器、调用栈、线程、变量一次看全。</p><a href="https://github.com/cyrus-and/gdb-dashboard" target="_blank"><div><div><div><div></div><div>cyrus-and</div></div><div>/</div><div>gdb-dashboard</div></div><div></div></div><div>Modular visual interface for GDB in Python</div><div><div>—</div><div>—</div><div>MIT</div><span>Python</span></div></a><section><h3>配置<a href="#配置"><span>#</span></a></h3><p>这是我 NixOS 配置里的 <code>home/dev/gdb.nix</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>config</span><span>,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>lib</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>pkgs</span><span>,</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>...</span></div></div><div><div><div>6</div></div><div><span>}:</span></div></div><div><div><div>7</div></div><div><span>let</span></div></div><div><div><div>8</div></div><div><span>  </span><span>cfg</span><span><span> = </span><span>config</span><span>.</span><span>myHome</span><span>.</span><span>dev</span><span>.</span><span>gdb</span><span>;</span></span></div></div><div><div><div>9</div></div><div><span>in</span></div></div><div><div><div>10</div></div><div><span>{</span></div></div><div><div><div>11</div></div><div><span>  </span><span>options</span><span>.</span><span>myHome</span><span>.</span><span>dev</span><span>.</span><span>gdb</span><span>.</span><span>enable</span><span><span> = </span><span>lib</span><span>.</span><span>mkEnableOption</span><span> </span></span><span>"GDB configuration"</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>config</span><span><span> = </span><span>lib</span><span>.</span><span>mkIf</span><span> </span><span>cfg</span><span>.</span><span>enable</span><span> {</span></span></div></div><div><div><div>13</div></div><div><span>    </span><span>home</span><span>.</span><span>packages</span><span><span> = [ </span><span>pkgs</span><span>.</span><span>gdb</span><span> ];</span></span></div></div><div><div><div>14</div></div><div><span>    </span><span>home</span><span>.</span><span>file</span><span>.</span><span>".gdbinit"</span><span>.</span><span>text</span><span> = </span><span>''</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>source </span><span>${</span><span><span>pkgs</span><span>.</span><span>gdb-dashboard</span></span><span>}</span><span>/share/gdb-dashboard/gdbinit</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>set pagination off</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>set print pretty on</span></div></div><div><div><div>18</div></div><div><span><span>      </span></span><span>set confirm off</span></div></div><div><div><div>19</div></div><div><span><span>      </span></span><span>set history save on</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>set history size 1000</span></div></div><div><div><div>21</div></div><div><span><span>      </span></span><span>set history filename ~/.gdb_history</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>''</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>home.file.".gdbinit".text</code> 让 home-manager 把这段文本写成 <code>~/.gdbinit</code>，第一行 <code>source</code> 加载 Nixpkgs 打包的 gdb-dashboard 0.17.4，它是一个 2388 行的 gdbinit 文件，主体是一大段 Python。后面几行是我自己的偏好：</p><ul>
<li><code>set pagination off</code>：关掉分页。前面 <code>-O2</code> 那节里 <code>disassemble /s main</code> 输出太长，GDB 停下来问 <code>--Type &lt;RET&gt; for more, q to quit, c to continue without paging--</code>，就是分页在起作用。</li>
<li><code>set print pretty on</code>：结构体按缩进多行打印，效果和前面 <code>print mutex</code> 那节一样。</li>
<li><code>set confirm off</code>：不再确认。前面 <code>quit</code>、<code>return 100</code>、<code>delete</code> 时的 <code>(y or n)</code> 提问都会消失，GDB 直接照做。这很省事，也意味着手滑的 <code>delete</code> 会一次删光所有断点。</li>
<li><code>set history save on</code> 和后两行：把命令历史存进 <code>~/.gdb_history</code>，下次启动 GDB 还能用上方向键翻到上一次的命令。</li>
</ul></section><section><h3>在本文的 flake 里它坏了<a href="#在本文的-flake-里它坏了"><span>#</span></a></h3><p>我把这套配置带进本文的 flake shell，第一次 <code>run</code> 就得到了一屏 Python 异常：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>&gt;&gt;&gt; break 29</span></div></div><div><div><div>4</div></div><div><span>Breakpoint 1 at 0x248d: file gdb_demo.cpp, line 29.</span></div></div><div><div><div>5</div></div><div><span>&gt;&gt;&gt; run</span></div></div><div><div><div>6</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>7</div></div><div><span>Traceback (most recent call last):</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>File "&lt;string&gt;", line 430, in on_continue</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>File "&lt;string&gt;", line 616, in get_term_size</span></div></div><div><div><div>10</div></div><div><span>SystemError: buffer overflow</span></div></div><div><div><div>11</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>12</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libth</span></div></div><div><div><div>13</div></div><div><span>read_db.so.1".</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb808) at gdb_demo.cpp:29</span></div></div><div><div><div>16</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>17</div></div><div><span>Cannot write the dashboard</span></div></div><div><div><div>18</div></div><div><span>Traceback (most recent call last):</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>File "&lt;string&gt;", line 522, in render</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>File "&lt;string&gt;", line 616, in get_term_size</span></div></div><div><div><div>21</div></div><div><span>SystemError: buffer overflow</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>During handling of the above exception, another exception occurred:</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>Traceback (most recent call last):</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>File "&lt;string&gt;", line 524, in render</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>File "&lt;string&gt;", line 616, in get_term_size</span></div></div><div><div><div>28</div></div><div><span>SystemError: buffer overflow</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>get_term_size</code> 是 dashboard 读取终端宽高的函数，它失败之后整个 dashboard 都画不出来（<code>Cannot write the dashboard</code>），只剩 GDB 原本的输出。奇怪的是，同一个 <code>~/.gdbinit</code> 在我平时的系统 gdb 里一直正常。两个 gdb 都是 17.2，区别在嵌入的 Python 版本上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ /etc/profiles/per-user/bfmhno3/bin/gdb -nx -batch -ex 'python import sys; print(sys.version)'</span></div></div><div><div><div>2</div></div><div><span>3.13.15 (main, Aug  5 2026, 12:25:43) [GCC 15.3.0]</span></div></div><div><div><div>3</div></div><div><span>$ /etc/profiles/per-user/bfmhno3/bin/gdb -nx -batch -ex 'python import fcntl, termios, struct; print(struct.unpack("hh", fcntl.ioctl(0, termios.TIOCGWINSZ, " " * 4)))'</span></div></div><div><div><div>4</div></div><div><span>(40, 120)</span></div></div><div><div><div>5</div></div><div><span>$ nix develop -c gdb -nx -batch -ex 'python import sys; print(sys.version)' 2&gt;/dev/null | tail -n 1</span></div></div><div><div><div>6</div></div><div><span>3.14.7 (main, Aug  5 2026, 10:29:49) [GCC 15.3.0]</span></div></div><div><div><div>7</div></div><div><span>$ nix develop -c gdb -nx -batch -ex 'python import fcntl, termios, struct; print(struct.unpack("hh", fcntl.ioctl(0, termios.TIOCGWINSZ, " " * 4)))' 2&gt;&amp;1 | tail -n 2</span></div></div><div><div><div>8</div></div><div><span>Python Exception &lt;class 'SystemError'&gt;: buffer overflow</span></div></div><div><div><div>9</div></div><div><span>Error occurred in Python: buffer overflow</span></div></div><div><div><div>10</div></div><div><span>$ nix develop -c gdb -nx -batch -ex 'python import fcntl, termios, struct; print(struct.unpack("hhhh", fcntl.ioctl(0, termios.TIOCGWINSZ, bytes(8))))' 2&gt;&amp;1 | tail -n 1</span></div></div><div><div><div>11</div></div><div><span>(40, 120, 1920, 1280)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我的系统配置还停在 Python 3.13，本文 flake 锁定的 nixpkgs 里 gdb 已经换成了 Python 3.14。dashboard 调用 <code>fcntl.ioctl(fd, termios.TIOCGWINSZ, ' ' * 4)</code> 时只准备了 4 个字节的缓冲区，但内核返回的 <code>struct winsize</code> 是 4 个 <code>unsigned short</code>，一共 8 字节：行数、列数，再加上两个像素尺寸（上面的 <code>1920, 1280</code> 就是 tmux 报告的像素宽高）。在 Python 3.13 里这 4 个多出来的字节会被悄悄丢掉；从我的测试看，Python 3.14 改成了直接抛出 <code>SystemError: buffer overflow</code>。缓冲区换成 8 字节就恢复正常。</p><p>修复不需要改 dashboard 本身。dashboard 启动时会按顺序遍历几个配置目录，其中之一是 <code>$XDG_CONFIG_HOME/gdb-dashboard</code>（默认是 <code>~/.config/gdb-dashboard</code>），<code>.py</code> 文件会先于普通 GDB 命令文件加载，而且和 dashboard 运行在同一个 Python 命名空间里。所以我放一个很小的补丁文件 <code>~/.config/gdb-dashboard/00-term-size.py</code>，把 <code>Dashboard.get_term_size</code> 换掉：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># gdb-dashboard 0.17.4 只给 TIOCGWINSZ 传了 4 字节缓冲区，</span></div></div><div><div><div>2</div></div><div><span># Python 3.14 会因为内核写回 8 字节的 struct winsize 而抛出 SystemError。</span></div></div><div><div><div>3</div></div><div><span>import</span><span> fcntl</span></div></div><div><div><div>4</div></div><div><span>import</span><span> struct</span></div></div><div><div><div>5</div></div><div><span>import</span><span> termios</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>def</span><span> </span><span>_get_term_size</span><span>(</span><span>fd</span><span>=</span><span>1</span><span>):</span></div></div><div><div><div>9</div></div><div><span>    </span><span>try</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>raw </span><span>=</span><span> fcntl.</span><span>ioctl</span><span>(fd, termios.</span><span>TIOCGWINSZ</span><span>, </span><span>bytes</span><span>(</span><span>8</span><span>))</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>height, width, _, _ </span><span>=</span><span> struct.</span><span>unpack</span><span>(</span><span>"hhhh"</span><span>, raw)</span></div></div><div><div><div>12</div></div><div><span>        </span><span>return</span><span> </span><span>int</span><span>(width), </span><span>int</span><span>(height)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>except</span><span> </span><span>OSError</span><span>:</span></div></div><div><div><div>14</div></div><div><span>        </span><span>return</span><span> </span><span>80</span><span>, </span><span>24</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>Dashboard.get_term_size </span><span>=</span><span> </span></span><span>staticmethod</span><span>(_get_term_size)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>在 home-manager 里，可以把这个文件保存成 <code>gdb.nix</code> 旁边的 <code>gdb-dashboard-term-size.py</code>，然后在上面的 <code>config = lib.mkIf cfg.enable { ... }</code> 里加上一项：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>xdg</span><span>.</span><span>configFile</span><span>.</span></span><span>"gdb-dashboard/00-term-size.py"</span><span><span>.</span><span>source</span><span> </span></span><span>=</span><span> </span><span>./gdb-dashboard-term-size.py</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我是用 <code>XDG_CONFIG_HOME</code> 指向一个临时目录来验证这个补丁的，下面的截图和输出都在打了补丁的 flake shell 里生成。home-manager 那一行我还没有在自己的配置里 rebuild 过，写法是 home-manager 的标准 <code>xdg.configFile</code> 选项。</p></section><section><h3>一屏里有什么<a href="#一屏里有什么"><span>#</span></a></h3><p>补丁生效后，同样停在第 29 行，终端变成了这样（100 列宽的终端，颜色是 dashboard 的默认配色）：</p><p></p><figure><img src="/assets/images/gdb-dashboard-overview.webp" alt="gdb-dashboard 停在 gdb_demo.cpp 第 29 行时的完整界面，从上到下依次是 Output/messages、Assembly、Breakpoints、Expressions、History、Memory、Registers、Source、Stack、Threads 和 Variables 模块" /><figcaption>gdb-dashboard 停在 gdb_demo.cpp 第 29 行时的完整界面，从上到下依次是 Output/messages、Assembly、Breakpoints、Expressions、History、Memory、Registers、Source、Stack、Threads 和 Variables 模块</figcaption></figure><p></p><p>纯文本版本如下，方便复制对照：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>─── Output/messages ────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>2</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>3</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libth</span></div></div><div><div><div>4</div></div><div><span>read_db.so.1".</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7e8) at gdb_demo.cpp:29</span></div></div><div><div><div>7</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>8</div></div><div><span>─── Assembly ───────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>0x0000555555556473  main(int, char**)+239 cvtsi2sd %rdx,%xmm0</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>0x0000555555556478  main(int, char**)+244 addsd  %xmm0,%xmm0</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>0x000055555555647c  main(int, char**)+248 movsd  -0x118(%rbp),%xmm1</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>0x0000555555556484  main(int, char**)+256 divsd  %xmm0,%xmm1</span></div></div><div><div><div>13</div></div><div><span><span> </span></span><span>0x0000555555556488  main(int, char**)+260 movsd  %xmm1,-0x30(%rbp)</span></div></div><div><div><div>14</div></div><div><span>!0x000055555555648d  main(int, char**)+265 lea    0x2bae(%rip),%rdx        # 0x555555559042</span></div></div><div><div><div>15</div></div><div><span><span> </span></span><span>0x0000555555556494  main(int, char**)+272 lea    0x5c65(%rip),%rax        # 0x55555555c100 &lt;_ZSt4co</span></div></div><div><div><div>16</div></div><div><span>ut@GLIBCXX_3.4&gt;</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>0x000055555555649b  main(int, char**)+279 mov    %rdx,%rsi</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>0x000055555555649e  main(int, char**)+282 mov    %rax,%rdi</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>0x00005555555564a1  main(int, char**)+285 call   0x555555556130 &lt;_ZStlsISt11char_traitsIcEERSt13bas</span></div></div><div><div><div>20</div></div><div><span>ic_ostreamIcT_ES5_PKc@plt&gt;</span></div></div><div><div><div>21</div></div><div><span>─── Breakpoints ────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>22</div></div><div><span>[1] break at 0x000055555555648d in gdb_demo.cpp:29 for gdb_demo.cpp:29 hit 1 time</span></div></div><div><div><div>23</div></div><div><span>─── Expressions ────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>24</div></div><div><span>─── History ────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>25</div></div><div><span>─── Memory ─────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>26</div></div><div><span>─── Registers ──────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>27</div></div><div><span><span>          </span></span><span>rax 0x0000000000000003          rbx 0x0000000000000000         rcx 0x0000555555559380</span></div></div><div><div><div>28</div></div><div><span><span>          </span></span><span>rdx 0x0000000000000010          rsi 0x0000000000000004         rdi 0x00007fffffffb600</span></div></div><div><div><div>29</div></div><div><span><span>          </span></span><span>rbp 0x00007fffffffb6c0          rsp 0x00007fffffffb5a0          r8 0x0000000000000000</span></div></div><div><div><div>30</div></div><div><span><span>           </span></span><span>r9 0x0000000000000000          r10 0x0000000000000000         r11 0x00007ffff797f0c0</span></div></div><div><div><div>31</div></div><div><span><span>          </span></span><span>r12 0x0000555555559380          r13 0x0000000000000004         r14 0x00007ffff7ffd000</span></div></div><div><div><div>32</div></div><div><span><span>          </span></span><span>r15 0x000055555555bd20          rip 0x000055555555648d      eflags [ PF IF ]</span></div></div><div><div><div>33</div></div><div><span><span>           </span></span><span>cs 0x00000033                   ss 0x0000002b                  ds 0x00000000</span></div></div><div><div><div>34</div></div><div><span><span>           </span></span><span>es 0x00000000                   fs 0x00000000                  gs 0x00000000</span></div></div><div><div><div>35</div></div><div><span><span>      </span></span><span>fs_base 0x00007ffff7e90780      gs_base 0x0000000000000000</span></div></div><div><div><div>36</div></div><div><span>─── Source ─────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>37</div></div><div><span><span> </span></span><span>24  int main(int argc, char* argv[]) {</span></div></div><div><div><div>38</div></div><div><span><span> </span></span><span>25      const std::string mode = argc &gt; 1 ? argv[1] : "";</span></div></div><div><div><div>39</div></div><div><span><span> </span></span><span>26      std::vector&lt;int&gt; values{3, 5, 7, 9};</span></div></div><div><div><div>40</div></div><div><span><span> </span></span><span>27      const int total = recursive_sum(values, 0);</span></div></div><div><div><div>41</div></div><div><span><span> </span></span><span>28      const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>42</div></div><div><span>!29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>43</div></div><div><span><span> </span></span><span>30                &lt;&lt; ", expected_average=6\n";</span></div></div><div><div><div>44</div></div><div><span><span> </span></span><span>31</span></div></div><div><div><div>45</div></div><div><span><span> </span></span><span>32      int counter = 0;</span></div></div><div><div><div>46</div></div><div><span><span> </span></span><span>33      std::mutex mutex;</span></div></div><div><div><div>47</div></div><div><span>─── Stack ──────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>48</div></div><div><span>[0] from 0x000055555555648d in main(int, char**)+265 at gdb_demo.cpp:29</span></div></div><div><div><div>49</div></div><div><span>─── Threads ────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>50</div></div><div><span>[1] id 207278 name gdb_demo from 0x000055555555648d in main(int, char**)+265 at gdb_demo.cpp:29</span></div></div><div><div><div>51</div></div><div><span>─── Variables ──────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>52</div></div><div><span>arg argc = 1, argv = 0x7fffffffb7e8: 47 '/'</span></div></div><div><div><div>53</div></div><div><span>loc mode = "", values = std::vector of length 4, capacity 4 = {[0] = 3, [1] = 5, [2] = 7, [3] = 9},</span></div></div><div><div><div>54</div></div><div><span>total = 24, average = 8, counter = -1, mutex = {&lt;std::__mutex_base&gt; = {_M_mutex = {__data = {__lock</span></div></div><div><div><div>55</div></div><div><span>= 1128415552,__count = 1195787588,__owner = 143…, first = {_M_id = {_M_thread = 140737346454874}}, s</span></div></div><div><div><div>56</div></div><div><span>econd = {_M_id = {_M_thread = 140737488336448}}</span></div></div><div><div><div>57</div></div><div><span>────────────────────────────────────────────────────────────────────────────────────────────────────</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>有了前面的铺垫，这一屏里几乎每一块都能对应到一个已经读过的命令：</p><ul>
<li><code>Output/messages</code>：每次继续执行前，dashboard 会清屏并画出这个分隔线，之后 GDB 自己的输出和程序的输出都落在这里。<code>Breakpoint 1, main (...) at gdb_demo.cpp:29</code> 和之前的格式完全一样。</li>
<li><code>Assembly</code>：相当于在 pc 附近自动执行 <code>x/10i</code>。行首的红色 <code>!</code> 表示这条指令上有一个启用的断点，绿色高亮的是当前 pc。紧挨在当前指令上面的那条，就是前面反向执行那一节找到的 <code>movsd %xmm1,-0x30(%rbp)</code>，<code>average</code> 就是在那里被写成 <code>8</code> 的。</li>
<li><code>Breakpoints</code>：精简版的 <code>info breakpoints</code>，<code>hit 1 time</code> 是命中次数。</li>
<li><code>Expressions</code>、<code>History</code>、<code>Memory</code>：默认是空的，需要自己添加监视项，后面会用到。</li>
<li><code>Registers</code>：<code>info registers</code> 的紧凑版，三列排列。单步之后值发生变化的寄存器会被高亮，这是我最喜欢的功能之一：一眼就能看出上一条指令改了哪个寄存器。</li>
<li><code>Source</code>：自动执行的 <code>list</code>，<code>!</code> 依然是断点，当前行用绿色标出。</li>
<li><code>Stack</code>：<code>bt</code> 的精简版。<code>[0] from 0x000055555555648d in main(int, char**)+265 at gdb_demo.cpp:29</code> 里，<code>+265</code> 和 <code>x/i</code> 输出里的 <code>&lt;main(int, char**)+265&gt;</code> 是同一个意思。</li>
<li><code>Threads</code>：<code>info threads</code>。<code>id 207278</code> 是 LWP，不是 GDB 线程编号，GDB 编号在方括号里。</li>
<li><code>Variables</code>：<code>info args</code> 加 <code>info locals</code>，<code>arg</code> 前缀是参数，<code>loc</code> 前缀是局部变量。<code>argv = 0x7fffffffb7e8: 47 '/'</code> 是 dashboard 顺手对指针做了一次解引用：<code>argv</code> 指向的第一个字节是 <code>'/'</code>，ASCII 码 47，也就是 <code>argv[0]</code> 字符串 <code>/tmp/gdb-post/gdb_demo</code> 的第一个字符。太长的值会用 <code>…</code> 截断，所以 <code>mutex</code> 那一串残留值没显示完。</li>
</ul><p><code>next</code> 两次之后，其他模块在原地刷新，新内容只出现在 <code>Output/messages</code> 里（下面只截取了这一块）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>─── Output/messages ────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>2</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>3</div></div><div><span>32      int counter = 0;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>程序的输出 <code>total=24, average=8, expected_average=6</code> 和 GDB 打印的 <code>32	    int counter = 0;</code> 挤在一起，这是一次 <code>next</code> 期间全部的新输出。</p></section><section><h3>定制布局<a href="#定制布局"><span>#</span></a></h3><p>默认 11 个模块一屏放不下，小终端上源码会被挤到很下面。我通常只留几个，接着上面停在第 29 行的会话输入：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&gt;&gt;&gt; dashboard -layout source variables expressions memory</span></div></div><div><div><div>2</div></div><div><span>&gt;&gt;&gt; dashboard source -style height 8</span></div></div><div><div><div>3</div></div><div><span>&gt;&gt;&gt; dashboard variables -style compact False</span></div></div><div><div><div>4</div></div><div><span>&gt;&gt;&gt; dashboard expressions watch (double) total / values.size()</span></div></div><div><div><div>5</div></div><div><span>&gt;&gt;&gt; dashboard memory watch &amp;values[0] 16</span></div></div><div><div><div>6</div></div><div><span>&gt;&gt;&gt; next</span></div></div><div><div><div>7</div></div><div><span>&gt;&gt;&gt; next</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一次 <code>next</code> 之后，屏幕上是这样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>─── Output/messages ────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>2</div></div><div><span>total=24, average=8, expected_average=6</span></div></div><div><div><div>3</div></div><div><span>32      int counter = 0;</span></div></div><div><div><div>4</div></div><div><span>─── Source ─────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>28      const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>6</div></div><div><span>!29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>30                &lt;&lt; ", expected_average=6\n";</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>31</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>32      int counter = 0;</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>33      std::mutex mutex;</span></div></div><div><div><div>11</div></div><div><span><span> </span></span><span>34      std::thread first(worker, std::ref(counter), std::ref(mutex));</span></div></div><div><div><div>12</div></div><div><span><span> </span></span><span>35      std::thread second(worker, std::ref(counter), std::ref(mutex));</span></div></div><div><div><div>13</div></div><div><span>─── Variables ──────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>14</div></div><div><span>arg argc = 1</span></div></div><div><div><div>15</div></div><div><span>arg argv = 0x7fffffffb7e8: 47 '/'</span></div></div><div><div><div>16</div></div><div><span>loc mode = ""</span></div></div><div><div><div>17</div></div><div><span>loc values = std::vector of length 4, capacity 4 = {[0] = 3, [1] = 5, [2] = 7, [3] = 9}</span></div></div><div><div><div>18</div></div><div><span>loc total = 24</span></div></div><div><div><div>19</div></div><div><span>loc average = 8</span></div></div><div><div><div>20</div></div><div><span>loc counter = -1</span></div></div><div><div><div>21</div></div><div><span>loc mutex = {&lt;std::__mutex_base&gt; = {_M_mutex = {__data = {__lock = 1128415552,__count = 1195787588,_</span></div></div><div><div><div>22</div></div><div><span>_owner = 143…</span></div></div><div><div><div>23</div></div><div><span>loc first = {_M_id = {_M_thread = 140737346454874}}</span></div></div><div><div><div>24</div></div><div><span>loc second = {_M_id = {_M_thread = 140737488336448}}</span></div></div><div><div><div>25</div></div><div><span>─── Expressions ────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>26</div></div><div><span>[1] (double) total / values.size() = 6</span></div></div><div><div><div>27</div></div><div><span>─── Memory ─────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>28</div></div><div><span>─── &amp;values[0] ─────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>29</div></div><div><span>0x000055555556f320  03 00 00 00 05 00 00 00 07 00 00 00 09 00 00 00  ················</span></div></div><div><div><div>30</div></div><div><span>────────────────────────────────────────────────────────────────────────────────────────────────────</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>dashboard -layout</code> 后面跟模块名，只显示这些模块，顺序也按这里排。</li>
<li><code>dashboard source -style height 8</code> 把源码窗口限制为 8 行。每个模块都有自己的一组 style。</li>
<li><code>dashboard variables -style compact False</code> 让每个变量独占一行，而不是挤成一段。</li>
<li><code>dashboard expressions watch ...</code> 添加一个监视表达式，每次停下都重新求值，效果类似前面的 <code>display</code>，但它固定显示在自己的面板里。我把正确公式 <code>(double) total / values.size()</code> 放了进去，它一直显示 <code>6</code>，而旁边的 <code>average</code> 是 <code>8</code>，这个 bug 就摆在屏幕上。</li>
<li><code>dashboard memory watch &amp;values[0] 16</code> 监视从 <code>&amp;values[0]</code> 开始的 16 个字节，格式和前面的 <code>x/16xb</code> 一样，右边还附带了 ASCII 列，不可打印字符显示成 <code>·</code>。被修改的字节会高亮。</li>
<li>单独执行 <code>dashboard</code> 会立即重画一次。</li>
</ul><p>调好之后，<code>dashboard -configuration</code> 会把当前布局和样式导出成 GDB 命令：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&gt;&gt;&gt; dashboard -configuration /tmp/gdb-post/dash-config</span></div></div><div><div><div>2</div></div><div><span>dashboard -layout source variables expressions memory !assembly !breakpoints !history !registers !st</span></div></div><div><div><div>3</div></div><div><span>ack !threads</span></div></div><div><div><div>4</div></div><div><span>dashboard source -style height 8</span></div></div><div><div><div>5</div></div><div><span>dashboard variables -style compact False</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前面带 <code>!</code> 的模块是被关掉的。把这几行存进 <code>~/.config/gdb-dashboard/</code> 里的某个文件（不带 <code>.py</code> 后缀，按普通 GDB 命令加载），下次启动就是这个布局。监视表达式不会被导出，因为它们通常和具体程序绑定，我会把它们写进项目自己的 <code>debug.gdb</code>。</p><p>我还试过 <code>dashboard -style ansi False</code> 关掉颜色，想让复制出来的文本带上 <code>&gt;</code> 这样的纯文本标记。结果在 0.17.4 里，这个模式下 <code>Variables</code> 模块直接抛出 <code>TypeError: unsupported operand type(s) for +: 'gdb.Symbol' and 'str'</code>，<code>Breakpoints</code> 还把一个启用的断点标成了 <code>disabled</code>。读源码看，两处都是 <code>ansi=False</code> 分支里的小 bug，所以我一直用默认的彩色模式。</p></section><section><h3>把 dashboard 放到另一个终端<a href="#把-dashboard-放到另一个终端"><span>#</span></a></h3><p>dashboard 最舒服的用法是把它输出到另一个终端窗口，GDB 的命令行就能保持干净。我在 tmux 里左右分屏，右边的 pane 只运行一个 <code>sleep</code>，让它的终端空着，然后在左边的 GDB 里把 dashboard 指过去。右边 pane 的终端设备可以用 tmux 的 <code>#{pane_tty}</code> 格式变量查到，我这里是 <code>/dev/pts/4</code>。然后在左边：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ gdb -q ./gdb_demo</span></div></div><div><div><div>2</div></div><div><span>Reading symbols from ./gdb_demo...</span></div></div><div><div><div>3</div></div><div><span>&gt;&gt;&gt; dashboard -output /dev/pts/4</span></div></div><div><div><div>4</div></div><div><span>&gt;&gt;&gt; dashboard -layout source variables stack</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>Dashboard    /dev/pts/4</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span> </span></span><span>source       (default TTY)</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>variables    (default TTY)</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>stack        (default TTY)</span></div></div><div><div><div>10</div></div><div><span>!assembly     (default TTY)</span></div></div><div><div><div>11</div></div><div><span>!breakpoints  (default TTY)</span></div></div><div><div><div>12</div></div><div><span>!expressions  (default TTY)</span></div></div><div><div><div>13</div></div><div><span>!history      (default TTY)</span></div></div><div><div><div>14</div></div><div><span>!memory       (default TTY)</span></div></div><div><div><div>15</div></div><div><span>!registers    (default TTY)</span></div></div><div><div><div>16</div></div><div><span>!threads      (default TTY)</span></div></div><div><div><div>17</div></div><div><span>&gt;&gt;&gt; dashboard source -style height 8</span></div></div><div><div><div>18</div></div><div><span>&gt;&gt;&gt; dashboard variables -style compact False</span></div></div><div><div><div>19</div></div><div><span>&gt;&gt;&gt; break 29</span></div></div><div><div><div>20</div></div><div><span>Breakpoint 1 at 0x248d: file gdb_demo.cpp, line 29.</span></div></div><div><div><div>21</div></div><div><span>&gt;&gt;&gt; run</span></div></div><div><div><div>22</div></div><div><span>Starting program: /tmp/gdb-post/gdb_demo</span></div></div><div><div><div>23</div></div><div><span>[Thread debugging using libthread_db enabled]</span></div></div><div><div><div>24</div></div><div><span>Using host libthread_db library "/nix/store/lm3pknxi0ipypy3lxh1wmm8wvvavdwrn-glibc-2.42-84/lib/libth</span></div></div><div><div><div>25</div></div><div><span>read_db.so.1".</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>Breakpoint 1, main (argc=1, argv=0x7fffffffb7e8) at gdb_demo.cpp:29</span></div></div><div><div><div>28</div></div><div><span>29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>29</div></div><div><span>&gt;&gt;&gt; next</span></div></div><div><div><div>30</div></div><div><span>30                &lt;&lt; ", expected_average=6\n";</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>右边的 pane 里是这样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>─── Source ─────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>26      std::vector&lt;int&gt; values{3, 5, 7, 9};</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>27      const int total = recursive_sum(values, 0);</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>28      const double average = static_cast&lt;double&gt;(total) / (values.size() - 1);</span></div></div><div><div><div>5</div></div><div><span>!29      std::cout &lt;&lt; "total=" &lt;&lt; total &lt;&lt; ", average=" &lt;&lt; average</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>30                &lt;&lt; ", expected_average=6\n";</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>31</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>32      int counter = 0;</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>33      std::mutex mutex;</span></div></div><div><div><div>10</div></div><div><span>─── Variables ──────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>11</div></div><div><span>arg argc = 1</span></div></div><div><div><div>12</div></div><div><span>arg argv = 0x7fffffffb7e8: 47 '/'</span></div></div><div><div><div>13</div></div><div><span>loc mode = ""</span></div></div><div><div><div>14</div></div><div><span>loc values = std::vector of length 4, capacity 4 = {[0] = 3, [1] = 5, [2] = 7, [3] = 9}</span></div></div><div><div><div>15</div></div><div><span>loc total = 24</span></div></div><div><div><div>16</div></div><div><span>loc average = 8</span></div></div><div><div><div>17</div></div><div><span>loc counter = -1</span></div></div><div><div><div>18</div></div><div><span>loc mutex = {&lt;std::__mutex_base&gt; = {_M_mutex = {__data = {__lock = 1128415552,__count = 1195787588,_</span></div></div><div><div><div>19</div></div><div><span>_owner = 143…</span></div></div><div><div><div>20</div></div><div><span>loc first = {_M_id = {_M_thread = 140737346454874}}</span></div></div><div><div><div>21</div></div><div><span>loc second = {_M_id = {_M_thread = 140737488336448}}</span></div></div><div><div><div>22</div></div><div><span>─── Stack ──────────────────────────────────────────────────────────────────────────────────────────</span></div></div><div><div><div>23</div></div><div><span>[0] from 0x00005555555564e2 in main(int, char**)+350 at gdb_demo.cpp:30</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>dashboard -output /dev/pts/4</code> 把整个 dashboard 写到那个终端，<code>dashboard -layout</code> 的输出第一行 <code>Dashboard    /dev/pts/4</code> 确认了这一点。左边的 GDB 会话看起来和前面 <code>-nx</code> 的会话几乎一样，只是提示符变成了 <code>&gt;&gt;&gt;</code>；每次停下，右边原地刷新。单个模块也可以分别输出，比如 <code>dashboard source -output /dev/pts/5</code>，把源码放到第三个窗口。</p><p>说到底，dashboard 并没有给 GDB 增加新的能力。它只是在每次停下时替我执行了一遍 <code>x/i</code>、<code>info registers</code>、<code>list</code>、<code>bt</code>、<code>info threads</code>、<code>info locals</code>，再加上几个 <code>display</code>，然后把结果排好版。正因为前面把这些命令的原始输出一行行读过，这一屏信息才不是噪声：我知道 <code>+265</code> 是什么，知道为什么 <code>argv</code> 后面跟着一个 <code>'/'</code>，也知道当 dashboard 自己坏掉时，退回 <code>gdb -nx</code> 依然能把事情做完。</p></section></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><p>下面是我写这篇文章时实际查阅过的资料。GDB 手册链接指向当前版本的在线文档，行为细节以你手里的 GDB 版本为准。</p><section><h3>GDB 手册<a href="#gdb-手册"><span>#</span></a></h3><ul>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/" target="_blank">Debugging with GDB</a>：GDB 官方手册首页。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Breakpoints.html" target="_blank">Breakpoints</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Set-Watchpoints.html" target="_blank">Set Watchpoints</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Set-Catchpoints.html" target="_blank">Set Catchpoints</a>：断点、watchpoint（包括 <code>watch -location</code>）和 catchpoint。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Continuing-and-Stepping.html" target="_blank">Continuing and Stepping</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Returning.html" target="_blank">Returning</a>：<code>next</code>、<code>step</code>、<code>until</code>、<code>advance</code>、<code>finish</code> 和 <code>return</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Frames.html" target="_blank">Frames</a>：栈帧、<code>backtrace</code> 和 <code>info frame</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Memory.html" target="_blank">Memory</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Output-Formats.html" target="_blank">Output Formats</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Print-Settings.html" target="_blank">Print Settings</a>：<code>x/nfu</code>、<code>print/x</code> 等格式，以及 <code>print pretty</code>、<code>print frame-arguments</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Auto-Display.html" target="_blank">Automatic Display</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Convenience-Vars.html" target="_blank">Convenience Variables</a>：<code>display</code> 与 <code>$_exitcode</code> 这类便利变量。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Registers.html" target="_blank">Registers</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Machine-Code.html" target="_blank">Source and Machine Code</a>：<code>info registers</code>、<code>$pc</code> 和 <code>disassemble /s</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Signals.html" target="_blank">Signals</a>：<code>handle</code> 和 <code>info signals</code> 那张表。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Threads.html" target="_blank">Threads</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/All_002dStop-Mode.html" target="_blank">All-Stop Mode</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Thread-Stops.html" target="_blank">Stopping and Starting Multi-thread Programs</a>：线程编号、all-stop 模式和 <code>set scheduler-locking</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Forks.html" target="_blank">Debugging Forks</a>：<code>follow-fork-mode</code>、<code>detach-on-fork</code> 和多 inferior。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Remote-Debugging.html" target="_blank">Debugging Remote Programs</a>：<code>gdbserver</code>、<code>target remote</code> 与 <code>set sysroot</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Core-File-Generation.html" target="_blank">Core File Generation</a>：<code>generate-core-file</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Optimized-Code.html" target="_blank">Debugging Optimized Code</a>：内联函数和 <code>&lt;optimized out&gt;</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Process-Record-and-Replay.html" target="_blank">Process Record and Replay</a>：<code>record full</code> 与各种 <code>reverse-*</code> 命令。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/TUI.html" target="_blank">TUI</a>：<code>layout</code>、<code>focus</code> 和 TUI 快捷键。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Command-Files.html" target="_blank">Command Files</a>：<code>source</code>、<code>-x</code>、<code>-batch</code> 和 <code>define</code>。</li>
<li><a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Separate-Debug-Files.html" target="_blank">Debugging Information in Separate Files</a>、<a href="https://sourceware.org/gdb/current/onlinedocs/gdb.html/Auto_002dloading-safe-path.html" target="_blank">The auto-load safe-path</a>：<code>debug-file-directory</code> 和脚本自动加载的安全路径。</li>
</ul></section><section><h3>编译器与平台<a href="#编译器与平台"><span>#</span></a></h3><ul>
<li><a href="https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html" target="_blank">GCC: Options for Debugging Your Program</a>：<code>-g3</code> 等调试信息选项。</li>
<li><a href="https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html" target="_blank">GCC: Options That Control Optimization</a>：<code>-O0</code>、<code>-O2</code> 与 <code>-fno-omit-frame-pointer</code>。</li>
<li><a href="https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug.html" target="_blank">libstdc++: Debugging Support</a>：libstdc++ 的 GDB pretty printer。</li>
<li><a href="https://gitlab.com/x86-psABIs/x86-64-ABI" target="_blank">x86-64 psABI</a>：System V x86-64 调用约定，参数寄存器 <code>rdi</code>、<code>rsi</code> 的来源。</li>
<li><a href="https://man7.org/linux/man-pages/man2/TIOCGWINSZ.2const.html" target="_blank">TIOCGWINSZ(2const)</a>：<code>struct winsize</code> 的定义，gdb-dashboard 那个 4 字节缓冲区问题的根源。</li>
<li><a href="https://man7.org/linux/man-pages/man1/coredumpctl.1.html" target="_blank">coredumpctl(1)</a>：<code>coredumpctl list</code> 和 <code>coredumpctl debug</code>。</li>
<li><a href="https://docs.python.org/3/library/fcntl.html" target="_blank">Python fcntl 模块文档</a>：<code>fcntl.ioctl</code> 的缓冲区参数语义。</li>
</ul></section><section><h3>Nix 与 NixOS<a href="#nix-与-nixos"><span>#</span></a></h3><ul>
<li><a href="https://nixos.org/manual/nixpkgs/stable/#sec-hardening-in-nixpkgs" target="_blank">Nixpkgs 手册：Hardening in Nixpkgs</a>：cc-wrapper 默认启用的加固参数和 <code>hardeningDisable</code>。</li>
<li><a href="https://github.com/NixOS/nixpkgs/tree/master/pkgs/build-support/cc-wrapper" target="_blank">Nixpkgs cc-wrapper 源码</a>：<code>NIX_DEBUG=1</code> 打印的 <code>extra flags before</code> 就来自这里。</li>
<li><a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/gd/gdb/package.nix" target="_blank">Nixpkgs gdb 包定义</a> 和 <a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/gd/gdb/debug-info-from-env.patch" target="_blank">debug-info-from-env.patch</a>：<code>auto-load safe-path</code> 的默认值，以及 <code>NIX_DEBUG_INFO_DIRS</code> 是怎么被读取的。</li>
<li><a href="https://wiki.nixos.org/wiki/Debug_Symbols" target="_blank">NixOS Wiki: Debug Symbols</a>：在 NixOS 上获取分离调试符号的几种方式。</li>
<li><a href="https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-develop" target="_blank">nix develop</a>：flake 开发环境。</li>
<li><a href="https://nix-community.github.io/home-manager/options.xhtml" target="_blank">Home Manager 选项手册</a>：<code>home.file</code> 与 <code>xdg.configFile</code>。</li>
</ul></section><section><h3>其他工具<a href="#其他工具"><span>#</span></a></h3><ul>
<li><a href="https://github.com/cyrus-and/gdb-dashboard" target="_blank">gdb-dashboard</a>、<a href="https://github.com/cyrus-and/gdb-dashboard/wiki" target="_blank">它的 Wiki</a> 与 <a href="https://github.com/cyrus-and/gdb-dashboard/blob/master/.gdbinit" target="_blank">.gdbinit 源码</a>：题外话一节里的配置目录、<code>-layout</code>、<code>-output</code> 和 <code>get_term_size</code> 都能在源码里找到。</li>
<li><a href="https://rr-project.org/" target="_blank">rr</a>：面向多线程程序的录制与回放调试器。</li>
<li><a href="https://clang.llvm.org/docs/ThreadSanitizer.html" target="_blank">ThreadSanitizer</a>：数据竞争检测工具。</li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/latex-project-structure/</id>
      <title type="text">LaTeX 项目结构与 latexmk 配置</title>
      <published>2026-06-18T03:20:00.000Z</published>
      <updated>2026-06-18T03:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/latex-project-structure/"/>
      <summary type="text">我用 LaTeX 写东西时的项目目录结构、main.tex 模板，以及一份跨平台的 .latexmkrc 配置。</summary>
      <content type="html"><![CDATA[<section><h2>目录结构<a href="#目录结构"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>C:.</span></div></div><div><div><div>2</div></div><div><span>│</span><span>  </span><span>.gitignore</span></div></div><div><div><div>3</div></div><div><span>│</span><span>  </span><span>.latexmkrc</span></div></div><div><div><div>4</div></div><div><span>│</span><span>  </span><span>LICENSE</span></div></div><div><div><div>5</div></div><div><span>│</span><span>  </span><span>main.tex</span></div></div><div><div><div>6</div></div><div><span>│</span><span>  </span><span>refs.bib</span></div></div><div><div><div>7</div></div><div><span>│</span></div></div><div><div><div>8</div></div><div><span>├─contents</span></div></div><div><div><div>9</div></div><div><span>├─docs</span></div></div><div><div><div>10</div></div><div><span>├─figs</span></div></div><div><div><div>11</div></div><div><span>└─settings</span></div></div><div><div><div>12</div></div><div><span>    </span><span>format.tex</span></div></div><div><div><div>13</div></div><div><span>    </span><span>packages.tex</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>main.tex</code>：入口文件，只负责把各部分拼起来。</li>
<li><code>refs.bib</code>：参考文献数据库。用 BibTeX 格式存储文献条目，正文里用 <code>\cite{key}</code> 引用。配合 <code>biblatex</code> 使用时，在 <code>format.tex</code> 里用 <code>\addbibresource{refs.bib}</code> 指定路径。</li>
<li><code>contents/</code>：章节内容，按章（<code>book</code>/<code>report</code> 类）或按节（<code>article</code> 类）建子文件夹。</li>
<li><code>figs/</code>：图片资源。配合 <code>\graphicspath{{figs/}}</code> 使用，正文里直接写文件名就行。</li>
<li><code>docs/</code>：项目文档，比如编译好的 PDF、使用说明等。</li>
<li><code>settings/packages.tex</code>：宏包导入。</li>
<li><code>settings/format.tex</code>：格式配置（页面布局、标题样式、字体等）。</li>
<li><code>.latexmkrc</code>：latexmk 的编译配置，后面会详细讲。</li>
<li><code>.gitignore</code>：LaTeX 项目的忽略模板可以参考 <a href="https://github.com/github/gitignore/blob/main/TeX.gitignore" target="_blank">github/gitignore/TeX.gitignore</a>。注意官方模板里没有 <code>build/</code> 目录，如果你像我一样把编译产物输出到 <code>build/</code>，记得手动加上。</li>
</ul><p><code>packages.tex</code> 和 <code>format.tex</code> 的最小示例：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>% settings/packages.tex -- 只管导入，不管配置</span></div></div><div><div><div>2</div></div><div><span>\usepackage</span><span>{fontspec}</span></div></div><div><div><div>3</div></div><div><span>\usepackage</span><span>{xeCJK}</span></div></div><div><div><div>4</div></div><div><span>\usepackage</span><span>{geometry}</span></div></div><div><div><div>5</div></div><div><span>\usepackage</span><span>{fancyhdr}</span></div></div><div><div><div>6</div></div><div><span>\usepackage</span><span>{amsmath}</span></div></div><div><div><div>7</div></div><div><span>\usepackage</span><span>{amssymb}</span></div></div><div><div><div>8</div></div><div><span>\usepackage</span><span>{amsfonts}</span></div></div><div><div><div>9</div></div><div><span>\usepackage</span><span>{mathtools}</span></div></div><div><div><div>10</div></div><div><span>\usepackage</span><span>{graphicx}</span></div></div><div><div><div>11</div></div><div><span>\usepackage</span><span>{tabularray}</span></div></div><div><div><div>12</div></div><div><span>\usepackage</span><span>{subcaption}</span></div></div><div><div><div>13</div></div><div><span>\usepackage</span><span>{listings}</span></div></div><div><div><div>14</div></div><div><span>\usepackage</span><span>{enumitem}</span></div></div><div><div><div>15</div></div><div><span>\usepackage</span><span>{siunitx}</span></div></div><div><div><div>16</div></div><div><span>\usepackage</span><span>[backend=biber, style=gb7714-2015]{biblatex}</span></div></div><div><div><div>17</div></div><div><span>\usepackage</span><span>[colorlinks=true, linkcolor=blue, citecolor=blue, urlcolor=blue]{hyperref}</span></div></div><div><div><div>18</div></div><div><span>\usepackage</span><span>[capitalise]{cleveref}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>% settings/format.tex -- 字体、布局、宏包的后续配置</span></div></div><div><div><div>2</div></div><div><span>\setmainfont</span><span>{TeX Gyre Termes}</span></div></div><div><div><div>3</div></div><div><span>\setsansfont</span><span>{TeX Gyre Heros}</span></div></div><div><div><div>4</div></div><div><span>\setmonofont</span><span>{TeX Gyre Cursor}</span></div></div><div><div><div>5</div></div><div><span>\setCJKmainfont</span><span>{Source Han Serif SC}</span></div></div><div><div><div>6</div></div><div><span>\setCJKsansfont</span><span>{Source Han Sans SC}</span></div></div><div><div><div>7</div></div><div><span>\setCJKmonofont</span><span>{Source Han Mono SC}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>\geometry</span><span>{a4paper, margin=2.5cm}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>\pagestyle</span><span>{fancy}</span></div></div><div><div><div>12</div></div><div><span>\fancyhf</span><span>{}</span></div></div><div><div><div>13</div></div><div><span>\fancyhead</span><span>[L]{</span><span>\leftmark</span><span>}</span></div></div><div><div><div>14</div></div><div><span>\fancyfoot</span><span>[C]{</span><span>\thepage</span><span>}</span></div></div><div><div><div>15</div></div><div><span>\renewcommand</span><span>{</span><span>\headrulewidth</span><span>}{0.4pt}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>\UseTblrLibrary</span><span>{booktabs, siunitx}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>\graphicspath</span><span>{{figs/}}</span></div></div><div><div><div>20</div></div><div><span>\sisetup</span><span>{range-phrase=</span><span>\ensuremath</span><span>{</span><span>\sim</span><span>}, range-units=single}</span></div></div><div><div><div>21</div></div><div><span>\addbibresource</span><span>{refs.bib}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>% tabularray 常用全局设置</span></div></div><div><div><div>24</div></div><div><span>\SetTblrStyle</span><span>{firsthead}{font=</span><span>\bfseries</span><span>}</span></div></div><div><div><div>25</div></div><div><span>\SetTblrStyle</span><span>{firstfoot}{font=</span><span>\small</span><span>}</span></div></div><div><div><div>26</div></div><div><span>\SetTblrStyle</span><span>{middlefoot}{font=</span><span>\small</span><span>}</span></div></div><div><div><div>27</div></div><div><span>\SetTblrStyle</span><span>{lastfoot}{font=</span><span>\small</span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>实际使用时按需增减。宏包的加载顺序有讲究，后面”常用宏包”一节会说明。</p></section>
<section><h2><code>main.tex</code> 模板<a href="#maintex-模板"><span>#</span></a></h2><section><h3><code>book</code> 类<a href="#book-类"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\documentclass</span><span>{book}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>\input</span><span>{settings/packages}</span></div></div><div><div><div>4</div></div><div><span>\input</span><span>{settings/format}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>\begin</span><span>{document}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>\maketitle</span></div></div><div><div><div>9</div></div><div><span>\frontmatter</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>\tableofcontents</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>\mainmatter</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>% \input{contents/chap_01}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>\appendix</span></div></div><div><div><div>18</div></div><div><span>% \input{contents/appendix}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>\backmatter</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>\end</span><span>{document}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3><code>article</code> 类<a href="#article-类"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\documentclass</span><span>{article}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>\input</span><span>{settings/packages}</span></div></div><div><div><div>4</div></div><div><span>\input</span><span>{settings/format}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>\begin</span><span>{document}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>\maketitle</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>\tableofcontents</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>% \input{contents/sec_01}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>\appendix</span></div></div><div><div><div>15</div></div><div><span>% \input{contents/appendix}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>\end</span><span>{document}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3><code>report</code> 类<a href="#report-类"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\documentclass</span><span>{report}</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>\input</span><span>{settings/packages}</span></div></div><div><div><div>4</div></div><div><span>\input</span><span>{settings/format}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>\begin</span><span>{document}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>\maketitle</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>\tableofcontents</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>% \input{contents/chap_01}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>\appendix</span></div></div><div><div><div>15</div></div><div><span>% \input{contents/appendix}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>\end</span><span>{document}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>三者差在哪<a href="#三者差在哪"><span>#</span></a></h3>

<table><thead><tr><th></th><th><code>article</code></th><th><code>report</code></th><th><code>book</code></th></tr></thead><tbody><tr><td>最高章节</td><td><code>\section</code></td><td><code>\chapter</code></td><td><code>\chapter</code></td></tr><tr><td>默认排版</td><td>单面</td><td>单面</td><td>双面</td></tr><tr><td>标题页</td><td>正文内</td><td>独立一页</td><td>独立一页</td></tr><tr><td><code>\frontmatter</code> / <code>\backmatter</code></td><td>无</td><td>无</td><td>有</td></tr><tr><td>典型用途</td><td>短文、笔记、报告</td><td>课程报告、毕业论文</td><td>书籍、学位论文</td></tr></tbody></table><p><code>report</code> 和 <code>book</code> 都有 <code>\chapter</code>，区别主要在排版方式和文档结构命令。<code>book</code> 多了 <code>\frontmatter</code>、<code>\mainmatter</code>、<code>\backmatter</code>，默认双面排版，适合正式出版物。<code>report</code> 默认单面，没有前言/正文/后记的划分，适合不需要装订的长文档。</p><p><code>article</code> 没有 <code>\chapter</code>，从 <code>\section</code> 开始。写短文、笔记够用了。</p><p>三者的标题页也不同。<code>book</code> 和 <code>report</code> 的 <code>\maketitle</code> 都是独立一页，<code>article</code> 的只是在正文开头插一行。</p></section></section>
<section><h2>工具简介<a href="#工具简介"><span>#</span></a></h2><p>在看 <code>.latexmkrc</code> 之前，先简单介绍一下涉及的几个工具。</p><p><strong>XeLaTeX</strong></p><p>LaTeX 的编译引擎之一。相比传统的 pdfLaTeX，XeLaTeX 原生支持 Unicode 和系统字体（通过 fontspec），处理中文要方便得多。如果你主要写中文文档，XeLaTeX（或 LuaLaTeX）基本是必选的。</p><p><strong>latexmk</strong></p><p>一个 Perl 写的自动化编译工具。LaTeX 的编译经常需要跑多遍（交叉引用、目录、参考文献），手动执行很烦。latexmk 会自动判断需要跑几遍、要不要调用 BibTeX/Biber，直到所有引用都稳定为止。配合 <code>-pvc</code> 参数还能监控文件变化，保存即编译。</p><p><strong>BibTeX 和 Biber</strong></p><p>两者都是处理参考文献的工具。BibTeX 是老方案，Biber 是配合 biblatex 宏包的新方案，功能更强（支持 Unicode、更灵活的样式定制）。latexmk 的 <code>$bibtex_use = 2</code> 会自动检测你用的是哪个，不用手动切换。</p><p><strong>SyncTeX</strong></p><p>在编辑器和 PDF 阅读器之间做双向跳转的数据格式。加了 <code>-synctex=1</code> 之后，你可以在编辑器里点一个位置，PDF 阅读器自动翻到对应页面，反过来也行。</p></section>
<section><h2><code>.latexmkrc</code><a href="#latexmkrc"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 主文件</span></div></div><div><div><div>2</div></div><div><span>$root</span><span> = </span><span>'main.tex'</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 输出目录</span></div></div><div><div><div>5</div></div><div><span>$out_dir</span><span> = </span><span>'build'</span><span>;</span></div></div><div><div><div>6</div></div><div><span>$aux_dir</span><span> = </span><span>'build'</span><span>;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 编译引擎: XeLaTeX</span></div></div><div><div><div>9</div></div><div><span>$pdf_mode</span><span> = 5;</span></div></div><div><div><div>10</div></div><div><span>$xelatex</span><span> = </span><span>'xelatex -interaction=nonstopmode -file-line-error -synctex=1 %O %S'</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span># 自动运行 BibTeX 或 Biber</span></div></div><div><div><div>13</div></div><div><span>$bibtex_use</span><span> = 2;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span># 清理时保留 PDF</span></div></div><div><div><div>16</div></div><div><span>$clean_ext</span><span> = </span><span>'aux bbl bcf blg fdb_latexmk fls log out run.xml synctex.gz toc'</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span># 跨平台 PDF 预览器</span></div></div><div><div><div>19</div></div><div><span>if</span><span> ($^O </span><span>eq</span><span> </span><span>'MSWin32'</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>$pdf_previewer</span><span> = </span><span>'start %S'</span><span>;</span></div></div><div><div><div>21</div></div><div><span>} </span><span>elsif</span><span> ($^O </span><span>eq</span><span> </span><span>'darwin'</span><span>) {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>$pdf_previewer</span><span> = </span><span>'open %S'</span><span>;</span></div></div><div><div><div>23</div></div><div><span>} </span><span>else</span><span> {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>$pdf_previewer</span><span> = </span><span>'xdg-open %S 2&gt;/dev/null || evince %S'</span><span>;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span># Windows 下清理时不删除被占用的文件</span></div></div><div><div><div>28</div></div><div><span>if</span><span> ($^O </span><span>eq</span><span> </span><span>'MSWin32'</span><span>) {</span></div></div><div><div><div>29</div></div><div><span>    </span><span>$cleanup_includes_generated</span><span> = 0;</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span># 自定义依赖示例（取消注释即可用）</span></div></div><div><div><div>33</div></div><div><span># add_cus_dep('glo', 'gls', 0, 'makeglos');</span></div></div><div><div><div>34</div></div><div><span># sub makeglos {</span></div></div><div><div><div>35</div></div><div><span>#     system("makeindex -s $_[0].ist -t $_[0].glg -o $_[0].gls $_[0].glo");</span></div></div><div><div><div>36</div></div><div><span># }</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>逐项说明<a href="#逐项说明"><span>#</span></a></h3><ul>
<li><code>$root</code>：主文件名。latexmk 从这里开始分析依赖。改成你的实际文件名就行。</li>
<li><code>$out_dir</code> / <code>$aux_dir</code>：编译产物放哪。设成 <code>build</code> 能让根目录干净一些，PDF 会出现在 <code>build/main.pdf</code>。记得在 <code>.gitignore</code> 里加上 <code>build/</code>。</li>
<li><code>$pdf_mode</code>：选择编译引擎。1 是 pdfLaTeX，4 是 LuaLaTeX，5 是 XeLaTeX。</li>
<li><code>$xelatex</code>：XeLaTeX 的调用参数。<code>-interaction=nonstopmode</code> 让它遇到错误继续编译而不是停下来等你按回车；<code>-file-line-error</code> 让报错信息带上文件名和行号；<code>-synctex=1</code> 生成编辑器跳转用的数据。<code>%O</code> 和 <code>%S</code> 是 latexmk 的占位符，分别代表额外选项和源文件。</li>
<li><code>$bibtex_use</code>：设为 2 就行，自动检测 BibTeX 还是 Biber。设为 0 关闭自动处理，1 只在 <code>.bbl</code> 已存在时才跑。</li>
<li><code>$clean_ext</code>：<code>latexmk -c</code> 会删掉这些后缀的文件。PDF 不在里面，所以清理后 PDF 还在。注意区分 <code>-c</code>（清理中间文件，保留 PDF）和 <code>-C</code>（连 PDF 一起删），别手滑用了 <code>-C</code>。</li>
<li><strong>跨平台预览器</strong>：Perl 的 <code>$^O</code> 变量存着操作系统类型：Windows 是 <code>MSWin32</code>，macOS 是 <code>darwin</code>，Linux 就是 <code>linux</code>。<code>start</code>、<code>open</code>、<code>xdg-open</code> 分别是三个平台打开文件的命令。<code>%S</code> 会被替换成 PDF 路径。配合 <code>latexmk -pvc</code> 使用，保存文件后自动编译并刷新 PDF 预览，不用手动跑命令。</li>
<li><code>$cleanup_includes_generated</code>：Windows 上文件锁比较死板，latexmk 清理时可能删不掉正在被占用的文件。设为 0 可以避免这个问题。</li>
<li><strong>自定义依赖</strong>：<code>add_cus_dep</code> 用来告诉 latexmk “如果某个 <code>.glo</code> 文件变了，要重新生成对应的 <code>.gls</code>”。示例里是术语表的场景，取消注释就能用。</li>
</ul></section></section>
<section><h2>常用宏包<a href="#常用宏包"><span>#</span></a></h2><p>以下是写中文 LaTeX 文档时经常会用到的宏包，以及它们的典型配置。</p><p>加载顺序需要注意几点：<code>fontspec</code> 和 <code>xeCJK</code> 要在大多数宏包之前加载；<code>amsmath</code> 要在 <code>mathtools</code> 之前；<code>tabularray</code> 的 <code>\UseTblrLibrary</code> 要在 <code>siunitx</code> 之后（因为 <code>siunitx</code> 库需要 siunitx 已加载）；<code>hyperref</code> 几乎总是放在最后，唯一的例外是 <code>cleveref</code>，它必须在 <code>hyperref</code> 之后。上面 <code>packages.tex</code> 示例中的顺序可以直接用。</p><section><h3>字体与编码<a href="#字体与编码"><span>#</span></a></h3><p><strong>fontspec</strong></p><p>配合 XeLaTeX/LuaLaTeX 使用，用来设置系统字体。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{fontspec}</span></div></div><div><div><div>2</div></div><div><span>\setmainfont</span><span>{TeX Gyre Termes}          </span><span>% 正文衬线字体</span></div></div><div><div><div>3</div></div><div><span>\setsansfont</span><span>{TeX Gyre Heros}           </span><span>% 无衬线字体</span></div></div><div><div><div>4</div></div><div><span>\setmonofont</span><span>{TeX Gyre Cursor}          </span><span>% 等宽字体</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>xeCJK</strong></p><p>处理中文排版的核心宏包，自动处理中英文之间的间距、标点挤压等。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{xeCJK}</span></div></div><div><div><div>2</div></div><div><span>\setCJKmainfont</span><span>{Source Han Serif SC}   </span><span>% 中文衬线（思源宋体）</span></div></div><div><div><div>3</div></div><div><span>\setCJKsansfont</span><span>{Source Han Sans SC}    </span><span>% 中文无衬线（思源黑体）</span></div></div><div><div><div>4</div></div><div><span>\setCJKmonofont</span><span>{Source Han Mono SC}    </span><span>% 中文等宽</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>页面布局<a href="#页面布局"><span>#</span></a></h3><p><strong>geometry</strong></p><p>控制页面尺寸和边距，比 LaTeX 默认的布局参数好用得多。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>[a4paper, margin=2.5cm]{geometry}</span></div></div><div><div><div>2</div></div><div><span>% 也可以精细控制</span></div></div><div><div><div>3</div></div><div><span>% \usepackage[a4paper, top=3cm, bottom=2.5cm, left=2.8cm, right=2.2cm]{geometry}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>fancyhdr</strong></p><p>自定义页眉页脚。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{fancyhdr}</span></div></div><div><div><div>2</div></div><div><span>\pagestyle</span><span>{fancy}</span></div></div><div><div><div>3</div></div><div><span>\fancyhf</span><span>{}                             </span><span>% 清空默认设置</span></div></div><div><div><div>4</div></div><div><span>\fancyhead</span><span>[L]{</span><span>\leftmark</span><span>}               </span><span>% 页眉左侧放章节名</span></div></div><div><div><div>5</div></div><div><span>\fancyfoot</span><span>[C]{</span><span>\thepage</span><span>}                </span><span>% 页脚中间放页码</span></div></div><div><div><div>6</div></div><div><span>\renewcommand</span><span>{</span><span>\headrulewidth</span><span>}{0.4pt}   </span><span>% 页眉横线粗细</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>数学与代码<a href="#数学与代码"><span>#</span></a></h3><p><strong>amsmath / amssymb / amsfonts</strong></p><p>数学排版的基础三件套，基本上写公式就得用。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{amsmath}</span></div></div><div><div><div>2</div></div><div><span>\usepackage</span><span>{amssymb}</span></div></div><div><div><div>3</div></div><div><span>\usepackage</span><span>{amsfonts}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>mathtools</strong></p><p>amsmath 的扩展包，修复了一些设计问题并增加了实用工具。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{mathtools}</span></div></div><div><div><div>2</div></div><div><span>% 例如：可以给公式加编号标签</span></div></div><div><div><div>3</div></div><div><span>% \usetagform{default}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>listings</strong></p><p>排版代码块。支持语法高亮，可以指定语言和样式。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{listings}</span></div></div><div><div><div>2</div></div><div><span>\lstset</span><span>{</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>basicstyle=</span><span>\ttfamily\small</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>breaklines=true,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>frame=single,</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>numbers=left,</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>numberstyle=</span><span>\tiny\color</span><span>{gray},</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>keywordstyle=</span><span>\color</span><span>{blue},</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>commentstyle=</span><span>\color</span><span>{green!60!black},</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>stringstyle=</span><span>\color</span><span>{red!70!black},</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>minted</strong></p><p>基于 Pygments 的代码排版，高亮效果比 listings 好，但需要 Python 环境且编译时要加 <code>-shell-escape</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{minted}</span></div></div><div><div><div>2</div></div><div><span>% 使用示例：</span></div></div><div><div><div>3</div></div><div><span>% \begin{minted}{python}</span></div></div><div><div><div>4</div></div><div><span>% def hello():</span></div></div><div><div><div>5</div></div><div><span>%     print("world")</span></div></div><div><div><div>6</div></div><div><span>% \end{minted}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>图表与浮动体<a href="#图表与浮动体"><span>#</span></a></h3><p><strong>graphicx</strong></p><p>插入图片。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{graphicx}</span></div></div><div><div><div>2</div></div><div><span>\graphicspath</span><span>{{figs/}}                 </span><span>% 指定图片搜索路径</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>tabularray</strong></p><p>现代表格宏包，统一了表格的样式配置和内容分离。用 <code>tblr</code> 环境替代传统的 <code>tabular</code>，样式参数写在 <code>\begin{tblr}{...}</code> 的花括号里，正文和样式彻底分开。配合 <code>booktabs</code> 库可以使用 <code>\toprule</code>、<code>\midrule</code>、<code>\bottomrule</code>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{tabularray}</span></div></div><div><div><div>2</div></div><div><span>\UseTblrLibrary</span><span>{booktabs, siunitx}</span></div></div><div><div><div>3</div></div><div><span>% \begin{table}</span></div></div><div><div><div>4</div></div><div><span>% \begin{tblr}{</span></div></div><div><div><div>5</div></div><div><span>%   colspec = {lrc},</span></div></div><div><div><div>6</div></div><div><span>%   row{1} = {font=\bfseries},</span></div></div><div><div><div>7</div></div><div><span>% }</span></div></div><div><div><div>8</div></div><div><span>%   \toprule</span></div></div><div><div><div>9</div></div><div><span>%   姓名 &amp; 分数 &amp; 等级 \\</span></div></div><div><div><div>10</div></div><div><span>%   \midrule</span></div></div><div><div><div>11</div></div><div><span>%   张三 &amp; 95 &amp; A \\</span></div></div><div><div><div>12</div></div><div><span>%   李四 &amp; 82 &amp; B \\</span></div></div><div><div><div>13</div></div><div><span>%   \bottomrule</span></div></div><div><div><div>14</div></div><div><span>% \end{tblr}</span></div></div><div><div><div>15</div></div><div><span>% \end{table}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>booktabs</code> 库提供三线表的线条命令，<code>siunitx</code> 库让列可以用 <code>S</code> 类型对齐数字。tabularray 还支持 <code>longtblr</code>（跨页长表格）和 <code>talltblr</code>（不跨页但可加脚注的表格），基本覆盖了所有表格场景。</p><p><strong>subcaption</strong></p><p>在一个浮动体里放多张子图，每个子图独立编号和引用。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{subcaption}</span></div></div><div><div><div>2</div></div><div><span>% \begin{figure}[htbp]</span></div></div><div><div><div>3</div></div><div><span>%   \begin{subfigure}{0.48\textwidth}</span></div></div><div><div><div>4</div></div><div><span>%     \includegraphics[width=\textwidth]{fig_a}</span></div></div><div><div><div>5</div></div><div><span>%     \caption{子图 A}</span></div></div><div><div><div>6</div></div><div><span>%   \end{subfigure}</span></div></div><div><div><div>7</div></div><div><span>%   \hfill</span></div></div><div><div><div>8</div></div><div><span>%   \begin{subfigure}{0.48\textwidth}</span></div></div><div><div><div>9</div></div><div><span>%     \includegraphics[width=\textwidth]{fig_b}</span></div></div><div><div><div>10</div></div><div><span>%     \caption{子图 B}</span></div></div><div><div><div>11</div></div><div><span>%   \end{subfigure}</span></div></div><div><div><div>12</div></div><div><span>%   \caption{总标题}</span></div></div><div><div><div>13</div></div><div><span>% \end{figure}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>参考文献<a href="#参考文献"><span>#</span></a></h3><p><strong>biblatex</strong></p><p>现代的参考文献管理方案，配合 Biber 后端使用。比 BibTeX 灵活很多，支持多语言、多文献列表等。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>[backend=biber, style=gb7714-2015]{biblatex}</span></div></div><div><div><div>2</div></div><div><span>\addbibresource</span><span>{refs.bib}              </span><span>% 指定 .bib 文件</span></div></div><div><div><div>3</div></div><div><span>% 正文中用 \cite{key} 引用</span></div></div><div><div><div>4</div></div><div><span>% 文末用 \printbibliography 输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>style=gb7714-2015</code> 是中文文献常用的国标样式，需要 <code>biblatex-gb7714-2015</code> 宏包。英文文档常用 <code>style=ieee</code>、<code>style=apa</code> 或 <code>style=alphabetic</code>。</p><p><strong>hyperref</strong></p><p>给交叉引用、目录、参考文献加上超链接。通常放在 <code>\usepackage</code> 的最后（少数例外，如 cleveref 要在 hyperref 之后）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>[</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>colorlinks=true,</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>linkcolor=blue,</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>citecolor=blue,</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>urlcolor=blue,</span></div></div><div><div><div>6</div></div><div><span>]{hyperref}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>其他实用工具<a href="#其他实用工具"><span>#</span></a></h3><p><strong>ctex</strong></p><p>如果不想手动配 xeCJK，直接用 ctex 宏包或 <code>ctexart</code>/<code>ctexbook</code> 文档类，一键搞定中文支持。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>[UTF8, heading=true]{ctex}</span></div></div><div><div><div>2</div></div><div><span>% 或者直接用 \documentclass{ctexart}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>enumitem</strong></p><p>定制列表环境（enumerate、itemize、description）的间距和标签样式。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{enumitem}</span></div></div><div><div><div>2</div></div><div><span>\setlist</span><span>{nosep}                        </span><span>% 去掉列表的多余间距</span></div></div><div><div><div>3</div></div><div><span>% \begin{enumerate}[label=(\arabic*)]</span></div></div><div><div><div>4</div></div><div><span>%   \item 第一项</span></div></div><div><div><div>5</div></div><div><span>%   \item 第二项</span></div></div><div><div><div>6</div></div><div><span>% \end{enumerate}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>tikz</strong></p><p>画矢量图。学习曲线陡峭，但画出来的东西质量很高，跟 LaTeX 文档浑然一体。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{tikz}</span></div></div><div><div><div>2</div></div><div><span>\usetikzlibrary</span><span>{arrows.meta, positioning}</span></div></div><div><div><div>3</div></div><div><span>% 简单示例：画一个带箭头的直线</span></div></div><div><div><div>4</div></div><div><span>% \begin{tikzpicture}</span></div></div><div><div><div>5</div></div><div><span>%   \draw[-&gt;] (0,0) -- (2,1) node[right] {$y = x/2$};</span></div></div><div><div><div>6</div></div><div><span>% \end{tikzpicture}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>siunitx</strong></p><p>处理数值和单位的排版。数字的千分位、科学记数法、单位的正体和间距，它都能管。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>{siunitx}</span></div></div><div><div><div>2</div></div><div><span>\sisetup</span><span>{</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>range-phrase = </span><span>\ensuremath</span><span>{</span><span>\sim</span><span>},</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>range-units = single,</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>% \qty{10}{\metre}           -&gt; 10 m</span></div></div><div><div><div>7</div></div><div><span>% \qty{1.23e5}{\pascal}      -&gt; 1.23 × 10⁵ Pa</span></div></div><div><div><div>8</div></div><div><span>% \qtyrange{1}{10}{\metre}   -&gt; 1~10 m（单位只出现一次，用 ~ 连接）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>range-units=single</code> 让范围表达中单位只出现一次，而不是”1 m 到 10 m”。<code>range-phrase</code> 设成 <code>\ensuremath{\sim}</code> 把分隔符换成 <code>~</code>（波浪号），<code>\ensuremath</code> 确保它在数学模式和文本模式下都能正确渲染。</p><p><strong>cleveref</strong></p><p>智能引用，自动加上”图""表""公式”等前缀，不用自己写 <code>\ref{fig:xxx} 的图</code>。注意要放在 hyperref 之后加载。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\usepackage</span><span>[capitalise]{cleveref}</span></div></div><div><div><div>2</div></div><div><span>% 使用 \cref{fig:example} 会自动输出"图 1"</span></div></div><div><div><div>3</div></div><div><span>% \cref{eq:main,eq:sub} 会输出"公式 (1) 和 (2)"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/devicetree-study-notes/</id>
      <title type="text">设备树入门：从第一份 DTS 到 Linux 驱动匹配</title>
      <published>2026-05-20T03:15:00.000Z</published>
      <updated>2026-05-20T03:15:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/devicetree-study-notes/"/>
      <summary type="text">我从一份可编译的最小 DTS 开始，走完 DTS、DTB、boot program 到 Linux 驱动匹配的链路，再用真实工具和内核调试路径验证设备树。</summary>
      <content type="html"><![CDATA[<p>我以前把设备树当成一堆需要背下来的属性名：<code>compatible</code>、<code>reg</code>、<code>interrupts</code>，还有几个看起来像预处理器产物的 <code>#*-cells</code>。后来我决定只做一件事：写一份足够小的 DTS，把它编译成 DTB，再从 Linux 看到它实际收到的内容。设备树一下从名词表变成了一条可以跑通的交接链路。</p>
<p>本文不绑定具体开发板。我们会用一个虚构但自洽的 SoC 和 UART 做实验，所以这份 DTB 不会神奇地让你的电脑多出一个串口（遗憾，但也算避免了一次售后事故）。</p>
<section><h2>设备树到底解决什么问题？<a href="#设备树到底解决什么问题"><span>#</span></a></h2><p>PCI 和 USB 这类总线可以通过协议探测设备。Linux 询问总线，设备回答自己的身份，操作系统再加载驱动。SoC 内部的 UART、GPIO、定时器和很多 I2C 设备通常没有这样的自发现协议。CPU 只知道某个地址范围可能连着寄存器，却不知道那里是什么设备，或者这根中断线接到了哪里。</p><p>设备树把这份外部知识交给启动链路。把它想成搬家时交接的一张机器清单：清单说明房间里有什么、门牌号是什么、钥匙交给谁；它不会替你操作洗衣机。对应到规范术语，boot program 可以是固件、bootloader 或 hypervisor，client program 可以是 Linux，也可以是另一个 bootloader。boot program 把 devicetree 放进内存，然后把它传给 client program。</p><p>因此职责边界很重要：</p><ul>
<li><strong>DTS/DTB</strong> 描述硬件的拓扑、资源和连接关系。</li>
<li><strong>boot program</strong> 加载 DTB，并把它传给下一个程序。</li>
<li><strong>Linux 驱动</strong> 根据描述找到资源，映射寄存器，申请中断，然后真正操作硬件。</li>
<li><strong>binding</strong> 规定某类设备允许使用哪些 <code>compatible</code> 和属性，是接口约束，不是驱动实现。</li>
</ul><p>设备树不是硬件扫描器，也不是驱动代码。往 <code>reg</code> 里写一个地址，不会自动生成对该地址执行读写的驱动。</p><div><div><div></div><div>Warning</div></div><div><p><strong>注意：设备树不会替代驱动。</strong> <code>compatible</code> 只负责匹配，<code>reg</code> 只描述资源。只有对应的 Linux 驱动成功匹配并执行 <code>probe()</code>，设备才可能被初始化。</p></div></div></section>
<section><h2>先跑通一条 DTS 到 DTB 的链路<a href="#先跑通一条-dts-到-dtb-的链路"><span>#</span></a></h2><p>我们先写一个只描述 UART 的最小例子。这里的地址 <code>0x10000000</code> 是实验数据，不属于任何真实芯片。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/</span><span>dts</span><span>-</span><span>v1</span><span>/</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>/</span><span> {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>compatible </span><span>=</span><span> </span><span>"example,hello-board"</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>model </span><span>=</span><span> </span><span>"Example Hello Board"</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>#address</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>#size</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>soc {</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>compatible </span><span>=</span><span> </span><span>"simple-bus"</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>#address</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>#size</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>ranges;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>uart0: serial@</span><span>10000000</span><span> {</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>compatible </span><span>=</span><span> </span><span>"example,uart"</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>            </span></span><span>reg </span><span>=</span><span> </span><span>&lt;</span><span><span>0x</span><span>10000000</span></span><span> </span><span><span>0x</span><span>100</span></span><span>&gt;</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>            </span></span><span>status </span><span>=</span><span> </span><span>"okay"</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>22</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>保存为 <code>/tmp/devicetree-tutorial.dts</code> 后，先确认工具是否存在：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>dtc</span><span> </span><span>--version</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>dtc</code> 是 Device Tree Compiler。它把 DTS 文本转换成 DTB，也能把 DTB 反编译回近似的 DTS。我的实验环境如果没有 <code>dtc</code>，这一步就应该明确停下来安装工具，而不是编造一段”成功输出”。在 Debian/Ubuntu 上通常由 <code>device-tree-compiler</code> 包提供，其他发行版请查自己的包管理器。</p><div><div><div></div><div>Caution</div></div><div><p><strong>工具检查。</strong> 如果 <code>dtc</code> 不在 <code>PATH</code> 中，请先安装 Device Tree Compiler。后续编译、反编译和 magic 检查都依赖它们；不要把”命令没有执行”写成”实验成功”。</p></div></div><p>编译命令的输入是 DTS，输出是 DTB：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>dtc</span><span> </span><span>-I</span><span> </span><span>dts</span><span> </span><span>-O</span><span> </span><span>dtb</span><span> </span><span>-o</span><span> </span><span>/tmp/devicetree-tutorial.dtb</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/tmp/devicetree-tutorial.dts</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>随后反编译：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>dtc</span><span> </span><span>-I</span><span> </span><span>dtb</span><span> </span><span>-O</span><span> </span><span>dts</span><span> </span><span>-o</span><span> </span><span>/tmp/devicetree-tutorial.roundtrip.dts</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>    </span><span>/tmp/devicetree-tutorial.dtb</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>反编译结果不是原文件的文本复制品。标签名、注释和部分源码排版可能消失，但根节点、<code>compatible</code>、<code>reg</code> 和 <code>status</code> 等设备树数据应该还在。若系统安装了 <code>fdtdump</code>，还可以查看底层结构；没有它时，至少可以观察 DTB 的头部：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>hexdump</span><span> </span><span>-C</span><span> </span><span>-n</span><span> </span><span>16</span><span> </span><span>/tmp/devicetree-tutorial.dtb</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>合法 DTB 的前四个字节按大端解释为 magic <code>0xd00dfeed</code>。这不是”文件前 40 字节永远是什么”的承诺，后面讲 FDT 结构时会看到，真正可靠的是头部字段里的偏移和长度。</p><p>这里的”大端”说的是<strong>多字节数值在内存或文件中的字节排列顺序</strong>。例如数值 <code>0x12345678</code> 由四个字节 <code>12 34 56 78</code> 组成：大端把高位字节 <code>12</code> 放在低地址一侧，小端则把低位字节 <code>78</code> 放在低地址一侧。设备树规范要求 DTB 中的 cell 按大端编码，所以用十六进制查看原始字节时，不能直接把字节序列当作当前 CPU 的本地整数来读。</p><div><div><div></div><div>Note</div></div><div><p><strong>大小端提醒。</strong> <code>0xd00dfeed</code> 是按大端解释出的 32 位 magic。看到原始字节时，先确认协议规定的字节序，再解释数值；否则很容易把同一组字节读成另一个整数。</p></div></div><p><strong>这份小树逐行在说什么？</strong></p><p><code>/dts-v1/;</code> 声明 DTS source format version 1。<code>/ { ... };</code> 是根节点，根节点没有 <code>@unit-address</code>。<code>model</code> 面向人，<code>compatible</code> 面向软件匹配；根节点的 <code>compatible</code> 也必须来自对应平台的约定，实验值只是实验值。</p><p><code>soc</code> 是一个总线节点。<code>ranges;</code> 是空属性，在常见的 <code>simple-bus</code> 写法里表示子总线地址空间与父地址空间相同。<code>uart0:</code> 是源码标签，只服务于 DTS 内部引用，标签本身不会作为标签字符串写入 DTB。</p><p><code>serial@10000000</code> 由两部分构成：通用节点名 <code>serial</code> 和 unit-address <code>10000000</code>。因为节点有 <code>reg</code>，unit-address 必须与 <code>reg</code> 中的第一个地址相匹配。节点名应使用 1 到 31 个合法字符，并以字母开头；具体总线 binding 还可能增加额外要求。</p></section>
<section><h2>DTS 语法的最小心智模型<a href="#dts-语法的最小心智模型"><span>#</span></a></h2><p>节点是树，属性是节点里的键值对。属性要写在子节点之前，子节点再继续向下展开。下面把常用值类型放在一处：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>empty</span><span>-</span><span>property;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>one</span><span>-</span><span>cell </span><span>=</span><span> </span><span>&lt;</span><span>42</span><span>&gt;</span><span>;</span></div></div><div><div><div>4</div></div><div><span>two</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span><span>0x</span><span>00000001</span></span><span> </span><span><span>0x</span><span>00000000</span></span><span>&gt;</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>one</span><span>-</span><span>string </span><span>=</span><span> </span><span>"hello"</span><span>;</span></div></div><div><div><div>7</div></div><div><span>string</span><span>-</span><span>list </span><span>=</span><span> </span><span>"example,device"</span><span>, </span><span>"example,bus"</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>bytes </span><span>=</span><span> [</span><span><span>0</span><span>0</span></span><span> </span><span>11</span><span> </span><span>22</span><span> aa];</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>尖括号里的每个 cell 是 32 位无符号整数。一个 64 位值由两个 cell 表示，高位在前。方括号表示 bytestring。多个逗号分隔的组件会拼接成同一个属性值。DTS 还支持 C 风格的算术、位运算和逻辑表达式，例如 <code>reg = &lt;(0x1000 + 0x200) 0x100&gt;;</code>。</p><p><code>reg</code> 不是一种到处一样的”地址加长度”语法。它的 cell 布局由<strong>父节点</strong>的 <code>#address-cells</code> 和 <code>#size-cells</code> 决定：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mmio</span><span>-</span><span>bus {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#address</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#size</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>device@</span><span>2000</span><span> {</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>reg </span><span>=</span><span> </span><span>&lt;</span><span><span>0x</span><span>2000</span></span><span> </span><span><span>0x</span><span>100</span></span><span>&gt;</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>8</div></div><div><span>};</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>i2c</span><span>-</span><span>bus {</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>#address</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>#size</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>0</span><span>&gt;</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>eeprom@</span><span>50</span><span> {</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>reg </span><span>=</span><span> </span><span>&lt;</span><span><span>0x</span><span>50</span></span><span>&gt;</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>17</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>在第一个例子里，每个条目是一个地址 cell 加一个长度 cell。第二个例子里，I2C 子设备的 <code>reg</code> 是从设备地址，父总线把 <code>#size-cells</code> 设为 0，所以没有长度 cell。不要从某块板子的数值推导普适规则；先看父节点，再看该总线和设备的 binding。</p><div><div><div></div><div>Important</div></div><div><p><strong><code>reg</code> 的关键规则。</strong> 永远先看父节点的 <code>#address-cells</code> 和 <code>#size-cells</code>，再按照对应 bus binding 解释 <code>reg</code>。I2C 从设备地址、MMIO 地址和 PCI 地址不是同一种语义。</p></div></div><p><code>ranges</code> 描述子总线地址空间到父总线地址空间的翻译。它不是 <code>reg</code> 的别名。<code>dma-ranges</code> 则描述 DMA 地址视角下的范围，是否需要它取决于平台的 DMA 地址转换。实际填写时必须查 binding 和平台 DTS。</p><p>标签和引用让设备树可以把物理层级与逻辑连接分开：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>intc: interrupt</span><span>-</span><span>controller {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>compatible </span><span>=</span><span> </span><span>"example,intc"</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>interrupt</span><span>-</span><span>controller;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>#interrupt</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>2</span><span>&gt;</span><span>;</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>serial@</span><span>10000000</span><span> {</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>interrupt</span><span>-</span><span>parent </span><span>=</span><span> </span><span>&lt;&amp;</span><span>intc</span><span>&gt;</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>interrupts </span><span>=</span><span> </span><span>&lt;</span><span>5</span><span> </span><span>4</span><span>&gt;</span><span>;</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>&amp;intc</code> 最终会被编码为 phandle 相关信息。<code>&amp;node { ... };</code> 是对已经定义节点的覆盖，板级 <code>.dts</code> 经常用它启用 SoC <code>.dtsi</code> 中默认禁用的外设：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&amp;</span><span>uart0 {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>status </span><span>=</span><span> </span><span>"okay"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>共享定义通常放进 <code>.dtsi</code>，再通过 <code>/include/ "soc.dtsi"</code> 引入。源码标签只存在于 DTS 语言；DTB 里保存的是供运行时解析的结构和 phandle 值。</p></section>
<section><h2>描述一个真实设备需要回答的问题<a href="#描述一个真实设备需要回答的问题"><span>#</span></a></h2><p>我写设备节点时会按下面的顺序检查，而不是看到属性名就往里塞：</p><ol>
<li><strong>设备是谁？</strong> 用 binding 规定的 <code>compatible</code>。列表通常从特化到通用，但只有当内核确实存在对应匹配项时，回退字符串才有意义。</li>
<li><strong>资源在哪里？</strong> 用该总线语义下的 <code>reg</code>、<code>ranges</code>、内存区域和时钟资源。</li>
<li><strong>设备怎么连？</strong> 用 phandle 引用表达时钟、reset、GPIO、DMA 和中断控制器。</li>
<li><strong>现在是否使用？</strong> 通常通过 binding 允许的 <code>status</code> 值表达，常见值是 <code>okay</code> 和 <code>disabled</code>。</li>
</ol><p>一个稍微完整的 SoC 节点可能长这样：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>soc {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>compatible </span><span>=</span><span> </span><span>"simple-bus"</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#address</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>#size</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>ranges;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>uart0: serial@</span><span>10000000</span><span> {</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>compatible </span><span>=</span><span> </span><span>"example,uart-v2"</span><span>, </span><span>"example,uart"</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>reg </span><span>=</span><span> </span><span>&lt;</span><span><span>0x</span><span>10000000</span></span><span> </span><span><span>0x</span><span>100</span></span><span>&gt;</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>interrupts</span><span>-</span><span>extended </span><span>=</span><span> </span><span>&lt;&amp;</span><span>intc </span><span>5</span><span> </span><span>4</span><span>&gt;</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>clocks </span><span>=</span><span> </span><span>&lt;&amp;</span><span>clk </span><span>3</span><span>&gt;</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>resets </span><span>=</span><span> </span><span>&lt;&amp;</span><span>reset </span><span>7</span><span>&gt;</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>pinctrl</span><span>-</span><span>names </span><span>=</span><span> </span><span>"default"</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>pinctrl</span><span>-</span><span>0</span><span> </span><span>=</span><span> </span><span>&lt;&amp;</span><span>uart0_pins</span><span>&gt;</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>dmas </span><span>=</span><span> </span><span>&lt;&amp;</span><span>dma </span><span>2</span><span>&gt;</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>status </span><span>=</span><span> </span><span>"okay"</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>18</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这段代码只是展示属性之间的关系，不是某个可直接提交的 Linux binding。<code>interrupts-extended</code> 的 cell 数量由被引用的中断控制器定义，<code>clocks</code>、<code>resets</code> 和 <code>dmas</code> 的参数也分别由 provider 的 <code>#*-cells</code> 和对应 binding 定义。设备树不是”把所有可能的属性都填上”竞赛。</p><p><code>compatible</code> 列表的顺序有意义：更特化的字符串在前，更通用的字符串在后。它不是承诺任意平台都存在一个通用回退驱动。正确流程是先查 Linux 内核 <code>Documentation/devicetree/bindings/</code> 下的 YAML，再决定字符串和必需属性。<code>make dtbs_check</code> 会用这些 schema 检查编译后的设备树，但 schema 检查通过也不等于硬件接线正确。</p><div><div><div></div><div>Warning</div></div><div><p><strong>不要凭感觉填写属性。</strong> <code>compatible</code>、中断 specifier、clock/reset/DMA 参数都由 binding 规定。<code>make dtbs_check</code> 通过，只说明描述符合 schema，不证明芯片接线、时钟或电源真的正确。</p></div></div></section>
<section><h2>I2C 设备和中断：两棵逻辑树<a href="#i2c-设备和中断两棵逻辑树"><span>#</span></a></h2><p>I2C 触摸控制器在设备树的物理拓扑中是 I2C 控制器的子节点，但它的中断线可能接到 GPIO 控制器，再由 GPIO 控制器连接到 GIC。于是”设备在哪”和”中断怎么走”是两种不同的关系。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&amp;</span><span>i2c0 {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>#address</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#size</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>0</span><span>&gt;</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>touch@</span><span>38</span><span> {</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>compatible </span><span>=</span><span> </span><span>"example,touch"</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>reg </span><span>=</span><span> </span><span>&lt;</span><span><span>0x</span><span>38</span></span><span>&gt;</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>interrupt</span><span>-</span><span>parent </span><span>=</span><span> </span><span>&lt;&amp;</span><span>gpio0</span><span>&gt;</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>interrupts </span><span>=</span><span> </span><span>&lt;</span><span>12</span><span> </span><span>8</span><span>&gt;</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>11</div></div><div><span>};</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>gpio0: gpio</span><span>-</span><span>controller {</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>compatible </span><span>=</span><span> </span><span>"example,gpio"</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>gpio</span><span>-</span><span>controller;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>#gpio</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>2</span><span>&gt;</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>interrupt</span><span>-</span><span>controller;</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>#interrupt</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>2</span><span>&gt;</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>interrupt</span><span>-</span><span>parent </span><span>=</span><span> </span><span>&lt;&amp;</span><span>gic</span><span>&gt;</span><span>;</span></div></div><div><div><div>20</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>interrupt-controller;</code> 是空属性，表示该节点提供中断域。<code>#interrupt-cells</code> 告诉消费者一个 interrupt specifier 有几个 cell，但具体每个 cell 的含义不能猜。ARM GIC、GPIO 控制器和其他中断域的数量可能不同。</p><p>简单设备通常写 <code>interrupt-parent</code> 和 <code>interrupts</code>。如果设备连接多个中断控制器，可以使用 <code>interrupts-extended</code>，它把每个中断控制器的 phandle 和对应 specifier 放在一起。复杂的 PCI host bridge 等 nexus 节点还会使用 <code>interrupt-map</code> 做域间翻译；入门阶段只需要记住：中断引用可以跨越设备树的物理父子关系，详细格式必须回到 binding。</p><p>GPIO、clock、reset、DMA 的写法也是同一类 phandle 思路：消费者引用 provider，provider 通过 <code>#gpio-cells</code> 等属性声明 specifier 的宽度。真正的 flags 和参数解释来自各自 binding，不来自属性名字的直觉。</p></section>
<section><h2>Linux 收到 DTB 后发生什么？<a href="#linux-收到-dtb-后发生什么"><span>#</span></a></h2><p>bootloader 把 DTB 地址交给内核后，Linux 解析扁平化设备树，建立自己的 device model。对于 <code>simple-bus</code> 下的 platform 设备，内核会根据节点创建设备对象；驱动注册时提供 <code>of_match_table</code>，其中的 <code>compatible</code> 与节点属性进行匹配。</p><p>匹配成功以后，驱动才会读取资源。常见的 platform driver 代码大致是：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>static</span><span> </span><span>const</span><span> </span><span>struct</span><span> of_device_id example_uart_of_match</span><span>[]</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>{ .compatible </span><span>=</span><span> </span><span>"example,uart-v2"</span><span> },</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>{ .compatible </span><span>=</span><span> </span><span>"example,uart"</span><span> },</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>{ }</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>example_uart_probe</span><span>(</span><span>struct</span><span> platform_device </span><span>*</span><span><span>pdev</span><span>)</span></span></div></div><div><div><div>8</div></div><div><span>{</span></div></div><div><div><div>9</div></div><div><span>    </span><span>struct</span><span> resource </span><span>*</span><span>mem;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>void</span><span> __iomem </span><span>*</span><span>base;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>int</span><span> irq;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>mem </span><span>=</span><span> </span><span>platform_get_resource</span><span>(pdev, IORESOURCE_MEM, </span><span>0</span><span>);</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>base </span><span>=</span><span> </span><span>devm_ioremap_resource</span><span>(</span><span>&amp;</span><span>pdev</span><span>-&gt;</span><span>dev</span><span>, mem);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>if</span><span> (</span><span>IS_ERR</span><span>(base))</span></div></div><div><div><div>16</div></div><div><span>        </span><span>return</span><span> </span><span>PTR_ERR</span><span>(base);</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>irq </span><span>=</span><span> </span><span>platform_get_irq</span><span>(pdev, </span><span>0</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>if</span><span> (irq </span><span>&lt;</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>20</div></div><div><span>        </span><span>return</span><span> irq;</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span> </span><span>devm_request_irq</span><span>(</span><span>&amp;</span><span>pdev</span><span>-&gt;</span><span>dev</span><span>, irq, example_uart_irq,</span></div></div><div><div><div>23</div></div><div><span>                            </span><span>0</span><span>, </span><span>dev_name</span><span>(</span><span>&amp;</span><span>pdev</span><span>-&gt;</span><span>dev</span><span>), pdev);</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里的关键不是背 API，而是看清边界：设备树提供资源描述，驱动通过 Linux API 取得并使用资源。<code>devm_ioremap_resource()</code> 不会验证地址背后真的焊了一块 UART；它只负责在内核资源模型允许的前提下建立映射。<code>devm_request_irq()</code> 也不替你修复接错的中断线。</p><div><div><div></div><div>Note</div></div><div><p><strong>排错边界。</strong> 设备树描述”有什么”和”资源在哪里”，驱动决定”怎么操作”。匹配失败、资源获取失败和硬件本身不工作，应分别从 <code>compatible</code>、资源属性以及时钟/复位/pinmux/接线排查。</p></div></div><p>因此排错时要区分三类失败：</p><ul>
<li><code>compatible</code> 不匹配：设备可能根本没有进入目标驱动的 <code>probe()</code>。</li>
<li><code>reg</code>、clock、reset 或中断描述错误：驱动进入 <code>probe()</code>，但获取资源失败。</li>
<li>DTS 和 binding 都正确，硬件仍不工作：继续检查时钟、复位、pinmux、电源和真实接线。</li>
</ul></section>
<section><h2>系统节点：不要把惯例写成绝对规则<a href="#系统节点不要把惯例写成绝对规则"><span>#</span></a></h2><p>完整系统描述通常还会出现这些节点：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>chosen {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>stdout</span><span>-</span><span>path </span><span>=</span><span> </span><span>&amp;</span><span>uart0;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>aliases {</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>serial0 </span><span>=</span><span> </span><span>&amp;</span><span>uart0;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>cpus {</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>#address</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>1</span><span>&gt;</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>#size</span><span>-</span><span>cells </span><span>=</span><span> </span><span>&lt;</span><span>0</span><span>&gt;</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>cpu@</span><span>0</span><span> {</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>device_type </span><span>=</span><span> </span><span>"cpu"</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>            </span></span><span>reg </span><span>=</span><span> </span><span>&lt;</span><span>0</span><span>&gt;</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>memory@</span><span>80000000</span><span> {</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>device_type </span><span>=</span><span> </span><span>"memory"</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>reg </span><span>=</span><span> </span><span>&lt;</span><span><span>0x</span><span>80000000</span></span><span> </span><span><span>0x</span><span>40000000</span></span><span>&gt;</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>24</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>/chosen</code> 是 boot program 向 client program 传递运行时选择的地方，例如 <code>bootargs</code> 和 <code>stdout-path</code>。<code>/aliases</code> 为长路径提供短名字。<code>/cpus</code> 描述 CPU 集合，<code>/memory</code> 描述可用 RAM。某种架构和启动方式可能要求这些节点，但不能脱离上下文说所有设备树在任何场景都必须有完全相同的节点。</p><p><code>/reserved-memory</code> 节点和 FDT 的 memory reservation block 也不是同一个东西。前者是设备树结构里的内存区域描述，常用于 CMA、framebuffer 或固件共享内存；后者是 DTB 二进制中的底层保留表，boot program 可以用它避免 client program 覆写仍在使用的区域。两者都和”这段内存不能随便拿来用”有关，但出现的阶段和表达层次不同。</p></section>
<section><h2>从 Linux 现场反查设备树<a href="#从-linux-现场反查设备树"><span>#</span></a></h2><p>拿到一台正在运行的 Linux 后，我会按这个顺序排查，而不是先盯着驱动源码：</p><ol>
<li>
<p>查看内核实际展开的设备树：</p>
<div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ls</span><span> </span><span>/sys/firmware/devicetree/base</span></div></div><div><div><div>2</div></div><div><span>ls</span><span> </span><span>/proc/device-tree</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
<p>两个路径通常指向内核提供的设备树视图，具体挂载方式取决于系统配置。</p>
</li>
<li>
<p>查看某个节点的属性。字符串属性以 NUL 结尾，不能总用普通文本工具直读：</p>
<div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>tr</span><span> </span><span>-d</span><span> </span><span>'\0'</span><span> &lt; </span><span>/sys/firmware/devicetree/base/model</span></div></div><div><div><div>2</div></div><div><span>printf</span><span> </span><span>'\n'</span></div></div><div><div><div>3</div></div><div><span>xxd</span><span> </span><span>/sys/firmware/devicetree/base/soc/serial@10000000/reg</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
</li>
<li>
<p>查看驱动和设备的启动日志：</p>
<div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>dmesg</span><span> | </span><span>grep</span><span> </span><span>-i</span><span> </span><span>-E</span><span> </span><span>'of|firmware|uart|probe'</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
</li>
<li>
<p>如果手里只有 DTB，先反编译，再把结果和板级 <code>.dts</code>、SoC <code>.dtsi</code> 以及对应 YAML binding 对照。</p>
</li>
<li>
<p>在 Linux 内核源码树中运行 schema 检查：</p>
<div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>make</span><span> </span><span>dtbs_check</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
</li>
</ol><p><code>dtbs_check</code> 主要检查设备树是否符合 binding schema。它不能替代 <code>dtc</code> 的语法检查，也不能替代示波器、逻辑分析仪或一双确认过连接器的眼睛。</p><div><div><div></div><div>Tip</div></div><div><p><strong>验证顺序。</strong> 先看 Linux 实际展开的设备树，再看 <code>dmesg</code>，然后对照 DTB、板级 DTS 和 binding。<code>dtbs_check</code> 是约束检查，不是硬件功能测试。</p></div></div></section>
<section><h2>DTB 为什么可以被搬到别的地址？<a href="#dtb-为什么可以被搬到别的地址"><span>#</span></a></h2><p>DTS 是源码，DTB 是给 client program 消费的紧凑二进制表示。FDT 的设计目标之一是让 boot program 能把它放入内存并传递给下一个程序，因此结构内部使用偏移和长度，而不是依赖加载地址的 C 裸指针。</p><p>DTB 头部包含 magic、总大小，以及 memory reservation block、structure block 和 strings block 的偏移与大小等字段。字段按规范使用大端编码。解析器应该读取这些字段，而不是假设某个区块永远位于固定位置。</p><p>structure block 把树线性化为 token 序列，常见 token 包括：</p><ul>
<li><code>FDT_BEGIN_NODE</code>：进入节点，后面跟节点名。</li>
<li><code>FDT_END_NODE</code>：离开节点。</li>
<li><code>FDT_PROP</code>：属性，包含长度、属性名在 strings block 中的偏移和属性值。</li>
<li><code>FDT_NOP</code>：保留的空操作 token。</li>
<li><code>FDT_END</code>：structure block 结束。</li>
</ul><p>属性名集中存放在 strings block，<code>FDT_PROP</code> 用偏移引用它们。节点和属性数据按 4 字节边界对齐；memory reservation entry 使用 64 位地址和大小。这里的数字和布局规则来自 FDT 结构规范及其头部定义，不应该简化成”固定 40 字节所以永远如此”。</p><p>我第一次看到这套布局时，感觉它像一个没有指针的压缩内存快照：解析器带着 offset 在几个区块之间走，遇到 begin/end token 用栈恢复树。这个设计牺牲了一点随机访问的舒服感，却换来了可复制、可重定位和早期启动阶段可解析的格式，算是很合理的嵌入式取舍。</p></section>
<section><h2>下一次动手做什么<a href="#下一次动手做什么"><span>#</span></a></h2><p>如果你已经能读懂上面的最小例子，下一步不要继续收藏属性清单。找一块真实开发板，按下面的路径走一遍：</p><ol>
<li>找到它的板级 <code>.dts</code> 和 SoC <code>.dtsi</code>。</li>
<li>沿着节点路径确认父节点的 <code>#address-cells</code>、<code>#size-cells</code> 和 <code>ranges</code>。</li>
<li>在内核 <code>Documentation/devicetree/bindings/</code> 中找到设备对应的 YAML。</li>
<li>只修改一个 <code>status</code> 或一个资源属性，重新编译 DTB。</li>
<li>用 <code>dtc</code> 反编译产物，确认修改确实进入 DTB。</li>
<li>启动 Linux，从 <code>/sys/firmware/devicetree/base</code> 和 <code>dmesg</code> 确认内核实际拿到的内容。</li>
</ol><p>未来同一份硬件描述继续被不同 client program 使用并不奇怪，但这不意味着设备树自动带来跨平台兼容。真正可移植的是清晰的描述接口；地址、时钟、中断和 binding 仍然属于具体硬件。至于我自己，下一步大概会挑一个真实 GPIO binding 做实验。先把一盏灯点亮，再考虑宇宙级抽象，yolo。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ol>
<li><a href="https://devicetree-specification.readthedocs.io/en/stable/intro.html" target="_blank">Devicetree Specification: Introduction</a></li>
<li><a href="https://devicetree-specification.readthedocs.io/en/stable/devicetree-basics.html" target="_blank">Devicetree Specification: The Devicetree</a></li>
<li><a href="https://devicetree-specification.readthedocs.io/en/stable/flattened-format.html" target="_blank">Devicetree Specification: Flattened Devicetree</a></li>
<li><a href="https://devicetree-specification.readthedocs.io/en/stable/devicetree-source.html" target="_blank">Devicetree Specification: Devicetree Source Format</a></li>
<li><a href="https://docs.kernel.org/devicetree/usage-model.html" target="_blank">Linux Kernel Documentation: Linux and the Devicetree</a></li>
<li><a href="https://docs.kernel.org/devicetree/bindings/index.html" target="_blank">Linux Kernel Documentation: Devicetree Bindings</a></li>
<li><a href="https://git.kernel.org/pub/scm/utils/dtc/dtc.git/" target="_blank">Device Tree Compiler 官方仓库</a></li>
<li><a href="https://docs.kernel.org/devicetree/" target="_blank">Linux 内核 dtc 文档入口</a></li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/bibtex-and-biblatex-guide/</id>
      <title type="text">BibTeX 与 BibLaTeX 入门：.bib 文件结构、引用与编译流程</title>
      <published>2026-05-19T14:00:00.000Z</published>
      <updated>2026-05-19T14:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/bibtex-and-biblatex-guide/"/>
      <summary type="text">从 .bib 条目结构出发，结合 BibTeX 与 BibLaTeX 两种工作流，梳理引用写法、编译顺序和常见注意事项。</summary>
      <content type="html"><![CDATA[<p>最近恰逢本科阶段的倒数第二个课程设计，初步估计又得三四十页，并且老师不发模板只发排版要求，美其名曰让我们提前适应毕业论文写作（但是我记得毕业论文也是有 Word 模板的吧😂）。</p>
<p>我并非是某一方的狂热爱好者，在我看来两者都是工具，只要能解决我的问题，那就是好工具。显然这种比较专业的排版需求还是使用 LaTeX 比较合适，而我发现我对 LaTeX 的引用数据库还是不太了解，就大概学习了一并下整理了这篇博客，不一定足够专业，但是应该能够应付我的课程设计了。</p>
<section><h2>1. <code>.bib</code> 文件的基本结构<a href="#1-bib-文件的基本结构"><span>#</span></a></h2><p><code>.bib</code> 文件可以理解成一个文献数据库，由多条文献条目组成。每条条目一般是下面这种形式：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@entrytype</span><span>{</span><span>citekey</span><span>,</span></div></div><div><div><div>2</div></div><div><span>  </span><span>field1</span><span> = </span><span>{</span><span>value1</span><span>}</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>field2</span><span> = </span><span>{</span><span>value2</span><span>}</span><span>,</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>...</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>@entrytype</code>：文献类型，例如 <code>@article</code>、<code>@book</code>、<code>@inproceedings</code>。</li>
<li><code>citekey</code>：引用键（citation key），在正文中通过 <code>\cite{citekey}</code> 调用。</li>
<li><code>field = {value}</code>：字段和值，如 <code>author</code>、<code>title</code>、<code>year</code>。</li>
</ul><section><h3>1.1 常见条目类型与常用字段<a href="#11-常见条目类型与常用字段"><span>#</span></a></h3>

<table><thead><tr><th>条目类型</th><th>说明</th><th>常用字段</th></tr></thead><tbody><tr><td><code>@article</code></td><td>期刊论文</td><td><code>author</code>, <code>title</code>, <code>journal</code>, <code>year</code>, <code>volume</code>, <code>number</code>, <code>pages</code>, <code>doi</code></td></tr><tr><td><code>@inproceedings</code></td><td>会议论文</td><td><code>author</code>, <code>title</code>, <code>booktitle</code>, <code>year</code>, <code>pages</code>, <code>organization</code></td></tr><tr><td><code>@book</code></td><td>书籍</td><td><code>author</code> 或 <code>editor</code>, <code>title</code>, <code>publisher</code>, <code>year</code></td></tr><tr><td><code>@phdthesis</code> / <code>@mastersthesis</code></td><td>学位论文</td><td><code>author</code>, <code>title</code>, <code>school</code>, <code>year</code></td></tr><tr><td><code>@misc</code> / <code>@online</code>（BibLaTeX）</td><td>网页、预印本</td><td><code>author</code>, <code>title</code>, <code>year</code>, <code>howpublished</code> 或 <code>url</code>, <code>note</code></td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p>不同条目在 BibTeX 与 BibLaTeX 中的必填字段会有差异，建议以所用样式文档为准。可以通过 <code>texdoc biblatex</code> 或 <code>texdoc bibtex</code> 查看官方说明。</p></div></div><hr /></section></section>
<section><h2>2. 一个最小可用的 <code>.bib</code> 示例<a href="#2-一个最小可用的-bib-示例"><span>#</span></a></h2><p>假设你新建一个文件 <code>references.bib</code>，内容如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@article</span><span>{</span><span>einstein1905</span><span>,</span></div></div><div><div><div>2</div></div><div><span>  </span><span>author</span><span>  = </span><span>{</span><span>Albert Einstein</span><span>}</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>title</span><span>   = </span><span>{</span><span>On the Electrodynamics of Moving Bodies</span><span>}</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>journal</span><span> = </span><span>{</span><span>Annalen der Physik</span><span>}</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>year</span><span>    = </span><span>{</span><span>1905</span><span>}</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>volume</span><span>  = </span><span>{</span><span>17</span><span>}</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>pages</span><span>   = </span><span>{</span><span>891--921</span><span>}</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>@book</span><span>{</span><span>knuth1984</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>author</span><span>    = </span><span>{</span><span>Donald E. Knuth</span><span>}</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>title</span><span>     = </span><span>{</span><span>The {TeX}book</span><span>}</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>publisher</span><span> = </span><span>{</span><span>Addison-Wesley</span><span>}</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>year</span><span>      = </span><span>{</span><span>1984</span><span>}</span><span>,</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>@inproceedings</span><span>{</span><span>he2016resnet</span><span>,</span></div></div><div><div><div>18</div></div><div><span>  </span><span>author</span><span>    = </span><span>{</span><span>Kaiming He and Xiangyu Zhang and Shaoqing Ren and Jian Sun</span><span>}</span><span>,</span></div></div><div><div><div>19</div></div><div><span>  </span><span>title</span><span>     = </span><span>{</span><span>Deep Residual Learning for Image Recognition</span><span>}</span><span>,</span></div></div><div><div><div>20</div></div><div><span>  </span><span>booktitle</span><span> = </span><span>{</span><span>Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR)</span><span>}</span><span>,</span></div></div><div><div><div>21</div></div><div><span>  </span><span>year</span><span>      = </span><span>{</span><span>2016</span><span>}</span><span>,</span></div></div><div><div><div>22</div></div><div><span>  </span><span>pages</span><span>     = </span><span>{</span><span>770--778</span><span>}</span><span>,</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个常用写法：</p><ul>
<li><strong>多个作者用 <code>and</code> 分隔</strong>（BibTeX/BibLaTeX 会自动识别作者列表）：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>author = {First Author and Second Author and Third Author},</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>页码用 <code>--</code> 而不是 <code>-</code></strong>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pages = {770--778},</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>大小写保护</strong>：专有名词或缩写（如 <code>TeX</code>、<code>CNN</code>）可用 <code>{}</code> 包起来，避免被样式改写：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>title = {Deep Residual Learning for {CNN} Image Recognition},</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>3. 在 LaTeX 里如何使用 .bib（两种主流工作流）<a href="#3-在-latex-里如何使用-bib两种主流工作流"><span>#</span></a></h2><ol>
<li><strong>传统路线</strong>：LaTeX + BibTeX</li>
<li><strong>现代路线</strong>：LaTeX + BibLaTeX + biber</li>
</ol><hr /><section><h3>3.1 传统：LaTeX + BibTeX 工作流<a href="#31-传统latex--bibtex-工作流"><span>#</span></a></h3><p><strong>文档示例（<code>main.tex</code>）：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\documentclass</span><span>{</span><span>article</span><span>}</span></div></div><div><div><div>2</div></div><div><span>\usepackage</span><span><span>[</span><span>utf8</span><span>]{</span></span><span>inputenc</span><span>} </span><span>% 使用 XeLaTeX/LuaLaTeX 时通常不需要</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>\begin</span><span><span>{</span><span>document</span><span>}</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>这是正文中的一条引用 </span><span>\cite</span><span>{</span><span>einstein1905</span><span>}，再来一条 </span><span>\cite</span><span>{</span><span>knuth1984</span><span>}。</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>\bibliographystyle</span><span>{ieeetr}   </span><span>% 选择参考文献样式，如 plain, unsrt, alpha, ieeetr 等</span></div></div><div><div><div>9</div></div><div><span>\bibliography</span><span>{references}    </span><span>% 不写 .bib 后缀</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>\end</span><span><span>{</span><span>document</span><span>}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>编译顺序（非常关键）：</strong></p><ol>
<li><code>pdflatex main</code></li>
<li><code>bibtex main</code></li>
<li><code>pdflatex main</code></li>
<li><code>pdflatex main</code></li>
</ol><p>很多编辑器可以自动完成这些步骤，但底层顺序仍然是这 4 步。</p><p><strong>特点</strong>：</p><ul>
<li><strong>优点</strong>：历史悠久、兼容性好，很多旧模板和投稿模板仍要求 BibTeX。</li>
<li><strong>缺点</strong>：对 UTF-8、多语种姓名支持相对有限；<code>.bst</code> 样式定制门槛较高；对 URL、DOI、在线资源的处理不如 BibLaTeX 灵活。</li>
</ul><hr /></section><section><h3>3.2 现代：BibLaTeX + biber 工作流<a href="#32-现代biblatex--biber-工作流"><span>#</span></a></h3><p><strong>文档示例（<code>main.tex</code>）：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\documentclass</span><span>{</span><span>article</span><span>}</span></div></div><div><div><div>2</div></div><div><span>\usepackage</span><span><span>[</span><span>utf8</span><span>]{</span></span><span>inputenc</span><span>} </span><span>% 使用 XeLaTeX/LuaLaTeX 时可省略</span></div></div><div><div><div>3</div></div><div><span>\usepackage</span><span><span>[</span><span>backend=biber,style=authoryear,sorting=nyt</span><span>]{</span></span><span>biblatex</span><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>\addbibresource</span><span>{references.bib} </span><span>% 指定 .bib 文件</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>\begin</span><span><span>{</span><span>document</span><span>}</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>这是正文中的一条引用 </span><span>\parencite</span><span>{</span><span>einstein1905</span><span>}，</span></div></div><div><div><div>10</div></div><div><span>以及一条行文引用 </span><span>\textcite</span><span>{</span><span>knuth1984</span><span>}。</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>\printbibliography</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>\end</span><span><span>{</span><span>document</span><span>}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>**<code>backend=biber</code>：**指定后端为 <code>biber</code>（而不是 <code>bibtex</code>）。</li>
<li>**<code>style=authoryear</code>：**作者-年份格式（如 “Einstein 1905”），也可改为 <code>numeric</code>、<code>ieee</code> 等。</li>
<li>**<code>sorting=nyt</code>：**按 <code>name-year-title</code> 排序。</li>
</ul><p><strong>编译顺序：</strong></p><ol>
<li><code>pdflatex main</code></li>
<li><code>biber main</code></li>
<li><code>pdflatex main</code></li>
<li><code>pdflatex main</code></li>
</ol><p>如果你用的是 XeLaTeX/LuaLaTeX，则把 <code>pdflatex</code> 换成 <code>xelatex</code> 或 <code>lualatex</code> 即可。</p><p><strong>特点</strong>：</p><ul>
<li><strong>优点</strong>：对 UTF-8 与多语言支持更好；字段更丰富（如 <code>@online</code>、<code>url</code>、<code>doi</code>、<code>eprint</code>）；样式可通过 LaTeX 宏定制，可维护性更高；支持按章节、按类型分组等高级功能。</li>
<li><strong>缺点</strong>：某些期刊/会议模板仍明确要求 BibTeX。</li>
</ul><hr /></section></section>
<section><h2>4. 该如何选择？<a href="#4-该如何选择"><span>#</span></a></h2><div><div><div></div><div>Important</div></div><div><p>如果模板没有明确限制，通常可以优先考虑 <code>XeLaTeX/LuaLaTeX + BibLaTeX + biber</code>。</p></div></div><hr /></section>
<section><h2>5. 写 <code>.bib</code> 时的一些实用小技巧<a href="#5-写-bib-时的一些实用小技巧"><span>#</span></a></h2><ul>
<li><strong>优先从数据库导出 BibTeX</strong>：例如 Google Scholar、arXiv、ACM、IEEE、Springer，可减少手写错误。</li>
<li><strong>统一命名规则（citekey）</strong>：</li>
</ul><p>比如 <code>authorYearShortTitle</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>vaswani2017attention</span></div></div><div><div><div>2</div></div><div><span>he2016resnet</span></div></div><div><div><div>3</div></div><div><span>knuth1984texbook</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>中文文献的处理</strong>：在 BibLaTeX + biber + UTF-8 工作流中，可直接使用中文 <code>title</code>、<code>author</code>；若投稿要求英文参考文献，可将英文信息写入主要字段，中文信息放在 <code>note</code> 或 <code>language</code>。</li>
<li><strong>注意特殊字符转义</strong>：<code>&amp;</code>, <code>%</code>, <code>_</code>, <code>#</code> 等应转义为 <code>\&amp;</code>, <code>\%</code>, <code>\_</code>, <code>\#</code>。</li>
</ul><hr /></section>
<section><h2>6. 完整示例（BibLaTeX + biber）<a href="#6-完整示例biblatex--biber"><span>#</span></a></h2><p>下面给出一套可直接实验的最小流程。</p><section><h3>6.1 新建 <code>references.bib</code><a href="#61-新建-referencesbib"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@article</span><span>{</span><span>vaswani2017attention</span><span>,</span></div></div><div><div><div>2</div></div><div><span>  </span><span>author</span><span>  = </span><span>{</span><span>Ashish Vaswani and Noam Shazeer and Niki Parmar and Jakob Uszkoreit</span></div></div><div><div><div>3</div></div><div><span><span>             </span></span><span>and Llion Jones and Aidan N. Gomez and Lukasz Kaiser and Illia Polosukhin</span><span>}</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>title</span><span>   = </span><span>{</span><span>Attention Is All You Need</span><span>}</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>journal</span><span> = </span><span>{</span><span>Advances in Neural Information Processing Systems</span><span>}</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>volume</span><span>  = </span><span>{</span><span>30</span><span>}</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>year</span><span>    = </span><span>{</span><span>2017</span><span>}</span><span>,</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>@book</span><span>{</span><span>knuth1984texbook</span><span>,</span></div></div><div><div><div>11</div></div><div><span>  </span><span>author</span><span>    = </span><span>{</span><span>Donald E. Knuth</span><span>}</span><span>,</span></div></div><div><div><div>12</div></div><div><span>  </span><span>title</span><span>     = </span><span>{</span><span>The {TeX}book</span><span>}</span><span>,</span></div></div><div><div><div>13</div></div><div><span>  </span><span>publisher</span><span> = </span><span>{</span><span>Addison-Wesley</span><span>}</span><span>,</span></div></div><div><div><div>14</div></div><div><span>  </span><span>year</span><span>      = </span><span>{</span><span>1984</span><span>}</span><span>,</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>6.2 新建 <code>main.tex</code><a href="#62-新建-maintex"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>\documentclass</span><span>{</span><span>article</span><span>}</span></div></div><div><div><div>2</div></div><div><span>\usepackage</span><span><span>[</span><span>utf8</span><span>]{</span></span><span>inputenc</span><span>}</span></div></div><div><div><div>3</div></div><div><span>\usepackage</span><span><span>[</span><span>backend=biber,style=authoryear,sorting=nyt</span><span>]{</span></span><span>biblatex</span><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>\addbibresource</span><span>{references.bib}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>\begin</span><span><span>{</span><span>document</span><span>}</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>这是对 Transformer 的引用 </span><span>\parencite</span><span>{</span><span>vaswani2017attention</span><span>}，</span></div></div><div><div><div>10</div></div><div><span>这是对 </span><span>\textcite</span><span>{</span><span>knuth1984texbook</span><span>} 的引用。</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>\printbibliography</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>\end</span><span><span>{</span><span>document</span><span>}</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>6.3 编译顺序<a href="#63-编译顺序"><span>#</span></a></h3><ol>
<li><code>pdflatex main</code></li>
<li><code>biber main</code></li>
<li><code>pdflatex main</code></li>
<li><code>pdflatex main</code></li>
</ol><p>如果你用的是 XeLaTeX，就把 <code>pdflatex</code> 换成 <code>xelatex</code>。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/note-of-tmux/</id>
      <title type="text">Tmux 使用笔记</title>
      <published>2026-04-25T13:55:00.000Z</published>
      <updated>2026-04-25T13:55:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/note-of-tmux/"/>
      <summary type="text">理解 tmux 的 C/S 架构和 Session、Window、Pane 的三层模型，快速掌握日常工作中最常用的快捷键，提升远程开发效率。</summary>
      <content type="html"><![CDATA[<section><h2>为什么我们需要 tmux？<a href="#为什么我们需要-tmux"><span>#</span></a></h2><section><h3>1. 终端的物理与逻辑基础<a href="#1-终端的物理与逻辑基础"><span>#</span></a></h3><p>在计算机世界里，我们通过 “终端（Terminal）” 与 “Shell（如 bash, zsh）” 进行交互。</p><p>当你打开一个终端窗口，或者通过 SSH 连接到远程服务器时，操作系统为你分配了一个 <strong>TTY（伪终端）</strong>，并在这个 TTY 上启动了一个 Shell 进程。你在终端里运行的所有程序（跑脚本、开服务器、编辑代码），都是这个 Shell 的<strong>子进程</strong>。</p></section><section><h3>2. 致命的耦合（痛点所在）<a href="#2-致命的耦合痛点所在"><span>#</span></a></h3><p>这就产生了一个致命的 “生命周期耦合” 问题：</p><ul>
<li><strong>规则</strong>：当你的终端窗口关闭，或者由于网络波动导致 SSH 连接断开时，系统会向该 TTY 上的 Shell 发送一个 <code>SIGHUP</code>（挂断信号）。</li>
<li><strong>结果</strong>：Shell 收到信号后会立即终止，并带着它所有的子进程一起退出。</li>
<li><strong>痛点</strong>：如果你正在服务器上训练一个需要跑 3 天的深度学习模型，或者正在编译一个巨大的项目，只要你的网络断开一秒钟，或者你不小心关掉了窗口，前功尽弃。</li>
</ul></section><section><h3>3. 需求推演<a href="#3-需求推演"><span>#</span></a></h3><p>为了解决这个问题，我们需要在 “显示界面（你的屏幕/SSH连接）” 和 “后台进程（Shell 及你的任务）” 之间，插入一个<strong>中间层</strong>。</p><p>这个中间层需要具备两个核心能力：</p><ol>
<li><strong>进程解耦（持久化）</strong>：它可以接管所有的 Shell 进程，并且无视网络断开和窗口关闭，永远在后台运行。</li>
<li><strong>多路复用（分屏）</strong>：既然它接管了终端，那它最好能把一个屏幕切分成多个区域，让我们可以同时看代码、看日志、跑服务，而不需要开一堆 SSH 连接。</li>
</ol><p>这就是 <strong>tmux</strong> 诞生的原因。</p></section></section>
<section><h2>tmux 是什么？（核心架构）<a href="#tmux-是什么核心架构"><span>#</span></a></h2><p><strong>tmux (Terminal Multiplexer，终端复用器)</strong> 就是我们刚刚推导出的那个“中间层”。</p><p>它的底层本质是一个 <strong>C/S 架构（Client-Server）</strong>：</p><ul>
<li><strong>Server（服务端）</strong>：当你第一次运行 tmux 时，它会在后台默默启动一个 Server。这个 Server 负责管理你所有的运行环境。只要服务器不重启，它就一直活着。</li>
<li><strong>Client（客户端）</strong>：当你在终端敲下 <code>tmux</code> 时，你实际上是启动了一个 Client，连接到了后台的 Server。把 Client 关掉，只是断开了 “显示器”，后台的 Server 和进程依旧在运转。</li>
</ul><section><h3>tmux 的层级逻辑（非常重要）<a href="#tmux-的层级逻辑非常重要"><span>#</span></a></h3><p>为了方便管理，tmux 将你的工作区抽象为三个层级：</p><ol>
<li><strong>Session（会话）</strong>：最大的单位。一个 Session 通常代表一个 “项目”。比如你可以建一个名叫 <code>work</code> 的 Session，里面跑你的代码；再建一个名叫 <code>download</code> 的 Session，里面专门用来挂机下载。</li>
<li><strong>Window（窗口）</strong>：Session 里面的 “标签页”。类似于浏览器的 Tab，一个屏幕只能显示一个 Window。</li>
<li><strong>Pane（窗格）</strong>：Window 里的 “分屏”。你可以把一个 Window 切分成左边看代码、右边看日志。</li>
</ol><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>🖥️ Tmux Server<br /><br />(后台进程，永不宕机)</p></span></div><div><span><p>📁 Session: work<br /><br />(项目 1)</p></span></div><div><span><p>📁 Session: download<br /><br />(项目 2)</p></span></div><div><span><p>📑 Window 1<br /><br />(编辑代码)</p></span></div><div><span><p>📑 Window 2<br /><br />(运行测试)</p></span></div><div><span><p>📑 Window 3<br /><br />(下载管理)</p></span></div><div><span><p>🖼️ Pane 1.1</p></span></div><div><span><p>🖼️ Pane 1.2</p></span></div><div><span><p>🖼️ Pane 1.3</p></span></div><div><span><p>🖼️ Pane 2.1</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>🖥️ Tmux Server<br /><br />(后台进程，永不宕机)</p></span></div><div><span><p>📁 Session: work<br /><br />(项目 1)</p></span></div><div><span><p>📁 Session: download<br /><br />(项目 2)</p></span></div><div><span><p>📑 Window 1<br /><br />(编辑代码)</p></span></div><div><span><p>📑 Window 2<br /><br />(运行测试)</p></span></div><div><span><p>📑 Window 3<br /><br />(下载管理)</p></span></div><div><span><p>🖼️ Pane 1.1</p></span></div><div><span><p>🖼️ Pane 1.2</p></span></div><div><span><p>🖼️ Pane 1.3</p></span></div><div><span><p>🖼️ Pane 2.1</p></span></div>
</div></div></div></section></section>
<section><h2>tmux 的使用方法<a href="#tmux-的使用方法"><span>#</span></a></h2><p>理解了上面的逻辑，使用 tmux 就只是死记硬背几个命令而已了。</p><section><h3>1. 前置概念：Prefix Key（前缀键）<a href="#1-前置概念prefix-key前缀键"><span>#</span></a></h3><p>在 tmux 中，所有的快捷键都必须配合一个<strong>前缀键</strong>使用，默认是 <code>Ctrl + b</code>。</p><p><strong>操作逻辑：</strong> 先同时按下 <code>Ctrl</code> 和 <code>b</code>，<strong>松开双手</strong>，然后再按下一个键。</p><div><div><div></div><div>Note</div></div><div><p><em>(下文中我将 <code>Ctrl + b</code> 简称为 <code>Prefix</code>)</em></p></div></div></section><section><h3>2. Session 管理（在普通的 Linux 终端打字）<a href="#2-session-管理在普通的-linux-终端打字"><span>#</span></a></h3><p>这是你和 Server 交互的命令：</p><ul>
<li><strong>新建会话</strong>：<code>tmux new -s myproject</code> （新建一个名为 myproject 的会话并进入）</li>
<li><strong>脱离会话（后台运行）：</strong> <code>Prefix</code> 然后按 <code>d</code> (detach)。你的界面会退回普通终端，但里面的程序还在跑！</li>
<li><strong>查看现有会话</strong>：<code>tmux ls</code></li>
<li><strong>重新连接会话</strong>：<code>tmux attach -t myproject</code> (attach)</li>
<li><strong>彻底杀死会话</strong>：<code>tmux kill-session -t myproject</code></li>
</ul></section><section><h3>3. Window 管理（在 tmux 内部使用快捷键）<a href="#3-window-管理在-tmux-内部使用快捷键"><span>#</span></a></h3><ul>
<li><strong>新建窗口</strong>：<code>Prefix</code> 然后按 <code>c</code> (create)</li>
<li><strong>切换窗口</strong>：<code>Prefix</code> 然后按 <code>n</code> (next，下一个)
<ul>
<li><code>Prefix</code> 然后按 <code>p</code> (previous，上一个)</li>
<li><code>Prefix</code> 然后按 <code>数字键</code> (直接跳转到指定窗口)</li>
</ul>
</li>
<li><strong>关闭当前窗口</strong>：<code>Prefix</code> 然后按 <code>&amp;</code> (会弹出确认提示)</li>
</ul></section><section><h3>4. Pane 管理（在 tmux 内部使用快捷键）<a href="#4-pane-管理在-tmux-内部使用快捷键"><span>#</span></a></h3><p>这是日常写代码最常用的分屏功能：</p><ul>
<li><strong>左右分屏</strong>：<code>Prefix</code> 然后按 <code>%</code></li>
<li><strong>上下分屏</strong>：<code>Prefix</code> 然后按 <code>"</code> (双引号)</li>
<li><strong>在分屏间移动光标</strong>：<code>Prefix</code> 然后按 <code>方向键</code> (上下左右)</li>
<li><strong>全屏/取消全屏当前分屏</strong>：<code>Prefix</code> 然后按 <code>z</code> (zoom，非常实用，看长报错信息时按一下放大，看完再按一下缩小)</li>
<li><strong>关闭当前分屏</strong>：<code>Prefix</code> 然后按 <code>x</code> 或者直接输入 <code>exit</code></li>
</ul></section></section>
<section><h2>进阶：让 tmux 变得更好用<a href="#进阶让-tmux-变得更好用"><span>#</span></a></h2><p>可以参考开源项目 <a href="https://github.com/gpakosz/.tmux" target="_blank">gpakosz/.tmux</a>，这里仅列举出开源项目中使用的快捷键绑定。</p><section><h3>格式说明<a href="#格式说明"><span>#</span></a></h3><ul>
<li><code>&lt;prefix&gt;</code> 表示需要按下 <kbd>Ctrl</kbd> + <kbd>a</kbd> 或者 <kbd>Ctrl</kbd> + <kbd>b</kbd>。</li>
<li><code>&lt;prefix&gt; c</code> 表示需要按下 <kbd>Ctrl</kbd> + <kbd>a</kbd> 或者 <kbd>Ctrl</kbd> + <kbd>b</kbd> 加上 <kbd>c</kbd>。</li>
<li><code>&lt;prefix&gt; C-c</code> 表示需要按下 <kbd>Ctrl</kbd> + <kbd>a</kbd> 或者 <kbd>Ctrl</kbd> + <kbd>b</kbd> 加上 <kbd>Ctrl</kbd> + <kbd>c</kbd>。</li>
</ul></section><section><h3>快捷键分类说明<a href="#快捷键分类说明"><span>#</span></a></h3><p>下表按<strong>使用频率和优先级</strong>分组，建议按顺序学习：</p><ul>
<li><strong>📌 必学</strong>：日常工作最核心的 5 个操作，学会了就能满足基本需求</li>
<li><strong>⭐ 常用</strong>：提升工作效率的 15 个快捷键，逐步熟悉即可</li>
<li><strong>🔧 进阶</strong>：高级功能和特殊场景，按需学习</li>
</ul></section><section><h3>快捷键速查表<a href="#快捷键速查表"><span>#</span></a></h3><section><h4>📌 会话与窗口基础<a href="#-会话与窗口基础"><span>#</span></a></h4>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>tmux new -s myproject</code></td><td><strong>新建会话</strong> myproject 并进入（普通终端执行）</td></tr><tr><td><code>tmux ls</code></td><td>列出所有会话（普通终端执行）</td></tr><tr><td><code>tmux attach -t myproject</code></td><td>重新连接会话（普通终端执行）</td></tr><tr><td><code>&lt;prefix&gt; d</code></td><td><strong>脱离会话</strong>，回到普通终端，后台保持运行</td></tr><tr><td><code>&lt;prefix&gt; c</code></td><td><strong>新建窗口</strong></td></tr><tr><td><code>&lt;prefix&gt; n</code></td><td>切换到 <strong>下一个</strong> 窗口</td></tr><tr><td><code>&lt;prefix&gt; p</code></td><td>切换到 <strong>上一个</strong> 窗口</td></tr><tr><td><code>&lt;prefix&gt; 数字</code></td><td>直接切换到指定窗口</td></tr></tbody></table></section><section><h4>📌 分屏基础<a href="#-分屏基础"><span>#</span></a></h4>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>&lt;prefix&gt; %</code></td><td><strong>左右分屏</strong></td></tr><tr><td><code>&lt;prefix&gt; "</code></td><td><strong>上下分屏</strong>（双引号）</td></tr><tr><td><code>&lt;prefix&gt; 方向键</code></td><td>在分屏间 <strong>移动光标</strong></td></tr><tr><td><code>&lt;prefix&gt; z</code></td><td><strong>放大/缩小</strong> 当前分屏</td></tr><tr><td><code>&lt;prefix&gt; x</code></td><td><strong>关闭</strong> 当前分屏</td></tr></tbody></table></section><section><h4>⭐ 常用操作<a href="#-常用操作"><span>#</span></a></h4>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>&lt;prefix&gt; e</code></td><td>打开 .local 配置文件（使用 <span><span>VISUAL或VISUAL 或 </span><span><span><span></span><span>V</span><span>I</span><span>S</span><span>U</span><span>A</span><span>L</span><span>或</span></span></span></span>EDITOR 变量定义的编辑器，默认 vim）</td></tr><tr><td><code>&lt;prefix&gt; r</code></td><td>重新加载配置</td></tr><tr><td><code>&lt;prefix&gt; C-c</code></td><td>新建会话</td></tr><tr><td><code>&lt;prefix&gt; C-f</code></td><td>按名称切换会话</td></tr></tbody></table></section><section><h4>⭐ 窗口与分屏导航<a href="#-窗口与分屏导航"><span>#</span></a></h4>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>C-l</code></td><td>清空屏幕和 tmux 历史记录</td></tr></tbody></table>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>&lt;prefix&gt; C-h</code></td><td>导航到上一个窗口</td></tr><tr><td><code>&lt;prefix&gt; C-l</code></td><td>导航到下一个窗口</td></tr><tr><td><code>&lt;prefix&gt; Tab</code></td><td>切换到最后活跃的窗口</td></tr></tbody></table>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>&lt;prefix&gt; -</code></td><td>垂直分屏</td></tr><tr><td><code>&lt;prefix&gt; _</code></td><td>水平分屏</td></tr><tr><td><code>&lt;prefix&gt; h</code></td><td>向左导航窗格（Vim 风格）</td></tr><tr><td><code>&lt;prefix&gt; j</code></td><td>向下导航窗格（Vim 风格）</td></tr><tr><td><code>&lt;prefix&gt; k</code></td><td>向上导航窗格（Vim 风格）</td></tr><tr><td><code>&lt;prefix&gt; l</code></td><td>向右导航窗格（Vim 风格）</td></tr></tbody></table></section><section><h4>🔧 进阶操作 - 窗格调整与交换<a href="#-进阶操作---窗格调整与交换"><span>#</span></a></h4>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>&lt;prefix&gt; H</code></td><td>向左调整窗格大小</td></tr><tr><td><code>&lt;prefix&gt; J</code></td><td>向下调整窗格大小</td></tr><tr><td><code>&lt;prefix&gt; K</code></td><td>向上调整窗格大小</td></tr><tr><td><code>&lt;prefix&gt; L</code></td><td>向右调整窗格大小</td></tr><tr><td><code>&lt;prefix&gt; &lt;</code></td><td>与左边的窗格交换</td></tr><tr><td><code>&lt;prefix&gt; &gt;</code></td><td>与右边的窗格交换</td></tr><tr><td><code>&lt;prefix&gt; +</code></td><td>最大化当前窗格到新窗口</td></tr></tbody></table></section><section><h4>🔧 进阶操作 - 鼠标与复制粘贴<a href="#-进阶操作---鼠标与复制粘贴"><span>#</span></a></h4>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>&lt;prefix&gt; m</code></td><td>切换鼠标模式（开启/关闭）</td></tr><tr><td><code>&lt;prefix&gt; Enter</code></td><td>进入复制模式</td></tr><tr><td><code>&lt;prefix&gt; b</code></td><td>查看粘贴缓冲区列表</td></tr><tr><td><code>&lt;prefix&gt; p</code></td><td>从顶部粘贴缓冲区粘贴</td></tr><tr><td><code>&lt;prefix&gt; P</code></td><td>选择粘贴缓冲区进行粘贴</td></tr></tbody></table></section><section><h4>🔧 进阶操作 - 工具集成<a href="#-进阶操作---工具集成"><span>#</span></a></h4>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>&lt;prefix&gt; U</code></td><td>启动 Urlscan（如果可用）或 Urlview</td></tr><tr><td><code>&lt;prefix&gt; F</code></td><td>启动 Facebook PathPicker（如果可用）</td></tr></tbody></table></section></section><section><h3>🔧 进阶操作 - Copy-mode-vi 绑定<a href="#-进阶操作---copy-mode-vi-绑定"><span>#</span></a></h3><p>配置文件中的 <code>copy-mode-vi</code> 设定与 Vim 配置相匹配，以下为复制模式下的快捷键：</p>

<table><thead><tr><th>快捷键</th><th>功能</th></tr></thead><tbody><tr><td><code>v</code></td><td>开始选择 / 可视模式</td></tr><tr><td><code>C-v</code></td><td>在块状可视模式和可视模式之间切换</td></tr><tr><td><code>H</code></td><td>跳转到行首</td></tr><tr><td><code>L</code></td><td>跳转到行尾</td></tr><tr><td><code>y</code></td><td>复制选择内容到顶部粘贴缓冲区</td></tr><tr><td><code>Escape</code></td><td>取消当前操作</td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p><strong>提示</strong>：你也可以通过在 .local 自定义配置文件中设置 <code>tmux_conf_preserve_stock_bindings</code> 变量为 true，来保留 tmux 的原始按键绑定。</p></div></div></section></section>
<section><h2>总结<a href="#总结"><span>#</span></a></h2><p>掌握 tmux 的关键不在于背熟所有的快捷键，而是在脑海中建立起 <strong>Server -&gt; Session -&gt; Window -&gt; Pane</strong> 的空间模型，以及理解它<strong>将显示与进程解耦</strong>的核心价值。习惯它之后，你会发现它是服务器端开发的终极神器。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/fhs-of-linux/</id>
      <title type="text">别再死记 Linux 目录：从 FHS 到运行时文件系统</title>
      <published>2026-04-21T07:00:00.000Z</published>
      <updated>2026-09-01T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/fhs-of-linux/"/>
      <summary type="text">从 FHS 的两条分类轴出发，拆开持久文件、内核接口与进程视图，再用 5 组命令串起 usr-merge、XDG、namespace、cgroup、容器和嵌入式文件系统。</summary>
      <content type="html"><![CDATA[<p>我以前理解 Linux 目录的方法，基本等价于背一张巨大的单词表：<code>/etc</code> 放配置，<code>/var</code> 放变化的数据，<code>/usr</code> 放程序，背到 <code>/sys</code>、<code>/proc</code> 和 <code>/run</code> 时，大脑就开始悄悄交换它们的位置。问题不在记忆力，而在模型错了。这次我换一种办法：先把目录树拆成几类性质完全不同的对象，再用几条命令观察一台正在运行的 Linux。</p>
<p>FHS 3.0 发布于 2015 年 3 月 19 日。到 2026 年，它已经 11 岁了。它仍然是理解 Linux 文件布局的好起点，但它不是现代 Linux 根目录的完整地图，更不是内核接口、容器存储和桌面应用数据的总规范。</p>
<section><h2>先建立一个能用的模型<a href="#先建立一个能用的模型"><span>#</span></a></h2><p>根目录 <code>/</code> 看起来是一棵树，树枝却不一定来自同一块磁盘。它更像一张总配线架：Ext4、Btrfs、tmpfs、procfs、sysfs 和设备节点都接入同一个路径空间，我们恰好可以用普通的路径去访问它们。</p><p>我把这棵树分成三层：</p><ol>
<li><strong>持久文件的放置约定</strong>：FHS 主要回答软件、配置、日志和用户数据应该放在哪里。</li>
<li><strong>内核与运行时接口</strong>：<code>/proc</code>、<code>/sys</code>、<code>/dev</code>、<code>/run</code> 往往由内核或早期用户空间在启动时提供。</li>
<li><strong>进程看到的文件系统视图</strong>：挂载命名空间和容器可以让两个进程看到不同的 <code>/</code>，即使它们共享同一个内核。</li>
</ol><p>把这三层混在一起，就会得到一篇目录百科；每一项似乎都对，合起来却没有因果关系。下面逐层拆。</p></section>
<section><h2>FHS 到底解决什么问题？<a href="#fhs-到底解决什么问题"><span>#</span></a></h2><p>FHS 不是为了让根目录看起来整齐。它要让软件、发行版、管理员和脚本能够预测文件的位置。标准用两条彼此独立的轴来组织数据：</p><ul>
<li><strong>静态 (static) / 可变 (variable)</strong>：文件是否会在没有管理员干预时变化。</li>
<li><strong>可共享 (shareable) / 不可共享 (unshareable)</strong>：同一份文件能否被多台主机使用。</li>
</ul><p>FHS 3.0 自己给出的例子很干净：</p>

<table><thead><tr><th></th><th>可共享</th><th>不可共享</th></tr></thead><tbody><tr><td>静态</td><td><code>/usr</code>、<code>/opt</code></td><td><code>/etc</code>、<code>/boot</code></td></tr><tr><td>可变</td><td><code>/var/mail</code>、<code>/var/spool/news</code></td><td><code>/var/run</code>、<code>/var/lock</code></td></tr></tbody></table><p>这张 <code>2 x 2</code> 表是设计原则，不是把每个目录硬塞进一个格子的分类游戏。例如 <code>/home</code> 是否跨主机共享取决于部署方式，<code>/srv</code> 的内容也由站点自己组织。模型负责解释边界，不负责替现实假装整齐。</p><p>静态和可变数据分开后，系统资源可以只读挂载，日志和队列可以采用不同的备份策略；可共享和本机专属数据分开后，多台机器可以复用同一份程序而保留各自配置。这才是目录划分真正带来的工程收益。</p></section>
<section><h2>根目录下的持久世界<a href="#根目录下的持久世界"><span>#</span></a></h2><p>FHS 3.0 要求根目录包含 14 个目录或指向它们的符号链接，并在对应子系统存在时再提供 3 个可选目录。没必要逐字背诵，我更愿意沿着文件的生命周期来理解它们。</p><section><h3><code>/usr</code>：发行版提供的静态系统资源<a href="#usr发行版提供的静态系统资源"><span>#</span></a></h3><p><code>/usr</code> 是主要的只读、可共享层。发行版或包管理器提供的大部分程序和静态资源都放在这里。</p><div><div><div></div><div><code>/usr</code> 到底是什么缩写？</div></div><div><p>网上经常把 <code>/usr</code> 解释为 “Universal System Resources”，但这不是 FHS 给出的官方全称，而是后来流传开的一种解释。FHS 3.0 并没有规定 <code>/usr</code> 应该展开成哪几个英文单词，它只定义这个目录的用途。因此，与其记忆一个有争议的全称，不如直接记住它的目录语义：<code>/usr</code> 主要保存通常可以只读挂载和跨主机共享的系统资源。</p></div></div><ul>
<li><code>/usr/bin</code>：适合从命令行调用的程序。</li>
<li><code>/usr/lib</code>：库、包的私有数据，以及不打算让用户直接执行的内部程序。</li>
<li><code>/usr/include</code>：面向用户空间开发的头文件。</li>
<li><code>/usr/share</code>：与 CPU 架构无关的数据，例如手册、时区和 locale 数据。</li>
<li><code>/usr/local</code>：系统管理员在本机安装的软件层，避免与发行版管理的 <code>/usr</code> 文件发生冲突。</li>
</ul><p>这里有一个容易踩的坑：<code>/usr/include/linux</code> 是提供给用户空间使用的 UAPI 头文件，不应该随手链接到任意内核源码树。编译外部内核模块时，常见入口是 <code>/lib/modules/$(uname -r)/build</code>；它通常再指向发行版为当前内核准备的构建目录。用户空间 ABI 和内核模块构建环境是两件事。</p></section><section><h3><code>/etc</code>：本机配置，而不是程序仓库<a href="#etc本机配置而不是程序仓库"><span>#</span></a></h3><p><code>/etc</code> 保存 host-specific system configuration，也就是本机系统配置。主机名、服务配置、账号数据库和网络策略通常落在这里。</p><p>FHS 明确要求 <code>/etc</code> 不放二进制程序。现代系统还经常把发行版默认值放在 <code>/usr</code>，只把管理员覆盖的配置放在 <code>/etc</code>。于是配置的含义变得很清楚：<code>/usr</code> 是供应方给出的默认状态，<code>/etc</code> 是这台机器做出的决定。</p></section><section><h3><code>/var</code>：需要跨重启保留的变化<a href="#var需要跨重启保留的变化"><span>#</span></a></h3><p><code>/var</code> 保存运行中会变化、而且通常要持久化的数据：</p><ul>
<li><code>/var/log</code>：日志。</li>
<li><code>/var/lib</code>：应用持久状态，例如数据库、包管理器状态和 <code>cloud-init</code> 实例数据。</li>
<li><code>/var/cache</code>：可以重新生成的缓存。</li>
<li><code>/var/spool</code>：等待处理的队列，例如邮件和打印任务。</li>
<li><code>/var/tmp</code>：预期可以跨重启保留的临时文件。</li>
</ul><p>“会变大” 不是判断 <code>/var</code> 的充分条件。配置文件也会变化，但它属于 <code>/etc</code>；用户文档也会变化，但它属于用户数据。关键问题是：这是不是系统或服务在运行中产生并需要保留的状态？</p></section><section><h3><code>/run</code>：只服务于本次启动<a href="#run只服务于本次启动"><span>#</span></a></h3><p><code>/run</code> 保存从本次启动开始才有意义的运行时数据，例如 PID 文件、Unix socket、锁和服务的瞬时状态。现代系统通常把它挂载为 <code>tmpfs</code>，所以重启后自然消失。早期放在 <code>/var/run</code> 和 <code>/var/lock</code> 的内容，如今通常通过兼容链接指向 <code>/run</code>。</p><p><code>/run</code> 与 <code>/var</code> 的分界不是 “内存对磁盘”，而是生命周期：</p><ul>
<li>重启后仍有意义，放 <code>/var</code>。</li>
<li>只描述当前这次启动，放 <code>/run</code>。</li>
</ul><p>这条规则比死记 “PID 文件放哪里” 有用得多。</p></section><section><h3>用户、临时文件与服务数据<a href="#用户临时文件与服务数据"><span>#</span></a></h3><ul>
<li><code>/home</code>：普通用户的主目录。程序应通过 <code>$HOME</code> 或用户数据库查找它，不要假定一定是 <code>/home/&lt;name&gt;</code>。</li>
<li><code>/root</code>：root 的主目录放在 <code>/home</code> 外，使 <code>/home</code> 尚未挂载时管理员仍可能登录修复。</li>
<li><code>/tmp</code>：短生命周期临时文件，可能在启动时或定期清理。多用户环境中必须用 <code>mkstemp(3)</code>、<code>mkdtemp(3)</code> 等安全接口创建文件。</li>
<li><code>/srv</code>：本机对外提供的服务数据，例如管理员维护的网站内容。</li>
<li><code>/opt</code>：附加软件包的独立目录。它仍在 FHS 中，但现代发行版是否使用它取决于打包策略。</li>
<li><code>/boot</code>：内核、initramfs 和引导加载器需要的静态文件。EFI 系统分区可能挂载在 <code>/boot</code>、<code>/boot/efi</code> 或 <code>/efi</code>，这是发行版和引导方案的选择，不是所有 Linux 都固定为 <code>/boot/efi</code>。</li>
</ul></section></section>
<section><h2>根目录下的运行时接口<a href="#根目录下的运行时接口"><span>#</span></a></h2><p>现在转向另一类东西。<code>/dev</code>、<code>/proc</code> 和 <code>/sys</code> 看起来也是目录，但它们主要不是用来 “存放普通文件” 的。</p><section><h3><code>/dev</code>：设备访问入口<a href="#dev设备访问入口"><span>#</span></a></h3><p><code>/dev</code> 中的条目是设备节点。它们把文件描述符这套统一接口连接到内核驱动，但 “一切皆文件” 只是一条有用的近似，不是说设备节点等价于磁盘上的普通文件。</p><p>例如，读写块设备、终端和随机数设备的语义完全不同。有些设备主要通过 <code>read</code>、<code>write</code> 工作，有些依赖 <code>ioctl</code>，还有些根本不允许普通用户访问。现代发行版通常由 <code>devtmpfs</code> 和 <code>udev</code> 动态维护这里的内容。</p></section><section><h3><code>/proc</code>：进程视图与历史形成的内核接口<a href="#proc进程视图与历史形成的内核接口"><span>#</span></a></h3><p><code>procfs</code> 最初围绕进程信息设计，因此 <code>/proc/&lt;pid&gt;</code> 暴露进程的状态、文件描述符、内存映射和命名空间。后来许多全局内核接口也进入 <code>/proc</code>，例如 <code>/proc/cpuinfo</code>、<code>/proc/meminfo</code> 和 <code>/proc/sys</code>。</p><p>它不是 “硬盘里恰好有很多零字节文件”。读取这些路径时，内核动态生成内容；写入某些路径时，内核把数据解释为控制请求。</p></section><section><h3><code>/sys</code>：按内核对象组织的设备模型<a href="#sys按内核对象组织的设备模型"><span>#</span></a></h3><p><code>sysfs</code> 导出内核对象、属性以及对象之间的关系。设备、驱动、总线、类和固件信息因此有了比 <code>/proc</code> 更结构化的位置。</p><p>几个常见入口：</p><ul>
<li><code>/sys/class</code>：按设备类别组织的视图，例如网络接口、LED 和 hwmon。</li>
<li><code>/sys/bus</code>：按总线组织设备与驱动。</li>
<li><code>/sys/devices</code>：设备拓扑。</li>
<li><code>/sys/firmware/devicetree/base</code>：设备树系统上，内核展开后的设备树视图。</li>
<li><code>/sys/fs</code>：cgroup、BPF、pstore 等文件系统或内核子系统的挂载入口。</li>
</ul><p><code>sysfs</code> 属性通常是稳定 ABI 的一部分，但并不意味着所有子目录都适合应用程序随意写入。例如旧的 sysfs GPIO 用户接口已经被内核文档标为废弃，新程序应优先使用 GPIO 字符设备接口。能 <code>echo 1</code> 不代表应该永远 <code>echo 1</code>，这是 Shell 最容易制造的乐观主义。</p></section></section>
<section><h2>用 5 条命令观察真实系统<a href="#用-5-条命令观察真实系统"><span>#</span></a></h2><p>到这里，目录名已经不再是一组定义。我们可以直接问正在运行的系统。</p><section><h3>1. 这个路径来自什么文件系统？<a href="#1-这个路径来自什么文件系统"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>findmnt</span><span> </span><span>-T</span><span> </span><span>/</span></div></div><div><div><div>2</div></div><div><span>findmnt</span><span> </span><span>-T</span><span> </span><span>/proc</span></div></div><div><div><div>3</div></div><div><span>findmnt</span><span> </span><span>-T</span><span> </span><span>/sys</span></div></div><div><div><div>4</div></div><div><span>findmnt</span><span> </span><span>-T</span><span> </span><span>/run</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关注 <code>SOURCE</code>、<code>FSTYPE</code> 和 <code>OPTIONS</code>。一台常见系统可能分别显示磁盘文件系统、<code>proc</code>、<code>sysfs</code> 和 <code>tmpfs</code>，但容器或不可变发行版会给出不同答案。<code>findmnt</code> 的输出比凭目录名猜测可靠。</p></section><section><h3>2. <code>/bin</code> 还是真目录吗？<a href="#2-bin-还是真目录吗"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>readlink</span><span> </span><span>-f</span><span> </span><span>/bin</span></div></div><div><div><div>2</div></div><div><span>readlink</span><span> </span><span>-f</span><span> </span><span>/sbin</span></div></div><div><div><div>3</div></div><div><span>readlink</span><span> </span><span>-f</span><span> </span><span>/lib</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这种布局称为 <strong>usr-merge（<code>/usr</code> 合并）</strong>：原本分散在根目录和 <code>/usr</code> 下的同类目录被合并到一起，文件统一放进 <code>/usr/bin</code>、<code>/usr/sbin</code>、<code>/usr/lib</code>，再让 <code>/bin</code>、<code>/sbin</code>、<code>/lib</code> 成为指向它们的符号链接。旧程序仍然可以访问 <code>/bin/ls</code>，系统实际执行的却与 <code>/usr/bin/ls</code> 是同一个文件。</p><p>早期系统把 <code>/bin</code> 和 <code>/usr/bin</code> 分开，是为了在 <code>/usr</code> 分区尚未挂载时，仍能依靠根分区里的少量工具启动和修复系统。现代 Linux 通常先进入 initramfs，由它准备存储设备并挂载真正的根文件系统，旧边界的实际价值已经很小。usr-merge 因此不是删除兼容路径，而是用符号链接保留旧接口，同时让发行版只维护一套系统文件。</p><p>运行上面的 <code>readlink</code> 命令，就能判断当前系统是否采用了这种布局。传统系统中的这些路径仍可能是彼此独立的目录。</p></section><section><h3>3. 一个程序实际加载了哪些库？<a href="#3-一个程序实际加载了哪些库"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ldd</span><span> </span><span>/usr/bin/ls</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出会暴露动态加载器与架构相关库目录。在 Debian 的 Multiarch 系统上，你可能看到 <code>/lib/x86_64-linux-gnu</code>；采用 multilib 的系统则常见 <code>/lib64</code>。这是发行版 ABI 布局，不应该被简化成 “64 位系统一律使用 <code>/lib64</code>”。</p><div><div><div></div><div>Note</div></div><div><p>不要对不可信的可执行文件运行 <code>ldd</code>。部分实现可能执行目标程序。分析未知文件时应使用发行版推荐的静态检查工具。</p></div></div></section><section><h3>4. 当前进程属于哪些命名空间？<a href="#4-当前进程属于哪些命名空间"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>readlink</span><span> </span><span>/proc/self/ns/mnt</span></div></div><div><div><div>2</div></div><div><span>readlink</span><span> </span><span>/proc/self/ns/net</span></div></div><div><div><div>3</div></div><div><span>readlink</span><span> </span><span>/proc/self/ns/pid</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出类似 <code>mnt:[4026531832]</code>。两个进程对应条目的设备号和 inode 相同，才说明它们处在同一个该类型命名空间。方括号里的数字是 namespace inode，不是所谓的 “物理隔离宇宙哈希值”。</p></section><section><h3>5. cgroup v2 怎样表达资源边界？<a href="#5-cgroup-v2-怎样表达资源边界"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>findmnt</span><span> </span><span>-T</span><span> </span><span>/sys/fs/cgroup</span></div></div><div><div><div>2</div></div><div><span>cat</span><span> </span><span>/proc/self/cgroup</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>现代 cgroup v2 通常呈现一棵统一层级。控制器通过 <code>cpu.max</code>、<code>memory.max</code> 等接口设置限制，进程通过 <code>cgroup.procs</code> 加入 cgroup。Docker 和 Kubernetes 会使用这些内核机制，但 <code>/sys/fs/cgroup</code> 本身不属于 FHS 的文件放置规则。</p></section></section>
<section><h2>开发程序时，文件应该落在哪里？<a href="#开发程序时文件应该落在哪里"><span>#</span></a></h2><p>如果我在写一个系统服务，比背目录更实用的是按所有者和生命周期做决定。</p><p>FHS 主要安排系统级文件，但桌面和命令行程序还需要保存每个用户自己的配置、数据、缓存和运行状态。过去很多程序直接在 <code>$HOME</code> 下创建 <code>.vim</code>、<code>.mozilla</code>、<code>.cache-of-something</code> 等隐藏目录，久而久之，用户主目录就变成了应用程序各自圈地的现场。</p><p><strong>XDG Base Directory 规范</strong>解决的就是这个问题。XDG 是 freedesktop.org 规范体系沿用的历史名称；这里真正需要记住的是 Base Directory：它不强迫所有程序使用几个写死的路径，而是通过一组环境变量，为不同生命周期的数据指定基础目录。程序优先读取环境变量，变量未设置时再使用规范给出的默认值。</p><p>例如，主题偏好属于配置，应放进 <code>$XDG_CONFIG_HOME</code>；下载后可以重新生成的缩略图属于缓存，应放进 <code>$XDG_CACHE_HOME</code>；Unix socket 只在当前登录会话中有效，应放进 <code>$XDG_RUNTIME_DIR</code>。这样备份配置时不必带上缓存，退出登录时也不必保留 socket。目录分类最终对应的是数据生命周期，而不是单纯为了让 <code>$HOME</code> 看起来漂亮。</p>

<table><thead><tr><th>内容</th><th>系统级位置</th><th>用户级默认位置</th></tr></thead><tbody><tr><td>可执行程序</td><td>发行版包用 <code>/usr/bin</code>，本机安装常用 <code>/usr/local/bin</code></td><td><code>~/.local/bin</code></td></tr><tr><td>静态数据</td><td><code>/usr/share/&lt;app&gt;</code> 或 <code>/usr/local/share/&lt;app&gt;</code></td><td><code>$XDG_DATA_HOME/&lt;app&gt;</code>，默认 <code>~/.local/share/&lt;app&gt;</code></td></tr><tr><td>管理员配置</td><td><code>/etc/&lt;app&gt;</code></td><td><code>$XDG_CONFIG_HOME/&lt;app&gt;</code>，默认 <code>~/.config/&lt;app&gt;</code></td></tr><tr><td>持久状态</td><td><code>/var/lib/&lt;app&gt;</code></td><td><code>$XDG_STATE_HOME/&lt;app&gt;</code>，默认 <code>~/.local/state/&lt;app&gt;</code></td></tr><tr><td>可重建缓存</td><td><code>/var/cache/&lt;app&gt;</code></td><td><code>$XDG_CACHE_HOME/&lt;app&gt;</code>，默认 <code>~/.cache/&lt;app&gt;</code></td></tr><tr><td>本次启动的 socket、PID</td><td><code>/run/&lt;app&gt;</code></td><td><code>$XDG_RUNTIME_DIR/&lt;app&gt;</code>，常见于 <code>/run/user/&lt;uid&gt;</code></td></tr><tr><td>日志</td><td><code>/var/log/&lt;app&gt;</code> 或日志服务</td><td>用户状态目录或日志服务</td></tr></tbody></table><p>XDG Base Directory 规范定义的不是三个固定隐藏目录，而是一组环境变量和回退值。尤其容易漏掉的是 <code>$XDG_STATE_HOME</code> 和 <code>$XDG_RUNTIME_DIR</code>：前者保存跨重启但不值得当用户数据同步的状态，后者只在登录会话期间有效，权限必须是 <code>0700</code>。</p><p>如果程序支持命令行参数或环境变量覆盖路径，优先级也应明确。目录规范解决的是默认位置，不应该剥夺管理员部署软件的能力。</p></section>
<section><h2>容器为什么能看到另一棵 <code>/</code>？<a href="#容器为什么能看到另一棵-"><span>#</span></a></h2><p>容器没有发明一套新的 FHS。它组合了几种内核机制：</p><ol>
<li><strong>Mount namespace</strong> 给进程一张独立的挂载表。</li>
<li><strong>OverlayFS</strong> 把只读 lower layer 和可写 upper layer 合并成一个视图。</li>
<li><strong>PID、network、UTS、IPC、user 等 namespace</strong> 隔离其他全局资源。</li>
<li><strong>cgroup</strong> 统计并限制一组进程的资源。</li>
</ol><p>因此，容器里的 <code>/etc</code> 仍然表达该容器视图中的配置，<code>/var</code> 仍然表达该视图中的可变状态。变化的是目录树背后的挂载来源和可见范围，不是路径语义凭空消失了。</p><p><code>/proc/&lt;pid&gt;/ns</code> 只是命名空间对象的句柄。命名空间由 <code>clone(2)</code>、<code>unshare(2)</code> 或 <code>setns(2)</code> 等系统调用创建或加入，不是 Docker “创建了一套文件链接”。文件系统在这里提供观察与引用对象的接口，而不是机制本身。</p></section>
<section><h2>嵌入式系统为什么看起来不太一样？<a href="#嵌入式系统为什么看起来不太一样"><span>#</span></a></h2><p>资源受限设备通常保留 FHS 的大致语义，却会改变底层存储方式。OpenWrt 常见的根文件系统就是一个好例子：只读 SquashFS 作为 lower layer，可写 JFFS2 或 UBIFS 作为 upper layer，再通过 overlay 组合成可写的 <code>/</code>。</p><p>这解释了两个看似矛盾的现象：用户可以修改 <code>/etc/config</code>，恢复出厂设置又能回到固件中的初始状态。修改通常进入可写层，而不是改写只读固件镜像。</p><p>原始 NAND 或 NOR Flash 则由 MTD 子系统暴露。<code>/dev/mtdX</code> 是字符设备接口，<code>/dev/mtdblockX</code> 提供受限的块设备视图。后者并不会凭空提供完整的磨损均衡；需要处理坏块和磨损时，应使用适合原始 Flash 的 UBI/UBIFS 等层。把 <code>mtdblock</code> 当成 “内核替我把 Flash 变成普通硬盘”，大概是数据最不希望听到的一句话。</p><p>设备树、MTD、OverlayFS 都是 Linux 机制，不属于 FHS。它们出现在同一棵目录树里，是因为 VFS 允许不同文件系统和内核对象共享统一的路径空间。</p></section>
<section><h2>最后保留这张判断表<a href="#最后保留这张判断表"><span>#</span></a></h2><p>遇到陌生目录时，我现在依次问 5 个问题：</p><ol>
<li>它是普通持久文件，还是一个挂载出来的运行时接口？</li>
<li>谁拥有它：发行版、管理员、服务、用户，还是内核？</li>
<li>内容是静态资源、配置、持久状态、缓存，还是瞬时状态？</li>
<li>它是否需要跨重启、跨用户或跨主机存在？</li>
<li>当前进程看到的路径，是否受 mount namespace 或 overlay 影响？</li>
</ol><p>这 5 个问题足以推导大多数目录的意义。FHS 提供第一层坐标，XDG 把同样的思想延伸到用户目录，内核伪文件系统暴露运行状态，namespace 和 VFS 再决定某个进程最终看见哪棵树。</p><p>再往下走，就不再是 “文件应该放哪里” 的问题了，而是 dentry、inode、mount、superblock 和内核对象如何把不同世界接到同一个 <code>/</code> 上。到这里目录表终于变成了一套系统模型，而不是 30 个待背单词。很好，我的大脑可以下班了。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ol>
<li><a href="https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html" target="_blank">Filesystem Hierarchy Standard 3.0</a>，FHS 的正式规范与二维分类定义。</li>
<li><a href="https://uapi-group.org/specifications/specs/linux_file_system_hierarchy/" target="_blank">UAPI.9 Linux File System Hierarchy</a>，面向现代 Linux 的目录布局说明，目前标记为 work in progress。</li>
<li><a href="https://man7.org/linux/man-pages/man7/hier.7.html" target="_blank">Linux manual page: hier(7)</a>，典型 Linux 目录层次的手册页。</li>
<li><a href="https://systemd.io/THE_CASE_FOR_THE_USR_MERGE/" target="_blank">systemd: The Case for the <code>/usr</code> Merge</a>，usr-merge 的布局、兼容性和历史背景。</li>
<li><a href="https://specifications.freedesktop.org/basedir/latest/" target="_blank">XDG Base Directory Specification</a>，用户配置、数据、状态、缓存与运行时目录规范。</li>
<li><a href="https://docs.kernel.org/filesystems/proc.html" target="_blank">Linux Kernel Documentation: The <code>/proc</code> Filesystem</a>，procfs 的内核文档。</li>
<li><a href="https://docs.kernel.org/filesystems/sysfs.html" target="_blank">Linux Kernel Documentation: sysfs</a>，sysfs 与内核对象模型。</li>
<li><a href="https://docs.kernel.org/admin-guide/cgroup-v2.html" target="_blank">Linux Kernel Documentation: Control Group v2</a>，cgroup v2 层级、控制器与接口文件。</li>
<li><a href="https://man7.org/linux/man-pages/man7/namespaces.7.html" target="_blank">Linux manual page: namespaces(7)</a>，Linux namespace 类型、系统调用与 <code>/proc/&lt;pid&gt;/ns</code> 接口。</li>
<li><a href="https://docs.kernel.org/filesystems/overlayfs.html" target="_blank">Linux Kernel Documentation: Overlay Filesystem</a>，OverlayFS 的 lower、upper、work 与 merged 视图。</li>
<li><a href="https://docs.kernel.org/driver-api/mtd/index.html" target="_blank">Linux Kernel Documentation: Memory Technology Device</a>，MTD 子系统与原始 Flash 设备接口。</li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/overview-of-english-grammar/</id>
      <title type="text">英语语法综述</title>
      <published>2026-04-06T09:00:00.000Z</published>
      <updated>2026-04-06T09:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/overview-of-english-grammar/"/>
      <summary type="text">本文系统梳理了英语语法的主体框架，从最基本的“简单句和五大定基本型”入手，解析句子成分与词性的区别。文章深入剖析了英语语法的核心枢纽——动词，涵盖其分类、时态、语气及非谓语形式，帮助读者以全局视角清晰建立造句的底层逻辑。</summary>
      <content type="html"><![CDATA[<p>我们为什么学语法？</p>
<div><div><div></div><div>Note</div></div><div><p>当然是为了考高分啊😂</p></div></div>
<p>其实，所有的英语语法都只有一个目的：<strong>造句</strong>。</p>
<p>英语和中文一样，我们说话写文章有的时候用短句子，有的时候用长句子。不同的短句子又可以组合成新的长句。但这里说的是句子的长度，并不适合语法讨论。</p>
<p>我们只有把句子拆开来，直到拆到不能再继续拆了，否则句意会不完整的 “基本句”，才好研究语法。因为如果 “基本句” 研究透了，长句子就是它们的组合。</p>
<section><h2>1. 简单句（Simple Sentence）<a href="#1-简单句simple-sentence"><span>#</span></a></h2><p>我们把这样的不能再拆的 “基本句” 称作<strong>简单句</strong>（Simple Sentence）。而这样的简单句，除去那些<strong>感叹句</strong>（“嗯”，“哦”，“啊”等）、<strong>省略句</strong>、<strong>（向整句提问的）问句</strong>，几乎全部都是在说：</p><div><div><div></div><div>Note</div></div><div><p><strong>什么</strong> + <strong>怎么样</strong></p></div></div><p>绝大多数句子粗略地分一共就只有这两个部分，而这个 “什么” 和 “怎么样” 分别对应了两个最基本的<strong>句子成分</strong>：</p><ul>
<li>“什么”： <strong>主语</strong>（Subject）。</li>
<li>“怎么样”：<strong>谓语</strong>（Predicate）。</li>
</ul><p>换句话说，几乎所有的英语句子结构都是：</p><div><div><div></div><div>Note</div></div><div><p><strong>主语</strong> + <strong>谓语</strong></p></div></div><p>这里的<strong>主语</strong>一般是人或物，不管是抽象还是具体。而这里的<strong>谓语</strong>则是指一个广义的动作或发生的事情。</p><div><div><div></div><div>Warning</div></div><div><p>这个 “动作” 不是我们平时狭义说的要动起来的 “动作”，而这个广义的 “动作” 也就是我们语法上说的<strong>动词</strong>（Verb）。</p></div></div></section>
<section><h2>2. 谓语动词<a href="#2-谓语动词"><span>#</span></a></h2>

<table><thead><tr><th>序号</th><th>动词类型</th><th>解释</th></tr></thead><tbody><tr><td>1</td><td>不及物动词</td><td>可以独立完成的动作</td></tr><tr><td>2</td><td>单及物动词</td><td>有一个动作的承受者</td></tr><tr><td>3</td><td>双及物动词</td><td>有两个动作承受者</td></tr><tr><td>4</td><td>复杂及物动词</td><td>只有一个动作承受者</td></tr><tr><td>5</td><td>系动词</td><td>非 “动作”</td></tr></tbody></table><section><h3>2.1 不及物动词（Intransitive Verb）<a href="#21-不及物动词intransitive-verb"><span>#</span></a></h3><p><strong>不及物动词</strong>（Intransitive Verb）就是指<strong>可以独立完成的动作</strong>，例如：</p><blockquote><p>Papa Rabbit sleeps.</p><p>兔老爹睡觉。</p></blockquote><p>类似 “sleeps” 这样的没有承受者的动词就是<strong>不及物动词</strong>（Intransitive Verb）。</p><p>不及物动词所对应的基本句子结构就是：</p><div><div><div></div><div>Tip</div></div><div><p><strong>主语</strong> + <strong>不及物动词</strong></p></div></div></section><section><h3>2.2 单及物动词（Monotransitive Verb）<a href="#22-单及物动词monotransitive-verb"><span>#</span></a></h3><blockquote><p>Papa Rabbit likes you.</p><p>兔老爹喜欢你。</p></blockquote><p>如果这里只说 “兔老爹喜欢”，你肯定觉得意思不完整，因为像 “喜欢” 这样的动词没有承受者就没有什么实际意义，这样的动词就属于<strong>及物动词</strong>（Transitive Verb），而这个动作承受者就是<strong>宾语</strong>（Object）。</p><p>具体而言，对于只有一个宾语及物动词，我们称为<strong>单及物动词</strong>（Monotransitive Verb）。</p><p>单及物动词对应的基本句子结构就是：</p><div><div><div></div><div>Tip</div></div><div><p><strong>主语</strong> + <strong>单及物动词</strong> + <strong>宾语</strong></p></div></div></section><section><h3>2.3 双及物动词（Ditransitive Verbs）<a href="#23-双及物动词ditransitive-verbs"><span>#</span></a></h3><blockquote><p>Papa Rabbit teaches you English.</p><p>兔老爹教你英语。</p></blockquote><p>这里的核心动词是 “teach”（教），但是教授的知识是 “英语”，而知识的接收对象是 “你”。在语法上，我们把这样的动词的两个承受者分别称为<strong>直接宾语</strong>（Direct Object）和<strong>间接宾语</strong>（Indirect Object）。</p><p>如果只说 “兔老爹教英语”，其实语义已经完整了，所以 “English” 这里就是<strong>直接宾语</strong>，而如果只说 “兔老爹教你”，而又没有上下文的话，你肯定觉得还缺了什么，所以 “you” 在这里是间接宾语。</p><p>这样的往往既有直接宾语又有间接宾语的动词，也是及物动词的一种，更具体地说，它属于<strong>双及物动词</strong>（Ditransitive Verbs）。</p><p>双及物动词对应的基本句子结构就是：</p><div><div><div></div><div>Tip</div></div><div><p><strong>主语</strong> + <strong>双及物动词</strong> + <strong>间接宾语</strong> + <strong>直接宾语</strong></p></div></div></section><section><h3>2.4 复杂及物动词（Complex-transitive Verb）<a href="#24-复杂及物动词complex-transitive-verb"><span>#</span></a></h3><blockquote><p>Papa Rabbit considers you smart.</p><p>兔老爹认为你聪明。</p></blockquote><p>这里虽然只有一个动作的承受者 “you”，但如果只说 “兔老爹认为你”，你肯定会觉得话没有说完，但是宾语 “you” 后面的 “smart” 却也不像双及物动词那样是另一个动作承受者。</p><p>这样的动词，必须要有个补充承受者的信息才意义完整，而这个补充的信息，我们在语法上称为<strong>补足语</strong>或<strong>补语</strong>（Complement）。更明确也可以说是<strong>宾语补语</strong>（Object Complement）。</p><p>这样的需要<strong>补语</strong>的动词，我们称之为<strong>复杂及物动词</strong>（Complex-transitive Verb）</p><p>复杂及物动词对应的基本句子结构就是：</p><div><div><div></div><div>Tip</div></div><div><p><strong>主语</strong> + <strong>复杂及物动词</strong> + <strong>宾语</strong> + <strong>（宾语）补语</strong></p></div></div></section><section><h3>2.5 系动词（Linking Verb）<a href="#25-系动词linking-verb"><span>#</span></a></h3><p>这种动作所表示的含义与狭义上的 “动作” 不太一样：</p><blockquote><p>Papa Rabbit is tall.</p><p>兔老爹是高的。</p></blockquote><p>这里的 “is” 在中文里经常翻译成 “是”，但它实际上的作用很简单：</p><div><div><div></div><div>Caution</div></div><div><p>就是把这个动词之后的信息<strong>赋予</strong>前面的主语。</p></div></div><p>这里的 “is” 说白了就是把 “Papa Rabbit” 和 “tall” 连在一起，划上等号而已。或者说，把后者信息赋予前者。如果把 “tall” 换成 “in the room”：</p><blockquote><p>Papa Rabbit is in the room.</p><p>兔老爹在房间里。</p></blockquote><p>其实也是把 “in the room” 这个状态性质赋予 “Papa Rabbit” 而已。</p><blockquote><p>Papa Rabbit looks tall.</p><p>兔老爹看起来高。</p></blockquote><p>这个 “look” 通常意义是 “看”，但是这里到底是谁在看？其实不是 “Papa Rabbit” 在看，“Papa Rabbit” 在这里是<strong>被看</strong>。这里的 “looks” 其实也是把后面的 “tall” 赋予前面的主语 “Papa Rabbit” 而已，也算是划等号，只不过这个划等号比之前的 “is” 多了个 “看上去”。</p><blockquote><p>Papa Rabbit smells nice.</p><p>兔老爹闻上去香。</p></blockquote><p>这里的 “smells” 还是把 “Papa Rabbit” 和 “nice” 划等号，只不过还有 “闻上去” 这层意思。</p><p>像这样的赋予主语某种性质状态的 “划等号” 的动词，我们称之为<strong>连系动词</strong>（Linking Verb），也称为<strong>系动词</strong>。</p><p>系动词后面的补充信息自然就是<strong>补足语</strong>或<strong>补语</strong>，更精确地说是<strong>主语补语</strong>，而<strong>主语补语</strong>在中国的英语教学中又被称为<strong>表语</strong>（Predicative）。</p><p>系动词对应的基本句子结构就是：</p><div><div><div></div><div>Tip</div></div><div><p><strong>主语</strong> + <strong>系动词</strong> + <strong>（主语）补语/表语</strong></p></div></div></section></section>
<section><h2>3. 五大基本句型<a href="#3-五大基本句型"><span>#</span></a></h2>

<table><thead><tr><th>序号</th><th>句型</th></tr></thead><tbody><tr><td>1</td><td>主语 + 不及物动词</td></tr><tr><td>2</td><td>主语 + 单及物动词 + 宾语</td></tr><tr><td>3</td><td>主语 + 双及物动词 + 间接宾语 + 直接宾语</td></tr><tr><td>4</td><td>主语 + 复杂及物动词 + 宾语 + 宾语补语</td></tr><tr><td>5</td><td>主语 + 系动词 + 主语补语/表语</td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p>还有一种 “八大句型” 的分类，即在 “五大句型” 基础上增加：</p><ol>
  <li><strong>there be</strong> 句型，如：There is a rabbit. 可理解为第 5 种句型 “主语 + 系动词 + 主语补语/表语” 的倒装。</li>
  <li><strong>主语</strong> + <strong>谓语</strong> + <strong>状语</strong>，如：I live in China. 可理解为第 1 种句型 “主语 + 不及物动词” 的延伸（虽然这里的状语很重要）。</li>
  <li><strong>主语</strong> + <strong>谓语动词</strong> + <strong>宾语</strong> + <strong>状语</strong>，如：I put the carrot on the table. 可理解为第 4 种句型 “主语 + 复杂及物动词 + 宾语 + 宾语补语” 的延伸。</li>
</ol></div></div><div><div><div></div><div>Warning</div></div><div><p>严格说来，谓语和谓语动词是有区别的。句子主语后面余下的部分就是谓语，谓语动词只是谓语的一部分。</p><p>但是在平时，通俗地，为了方便，很多人会直接称呼谓语动词为谓语，而把 “主语 + 谓语动词 + 宾语” 这样的句子结构直接称呼为 “主语 + 谓语 + 宾语”（或 “主谓宾”），这也不是什么大问题。</p></div></div></section>
<section><h2>4. 句子基本成分<a href="#4-句子基本成分"><span>#</span></a></h2>

<table><thead><tr><th>序号</th><th>成分</th><th>英文名</th><th>解释</th><th>例句</th></tr></thead><tbody><tr><td>1</td><td>主语</td><td>Subject</td><td>动作的发出者或被讨论的对象</td><td><strong>Papa Rabbit</strong> eats carrots.</td></tr><tr><td>2</td><td>谓语动词</td><td>Predicate Verb</td><td>表示具体的动作或状态</td><td>Papa Rabbit <strong>eats</strong> carrots.</td></tr><tr><td>3</td><td>宾语</td><td>Object</td><td>动作的承受者</td><td>Papa Rabbit eats <strong>carrots</strong>.</td></tr><tr><td>4</td><td>宾语补语</td><td>Object Complement</td><td>补充说明宾语的状态或特征</td><td>Papa Rabbit considers you <strong>smart</strong>.</td></tr><tr><td>5</td><td>主语补语</td><td>Subject Complement</td><td>补充说明主语的性质状态（即表语）</td><td>Papa Rabbit is <strong>tall</strong>.</td></tr><tr><td>6</td><td>定语</td><td>Attributive</td><td>修饰主语或宾语</td><td><strong>The little</strong> white rabbit ate <strong>a large</strong> carrot.</td></tr><tr><td>7</td><td>状语</td><td>Adverbial</td><td>修饰谓语动词</td><td>The rabbit ate <strong>quickly</strong>.</td></tr><tr><td>8</td><td>同位语</td><td>Appositive</td><td></td><td>Papa Rabbit, <strong>an English teacher</strong>, eats carrots.</td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p>你可能听过 “<strong>插入语</strong>”，如：This carrot, I think(插入语), is very tasty. 但是插入语是独立的，不作句子成分。</p></div></div></section>
<section><h2>5. 复杂句<a href="#5-复杂句"><span>#</span></a></h2><p>直到现在所说的都是<strong>简单句</strong>（Simple Sentence），说白了就是没法再拆成更多句子了。但是我们说话写文章可不全是简单句，而是经常会把这些简单句互相组合形成所谓<strong>复合句</strong>（Compound Sentence）和<strong>复杂句</strong>（Complex Sentence）。</p><p><strong>复合句</strong>说白了就是句子简单的叠加，句子和句子之间是<strong>并列关系</strong>，因此也被称为<strong>并列句</strong>。</p><p><strong>复杂句</strong>则是把一个句子套在另一个句子里，允许不断嵌套，是<strong>从属关系</strong>。</p><p>一个句子套另一个句子，在英语语法上分别叫做<strong>主句</strong>（Main Clause）和<strong>从句</strong>（Subordinate Clause）。</p><p>从句说白了就是把简单句修改一下来充当另一个句子的句子成分。</p><div><div><div><div><div><span><p>主句<br /><br />Main Clause</p></span></div><div><span></span></div><div><span><p>从句<br /><br />Subordinate Clause</p></span></div>
</div><div><div><span><p>主句<br /><br />Main Clause</p></span></div><div><span></span></div><div><span><p>从句<br /><br />Subordinate Clause</p></span></div>
</div></div></div></div><section><h3>5.1 从句类型<a href="#51-从句类型"><span>#</span></a></h3>

<table><thead><tr><th>序号</th><th>从句</th><th>作用</th><th>补充</th><th>例句</th></tr></thead><tbody><tr><td>1</td><td>主语从句</td><td>作主语</td><td>名词性从句</td><td><strong>What Papa Rabbit ate</strong> is a carrot.</td></tr><tr><td>2</td><td>宾语从句</td><td>作宾语</td><td>名词性从句</td><td>I know <strong>that Papa Rabbit likes carrots</strong>.</td></tr><tr><td>3</td><td>表语从句</td><td>作表语</td><td>名词性从句</td><td>The truth is <strong>that he likes you</strong>.</td></tr><tr><td>4</td><td>同位语从句</td><td>作同位语</td><td>名词性从句</td><td>The fact <strong>that he ate the carrot</strong> is obvious.</td></tr><tr><td>5</td><td>定语从句</td><td>作定语</td><td></td><td>The rabbit <strong>who ate the carrot</strong> is my papa.</td></tr><tr><td>6</td><td>状语从句</td><td>作状语</td><td></td><td>I will leave <strong>when Papa Rabbit comes</strong>.</td></tr></tbody></table></section></section>
<section><h2>6. 词性<a href="#6-词性"><span>#</span></a></h2><p>但是现在问题来了，两个句子也许句子组成的方式相同，比如都是 “主语 + 谓语动词 + 宾语”，可是句中包含的词的类型并不一定相同，比如：</p><blockquote><p><em>The rabbit</em> <strong>ate</strong> a carrot.</p><p><em>He</em> <strong>saw</strong> something over there.</p></blockquote><p>很显然，这两句话 “句型” 相同，但是包含的词却不太一样。也就是说，同一类句子成分里可能有不同的词类（或词类），这个概念中文中也有。</p>

<table><thead><tr><th>序号</th><th>词性</th><th>英文名</th><th>解释</th><th>例句</th></tr></thead><tbody><tr><td>1</td><td>动词</td><td>verb</td><td>表示动作或状态</td><td>Papa Rabbit <strong>eats</strong> a carrot.</td></tr><tr><td>2</td><td>名词</td><td>noun</td><td>表示人或物</td><td>Papa Rabbit is a <strong>rabbit</strong>.</td></tr><tr><td>3</td><td>冠词</td><td>article</td><td>用于说明人和事物</td><td>Papa Rabbit is <strong>a</strong> rabbit.</td></tr><tr><td>4</td><td>代词</td><td>pronoun</td><td>替代人和物</td><td><strong>I</strong> am a rabbit.</td></tr><tr><td>5</td><td>形容词</td><td>adjective</td><td>形容人和物</td><td>I am a <strong>smart</strong> rabbit.</td></tr><tr><td>6</td><td>数词</td><td>numeral</td><td>表数量</td><td>I ate <strong>two</strong> carrots.</td></tr><tr><td>7</td><td>副词</td><td>adverb</td><td>修饰动作或形容词</td><td>I ate two carrots <strong>quickly</strong>.</td></tr><tr><td>8</td><td>介词</td><td>preposition</td><td>表示和其他词关系的词</td><td>I ate two carrots <strong>with</strong> chopsticks.</td></tr><tr><td>9</td><td>叹词</td><td>interjection</td><td>表感叹</td><td><strong>Ah</strong>, the carrot is tasty!</td></tr><tr><td>10</td><td>连词</td><td>conjunction</td><td>连接词和句</td><td>I ate two carrots and a potato, <strong>because</strong> I was hungry.</td></tr></tbody></table><div><div><div></div><div>Caution</div></div><div><p><strong>句子成分</strong>（Clause Element）和句子中的<strong>词类</strong>（Parts of Speech）是完全不同的概念。</p></div></div><p>前文所述的每一种句子成分，除了<strong>谓语动词</strong>只能是动词，其他成分都可能包含不同的词类。</p></section>
<section><h2>7. 谓语动词的 “三大本领”<a href="#7-谓语动词的-三大本领"><span>#</span></a></h2><p>英语中的谓语动词有很多厉害的本领，而这些本领，中文中的动词都没有，是英语语法的难点之一：</p><ol>
<li>动作时间</li>
<li>动作状态</li>
<li>动作假设，情感等</li>
</ol><div><div><div></div><div>Warning</div></div><div><p>所谓 “中文动词没有这些本领” 是说中文动词本身没有 “时、体、气” 的概念，但是中文当然有相同或相似意思的表达。</p></div></div><section><h3>7.1 时态<a href="#71-时态"><span>#</span></a></h3><p>英语的时态就是<strong>时间</strong> + <strong>状态</strong>合在一起，而并非只有时间。</p>

<table><thead><tr><th></th><th><strong>现在</strong></th><th><strong>过去</strong></th><th><strong>将来</strong></th><th><strong>过去将来</strong></th></tr></thead><tbody><tr><td><em>一般</em></td><td><em>一般</em><strong>现在</strong></td><td><em>一般</em><strong>过去</strong></td><td><em>一般</em><strong>将来</strong></td><td><em>一般</em><strong>过去将来</strong></td></tr><tr><td><em>完成</em></td><td><strong>现在</strong><em>完成</em></td><td><strong>过去</strong><em>完成</em></td><td><strong>将来</strong><em>完成</em></td><td><strong>过去将来</strong><em>完成</em></td></tr><tr><td><em>进行</em></td><td><strong>现在</strong><em>进行</em></td><td><strong>过去</strong><em>进行</em></td><td><strong>将来</strong><em>进行</em></td><td><strong>过去将来</strong><em>进行</em></td></tr><tr><td><em>完成进行</em></td><td><strong>现在</strong><em>完成进行</em></td><td><strong>过去</strong><em>完成进行</em></td><td><strong>将来</strong><em>完成进行</em></td><td><strong>过去将来</strong><em>完成进行</em></td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p>时态是时间的排列组合，并不是一个线性的列表，更不是只和时间有关。</p></div></div></section><section><h3>7.2 语气<a href="#72-语气"><span>#</span></a></h3>

<table><thead><tr><th>序号</th><th>语气</th><th>解释</th><th>例句</th></tr></thead><tbody><tr><td>1</td><td>虚拟语气</td><td>用来表示意愿，与事实相反的假设等</td><td>If I <strong>were</strong> a rabbit…</td></tr><tr><td>2</td><td>陈述语气</td><td></td><td>I ate a carrot and…</td></tr><tr><td>3</td><td>祈使语气</td><td></td><td>Eat this carrot and…</td></tr></tbody></table></section><section><h3>7.3 助动词（Auxiliary Verb）<a href="#73-助动词auxiliary-verb"><span>#</span></a></h3><p>英语的谓语动词本身往往是不能够独立完成刚才说的上述的这些本领，而且谓语动词也无法完成比如表示否定、可能性、必须性等意思。</p><p>想让谓语动词充分发挥，我们必须用到另一类经常和动词一起用的词，来帮助完成任务，它们就是<strong>助动词</strong>（Auxiliary Verb）</p><p>例如对于动词 eat：</p>

<table><thead><tr><th>协助</th><th>助动词</th><th>组合</th></tr></thead><tbody><tr><td>表示 “吃过了（完成）”</td><td>have</td><td>have eaten</td></tr><tr><td>表示 “正在吃（进行）”</td><td>be</td><td>is eating</td></tr><tr><td>表示 “被吃”</td><td>be</td><td>is eaten</td></tr><tr><td>表示 “有能力吃”</td><td>can</td><td>can eat</td></tr><tr><td>表示 “有可能吃”</td><td>might</td><td>might eat</td></tr><tr><td>表示 “有义务吃”</td><td>must</td><td>must eat</td></tr><tr><td>表示 “不吃（否定）”</td><td>do</td><td>do not eat</td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p>以上的 can, might, must 也被称作 “情态动词”，可归类在 “助动词” 里，但是也有些语法体系把情态动词单独归类，这是分类的不同，不影响理解。</p></div></div><div><div><div></div><div>Caution</div></div><div><p>有些动词，既可以是助动词（无实义），也可以做实义动词，比如 have 也可以表示 “拥有”。千万不要把助动词和它的其他身份（作实义动词时）弄混淆。</p></div></div></section></section>
<section><h2>8. 非谓语动词<a href="#8-非谓语动词"><span>#</span></a></h2><p>之所以需要把动词叫做谓语动词，是因为还有所谓<strong>非谓语动词</strong>，这听起来似乎是一句废话。</p><p>不过，之所以这么叫，是因为动词除了充当句子中的谓语动词，还有可能充当：主语、宾语、宾语补语、主语补语、定语等，只不过需要变成非谓语动词。</p><p>不仅如此，非谓语动词几乎可以取代所有的从句，从而简化句子。只不过非谓语动词就不再具有表示动作时间、状态、语态、语气的功能，也就是它们丧失了时态、语气、语态这些本领了。</p><p>非谓语动词以不同的形式出现在句中，包括：</p><ul>
<li><strong>动词不定式</strong>：常表将来或具体的单次行为。如作主语：<strong>To eat</strong> a carrot is Papa Rabbit’s dream.</li>
<li><strong>现在分词</strong>：常表主动或正在进行。如作定语：The rabbit <strong>eating</strong> a carrot is my papa.</li>
<li><strong>动名词</strong>：将动词名词化，常表抽象概念或习惯。如作宾语：Papa Rabbit enjoys <strong>eating</strong> carrots.</li>
<li><strong>过去分词</strong>：常表被动或已经完成。如作宾语补语：Papa Rabbit wants the carrot <strong>eaten</strong>.</li>
</ul></section>
<section><h2>9. 总结<a href="#9-总结"><span>#</span></a></h2><p>上述这么多一直在讲动词，这是因为：</p><div><div><div></div><div>Caution</div></div><div><p>英语语法的核心就是动词，动词几乎可以串联起所有英语语法的核心概念。</p></div></div><p></p><figure><img src="/assets/images/english_grammar_system.png" alt="英语语法体系" /><figcaption>英语语法体系</figcaption></figure><p></p><p>以上就是英语语法体系的主体框架，绝大多数的语法规则，都逃不出这个框架。而无数的规则和特例也不过就是让这个核心体系更加丰富而已。</p></section>
<section><h2>10. 参考资料<a href="#10-参考资料"><span>#</span></a></h2><ol>
<li><a href="https://www.bilibili.com/video/BV1XY411J7aG?t=0&amp;p=2" target="_blank">英语兔：英语语法综述</a></li>
<li><a href="https://mmistakes.github.io/minimal-mistakes/docs/layouts" target="_blank">Minimal Mistakes: Layouts</a></li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/gnu-assembly/</id>
      <title type="text">GNU 汇编（GAS）通用语法与伪指令指南</title>
      <published>2026-04-05T08:35:00.000Z</published>
      <updated>2026-04-05T08:35:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/gnu-assembly/"/>
      <summary type="text">本文不聚焦某一指令集细节，而是系统梳理 GNU Assembler 的通用语法与伪指令体系，涵盖注释规则、操作数方向、寻址模式、宽度后缀、段与符号控制，以及局部标签与宏等实用写法，帮助你在 x86、ARM、RISC-V 间建立统一理解。</summary>
      <content type="html"><![CDATA[<p>由于机器指令及助记符高度依赖于芯片的具体架构，本文不讨论特定指令集的全部细节，而是聚焦于 GAS（GNU Assembler）通用的伪指令系统及其基本语法规则。</p>
<section><h2>1. 汇编器的本质与工作流<a href="#1-汇编器的本质与工作流"><span>#</span></a></h2><p>GNU Assembler（简称 <code>as</code>）的核心工作流可以用以下公式概括：</p><span><span><span>源代码 (.s)→汇编器 (as)可重定位目标文件 (.o)\text{源代码 (.s)} \xrightarrow{\text{汇编器 (as)}} \text{可重定位目标文件 (.o)}</span><span><span><span></span><span><span>源代码</span><span> (.s)</span></span><span></span><span><span><span><span><span><span></span><span><span><span><span>汇编器</span><span> (as)</span></span></span></span></span><span><span></span><span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span><span><span></span><span><span>可重定位目标文件</span><span> (.o)</span></span></span></span></span></span><p>生成的 <code>.o</code> 文件主要由 <strong>Section</strong>（段）和 <strong>Symbol Table</strong>（符号表）组成。我们常用的 GAS 伪指令（Directives/Pseudo-ops），其根本目的就是为了控制这两个核心要素的生成。</p><p>从本质上来说，GNU Assembler 的核心职能并非简单的 “代码翻译”，而是<strong>内存空间的规划</strong>与<strong>符号地址的解析</strong>。</p><p>汇编器内部维护着一个至关重要的变量：<strong>地址计数器</strong>（Location Counter，通常记为 <code>.</code>）。当汇编器读取源代码时，它实际上是在执行一个状态机循环：</p><ol>
<li><strong>解析伪指令</strong>：根据指令调整地址计数器的位置（例如跳转到新的段、预留空白空间）或修改符号的属性（如设置为全局可见）。</li>
<li><strong>发射字节流</strong>：将翻译好的机器指令或定义的静态数据 “填充” 到地址计数器当前指向的内存单元，随后让计数器自增，指向下一个空闲位置。</li>
<li><strong>构建符号表</strong>：记录所有标号（Label）对应的最终物理地址或相对于段的偏移量。</li>
</ol><p>因此，当我们编写汇编代码时，实际上是在做两件事：</p><ol>
<li><strong>填充内存</strong>：明确告诉汇编器，在当前的内存地址上应当放置什么数据（注意：机器指令在内存中本质上也是数据）。</li>
<li><strong>标记位置</strong>：给特定的内存地址起一个人类可读的名字（Label/Symbol），以便后续进行跳转或读写操作。</li>
</ol></section>
<section><h2>2. 指令语法<a href="#2-指令语法"><span>#</span></a></h2><p>GAS 语法的核心难点在于不同架构下对<strong>操作数方向</strong>的处理，以及如何消除符号的<strong>歧义</strong>。</p><section><h3>2.1 注释风格<a href="#21-注释风格"><span>#</span></a></h3><p>不同的 CPU 架构在 GAS 中通常使用不同的单行注释符号。这里仅列举常见架构的规范，更详细的列表可查阅 <a href="https://sourceware.org/binutils/docs/as/Machine-Dependencies.html" target="_blank">Binutils Docs: Machine Dependent Features</a>。</p><div><div><div></div><div>Note</div></div><div><p>推荐直接查阅 “<a href="https://sourceware.org/binutils/docs/as/Machine-Dependencies.html" target="_blank">Machine Dependent Features</a>” 文档，官方通用的 “<a href="https://sourceware.org/binutils/docs/as/Comments.html" target="_blank">Comments</a>” 文档包含大量历史遗留信息，较为混乱。</p></div></div><p><strong>多行注释</strong>在所有架构中是通用的，沿用了 C 语言风格：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/*</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>The only way to include a newline ('\n') in a comment</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>is to use this sort of comment.</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>*/</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>/* This sort of comment does not nest. */</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>单行注释</strong>则因架构而异：</p><section><h4>2.1.1 x86-64<a href="#211-x86-64"><span>#</span></a></h4><p>根据 <a href="https://sourceware.org/binutils/docs/as/i386_002dChars.html" target="_blank">Binutils Docs: i386_002dChars</a> 的说明：</p><blockquote><p>The presence of a <code>#</code> appearing anywhere on a line indicates the start of a comment that extends to the end of that line.</p><p>If a <code>#</code> appears as the first character of a line then the whole line is treated as a comment, but in this case the line can also be a logical line number directive…</p><p>If the —divide command line option has not been specified then the `/’ character appearing anywhere on a line also introduces a line comment.</p><p>The <code>;</code> character can be used to separate statements on the same line.</p></blockquote><p><strong>核心解读：</strong></p><ul>
<li><strong>首选注释符</strong>：使用 <code>#</code>。只要行内出现 <code>#</code>，其后内容均为注释。</li>
<li><strong>特殊情况</strong>：如果 <code>#</code> 出现在行首，汇编器会优先检查它是否为预处理命令（如 <code>#include</code>）或逻辑行号。</li>
<li><strong>避坑指南</strong>：虽然默认情况下 <code>/</code> 也可以作为注释开始，但这极易产生歧义。例如在指令 <code>movq $(10/2), %rax</code> 中，汇编器可能会把 <code>/2</code> 误判为注释。除非指定了 <code>--divide</code> 选项，否则<strong>严禁使用 <code>/</code> 写注释</strong>。</li>
<li><strong>语句分隔</strong>：分号 <code>;</code> 用于在同一行写多条指令，类似 C 语言。</li>
</ul></section><section><h4>2.1.2 ARM (32-bit)<a href="#212-arm-32-bit"><span>#</span></a></h4><p>根据 <a href="https://sourceware.org/binutils/docs/as/ARM_002dChars.html" target="_blank">Binutils Docs: ARM_002dChars</a> 的说明：</p><blockquote><p>The presence of a <code>@</code> anywhere on a line indicates the start of a comment that extends to the end of that line.</p><p>If a <code>#</code> appears as the first character of a line then the whole line is treated as a comment…</p><p>The <code>;</code> character can be used instead of a newline to separate statements.</p><p>Either <code>#</code> or <code>$</code> can be used to indicate immediate operands.</p></blockquote><p><strong>核心解读：</strong></p><ul>
<li><strong>首选注释符</strong>：使用 <code>@</code>。这是最安全的方式。</li>
<li><strong>分号的作用</strong>：在 GNU ARM 汇编中，<code>;</code> 是语句分隔符（注意：这与 ARM 官方汇编器如 Keil/armasm 不同，后者用 <code>;</code> 做注释）。</li>
<li><strong><code>#</code> 的双重身份</strong>：
<ul>
<li>如果在<strong>行首</strong>，它是注释。</li>
<li>如果在<strong>指令中</strong>，它表示立即数（例如 <code>MOV R0, #10</code>）。</li>
<li><strong>警告</strong>：切勿在指令行尾使用 <code>#</code> 做注释，汇编器会将其误认为是非法的立即数格式。</li>
</ul>
</li>
<li><strong>立即数</strong>：虽然支持 x86 风格的 <code>$</code>，但为了代码移植性，<strong>强烈建议使用 <code>#</code> 表示立即数</strong>。</li>
</ul></section><section><h4>2.1.3 AArch64 (ARM64)<a href="#213-aarch64-arm64"><span>#</span></a></h4><p>GNU 对 32 位 ARM 和 64 位 AArch64 做了严格区分，语法有所不同。</p><p><a href="https://sourceware.org/binutils/docs/as/AArch64_002dChars.html" target="_blank">Binutils Docs: AArch64_002dChars</a> 说明如下：</p><blockquote><p>The presence of a <code>//</code> on a line indicates the start of a comment that extends to the end of the current line.</p><p>If a <code>#</code> appears as the first character of a line, the whole line is treated as a comment.</p><p>The <code>#</code> can be optionally used to indicate immediate operands.</p></blockquote><p><strong>核心解读：</strong></p><ul>
<li><strong>首选注释符</strong>：使用 <code>//</code>（C++ 风格）。</li>
<li><strong>立即数</strong>：<code>#</code> 是可选的，但在阅读习惯上保留它更为清晰。</li>
</ul></section><section><h4>2.1.4 RISC-V<a href="#214-risc-v"><span>#</span></a></h4><p>RISC-V 的 GAS 实现较为现代，通常兼容性较好：</p><ul>
<li>支持行首 <code>#</code> 注释。</li>
<li>通常支持 <code>//</code> 或 <code>#</code> 作为行尾注释（具体取决于工具链版本，建议测试确认）。</li>
</ul><div><div><div></div><div>Warning</div></div><div><p>建议以 <code>#</code> 为主，不建议使用 <code>//</code> 作为通用写法。</p></div></div><hr /></section></section><section><h3>2.2 最大的分歧：数据流向（Direction）<a href="#22-最大的分歧数据流向direction"><span>#</span></a></h3><p>阅读 GAS 代码的第一道门槛是<strong>源操作数与目的操作数的顺序</strong>。GAS 历史上继承了 <strong>AT&amp;T</strong> 语法（源自 Unix），而芯片厂商文档通常使用 <strong>Intel/Native</strong> 语法。</p><section><h4>2.2.1 x86 环境下的 GAS（AT&amp;T 风格）<a href="#221-x86-环境下的-gasatt-风格"><span>#</span></a></h4><p>x86 GAS 遵循 “<strong>源在前，目的在后</strong>” 的原则。读代码时请默念“把左边的值给右边”。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>operation source, destination</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>movl %</span><span>eax</span><span>, %</span><span>ebx</span><span> </span><span> # 含义：EBX = EAX (将 EAX 的值搬运到 EBX)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>2.2.2 ARM/RISC-V 环境下的 GAS（官方风格）<a href="#222-armrisc-v-环境下的-gas官方风格"><span>#</span></a></h4><p>为了减少混乱，GNU 针对 ARM 和 RISC-V 做了妥协，采用了更符合官方文档的 “<strong>目的在前，源在后</strong>” 顺序：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>operation destination, source</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mov</span><span> r0, r1      </span></span><span> # 含义：r0 = r1 (将 r1 的值搬运到 r0)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>2.3 消除歧义：前缀与修饰符<a href="#23-消除歧义前缀与修饰符"><span>#</span></a></h3><p>汇编器必须通过“前缀”来区分一个数字到底是<strong>数值</strong>、<strong>地址</strong>还是<strong>寄存器</strong>。</p><section><h4>2.3.1 寄存器<a href="#231-寄存器"><span>#</span></a></h4><ul>
<li><strong>x86 (AT&amp;T)</strong>：必须加 <code>%</code>，例如 <code>%eax</code>, <code>%esp</code>。</li>
<li><strong>ARM/RISC-V</strong>：通常不加前缀，直接写 <code>r0</code>, <code>sp</code>, <code>pc</code>。为了避免与 C 语言变量名冲突，偶尔可见 <code>%r0</code> 写法，但不常见。</li>
</ul></section><section><h4>2.3.2 立即数<a href="#232-立即数"><span>#</span></a></h4><p>汇编器如何知道 <code>10</code> 是指“内存地址 10”还是“数字 10”？</p><ul>
<li><strong>x86 (AT&amp;T)</strong>：数字前必须加 <code>$</code>。</li>
<li><strong>ARM</strong>：官方推荐加 <code>#</code>。</li>
<li><strong>RISC-V</strong>：极其精简，通常不加前缀，依靠指令名（如 <code>li</code>, <code>addi</code>）的上下文自动推断。</li>
</ul><p><strong>对比示例：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># x86 (AT&amp;T)</span></div></div><div><div><div>2</div></div><div><span>movl </span><span>$10</span><span>, %</span><span>eax</span><span>  </span><span> # EAX = 10 (立即数)</span></div></div><div><div><div>3</div></div><div><span>movl </span><span>10</span><span>, %</span><span>eax</span><span>   </span><span> # EAX = *(int*)(0x0A) (读取内存地址 10 的内容)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># ARM</span></div></div><div><div><div>6</div></div><div><span><span>MOV</span><span> R0, #</span></span><span>10</span><span>      @ R0 = </span><span>10</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># RISC-V</span></div></div><div><div><div>9</div></div><div><span>li a0, </span><span>10</span><span>       </span><span> # Load Immediate: a0 = 10</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>2.4 内存寻址模式<a href="#24-内存寻址模式"><span>#</span></a></h3><p>在 64 位系统中，内存地址空间高达 <span><span>2642^{64}</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>64</span></span></span></span></span></span></span></span></span></span></span></span>，需要 8 字节才能完整表达。然而，一条机器指令通常只有 4 字节（RISC）或变长（x86）。如何在有限的指令长度内精确指向遥远的内存地址？这是寻址模式设计的核心挑战。</p><section><h4>2.4.1 x86 (AT&amp;T)<a href="#241-x86-att"><span>#</span></a></h4><p>x86 是 CISC（复杂指令集），其最大特点是<strong>允许运算指令直接操作内存</strong>。硬件内建了强大的 SIB（Scale-Index-Base）寻址单元，将地址计算视为一个多项式：</p><p><strong>通用格式</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>offset(base, index, scale)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>计算公式</strong>：</p><span><span><span>Address=base+(index×scale)+offset\text{Address} = \text{base} + (\text{index} \times \text{scale}) + \text{offset}</span><span><span><span></span><span><span>Address</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>base</span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>index</span></span><span></span><span>×</span><span></span></span><span><span></span><span><span>scale</span></span><span>)</span><span></span><span>+</span><span></span></span><span><span></span><span><span>offset</span></span></span></span></span></span><p><strong>应用场景</strong>：这非常适合访问数组。例如 <code>array[i]</code>，其中 <code>base</code> 是数组首地址，<code>index</code> 是变量 <code>i</code>，<code>scale</code> 是元素大小（如 <code>int</code> 为 4）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>movl </span><span>4</span><span>(%</span><span>ebp</span><span>), %</span><span>eax</span><span>   </span><span> # 取地址 (ebp + 4) 处的数据 -&gt; eax</span></div></div><div><div><div>2</div></div><div><span>movl array(,%</span><span>edi</span><span>,</span><span>4</span><span>), %</span><span>eax</span><span> # 取地址 (array + edi * 4) 处的数据 -&gt; eax</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>2.4.2 ARM<a href="#242-arm"><span>#</span></a></h4><p>ARM 是 RISC 且为 <strong>Load/Store 架构</strong>。这意味着不能直接对内存里的数进行 <code>add</code> 等运算，必须先 <code>ldr</code>（Load）到寄存器，运算后再 <code>str</code>（Store）回去。</p><p>ARM 的特色在于 <strong>Address Write-back（地址回写）</strong>。为了方便遍历数组，它允许在读写内存的同时，自动更新基址寄存器的值。</p><p>GAS 中使用中括号 <code>[]</code> 表示内存引用：</p><ul>
<li><strong>常规偏移</strong>：<code>[Base, Offset]</code></li>
<li><strong>寄存器偏移</strong>：<code>[Base, Register]</code>，甚至支持移位 <code>[Base, Reg, lsl #3]</code>。</li>
<li><strong>前索引（Pre-indexed）</strong>：<code>[r1, #8]!</code> —— 访问前先加 8，且<strong>修改</strong> x1 的值（x1 += 8）。</li>
<li><strong>后索引（Post-indexed）</strong>：<code>[r1], #8</code> —— 访问后再加 8，且<strong>修改</strong> x1 的值。</li>
</ul></section><section><h4>2.4.3 RISC-V<a href="#243-risc-v"><span>#</span></a></h4><p>RISC-V 追求极致的硬件精简。它不支持 ARM 那种复杂的自动回写，也不支持 x86 的复杂比例因子。</p><p>RISC-V 的基础指令多为 32 bit（但启用压缩扩展后也有 16 bit 指令），除去操作码和寄存器位，留给立即数偏移的空间只有 <strong>12 位</strong>（即 <span><span>±2KB\pm 2\text{KB}</span><span><span><span></span><span>±</span><span>2</span><span><span>KB</span></span></span></span></span>）。</p><p><strong>通用格式</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>offset(base)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>计算公式</strong>：</p><span><span><span>Address=base+offset\text{Address} = \text{base} + \text{offset}</span><span><span><span></span><span><span>Address</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>base</span></span><span></span><span>+</span><span></span></span><span><span></span><span><span>offset</span></span></span></span></span></span><p><strong>如何访问大范围地址？</strong></p><p>由于 12 位无法容纳 32/64 位地址，必须将大地址拆分为“高位”和“低位”两条指令处理：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 小范围寻址 (Offset &lt; 2048)</span></div></div><div><div><div>2</div></div><div><span>lw a0, </span><span>8</span><span>(</span><span>sp</span><span>)       </span><span> # 从 sp + 8 处加载一个 word</span></div></div><div><div><div>3</div></div><div><span>sw a0, </span><span>0</span><span>(t1)       </span><span> # 存入 t1 + 0 处</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 大范围/全局变量寻址 (Hi/Lo 拆分)</span></div></div><div><div><div>6</div></div><div><span>lui t0, %hi(VAL)     </span><span> # 加载 VAL 地址的高 20 位到 t0</span></div></div><div><div><div>7</div></div><div><span>lw  a0, %lo(VAL)(t0) </span><span> # 加载 VAL 地址的低 12 位，作为偏移加到 t0 上读取</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>2.5 指令后缀：操作数宽度 (Size Suffixes)<a href="#25-指令后缀操作数宽度-size-suffixes"><span>#</span></a></h3><p>在机器层面，CPU 处理数据时必须明确一个核心参数：<strong>位宽 (Width)</strong>。硬件需要知道它是应该只驱动数据总线上的 8 根线，还是激活全部 64 根线？是将数据放入寄存器的低 8 位（如 <code>AL</code>），还是覆盖整个 64 位（如 <code>RAX</code>）？</p><p>GAS 通过在指令助记符后添加<strong>后缀</strong>来显式控制这一行为。</p><section><h4>2.5.1 GAS 通用命名规则<a href="#251-gas-通用命名规则"><span>#</span></a></h4><p>GAS 使用一套单字母后缀来表示数据宽度。需要特别警惕的是，不同架构对 “Word” 的定义存在历史分歧。</p>

<table><thead><tr><th><strong>后缀</strong></th><th><strong>全称</strong></th><th><strong>大小 (Bits)</strong></th><th><strong>字节数</strong></th><th><strong>说明</strong></th></tr></thead><tbody><tr><td><strong>b</strong></td><td><strong>B</strong>yte</td><td>8-bit</td><td>1</td><td>通用。</td></tr><tr><td><strong>w</strong></td><td><strong>W</strong>ord</td><td><strong>16-bit</strong></td><td>2</td><td><strong>注意！</strong> 在 x86 语境下，Word 是 16 位（源于 8086）。但在 ARM/RISC-V 文档中，Word 通常指 32 位。GAS 的 <code>w</code> 后缀专指 16 位。</td></tr><tr><td><strong>l</strong></td><td><strong>L</strong>ong</td><td>32-bit</td><td>4</td><td>x86 的标准 32 位后缀。</td></tr><tr><td><strong>q</strong></td><td><strong>Q</strong>uad</td><td>64-bit</td><td>8</td><td>四字（4 x 16-bit），即 64 位。</td></tr></tbody></table></section><section><h4>2.5.2 x86 架构：显式后缀 (Explicit Suffixes)<a href="#252-x86-架构显式后缀-explicit-suffixes"><span>#</span></a></h4><p>x86 是变长指令集，且寄存器存在嵌套关系（<code>RAX</code> 包含 <code>EAX</code> 包含 <code>AX</code> 包含 <code>AL</code>）。为了消除歧义，GAS <strong>强烈推荐</strong>在所有操作码后加上后缀。</p><p>虽然现代汇编器有时能根据寄存器名字（如 <code>%eax</code>）推断出你是想操作 32 位，但在涉及内存操作数（无法看出大小）或立即数时，不写后缀极易报错。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>movb </span><span>$0xFF</span><span>, %</span></span><span>al</span><span>       </span><span> # 移动 1 个字节 (8-bit)</span></div></div><div><div><div>2</div></div><div><span><span>movw </span><span>$0xFFFF</span><span>, %</span></span><span>ax</span><span>     </span><span> # 移动 1 个字   (16-bit)</span></div></div><div><div><div>3</div></div><div><span><span>movl </span><span>$0xFFFFFFFF</span><span>, %</span></span><span>eax</span><span> </span><span> # 移动 1 个长字 (32-bit)</span></div></div><div><div><div>4</div></div><div><span><span>movq</span><span> </span></span><span>$1</span><span>, %</span><span>rax</span><span>         </span><span> # 移动 1 个四字 (64-bit)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span># 歧义示例：</span></div></div><div><div><div>7</div></div><div><span># mov $0, (%rbx)     # 错误！汇编器不知道你想把 rb 指向的内存清零 1 个字节还是 8 个字节。</span></div></div><div><div><div>8</div></div><div><span><span>movq</span><span> </span></span><span>$0</span><span>, (%</span><span>rbx</span><span>)     </span><span> # 正确：清零 8 个字节</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>2.5.3 ARM / RISC-V 架构：助记符内含宽度<a href="#253-arm--risc-v-架构助记符内含宽度"><span>#</span></a></h4><p>与 x86 不同，ARM 和 RISC-V 等 RISC 架构通常将数据宽度直接<strong>编码在指令助记符本身</strong>中，而不是依赖 GAS 的通用后缀。</p><p>在这些架构中，你通常不会看到 <code>movl</code> 这种写法，而是通过不同的指令变体来实现：</p><ul>
<li><strong>RISC-V</strong>:
<ul>
<li><code>lb</code> (Load Byte): 加载 8 位</li>
<li><code>lh</code> (Load Half-word): 加载 16 位</li>
<li><code>lw</code> (Load Word): 加载 32 位 (<strong>注意：这里 Word 是 32 位</strong>)</li>
<li><code>ld</code> (Load Double-word): 加载 64 位</li>
</ul>
</li>
<li><strong>ARM</strong>:
<ul>
<li><code>LDRB</code> (Byte): 加载 8 位</li>
<li><code>LDRH</code> (Half-word): 加载 16 位</li>
<li><code>LDR</code> (Word): 加载 32 位</li>
</ul>
</li>
</ul><div><div><div></div><div>Warning</div></div><div><p><strong>“Word” 的陷阱</strong></p><p>这是一个跨架构开发时最容易混淆的概念：</p><ul>
<li>在 <strong>x86</strong> 汇编中，<code>Word</code> = <strong>16 bits</strong>。</li>
<li>在 <strong>ARM / RISC-V</strong> 汇编中，<code>Word</code> = <strong>32 bits</strong>。</li>
</ul><p>当你使用 GAS 的 <code>.word</code> 伪指令定义数据时，它在所有架构上通常都表示 32 位（或 16 位，取决于具体配置，但在现代环境下多为 32 位）。务必查阅具体目标架构的文档确认 <code>.word</code> 的确切长度，或者直接使用 <code>.2byte</code> / <code>.4byte</code> 这种无歧义的写法。</p></div></div></section></section></section>
<section><h2>3. 伪指令系统 (Directives)<a href="#3-伪指令系统-directives"><span>#</span></a></h2><p>伪指令以 <code>.</code> 开头，它们不对应具体的 CPU 机器码，而是指挥汇编器如何组织二进制结构。</p><section><h3>3.1 数据定义：如何填充字节<a href="#31-数据定义如何填充字节"><span>#</span></a></h3><p>这些指令直接在当前地址计数器位置“发射”数据，常用于定义查找表或初始化变量。</p><ul>
<li>
<p><code>.byte</code> / <code>.hword</code> / <code>.word</code> / <code>.long</code> / <code>.quad</code>：</p>
<p>在当前地址写入特定宽度的整数。注意**字节序（Endianness）**取决于目标架构（x86 为小端，ARM/RISC-V 默认为小端，但可配置）。</p>
<p>代码段</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.</span><span>byte</span><span> </span><span>0x12</span><span>, </span><span>0x34</span><span>   </span><span> # 写入两个字节</span></div></div><div><div><div>2</div></div><div><span>.</span><span>word</span><span> </span><span>0xAABBCCDD</span><span>   </span><span> # 写入 4 字节。小端模式下内存为：DD CC BB AA</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
</li>
<li>
<p><code>.ascii</code> vs <code>.asciz</code> / <code>.string</code>：</p>
<ul>
<li><code>.ascii "Hello"</code>：仅填入字符 ASCII 码，<strong>无</strong>结尾 <code>\0</code>。</li>
<li><code>.asciz "Hello"</code> 或 <code>.string</code>：自动在末尾补 <code>0x00</code>（C 语言标准字符串）。</li>
</ul>
</li>
<li>
<p><code>.fill</code> / <code>.skip</code> / <code>.space</code> / <code>.zero</code>：</p>
<p>用于预留空间或清零，常用于定义栈（Stack）或 BSS 段。</p>
<p>代码段</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>stack_top:</span></div></div><div><div><div>2</div></div><div><span>.skip </span><span>1024</span><span>, </span><span>0</span><span>     </span><span> # 预留 1024 字节，全部填 0</span></div></div><div><div><div>3</div></div><div><span>stack_bottom:</span><span>     </span><span> # 栈底标号（假设栈向下生长，SP 初始指向此处）</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
</li>
</ul></section><section><h3>3.2 内存控制：如何管理地址<a href="#32-内存控制如何管理地址"><span>#</span></a></h3><ul>
<li>
<p><code>.section</code>：</p>
<p>告诉汇编器：“<strong>接下来的数据/指令，请分类归档到哪个‘抽屉’里</strong>”。</p>
<ul>
<li><code>.section .text</code>：代码段（只读，可执行）。</li>
<li><code>.section .data</code>：已初始化的全局变量（可读写）。</li>
<li><code>.section .bss</code>：未初始化变量（不占磁盘空间，程序启动时自动清零）。</li>
<li><strong>自定义段</strong>：例如 <code>.section .isr_vector</code>，配合链接脚本（Linker Script），可以将中断向量表精确固定在 Flash 的 0 地址处。</li>
</ul>
</li>
<li>
<p><code>.align</code> vs <code>.balign</code>：</p>
<ul>
<li><code>.align</code>：行为不统一！在 x86 下 <code>.align 4</code> 表示 4 字节对齐；但在 ARM 下可能表示 <span><span>24=162^4=16</span><span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>4</span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>16</span></span></span></span> 字节对齐。</li>
<li><code>.balign</code>（Byte Align）：<strong>推荐使用</strong>。<code>.balign 4, 0</code> 明确表示保证地址能被 4 整除，如果需要填充，则填 0。</li>
</ul>
</li>
<li>
<p><code>.org</code>：</p>
<p>强制修改当前的地址计数器。</p>
<ul>
<li><strong>示例</strong>：编写 MBR 引导扇区时，需要在第 510 字节写结束标志 <code>0x55AA</code>，会用到 <code>.org 510</code>。</li>
</ul>
</li>
</ul></section><section><h3>3.3 符号管理：与 C 语言的交互接口<a href="#33-符号管理与-c-语言的交互接口"><span>#</span></a></h3><p>这是 C 和汇编混合编程的桥梁。</p><ul>
<li>
<p><code>.global</code>（或 <code>.globl</code>）：</p>
<p>将符号标记为<strong>全局可见</strong>。如果不加此标记，符号仅在当前文件内部可见（类似 C 的 <code>static</code>），链接器无法找到它。</p>
<ul>
<li><strong>场景</strong>：Startup 文件（启动文件）必须声明 <code>.global Reset_Handler</code>，否则硬件复位后找不到入口。</li>
</ul>
</li>
<li>
<p><code>.extern</code>：</p>
<p>声明符号在外部定义。虽然 GAS 通常能自动推断，但显式声明能提高代码可读性。</p>
</li>
<li>
<p><code>.equ</code> / <code>.set</code>：</p>
<p>常量定义，相当于 C 的 <code>#define</code>。它们只存在于汇编阶段，不占用运行时的内存空间。</p>
<p>代码段</p>
<div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.</span><span>equ</span><span> STACK_SIZE, </span><span>0x400</span></div></div></code></pre><div><div></div><div></div></div></figure></div>
</li>
<li>
<p><code>.type</code>：</p>
<p>明确告诉链接器符号的类型（是函数还是数据）。</p>
<ul>
<li><strong>重要性</strong>：在 ARM Thumb-2 指令集中，如果链接器知道某符号是 <code>%function</code>，它会在计算跳转地址时自动将最低位置 1（Thumb 状态标志）。若省略此指令，可能导致切换状态失败而进入 HardFault。</li>
</ul>
</li>
<li>
<p><code>.weak</code>：</p>
<p><strong>弱符号定义</strong>。</p>
<ul>
<li><strong>机制</strong>：如果在其他地方定义了同名的强符号（普通的 <code>.global</code>），链接器优先使用那个；如果没有，则使用这里的默认定义。</li>
<li><strong>应用</strong>：STM32 启动文件中的中断处理函数通常用 <code>.weak</code> 定义为死循环。这允许用户在 C 代码中重写同名函数（如 <code>SysTick_Handler</code>）来覆盖默认行为，而无需修改启动代码。</li>
</ul>
</li>
</ul></section></section>
<section><h2>4. GAS 的特色功能 (Syntactic Sugar)<a href="#4-gas-的特色功能-syntactic-sugar"><span>#</span></a></h2><section><h3>4.1 局部数值标签 (Local Numeric Labels)<a href="#41-局部数值标签-local-numeric-labels"><span>#</span></a></h3><p>为了避免给简单的循环绞尽脑汁起名字（如 <code>loop1</code>, <code>loop_start</code>），GAS 允许使用数字作为临时标号。</p><ul>
<li><strong>定义</strong>：<code>&lt;num&gt;:</code></li>
<li><strong>引用</strong>：<code>&lt;num&gt;f</code> (forward, 向前/向下找)，<code>&lt;num&gt;b</code> (backward, 向后/向上找)。</li>
</ul><p><strong>示例</strong>：</p><p>代码段</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1</span><span>:         </span><span> # 定义标号 1</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>wfi    </span><span> # Wait for interrupt</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>j </span><span>1b</span><span>   </span><span> # 向后跳转到最近的标号 1 (相当于 while(1);)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4.2 宏 (Macros)<a href="#42-宏-macros"><span>#</span></a></h3><p>用于封装重复代码片段：</p><p>代码段</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.macro 宏名称 参数1, 参数2</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>// 宏体内容，可使用 \参数1 引用</span></div></div><div><div><div>3</div></div><div><span>.endm</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4.3 <code>.</code> (Current Location)<a href="#43--current-location"><span>#</span></a></h3><p>符号 <code>.</code> 代表当前地址计数器的值。</p><ul>
<li><strong>常见用法</strong>：<code>b .</code> 等同于 <code>while (1);</code>（跳转到当前地址，死循环）。</li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/armv7-a-isa/</id>
      <title type="text">ARMv7-A 指令体系全解：数据处理、访存、分支与同步</title>
      <published>2026-04-05T08:20:00.000Z</published>
      <updated>2026-04-05T08:20:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/armv7-a-isa/"/>
      <summary type="text">以 I.MX6ULL（ARM Cortex-A7）为背景，从物理本质出发，系统梳理 ARMv7-A 指令集的核心要点：定长 32 位指令结构与条件执行机制、16 个核心寄存器的 AAPCS 调用规范，以及数据处理、内存访问、分支控制、系统状态和原子同步五大类指令的功能与应用场景。</summary>
      <content type="html"><![CDATA[<p>I.MX6ULL 的核心是 ARM Cortex-A7，它基于 ARMv7-A 架构。</p>
<p>要真正理解指令集，就不能只是背诵汇编手册，而是要回答一个核心问题：如果不看代码，只看电路，CPU 在做什么？</p>
<p>本质上，每一条指令都是对 CPU 内部寄存器状态或外部<strong>总线</strong>（内存/外设）的一次<strong>原子操作</strong>。</p>
<div><div><div></div><div>Note</div></div><div><p><strong>总线</strong>（Bus）通俗理解就是主板上的 “高速公路”。它的硬件本质则是一组并排的导线，通过这组导线，连接 CPU 内部各部分或连接 CPU 和外设。</p><p>总线分为<strong>数据总线</strong>（传数据）、<strong>地址总线</strong>（告诉内存我要读哪一个单元）和<strong>控制总线</strong>（读还是写）。</p></div></div>
<div><div><div></div><div>Note</div></div><div><p><strong>原子操作</strong>（Atomic Operation）通俗理解就是，要么做完，要么完全不做，中间绝不允许被打断的操作。</p><p>在 ARM 中，单条读写指令通常是原子的，但 “读-改-写” 三步操作默认不是原子的，需要特殊指令（<code>ldrex</code> / <code>strex</code>） 保证。</p></div></div>
<section><h2>1. 物理本质<a href="#1-物理本质"><span>#</span></a></h2><p>在 ARM 状态下（Cortex-A7 上电默认状态），每一条指令都是严格的 32 位（4 字节）二进制码。</p><p>与 x86（变长指令）不同，ARM 的定长指令让译码器设计更加简洁高效。一条典型的 ARM 指令在物理电路上被切割为以下结构：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>Instruction</span><span> </span><span>=</span><span> [Codition </span><span>Code</span><span> (4 </span><span>bits</span><span>)] + [OpCode &amp; Flags] + [Operands (</span><span>Registers</span><span>/</span><span>Immediates</span><span>)]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>1.1 核心差异：条件执行（Conditional Execution）<a href="#11-核心差异条件执行conditional-execution"><span>#</span></a></h3><p>这是 ARM 架构最吸引人的设计之一。几乎每一条指令的高 4 位（Bits 31-28）都是条件码。这意味着：<strong>任何指令都可以是条件跳转指令的变体</strong>。</p>

<table><thead><tr><th>条件后缀</th><th>标志位要求（CPSR）</th><th>解释</th><th>汇编举例</th><th>C 语言映射</th></tr></thead><tbody><tr><td>eq</td><td><code>Z == 1</code></td><td>Equal（相等）</td><td><code>addeq r0, r1, r2</code><br />（如果相等，则相加）</td><td><code>if (a == b)</code></td></tr><tr><td>ne</td><td><code>Z == 0</code></td><td>Not Equal（不想等）</td><td><code>movne r0, #1</code><br />（如果不等，则赋值）</td><td><code>if (a != b)</code></td></tr><tr><td>al</td><td>Ignored</td><td>Always（默认）</td><td><code>add r0, r1, r2</code><br />（指令不写后缀默认就是 AL）</td><td>（无条件执行）</td></tr><tr><td>gt/lt</td><td></td><td>大于 / 小于</td><td><code>subgt ...</code></td><td><code>&gt;</code> 或 <code>&lt;</code></td></tr></tbody></table></section></section>
<section><h2>2. 寄存器模型<a href="#2-寄存器模型"><span>#</span></a></h2><p>在执行指令前，必须知道指令操作的对象。I.MX6ULL 是 Load/Store 架构。</p><ul>
<li>原则：ALU（算术逻辑单元）只能处理<strong>通用寄存器</strong>中的数据。</li>
<li>即不能对内存里的变量做运算，必须：搬运到寄存器 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 计算 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 搬回内存。</li>
</ul><p>I.MX6ULL 有 16 个核心寄存器（R0-R15）：</p><ul>
<li>R0-R12：通用寄存器。</li>
<li>R13（SP）：栈指针（当前的内存栈顶）。</li>
<li>R14（LR）：链接寄存器（保存函数调用的返回地址）。</li>
<li>R15（PC）：程序计数器（指向当前正在取指的地址）。<strong>修改 PC 就是跳转</strong>。</li>
</ul><p>在 ARM 开发中，寄存器最重要的属性是它的 AAPCS 角色（ARM Architecture Procedure Call Standard）。这决定了编译器如何使用它们。</p>

<table><thead><tr><th>寄存器</th><th>别名</th><th>硬件功能</th><th>AAPCS</th><th>谁负责保存？</th></tr></thead><tbody><tr><td>R0</td><td>a1</td><td>通用</td><td>参数 1 / 返回值</td><td>调用者（Caller）</td></tr><tr><td>R1</td><td>a2</td><td>通用</td><td>参数 2 / 返回值（64 位时）</td><td>调用者（Caller）</td></tr><tr><td>R2</td><td>a3</td><td>通用</td><td>参数 3</td><td>调用者（Caller）</td></tr><tr><td>R3</td><td>a4</td><td>通用</td><td>参数 4</td><td>调用者（Caller）</td></tr><tr><td>R4</td><td>v1</td><td>通用</td><td>局部变量</td><td>被调用者（Callee）</td></tr><tr><td>R5</td><td>v2</td><td>通用</td><td>局部变量</td><td>被调用者（Callee）</td></tr><tr><td>R6</td><td>v3</td><td>通用</td><td>局部变量</td><td>被调用者（Callee）</td></tr><tr><td>R7</td><td>v4</td><td>通用</td><td>局部变量（在 Thumb 模式下常用做栈帧指针）</td><td>被调用者（Callee）</td></tr><tr><td>R8</td><td>v5</td><td>通用</td><td>局部变量</td><td>被调用者（Callee）</td></tr><tr><td>R9</td><td>v6/SB</td><td>通用</td><td>局部变量（静态基址，平台相关）</td><td>被调用者（Callee）</td></tr><tr><td>R10</td><td>v7</td><td>通用</td><td>局部变量</td><td>被调用者（Callee）</td></tr><tr><td>R11</td><td>v8/FP</td><td>通用</td><td>帧指针（Frame Pointer）</td><td>被调用者（Callee）</td></tr><tr><td>R12</td><td>IP</td><td>通用</td><td>过程调用中间寄存器（Intra-Procedure-call scratch）</td><td>调用者（Caller）</td></tr><tr><td>R13</td><td>SP</td><td>栈指针</td><td>指向当前栈顶（Stach Pointer）</td><td>-</td></tr><tr><td>R14</td><td>LR</td><td>链接寄存器</td><td>保存函数返回地址（Link Register）</td><td>-</td></tr><tr><td>R15</td><td>PC</td><td>程序计数器</td><td>指向取指地址（Program Counter）</td><td>-</td></tr></tbody></table><p>除此之外还有两个非常重要的特殊状态寄存器 CPSR 和 SPSR。</p>

<table><thead><tr><th>寄存器</th><th>全称</th><th>解释</th><th>关键位域</th></tr></thead><tbody><tr><td>CPSR</td><td>Current Program Status Register</td><td>当前程序状态寄存器</td><td>N, Z, C, V（条件标志位）<br />I, F（中断屏蔽位）<br />Mode（运行模式位：User, SVC, IRQ 等）</td></tr><tr><td>SPSR</td><td>Saved Program Status Register</td><td>备份程序状态寄存器</td><td>仅在异常模式下存在<br />当中断发生时，硬件自动把 CPSR 里的值拷贝到 SPSR<br />当中断返回时，再从 SPSR 恢复到 CPSR</td></tr></tbody></table><p>ARMv7-A 实际上不止上述的 18 个寄存器，还有<strong>影子寄存器</strong>（Banked Registers）。</p><section><h3>2.1 为什么要区分调用者保存和被调用者保存？<a href="#21-为什么要区分调用者保存和被调用者保存"><span>#</span></a></h3><p>简单来说，这是 ARM 官方制定的规范，叫做 AAPCS（Procedure Call Standard for the ARM Architecture，ARM 架构过程调用标准）。ARM 公司不仅设计 CPU 硬件架构，还定义了软件应如何与这些硬件交互的 ABI（Application Binary Interface，应用程序二进制接口）。</p><p>制定这一规范的核心目的在于<strong>统一标准</strong>。它确保了二进制层面的兼容性——无论代码是使用 GCC 还是 ARM Compiler 编译的，只要遵循 AAPCS，它们就能无缝协作。你无需担心不同编译器在参数传递或堆栈管理上的差异，从而可以放心地调用第三方提供的预编译库。</p><p>当 Caller 调用 Callee 时，参数通过 R0-R3 传递。调用结束后，返回值通常保存在 R0 或 R0-R1 中。这就带来了一个问题：Callee 执行时不可避免地会使用寄存器，Caller 如何确保自己的数据不被覆盖？</p><p>AAPCS 为此制定了明确的规范：</p><ul>
<li><strong>易失性寄存器</strong>（Volatile Registers, R0-R3, R12）：AAPCS 规定 Callee 可以随意修改它们。因此，如果 Caller 在 R0-R3 中存放了重要数据且在调用后仍需要使用，Caller 必须自己负责在调用前将它们压栈保存（Caller-saved）。</li>
<li><strong>非易失性寄存器</strong>（Non-volatile Registers, R4-R11）：AAPCS 规定 Callee 必须保证函数返回时，这些寄存器的值与进入函数时完全一致。如果 Callee 内部需要使用 R4-R11，Callee 必须负责先将其压栈保存，并在返回前恢复（Callee-saved）。这样 Caller 就可以放心地将关键数据放在这里，无需担心被篡改。</li>
</ul><p>上述规则解决了函数间的协作问题，但<strong>中断</strong>（Interrupt）打破了这种默契。中断是异步突发的，主程序（Caller）根本不知道何时会被打断，因此无法预先保存 R0-R3 等易失性寄存器。</p><p>为了解决这个问题，ARM Cortex-M 架构引入了<strong>硬件压栈</strong>（Hardware Stacking）机制。当 CPU 响应中断时，硬件会自动将 Caller 上下文（包括 R0-R3、R12、LR、PC 和 xPSR）。这一过程无需软件干预。</p><p>因此，进入中断服务函数（ISR）后，寄存器已经被硬件保护好了。ISR 此时只需像普通函数一样，遵守 AAPCS 规则处理 R4-R11（即如果用到则保存）即可。这种硬件机制极大降低了中断延迟，也简化了软件设计。</p></section><section><h3>2.2 R12（IP）是干什么的？<a href="#22-r12ip是干什么的"><span>#</span></a></h3><p>它是一个被很多人容易忽略的寄存器，主要用于链接器（Linker）插入的代码。当你的程序非常大，<code>bl target</code> 跳转的目标太远（超过 <span><span>±32MB\pm 32\text{MB}</span><span><span><span></span><span>±</span><span>32</span><span><span>MB</span></span></span></span></span>）时，<code>bl</code> 指令够不着。此时，链接器会自动插入一段小代码（Veneer/Trampoline），先跳转到 R12 指向的中转站，再由 R12 赋值给 PC 进行长跳转。</p></section><section><h3>2.3 R15（PC）为什么有 +8 现象？<a href="#23-r15pc为什么有-8-现象"><span>#</span></a></h3><p>在 ARM 状态（32 位指令）下，读取 PC 的值，通常是当前指令地址 + 8。这则是因为 CPU 的流水线（Pipeline）设计，当 CPU 执行当前指令时，取指单元已经在取下下条指令了（当前指令地址 +8 地址处）。</p></section></section>
<section><h2>3. 指令集讲解<a href="#3-指令集讲解"><span>#</span></a></h2><section><h3>3.1 数据处理指令（Data Processing）<a href="#31-数据处理指令data-processing"><span>#</span></a></h3><div><div><div></div><div>Note</div></div><div><p><strong>核心逻辑</strong>：ALU 运算，结果写回寄存器，并可选更新 CPSR（如果指令后加 <code>s</code>，如 <code>adds</code>）</p></div></div><section><h4>3.1.1 算术类<a href="#311-算术类"><span>#</span></a></h4>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>C 语言映射</th></tr></thead><tbody><tr><td><code>add</code><br />(Add)</td><td>加法。</td><td><code>add r0, r1, r2</code><br />(<code>R0 = R1 + R2</code>)</td><td><code>a = b + c;</code></td></tr><tr><td><code>sub</code><br />(Subtract)</td><td>减法。</td><td><code>sub r0, r1, #1</code><br />(<code>R0 = R1 - 1</code>)</td><td><code>a = b - 1;</code></td></tr><tr><td><code>rsb</code><br />(Reverse Substract)</td><td>逆向减法。<br />用于 R0 = 常数 - Rx 的场景。</td><td><code>rsb r0, r1, #0</code><br />(<code>R0 = 0 - R1</code>)</td><td><code>a = -b;</code></td></tr><tr><td><code>adc</code><br />(Add with Carry)</td><td>带进位加法。<br />用于实现 64 位加法的高 32 位运算。</td><td><code>adc r0, r1, r2</code><br />(<code>R0 = R1 + R2 + Carry</code>)</td><td><code>(long long )a + (long long)b</code> 的高位部分</td></tr><tr><td><code>sbc</code><br />(Subtract with Carry)</td><td>带借位减法。<br />用于 64 位减法。</td><td><code>sbc r0, r1, r2</code><br />(<code>R0 = R1 - R2 - !Carry</code>)</td><td>64 位减法的高位部分</td></tr><tr><td><code>mul</code><br />(Multiply)</td><td>32 位乘法（结果取低 32 位）</td><td><code>mul r0, r1, r2</code><br />(<code>R0 = R1 * R2</code>)</td><td><code>int a = b * c;</code></td></tr><tr><td><code>mla</code><br />(Multiply Accumulate)</td><td>乘法运算。<br />DSP 算法核心指令。</td><td><code>mul r0, r1, r2, r3</code><br />(<code>R0 = R1 * R2 +  R3</code>)</td><td><code>a = (b * c) + d;</code></td></tr><tr><td><code>udiv</code><br />(Unsigned Divide)</td><td>无符号除法。</td><td><code>udiv r0, r1, r2</code></td><td><code>unsigned a = b / c;</code></td></tr><tr><td><code>sdiv</code><br />(Signed Divided)</td><td>有符号除法。</td><td><code>sdiv r0, r1, r2</code></td><td><code>int a = b / c;</code></td></tr></tbody></table></section><section><h4>3.1.2 逻辑类<a href="#312-逻辑类"><span>#</span></a></h4>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>C 语言映射</th></tr></thead><tbody><tr><td><code>and</code><br />(Logical AND)</td><td>按位与。<br />常用于<strong>提取</strong>某几位。</td><td><code>and r0, r1, #0xff</code></td><td><code>a = b &amp; 0xff;</code></td></tr><tr><td><code>orr</code><br />(Logical OR)</td><td>按位或。<br />常用于<strong>置位</strong>（设为 1）。</td><td><code>orr r0, r0, #(1&lt;&lt;3)</code></td><td>`r0</td></tr><tr><td><code>eor</code><br />(Exclusive OR)</td><td>按位异或。<br />常用于<strong>翻转</strong>某几位。</td><td><code>eor r0, r0, #1</code></td><td><code>a = b ^ 1;</code></td></tr><tr><td><code>bic</code><br />(Bit Clear)</td><td><strong>位清除</strong>。<br /><code>and</code> 的反向操作：<code>dest = op1 and (not op2)</code></td><td><code>bic r0, r0, #(1&lt;&lt;3)</code></td><td><code>r0 &amp;= ~(1 &lt;&lt; 3);</code></td></tr></tbody></table></section><section><h4>3.1.3 数据搬运<a href="#313-数据搬运"><span>#</span></a></h4>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>C 语言映射</th></tr></thead><tbody><tr><td><code>mov</code><br />(Move)</td><td>寄存器间传值，或立即数赋值。</td><td><code>mov r0, r1</code></td><td><code>a = b;</code></td></tr><tr><td><code>mvn</code><br />(Move Not)</td><td><strong>按位取反</strong>后传送。</td><td><code>mvn r0, #0</code><br />(<code>r0 = 0xffffffff</code>)</td><td><code>a = ~0;</code></td></tr></tbody></table><div><div><div></div><div>Warning</div></div><div><p>ARM 指令是定长 32 位的。除去操作码和条件码，留给立即数的空间只有 12 位。并且这 12 位还不是直接存数字，而是被拆分位 8 位常数 + 4 位循环右移。</p><p>这就意味着在向寄存器写入某些立即数时，例如 <code>mov r0, #0x12345678</code> 是会失败的，因为 <code>0x12345678</code> 这个立即数太乱了，无法通过 8 位常数 + 4 位循环右移凑出来。</p><p>解决办法是使用 <code>ldr r0, =0x12345678</code>。此时，汇编器会悄悄在代码段的末尾（Literal Pool）找个地方放 <code>0x12345678</code>，然后把这条指令翻译成去那个地址读数据。</p></div></div></section><section><h4>3.1.4 比较指令<a href="#314-比较指令"><span>#</span></a></h4><div><div><div></div><div>Note</div></div><div><p><strong>不保存运算结果</strong>，只更新 CPSR 的标志位（N, Z, C, V），为后面的条件执行做铺垫。</p></div></div>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>C 语言映射</th></tr></thead><tbody><tr><td><code>cmp</code><br />(Compare)</td><td>本质是 <code>sub</code>。<br />比较大小。</td><td><code>cmp r0, #5</code><br />(<code>r0 - 5</code>，更新 Z 标志位)</td><td><code>if (a &gt;= 5)</code></td></tr><tr><td><code>tst</code><br />(Test)</td><td>本质是 <code>and</code>。<br />测试某位是否为 1。</td><td><code>tst r0, #(1&lt;&lt;3)</code><br />(<code>r0 &amp; 0x8</code>，更新 Z 标志位)</td><td><code>if (a &amp; (1&lt;&lt;3))</code></td></tr><tr><td><code>teq</code><br />(Test Equivalence)</td><td>本质是 <code>eor</code>。<br />测试两个数是否相等（不影响进位标志）。</td><td><code>teq r0, r1</code></td><td>常用于汇编手写检查</td></tr></tbody></table></section><section><h4>3.1.5 桶形移位器（Barrel Shifter）<a href="#315-桶形移位器barrel-shifter"><span>#</span></a></h4><p>在 ARM 中，移位操作不是独立指令，而是数据进入 ALU 之前的<strong>预处理</strong>。它是免费的。指令 <code>add r0, r1, r2, lsl #2</code> 的物理含义是：</p><span><span><span>R0=R1+(R2≪2)\text{R0} = \text{R1} + (\text{R2} \ll 2)</span><span><span><span></span><span><span>R0</span></span><span></span><span>=</span><span></span></span><span><span></span><span><span>R1</span></span><span></span><span>+</span><span></span></span><span><span></span><span>(</span><span><span>R2</span></span><span></span><span>≪</span><span></span></span><span><span></span><span>2</span><span>)</span></span></span></span></span><p>这在单周期内完成，这就是为什么 ARM 处理数组索引（地址 = 基址 + 索引 <span><span>×\times</span><span><span><span></span><span>×</span></span></span></span> 4）特别快。</p>

<table><thead><tr><th>模式</th><th>物理动作</th><th>C 标志位更新</th><th>数学含义</th><th>C 语言映射</th><th>典型应用场景</th></tr></thead><tbody><tr><td><code>lsl</code><br />(Logical Shift Left)</td><td><strong>左移</strong>。<br />低位补 0，高位移出</td><td>最后移出的位</td><td><span><span>×2n\times 2^n</span><span><span><span></span><span>×</span><span><span>2</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span></td><td><code>x &lt;&lt; n</code></td><td>乘法优化、位字段对齐、生成掩码</td></tr><tr><td><code>lsr</code><br />(Logical Shift Right)</td><td><strong>右移</strong>。<br />高位补 0，低位移出</td><td>最后移出的位</td><td><span><span>/2n/2^n</span><span><span><span></span><span>/</span><span><span>2</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span><br />（无符号）</td><td><code>(unsigned)x &gt;&gt; n</code></td><td>无符号数除法、读取特定位段</td></tr><tr><td><code>asr</code><br />(Arithmetic Shift Right)</td><td><strong>算术右移</strong>。<br />高位补<strong>符号位</strong>，低位移出</td><td>最后移出的位</td><td><span><span>/2n/2^n</span><span><span><span></span><span>/</span><span><span>2</span><span><span><span><span><span><span></span><span><span>n</span></span></span></span></span></span></span></span></span></span></span><br />（有符号）</td><td><code>(signed)x &gt;&gt; n</code></td><td>有符号数除法</td></tr><tr><td><code>ror</code><br />(Rotate Right)</td><td><strong>循环右移</strong>。<br />低位移出后，<strong>填补到高位</strong></td><td>最后移出的位</td><td>-</td><td>`(x &gt;&gt; n)</td><td>(x &lt;&lt; (32-n))`</td></tr><tr><td><code>rrx</code><br />(Rotate Right with Extend)</td><td><strong>带扩展循环右移</strong>。<br />33 位循环<br />（32 位寄存器 + 1 位 Carry）</td><td>寄存器移出的 LSB</td><td>-</td><td>无</td><td>大数运算、多精度算术</td></tr></tbody></table></section></section><section><h3>3.2 内存访问指令（Memory Access）<a href="#32-内存访问指令memory-access"><span>#</span></a></h3><p>这是 CPU 与外部世界（DDR 内存、GPIO 控制器等）交互的唯一桥梁。</p><section><h4>3.2.1 单寄存器<a href="#321-单寄存器"><span>#</span></a></h4>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>C 语言映射</th></tr></thead><tbody><tr><td><code>ldr</code><br />(Load Register)</td><td>从内存读 4 字节到寄存器。</td><td><code>ldr r0, [r1]</code></td><td><code>r0 = *r1;</code></td></tr><tr><td><code>str</code><br />(Store Register)</td><td>把寄存器的值写入内存 4 字节。</td><td><code>str r0, [r1]</code></td><td><code>*r1 = r0;</code></td></tr><tr><td><code>ldrb</code> (8 bit)<br />(Load Byte)</td><td>读 1 字节，高 24 位<strong>补零</strong>。</td><td><code>ldrb r0, [r1]</code></td><td><code>r0 = (unsigned char)*r1;</code></td></tr><tr><td><code>ldrh</code> (16 bit)<br />(Load Halfword)</td><td>读 2 字节，高 16 位<strong>补零</strong>。</td><td><code>ldrh r0, [r1]</code></td><td><code>r0 = (unsigned short)*r1;</code></td></tr><tr><td><code>ldrsb</code><br />(Load Signed Byte)</td><td>读 1 字节，高 24 位<strong>补符号位</strong>。</td><td><code>ldrsb r0, [r1]</code></td><td><code>r0 = (char)*r1;</code></td></tr><tr><td><code>ldrsh</code><br />(Load Signed Halfword)</td><td>读 2 字节，高 16 位<strong>补符号位</strong>。</td><td><code>ldrsh r0, [r1]</code></td><td><code>r0 = (short)*r1;</code></td></tr></tbody></table></section><section><h4>3.2.2 寻址模式<a href="#322-寻址模式"><span>#</span></a></h4>

<table><thead><tr><th>寻址模式</th><th>汇编举例</th><th>C 语言映射</th><th>状态</th></tr></thead><tbody><tr><td>立即数偏移<br />（Offset）</td><td><code>ldr r0, [r1, #4]</code></td><td><code>r0 = *(r1 + 1);</code></td><td><code>r1</code> 保持不变</td></tr><tr><td>前索引<br />（Pre-indexed）</td><td><code>ldr r0, [r1, #4]!</code></td><td><code>r0 = *(++ptr);</code></td><td><code>r1</code> 在访问内存<strong>前</strong>更新为 <code>r1 + 4</code></td></tr><tr><td>后索引<br />（Post-indexed）</td><td><code>ldr r0, [r1], #4</code></td><td><code>r0 = *(ptr++);</code></td><td>访问内存<strong>后</strong>，<code>r1</code> 更新为 <code>r1 + 4</code></td></tr></tbody></table></section><section><h4>3.2.3 多寄存器<a href="#323-多寄存器"><span>#</span></a></h4><div><div><div></div><div>Note</div></div><div><p>这是 ARM 实现<strong>栈</strong>（Stack）和<strong>上下文切换</strong>（Context Switch）的基石。</p></div></div>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>场景</th></tr></thead><tbody><tr><td><code>ldm</code><br />(Load Multiple)</td><td>内存 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 多个寄存器。</td><td><code>ldm r0, {r4-r6, pc}</code></td><td>恢复现场并返回</td></tr><tr><td><code>stm</code><br />(Store Multiple)</td><td>多个寄存器 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 内存。</td><td><code>stm r0, {r4-r6, lr}</code></td><td>保存现场</td></tr></tbody></table><p>说明：</p><ul>
<li><code>ldm r0, {r4-r6, pc}</code>：从以 <code>r0</code> 为基址的内存地址开始，依次加载数据到寄存器 <code>r4</code>、<code>r5</code>、<code>r6</code> 和 <code>pc</code>。</li>
<li><code>stm r0, {r4-r6, lr}</code>：从寄存器 <code>r4</code>、<code>r5</code>、<code>r6</code> 和 <code>lr</code> 中，依次将数据存储到以 <code>r0</code> 为基址的内存地址。</li>
</ul><section><h5>后缀模式<a href="#后缀模式"><span>#</span></a></h5><p>虽然有 <code>ia</code>、<code>ib</code>、<code>da</code> 和 <code>db</code> 四种，但 ARM 官方推荐在操作栈时使用别名，方便记忆：</p><ul>
<li><code>stmfd</code> (Full Descending) = <code>stmdb</code> (Decrement Before)：入栈（压栈）。</li>
<li><code>ldmfd</code> (Full Descending) = <code>ldmia</code> (Increment After)：出栈。</li>
<li><code>push {reglist}</code> 等价于 <code>stmdb sp!, {reglist}</code>。</li>
<li><code>pop {reglist}</code> 等价于 <code>ldmia sp!, {reglist}</code>。</li>
</ul></section></section></section><section><h3>3.3 分支与控制流（Branch &amp; Control）<a href="#33-分支与控制流branch--control"><span>#</span></a></h3><p>打破顺序执行，实现 <code>if</code>、<code>for</code>、<code>function call</code>。</p>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>C 语言映射</th></tr></thead><tbody><tr><td><code>b</code><br />(Branch)</td><td>相对跳转（范围 <span><span>±32MB\pm 32\text{MB}</span><span><span><span></span><span>±</span><span>32</span><span><span>MB</span></span></span></span></span>）</td><td><code>b label</code></td><td><code>goto label;</code></td></tr><tr><td><code>bl</code><br />(Branch with Link)</td><td>带连接跳转。<br /><strong>保存下一条指令地址到 <code>lr</code></strong>。</td><td><code>bl func</code></td><td><code>func();</code></td></tr><tr><td><code>bx</code><br />(Branch Exchange)</td><td>跳转并根据目标地址最低位切换状态<br />（ARM <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> Thumb）。</td><td><code>bx lr</code></td><td><code>return;</code>（函数返回）</td></tr><tr><td><code>blx</code><br />(Branch Link Exchange)</td><td><code>bl</code> + <code>bx</code> 的结合。<br />调用不同指令集的函数。</td><td><code>blx r0</code></td><td>函数指针调用</td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p>还可以直接修改 PC 寄存器实现跳转，例如 <code>mov pc, lr</code>，这是函数返回的底层实现。</p></div></div></section><section><h3>3.4 系统与状态指令（System &amp; Status）<a href="#34-系统与状态指令system--status"><span>#</span></a></h3><p>这部分在使用上层语言实现业务逻辑时几乎看不到，但是对于操作系统而言至关重要。</p><section><h4>3.4.1 状态寄存器访问<a href="#341-状态寄存器访问"><span>#</span></a></h4>

<table><thead><tr><th>指令</th><th>解释</th><th>汇编举例</th><th>场景</th></tr></thead><tbody><tr><td><code>mrs</code><br />(Move Register from Status)</td><td>读取 CPSR/SPSR 到通用寄存器。</td><td><code>mrs r0, cpsr</code></td><td>读取当前中断状态</td></tr><tr><td><code>msr</code><br />(Move Status from Register)</td><td>将通用寄存器写会 CPSR/SPSR。</td><td><code>msr cpsr_c, r0</code></td><td>开/关中断</td></tr></tbody></table></section><section><h4>3.4.2 异常与中断生成<a href="#342-异常与中断生成"><span>#</span></a></h4>

<table><thead><tr><th>指令</th><th>解释</th><th>场景</th></tr></thead><tbody><tr><td><code>svc</code><br />(Supervisor Call)</td><td><strong>软中断</strong>。<br />CPU 进入 SVC 模式。<br />Linux 中用于从用户态（User）陷入内核态（Kernel）。</td><td>系统调用（<code>open</code>，<code>read</code> 等）。</td></tr><tr><td><code>bkpt</code><br />(Breakpoint)</td><td>断点指令。<br />执行到此时 CPU 暂停并通知调试器。</td><td>JTAG/GDB 调试。</td></tr></tbody></table></section><section><h4>3.4.3 协处理器指令<a href="#343-协处理器指令"><span>#</span></a></h4><p>I.MX6ULL 中，CP15 协处理器控制着 MMU、Cache 和中断向量表基地址。</p>

<table><thead><tr><th>指令</th><th>解释</th><th>助记法</th></tr></thead><tbody><tr><td><code>mcr</code><br />(Move to Coprocessor from Register)</td><td>CPU <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> CP15<br />（写配置）</td><td>R（Register）在最后，表示源</td></tr><tr><td><code>mrc</code><br />(Move to Register from Coprocessor)</td><td>CP15 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> CPU<br />（读状态）</td><td>C（Coprocessor）在最后，表示源</td></tr></tbody></table><p>例如：<code>mcr p15, 0, r0, c1, c0, 0</code>（开启 MMU）。</p><div><div><div></div><div>Note</div></div><div><p><strong>协处理器</strong>（Coprocessor）时依附于 CPU 主核的独立电路单元。ARM 主核（ALU）只管算逻辑和搬砖，像 MMU（内存映射管理）、Cache 配置这些脏活累活，都丢给 CP15 协处理器；浮点运算丢给 CP10/CP11。</p></div></div></section><section><h4>3.4.4 内存屏障（Memory Barriers）<a href="#344-内存屏障memory-barriers"><span>#</span></a></h4>

<table><thead><tr><th>指令</th><th>解释</th><th>场景</th></tr></thead><tbody><tr><td><code>dmb</code><br />(Data Memory Barrier)</td><td>确保指令前的内存访问在指令后的内存访问之前完成。</td><td>操作 DMA 描述符时</td></tr><tr><td><code>dsb</code><br />(Data Synchronization Barrier)</td><td>比 DMB 更强。<br />确保前面的访存全部完成，CPU 才会执行下一条指令。</td><td>修改页表后</td></tr><tr><td><code>isb</code><br />(Instruction Synchronization Barrier)</td><td>冲刷流水线。确保重新取指。</td><td>修改了代码段或中断向量表后</td></tr></tbody></table></section></section><section><h3>3.5 同步与原子操作（Synchronization）<a href="#35-同步与原子操作synchronization"><span>#</span></a></h3><p>对于多任务操作系统，需要处理竞态条件。</p>

<table><thead><tr><th>指令</th><th>解释</th><th>C 语言映射</th></tr></thead><tbody><tr><td><code>ldrex</code><br />(Load Register Exclusive)</td><td>读取并设置独占监视器</td><td><code>atomic_load</code> (C11)</td></tr><tr><td><code>strex</code><br />(Store Register Exclusive)</td><td>如果监视状态未被破坏，则写入。<br />返回 0 成功，1 失败。</td><td><code>atomic_compare_exchange</code> (C11)</td></tr><tr><td><code>clrex</code><br />(Clear Exclusive)</td><td>清除监视器状态。</td><td>上下文切换时清理残余状态</td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p>ARM 没有 x86 的 <code>lock</code> 总线锁，它使用 “<strong>独占监视器</strong>” 硬件。</p><ul>
<li><code>ldrex</code> 读取数据的同时标记该地址为 “被我监视”。</li>
<li><code>strex</code> 尝试写入。如果期间没有其他核心 / DMA 修改过该地址，写入成功；否则写入失败。</li>
</ul><p>这会实现 <code>mutex</code>（互斥锁）和 <code>semaphore</code>（信号量）的基石。</p></div></div><div><div><div></div><div>Note</div></div><div><p>互斥锁（Mutex）&amp; 信号量（Semaphore）：</p><ul>
<li>互斥锁（厕所钥匙）：只有一个资源（厕所）。你拿了钥匙进去锁门，别惹只能在门口排队（阻塞），直到你出来还钥匙。用户保护<strong>临界区</strong>。</li>
<li>信号量（餐厅叫号）：有 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 个资源（空桌子）。来一个人 <span><span>N−1N-1</span><span><span><span></span><span>N</span><span></span><span>−</span><span></span></span><span><span></span><span>1</span></span></span></span>，走一个人 <span><span>N+1N+1</span><span><span><span></span><span>N</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span>。当 <span><span>N=0N=0</span><span><span><span></span><span>N</span><span></span><span>=</span><span></span></span><span><span></span><span>0</span></span></span></span> 时，新来的人必须等到。</li>
</ul><p>这两个都是软件概念，但底层必须由<strong>原子指令</strong>支持。</p></div></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/wsl2-image-migration-guide/</id>
      <title type="text">如何将 WSL2 镜像无损迁移至非系统盘</title>
      <published>2026-03-14T05:16:00.000Z</published>
      <updated>2026-03-14T05:16:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/wsl2-image-migration-guide/"/>
      <summary type="text">详细记录在 Windows 11 环境下，将默认安装在 C 盘的 WSL2 Ubuntu 镜像无损迁移至其他数据盘的完整过程，包含导出、导入、默认用户配置及原实例清理，有效释放系统盘空间。</summary>
      <content type="html"><![CDATA[<section><h2>1 引言<a href="#1-引言"><span>#</span></a></h2><p>随着开发环境的日益庞大，默认安装在 <code>C:\</code> 盘的 WSL2 (Windows Subsystem for Linux) 镜像体积也会不断膨胀。本文将分享如何通过命令行操作，将 WSL 迁移至 <code>E:\</code> 盘乃至其他盘符，彻底释放系统盘空间。</p><section><h3>1.1 笔者环境<a href="#11-笔者环境"><span>#</span></a></h3>

<table><thead><tr><th>名称</th><th>版本</th></tr></thead><tbody><tr><td>Windows 11 Pro</td><td>25H2 26200.8037</td></tr><tr><td>PWSH</td><td>7.5.5</td></tr><tr><td>WSL</td><td>2.6.3.0</td></tr><tr><td>Ubuntu</td><td>22.04 LTS</td></tr><tr><td>Ubuntu</td><td>24.04 LTS</td></tr></tbody></table><div><div><div></div><div>Note</div></div><div><p><strong>注意</strong>：这里的 PWSH 并不是指 Windows 系统内置的经典 PowerShell，而是跨平台的开源项目 <a href="https://github.com/PowerShell/PowerShell" target="_blank">PowerShell Core</a>。下文中使用 PowerShell 指代系统默认版本，PWSH 指代开源版本。</p></div></div></section></section>
<section><h2>2 准备工作<a href="#2-准备工作"><span>#</span></a></h2><section><h3>2.1 确认当前安装状态<a href="#21-确认当前安装状态"><span>#</span></a></h3><p>如果你还没有安装镜像，或想查看当前支持的发行版，可以使用以下命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 查看支持的在线发行版</span></div></div><div><div><div>2</div></div><div><span>wsl</span><span> </span><span>--list</span><span> </span><span>--online</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 安装 Ubuntu 镜像</span></div></div><div><div><div>5</div></div><div><span>wsl</span><span> </span><span>--install</span><span> </span><span>Ubuntu-22.04</span></div></div><div><div><div>6</div></div><div><span>wsl</span><span> </span><span>--install</span><span> </span><span>Ubuntu-24.04</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在终端输入以下命令，查看当前已安装的发行版：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wsl</span><span> </span><span>-l</span><span> </span><span>-v</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2.1 确定原镜像及目标路径<a href="#21-确定原镜像及目标路径"><span>#</span></a></h3><p>WSL2 的虚拟磁盘文件通常为 <code>ext4.vhdx</code>。你可以使用强大的文件搜索工具 <a href="https://www.voidtools.com/zh-cn/" target="_blank">Everything</a> 来定位它们。不同版本的 Ubuntu 默认安装路径可能略有不同。</p>

<table><thead><tr><th>镜像</th><th>原始路径</th></tr></thead><tbody><tr><td>Ubuntu 22.04 LTS</td><td><code>C:\Users\&lt;your_windows_username&gt;\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu22.04LTS_...\LocalState</code></td></tr><tr><td>Ubuntu 24.04 LTS</td><td><code>C:\Users\&lt;your_windows_username&gt;\AppData\wsl\{UUID}</code></td></tr></tbody></table><p>可以看到这里两者的路径有着细微差别，Ubuntu 22.04 LTS 安装在 <code>Local\Packages</code> 下，而 Ubuntu 24.04 LTS 却安装在 <code>Local</code> 同级的 <code>wsl</code> 下。</p><p>具体原因犹未可知，笔者暂时没有时间去查阅官方文档，但是并不影响后续步骤以及日常使用。</p></section><section><h3>2.2 建立新的位置<a href="#22-建立新的位置"><span>#</span></a></h3><p>在上一步中已经获取到了原始的位置，根据笔者的风格，迁移之后的镜像路径应该尽可能维持原来的目录结构，但是笔者还是做了一点细微的修改，迁移路径具体如下：</p>

<table><thead><tr><th>镜像</th><th>迁移路径</th></tr></thead><tbody><tr><td>Ubuntu 22.04 LTS</td><td><code>E:\Users\&lt;your_windows_username&gt;\AppData\wsl\ubuntu2204</code></td></tr><tr><td>Ubuntu 24.04 LTS</td><td><code>E:\Users\&lt;your_windows_username&gt;\AppData\wsl\ubuntu2404</code></td></tr></tbody></table><div><div><div></div><div>Warning</div></div><div><p>请在后续的命令行中将 <code>&lt;your_windows_username&gt;</code> 替换为你的实际 Windows 用户名。</p></div></div><p>在新版的 PWSH 中可以直接使用以下命令创建目录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>mkdir </span><span>-</span><span>p E:\Users\&lt;your_windows_username&gt;\AppData\wsl\ubuntu2204\</span></span></div></div><div><div><div>2</div></div><div><span><span>mkdir </span><span>-</span><span>p E:\Users\&lt;your_windows_username&gt;\AppData\wsl\ubuntu2404\</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>或者以下命令（支持在 PowerShell 中使用）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>New-Item</span><span><span> </span><span>-</span><span>Path E:\Users\&lt;your_windows_username&gt;\AppData\wsl\ubuntu2204\ </span><span>-</span><span>ItemType Directory</span></span></div></div><div><div><div>2</div></div><div><span>New-Item</span><span><span> </span><span>-</span><span>Path E:\Users\&lt;your_windows_username&gt;\AppData\wsl\ubuntu2404\ </span><span>-</span><span>ItemType Directory</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>3 迁移镜像<a href="#3-迁移镜像"><span>#</span></a></h2><section><h3>3.1 导出原 WSL 分发<a href="#31-导出原-wsl-分发"><span>#</span></a></h3><p>在进行导出操作前，<strong>强烈建议先停止 WSL2 中的所有工作并彻底关闭所有正在运行的 WSL 实例</strong>以防止数据损坏：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wsl</span><span> </span><span>--shutdown</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>接着，将现有的系统状态导出为 <code>.tar</code> 压缩包：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wsl</span><span> </span><span>--export</span><span> </span><span>Ubuntu-22.04</span><span> </span><span>E:</span><span>\U</span><span>sers</span><span>\&lt;</span><span>your_windows_username&gt;</span><span>\A</span><span>ppData</span><span>\w</span><span>sl</span><span>\u</span><span>buntu2204.tar</span></div></div><div><div><div>2</div></div><div><span>wsl</span><span> </span><span>--export</span><span> </span><span>Ubuntu-24.04</span><span> </span><span>E:</span><span>\U</span><span>sers</span><span>\&lt;</span><span>your_windows_username&gt;</span><span>\A</span><span>ppData</span><span>\w</span><span>sl</span><span>\u</span><span>buntu2404.tar</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3.2 将压缩包导入新位置<a href="#32-将压缩包导入新位置"><span>#</span></a></h3><p>使用 <code>wsl --import</code> 命令将刚才导出的 <code>.tar</code> 文件导入到我们在 E 盘创建的新目录中。为了方便区分，我们将导入后的新实例命名为小写的 <code>ubuntu2204</code> 和 <code>ubuntu240</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wsl</span><span> </span><span>--import</span><span> </span><span>ubuntu2204</span><span> </span><span>E:</span><span>\U</span><span>sers</span><span>\&lt;</span><span>your_windows_username&gt;</span><span>\A</span><span>ppData</span><span>\w</span><span>sl</span><span>\u</span><span>buntu2204</span><span> </span><span>E:</span><span>\U</span><span>sers</span><span>\a</span><span>mara</span><span>\A</span><span>ppData</span><span>\w</span><span>sl</span><span>\u</span><span>buntu2204.tar</span></div></div><div><div><div>2</div></div><div><span>wsl</span><span> </span><span>--import</span><span> </span><span>ubuntu2404</span><span> </span><span>E:</span><span>\U</span><span>sers</span><span>\&lt;</span><span>your_windows_username&gt;</span><span>\A</span><span>ppData</span><span>\w</span><span>sl</span><span>\u</span><span>buntu2404</span><span> </span><span>E:</span><span>\U</span><span>sers</span><span>\a</span><span>mara</span><span>\A</span><span>ppData</span><span>\w</span><span>sl</span><span>\u</span><span>buntu2404.tar</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Note</div></div><div><p>利用 <code>wsl --import</code>，你可以将同一个 <code>.tar</code> 包导入多次并赋予不同名称，从而实现多开同一个发行版。虽然在隔离环境方面类似于 Docker，但 WSL2 原生支持 WSLg，可以直接运行 Linux 图形化应用，这在某些桌面开发场景下比 Docker 更具优势。</p></div></div></section><section><h3>3.3 恢复默认用户<a href="#33-恢复默认用户"><span>#</span></a></h3><p>导入完成并启动新的分发后，你可能会发现默认登录用户变成了 <code>root</code>。我们需要将其改回你原本的 Linux 用户名。如果你的分发并没有发现此类问题，可以跳过本节。</p><p>进入新的 WSL 实例：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wsl</span><span> </span><span>~</span><span> </span><span>-d</span><span> </span><span>ubuntu2204</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>使用 <code>nano</code> 编辑器修改 WSL 配置文件（如果你更习惯图形化界面，也可以执行 <code>apt update &amp;&amp; apt install gedit -y</code> 后使用 <code>gedit</code>）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>sudo</span><span> </span><span>nano</span><span> </span><span>/etc/wsl.conf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>在文件中添加或修改以下内容（将 <code>&lt;your_linux_username&gt;</code> 替换为你在 WSL 中的用户名）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[user]</span></div></div><div><div><div>2</div></div><div><span>default</span><span><span>=</span><span>&lt;your_linux_username&gt;</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保存并退出：依次按下 <kbd>Ctrl</kbd> + <kbd>X</kbd>，输入 <kbd>Y</kbd> 确认保存，最后按 <kbd>Enter</kbd> 退出。</p><p>退出当前 WSL 会话，并在 Windows 终端中重启改实例使配置生效：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 终止实例</span></div></div><div><div><div>2</div></div><div><span>wsl</span><span> </span><span>--terminate</span><span> </span><span>ubuntu2204</span></div></div><div><div><div>3</div></div><div><span># 重新进入验证用户是否恢复正常</span></div></div><div><div><div>4</div></div><div><span>wsl</span><span> </span><span>~</span><span> </span><span>-d</span><span> </span><span>ubuntu2204</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3.4 设置为默认分发并清理善后<a href="#34-设置为默认分发并清理善后"><span>#</span></a></h3><p>如果一切验证无误，将新的实例设置为系统的默认 WSL 分发：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wsl</span><span> </span><span>--set-default</span><span> </span><span>ubuntu2404</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>确认无误后，就可以卸载 C 盘中的旧分发了：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wsl</span><span> </span><span>--unregister</span><span> </span><span>Ubuntu-22.04</span></div></div><div><div><div>2</div></div><div><span>wsl</span><span> </span><span>--unregister</span><span> </span><span>Ubuntu-24.04</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后，删除存放在 E 盘的过渡文件 <code>.tar</code> 压缩包以释放空间。此时再次执行 <code>wsl -l -v</code>，你应该能看到干净清爽的列表了。</p></section></section>
<section><h2>4 总结<a href="#4-总结"><span>#</span></a></h2><p>迁移安装最大的好处就是能显著减小 C 盘的存储压力。不过，如果你的物理磁盘总容量本身较小（比如只有 512GB 单盘），笔者依然更推荐直接重装系统并只保留一个 C 盘分区，统一管理存储空间。</p><p>此外，根据<a href="https://bfmhno3.github.io/tutorial/wsl2-image-migration-guide/">微软官方文档的建议</a>，为了获得最佳的文件 I/O 性能，进行 Linux 相关的开发时，请务必将项目文件直接存储在 WSL 的 Linux 文件系统（如 <code>~</code> 目录下），而不是跨文件系统存储在 Windows 盘符（如 <code>/mnt/c</code>）中。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ol>
<li><a href="https://superuser.com/questions/1701175/installing-ubuntu-on-mnt-d-with-wsl" target="_blank">Installing Ubuntu on /mnt/d with WSL</a></li>
<li><a href="https://github.com/OsbertWang/install-latex-guide-zh-cn" target="_blank">install-latex-guide-zh-cn (附录B.5 迁移 WSL 的安装位置)</a></li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/stack-in-cpp/</id>
      <title type="text">C++ 中的 stack</title>
      <published>2026-02-19T02:45:00.000Z</published>
      <updated>2026-02-19T02:45:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/stack-in-cpp/"/>
      <summary type="text">本文从本质需求出发解释栈（LIFO）为何存在，串联函数调用、递归、撤销与括号匹配等典型场景，并结合 C++ STL 的 std::stack 用法、异常安全设计与底层容器选择，最后配合多道题目示例完成从概念到实战的闭环。</summary>
      <content type="html"><![CDATA[<p>要真正理解<strong>栈</strong>这种结构，意味着我们要剥离事物表象，追究其最本质的问题，即<strong>我们到底面临什么样的数据处理需求，才导致了 “栈” 这种结构的诞生？</strong></p>
<p>我们不需要把栈仅仅看作是教科书上的定义，而是把它看作是<strong>为了解决特定时序问题而设计的一种 “受限” 模型</strong>。</p>
<section><h2>1. 什么是栈？<a href="#1-什么是栈"><span>#</span></a></h2><p>从本质上来说，计算机处理任务通常有两种模式：</p><ol>
<li><strong>并行/随机</strong>：所有任务都在桌面上铺开，想拿哪个拿哪个（类似数组或哈希表）。</li>
<li><strong>嵌套/依赖</strong>：任务 B 必须依赖任务 A 的环境，任务 C 又依赖 B。要完成 A，必须先挂起 A 去做 B；要做 B，必须先挂起 B 去做 C。</li>
</ol><p><strong>栈</strong>（Stack）就是为了解决第二种模式而诞生的。</p><section><h3>1.1 核心定义：LIFO<a href="#11-核心定义lifo"><span>#</span></a></h3><p>栈是一种<strong>线性表</strong>，但它是一种受限的线性表。它的核心逻辑是<strong>后进先出</strong>（Last In, First Out - LIFO）。</p><p>你可以把它想象成<strong>叠盘子</strong>：</p><ul>
<li>压入（Push）：你只能把新盘子放在最上面。</li>
<li>弹出（Pop）：你只能取走最上面的盘子。</li>
<li>查看（Top/Peek）：你只能看到最上面盘子的花纹，无法直接看到底部的盘子。</li>
</ul><p></p><figure><img src="/assets/images/a_stack_of_plates.jpg" alt="生活中的栈模型：想要拿到底部的盘子，必须先移走顶部的盘子" /><figcaption>生活中的栈模型：想要拿到底部的盘子，必须先移走顶部的盘子</figcaption></figure><p></p></section><section><h3>1.2 为什么它是 “受限” 的？<a href="#12-为什么它是-受限-的"><span>#</span></a></h3><p>数组（Array）允许你访问 <code>index[0]</code> 也可以访问 <code>index[100]</code>。栈为什么要把这种能力 “阉割” 掉？</p><p>答案就是<strong>为了强制秩序</strong>。</p><p>通过限制 “只能操作顶部”，栈强制程序员必须按照严格的 “原路返回” 逻辑来处理数据，从而消除了随机访问带来的复杂性和潜在错误。</p></section></section>
<section><h2>2. 为什么需要栈？<a href="#2-为什么需要栈"><span>#</span></a></h2><p>如果计算机世界没有栈，很多基础逻辑就会崩塌。我们需要栈，是因为它是<strong>保存上下文</strong>（Context）和<strong>实现回朔</strong>（Backtacking）最自然的模型。</p><section><h3>2.1 函数调用（The Call Stack）<a href="#21-函数调用the-call-stack"><span>#</span></a></h3><p>这是栈存在最底层、也最不可或缺的原因。</p><p>从本质上来说，CPU 在执行代码时是个高度专注的执行机器，它完全依赖程序计数器（PC, Program Counter）来决定下一步跑哪条指令。当代码从 <code>main()</code> 跳转到 <code>func_a()</code> 时，本质上是强行修改了 PC 寄存器的值。随之而来的致命问题是：当 <code>func_a()</code> 跑完，CPU 怎么知道该跳回 <code>main()</code> 里的哪一行继续执行？</p><p>你可能会想，用一个专门的寄存器（比如链接寄存器 LR）把返回地址保存起来不就行了？</p><p>在只有单层调用的情况下，这确实可行。但现实是，代码中充满了<strong>嵌套调用</strong>：<code>main()</code> 调用了 <code>func_a()</code>，而 <code>func_a()</code> 中又调用了 <code>func_b()</code>。如果只依赖单一寄存器，当调起 <code>func_b()</code> 时，新的返回地址回无情地覆盖掉之前 <code>func_a()</code> 的返回地址。等到 <code>func_b()</code> 执行结束，程序就再也找不到回 <code>main()</code> 的路了。</p><p>此外，函数不仅需要记住 “从哪儿来”，还要保护好自己的 “现场”。每个函数都有自己的局部变量，执行时还会征用各种通用寄存器。在跳入下一个函数前，当前的这些状态（Context）必须被原封不动地保存下来。</p><p>由于函数调用的逻辑是绝对的 “嵌套与回溯”——最后被调用的 <code>func_b()</code> 必须最先执行完毕并交出控制权，<code>func_a()</code> 才能继续，做后才轮到最外层的 <code>main()</code>。这种 “<strong>后出发的先结束，先出发的最后兜底</strong>” 的生命周期，与 LIFO (Last-In, First-Out) 结构严丝合缝。</p><p>所有，系统在内存中专门开辟了<strong>调用栈</strong>（Call Stack）。每次进入新函数，就把它的返回地址、局部变量和寄存器状态打包成一个<strong>栈帧</strong>（Stack Frame）压入栈顶；函数一结束，就将栈顶<strong>弹出</strong>，完美恢复上一层函数的现场。没有栈，现代编程语言的函数嵌套和递归机制就根本无法实现。</p><p></p><figure><img src="/assets/images/call_stack_layout.svg" alt="调用栈示例：如果子程序 DrawSquare 调用了 DrawLine，DrawLine 的栈帧就会被压入栈顶（如图中绿色部分所示）。栈指针（Stack Pointer）始终指向最新的栈顶位置" /><figcaption>调用栈示例：如果子程序 DrawSquare 调用了 DrawLine，DrawLine 的栈帧就会被压入栈顶（如图中绿色部分所示）。栈指针（Stack Pointer）始终指向最新的栈顶位置</figcaption></figure><p></p></section><section><h3>2.2 递归（Recursion）<a href="#22-递归recursion"><span>#</span></a></h3><p>既然函数调用依赖栈来保存上下文，那么<strong>递归</strong>可以说是把栈的这种特性压榨到了机制。</p><p></p><figure><img src="/assets/images/recursive_drawing_of_a_sierpinski_triangle_through_turtle_graphics.gif" alt="使用海龟绘图（Turtle Graphics）递归绘制谢尔宾斯基三角形（Sierpinski triangle）的过程" /><figcaption>使用海龟绘图（Turtle Graphics）递归绘制谢尔宾斯基三角形（Sierpinski triangle）的过程</figcaption></figure><p></p><p>从人类的思维来看，递归是<strong>自己调用自己</strong>，但在 CPU 和内存眼里，根本没有 “同一个函数” 的概念。对于计算机而言，不管是 <code>func_a()</code> 调用 <code>func)b()</code>，还是 <code>func_a()</code> 调用 <code>func_a()</code>，动作是完全一样的——仅仅是 PC（程序计数器）指针跳转，并在内存中开辟一个新的栈帧。</p><p>假设我们在写一个计算阶乘的递归函数（<code>fact(n)</code>）。当 <code>func(3)</code> 内部调用 <code>fact(2)</code> 时，此时的局部变量 <code>n=3</code> 必须被冻结并保护起来。如果没有栈 <code>fact(2)</code> 内部的变量就会直接覆盖掉原来 <code>n=3</code> 的内存空间。等到递归触底反弹时，程序根本想不起最初传入的参数是什么。</p><p>得益于栈的 LIFO 机制，每一次自我调用，系统都会把当前的参数、局部变量和返回地址像叠盘子一样<strong>压入</strong>（Push）栈中。不管向下深挖了多少层，每一层的数据都被独立且安全地封存在自己的栈帧里。当遇到终止条件（Base Case）开始回归时，栈再一层层<strong>弹出</strong>（Pop），精准地恢复每一层的现场。可以说，<strong>栈时递归能够成立的物理载体</strong>。</p><p>当然，如果嵌套层数过深导致栈空间耗尽，就会引发经典的 Stack Overflow 栈溢出。</p></section><section><h3>2.3 状态撤销（Undo）与历史回溯<a href="#23-状态撤销undo与历史回溯"><span>#</span></a></h3><p>你在编辑器里敲击键盘，后者在设计软件中画图，想要按下 <kbd>Ctrl</kbd> + <kbd>Z</kbd> 撤销刚刚的失误。</p><p>人类的时间是线性向前的，但 “撤销” 操作要求我们能够<strong>逆转时间</strong>。这种逆转不是随机的，而是必须严格遵守 “最近发生的事情，最先被抹去” 的自然法则。你最后敲下的那一个字符，必须是第一个被删除的。</p><p>所以，这里的历史记录被抽象成了一个栈：</p><ul>
<li><strong>入栈</strong>（Push）：每当你执行一个操作（比如输入字符、改变颜色、移动光标），系统就会把这个动作的 “状态快照” 或 “增量数据” 打包，压入历史记录栈的栈顶。</li>
<li><strong>出栈</strong>（Pop）：当年你触发撤销（Undo）时，系统直接从栈顶取回最后一次保存的状态，并将其弹出，界面随之恢复到上一个时刻。</li>
</ul><p>没有栈这种 “后进先出” 的模型，系统就无法建立起具有时间顺序的后悔药机制。<strong>任何需要 “原路” 返回的场景</strong>（比如浏览器的 “后退” 按钮、迷宫算法的回溯路径），其背后的灵魂数据机构都是栈。</p></section><section><h3>2.4 符号匹配与语法解析<a href="#24-符号匹配与语法解析"><span>#</span></a></h3><p>编译器在编译代码时，或者解析器在处理一段复杂公式时，需要检查代码中的 <code>{ [ ( ) ] }</code> 括号是否成对闭合、合法嵌套。</p><p>编译器阅读代码的顺序是从左到右的单向扫描。但在代码的世界里，<strong>最晚开启的作用于，必须最早被关闭</strong>。这种一种典型的 LIFO 结构需求。</p><p>面对一长串括号，系统处理的算法极其优雅，完全依赖栈来实现：</p><ol>
<li><strong>扫描与入栈</strong>：逐个字符读取，只要遇到 “左括号”（如 <code>{</code>、<code>[</code>、<code>(</code>），不管三七二十一，直接压入栈中。这代表着进入了一个新的层级，正在等待它的结束。</li>
<li><strong>匹配与出栈</strong>：一旦遇到 “右括号”（如 <code>)</code>、<code>]</code>、<code>}</code>），此时立刻去查看<strong>栈顶</strong>元素。如果栈顶正好是能与它匹配的左括号，说明当前最内层的逻辑闭环了，于是将栈顶弹出，继续往下走。</li>
</ol><p></p><figure><img src="/assets/images/parentheses_matching.webp" alt="利用栈进行括号匹配的逻辑：左括号入栈，右括号消栈，最后栈空则合法" /><figcaption>利用栈进行括号匹配的逻辑：左括号入栈，右括号消栈，最后栈空则合法</figcaption></figure><p></p><p>如果遇到右括号时栈是空的（说明没有多余的左括号等待闭合），或者栈顶的左括号类型不匹配（比如 <code>{</code> 遇到了 <code>]</code>），那么编译器就可以立刻抛出 <code>Syntax Error</code>（语法错误）。当整个文件扫描完，如果栈里面还有残留的左括号，说明有作用域没关上。</p><p>利用栈，编译器不仅能判断对错，还能精准定位到哪一层的括号出了问题。</p></section></section>
<section><h2>3. C++ STL 中的 <code>std::stack</code><a href="#3-c-stl-中的-stdstack"><span>#</span></a></h2><p>在 C++ 标准模板库（STL）中，<code>std::stack</code> 被设计得非常精妙。它不是一个从零开始写的容器，而是一个<strong>容器适配器</strong>（Container Adapter）。</p><section><h3>3.1 什么是 “容器适配器”？<a href="#31-什么是-容器适配器"><span>#</span></a></h3><p><code>std::stack</code> 本身不直接管理内存，它更像是一个外壳。它内部包裹着另一个真正的容器（比如 <code>std::vector</code>、<code>std::deque</code> 后 <code>std::list</code>）。</p><p>它屏蔽了底层容器的随机访问结构（如 <code>[]</code>），只暴露 <code>push</code>、<code>pop</code>、<code>top</code> 等接口，从而强制你遵守栈的规则。</p></section><section><h3>3.2 核心语法与操作<a href="#32-核心语法与操作"><span>#</span></a></h3><p>在使用前需包含头文件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;stack&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>基本操作（时间复杂度均为 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>）。</p>

<table><thead><tr><th>操作</th><th>函数</th><th>描述</th><th>注意事项</th></tr></thead><tbody><tr><td>入栈</td><td><code>push(val)</code></td><td>将元素拷贝到栈顶</td><td>设计拷贝/移动构造</td></tr><tr><td>原地构造</td><td><code>emplace(args)</code></td><td>在栈顶直接构造元素</td><td><strong>推荐</strong>，比 <code>push</code> 更高效，省去拷贝</td></tr><tr><td>出栈</td><td><code>pop()</code></td><td>移除栈顶元素</td><td><strong>返回值为 <code>void</code></strong>，不返回被移除的元素</td></tr><tr><td>查看</td><td><code>top()</code></td><td>返回栈顶元素的引用</td><td>如果栈为空调用此函数回导致未定义行为（通常崩溃）</td></tr><tr><td>判空</td><td><code>empty()</code></td><td>栈为空返回 <code>true</code></td><td>操作前务必检查</td></tr><tr><td>大小</td><td><code>size()</code></td><td>返回元素数量</td><td>-</td></tr></tbody></table><p></p><figure><img src="/assets/images/a_stack_runtime_with_push_and_pop_operations.svg" alt="Push 操作将元素压入栈顶，Pop 操作移除栈顶元素，两者都不能触及栈底" /><figcaption>Push 操作将元素压入栈顶，Pop 操作移除栈顶元素，两者都不能触及栈底</figcaption></figure><p></p></section><section><h3>3.3 为什么 C++ 的 <code>pop()</code> 不返回元素？<a href="#33-为什么-c-的-pop-不返回元素"><span>#</span></a></h3><p>在很多其他语言（如 Java, Python）中，<code>pop()</code> 会移除并返回栈顶元素。但在 C++ STL 中，<code>pop()</code> 是 <code>void</code>，你需要先 <code>top()</code> 获取值，再 <code>pop()</code> 移除。</p><p>究其原因是<strong>异常安全</strong>（Exception Safety）。</p><p>如果 <code>pop()</code> 函数即移除元素又返回元素（按值返回），在返回过程中如果发生了<strong>拷贝构造异常</strong>（比如内存不足），那么这个元素即从栈里丢了，又没能成功交给调用者，导致数据永久丢失。</p><p>将 <code>top()</code> 和 <code>pop()</code> 分离是 C++ 追求安全的体现。</p></section></section>
<section><h2>4. 算法题实战<a href="#4-算法题实战"><span>#</span></a></h2><p>点击题目即可跳转。</p><section><h3>4.1 <a href="https://www.luogu.com.cn/problem/B3614" target="_blank">洛谷：B3614【模板】栈</a><a href="#41-洛谷b3614模板栈"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;cstdint&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>using</span><span> </span><span>namespace</span><span><span> </span><span>std</span><span>;</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>const</span><span> </span><span>uint64_t</span><span> </span><span>N</span><span> </span><span>=</span><span> </span><span><span>10</span><span>e</span><span>6</span></span><span> </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>8</div></div><div><span>uint64_t</span><span> </span><span>stk</span><span>[</span><span>N</span><span>] </span><span>=</span><span> {</span><span>0</span><span>};</span></div></div><div><div><div>9</div></div><div><span>uint64_t</span><span> </span><span>top_idx</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span><span> // 栈顶指针，指向栈顶元素的下一个位置</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>uint64_t</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>t</span><span>;</span><span> // 数据组数</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>    </span><span>while</span><span> (</span><span>t</span><span>--</span><span>) {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>top_idx</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span><span> // 每组数据都清空栈</span></div></div><div><div><div>17</div></div><div><span>        </span><span>uint64_t</span><span> </span><span>n</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span><span> // 操作的次数</span></div></div><div><div><div>18</div></div><div><span>        </span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>n</span><span>;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>        </span><span>while</span><span> (</span><span>n</span><span>--</span><span>) {</span></div></div><div><div><div>21</div></div><div><span>            </span><span>string</span><span> </span><span>operation</span><span>;</span></div></div><div><div><div>22</div></div><div><span>            </span><span>uint64_t</span><span> </span><span>x</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>23</div></div><div><span>            </span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>operation</span><span>;</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>            </span><span>if</span><span> (</span><span>"push"</span><span> </span><span>==</span><span> </span><span>operation</span><span>) {</span></div></div><div><div><div>26</div></div><div><span>                </span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>27</div></div><div><span>                </span><span>stk</span><span>[</span><span>top_idx</span><span>++</span><span>] </span><span>=</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>"pop"</span><span> </span><span>==</span><span> </span><span>operation</span><span>) {</span></div></div><div><div><div>29</div></div><div><span>                </span><span>if</span><span> (</span><span>0</span><span> </span><span>==</span><span> </span><span>top_idx</span><span>) {</span></div></div><div><div><div>30</div></div><div><span>                    </span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"Empty"</span><span> </span><span>&lt;&lt;</span><span> </span><span>endl</span><span>;</span></div></div><div><div><div>31</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>32</div></div><div><span>                    </span><span>--</span><span>top_idx</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>"query"</span><span> </span><span>==</span><span> </span><span>operation</span><span>) {</span></div></div><div><div><div>35</div></div><div><span>                </span><span>if</span><span> (</span><span>0</span><span> </span><span>==</span><span> </span><span>top_idx</span><span>) {</span></div></div><div><div><div>36</div></div><div><span>                    </span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"Anguei!"</span><span> </span><span>&lt;&lt;</span><span> </span><span>endl</span><span>;</span></div></div><div><div><div>37</div></div><div><span><span>                </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>38</div></div><div><span>                    </span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>stk</span><span>[</span><span>top_idx</span><span> </span><span>-</span><span> </span><span>1</span><span>] </span><span>&lt;&lt;</span><span> </span><span>endl</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>"size"</span><span> </span><span>==</span><span> </span><span>operation</span><span>) {</span></div></div><div><div><div>41</div></div><div><span>                </span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>top_idx</span><span> </span><span>&lt;&lt;</span><span> </span><span>endl</span><span>;</span></div></div><div><div><div>42</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>43</div></div><div><span>                </span><span>continue</span><span>;</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>49</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>4.2 <a href="https://leetcode.cn/problems/valid-parentheses/description/" target="_blank">力扣：20. 有效的括号</a><a href="#42-力扣20-有效的括号"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Solution</span><span> {</span></div></div><div><div><div>2</div></div><div><span>public:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>bool</span><span> </span><span>isValid</span><span>(</span><span>string</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>std::</span><span>stack</span><span>&lt;</span><span>char</span><span>&gt; </span><span>stk</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> (</span><span>const</span><span> </span><span>char&amp;</span><span> </span><span>c</span><span> : </span><span>s</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>if</span><span> (</span><span>'('</span><span> </span><span>==</span><span> </span><span>c</span><span> </span><span>||</span><span> </span><span>'['</span><span> </span><span>==</span><span> </span><span>c</span><span> </span><span>||</span><span> </span><span>'{'</span><span> </span><span>==</span><span> </span><span>c</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>                </span><span>stk</span><span>.</span><span>push</span><span>(</span><span>c</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>if</span><span> (</span><span>stk</span><span>.</span><span>empty</span><span>()) {</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>                </span><span>char</span><span> </span><span>top</span><span> </span><span>=</span><span> </span><span>stk</span><span>.</span><span>top</span><span>();</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>                </span><span>if</span><span> (</span><span>')'</span><span> </span><span>==</span><span> </span><span>c</span><span> </span><span>&amp;&amp;</span><span> </span><span>'('</span><span> </span><span>!=</span><span> </span><span>top</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>                    </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>                </span><span>if</span><span> (</span><span>']'</span><span> </span><span>==</span><span> </span><span>c</span><span> </span><span>&amp;&amp;</span><span> </span><span>'['</span><span> </span><span>!=</span><span> </span><span>top</span><span>) {</span></div></div><div><div><div>21</div></div><div><span>                    </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>                </span><span>if</span><span> (</span><span>'}'</span><span> </span><span>==</span><span> </span><span>c</span><span> </span><span>&amp;&amp;</span><span> </span><span>'{'</span><span> </span><span>!=</span><span> </span><span>top</span><span>) {</span></div></div><div><div><div>25</div></div><div><span>                    </span><span>return</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>                </span><span>stk</span><span>.</span><span>pop</span><span>();</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>        </span><span>return</span><span> </span><span>stk</span><span>.</span><span>empty</span><span>();</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>4.3 <a href="https://www.luogu.com.cn/problem/P4387" target="_blank">洛谷：【深基15.习9】验证栈序列</a><a href="#43-洛谷深基15习9验证栈序列"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;stack&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>N</span><span> </span><span>=</span><span> </span><span>1000000</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>pushed</span><span>(</span><span>N</span><span>);</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>popped</span><span>(</span><span>N</span><span>);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>    </span><span>int</span><span> </span><span>q</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>std::</span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>q</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>while</span><span> (</span><span>q</span><span>--</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>int</span><span> </span><span>n</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>std::</span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>n</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>// 读取入栈序列</span></div></div><div><div><div>19</div></div><div><span>        </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>std::</span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>pushed</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>// 读取出栈序列</span></div></div><div><div><div>24</div></div><div><span>        </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>25</div></div><div><span><span>            </span></span><span>std::</span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>popped</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>// 模拟验证</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>std::</span><span>stack</span><span>&lt;</span><span>int</span><span>&gt; </span><span>stk</span><span>;</span></div></div><div><div><div>30</div></div><div><span>        </span><span>int</span><span> </span><span>popped_idx</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>// 依次入栈</span></div></div><div><div><div>33</div></div><div><span>        </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>34</div></div><div><span>            </span><span>stk</span><span>.</span><span>push</span><span>(</span><span>pushed</span><span>[</span><span>i</span><span>]);</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>            </span><span>while</span><span> (</span><span>popped_idx</span><span> </span><span>&lt;</span><span> </span><span>n</span><span> </span><span>&amp;&amp;</span><span> </span><span>!</span><span>stk</span><span>.</span><span>empty</span><span>() </span><span>&amp;&amp;</span><span> </span><span>stk</span><span>.</span><span>top</span><span>() </span><span>==</span><span> </span><span>popped</span><span>[</span><span>popped_idx</span><span>]) {</span></div></div><div><div><div>37</div></div><div><span>                </span><span>stk</span><span>.</span><span>pop</span><span>();</span></div></div><div><div><div>38</div></div><div><span>                </span><span>popped_idx</span><span>++</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span><span>        </span></span><span>// 判断</span></div></div><div><div><div>43</div></div><div><span>        </span><span>if</span><span> (</span><span>stk</span><span>.</span><span>empty</span><span>()) {</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"Yes"</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>endl</span><span>;</span></div></div><div><div><div>45</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>46</div></div><div><span><span>            </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"No"</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>endl</span><span>;</span></div></div><div><div><div>47</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>49</div></div><div>
</div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>52</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>4.4 <a href="https://www.luogu.com.cn/problem/P1449" target="_blank">洛谷：P1449 后缀表达式</a><a href="#44-洛谷p1449-后缀表达式"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;stack&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>std::</span><span>stack</span><span>&lt;</span><span>int</span><span>&gt; </span><span>stk</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>expression</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>num_str</span><span>;</span></div></div><div><div><div>9</div></div><div><span>    </span><span>int</span><span> </span><span>num</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>10</div></div><div><span>    </span><span>int</span><span> </span><span>result</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>std::</span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>expression</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>for</span><span> (</span><span>const</span><span> </span><span>char&amp;</span><span> </span><span>ch</span><span> : </span><span>expression</span><span>) {</span></div></div><div><div><div>15</div></div><div><span>        </span><span>if</span><span> (</span><span>ch</span><span> </span><span>==</span><span> </span><span>'@'</span><span>) </span><span>break</span><span>;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>        </span><span>if</span><span><span> (</span><span>std</span><span>::</span></span><span>isdigit</span><span>(</span><span><span>static_cast</span><span>&lt;unsigned</span></span><span> </span><span>char&gt;</span><span>(</span><span>ch</span><span>))) {</span></div></div><div><div><div>18</div></div><div><span>            </span><span>num_str</span><span> </span><span>+=</span><span> </span><span>ch</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>ch</span><span> </span><span>==</span><span> </span><span>'.'</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>if</span><span> (</span><span>!</span><span>num_str</span><span>.</span><span>empty</span><span>()) {</span></div></div><div><div><div>21</div></div><div><span>                </span><span>stk</span><span>.</span><span>push</span><span><span>(</span><span>std</span><span>::</span></span><span>stoi</span><span>(</span><span>num_str</span><span>));</span></div></div><div><div><div>22</div></div><div><span>                </span><span>num_str</span><span>.</span><span>clear</span><span>();</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>int</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>stk</span><span>.</span><span>top</span><span>(); </span><span>stk</span><span>.</span><span>pop</span><span>();</span></div></div><div><div><div>26</div></div><div><span>            </span><span>int</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>stk</span><span>.</span><span>top</span><span>(); </span><span>stk</span><span>.</span><span>pop</span><span>();</span></div></div><div><div><div>27</div></div><div><span>            </span><span>int</span><span> </span><span>result</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>28</div></div><div><span>            </span><span>switch</span><span> (</span><span>ch</span><span>) {</span></div></div><div><div><div>29</div></div><div><span>                </span><span>case</span><span> </span><span>'+'</span><span>: </span><span>result</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>+</span><span> </span><span>a</span><span>; </span><span>break</span><span>;</span></div></div><div><div><div>30</div></div><div><span>                </span><span>case</span><span> </span><span>'-'</span><span>: </span><span>result</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>-</span><span> </span><span>a</span><span>; </span><span>break</span><span>;</span></div></div><div><div><div>31</div></div><div><span>                </span><span>case</span><span> </span><span>'*'</span><span>: </span><span>result</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>*</span><span> </span><span>a</span><span>; </span><span>break</span><span>;</span></div></div><div><div><div>32</div></div><div><span>                </span><span>case</span><span> </span><span>'/'</span><span>: </span><span>result</span><span> </span><span>=</span><span> </span><span>b</span><span> </span><span>/</span><span> </span><span>a</span><span>; </span><span>break</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>34</div></div><div><span>            </span><span>stk</span><span>.</span><span>push</span><span>(</span><span>result</span><span>);</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>    </span><span>result</span><span> </span><span>=</span><span> </span><span>stk</span><span>.</span><span>top</span><span>();</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>result</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>endl</span><span>;</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>4.5 <a href="https://www.luogu.com.cn/problem/P1241" target="_blank">洛谷：P1241 括号序列</a><a href="#45-洛谷p1241-括号序列"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;stack&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>bool</span><span> </span><span>is_match</span><span>(</span><span>char</span><span><span> </span><span>left</span><span>, </span></span><span>char</span><span><span> </span><span>right</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span>    </span><span>return</span><span> (</span><span>'('</span><span> </span><span>==</span><span> </span><span>left</span><span> </span><span>&amp;&amp;</span><span> </span><span>')'</span><span> </span><span>==</span><span> </span><span>right</span><span>) </span><span>||</span><span> (</span><span>'['</span><span> </span><span>==</span><span> </span><span>left</span><span> </span><span>&amp;&amp;</span><span> </span><span>']'</span><span> </span><span>==</span><span> </span><span>right</span><span>);</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>expression</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>std::</span><span>cin</span><span> </span><span>&gt;&gt;</span><span> </span><span>expression</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>std::</span><span>stack</span><span>&lt;</span><span>int</span><span>&gt; </span><span>stk</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>bool</span><span>&gt; </span><span>matched</span><span>(</span><span>expression</span><span>.</span><span>size</span><span>(), </span><span>false</span><span>);</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>expression</span><span>.</span><span>size</span><span>()); </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>char</span><span> </span><span>ch</span><span> </span><span>=</span><span> </span><span>expression</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>19</div></div><div><span>        </span><span>if</span><span> (</span><span>'('</span><span> </span><span>==</span><span> </span><span>ch</span><span> </span><span>||</span><span> </span><span>'['</span><span> </span><span>==</span><span> </span><span>ch</span><span>) {</span></div></div><div><div><div>20</div></div><div><span>            </span><span>stk</span><span>.</span><span>push</span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>21</div></div><div><span>            </span><span>continue</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>        </span><span>if</span><span> (</span><span>!</span><span>stk</span><span>.</span><span>empty</span><span>() </span><span>&amp;&amp;</span><span> </span><span>is_match</span><span>(</span><span>expression</span><span>[</span><span>stk</span><span>.</span><span>top</span><span>()], </span><span>ch</span><span>)) {</span></div></div><div><div><div>25</div></div><div><span>            </span><span>matched</span><span>[</span><span>stk</span><span>.</span><span>top</span><span>()] </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>26</div></div><div><span>            </span><span>matched</span><span>[</span><span>i</span><span>] </span><span>=</span><span> </span><span>true</span><span>;</span></div></div><div><div><div>27</div></div><div><span>            </span><span>stk</span><span>.</span><span>pop</span><span>();</span></div></div><div><div><div>28</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>expression</span><span>.</span><span>size</span><span>()); </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>32</div></div><div><span>        </span><span>if</span><span> (</span><span>matched</span><span>[</span><span>i</span><span>]) {</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>expression</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>34</div></div><div><span>            </span><span>continue</span><span>;</span></div></div><div><div><div>35</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>        </span><span>if</span><span> (</span><span>'('</span><span> </span><span>==</span><span> </span><span>expression</span><span>[</span><span>i</span><span>] </span><span>||</span><span> </span><span>')'</span><span> </span><span>==</span><span> </span><span>expression</span><span>[</span><span>i</span><span>]) {</span></div></div><div><div><div>38</div></div><div><span><span>            </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"()"</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>40</div></div><div><span><span>            </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"[]"</span><span>;</span></div></div><div><div><div>41</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>endl</span><span>;</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>
<section><h2>5. 进阶知识（底层容器的选择）<a href="#5-进阶知识底层容器的选择"><span>#</span></a></h2><p><a href="https://en.cppreference.com/w/cpp/container/stack.html" target="_blank"><code>std::stack</code></a> 是一个模板类（定义在头文件 <code>&lt;stack&gt;</code>），定义如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span>&lt;</span><span>class</span><span> </span><span>T</span><span>, </span><span>class</span><span> </span><span>Container</span><span> </span><span>=</span><span> std::</span><span>deque</span><span>&lt;</span><span>T</span><span>&gt;&gt;</span></div></div><div><div><div>2</div></div><div><span>class</span><span> </span><span>stack</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意第二个参数 <code>Container</code>。默认情况下，C++ 使用 <code>std::deque</code>（双端队列）作为底层容器，而不是 <code>std::vector</code>。</p><section><h3>5.1 为什么默认用 <code>std::deque</code> 而不是 <code>std::vector</code>？<a href="#51-为什么默认用-stddeque-而不是-stdvector"><span>#</span></a></h3><ol>
<li><strong>扩容代价</strong>：<code>std::vector</code> 在内存不足扩容时，需要把所有元素搬运到新内存块，代价较大。<code>std::deque</code> 是分段内存，扩容时只需分配新段，不需要搬运旧数据。</li>
<li><strong>内存效率</strong>：对于大量进出的栈操作，<code>std::deque</code> 的内存回收和分配策略通常比 <code>std::vector</code> 更平滑。</li>
</ol><p></p><figure><img src="/assets/images/double_ended_queue_in_stl.png" alt="std::deque 的内部结构：由中控器（Map）管理的一系列分段连续内存缓冲区，非常适合两端操作" /><figcaption>std::deque 的内部结构：由中控器（Map）管理的一系列分段连续内存缓冲区，非常适合两端操作</figcaption></figure><p></p><p>当然，如果你确定需要极致的连续内存性能，也可以手动指定：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>stack</span><span>&lt;int</span><span>, std::</span><span>vector</span><span>&lt;int&gt;&gt;</span><span> </span><span>stk</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ol>
<li><a href="https://en.wikipedia.org/wiki/Stack_(abstract_data_type)" target="_blank">Wikipedia: Stack (abstract data type)</a></li>
<li><a href="https://en.wikipedia.org/wiki/Call_stack" target="_blank">Wikipedia: Call stack</a></li>
<li><a href="https://en.wikipedia.org/wiki/Recursion_(computer_science)" target="_blank">Wikipedia: Recursion (computer science)</a></li>
<li><a href="https://medium.com/@goyalarchana17/scala-interview-series-valid-parentheses-in-a-string-ac7002c79642" target="_blank">Medium: Valid Parentheses in a String</a></li>
<li><a href="https://stackoverflow.com/questions/1436020/whats-the-difference-between-deque-and-list-stl-containers" target="_blank">StackOverflow: Difference between deque and list</a></li>
<li><a href="https://cppreference.com/w/cpp/container/stack" target="_blank">cppreference.com: std::stack</a></li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/process-in-os/</id>
      <title type="text">操作系统中的进程、线程与协程</title>
      <published>2026-01-26T22:10:00.000Z</published>
      <updated>2026-10-05T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/process-in-os/"/>
      <summary type="text">进程、线程、协程是同一个问题的三个成本档位：状态存在哪、谁来调度、切换一次多贵。本文从 ENIAC 的接线讲起，把三层抽象逐层拆开，并用 C/C++、Python、Rust 各写一遍，附本机实测的切换与内存数字。</summary>
      <content type="html"><![CDATA[<blockquote><p>“What I cannot create, I do not understand.”（Richard Feynman）</p></blockquote>
<p>我第一次写这篇笔记，只写到进程。当时我觉得教科书那句”进程是程序的一次执行过程”已经够用了，剩下的都是细节。后来越往下写，问题反而越多：为什么 Python 里开两个线程做同一件 CPU 密集的事，墙钟时间几乎翻倍？为什么 Rust 的 <code>async fn</code> 不接一个运行时根本不会跑？为什么一台机器能用线程扛住几百个连接，却扛不住十万个？</p>
<p>这些问题最后收敛到同一组东西上：<strong>一个并发实体的状态存在哪里，谁在调度它，切换一次要花多少</strong>。进程、线程、协程，本质上是这个问题在三个不同成本档位上的答案。这篇文章就按这三层走，每层都用 C/C++、Python、Rust 各写一遍，并把本机的实测数字贴出来。我不想只复述定义，我想把每层的”账单”算给你看。</p>
<section><h2>先交代测量环境<a href="#先交代测量环境"><span>#</span></a></h2><p>后面所有数字都来自同一台机器，同一份代码，<code>-O2</code> 编译，取多次运行里最好的一次。</p>

<table><thead><tr><th>项目</th><th>值</th></tr></thead><tbody><tr><td>内核</td><td>Linux 6.18.55（x86_64，PREEMPT_DYNAMIC）</td></tr><tr><td>逻辑 CPU</td><td>16</td></tr><tr><td>C / C++</td><td>gcc / g++ 15.3.0</td></tr><tr><td>Python</td><td>CPython 3.14.7（非自由线程构建，带 GIL）</td></tr><tr><td>Rust</td><td>rustc 1.95.0（edition 2024）</td></tr></tbody></table><p>时钟分辨率、调度抖动、CPU 频率都会影响微基准，所以下面的数字请当作量级参考，而不是绝对真理。复现命令和完整代码在文末。</p></section>
<section><h2>一、进程：把一台机器虚拟成很多台<a href="#一进程把一台机器虚拟成很多台"><span>#</span></a></h2><p>要讲清楚进程，得先讲清楚它替谁擦屁股。进程这个概念不是被设计出来的，是被两个速度差逼出来的：<strong>人的速度</strong> 对 <strong>CPU 的速度</strong>，以及 <strong>I/O 的速度</strong> 对 <strong>CPU 的速度</strong>。</p><section><h3>0. 史前时代：硬件即程序<a href="#0-史前时代硬件即程序"><span>#</span></a></h3><p>在进程出现之前，计算机里没有”软件”和”硬件”的清楚界限。代表是 <a href="https://en.wikipedia.org/wiki/ENIAC" target="_blank">ENIAC</a>（Electronic Numerical Integrator and Computer）：占地 167 平方米，重 27 吨，同一时间只能解决一个问题。</p><p></p><figure><img src="/assets/images/process-in-os/eniac-programming.jpg" alt="早期的编程：Glen Beck 与 Betty Snyder 正在对 ENIAC 进行硬连线" /><figcaption>早期的编程：Glen Beck 与 Betty Snyder 正在对 ENIAC 进行硬连线</figcaption></figure><p></p><p>在 ENIAC 上”切换任务”不是双击图标，而是体力活。程序员（当时多为女性数学家）拿着粗大的连接线在配线板上物理接线，每一个插孔、每一排开关都对应一段逻辑。接好线，这台机器就变成解那个特定方程的专用电路。</p><p></p><figure><img src="/assets/images/process-in-os/eniac-operators.jpg" alt="操作员正在手动更改 ENIAC 的线路配置" /><figcaption>操作员正在手动更改 ENIAC 的线路配置</figcaption></figure><p></p><p>它每秒能做 5000 次加法，而”准备一次计算”要花几分钟到几小时。正是这个<strong>极高的计算速度</strong>和<strong>极低的任务切换效率</strong>之间的鸿沟，推着计算机往前走。</p><div><div><div></div><div>Note</div></div><div><p>这种”接线即编程”的思路今天还在：FPGA 做的事，本质就是给一段逻辑烧出一个专用电路。</p></div></div><p></p><figure><img src="/assets/images/process-in-os/eniac-overview.jpg" alt="ENIAC 全景：巨大的体积与原始的操作方式" /><figcaption>ENIAC 全景：巨大的体积与原始的操作方式</figcaption></figure><p></p></section><section><h3>1. 批处理：把人的延迟消掉<a href="#1-批处理把人的延迟消掉"><span>#</span></a></h3><p>1945 年，von Neumann 在《<a href="https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC" target="_blank">First Draft of a Report on the EDVAC</a>》里确立了存储程序架构，程序终于变成躺在磁带上的指令，不再需要物理插拔线缆。</p><p></p><figure><img src="/assets/images/process-in-os/von-neumann-architecture.svg" alt="冯·诺依曼架构：存储程序计算机的基石" /><figcaption>冯·诺依曼架构：存储程序计算机的基石</figcaption></figure><p></p><p>但早期的 Open Shop 模式还是人主导：程序员带着磁带进机房，装带、运行、卸带、走人，下一个人再进来。CPU 每秒能跑几万条指令，而人装一次磁带要几分钟。让这么快的 CPU 停下来等人，是极大的浪费。</p><p>于是出现了 <a href="https://en.wikipedia.org/wiki/Resident_monitor" target="_blank">Resident Monitor</a> 这种常驻内存的小程序，它负责：读下一个<strong>作业</strong>（Job）、装载、运行、收回控制权、再读下一个。1956 年通用汽车为 IBM 704 写的 <a href="https://en.wikipedia.org/wiki/GM-NAA_I/O" target="_blank">GM-NAA I/O</a> 通常被认为是第一个真正意义上的操作系统。</p><p></p><figure><img src="/assets/images/process-in-os/batch-processing.png" alt="批处理系统的工作流示意图" /><figcaption>批处理系统的工作流示意图</figcaption></figure><p></p><p></p><figure><img src="/assets/images/process-in-os/resident-monitor.png" alt="常驻监控程序 (Monitor) 的内存布局" /><figcaption>常驻监控程序 (Monitor) 的内存布局</figcaption></figure><p></p><p>注意，这个阶段还没有<strong>进程</strong>，只有<strong>作业</strong>，而且内存里一次仍然只住一个程序。</p></section><section><h3>2. 多道程序设计：把 I/O 的延迟消掉<a href="#2-多道程序设计把-io-的延迟消掉"><span>#</span></a></h3><p>批处理解决了人的问题，没解决硬件的问题。程序要读磁带或磁盘时，机械设备的延迟比 CPU 慢好几个数量级（毫秒对微秒）。CPU 只能干等。</p><p>工程师的答案很直接：既然内存变大了，为什么不同时装好几个作业？</p><ol>
<li>Job A 在等磁带（阻塞）时，把 CPU 切给 Job B；</li>
<li>Job B 在等打印机时，再切给 Job C。</li>
</ol><p></p><figure><img src="/assets/images/process-in-os/multiprogramming.png" alt="多道程序设计下的内存划分" /><figcaption>多道程序设计下的内存划分</figcaption></figure><p></p><p>IBM 在 System/360 上大力推行这个概念，也是在这里，计算机科学迎来一个关键转折：<strong>为了在多个程序之间来回切，必须保存每个程序的状态</strong>。</p><p></p><figure><img src="/assets/images/process-in-os/ibm-system-360.jpg" alt="IBM System/360 Model 30 控制台" /><figcaption>IBM System/360 Model 30 控制台</figcaption></figure><p></p><p>OS/360 里出现了描述任务状态的数据结构 <strong>TCB</strong>（Task Control Block），它是后来 <strong>PCB</strong>（Process Control Block）的前身：</p><p></p><figure><img src="/assets/images/process-in-os/os-360-control-blocks.jpg" alt="OS/360 中的系统控制块 (TCB) 结构" /><figcaption>OS/360 中的系统控制块 (TCB) 结构</figcaption></figure><p></p><p>TCB 主要做两件事：</p><ol>
<li><strong>现场保护</strong>：CPU 从 A 切到 B 时，A 的寄存器（算到一半的结果、程序计数器 PC、栈指针 SP 等）必须存进 TCB。因为物理寄存器只有一套，不存就丢了。</li>
<li><strong>状态标记</strong>：系统要记录谁在 Running、谁在 Waiting、谁 Ready。</li>
</ol></section><section><h3>3. 分时：把响应时间消掉，进程正式诞生<a href="#3-分时把响应时间消掉进程正式诞生"><span>#</span></a></h3><p>到 60 年代末，用户开始通过终端和机器交互。如果沿用多道程序的逻辑（只有 I/O 阻塞才切），一个 <code>while(1)</code> 死循环就能永远霸占 CPU，终端前的其他人敲键盘毫无反应。</p><p>MIT、贝尔实验室和通用电气联合做的 <a href="https://en.wikipedia.org/wiki/Multics" target="_blank">Multics</a> 引入了<strong>硬件定时器</strong>，这一改变是革命性的：</p><ul>
<li><strong>抢占式调度</strong>：无论程序 A 有没有跑完，每隔几十毫秒（一个时间片）操作系统强制打断它，保存上下文，把 CPU 交给 B；</li>
<li><strong>幻觉</strong>：快速切换让每个用户都以为自己独占了一台电脑。</li>
</ul><p></p><figure><img src="/assets/images/process-in-os/round-robin-scheduling.jpg" alt="轮转调度算法 (Round-Robin) 时间片轮转示意" /><figcaption>轮转调度算法 (Round-Robin) 时间片轮转示意</figcaption></figure><p></p><p>也就是在这个时期，<strong>进程</strong>被正式确立，并赋予了现代含义。为了维持”独占”的幻觉，操作系统筑起了两道墙：</p><ol>
<li><strong>时间的虚拟化</strong>：保存 / 恢复寄存器上下文，让每个进程觉得拥有独立的 CPU；</li>
<li><strong>空间的虚拟化</strong>：用 MMU 和页表，让每个进程觉得拥有独立、连续的内存空间。</li>
</ol><p>至此，进程不再只是用户的代码，它是：</p><div><div><div></div><div>Important</div></div><div><p><strong>代码</strong> + <strong>动态执行上下文</strong>（某一时刻寄存器里的值）+ <strong>虚拟地址空间</strong></p></div></div><p>这就是为什么”程序的一次执行”这种定义是对的但没用：真正的问题在于”执行上下文存在哪”和”地址空间怎么隔离”。</p></section></section>
<section><h2>二、进程里到底有什么<a href="#二进程里到底有什么"><span>#</span></a></h2><section><h3>虚拟地址空间：进程看到的世界<a href="#虚拟地址空间进程看到的世界"><span>#</span></a></h3><p>每个进程都以为自己独占一整块连续的虚拟内存。它的典型布局是这样的：</p><p></p><figure><img src="/assets/images/process-in-os/address-space.svg" alt="进程的虚拟地址空间布局" /><figcaption>进程的虚拟地址空间布局</figcaption></figure><p></p><p>几个容易踩的点：</p><ul>
<li><strong>栈向下长，堆向上长</strong>，中间留一大片空洞按需分配。栈溢出和堆撞栈是两种不同的错误。</li>
<li><strong>内核空间也映射在每个进程里</strong>（高地址区），但在用户态不可访问；进入内核靠系统调用切换特权级。</li>
<li><strong>地址是虚拟的</strong>。同一个虚拟地址 <code>0x400000</code>，在两个进程里可能落在完全不同的物理页上。</li>
</ul><p>那隔离是怎么做到的？答案是页表加 MMU：</p><p></p><figure><img src="/assets/images/process-in-os/memory-isolation.svg" alt="隔离：同一个虚拟地址落在不同物理页" /><figcaption>隔离：同一个虚拟地址落在不同物理页</figcaption></figure><p></p><p>写坏自己的进程碰不到别人，靠的不是”约定”，而是硬件每次访存都会查页表。</p><div><div><div></div><div>Tip</div></div><div><p>想亲眼看，可以跑 <code>cat /proc/self/maps</code> 和 <code>cat /proc/self/status</code>，前者是虚拟地址空间的条目，后者里的 <code>VmSize</code> / <code>VmRSS</code> 是虚拟和常驻的大小。</p></div></div></section><section><h3>内核眼里的进程：PCB 与上下文切换<a href="#内核眼里的进程pcb-与上下文切换"><span>#</span></a></h3><p>用户看到的是地址空间，内核看到的是数据结构。Linux 里这个结构叫 <code>task_struct</code>，一个进程对应一个任务。它记录着 pid、状态、打开的文件、信号、以及最重要的：<strong>寄存器现场</strong>。</p><p>关键事实是：<strong>CPU 只有一套寄存器</strong>。所以”切换进程”这件事，物理上就是”把当前寄存器存进一个结构，再从另一个结构装载寄存器”：</p><p></p><figure><img src="/assets/images/process-in-os/context-switch.svg" alt="上下文切换：寄存器与 PCB" /><figcaption>上下文切换：寄存器与 PCB</figcaption></figure><p></p><p>进程切换比”存寄存器”还要多一步：如果换的是进程而不是线程，还得切换 <code>CR3</code>（页表基址），也就是换掉整个地址空间。换地址空间会带来 TLB 失效的连带成本，这是进程切换比线程切换贵的一个主要原因。</p><p>进程在这几个状态之间来回走：</p><p></p><figure><img src="/assets/images/process-in-os/process-state-machine.svg" alt="进程状态机：新建、就绪、运行、阻塞、终止" /><figcaption>进程状态机：新建、就绪、运行、阻塞、终止</figcaption></figure><p></p></section><section><h3>进程有多贵（实测）<a href="#进程有多贵实测"><span>#</span></a></h3>

<table><thead><tr><th>操作</th><th>实测</th></tr></thead><tbody><tr><td><code>fork()</code> + 子进程退出 + <code>wait()</code></td><td>173 µs</td></tr><tr><td>内核进程间切换（管道往返折算，含系统调用）</td><td>2.81 µs</td></tr></tbody></table><p>这两个数是这样量出来的：<code>fork</code> 用”创建 + 回收”整个循环除以次数；线程 / 进程间的切换用 lmbench 式管道往返，两个执行流绑在同一个核上，来回传一个字节，一次往返除以 2：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* 单调时钟，返回秒 */</span></div></div><div><div><div>2</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>now</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span>  </span><span>struct</span><span> timespec ts;</span></div></div><div><div><div>5</div></div><div><span>  </span><span>clock_gettime</span><span>(CLOCK_MONOTONIC, </span><span>&amp;</span><span>ts);</span></div></div><div><div><div>6</div></div><div><span>  </span><span>return</span><span> (</span><span>double</span><span>)</span><span>ts</span><span>.</span><span>tv_sec</span><span> </span><span>+</span><span> (</span><span>double</span><span>)</span><span>ts</span><span>.</span><span>tv_nsec</span><span> </span><span>*</span><span> </span><span><span>1</span><span>e</span></span><span>-</span><span>9</span><span>;</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>/* fork + 子进程退出 + wait，除以次数 */</span></div></div><div><div><div>10</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>bench_fork</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>11</div></div><div><span>{</span></div></div><div><div><div>12</div></div><div><span>  </span><span>double</span><span> t0 </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>13</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>pid_t</span><span> p </span><span>=</span><span> </span><span>fork</span><span>();</span></div></div><div><div><div>15</div></div><div><span>    </span><span>if</span><span> (p </span><span>==</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>16</div></div><div><span>      </span><span>_exit</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>17</div></div><div><span>    </span><span>int</span><span> st;</span></div></div><div><div><div>18</div></div><div><span>    </span><span>waitpid</span><span>(p, </span><span>&amp;</span><span>st, </span><span>0</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>  </span><span>return</span><span> (</span><span>now</span><span>() </span><span>-</span><span> t0) </span><span>/</span><span> n;</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>/* 两个线程绑到同一个核，用两条管道来回传一个字节 */</span></div></div><div><div><div>24</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>bench_pp_thread</span><span>(</span><span>long</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>25</div></div><div><span>{</span></div></div><div><div><div>26</div></div><div><span>  </span><span>if</span><span> (</span><span>pipe</span><span>(pp_a) </span><span>||</span><span> </span><span>pipe</span><span>(pp_b)) {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>perror</span><span>(</span><span>"pipe"</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>exit</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>pp_n </span><span>=</span><span> n;</span></div></div><div><div><div>31</div></div><div><span>  </span><span>pthread_t</span><span> t;</span></div></div><div><div><div>32</div></div><div><span>  </span><span>pthread_create</span><span>(</span><span>&amp;</span><span>t, </span><span>NULL</span><span>, pp_peer, </span><span>NULL</span><span>);</span></div></div><div><div><div>33</div></div><div><span>  </span><span>pin0</span><span>();</span><span> /* 把自己也绑到 0 号核 */</span></div></div><div><div><div>34</div></div><div><span>  </span><span>char</span><span> c </span><span>=</span><span> </span><span>'x'</span><span>;</span></div></div><div><div><div>35</div></div><div><span>  </span><span>double</span><span> t0 </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>36</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>if</span><span> (</span><span>write</span><span>(</span><span>pp_a</span><span>[</span><span>1</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>38</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>39</div></div><div><span>    </span><span>if</span><span> (</span><span>read</span><span>(</span><span>pp_b</span><span>[</span><span>0</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>40</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>42</div></div><div><span>  </span><span>double</span><span> dt </span><span>=</span><span> </span><span>now</span><span>() </span><span>-</span><span> t0;</span></div></div><div><div><div>43</div></div><div><span>  </span><span>pthread_join</span><span>(t, </span><span>NULL</span><span>);</span></div></div><div><div><div>44</div></div><div><span>  </span><span>return</span><span> dt </span><span>/</span><span> n </span><span>/</span><span> </span><span>2.0</span><span>;</span><span> /* 一次往返 = 两次切换 */</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>进程间的版本只是把 <code>pthread_create</code> 换成 <code>fork</code>，其余一样（<code>pin0</code> / <code>pp_peer</code> 等辅助函数见附录 A 的完整程序）。量出来的是”切换 + 系统调用”的总和，所以它是内核切换成本的一个上界。</p><p><code>fork</code> 贵有它的道理：内核要新建一个 <code>task_struct</code>、复制页表（页本身靠写时复制 COW 共享）、分配内核栈、复制一堆文件描述符。所以”创建一个进程”这个动作，本身就是一笔不小的开销。</p><p>这里要澄清一个常见误解：<strong>同一台机器上，进程间切换（2.81 µs）和线程间切换（2.69 µs）差得并不多</strong>。真正贵的不是”切”，而是”建”和”销毁”，以及切换时换地址空间带来的间接代价。</p></section><section><h3>C：fork 的地址空间隔离<a href="#cfork-的地址空间隔离"><span>#</span></a></h3><p>亲手摸一下隔离到底是什么意思：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;sys/wait.h&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;unistd.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>6</div></div><div><span>{</span></div></div><div><div><div>7</div></div><div><span>  </span><span>int</span><span> x </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>printf</span><span>(</span><span>"fork 之前:  x=</span><span>%d</span><span>, pid=</span><span>%d</span><span>\n</span><span>"</span><span>, x, </span><span>getpid</span><span>());</span></div></div><div><div><div>9</div></div><div><span>  </span><span>fflush</span><span>(stdout);</span></div></div><div><div><div>10</div></div><div><span>  </span><span>pid_t</span><span> p </span><span>=</span><span> </span><span>fork</span><span>();</span></div></div><div><div><div>11</div></div><div><span>  </span><span>if</span><span> (p </span><span>==</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>x </span><span>=</span><span> </span><span>42</span><span>;</span></div></div><div><div><div>13</div></div><div><span>    </span><span>printf</span><span>(</span><span>"子进程里:   x=</span><span>%d</span><span>, pid=</span><span>%d</span><span>\n</span><span>"</span><span>, x, </span><span>getpid</span><span>());</span></div></div><div><div><div>14</div></div><div><span>    </span><span>fflush</span><span>(stdout);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>_exit</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>  </span><span>wait</span><span>(</span><span>NULL</span><span>);</span></div></div><div><div><div>18</div></div><div><span>  </span><span>printf</span><span>(</span><span>"父进程里:   x=</span><span>%d</span><span>, pid=</span><span>%d</span><span>\n</span><span>"</span><span>, x, </span><span>getpid</span><span>());</span></div></div><div><div><div>19</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fork 之前:  x=1, pid=258617</span></div></div><div><div><div>2</div></div><div><span>子进程里:   x=42, pid=258618</span></div></div><div><div><div>3</div></div><div><span>父进程里:   x=1, pid=258617</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>子进程改的 <code>x</code> 对父进程毫无影响，因为它俩各有一份独立地址空间。这就是为什么进程间通信必须走管道、共享内存、socket 这些<strong>显式</strong>的 IPC 机制。</p><div><div><div></div><div>Note</div></div><div><p>示例里我加了 <code>fflush</code>，否则子进程 <code>_exit</code> 会丢掉标准输出缓冲区里的内容。这个坑本身就很”进程”：缓冲区是进程私有状态的一部分。</p></div></div></section><section><h3>Python：要真并行，得开进程<a href="#python要真并行得开进程"><span>#</span></a></h3><p>CPython 有 GIL（后面细讲），所以想要 CPU 并行只能开进程。用一个纯 Python 的 CPU 密集函数量一下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>burn</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>3</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> i </span><span>*</span><span> i</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span> s</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>N: </span><span>int</span><span><span> </span><span>=</span><span> </span></span><span>20_000_000</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>方式</th><th>墙钟时间</th><th>相对单线程加速</th></tr></thead><tbody><tr><td>1 个线程</td><td>1.060 s</td><td>1.00x</td></tr><tr><td>2 个线程</td><td>2.018 s</td><td>1.05x</td></tr><tr><td>2 个进程</td><td>1.087 s</td><td>1.95x</td></tr></tbody></table><p>两个线程做双倍工作，用了几乎双倍时间（完全没有加速）；两个进程接近 1.95x，因为进程有各自独立的解释器和 GIL。</p></section><section><h3>Rust：<code>std::process</code><a href="#ruststdprocess"><span>#</span></a></h3><p>Rust 没有魔法，<code>std::process::Command</code> 就是对 <code>fork</code> + <code>exec</code> 的封装。进程的隔离和成本，与 C 完全一样，语言层帮不上忙。Rust 真正有意思的地方在线程和协程，后面再说。</p></section><section><h3>进程的两张账单<a href="#进程的两张账单"><span>#</span></a></h3><p>总结一下进程：</p><ul>
<li><strong>买到的</strong>：强隔离（一个进程崩溃不影响另一个）、独立地址空间、独立资源配额。</li>
<li><strong>付出的</strong>：创建贵（173 µs，比协程 resume 贵约 80000 倍）、切换要换地址空间、通信必须显式 IPC。</li>
</ul><p>如果一个程序内部需要并发，但又不想付这些成本，答案就是线程。</p></section></section>
<section><h2>三、线程：共享地址空间的执行流<a href="#三线程共享地址空间的执行流"><span>#</span></a></h2><section><h3>动机：把”隔离”这份保险退掉<a href="#动机把隔离这份保险退掉"><span>#</span></a></h3><p>线程的核心思想很朴素：<strong>如果一个进程内部的多条执行流本来就要共享数据，那就让它们共享整个地址空间</strong>，省掉切换页表和 IPC 拷贝。</p><p></p><figure><img src="/assets/images/process-in-os/thread-memory-model.svg" alt="线程内存模型：共享地址空间，独立栈与寄存器" /><figcaption>线程内存模型：共享地址空间，独立栈与寄存器</figcaption></figure><p></p><p>线程共享代码、全局变量、堆和打开的文件；每个线程私有栈、寄存器、线程本地存储（TLS）。这带来两个直接结果：</p><ul>
<li><strong>好</strong>：线程间传数据就像传指针，零拷贝；线程切换不用换 <code>CR3</code>。</li>
<li><strong>坏</strong>：一个线程写坏地址空间，整个进程一起完蛋；共享数据的读写顺序要靠锁、原子操作或语言规则来管。</li>
</ul></section><section><h3>Linux 没有单独的”线程”：clone 的两副面孔<a href="#linux-没有单独的线程clone-的两副面孔"><span>#</span></a></h3><p>这个概念很值得记住：<strong>Linux 内核里没有独立的线程概念</strong>。进程和线程都是 <code>task_struct</code>，区别只在 <code>clone</code> 系统调用传了哪些标志位。所谓”线程”，只是共享了一大堆东西的 <code>task</code>：</p><p></p><figure><img src="/assets/images/process-in-os/clone-flags.svg" alt="fork 与 pthread_create 在 Linux 里都是 clone" /><figcaption>fork 与 pthread_create 在 Linux 里都是 clone</figcaption></figure><p></p><p>这解释了为什么线程这么”轻”：它不需要复制页表，只需要新建一个 <code>task_struct</code>、分配一个内核栈和用户栈，然后共享父任务的 <code>mm_struct</code>、文件表指针等。</p><p>也解释了为什么线程在系统里看起来还是一个个”任务”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$ ls /proc/258721/task</span></div></div><div><div><div>2</div></div><div><span>258721  258722  258723  258724</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个主线程加三个工作线程，在内核里就是四个 task，各有各的 TID。</p></section><section><h3>线程有多贵（实测）<a href="#线程有多贵实测"><span>#</span></a></h3>

<table><thead><tr><th>项目</th><th>实测</th></tr></thead><tbody><tr><td><code>pthread_create</code> + <code>join</code>（C）</td><td>23.6 µs</td></tr><tr><td><code>std::thread</code> 创建 + <code>join</code>（C++）</td><td>21.3 µs</td></tr><tr><td><code>std::thread</code> 创建 + <code>join</code>（Rust）</td><td>36.0 µs</td></tr><tr><td><code>threading.Thread</code> start + join（Python）</td><td>55.3 µs</td></tr><tr><td>内核线程间切换（含系统调用折算）</td><td>2.69 µs</td></tr><tr><td>默认线程栈</td><td>8192 KB</td></tr><tr><td>2000 线程时每个线程预留的虚拟内存（C）</td><td>8192 KB</td></tr><tr><td>2000 线程时每个线程的常驻内存（C）</td><td>8.0 KB</td></tr><tr><td>2000 线程时每个线程预留的虚拟内存（Rust）</td><td>6192 KB</td></tr><tr><td>2000 线程时每个线程的常驻内存（Rust）</td><td>9.8 KB</td></tr></tbody></table><p>线程创建用”<code>pthread_create</code> + <code>join</code> 循环除以次数”；线程内存这样量：先读 <code>/proc/self/status</code>，创建 2000 个阻塞在条件变量上的线程，再读一次，差值除以 2000：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* 从 /proc/self/status 里读一个 KB 单位的字段 */</span></div></div><div><div><div>2</div></div><div><span>static</span><span> </span><span>long</span><span> </span><span>vm</span><span>(</span><span>const</span><span> </span><span>char</span><span> </span><span>*</span><span><span>field</span><span>)</span></span></div></div><div><div><div>3</div></div><div><span>{</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>FILE </span><span>*</span><span>f </span><span>=</span><span> </span><span>fopen</span><span>(</span><span>"/proc/self/status"</span><span>, </span><span>"r"</span><span>);</span></div></div><div><div><div>5</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>f)</span></div></div><div><div><div>6</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>char</span><span> </span><span>line</span><span>[</span><span>256</span><span>];</span></div></div><div><div><div>8</div></div><div><span>  </span><span>long</span><span> v </span><span>=</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>size_t</span><span> fl </span><span>=</span><span> </span><span>strlen</span><span>(field);</span></div></div><div><div><div>10</div></div><div><span>  </span><span>while</span><span> (</span><span>fgets</span><span>(line, </span><span>sizeof</span><span> line, f)) {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>strncmp</span><span>(line, field, fl)) {</span></div></div><div><div><div>12</div></div><div><span>      </span><span>sscanf</span><span>(line </span><span>+</span><span> fl, </span><span>"</span><span>%ld</span><span>"</span><span>, </span><span>&amp;</span><span>v);</span></div></div><div><div><div>13</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>  </span><span>fclose</span><span>(f);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>return</span><span> v;</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>21</div></div><div><span>{</span></div></div><div><div><div>22</div></div><div><span>  </span><span>int</span><span> NT </span><span>=</span><span> </span><span>2000</span><span>;</span></div></div><div><div><div>23</div></div><div><span>  </span><span>pthread_t</span><span> </span><span>*</span><span>ts </span><span>=</span><span> </span><span>malloc</span><span>(</span><span>sizeof</span><span>(</span><span>pthread_t</span><span>) </span><span>*</span><span> NT);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>long</span><span> v0 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmSize:"</span><span>), r0 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>/* waiter 阻塞在条件变量上 */</span></div></div><div><div><div>26</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NT; i</span><span>++</span><span>)</span></div></div><div><div><div>27</div></div><div><span>    </span><span>pthread_create</span><span>(</span><span>&amp;</span><span>ts</span><span>[i], </span><span>NULL</span><span>, waiter, </span><span>NULL</span><span>);</span></div></div><div><div><div>28</div></div><div><span>  </span><span>long</span><span> v1 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmSize:"</span><span>), r1 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>29</div></div><div><span>  </span><span>printf</span><span>(</span><span>"vm_per_thread_kb,</span><span>%.1f</span><span>\n</span><span>"</span><span>, (</span><span>double</span><span>)(v1 </span><span>-</span><span> v0) </span><span>/</span><span> NT);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>printf</span><span>(</span><span>"rss_per_thread_kb,</span><span>%.1f</span><span>\n</span><span>"</span><span>, (</span><span>double</span><span>)(r1 </span><span>-</span><span> r0) </span><span>/</span><span> NT);</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>/* 唤醒 g_go 并 join 所有线程，略 */</span></div></div><div><div><div>32</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>33</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>ps</code> 看不到进程里有多少线程时，直接数 <code>/proc/&lt;pid&gt;/task</code> 目录：一个主线程加三个工作线程，就是四个 task 目录。</p><p>注意最后四行：<strong>每个 pthread 默认预留 8 MiB 的虚拟地址空间</strong>，但真正摸到的常驻内存只有 8 KB 左右。这看起来像是”线程很省”，其实是个陷阱：真正卡住线程数量的是<strong>虚拟地址预留 + 内核里每个 task 的结构</strong>，不是 RSS。你开不出 100 万个线程，不是因为内存被用光了，而是地址空间和内核结构先撑爆了。</p></section><section><h3>数据竞争：C 里的实测<a href="#数据竞争c-里的实测"><span>#</span></a></h3><p>线程共享内存的直接代价，就是同一份数据可能被两个线程同时读改写。下面这段程序用 4 个线程各把两个全局计数器加 100 万次：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;pthread.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;stdatomic.h&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>/* volatile 强制每次都读内存、写内存，让读-改-写的窗口暴露出来 */</span></div></div><div><div><div>6</div></div><div><span>static</span><span> </span><span>volatile</span><span> </span><span>long</span><span> plain </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>7</div></div><div><span>static</span><span> </span><span>atomic_long</span><span> atom </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>*</span><span>bump_plain</span><span>(</span><span>void</span><span> </span><span>*</span><span><span>a</span><span>)</span></span></div></div><div><div><div>10</div></div><div><span>{</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>(</span><span>void</span><span>)a;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>1000000</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>long</span><span> v </span><span>=</span><span> plain;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>plain </span><span>=</span><span> v </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>  </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>*</span><span>bump_atom</span><span>(</span><span>void</span><span> </span><span>*</span><span><span>a</span><span>)</span></span></div></div><div><div><div>20</div></div><div><span>{</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>(</span><span>void</span><span>)a;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>1000000</span><span>; i</span><span>++</span><span>)</span></div></div><div><div><div>23</div></div><div><span>    </span><span>atomic_fetch_add</span><span>(</span><span>&amp;</span><span>atom, </span><span>1</span><span>);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出（三次运行）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>普通全局变量 = 1299722（期望 4000000）</span></div></div><div><div><div>2</div></div><div><span>普通全局变量 = 1443475（期望 4000000）</span></div></div><div><div><div>3</div></div><div><span>普通全局变量 = 1365161（期望 4000000）</span></div></div><div><div><div>4</div></div><div><span>原子变量     = 4000000（期望 4000000）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>普通变量丢了大约三分之二的更新，因为 <code>plain = v + 1</code> 不是原子的：读和写之间，别的线程可能已经改过了。原子操作把读-改-写变成一个不可分割的动作，才得到正确结果。</p><div><div><div></div><div>Warning</div></div><div><p>这是未定义行为在实践里的样子。更麻烦的是，它往往在开发机上”看起来是对的”，到了生产环境才丢数据。</p></div></div></section><section><h3>C++：<code>std::thread</code> 与 <code>std::atomic</code><a href="#cstdthread-与-stdatomic"><span>#</span></a></h3><p>C++ 的 <code>std::thread</code> 就是对 pthread 的薄封装，实测 21.3 µs 与 C 的 23.6 µs 基本一致。C++ 真正加的是类型系统和 RAII：</p><ul>
<li><code>std::jthread</code>（C++20）在析构时自动 <code>join</code>，并支持 <code>stop_token</code> 协作取消，避免忘记 join 导致的 <code>terminate</code>；</li>
<li><code>std::atomic&lt;T&gt;</code> 把原子操作变成类型，比裸的 <code>atomic_long</code> 更安全；</li>
<li><strong>数据竞争在 C++ 里仍然是未定义行为</strong>，编译器不会替你检查。</li>
</ul><p>C++ 的并发能力很全，但也意味着它把”要不要加锁”这个判断完全交给你。</p></section><section><h3>Python：GIL 的实测<a href="#pythongil-的实测"><span>#</span></a></h3><p>Python 的线程是货真价实的 OS 线程，但 CPython 的解释器里有一把<strong>全局解释器锁（GIL）</strong>：同一时刻只有一个线程能执行 Python 字节码。它每 5 ms 左右释放一次（实测 <code>sys.getswitchinterval()</code> 返回 <code>0.0050</code>），让别的线程有机会跑。</p><p>结果就是前面那张表：两个 CPU 密集型线程毫无加速（1.05x），两个进程才有（1.95x）。</p><p>那 Python 线程还有什么用？<strong>I/O 密集</strong>。当线程卡在 <code>socket.recv()</code>、文件读写这类操作上时，它会释放 GIL，别的线程就能继续跑。所以”下载 100 个文件”用线程池是有效的，而”算 100 万个质数”应该用进程池。</p><div><div><div></div><div>Note</div></div><div><p>这是 CPython 的实现细节，不是 Python 语言的语义。CPython 3.13 起提供了官方的自由线程（free-threaded）构建，正在逐步弱化 GIL。但本文用的 3.14.7 仍是带 GIL 的常规构建。</p></div></div></section><section><h3>Rust：把数据竞争挡在编译期<a href="#rust把数据竞争挡在编译期"><span>#</span></a></h3><p>Rust 的线程创建要 36.0 µs，比 pthread 略贵一点（包装更多）。它真正独特的是 <strong><code>Send</code> / <code>Sync</code> 这两个自动 trait</strong>：编译器会检查跨线程传递的类型是否安全，不安全就拒绝编译。</p><p>试着把一个 <code>Rc</code> 移进新线程：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>rc</span><span>::</span></span><span>Rc</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>let</span><span> </span><span>shared</span><span><span> </span><span>=</span><span> </span></span><span>Rc</span><span>::</span><span>new</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>std</span><span>::</span><span>thread</span><span>::</span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>println!</span><span>(</span><span>"{}"</span><span>, </span><span>shared</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器直接拦下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error[E0277]: `Rc&lt;i32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>--&gt; demo_rs_send.rs:5:24</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>4</div></div><div><span>5 |       std::thread::spawn(move || {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>|       ------------------ ^------</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>| |     |</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>| |     required by a bound introduced by this call</span></div></div><div><div><div>8</div></div><div><span>6 | |         println!("{}", shared);</span></div></div><div><div><div>9</div></div><div><span>7 | |     });</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>| |_____^ `Rc&lt;i32&gt;` cannot be sent between threads safely</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>|</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>= help: within `{closure@demo_rs_send.rs:5:24: 5:31}`, the trait `Send` is not implemented for `Rc&lt;i32&gt;`</span></div></div><div><div><div>13</div></div><div><span>note: required because it's used within this closure</span></div></div><div><div><div>14</div></div><div><span>note: required by a bound in `spawn`</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>Rc</code> 的引用计数不是原子的，跨线程共享会撕裂计数，所以它没有实现 <code>Send</code>。要用就得换成 <code>Arc</code>（原子引用计数）。这类错误在 C/C++ 里是运行时的数据竞争，在 Rust 里是编译错误。</p></section><section><h3>线程的上限<a href="#线程的上限"><span>#</span></a></h3><p>线程是一个很好的”真并行”工具，但它有三个天花板：</p><ol>
<li><strong>一 MiB 级的栈预留</strong>，把线程总数卡在几万级；</li>
<li><strong>创建和切换都要进内核</strong>，切换约 2.7 µs；</li>
<li><strong>调度交给内核</strong>，你无法决定”先跑哪个就绪线程”。</li>
</ol><p>面对”十万个并发连接”，这三条里的每一条都是致命的。于是有了协程。</p></section></section>
<section><h2>四、协程：把”暂停”这件事挪到用户态<a href="#四协程把暂停这件事挪到用户态"><span>#</span></a></h2><section><h3>先算一笔账<a href="#先算一笔账"><span>#</span></a></h3><p>假设要做 10 万个并发连接。用”一个连接一个线程”，光栈的虚拟地址预留就是：</p><p><span><span>100,000×8 MiB=800 GiB100{,}000 \times 8\ \text{MiB} = 800\ \text{GiB}</span><span><span><span></span><span>100</span><span><span>,</span></span><span>000</span><span></span><span>×</span><span></span></span><span><span></span><span>8</span><span> </span><span><span>MiB</span></span><span></span><span>=</span><span></span></span><span><span></span><span>800</span><span> </span><span><span>GiB</span></span></span></span></span></p><p>这还没算内核结构。所以线程模型在这种场景下直接出局。这不是”优化一下”能解决的，是模型选错了。</p></section><section><h3>协程的本质：一个可以暂停、之后接着跑的函数<a href="#协程的本质一个可以暂停之后接着跑的函数"><span>#</span></a></h3><p>协程（coroutine）的出发点是：<strong>大多数连接在绝大多数时间里都在等 I/O，凭什么给它分配一个完整的栈、还让内核来管它？</strong></p><p>普通函数只有”调用、返回”两种结局。协程多了一种状态：<strong>暂停（suspend）</strong>，把当前位置和局部状态记住，把控制权交还给调度器；之后<strong>恢复（resume）</strong>，从暂停处继续。因为暂停和恢复都发生在用户态，不需要陷入内核，切换一次只要几十纳秒到几微秒。</p><p></p><figure><img src="/assets/images/process-in-os/stackful-vs-stackless.svg" alt="有栈协程与无栈协程的对比" /><figcaption>有栈协程与无栈协程的对比</figcaption></figure><p></p><p>两种实现方式：</p><ul>
<li><strong>有栈协程（stackful）</strong>：每个协程自带一段栈，切换就是换栈指针。它可以在任意嵌套调用深处挂起，代价是每个协程要预留栈空间（KB 到 MB 级）。C 的 <code>ucontext</code>、早期的一些库走这条路。</li>
<li><strong>无栈协程（stackless）</strong>：协程是编译器生成的一个<strong>状态机</strong>，没有自己的栈，局部变量和恢复点打包成一个”帧”放在堆上。它只能在显式的 <code>await</code> / <code>yield</code> 处挂起，换来的是帧大小在编译期就确定、可以开到几十万上百万个。C++20 协程、Python 的 <code>async</code>、Rust 的 <code>async</code> 都是这一类。</li>
</ul></section><section><h3>两层成本对照（实测）<a href="#两层成本对照实测"><span>#</span></a></h3><p>一次切换 / 恢复 / 创建的耗时，横跨六个数量级：</p><p></p><figure><img src="/assets/images/process-in-os/cost-ladder.svg" alt="一次切换、恢复、创建的耗时（对数刻度）" /><figcaption>一次切换、恢复、创建的耗时（对数刻度）</figcaption></figure><p></p><p>同样的量级差异，也体现在内存上：</p><p></p><figure><img src="/assets/images/process-in-os/memory-ladder.svg" alt="每个并发实体的内存（对数刻度）" /><figcaption>每个并发实体的内存（对数刻度）</figcaption></figure><p></p><p>这两张图是整篇文章的核心。<strong>协程 resume 比内核线程切换便宜约一千倍</strong>；而同样开 100 万个，Rust 的 Future 只要 15 MB，OS 线程的栈预留要 8 TiB（直接不可行）。</p></section><section><h3>C：语言里没有协程，就自己造一个<a href="#c语言里没有协程就自己造一个"><span>#</span></a></h3><p>C 语言本身没有协程关键字。要用，就得靠 <code>ucontext</code>（或 <code>setjmp</code> / <code>longjmp</code>、或第三方库）手工实现有栈协程：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#define</span><span> </span><span>_XOPEN_SOURCE</span><span> </span><span>700</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;ucontext.h&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>static</span><span> </span><span>ucontext_t</span><span> main_ctx, a_ctx, b_ctx;</span></div></div><div><div><div>6</div></div><div><span>static</span><span> </span><span>char</span><span> </span><span>stk_a</span><span>[</span><span>65536</span><span>], </span><span>stk_b</span><span>[</span><span>65536</span><span>];</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>co_a</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>9</div></div><div><span>{</span></div></div><div><div><div>10</div></div><div><span>  </span><span>for</span><span> (;;) {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>printf</span><span>(</span><span>"A"</span><span>);</span></div></div><div><div><div>12</div></div><div><span>    </span><span>fflush</span><span>(stdout);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>swapcontext</span><span>(</span><span>&amp;</span><span>a_ctx, </span><span>&amp;</span><span>main_ctx);</span><span> /* 暂停 A，回到 main */</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>co_b</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>18</div></div><div><span>{</span></div></div><div><div><div>19</div></div><div><span>  </span><span>for</span><span> (;;) {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>printf</span><span>(</span><span>"B"</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>fflush</span><span>(stdout);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>swapcontext</span><span>(</span><span>&amp;</span><span>b_ctx, </span><span>&amp;</span><span>main_ctx);</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>27</div></div><div><span>{</span></div></div><div><div><div>28</div></div><div><span>  </span><span>getcontext</span><span>(</span><span>&amp;</span><span>a_ctx);</span></div></div><div><div><div>29</div></div><div><span>  </span><span>a_ctx</span><span>.</span><span>uc_stack</span><span>.</span><span>ss_sp</span><span> </span><span>=</span><span> stk_a;</span></div></div><div><div><div>30</div></div><div><span>  </span><span>a_ctx</span><span>.</span><span>uc_stack</span><span>.</span><span>ss_size</span><span> </span><span>=</span><span> </span><span>sizeof</span><span> stk_a;</span></div></div><div><div><div>31</div></div><div><span>  </span><span>a_ctx</span><span>.</span><span>uc_link</span><span> </span><span>=</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>32</div></div><div><span>  </span><span>makecontext</span><span>(</span><span>&amp;</span><span>a_ctx, co_a, </span><span>0</span><span>);</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>  </span><span>getcontext</span><span>(</span><span>&amp;</span><span>b_ctx);</span></div></div><div><div><div>35</div></div><div><span>  </span><span>b_ctx</span><span>.</span><span>uc_stack</span><span>.</span><span>ss_sp</span><span> </span><span>=</span><span> stk_b;</span></div></div><div><div><div>36</div></div><div><span>  </span><span>b_ctx</span><span>.</span><span>uc_stack</span><span>.</span><span>ss_size</span><span> </span><span>=</span><span> </span><span>sizeof</span><span> stk_b;</span></div></div><div><div><div>37</div></div><div><span>  </span><span>b_ctx</span><span>.</span><span>uc_link</span><span> </span><span>=</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>38</div></div><div><span>  </span><span>makecontext</span><span>(</span><span>&amp;</span><span>b_ctx, co_b, </span><span>0</span><span>);</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>4</span><span>; i</span><span>++</span><span>) {</span></div></div><div><div><div>41</div></div><div><span>    </span><span>swapcontext</span><span>(</span><span>&amp;</span><span>main_ctx, </span><span>&amp;</span><span>a_ctx);</span></div></div><div><div><div>42</div></div><div><span>    </span><span>swapcontext</span><span>(</span><span>&amp;</span><span>main_ctx, </span><span>&amp;</span><span>b_ctx);</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span>  </span><span>printf</span><span>(</span><span>"</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>45</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ABABABAB</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>swapcontext</code> 做的事情，本质上就是”保存当前寄存器到 <code>main_ctx</code>，从 <code>a_ctx</code> 恢复寄存器”，和进程切换的原理一样，只不过全程在用户态，而且切换的是我们自己造的上下文。<code>ucontext</code> 在新版 POSIX 里已经被标记为废弃，但它把”协程”这件事拆得最干净，值得亲手写一遍。</p><div><div><div></div><div>Note</div></div><div><p>这也是绿色线程（green thread）的基础：在用户态维护一个协程池，再把它们多路复用到少量内核线程上。</p></div></div></section><section><h3>C++20：编译器把函数改写成状态机<a href="#c20编译器把函数改写成状态机"><span>#</span></a></h3><p>C++20 引入了语言级协程。它最反直觉的一点是：<strong>协程的语义几乎全靠编译器把函数改写成状态机</strong>，标准库只提供”搭子”（<code>std::coroutine_handle</code>、<code>std::suspend_always</code> 等）。写一个最小的生成器只需要实现 <code>promise_type</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;coroutine&gt;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>struct</span><span> </span><span>Gen</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>struct</span><span> </span><span>promise_type</span><span> {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>int</span><span> </span><span>value</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>yield_value</span><span>(</span><span>int</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span>      </span><span>value</span><span> </span><span>=</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>8</div></div><div><span>      </span><span>return</span><span> {};</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>    </span><span>Gen</span><span> </span><span>get_return_object</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>      </span><span>return</span><span> </span><span>Gen</span><span><span>{</span><span>std</span><span>::</span><span>coroutine_handle</span><span>&lt;</span></span><span>promise_type</span><span>&gt;::</span><span>from_promise</span><span>(</span><span>*</span><span>this</span><span>)};</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>initial_suspend</span><span>() </span><span>noexcept</span><span> { </span><span>return</span><span> {}; }</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>final_suspend</span><span>() </span><span>noexcept</span><span> { </span><span>return</span><span> {}; }</span></div></div><div><div><div>15</div></div><div><span>    </span><span>void</span><span> </span><span>return_void</span><span>() {}</span></div></div><div><div><div>16</div></div><div><span>    </span><span>void</span><span> </span><span>unhandled_exception</span><span>() {}</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>coroutine_handle</span><span>&lt;</span><span>promise_type</span><span>&gt; </span><span>h</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>bool</span><span> </span><span>next</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>h</span><span>.</span><span>resume</span><span>();</span></div></div><div><div><div>21</div></div><div><span>    </span><span>return</span><span> </span><span>!</span><span>h</span><span>.</span><span>done</span><span>();</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>};</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>Gen</span><span> </span><span>gen</span><span>(</span><span>long</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>26</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>i</span><span>++</span><span>) </span><span>co_yield</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>co_yield</code> 每次都会暂停，<code>h.resume()</code> 是从暂停处继续。把 1 亿次 resume 和一个普通循环对比：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>long</span><span> </span><span>N</span><span> </span><span>=</span><span> </span><span><span>100000000</span><span>L</span></span><span>;</span></div></div><div><div><div>2</div></div><div><span>Gen</span><span> </span><span>g</span><span> </span><span>=</span><span> </span><span>gen</span><span>(</span><span>N</span><span>);</span></div></div><div><div><div>3</div></div><div><span>double</span><span> </span><span>t0</span><span> </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>4</div></div><div><span>for</span><span> (</span><span>long</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) </span><span>g</span><span>.</span><span>next</span><span>();</span><span> /* 每次 next() 就是一次 resume */</span></div></div><div><div><div>5</div></div><div><span>double</span><span> </span><span>t1</span><span> </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>6</div></div><div><span>/* (t1 - t0) / N 就是单次 resume 的秒数 */</span></div></div></code></pre><div><div></div><div></div></div></figure></div>

<table><thead><tr><th>操作</th><th>每次耗时</th></tr></thead><tbody><tr><td>协程 resume</td><td>2.08 ns</td></tr><tr><td>普通循环迭代</td><td>0.23 ns</td></tr></tbody></table><p>一次 resume 大约是一个循环迭代的 9 倍，但仍然是<strong>纳秒级</strong>。而且协程帧在堆上，一个 1 亿次循环的生成器只占一个帧。实测 100 万个挂起的协程帧，总共 68.8 MB，平均每个 72.2 字节。</p><div><div><div></div><div>Tip</div></div><div><p><code>co_await</code>、<code>co_yield</code>、<code>co_return</code> 是 C++ 里仅有的三个”协程关键字”。任何函数只要出现其中任意一个，编译器就会把它当成协程改写。这也是 C++ 协程看起来”难”的原因：你看到的是普通函数，编译器看到的是一台状态机。</p></div></div></section><section><h3>Python：从 yield 到 async/await 的十年<a href="#python从-yield-到-asyncawait-的十年"><span>#</span></a></h3><p>Python 的协程不是一蹴而就的，它是一步步长出来的：</p><ol>
<li><strong><code>yield</code></strong>（Python 2.2）：生成器，函数可以在 <code>yield</code> 处暂停并保留局部状态；</li>
<li><strong><code>send()</code></strong>（PEP 342，2.5）：可以把值送进暂停中的生成器，协程有了”双向通信”；</li>
<li><strong><code>yield from</code></strong>（PEP 380，3.3）：可以把一个生成器的暂停委托给另一个，解决嵌套；</li>
<li><strong><code>async</code> / <code>await</code></strong>（PEP 492，3.5）：给协程一套独立的语法，和生成器分开。</li>
</ol><p>最后一步的底层，仍然是生成器那套机制。用 <code>dis</code> 反汇编一个 <code>async</code> 函数，能看到暂停是通过 <code>YIELD_VALUE</code> 实现的：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>   </span></span><span>6           RETURN_GENERATOR</span></div></div><div><div><div>2</div></div><div><span><span>               </span></span><span>POP_TOP</span></div></div><div><div><div>3</div></div><div><span><span>       </span></span><span>L1:     RESUME                   0</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>7           LOAD_GLOBAL              0 (asyncio)</span></div></div><div><div><div>6</div></div><div><span><span>               </span></span><span>LOAD_ATTR                2 (sleep)</span></div></div><div><div><div>7</div></div><div><span><span>               </span></span><span>PUSH_NULL</span></div></div><div><div><div>8</div></div><div><span><span>               </span></span><span>LOAD_SMALL_INT           0</span></div></div><div><div><div>9</div></div><div><span><span>               </span></span><span>CALL                     1</span></div></div><div><div><div>10</div></div><div><span><span>               </span></span><span>GET_AWAITABLE            0</span></div></div><div><div><div>11</div></div><div><span><span>               </span></span><span>LOAD_CONST               1 (None)</span></div></div><div><div><div>12</div></div><div><span><span>       </span></span><span>L2:     SEND                     3 (to L5)</span></div></div><div><div><div>13</div></div><div><span><span>       </span></span><span>L3:     YIELD_VALUE              1</span></div></div><div><div><div>14</div></div><div><span><span>       </span></span><span>L4:     RESUME                   3</span></div></div><div><div><div>15</div></div><div><span><span>               </span></span><span>JUMP_BACKWARD_NO_INTERRUPT 5 (to L2)</span></div></div><div><div><div>16</div></div><div><span><span>       </span></span><span>L5:     END_SEND</span></div></div><div><div><div>17</div></div><div><span><span>               </span></span><span>POP_TOP</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>GET_AWAITABLE</code> 拿出被等待对象，<code>SEND</code> 驱动它，<code>YIELD_VALUE</code> 把控制权交还给事件循环，<code>RESUME</code> 是被唤醒后的入口。<strong>协程的”暂停”在字节码层面就是一次 <code>yield</code>。</strong></p><p>代价对比很清楚：</p>

<table><thead><tr><th>操作</th><th>每次耗时</th></tr></thead><tbody><tr><td>生成器 resume（纯协程切换）</td><td>40 ns</td></tr><tr><td>asyncio 切换（经由事件循环）</td><td>2.13 µs</td></tr><tr><td>asyncio 任务创建 + 运行</td><td>6.07 µs</td></tr><tr><td>100 万挂起 Task 的内存</td><td>678.6 MB（每个 711.6 B）</td></tr></tbody></table><p>生成器 resume 只要 40 ns，而经过 asyncio 的 <code>await</code> 要 2.13 µs，差了 50 倍。差在哪？差在 <strong>Task、Future、事件循环的簿记</strong>：每次 <code>await</code> 都要经过事件循环的调度、回调入队、状态检查。真正”暂停一个函数”很便宜，让整个调度器转一圈不便宜。</p><p>单个 Task 要 712 字节，也比 C++ / Rust 的帧大得多，因为它同时挂了协程对象、Task 对象、回调列表等。100 万个 Task 要 679 MB，比 Rust 多两个数量级，但仍然远好于”100 万个线程”。</p><p>单线程上交错运行两个任务：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A 0</span></div></div><div><div><div>2</div></div><div><span>B 0</span></div></div><div><div><div>3</div></div><div><span>A 1</span></div></div><div><div><div>4</div></div><div><span>B 1</span></div></div><div><div><div>5</div></div><div><span>A 2</span></div></div><div><div><div>6</div></div><div><span>B 2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个 <code>worker</code> 交替推进，因为它们都在 <code>await asyncio.sleep(0)</code> 处主动让出。这就是<strong>协作式调度</strong>：切换点由代码显式写出来，不像线程那样随时可能被抢占。</p><div><div><div></div><div>Note</div></div><div><p>因为 asyncio 只用一个线程，GIL 对它不再是问题。协程的并行来自 I/O 的并发等待，而不是 CPU 的并行计算。CPU 密集的部分该用进程池还得用。</p></div></div></section><section><h3>Rust：Future 就是状态机，运行时可以自己写<a href="#rustfuture-就是状态机运行时可以自己写"><span>#</span></a></h3><p>Rust 的 <code>async fn</code> 编译成一个实现了 <code>Future</code> 的状态机。<code>Future</code> 的定义只有一句：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pub</span><span> </span><span>trait</span><span> </span><span>Future</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>fn</span><span> </span><span>poll</span><span>(</span><span>self</span><span>: </span><span>Pin</span><span>&lt;&amp;</span><span>mut</span><span> </span><span>Self</span><span>&gt;, </span><span>cx</span><span>: &amp;</span><span>mut</span><span> </span><span>Context</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Poll</span><span>&lt;</span><span>Self</span><span>::</span><span>Output</span><span>&gt;;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>每次 <code>poll</code> 返回 <code>Ready</code>（完成）或 <code>Pending</code>（未完成，稍后再问）。<code>std</code> 里<strong>没有任何执行器</strong>，所以一个不接运行时的 <code>async fn</code> 就是一块不会自己跑的代码。这反而给了我们一个机会：手写一个最小的执行器，看清协程到底是怎么被驱动的。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>task</span><span>::{</span></span><span>Context</span><span>, </span><span>Poll</span><span>, </span><span>RawWaker</span><span>, </span><span>RawWakerVTable</span><span>, </span><span>Waker</span><span>};</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>const</span><span> </span><span>RAW</span><span>: </span><span>RawWaker</span><span><span> </span><span>=</span><span> </span></span><span>RawWaker</span><span>::</span><span>new</span><span><span>(</span><span>std</span><span>::</span><span>ptr</span><span>::</span></span><span>null</span><span>(), &amp;</span><span>VTABLE</span><span>);</span></div></div><div><div><div>4</div></div><div><span>const</span><span> </span><span>VTABLE</span><span>: </span><span>RawWakerVTable</span><span><span> </span><span>=</span><span> </span></span><span>RawWakerVTable</span><span>::</span><span>new</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> </span></span><span>RAW</span><span><span>, </span><span>|</span></span><span>_</span><span><span>|</span><span> {}, </span><span>|</span></span><span>_</span><span><span>|</span><span> {}, </span><span>|</span></span><span>_</span><span><span>|</span><span> {});</span></span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>fn</span><span> </span><span>noop_waker</span><span>() -&gt; </span><span>Waker</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>unsafe</span><span> { </span><span>Waker</span><span>::</span><span>from_raw</span><span>(</span><span>RAW</span><span>) }</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>/// 最小执行器：不接任何运行时，轮询到完成</span></div></div><div><div><div>11</div></div><div><span>fn</span><span> </span><span>block_on</span><span>&lt;</span><span>F</span><span>: </span><span>Future</span><span>&gt;(</span><span>f</span><span>: </span><span>F</span><span><span>) -&gt; </span><span>F</span><span>::</span></span><span>Output</span><span> {</span></div></div><div><div><div>12</div></div><div><span>    </span><span>let</span><span> </span><span>w</span><span><span> </span><span>=</span><span> </span></span><span>noop_waker</span><span>();</span></div></div><div><div><div>13</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>cx</span><span><span> </span><span>=</span><span> </span></span><span>Context</span><span>::</span><span>from_waker</span><span>(&amp;</span><span>w</span><span>);</span></div></div><div><div><div>14</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Box</span><span>::</span><span>pin</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>15</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>16</div></div><div><span>        </span><span>match</span><span> </span><span>f</span><span>.</span><span>as_mut</span><span>().</span><span>poll</span><span>(&amp;</span><span>mut</span><span> </span><span>cx</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Ready</span><span>(</span><span>v</span><span>) =&gt; </span><span>return</span><span> </span><span>v</span><span>,</span></div></div><div><div><div>18</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Pending</span><span><span> =&gt; </span><span>std</span><span>::</span><span>hint</span><span>::</span></span><span>spin_loop</span><span>(),</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>配一个”每次 poll 都返回 Pending、直到第 N 次才 Ready”的 future，用上面这个 <code>block_on</code> 驱动，就能量出单次 poll 的成本：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Spin</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>n</span><span>: </span><span>u64</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>target</span><span>: </span><span>u64</span><span>,</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>impl</span><span> </span><span>Future</span><span> </span><span>for</span><span> </span><span>Spin</span><span> {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>u64</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fn</span><span> </span><span>poll</span><span>(</span><span>mut</span><span> </span><span>self</span><span>: </span><span>Pin</span><span>&lt;&amp;</span><span>mut</span><span> </span><span>Self</span><span>&gt;, </span><span>cx</span><span>: &amp;</span><span>mut</span><span> </span><span>Context</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Poll</span><span>&lt;</span><span>u64</span><span>&gt; {</span></div></div><div><div><div>9</div></div><div><span>        </span><span>self</span><span><span>.n </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>10</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.n </span><span>&gt;=</span><span> </span></span><span>self</span><span>.target {</span></div></div><div><div><div>11</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Ready</span><span>(</span><span>self</span><span>.n)</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>13</div></div><div><span>            </span><span>cx</span><span>.</span><span>waker</span><span>().</span><span>wake_by_ref</span><span>();</span></div></div><div><div><div>14</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Pending</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>let</span><span> </span><span>t0</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>20</div></div><div><span>block_on</span><span>(</span><span>Spin</span><span> {</span></div></div><div><div><div>21</div></div><div><span>    </span><span>n</span><span>: </span><span>0</span><span>,</span></div></div><div><div><div>22</div></div><div><span>    </span><span>target</span><span>: </span><span>100_000_000</span><span>,</span></div></div><div><div><div>23</div></div><div><span>});</span></div></div><div><div><div>24</div></div><div><span>let</span><span> </span><span>per_poll</span><span><span> </span><span>=</span><span> </span></span><span>t0</span><span>.</span><span>elapsed</span><span>().</span><span>as_secs_f64</span><span>() / </span><span>100_000_000.0</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div>

<table><thead><tr><th>操作</th><th>每次耗时</th></tr></thead><tbody><tr><td><code>Future::poll</code>（空转执行器）</td><td>14.8 ns</td></tr><tr><td>一个 <code>async fn</code> 产生的 Future 大小</td><td>16 B</td></tr><tr><td>100 万个挂起 Future 的内存</td><td>15.3 MB（每个 16 B）</td></tr></tbody></table><p>14.8 ns 比 C++ 的 2.08 ns 高一些，因为这里多了 waker 调用和 <code>spin_loop</code>，但仍然是纳秒级。</p><p>Future 的大小是编译期已知的，而且会<strong>随它跨 <code>await</code> 持有的状态增长</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>big</span><span>() -&gt; </span><span>u8</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>let</span><span> </span><span>buf</span><span><span> </span><span>=</span><span> [</span></span><span>0</span><span>u8</span><span>; </span><span>4096</span><span>]; </span><span>// 4 KiB 的局部数组</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>std</span><span>::</span><span>future</span><span>::</span><span>pending</span><span>::&lt;()&gt;().</span><span>await</span><span>; </span><span>// 永远挂起</span></div></div><div><div><div>4</div></div><div><span>    </span><span>buf</span><span>[</span><span>0</span><span>]</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>let</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>big</span><span>();</span></div></div><div><div><div>9</div></div><div><span>    </span><span>println!</span><span>(</span><span>"big future size = {} bytes"</span><span><span>, </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of_val</span><span>(&amp;</span><span>f</span><span>));</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>big future size = 4097 bytes</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>4096 字节的数组被原样放进了状态机帧。这说明一个反直觉但重要的事实：<strong>无栈协程的”栈”并没有消失，它变成了堆上这个编译期确定大小、按需分配的状态机帧</strong>。小帧 16 字节，大帧几 KB，取决于你跨 <code>await</code> 持有什么。</p><div><div><div></div><div>Important</div></div><div><p>三个关键类型：<code>Pin</code> 保证帧在内存里不会移动（自引用安全），<code>Waker</code> 是”我先睡了，完事叫我”的回调，<code>Context</code> 把 waker 传给 <code>poll</code>。<code>tokio</code> 这类运行时做的事，就是提供一个高效的调度器，把这些 Future 多路复用到少量线程上。运行时不在 <code>std</code> 里，是 Rust 刻意的选择：标准库不替你做”用什么调度策略”的决定。</p></div></div></section></section>
<section><h2>五、三层抽象放在一起<a href="#五三层抽象放在一起"><span>#</span></a></h2><p></p><figure><img src="/assets/images/process-in-os/abstraction-nesting.svg" alt="进程、线程与协程的包含关系" /><figcaption>进程、线程与协程的包含关系</figcaption></figure><p></p><p>包含关系是这样的：协程跑在线程里，线程跑在进程里。调度层次也是嵌套的：内核调度进程和线程，运行时调度协程。</p><p>横向对比：</p>

<table><thead><tr><th>维度</th><th>进程</th><th>线程</th><th>协程（无栈）</th></tr></thead><tbody><tr><td>地址空间</td><td>独立</td><td>共享</td><td>共享</td></tr><tr><td>私有状态</td><td>全套</td><td>栈 + 寄存器 + TLS</td><td>堆上一个状态机帧</td></tr><tr><td>谁调度</td><td>内核</td><td>内核</td><td>运行时（用户态）</td></tr><tr><td>切换成本（实测）</td><td>2.81 µs</td><td>2.69 µs</td><td>2 ns - 2 µs</td></tr><tr><td>创建成本（实测）</td><td>173 µs</td><td>21-55 µs</td><td>16-712 B / 个</td></tr><tr><td>数量级</td><td>几十到几百</td><td>几千到几万</td><td>几十万到几百万</td></tr><tr><td>通信</td><td>IPC（要拷贝或映射）</td><td>共享内存 + 同步原语</td><td>共享内存 + 运行时</td></tr><tr><td>隔离性</td><td>强</td><td>弱</td><td>弱</td></tr><tr><td>抢占</td><td>有</td><td>有</td><td>无（协作式）</td></tr></tbody></table><section><h3>怎么选<a href="#怎么选"><span>#</span></a></h3><ul>
<li><strong>要强隔离、要容错</strong>：进程。一个进程崩了，其他进程还在。浏览器标签页、数据库的多进程模型都是这个逻辑。</li>
<li><strong>要真并行、共享数据</strong>：线程。CPU 密集、数据高度共享，用线程池。注意 Python 的这个选项要让位给多进程。</li>
<li><strong>要海量并发、以 I/O 等待为主</strong>：协程。十万连接的服务器、爬虫、网关，协程几乎是唯一划算的模型。</li>
<li><strong>不要在协程里跑 CPU 密集任务</strong>：它会霸占整个事件循环，把其他协程一起饿死。该扔给线程池或进程池。</li>
</ul><div><div><div></div><div>Warning</div></div><div><p>一个常见反模式：在 asyncio 里调用同步的阻塞函数（比如 <code>requests.get</code>）。它会卡住整个事件循环，让所有协程停摆。要么换成异步库，要么显式扔进 <code>run_in_executor</code>。</p></div></div></section></section>
<section><h2>六、怎么自己复现<a href="#六怎么自己复现"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># C / C++</span></div></div><div><div><div>2</div></div><div><span>gcc</span><span> </span><span>-O2</span><span> </span><span>-pthread</span><span> </span><span>bench_c.c</span><span> </span><span>-o</span><span> </span><span>bench_c</span><span> &amp;&amp; </span><span>./bench_c</span></div></div><div><div><div>3</div></div><div><span>g++</span><span> </span><span>-O2</span><span> </span><span>-std=c++20</span><span> </span><span>bench_cpp.cpp</span><span> </span><span>-o</span><span> </span><span>bench_cpp</span><span> &amp;&amp; </span><span>./bench_cpp</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># Python</span></div></div><div><div><div>6</div></div><div><span>python3</span><span> </span><span>bench_py.py</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># Rust（无外部依赖）</span></div></div><div><div><div>9</div></div><div><span>rustc</span><span> </span><span>--edition</span><span> </span><span>2024</span><span> </span><span>-O</span><span> </span><span>bench_rs.rs</span><span> </span><span>-o</span><span> </span><span>bench_rs</span><span> &amp;&amp; </span><span>./bench_rs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>进程间/线程间的管道往返测量，本质是 lmbench 里 <code>lat_ctx</code> 的思路：两个执行流绑在同一个核上，用一个字节来回交接，一次往返除以 2 就是单次切换。这个数字<strong>包含了系统调用开销</strong>，所以它是内核切换成本的一个上界；<code>perf sched</code> 能看到更纯粹的数字。</p><p>其他值得自己动手的工具：</p><ul>
<li><code>taskset -c 0 ./program</code>：把进程绑到某个核上，减少迁移带来的噪声；</li>
<li><code>cat /proc/&lt;pid&gt;/status</code>：看 <code>VmSize</code> / <code>VmRSS</code> / <code>Threads</code>；</li>
<li><code>ls /proc/&lt;pid&gt;/task</code>：看这个进程里到底有几个”任务”；</li>
<li><code>perf sched latency</code> / <code>perf sched record</code>：看真实的调度延迟和切换。</li>
</ul></section>
<section><h2>七、往前看<a href="#七往前看"><span>#</span></a></h2><p>我写完这三层，最大的感受是：并发模型的演进史，本质上是”把成本更高的抽象，换成成本更低的抽象”。先有进程（为了隔离，付全套虚拟化的代价），再有线程（为了共享，退掉地址空间隔离），最后有协程（为了数量，连内核调度和栈都退掉）。每一层都在用<strong>隔离性</strong>或<strong>调度能力</strong>，去换<strong>规模</strong>。</p><p>再往下走，我猜方向还是同一个。有几个正在发生的趋势：</p><ul>
<li><strong>结构化并发</strong>：让并发生命周期和词法作用域绑定，父任务负责回收子任务，解决”协程泄漏”和取消传播。Rust 的 <code>tokio::task</code> 生态、Python 的 <code>TaskGroup</code>、C++ 的 <code>std::execution</code> 都在往这个方向收。</li>
<li><strong>内核态的异步 I/O</strong>：<code>io_uring</code> 把”提交一批 I/O、之后统一收割”做成了一次系统调用，进一步压低 I/O 密集场景下每个事件的成本。</li>
<li><strong>把有栈协程的便利和无栈协程的可扩展性合起来</strong>：这是有栈 M 调度一直在尝试、但还没统一的问题。</li>
</ul><p>如果只记一句：<strong>进程买隔离，线程买并行，协程买规模</strong>。搞清楚你现在缺的是哪一样，选型就不会错。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><p>本文参考了以下资料，排名无先后顺序：</p><ol>
<li><a href="https://en.wikipedia.org/wiki/Timeline_of_operating_systems" target="_blank">Timeline of operating systems - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/History_of_operating_systems" target="_blank">History of operating systems - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/ENIAC" target="_blank">ENIAC - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC" target="_blank">First Draft of a Report on the EDVAC - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/GM-NAA_I/O" target="_blank">GM-NAA I/O - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/Resident_monitor" target="_blank">Resident monitor - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/Multics" target="_blank">Multics - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/Unix" target="_blank">Unix - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/Task_Control_Block" target="_blank">Task Control Block - Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/Round-robin_scheduling" target="_blank">Round-robin scheduling - Wikipedia</a></li>
<li><a href="https://man7.org/linux/man-pages/man2/clone.2.html" target="_blank">clone(2) - Linux man page</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/pthreads.7.html" target="_blank">pthreads(7) - Linux man page</a></li>
<li><a href="https://peps.python.org/pep-0492/" target="_blank">PEP 492 - Coroutines with async and await syntax</a></li>
<li><a href="https://peps.python.org/pep-0380/" target="_blank">PEP 380 - Syntax for delegating to a subgenerator</a></li>
<li><a href="https://peps.python.org/pep-0342/" target="_blank">PEP 342 - Coroutines via Enhanced Generators</a></li>
<li><a href="https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await" target="_blank">C++ Coroutines: Understanding operator co_await</a></li>
<li><a href="https://doc.rust-lang.org/std/future/trait.Future.html" target="_blank">std::future - Rust Standard Library</a></li>
<li><a href="https://doc.rust-lang.org/nomicon/send-and-sync.html" target="_blank">The Rustonomicon: Send and Sync</a></li>
<li><a href="https://bitsavers.org/pdf/ibm/360/os/R21.7_Apr73/GC28-6628-9_OS_System_Ctl_Blks_R21.7_Apr73.pdf" target="_blank">IBM OS/360 System Control Blocks (PDF)</a></li>
</ol></section>
<section><h2>附录：完整基准代码<a href="#附录完整基准代码"><span>#</span></a></h2><p>下面是产出正文所有数字的完整程序。除标准库外无任何依赖，可以直接复制、编译、跑出你自己的数字。C / C++ 用 <code>-O2</code>，Rust 用 <code>--edition 2024 -O</code>，Python 直接用解释器。</p><section><h3>A.1 C：进程 / 线程的创建、切换与内存（<code>bench_c.c</code>）<a href="#a1-c进程--线程的创建切换与内存bench_cc"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#define</span><span> </span><span>_GNU_SOURCE</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;pthread.h&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;sched.h&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;stdlib.h&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;string.h&gt;</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>&lt;sys/wait.h&gt;</span></div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>&lt;time.h&gt;</span></div></div><div><div><div>9</div></div><div><span>#include</span><span> </span><span>&lt;unistd.h&gt;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>now</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>12</div></div><div><span>{</span></div></div><div><div><div>13</div></div><div><span>  </span><span>struct</span><span> timespec ts;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>clock_gettime</span><span>(CLOCK_MONOTONIC, </span><span>&amp;</span><span>ts);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>return</span><span> (</span><span>double</span><span>)</span><span>ts</span><span>.</span><span>tv_sec</span><span> </span><span>+</span><span> (</span><span>double</span><span>)</span><span>ts</span><span>.</span><span>tv_nsec</span><span> </span><span>*</span><span> </span><span><span>1</span><span>e</span></span><span>-</span><span>9</span><span>;</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>/* 把自己绑到 0 号核，减少迁移噪声 */</span></div></div><div><div><div>19</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>pin0</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>20</div></div><div><span>{</span></div></div><div><div><div>21</div></div><div><span>  </span><span>cpu_set_t</span><span> s;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>CPU_ZERO</span><span>(</span><span>&amp;</span><span>s);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>CPU_SET</span><span>(</span><span>0</span><span>, </span><span>&amp;</span><span>s);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>sched_setaffinity</span><span>(</span><span>0</span><span>, </span><span>sizeof</span><span> s, </span><span>&amp;</span><span>s);</span></div></div><div><div><div>25</div></div><div><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>*</span><span>nop</span><span>(</span><span>void</span><span> </span><span>*</span><span><span>a</span><span>)</span></span></div></div><div><div><div>28</div></div><div><span>{</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>(</span><span>void</span><span>)a;</span></div></div><div><div><div>30</div></div><div><span>  </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>bench_fork</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>34</div></div><div><span>{</span></div></div><div><div><div>35</div></div><div><span>  </span><span>double</span><span> t0 </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>36</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>37</div></div><div><span>    </span><span>pid_t</span><span> p </span><span>=</span><span> </span><span>fork</span><span>();</span></div></div><div><div><div>38</div></div><div><span>    </span><span>if</span><span> (p </span><span>==</span><span> </span><span>0</span><span>)</span></div></div><div><div><div>39</div></div><div><span>      </span><span>_exit</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>40</div></div><div><span>    </span><span>int</span><span> st;</span></div></div><div><div><div>41</div></div><div><span>    </span><span>waitpid</span><span>(p, </span><span>&amp;</span><span>st, </span><span>0</span><span>);</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>43</div></div><div><span>  </span><span>return</span><span> (</span><span>now</span><span>() </span><span>-</span><span> t0) </span><span>/</span><span> n;</span></div></div><div><div><div>44</div></div><div><span>}</span></div></div><div><div><div>45</div></div><div>
</div></div><div><div><div>46</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>bench_thread</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>47</div></div><div><span>{</span></div></div><div><div><div>48</div></div><div><span>  </span><span>double</span><span> t0 </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>49</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>50</div></div><div><span>    </span><span>pthread_t</span><span> t;</span></div></div><div><div><div>51</div></div><div><span>    </span><span>pthread_create</span><span>(</span><span>&amp;</span><span>t, </span><span>NULL</span><span>, nop, </span><span>NULL</span><span>);</span></div></div><div><div><div>52</div></div><div><span>    </span><span>pthread_join</span><span>(t, </span><span>NULL</span><span>);</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>54</div></div><div><span>  </span><span>return</span><span> (</span><span>now</span><span>() </span><span>-</span><span> t0) </span><span>/</span><span> n;</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>/* two threads, same core, handing off one byte each way */</span></div></div><div><div><div>58</div></div><div><span>static</span><span> </span><span>int</span><span> </span><span>pp_a</span><span>[</span><span>2</span><span>], </span><span>pp_b</span><span>[</span><span>2</span><span>];</span></div></div><div><div><div>59</div></div><div><span>static</span><span> </span><span>long</span><span> pp_n;</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>*</span><span>pp_peer</span><span>(</span><span>void</span><span> </span><span>*</span><span><span>a</span><span>)</span></span></div></div><div><div><div>62</div></div><div><span>{</span></div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>(</span><span>void</span><span>)a;</span></div></div><div><div><div>64</div></div><div><span>  </span><span>pin0</span><span>();</span></div></div><div><div><div>65</div></div><div><span>  </span><span>char</span><span> c;</span></div></div><div><div><div>66</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> pp_n; i</span><span>++</span><span>) {</span></div></div><div><div><div>67</div></div><div><span>    </span><span>if</span><span> (</span><span>read</span><span>(</span><span>pp_a</span><span>[</span><span>0</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>68</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>69</div></div><div><span>    </span><span>if</span><span> (</span><span>write</span><span>(</span><span>pp_b</span><span>[</span><span>1</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>70</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>72</div></div><div><span>  </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>73</div></div><div><span>}</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>bench_pp_thread</span><span>(</span><span>long</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>76</div></div><div><span>{</span></div></div><div><div><div>77</div></div><div><span>  </span><span>if</span><span> (</span><span>pipe</span><span>(pp_a) </span><span>||</span><span> </span><span>pipe</span><span>(pp_b)) {</span></div></div><div><div><div>78</div></div><div><span>    </span><span>perror</span><span>(</span><span>"pipe"</span><span>);</span></div></div><div><div><div>79</div></div><div><span>    </span><span>exit</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>80</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span><span>  </span></span><span>pp_n </span><span>=</span><span> n;</span></div></div><div><div><div>82</div></div><div><span>  </span><span>pthread_t</span><span> t;</span></div></div><div><div><div>83</div></div><div><span>  </span><span>pthread_create</span><span>(</span><span>&amp;</span><span>t, </span><span>NULL</span><span>, pp_peer, </span><span>NULL</span><span>);</span></div></div><div><div><div>84</div></div><div><span>  </span><span>pin0</span><span>();</span></div></div><div><div><div>85</div></div><div><span>  </span><span>char</span><span> c </span><span>=</span><span> </span><span>'x'</span><span>;</span></div></div><div><div><div>86</div></div><div><span>  </span><span>double</span><span> t0 </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>87</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>88</div></div><div><span>    </span><span>if</span><span> (</span><span>write</span><span>(</span><span>pp_a</span><span>[</span><span>1</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>89</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>90</div></div><div><span>    </span><span>if</span><span> (</span><span>read</span><span>(</span><span>pp_b</span><span>[</span><span>0</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>91</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>92</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>93</div></div><div><span>  </span><span>double</span><span> dt </span><span>=</span><span> </span><span>now</span><span>() </span><span>-</span><span> t0;</span></div></div><div><div><div>94</div></div><div><span>  </span><span>pthread_join</span><span>(t, </span><span>NULL</span><span>);</span></div></div><div><div><div>95</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_a</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>96</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_a</span><span>[</span><span>1</span><span>]);</span></div></div><div><div><div>97</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_b</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>98</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_b</span><span>[</span><span>1</span><span>]);</span></div></div><div><div><div>99</div></div><div><span>  </span><span>return</span><span> dt </span><span>/</span><span> n </span><span>/</span><span> </span><span>2.0</span><span>;</span><span> /* two switches per round trip */</span></div></div><div><div><div>100</div></div><div><span>}</span></div></div><div><div><div>101</div></div><div>
</div></div><div><div><div>102</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>bench_pp_proc</span><span>(</span><span>long</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>103</div></div><div><span>{</span></div></div><div><div><div>104</div></div><div><span>  </span><span>if</span><span> (</span><span>pipe</span><span>(pp_a) </span><span>||</span><span> </span><span>pipe</span><span>(pp_b)) {</span></div></div><div><div><div>105</div></div><div><span>    </span><span>perror</span><span>(</span><span>"pipe"</span><span>);</span></div></div><div><div><div>106</div></div><div><span>    </span><span>exit</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>107</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>108</div></div><div><span>  </span><span>pid_t</span><span> pid </span><span>=</span><span> </span><span>fork</span><span>();</span></div></div><div><div><div>109</div></div><div><span>  </span><span>if</span><span> (pid </span><span>==</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>110</div></div><div><span>    </span><span>pin0</span><span>();</span></div></div><div><div><div>111</div></div><div><span>    </span><span>char</span><span> c;</span></div></div><div><div><div>112</div></div><div><span>    </span><span>for</span><span> (</span><span>long</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>113</div></div><div><span>      </span><span>if</span><span> (</span><span>read</span><span>(</span><span>pp_a</span><span>[</span><span>0</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>114</div></div><div><span>        </span><span>break</span><span>;</span></div></div><div><div><div>115</div></div><div><span>      </span><span>if</span><span> (</span><span>write</span><span>(</span><span>pp_b</span><span>[</span><span>1</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>116</div></div><div><span>        </span><span>break</span><span>;</span></div></div><div><div><div>117</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>118</div></div><div><span>    </span><span>_exit</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>119</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>120</div></div><div><span>  </span><span>pin0</span><span>();</span></div></div><div><div><div>121</div></div><div><span>  </span><span>char</span><span> c </span><span>=</span><span> </span><span>'x'</span><span>;</span></div></div><div><div><div>122</div></div><div><span>  </span><span>double</span><span> t0 </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>123</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> n; i</span><span>++</span><span>) {</span></div></div><div><div><div>124</div></div><div><span>    </span><span>if</span><span> (</span><span>write</span><span>(</span><span>pp_a</span><span>[</span><span>1</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>125</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>126</div></div><div><span>    </span><span>if</span><span> (</span><span>read</span><span>(</span><span>pp_b</span><span>[</span><span>0</span><span>], </span><span>&amp;</span><span>c, </span><span>1</span><span>) </span><span>!=</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>127</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>128</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>129</div></div><div><span>  </span><span>double</span><span> dt </span><span>=</span><span> </span><span>now</span><span>() </span><span>-</span><span> t0;</span></div></div><div><div><div>130</div></div><div><span>  </span><span>int</span><span> st;</span></div></div><div><div><div>131</div></div><div><span>  </span><span>waitpid</span><span>(pid, </span><span>&amp;</span><span>st, </span><span>0</span><span>);</span></div></div><div><div><div>132</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_a</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>133</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_a</span><span>[</span><span>1</span><span>]);</span></div></div><div><div><div>134</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_b</span><span>[</span><span>0</span><span>]);</span></div></div><div><div><div>135</div></div><div><span>  </span><span>close</span><span>(</span><span>pp_b</span><span>[</span><span>1</span><span>]);</span></div></div><div><div><div>136</div></div><div><span>  </span><span>return</span><span> dt </span><span>/</span><span> n </span><span>/</span><span> </span><span>2.0</span><span>;</span></div></div><div><div><div>137</div></div><div><span>}</span></div></div><div><div><div>138</div></div><div>
</div></div><div><div><div>139</div></div><div><span>/* 从 /proc/self/status 里读一个 KB 单位的字段 */</span></div></div><div><div><div>140</div></div><div><span>static</span><span> </span><span>long</span><span> </span><span>vm</span><span>(</span><span>const</span><span> </span><span>char</span><span> </span><span>*</span><span><span>field</span><span>)</span></span></div></div><div><div><div>141</div></div><div><span>{</span></div></div><div><div><div>142</div></div><div><span><span>  </span></span><span>FILE </span><span>*</span><span>f </span><span>=</span><span> </span><span>fopen</span><span>(</span><span>"/proc/self/status"</span><span>, </span><span>"r"</span><span>);</span></div></div><div><div><div>143</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>f)</span></div></div><div><div><div>144</div></div><div><span>    </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>145</div></div><div><span>  </span><span>char</span><span> </span><span>line</span><span>[</span><span>256</span><span>];</span></div></div><div><div><div>146</div></div><div><span>  </span><span>long</span><span> v </span><span>=</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>147</div></div><div><span>  </span><span>size_t</span><span> fl </span><span>=</span><span> </span><span>strlen</span><span>(field);</span></div></div><div><div><div>148</div></div><div><span>  </span><span>while</span><span> (</span><span>fgets</span><span>(line, </span><span>sizeof</span><span> line, f)) {</span></div></div><div><div><div>149</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>strncmp</span><span>(line, field, fl)) {</span></div></div><div><div><div>150</div></div><div><span>      </span><span>sscanf</span><span>(line </span><span>+</span><span> fl, </span><span>"</span><span>%ld</span><span>"</span><span>, </span><span>&amp;</span><span>v);</span></div></div><div><div><div>151</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>152</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>153</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>154</div></div><div><span>  </span><span>fclose</span><span>(f);</span></div></div><div><div><div>155</div></div><div><span>  </span><span>return</span><span> v;</span></div></div><div><div><div>156</div></div><div><span>}</span></div></div><div><div><div>157</div></div><div>
</div></div><div><div><div>158</div></div><div><span>static</span><span> </span><span>pthread_mutex_t</span><span> g_mtx </span><span>=</span><span> PTHREAD_MUTEX_INITIALIZER;</span></div></div><div><div><div>159</div></div><div><span>static</span><span> </span><span>pthread_cond_t</span><span> g_cv </span><span>=</span><span> PTHREAD_COND_INITIALIZER;</span></div></div><div><div><div>160</div></div><div><span>static</span><span> </span><span>int</span><span> g_go </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>161</div></div><div>
</div></div><div><div><div>162</div></div><div><span>static</span><span> </span><span>void</span><span> </span><span>*</span><span>waiter</span><span>(</span><span>void</span><span> </span><span>*</span><span><span>a</span><span>)</span></span></div></div><div><div><div>163</div></div><div><span>{</span></div></div><div><div><div>164</div></div><div><span><span>  </span></span><span>(</span><span>void</span><span>)a;</span></div></div><div><div><div>165</div></div><div><span>  </span><span>pthread_mutex_lock</span><span>(</span><span>&amp;</span><span>g_mtx);</span></div></div><div><div><div>166</div></div><div><span>  </span><span>while</span><span> (</span><span>!</span><span>g_go)</span></div></div><div><div><div>167</div></div><div><span>    </span><span>pthread_cond_wait</span><span>(</span><span>&amp;</span><span>g_cv, </span><span>&amp;</span><span>g_mtx);</span></div></div><div><div><div>168</div></div><div><span>  </span><span>pthread_mutex_unlock</span><span>(</span><span>&amp;</span><span>g_mtx);</span></div></div><div><div><div>169</div></div><div><span>  </span><span>return</span><span> </span><span>NULL</span><span>;</span></div></div><div><div><div>170</div></div><div><span>}</span></div></div><div><div><div>171</div></div><div>
</div></div><div><div><div>172</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>best3</span><span>(</span><span>double</span><span><span> </span><span>(</span></span><span>*</span><span><span>fn</span><span>)(</span></span><span>int</span><span><span>),</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>)</span></span></div></div><div><div><div>173</div></div><div><span>{</span></div></div><div><div><div>174</div></div><div><span>  </span><span>double</span><span> b </span><span>=</span><span> </span><span><span>1</span><span>e</span><span>18</span></span><span>;</span></div></div><div><div><div>175</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> r </span><span>=</span><span> </span><span>0</span><span>; r </span><span>&lt;</span><span> </span><span>3</span><span>; r</span><span>++</span><span>) {</span></div></div><div><div><div>176</div></div><div><span>    </span><span>double</span><span> v </span><span>=</span><span> </span><span>fn</span><span>(n);</span></div></div><div><div><div>177</div></div><div><span>    </span><span>if</span><span> (v </span><span>&lt;</span><span> b)</span></div></div><div><div><div>178</div></div><div><span><span>      </span></span><span>b </span><span>=</span><span> v;</span></div></div><div><div><div>179</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>180</div></div><div><span>  </span><span>return</span><span> b;</span></div></div><div><div><div>181</div></div><div><span>}</span></div></div><div><div><div>182</div></div><div>
</div></div><div><div><div>183</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>)</span></div></div><div><div><div>184</div></div><div><span>{</span></div></div><div><div><div>185</div></div><div><span>  </span><span>bench_fork</span><span>(</span><span>50</span><span>);</span></div></div><div><div><div>186</div></div><div><span>  </span><span>bench_thread</span><span>(</span><span>200</span><span>);</span></div></div><div><div><div>187</div></div><div>
</div></div><div><div><div>188</div></div><div><span>  </span><span>printf</span><span>(</span><span>"nproc,</span><span>%ld</span><span>\n</span><span>"</span><span>, </span><span>sysconf</span><span>(_SC_NPROCESSORS_ONLN));</span></div></div><div><div><div>189</div></div><div><span>  </span><span>printf</span><span>(</span><span>"fork_us,</span><span>%.2f</span><span>\n</span><span>"</span><span>, </span><span>best3</span><span>(bench_fork, </span><span>2000</span><span>) </span><span>*</span><span> </span><span><span>1</span><span>e</span><span>6</span></span><span>);</span></div></div><div><div><div>190</div></div><div><span>  </span><span>printf</span><span>(</span><span>"pthread_create_join_us,</span><span>%.2f</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>191</div></div><div><span>         </span><span>best3</span><span>(bench_thread, </span><span>20000</span><span>) </span><span>*</span><span> </span><span><span>1</span><span>e</span><span>6</span></span><span>);</span></div></div><div><div><div>192</div></div><div><span>  </span><span>printf</span><span>(</span><span>"ctx_switch_thread_us,</span><span>%.3f</span><span>\n</span><span>"</span><span>, </span><span>bench_pp_thread</span><span>(</span><span>200000</span><span>) </span><span>*</span><span> </span><span><span>1</span><span>e</span><span>6</span></span><span>);</span></div></div><div><div><div>193</div></div><div><span>  </span><span>printf</span><span>(</span><span>"ctx_switch_process_us,</span><span>%.3f</span><span>\n</span><span>"</span><span>, </span><span>bench_pp_proc</span><span>(</span><span>50000</span><span>) </span><span>*</span><span> </span><span><span>1</span><span>e</span><span>6</span></span><span>);</span></div></div><div><div><div>194</div></div><div>
</div></div><div><div><div>195</div></div><div><span>  </span><span>pthread_attr_t</span><span> attr;</span></div></div><div><div><div>196</div></div><div><span>  </span><span>pthread_attr_init</span><span>(</span><span>&amp;</span><span>attr);</span></div></div><div><div><div>197</div></div><div><span>  </span><span>size_t</span><span> ss </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>198</div></div><div><span>  </span><span>pthread_attr_getstacksize</span><span>(</span><span>&amp;</span><span>attr, </span><span>&amp;</span><span>ss);</span></div></div><div><div><div>199</div></div><div><span>  </span><span>printf</span><span>(</span><span>"pthread_default_stack_kb,</span><span>%zu</span><span>\n</span><span>"</span><span>, ss </span><span>/</span><span> </span><span>1024</span><span>);</span></div></div><div><div><div>200</div></div><div>
</div></div><div><div><div>201</div></div><div><span>  </span><span>int</span><span> NT </span><span>=</span><span> </span><span>2000</span><span>;</span></div></div><div><div><div>202</div></div><div><span>  </span><span>long</span><span> v0 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmSize:"</span><span>), r0 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>203</div></div><div><span>  </span><span>pthread_t</span><span> </span><span>*</span><span>ts </span><span>=</span><span> </span><span>malloc</span><span>(</span><span>sizeof</span><span>(</span><span>pthread_t</span><span>) </span><span>*</span><span> NT);</span></div></div><div><div><div>204</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NT; i</span><span>++</span><span>)</span></div></div><div><div><div>205</div></div><div><span>    </span><span>pthread_create</span><span>(</span><span>&amp;</span><span>ts</span><span>[i], </span><span>NULL</span><span>, waiter, </span><span>NULL</span><span>);</span></div></div><div><div><div>206</div></div><div><span>  </span><span>long</span><span> v1 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmSize:"</span><span>), r1 </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>207</div></div><div><span>  </span><span>printf</span><span>(</span><span>"threads,</span><span>%d</span><span>\n</span><span>"</span><span>, NT);</span></div></div><div><div><div>208</div></div><div><span>  </span><span>printf</span><span>(</span><span>"vm_delta_per_thread_kb,</span><span>%.1f</span><span>\n</span><span>"</span><span>, (</span><span>double</span><span>)(v1 </span><span>-</span><span> v0) </span><span>/</span><span> NT);</span></div></div><div><div><div>209</div></div><div><span>  </span><span>printf</span><span>(</span><span>"rss_delta_per_thread_kb,</span><span>%.1f</span><span>\n</span><span>"</span><span>, (</span><span>double</span><span>)(r1 </span><span>-</span><span> r0) </span><span>/</span><span> NT);</span></div></div><div><div><div>210</div></div><div><span>  </span><span>pthread_mutex_lock</span><span>(</span><span>&amp;</span><span>g_mtx);</span></div></div><div><div><div>211</div></div><div><span><span>  </span></span><span>g_go </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>212</div></div><div><span>  </span><span>pthread_cond_broadcast</span><span>(</span><span>&amp;</span><span>g_cv);</span></div></div><div><div><div>213</div></div><div><span>  </span><span>pthread_mutex_unlock</span><span>(</span><span>&amp;</span><span>g_mtx);</span></div></div><div><div><div>214</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> NT; i</span><span>++</span><span>)</span></div></div><div><div><div>215</div></div><div><span>    </span><span>pthread_join</span><span>(</span><span>ts</span><span>[i], </span><span>NULL</span><span>);</span></div></div><div><div><div>216</div></div><div><span>  </span><span>free</span><span>(ts);</span></div></div><div><div><div>217</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>218</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译运行：<code>gcc -O2 -pthread bench_c.c -o bench_c &amp;&amp; ./bench_c</code></p></section><section><h3>A.2 C++：<code>std::thread</code> 与 C++20 协程（<code>bench_cpp.cpp</code>）<a href="#a2-cstdthread-与-c20-协程bench_cppcpp"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;chrono&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;coroutine&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;cstdint&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;cstdio&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;cstdlib&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;cstring&gt;</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>&lt;thread&gt;</span></div></div><div><div><div>8</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>now</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>  </span><span>using</span><span> </span><span>namespace</span><span><span> std::</span><span>chrono</span><span>;</span></span></div></div><div><div><div>12</div></div><div><span>  </span><span>return</span><span> </span><span>duration</span><span>&lt;</span><span>double</span><span><span>&gt;(</span><span>steady_clock</span><span>::</span></span><span>now</span><span>().</span><span>time_since_epoch</span><span>()).</span><span>count</span><span>();</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>static</span><span> </span><span>long</span><span> </span><span>vm</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>field</span><span>) {</span></span></div></div><div><div><div>16</div></div><div><span>  </span><span>FILE</span><span>*</span><span> </span><span>f</span><span> </span><span>=</span><span> </span><span>fopen</span><span>(</span><span>"/proc/self/status"</span><span>, </span><span>"r"</span><span>);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>f</span><span>) </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>char</span><span> </span><span>line</span><span>[</span><span>256</span><span>];</span></div></div><div><div><div>19</div></div><div><span>  </span><span>long</span><span> </span><span>v</span><span> </span><span>=</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>size_t</span><span> </span><span>fl</span><span> </span><span>=</span><span> </span><span>strlen</span><span>(</span><span>field</span><span>);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>while</span><span> (</span><span>fgets</span><span>(</span><span>line</span><span>, </span><span>sizeof</span><span> </span><span>line</span><span>, </span><span>f</span><span>)) {</span></div></div><div><div><div>22</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>strncmp</span><span>(</span><span>line</span><span>, </span><span>field</span><span>, </span><span>fl</span><span>)) {</span></div></div><div><div><div>23</div></div><div><span>      </span><span>sscanf</span><span>(</span><span>line</span><span> </span><span>+</span><span> </span><span>fl</span><span>, </span><span>"</span><span>%ld</span><span>"</span><span>, </span><span>&amp;</span><span>v</span><span>);</span></div></div><div><div><div>24</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span>  </span><span>fclose</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>28</div></div><div><span>  </span><span>return</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span>static</span><span> </span><span>double</span><span> </span><span>bench_std_thread</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>32</div></div><div><span>  </span><span>double</span><span> </span><span>t0</span><span> </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>33</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>i</span><span>++</span><span>) {</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>std::</span><span>thread</span><span> </span><span>t</span><span>([]() {});</span></div></div><div><div><div>35</div></div><div><span>    </span><span>t</span><span>.</span><span>join</span><span>();</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span>  </span><span>return</span><span> (</span><span>now</span><span>() </span><span>-</span><span> </span><span>t0</span><span>) </span><span>/</span><span> </span><span>n</span><span>;</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>/* 最小的无栈生成器；每次 next() 恢复一次协程帧 */</span></div></div><div><div><div>41</div></div><div><span>struct</span><span> </span><span>Gen</span><span> {</span></div></div><div><div><div>42</div></div><div><span>  </span><span>struct</span><span> </span><span>promise_type</span><span> {</span></div></div><div><div><div>43</div></div><div><span>    </span><span>int</span><span> </span><span>value</span><span>;</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>yield_value</span><span>(</span><span>int</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>45</div></div><div><span>      </span><span>value</span><span> </span><span>=</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>46</div></div><div><span>      </span><span>return</span><span> {};</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>    </span><span>Gen</span><span> </span><span>get_return_object</span><span>() {</span></div></div><div><div><div>49</div></div><div><span>      </span><span>return</span><span> </span><span>Gen</span><span><span>{</span><span>std</span><span>::</span><span>coroutine_handle</span><span>&lt;</span></span><span>promise_type</span><span>&gt;::</span><span>from_promise</span><span>(</span><span>*</span><span>this</span><span>)};</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>51</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>initial_suspend</span><span>() </span><span>noexcept</span><span> { </span><span>return</span><span> {}; }</span></div></div><div><div><div>52</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>final_suspend</span><span>() </span><span>noexcept</span><span> { </span><span>return</span><span> {}; }</span></div></div><div><div><div>53</div></div><div><span>    </span><span>void</span><span> </span><span>return_void</span><span>() {}</span></div></div><div><div><div>54</div></div><div><span>    </span><span>void</span><span> </span><span>unhandled_exception</span><span>() {}</span></div></div><div><div><div>55</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>56</div></div><div><span><span>  </span></span><span>std::</span><span>coroutine_handle</span><span>&lt;</span><span>promise_type</span><span>&gt; </span><span>h</span><span>;</span></div></div><div><div><div>57</div></div><div><span>  </span><span>bool</span><span> </span><span>next</span><span>() {</span></div></div><div><div><div>58</div></div><div><span>    </span><span>h</span><span>.</span><span>resume</span><span>();</span></div></div><div><div><div>59</div></div><div><span>    </span><span>return</span><span> </span><span>!</span><span>h</span><span>.</span><span>done</span><span>();</span></div></div><div><div><div>60</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>61</div></div><div><span>};</span></div></div><div><div><div>62</div></div><div>
</div></div><div><div><div>63</div></div><div><span>static</span><span> </span><span>Gen</span><span> </span><span>gen</span><span>(</span><span>long</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>64</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>i</span><span>++</span><span>) </span><span>co_yield</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>65</div></div><div><span>}</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>68</div></div><div><span>  </span><span>bench_std_thread</span><span>(</span><span>200</span><span>);</span></div></div><div><div><div>69</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_std_thread_create_join_us,</span><span>%.2f</span><span>\n</span><span>"</span><span>, </span><span>bench_std_thread</span><span>(</span><span>20000</span><span>) </span><span>*</span><span> </span><span><span>1</span><span>e</span><span>6</span></span><span>);</span></div></div><div><div><div>70</div></div><div>
</div></div><div><div><div>71</div></div><div><span>  </span><span>const</span><span> </span><span>long</span><span> </span><span>N</span><span> </span><span>=</span><span> </span><span><span>100000000</span><span>L</span></span><span>;</span></div></div><div><div><div>72</div></div><div><span>  </span><span>double</span><span> </span><span>t0</span><span> </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>73</div></div><div><span>  </span><span>long</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>74</div></div><div><span>  </span><span>Gen</span><span> </span><span>g</span><span> </span><span>=</span><span> </span><span>gen</span><span>(</span><span>N</span><span>);</span></div></div><div><div><div>75</div></div><div><span>  </span><span>while</span><span> (</span><span>g</span><span>.</span><span>next</span><span>()) </span><span>c</span><span>++</span><span>;</span></div></div><div><div><div>76</div></div><div><span>  </span><span>double</span><span> </span><span>t1</span><span> </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>77</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_coro_resumes,</span><span>%ld</span><span>\n</span><span>"</span><span>, </span><span>c</span><span>);</span></div></div><div><div><div>78</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_coro_resume_ns,</span><span>%.2f</span><span>\n</span><span>"</span><span>, (</span><span>t1</span><span> </span><span>-</span><span> </span><span>t0</span><span>) </span><span>/</span><span> </span><span>N</span><span> </span><span>*</span><span> </span><span><span>1</span><span>e</span><span>9</span></span><span>);</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>  </span><span>volatile</span><span> </span><span>long</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>81</div></div><div><span>  </span><span>double</span><span> </span><span>t2</span><span> </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>82</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) </span><span>s</span><span> </span><span>+=</span><span> </span><span>i</span><span>;</span></div></div><div><div><div>83</div></div><div><span>  </span><span>double</span><span> </span><span>t3</span><span> </span><span>=</span><span> </span><span>now</span><span>();</span></div></div><div><div><div>84</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_plain_loop_ns,</span><span>%.2f</span><span>\n</span><span>"</span><span>, (</span><span>t3</span><span> </span><span>-</span><span> </span><span>t2</span><span>) </span><span>/</span><span> </span><span>N</span><span> </span><span>*</span><span> </span><span><span>1</span><span>e</span><span>9</span></span><span>);</span></div></div><div><div><div>85</div></div><div>
</div></div><div><div><div>86</div></div><div><span><span>  </span></span><span>/* 大量挂起的协程帧：在堆上，不占线程栈 */</span></div></div><div><div><div>87</div></div><div><span>  </span><span>long</span><span> </span><span>r0</span><span> </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>88</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>coroutine_handle</span><span>&lt;Gen::</span><span>promise_type</span><span>&gt;&gt; </span><span>v</span><span>;</span></div></div><div><div><div>89</div></div><div><span>  </span><span>v</span><span>.</span><span>reserve</span><span>(</span><span>100000</span><span>);</span></div></div><div><div><div>90</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>100000</span><span>; </span><span>i</span><span>++</span><span>) </span><span>v</span><span>.</span><span>push_back</span><span>(</span><span>gen</span><span>(</span><span>10</span><span>).</span><span>h</span><span>);</span></div></div><div><div><div>91</div></div><div><span>  </span><span>long</span><span> </span><span>r1</span><span> </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>92</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_suspended_coroutines,100000</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>93</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_frame_bytes_est,</span><span>%.1f</span><span>\n</span><span>"</span><span>, (</span><span>r1</span><span> </span><span>-</span><span> </span><span>r0</span><span>) </span><span>*</span><span> </span><span>1024.0</span><span> </span><span>/</span><span> </span><span>100000</span><span>);</span></div></div><div><div><div>94</div></div><div><span>  </span><span>for</span><span> (</span><span>auto</span><span> </span><span>h</span><span> : </span><span>v</span><span>) </span><span>h</span><span>.</span><span>destroy</span><span>();</span></div></div><div><div><div>95</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>96</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译运行：<code>g++ -O2 -std=c++20 bench_cpp.cpp -o bench_cpp &amp;&amp; ./bench_cpp</code></p></section><section><h3>A.3 Python：线程 / GIL / asyncio / 生成器（<code>bench_py.py</code>）<a href="#a3-python线程--gil--asyncio--生成器bench_pypy"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> asyncio</span></div></div><div><div><div>2</div></div><div><span>import</span><span> multiprocessing</span></div></div><div><div><div>3</div></div><div><span>import</span><span> os</span></div></div><div><div><div>4</div></div><div><span>import</span><span> sys</span></div></div><div><div><div>5</div></div><div><span>import</span><span> threading</span></div></div><div><div><div>6</div></div><div><span>import</span><span> time</span></div></div><div><div><div>7</div></div><div><span>from</span><span> collections.abc </span><span>import</span><span> Callable, Iterator</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>N: </span><span>int</span><span><span> </span><span>=</span><span> </span></span><span>20_000_000</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>def</span><span> </span><span>now</span><span>() -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>13</div></div><div><span>    </span><span>return</span><span><span> time.</span><span>perf_counter</span><span>()</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>def</span><span> </span><span>best3</span><span>(</span><span>fn</span><span>:</span><span> Callable[</span><span>...</span><span>, </span><span>float</span><span>]</span><span>,</span><span> </span><span>*</span><span>args</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>best </span><span>=</span><span> </span><span>float</span><span>(</span><span>"inf"</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>3</span><span>):</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>best </span><span>=</span><span> </span><span>min</span><span><span>(best, </span><span>fn</span><span>(*args))</span></span></div></div><div><div><div>20</div></div><div><span>    </span><span>return</span><span> best</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span>def</span><span> </span><span>thread_create_join</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>24</div></div><div><span>    </span><span>def</span><span> </span><span>nop</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>25</div></div><div><span>        </span><span>pass</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>t0 </span><span>=</span><span> </span><span>now</span><span>()</span></div></div><div><div><div>28</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>29</div></div><div><span><span>        </span></span><span>t </span><span>=</span><span> threading.</span><span>Thread</span><span>(</span><span>target</span><span><span>=</span><span>nop)</span></span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>t.</span><span>start</span><span>()</span></div></div><div><div><div>31</div></div><div><span><span>        </span></span><span>t.</span><span>join</span><span>()</span></div></div><div><div><div>32</div></div><div><span>    </span><span>return</span><span><span> (</span><span>now</span><span>() </span><span>-</span><span> t0) </span><span>/</span><span> n</span></span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span>def</span><span> </span><span>burn</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>s </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>37</div></div><div><span>    </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>38</div></div><div><span><span>        </span></span><span>s </span><span>+=</span><span> i </span><span>*</span><span> i</span></div></div><div><div><div>39</div></div><div><span>    </span><span>return</span><span> s</span></div></div><div><div><div>40</div></div><div>
</div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>def</span><span> </span><span>gil_one</span><span>() -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>t0 </span><span>=</span><span> </span><span>now</span><span>()</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>burn</span><span>(N)</span></div></div><div><div><div>45</div></div><div><span>    </span><span>return</span><span><span> </span><span>now</span><span>() </span><span>-</span><span> t0</span></span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div><span>def</span><span> </span><span>gil_two_threads</span><span>() -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>t0 </span><span>=</span><span> </span><span>now</span><span>()</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>ts </span><span>=</span><span> [threading.</span><span>Thread</span><span>(</span><span>target</span><span><span>=</span><span>burn, </span></span><span>args</span><span><span>=</span><span>(N,)) </span></span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>)]</span></div></div><div><div><div>51</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> ts:</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>t.</span><span>start</span><span>()</span></div></div><div><div><div>53</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> ts:</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>t.</span><span>join</span><span>()</span></div></div><div><div><div>55</div></div><div><span>    </span><span>return</span><span><span> </span><span>now</span><span>() </span><span>-</span><span> t0</span></span></div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>def</span><span> </span><span>gil_two_procs</span><span>() -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>59</div></div><div><span><span>    </span></span><span>t0 </span><span>=</span><span> </span><span>now</span><span>()</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>ps </span><span>=</span><span> [multiprocessing.</span><span>Process</span><span>(</span><span>target</span><span><span>=</span><span>burn, </span></span><span>args</span><span><span>=</span><span>(N,)) </span></span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(</span><span>2</span><span>)]</span></div></div><div><div><div>61</div></div><div><span>    </span><span>for</span><span> p </span><span>in</span><span> ps:</span></div></div><div><div><div>62</div></div><div><span><span>        </span></span><span>p.</span><span>start</span><span>()</span></div></div><div><div><div>63</div></div><div><span>    </span><span>for</span><span> p </span><span>in</span><span> ps:</span></div></div><div><div><div>64</div></div><div><span><span>        </span></span><span>p.</span><span>join</span><span>()</span></div></div><div><div><div>65</div></div><div><span>    </span><span>return</span><span><span> </span><span>now</span><span>() </span><span>-</span><span> t0</span></span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div>
</div></div><div><div><div>68</div></div><div><span>async</span><span> </span><span>def</span><span> </span><span>_step</span><span>() -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>69</div></div><div><span>    </span><span>return</span><span> </span><span>1</span></div></div><div><div><div>70</div></div><div>
</div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>async</span><span> </span><span>def</span><span> </span><span>_tight</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>73</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(n):</span></div></div><div><div><div>74</div></div><div><span>        </span><span>await</span><span><span> asyncio.</span><span>sleep</span><span>(</span></span><span>0</span><span>)</span></div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span>async</span><span> </span><span>def</span><span> </span><span>_gather</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>78</div></div><div><span>    </span><span>await</span><span><span> asyncio.</span><span>gather</span><span>(*(</span><span>_step</span><span>() </span></span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(n)))</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div>
</div></div><div><div><div>81</div></div><div><span>def</span><span> </span><span>asyncio_tasks</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>t0 </span><span>=</span><span> </span><span>now</span><span>()</span></div></div><div><div><div>83</div></div><div><span><span>    </span></span><span>asyncio.</span><span>run</span><span>(</span><span>_gather</span><span>(n))</span></div></div><div><div><div>84</div></div><div><span>    </span><span>return</span><span><span> </span><span>now</span><span>() </span><span>-</span><span> t0</span></span></div></div><div><div><div>85</div></div><div>
</div></div><div><div><div>86</div></div><div>
</div></div><div><div><div>87</div></div><div><span>def</span><span> </span><span>gen_resume</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>float</span><span>:</span></div></div><div><div><div>88</div></div><div><span>    </span><span>def</span><span> </span><span>gen</span><span>(</span><span>k</span><span>:</span><span> </span><span>int</span><span>) -&gt; Iterator[</span><span>int</span><span>]:</span></div></div><div><div><div>89</div></div><div><span>        </span><span>for</span><span> i </span><span>in</span><span> </span><span>range</span><span>(k):</span></div></div><div><div><div>90</div></div><div><span>            </span><span>yield</span><span> i</span></div></div><div><div><div>91</div></div><div>
</div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>g </span><span>=</span><span> </span><span>gen</span><span>(n)</span></div></div><div><div><div>93</div></div><div><span><span>    </span></span><span>t0 </span><span>=</span><span> </span><span>now</span><span>()</span></div></div><div><div><div>94</div></div><div><span><span>    </span></span><span>c </span><span>=</span><span> </span><span>0</span></div></div><div><div><div>95</div></div><div><span>    </span><span>for</span><span> _ </span><span>in</span><span> g:</span></div></div><div><div><div>96</div></div><div><span><span>        </span></span><span>c </span><span>+=</span><span> </span><span>1</span></div></div><div><div><div>97</div></div><div><span>    </span><span>return</span><span><span> (</span><span>now</span><span>() </span><span>-</span><span> t0) </span><span>/</span><span> c</span></span></div></div><div><div><div>98</div></div><div>
</div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div><span>def</span><span> </span><span>main</span><span>() -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>101</div></div><div><span><span>    </span></span><span>thread_create_join</span><span>(</span><span>200</span><span>)</span></div></div><div><div><div>102</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,cpu_count,</span><span>%d</span><span>"</span><span><span> </span><span>%</span><span> (os.</span><span>cpu_count</span><span>() </span></span><span>or</span><span> </span><span>1</span><span>))</span></div></div><div><div><div>103</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,switch_interval_s,</span><span>%.4f</span><span>"</span><span><span> </span><span>%</span><span> sys.</span><span>getswitchinterval</span><span>())</span></span></div></div><div><div><div>104</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>105</div></div><div><span>        </span><span>"python,thread_create_join_us,</span><span>%.2f</span><span>"</span></div></div><div><div><div>106</div></div><div><span><span>        </span></span><span>%</span><span> (</span><span>best3</span><span>(thread_create_join, </span><span>20000</span><span><span>) </span><span>*</span><span> </span></span><span>1e6</span><span>)</span></div></div><div><div><div>107</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>108</div></div><div>
</div></div><div><div><div>109</div></div><div><span><span>    </span></span><span>one </span><span>=</span><span> </span><span>best3</span><span>(gil_one)</span></div></div><div><div><div>110</div></div><div><span><span>    </span></span><span>two </span><span>=</span><span> </span><span>best3</span><span>(gil_two_threads)</span></div></div><div><div><div>111</div></div><div><span><span>    </span></span><span>mp </span><span>=</span><span> </span><span>best3</span><span>(gil_two_procs)</span></div></div><div><div><div>112</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,burn_1thread_s,</span><span>%.3f</span><span>"</span><span><span> </span><span>%</span><span> one)</span></span></div></div><div><div><div>113</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,burn_2threads_s,</span><span>%.3f</span><span>"</span><span><span> </span><span>%</span><span> two)</span></span></div></div><div><div><div>114</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,burn_2procs_s,</span><span>%.3f</span><span>"</span><span><span> </span><span>%</span><span> mp)</span></span></div></div><div><div><div>115</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,gil_speedup_2threads,</span><span>%.2f</span><span>x"</span><span><span> </span><span>%</span><span> (one </span><span>*</span><span> </span></span><span>2</span><span><span> </span><span>/</span><span> two))</span></span></div></div><div><div><div>116</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,mp_speedup_2procs,</span><span>%.2f</span><span>x"</span><span><span> </span><span>%</span><span> (one </span><span>*</span><span> </span></span><span>2</span><span><span> </span><span>/</span><span> mp))</span></span></div></div><div><div><div>117</div></div><div>
</div></div><div><div><div>118</div></div><div><span><span>    </span></span><span>asyncio.</span><span>run</span><span>(</span><span>_tight</span><span>(</span><span>1000</span><span>))</span></div></div><div><div><div>119</div></div><div><span><span>    </span></span><span>ns </span><span>=</span><span> </span><span>2_000_000</span></div></div><div><div><div>120</div></div><div><span><span>    </span></span><span>t0 </span><span>=</span><span> </span><span>now</span><span>()</span></div></div><div><div><div>121</div></div><div><span><span>    </span></span><span>asyncio.</span><span>run</span><span>(</span><span>_tight</span><span>(ns))</span></div></div><div><div><div>122</div></div><div><span><span>    </span></span><span>dt </span><span>=</span><span> </span><span>now</span><span>() </span><span>-</span><span> t0</span></div></div><div><div><div>123</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,asyncio_await_switch_ns,</span><span>%.1f</span><span>"</span><span><span> </span><span>%</span><span> (dt </span><span>/</span><span> ns </span><span>*</span><span> </span></span><span>1e9</span><span>))</span></div></div><div><div><div>124</div></div><div>
</div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>nt </span><span>=</span><span> </span><span>200_000</span></div></div><div><div><div>126</div></div><div><span><span>    </span></span><span>dt </span><span>=</span><span> </span><span>asyncio_tasks</span><span>(nt)</span></div></div><div><div><div>127</div></div><div><span>    </span><span>print</span><span>(</span><span>"python,asyncio_task_create_run_us,</span><span>%.3f</span><span>"</span><span><span> </span><span>%</span><span> (dt </span><span>/</span><span> nt </span><span>*</span><span> </span></span><span>1e6</span><span>))</span></div></div><div><div><div>128</div></div><div>
</div></div><div><div><div>129</div></div><div><span>    </span><span>print</span><span>(</span></div></div><div><div><div>130</div></div><div><span>        </span><span>"python,generator_resume_ns,</span><span>%.1f</span><span>"</span></div></div><div><div><div>131</div></div><div><span><span>        </span></span><span>%</span><span> (</span><span>best3</span><span>(gen_resume, </span><span>50_000_000</span><span><span>) </span><span>*</span><span> </span></span><span>1e9</span><span>)</span></div></div><div><div><div>132</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>133</div></div><div>
</div></div><div><div><div>134</div></div><div>
</div></div><div><div><div>135</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>"__main__"</span><span>:</span></div></div><div><div><div>136</div></div><div><span><span>    </span></span><span>main</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>运行：<code>python3 bench_py.py</code></p></section><section><h3>A.4 Rust：<code>std::thread</code> 与零依赖执行器（<code>bench_rs.rs</code>）<a href="#a4-ruststdthread-与零依赖执行器bench_rsrs"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>pin</span><span>::</span></span><span>Pin</span><span>;</span></div></div><div><div><div>2</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>sync</span><span>::{</span></span><span>Arc</span><span>, </span><span>Barrier</span><span>};</span></div></div><div><div><div>3</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>task</span><span>::{</span></span><span>Context</span><span>, </span><span>Poll</span><span>, </span><span>RawWaker</span><span>, </span><span>RawWakerVTable</span><span>, </span><span>Waker</span><span>};</span></div></div><div><div><div>4</div></div><div><span>use</span><span><span> </span><span>std</span><span>::</span><span>time</span><span>::</span></span><span>Instant</span><span>;</span></div></div><div><div><div>5</div></div><div><span>use</span><span><span> </span><span>std</span><span>::{mem, thread};</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>const</span><span> </span><span>RAW</span><span>: </span><span>RawWaker</span><span><span> </span><span>=</span><span> </span></span><span>RawWaker</span><span>::</span><span>new</span><span><span>(</span><span>std</span><span>::</span><span>ptr</span><span>::</span></span><span>null</span><span>(), &amp;</span><span>VTABLE</span><span>);</span></div></div><div><div><div>8</div></div><div><span>const</span><span> </span><span>VTABLE</span><span>: </span><span>RawWakerVTable</span><span><span> </span><span>=</span><span> </span></span><span>RawWakerVTable</span><span>::</span><span>new</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> </span></span><span>RAW</span><span><span>, </span><span>|</span></span><span>_</span><span><span>|</span><span> {}, </span><span>|</span></span><span>_</span><span><span>|</span><span> {}, </span><span>|</span></span><span>_</span><span><span>|</span><span> {});</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>fn</span><span> </span><span>noop_waker</span><span>() -&gt; </span><span>Waker</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>// SAFETY: vtable 里的函数不会解引用那个空数据指针</span></div></div><div><div><div>12</div></div><div><span>    </span><span>unsafe</span><span> { </span><span>Waker</span><span>::</span><span>from_raw</span><span>(</span><span>RAW</span><span>) }</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>/// 最小执行器：不接任何运行时，轮询到完成</span></div></div><div><div><div>16</div></div><div><span>fn</span><span> </span><span>block_on</span><span>&lt;</span><span>F</span><span>: </span><span>Future</span><span>&gt;(</span><span>f</span><span>: </span><span>F</span><span><span>) -&gt; </span><span>F</span><span>::</span></span><span>Output</span><span> {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>let</span><span> </span><span>w</span><span><span> </span><span>=</span><span> </span></span><span>noop_waker</span><span>();</span></div></div><div><div><div>18</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>cx</span><span><span> </span><span>=</span><span> </span></span><span>Context</span><span>::</span><span>from_waker</span><span>(&amp;</span><span>w</span><span>);</span></div></div><div><div><div>19</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>Box</span><span>::</span><span>pin</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>20</div></div><div><span>    </span><span>loop</span><span> {</span></div></div><div><div><div>21</div></div><div><span>        </span><span>match</span><span> </span><span>f</span><span>.</span><span>as_mut</span><span>().</span><span>poll</span><span>(&amp;</span><span>mut</span><span> </span><span>cx</span><span>) {</span></div></div><div><div><div>22</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Ready</span><span>(</span><span>v</span><span>) =&gt; </span><span>return</span><span> </span><span>v</span><span>,</span></div></div><div><div><div>23</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Pending</span><span><span> =&gt; </span><span>std</span><span>::</span><span>hint</span><span>::</span></span><span>spin_loop</span><span>(),</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>/// 返回 target 次 Pending，然后 Ready</span></div></div><div><div><div>29</div></div><div><span>struct</span><span> </span><span>Spin</span><span> {</span></div></div><div><div><div>30</div></div><div><span>    </span><span>n</span><span>: </span><span>u64</span><span>,</span></div></div><div><div><div>31</div></div><div><span>    </span><span>target</span><span>: </span><span>u64</span><span>,</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>impl</span><span> </span><span>Future</span><span> </span><span>for</span><span> </span><span>Spin</span><span> {</span></div></div><div><div><div>35</div></div><div><span>    </span><span>type</span><span> </span><span>Output</span><span><span> </span><span>=</span><span> </span></span><span>u64</span><span>;</span></div></div><div><div><div>36</div></div><div><span>    </span><span>fn</span><span> </span><span>poll</span><span>(</span><span>mut</span><span> </span><span>self</span><span>: </span><span>Pin</span><span>&lt;&amp;</span><span>mut</span><span> </span><span>Self</span><span>&gt;, </span><span>cx</span><span>: &amp;</span><span>mut</span><span> </span><span>Context</span><span>&lt;'</span><span>_</span><span>&gt;) -&gt; </span><span>Poll</span><span>&lt;</span><span>u64</span><span>&gt; {</span></div></div><div><div><div>37</div></div><div><span>        </span><span>self</span><span><span>.n </span><span>+=</span><span> </span></span><span>1</span><span>;</span></div></div><div><div><div>38</div></div><div><span>        </span><span>if</span><span> </span><span>self</span><span><span>.n </span><span>&gt;=</span><span> </span></span><span>self</span><span>.target {</span></div></div><div><div><div>39</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Ready</span><span>(</span><span>self</span><span>.n)</span></div></div><div><div><div>40</div></div><div><span><span>        </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>41</div></div><div><span>            </span><span>cx</span><span>.</span><span>waker</span><span>().</span><span>wake_by_ref</span><span>();</span></div></div><div><div><div>42</div></div><div><span>            </span><span>Poll</span><span>::</span><span>Pending</span></div></div><div><div><div>43</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>leaf</span><span>(</span><span>x</span><span>: </span><span>u64</span><span>) -&gt; </span><span>u64</span><span> {</span></div></div><div><div><div>48</div></div><div><span>    </span><span>x</span><span>.</span><span>wrapping_add</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>49</div></div><div><span>}</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>fn</span><span> </span><span>bench_thread</span><span>(</span><span>n</span><span>: </span><span>usize</span><span>) -&gt; </span><span>f64</span><span> {</span></div></div><div><div><div>52</div></div><div><span>    </span><span>let</span><span> </span><span>t0</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>53</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>n</span><span> {</span></div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>thread</span><span>::</span><span>spawn</span><span><span>(</span><span>||</span><span> {}).</span></span><span>join</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>55</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>56</div></div><div><span>    </span><span>t0</span><span>.</span><span>elapsed</span><span>().</span><span>as_secs_f64</span><span>() / </span><span>n</span><span> </span><span>as</span><span> </span><span>f64</span></div></div><div><div><div>57</div></div><div><span>}</span></div></div><div><div><div>58</div></div><div>
</div></div><div><div><div>59</div></div><div><span>fn</span><span> </span><span>vm</span><span>(</span><span>field</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>60</div></div><div><span>    </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>fs</span><span>::</span></span><span>read_to_string</span><span>(</span><span>"/proc/self/status"</span><span>).</span><span>unwrap_or_default</span><span>();</span></div></div><div><div><div>61</div></div><div><span>    </span><span>for</span><span> </span><span>l</span><span> </span><span>in</span><span> </span><span>s</span><span>.</span><span>lines</span><span>() {</span></div></div><div><div><div>62</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>r</span><span><span>) </span><span>=</span><span> </span></span><span>l</span><span>.</span><span>strip_prefix</span><span>(</span><span>field</span><span>) {</span></div></div><div><div><div>63</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>tok</span><span><span>) </span><span>=</span><span> </span></span><span>r</span><span>.</span><span>split_whitespace</span><span>().</span><span>next</span><span>() {</span></div></div><div><div><div>64</div></div><div><span>                </span><span>if</span><span> </span><span>let</span><span> </span><span>Ok</span><span>(</span><span>v</span><span><span>) </span><span>=</span><span> </span></span><span>tok</span><span>.</span><span>parse</span><span>() {</span></div></div><div><div><div>65</div></div><div><span>                    </span><span>return</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>66</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>67</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>68</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>69</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>70</div></div><div><span><span>    </span></span><span>-</span><span>1</span></div></div><div><div><div>71</div></div><div><span>}</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>74</div></div><div><span>    </span><span>bench_thread</span><span>(</span><span>200</span><span>);</span></div></div><div><div><div>75</div></div><div><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>best</span><span><span> </span><span>=</span><span> </span></span><span>f64</span><span>::</span><span>MAX</span><span>;</span></div></div><div><div><div>76</div></div><div><span>    </span><span>for</span><span> </span><span>_</span><span> </span><span>in</span><span> </span><span>0</span><span>..</span><span>3</span><span> {</span></div></div><div><div><div>77</div></div><div><span>        </span><span>let</span><span> </span><span>v</span><span><span> </span><span>=</span><span> </span></span><span>bench_thread</span><span>(</span><span>20000</span><span>);</span></div></div><div><div><div>78</div></div><div><span>        </span><span>if</span><span> </span><span>v</span><span><span> </span><span>&lt;</span><span> </span></span><span>best</span><span> {</span></div></div><div><div><div>79</div></div><div><span>            </span><span>best</span><span><span> </span><span>=</span><span> </span></span><span>v</span><span>;</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>82</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_thread_create_join_us,{:.2}"</span><span>, </span><span>best</span><span> * </span><span>1</span><span>e</span><span>6</span><span>);</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span>    </span><span>// 手动 block_on，没有运行时</span></div></div><div><div><div>85</div></div><div><span>    </span><span>let</span><span> </span><span>val</span><span><span> </span><span>=</span><span> </span></span><span>block_on</span><span>(</span><span>leaf</span><span>(</span><span>41</span><span>));</span></div></div><div><div><div>86</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_block_on_value,{}"</span><span>, </span><span>val</span><span>);</span></div></div><div><div><div>87</div></div><div>
</div></div><div><div><div>88</div></div><div><span>    </span><span>const</span><span> </span><span>N</span><span>: </span><span>u64</span><span><span> </span><span>=</span><span> </span></span><span>100_000_000</span><span>;</span></div></div><div><div><div>89</div></div><div><span>    </span><span>let</span><span> </span><span>t0</span><span><span> </span><span>=</span><span> </span></span><span>Instant</span><span>::</span><span>now</span><span>();</span></div></div><div><div><div>90</div></div><div><span>    </span><span>let</span><span> </span><span>r</span><span><span> </span><span>=</span><span> </span></span><span>block_on</span><span>(</span><span>Spin</span><span> { </span><span>n</span><span>: </span><span>0</span><span>, </span><span>target</span><span>: </span><span>N</span><span> });</span></div></div><div><div><div>91</div></div><div><span>    </span><span>let</span><span> </span><span>dt</span><span><span> </span><span>=</span><span> </span></span><span>t0</span><span>.</span><span>elapsed</span><span>().</span><span>as_secs_f64</span><span>();</span></div></div><div><div><div>92</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_polls,{}"</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>93</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_poll_ns,{:.2}"</span><span>, </span><span>dt</span><span> / </span><span>N</span><span> </span><span>as</span><span> </span><span>f64</span><span> * </span><span>1</span><span>e</span><span>9</span><span>);</span></div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span>    </span><span>let</span><span> </span><span>f</span><span><span> </span><span>=</span><span> </span></span><span>leaf</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>96</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_future_size_bytes,{}"</span><span><span>, </span><span>mem</span><span>::</span></span><span>size_of_val</span><span>(&amp;</span><span>f</span><span>));</span></div></div><div><div><div>97</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_spin_size_bytes,{}"</span><span><span>, </span><span>mem</span><span>::</span></span><span>size_of</span><span>::&lt;</span><span>Spin</span><span>&gt;());</span></div></div><div><div><div>98</div></div><div>
</div></div><div><div><div>99</div></div><div><span>    </span><span>// 10 万个从未 poll 过的 Future，只算它们的内部状态</span></div></div><div><div><div>100</div></div><div><span>    </span><span>let</span><span> </span><span>vs</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>100_000</span><span>u64</span><span>).</span><span>map</span><span>(</span><span>leaf</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>101</div></div><div><span>    </span><span>let</span><span> </span><span>total</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>vs</span><span>.</span><span>iter</span><span>().</span><span>map</span><span><span>(</span><span>|</span></span><span>f</span><span><span>|</span><span> </span><span>mem</span><span>::</span></span><span>size_of_val</span><span>(</span><span>f</span><span>)).</span><span>sum</span><span>();</span></div></div><div><div><div>102</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_100k_futures_bytes,{}"</span><span>, </span><span>total</span><span>);</span></div></div><div><div><div>103</div></div><div>
</div></div><div><div><div>104</div></div><div><span>    </span><span>// 线程：每个默认预留 2 MiB 栈</span></div></div><div><div><div>105</div></div><div><span>    </span><span>const</span><span> </span><span>NT</span><span>: </span><span>usize</span><span><span> </span><span>=</span><span> </span></span><span>2000</span><span>;</span></div></div><div><div><div>106</div></div><div><span>    </span><span>let</span><span> </span><span>barrier</span><span><span> </span><span>=</span><span> </span></span><span>Arc</span><span>::</span><span>new</span><span><span>(</span><span>Barrier</span><span>::</span></span><span>new</span><span>(</span><span>NT</span><span> + </span><span>1</span><span>));</span></div></div><div><div><div>107</div></div><div><span>    </span><span>let</span><span> </span><span>v0</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>(</span><span>"VmSize:"</span><span>);</span></div></div><div><div><div>108</div></div><div><span>    </span><span>let</span><span> </span><span>r0</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>109</div></div><div><span>    </span><span>let</span><span> </span><span>handles</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>NT</span><span>)</span></div></div><div><div><div>110</div></div><div><span><span>        </span></span><span>.</span><span>map</span><span><span>(</span><span>|</span></span><span>_</span><span><span>|</span><span> {</span></span></div></div><div><div><div>111</div></div><div><span>            </span><span>let</span><span> </span><span>b</span><span><span> </span><span>=</span><span> </span></span><span>barrier</span><span>.</span><span>clone</span><span>();</span></div></div><div><div><div>112</div></div><div><span><span>            </span></span><span>thread</span><span>::</span><span>spawn</span><span>(</span><span>move</span><span><span> </span><span>||</span><span> {</span></span></div></div><div><div><div>113</div></div><div><span>                </span><span>b</span><span>.</span><span>wait</span><span>();</span></div></div><div><div><div>114</div></div><div><span><span>            </span></span><span>})</span></div></div><div><div><div>115</div></div><div><span><span>        </span></span><span>})</span></div></div><div><div><div>116</div></div><div><span><span>        </span></span><span>.</span><span>collect</span><span>();</span></div></div><div><div><div>117</div></div><div><span>    </span><span>let</span><span> </span><span>v1</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>(</span><span>"VmSize:"</span><span>);</span></div></div><div><div><div>118</div></div><div><span>    </span><span>let</span><span> </span><span>r1</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>(</span><span>"VmRSS:"</span><span>);</span></div></div><div><div><div>119</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_vm_per_thread_kb,{:.1}"</span><span>, (</span><span>v1</span><span> - </span><span>v0</span><span>) </span><span>as</span><span> </span><span>f64</span><span> / </span><span>NT</span><span> </span><span>as</span><span> </span><span>f64</span><span>);</span></div></div><div><div><div>120</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_rss_per_thread_kb,{:.1}"</span><span>, (</span><span>r1</span><span> - </span><span>r0</span><span>) </span><span>as</span><span> </span><span>f64</span><span> / </span><span>NT</span><span> </span><span>as</span><span> </span><span>f64</span><span>);</span></div></div><div><div><div>121</div></div><div><span>    </span><span>barrier</span><span>.</span><span>wait</span><span>();</span></div></div><div><div><div>122</div></div><div><span>    </span><span>for</span><span> </span><span>h</span><span> </span><span>in</span><span> </span><span>handles</span><span> {</span></div></div><div><div><div>123</div></div><div><span>        </span><span>h</span><span>.</span><span>join</span><span>().</span><span>unwrap</span><span>();</span></div></div><div><div><div>124</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>125</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>编译运行（edition 2024 的 prelude 已含 <code>Future</code>）：<code>rustc --edition 2024 -O bench_rs.rs -o bench_rs &amp;&amp; ./bench_rs</code></p></section><section><h3>A.5 百万级内存测试<a href="#a5-百万级内存测试"><span>#</span></a></h3><p>C++：100 万个挂起的协程帧，读 <code>VmHWM</code>（峰值常驻）算差值。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;coroutine&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;cstdio&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;cstring&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>static</span><span> </span><span>long</span><span> </span><span>vm</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>field</span><span>) {</span></span></div></div><div><div><div>7</div></div><div><span>  </span><span>FILE</span><span>*</span><span> </span><span>f</span><span> </span><span>=</span><span> </span><span>fopen</span><span>(</span><span>"/proc/self/status"</span><span>, </span><span>"r"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span>f</span><span>) </span><span>return</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>char</span><span> </span><span>line</span><span>[</span><span>256</span><span>];</span></div></div><div><div><div>10</div></div><div><span>  </span><span>long</span><span> </span><span>v</span><span> </span><span>=</span><span> </span><span>-</span><span>1</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>size_t</span><span> </span><span>fl</span><span> </span><span>=</span><span> </span><span>strlen</span><span>(</span><span>field</span><span>);</span></div></div><div><div><div>12</div></div><div><span>  </span><span>while</span><span> (</span><span>fgets</span><span>(</span><span>line</span><span>, </span><span>sizeof</span><span> </span><span>line</span><span>, </span><span>f</span><span>)) {</span></div></div><div><div><div>13</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span>strncmp</span><span>(</span><span>line</span><span>, </span><span>field</span><span>, </span><span>fl</span><span>)) {</span></div></div><div><div><div>14</div></div><div><span>      </span><span>sscanf</span><span>(</span><span>line</span><span> </span><span>+</span><span> </span><span>fl</span><span>, </span><span>"</span><span>%ld</span><span>"</span><span>, </span><span>&amp;</span><span>v</span><span>);</span></div></div><div><div><div>15</div></div><div><span>      </span><span>break</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>  </span><span>fclose</span><span>(</span><span>f</span><span>);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>return</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>struct</span><span> </span><span>Gen</span><span> {</span></div></div><div><div><div>23</div></div><div><span>  </span><span>struct</span><span> </span><span>promise_type</span><span> {</span></div></div><div><div><div>24</div></div><div><span>    </span><span>int</span><span> </span><span>value</span><span>;</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>yield_value</span><span>(</span><span>int</span><span><span> </span><span>v</span><span>) {</span></span></div></div><div><div><div>26</div></div><div><span>      </span><span>value</span><span> </span><span>=</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>27</div></div><div><span>      </span><span>return</span><span> {};</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>    </span><span>Gen</span><span> </span><span>get_return_object</span><span>() {</span></div></div><div><div><div>30</div></div><div><span>      </span><span>return</span><span> </span><span>Gen</span><span><span>{</span><span>std</span><span>::</span><span>coroutine_handle</span><span>&lt;</span></span><span>promise_type</span><span>&gt;::</span><span>from_promise</span><span>(</span><span>*</span><span>this</span><span>)};</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>initial_suspend</span><span>() </span><span>noexcept</span><span> { </span><span>return</span><span> {}; }</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>std::</span><span>suspend_always</span><span> </span><span>final_suspend</span><span>() </span><span>noexcept</span><span> { </span><span>return</span><span> {}; }</span></div></div><div><div><div>34</div></div><div><span>    </span><span>void</span><span> </span><span>return_void</span><span>() {}</span></div></div><div><div><div>35</div></div><div><span>    </span><span>void</span><span> </span><span>unhandled_exception</span><span>() {}</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>std::</span><span>coroutine_handle</span><span>&lt;</span><span>promise_type</span><span>&gt; </span><span>h</span><span>;</span></div></div><div><div><div>38</div></div><div><span>};</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>static</span><span> </span><span>Gen</span><span> </span><span>gen</span><span>(</span><span>long</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>41</div></div><div><span>  </span><span>for</span><span> (</span><span>long</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>i</span><span>++</span><span>) </span><span>co_yield</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>45</div></div><div><span>  </span><span>const</span><span> </span><span>int</span><span> </span><span>N</span><span> </span><span>=</span><span> </span><span>1000000</span><span>;</span></div></div><div><div><div>46</div></div><div><span>  </span><span>long</span><span> </span><span>h0</span><span> </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmHWM:"</span><span>);</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>coroutine_handle</span><span>&lt;Gen::</span><span>promise_type</span><span>&gt;&gt; </span><span>v</span><span>;</span></div></div><div><div><div>48</div></div><div><span>  </span><span>v</span><span>.</span><span>reserve</span><span>(</span><span>N</span><span>);</span></div></div><div><div><div>49</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>N</span><span>; </span><span>i</span><span>++</span><span>) </span><span>v</span><span>.</span><span>push_back</span><span>(</span><span>gen</span><span>(</span><span>10</span><span>).</span><span>h</span><span>);</span></div></div><div><div><div>50</div></div><div><span>  </span><span>long</span><span> </span><span>h1</span><span> </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmHWM:"</span><span>);</span></div></div><div><div><div>51</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_peak_delta_mb,</span><span>%.1f</span><span>\n</span><span>"</span><span>, (</span><span>h1</span><span> </span><span>-</span><span> </span><span>h0</span><span>) </span><span>/</span><span> </span><span>1024.0</span><span>);</span></div></div><div><div><div>52</div></div><div><span>  </span><span>printf</span><span>(</span><span>"cpp_bytes_each,</span><span>%.1f</span><span>\n</span><span>"</span><span>, (</span><span>h1</span><span> </span><span>-</span><span> </span><span>h0</span><span>) </span><span>*</span><span> </span><span>1024.0</span><span> </span><span>/</span><span> </span><span>N</span><span>);</span></div></div><div><div><div>53</div></div><div><span>  </span><span>for</span><span> (</span><span>auto</span><span> </span><span>h</span><span> : </span><span>v</span><span>) </span><span>h</span><span>.</span><span>destroy</span><span>();</span></div></div><div><div><div>54</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>55</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Rust：100 万个 Future，同时看 <code>sizeof</code> 的精确总量。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>async</span><span> </span><span>fn</span><span> </span><span>leaf</span><span>(</span><span>x</span><span>: </span><span>u64</span><span>) -&gt; </span><span>u64</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>x</span><span>.</span><span>wrapping_add</span><span>(</span><span>1</span><span>)</span></div></div><div><div><div>3</div></div><div><span>}</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>fn</span><span> </span><span>vm</span><span>(</span><span>field</span><span>: &amp;</span><span>str</span><span>) -&gt; </span><span>i64</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>let</span><span> </span><span>s</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>fs</span><span>::</span></span><span>read_to_string</span><span>(</span><span>"/proc/self/status"</span><span>).</span><span>unwrap_or_default</span><span>();</span></div></div><div><div><div>7</div></div><div><span>    </span><span>for</span><span> </span><span>l</span><span> </span><span>in</span><span> </span><span>s</span><span>.</span><span>lines</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>        </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>r</span><span><span>) </span><span>=</span><span> </span></span><span>l</span><span>.</span><span>strip_prefix</span><span>(</span><span>field</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>            </span><span>if</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>tok</span><span><span>) </span><span>=</span><span> </span></span><span>r</span><span>.</span><span>split_whitespace</span><span>().</span><span>next</span><span>() {</span></div></div><div><div><div>10</div></div><div><span>                </span><span>if</span><span> </span><span>let</span><span> </span><span>Ok</span><span>(</span><span>v</span><span><span>) </span><span>=</span><span> </span></span><span>tok</span><span>.</span><span>parse</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>                    </span><span>return</span><span> </span><span>v</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>-</span><span>1</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>fn</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>20</div></div><div><span>    </span><span>const</span><span> </span><span>N</span><span>: </span><span>u64</span><span><span> </span><span>=</span><span> </span></span><span>1_000_000</span><span>;</span></div></div><div><div><div>21</div></div><div><span>    </span><span>let</span><span> </span><span>h0</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>(</span><span>"VmHWM:"</span><span>);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>let</span><span> </span><span>vs</span><span>: </span><span>Vec</span><span>&lt;</span><span>_</span><span><span>&gt; </span><span>=</span><span> (</span></span><span>0</span><span>..</span><span>N</span><span>).</span><span>map</span><span>(</span><span>leaf</span><span>).</span><span>collect</span><span>();</span></div></div><div><div><div>23</div></div><div><span>    </span><span>let</span><span> </span><span>h1</span><span><span> </span><span>=</span><span> </span></span><span>vm</span><span>(</span><span>"VmHWM:"</span><span>);</span></div></div><div><div><div>24</div></div><div><span>    </span><span>let</span><span> </span><span>total</span><span><span> </span><span>=</span><span> </span><span>std</span><span>::</span><span>mem</span><span>::</span></span><span>size_of_val</span><span>(</span><span>vs</span><span>.</span><span>as_slice</span><span>());</span></div></div><div><div><div>25</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_peak_delta_mb,{:.1}"</span><span>, (</span><span>h1</span><span> - </span><span>h0</span><span>) </span><span>as</span><span> </span><span>f64</span><span> / </span><span>1024.0</span><span>);</span></div></div><div><div><div>26</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_bytes_each,{:.1}"</span><span>, (</span><span>h1</span><span> - </span><span>h0</span><span>) </span><span>as</span><span> </span><span>f64</span><span> * </span><span>1024.0</span><span> / </span><span>N</span><span> </span><span>as</span><span> </span><span>f64</span><span>);</span></div></div><div><div><div>27</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_sizeof_total_mb,{:.1}"</span><span>, </span><span>total</span><span> </span><span>as</span><span> </span><span>f64</span><span> / </span><span>1024.0</span><span> / </span><span>1024.0</span><span>);</span></div></div><div><div><div>28</div></div><div><span>    </span><span>println!</span><span>(</span><span>"rs_sizeof_each,{}"</span><span>, </span><span>total</span><span> / </span><span>N</span><span> </span><span>as</span><span> </span><span>usize</span><span>);</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Python：100 万个真正挂起（等待同一个 <code>Event</code>）的 asyncio Task，量峰值内存。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> asyncio</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>def</span><span> </span><span>vm</span><span>(</span><span>field</span><span>:</span><span> </span><span>str</span><span>) -&gt; </span><span>int</span><span>:</span></div></div><div><div><div>5</div></div><div><span>    </span><span>with</span><span> </span><span>open</span><span>(</span><span>"/proc/self/status"</span><span>) </span><span>as</span><span> f:</span></div></div><div><div><div>6</div></div><div><span>        </span><span>for</span><span> line </span><span>in</span><span> f:</span></div></div><div><div><div>7</div></div><div><span>            </span><span>if</span><span><span> line.</span><span>startswith</span><span>(field):</span></span></div></div><div><div><div>8</div></div><div><span>                </span><span>return</span><span> </span><span>int</span><span><span>(line.</span><span>split</span><span>()[</span></span><span>1</span><span>])</span></div></div><div><div><div>9</div></div><div><span>    </span><span>return</span><span><span> </span><span>-</span></span><span>1</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>async</span><span> </span><span>def</span><span> </span><span>wait_forever</span><span>(</span><span>ev</span><span>:</span><span> asyncio.Event</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>13</div></div><div><span>    </span><span>await</span><span><span> ev.</span><span>wait</span><span>()</span></span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>async</span><span> </span><span>def</span><span> </span><span>main</span><span>(</span><span>n</span><span>:</span><span> </span><span>int</span><span>) -&gt; </span><span>None</span><span>:</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>base </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmHWM:"</span><span>)</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>ev </span><span>=</span><span> asyncio.</span><span>Event</span><span>()</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>tasks </span><span>=</span><span> [asyncio.</span><span>create_task</span><span>(</span><span>wait_forever</span><span>(ev)) </span><span>for</span><span> _ </span><span>in</span><span> </span><span>range</span><span>(n)]</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>peak </span><span>=</span><span> </span><span>vm</span><span>(</span><span>"VmHWM:"</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>"py_tasks,</span><span>%d</span><span>"</span><span><span> </span><span>%</span><span> n)</span></span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>"py_peak_delta_mb,</span><span>%.1f</span><span>"</span><span><span> </span><span>%</span><span> ((peak </span><span>-</span><span> base) </span><span>/</span><span> </span></span><span>1024.0</span><span>))</span></div></div><div><div><div>23</div></div><div><span>    </span><span>print</span><span>(</span><span>"py_bytes_each,</span><span>%.1f</span><span>"</span><span><span> </span><span>%</span><span> ((peak </span><span>-</span><span> base) </span><span>*</span><span> </span></span><span>1024.0</span><span><span> </span><span>/</span><span> n))</span></span></div></div><div><div><div>24</div></div><div><span>    </span><span>for</span><span> t </span><span>in</span><span> tasks:</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>t.</span><span>cancel</span><span>()</span></div></div><div><div><div>26</div></div><div><span>    </span><span>await</span><span><span> asyncio.</span><span>gather</span><span>(*tasks, </span></span><span>return_exceptions</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>asyncio.</span><span>run</span><span>(</span><span>main</span><span>(</span></span><span>1_000_000</span><span>))</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/initialization-in-cpp/</id>
      <title type="text">C++ 中的初始化列表和列表初始化</title>
      <published>2026-01-03T15:07:00.000Z</published>
      <updated>2026-01-03T15:07:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/initialization-in-cpp/"/>
      <summary type="text">深入理解 C++ 中初始化列表与列表初始化的区别：前者解决对象生命周期与内存模型问题，后者统一初始化语法并提供类型安全保障。</summary>
      <content type="html"><![CDATA[<p>很多初学者容易混淆<strong>初始化列表</strong>（Member Initializer List）和<strong>列表初始化</strong>（List Initialization），因为它们的名字很像，但它们实际上解决的是完全不同的问题：</p>
<ol>
<li><strong>初始化列表</strong>：解决的是<strong>对象生命周期与内存模型</strong>的问题（“什么时候赋初值”）</li>
<li><strong>列表初始化</strong>：解决的是<strong>类型系统的统一性与安全性</strong>的问题（“用什么语法赋初值”）</li>
</ol>
<section><h2>初始化列表（Member Initializer List）<a href="#初始化列表member-initializer-list"><span>#</span></a></h2><p>它的形式出现在构造函数参数列表之后，函数体大括号之前，以 <code>:</code> 开头。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>A</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>int</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>3</div></div><div><span>public:</span></div></div><div><div><div>4</div></div><div><span>    </span><span>A</span><span>(</span><span>int</span><span><span> </span><span>val</span><span>) : </span></span><span>x</span><span>(</span><span>val</span><span>) {}</span><span> // 初始化列表</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>初始化 vs. 赋值<a href="#初始化-vs-赋值"><span>#</span></a></h3><p>在 C++ 的对象模型中，“初始化”（Initialization）和 “赋值”（Assignment）是两个截然不同的物理过程。</p><ol>
<li><strong>内存分配</strong>（<em>Allocation</em>）：在栈上或堆上划出一块内存</li>
<li><strong>初始化</strong>（<em>Initialization</em>）：在这块内存上构建对象，使其成为一个合法的实例</li>
<li><strong>赋值</strong>（<em>Assignment</em>）：对象已经存在了，擦出旧值，填入新值。</li>
</ol><p>构造函数的执行时间线：</p><ol>
<li>进入构造函数之前：编译器必须确保所有成员变量都已经 “出生”（初始化完成）</li>
<li>进入构造函数体（<code>{...}</code>）：这已经是 “出生后” 的世界了，这里面写的代码都是 “赋值” 操作。</li>
</ol></section><section><h3>为什么要用初始化列表？<a href="#为什么要用初始化列表"><span>#</span></a></h3><p>如果你不用初始化列表，而是写在函数体内：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Person</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>name</span><span>;</span></div></div><div><div><div>3</div></div><div><span>public:</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>// 写法 1：在函数体内赋值</span></div></div><div><div><div>5</div></div><div><span>    </span><span>Person</span><span>(</span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>        </span><span>name</span><span> </span><span>=</span><span> </span><span>n</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>底层发生了什么？</p><ol>
<li>隐式初始化：在进入 <code>{</code> 之前，编译器发现 <code>name</code> 还没有初始化，于是强行用 <code>std::string</code> 的默认构造函数。<code>name</code> 变成了一个空字符串 <code>""</code></li>
<li><strong>赋值操作</strong>：进入 <code>{</code> 后，执行 <code>name = n;</code>。调用 <code>std::string</code> 的赋值运算符，把刚才那个空字符串的内容丢弃，拷贝 <code>n</code> 的内容</li>
</ol><p>这就类似于，你先建了一个空房子，然后立即把它拆了重建成你想要的样子，这就是双倍的开销。</p><p>正确的写法（初始化列表）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 写法 2：使用初始化列表</span></div></div><div><div><div>2</div></div><div><span>Person</span><span>(</span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span> </span><span>n</span><span>) : </span><span>name</span><span>(</span><span>n</span><span>) {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>底层发生了什么？</p><ol>
<li>直接构造：直接调用 <code>std::string</code> 的拷贝构造函数，用 <code>n</code> 来 “生出” <code>name</code></li>
<li>函数体为空，无操作。收益：省去了一次默认构造和一次赋值操作。</li>
</ol></section><section><h3>必须使用的场景（物理限制）<a href="#必须使用的场景物理限制"><span>#</span></a></h3><p>有些东西必须 “出生时” 就确定，生出来后再改就晚了。这些情况必须使用初始化列表：</p><ul>
<li><code>const</code> 成员：常量一旦出生就不能修改（不能赋值）</li>
<li>引用成员（<code>&amp;</code>）：引用一旦出生必须绑定到一个对象，不能重新绑定（不能赋值）</li>
<li>没有默认构造函数的类成员：如果成员是一个类，且它没有默认构造函数（必须带参），编译器无法在进入函数体前 “隐式初始化” 它，必须显式指定</li>
</ul></section><section><h3>初始化的顺序（内存布局决定）<a href="#初始化的顺序内存布局决定"><span>#</span></a></h3><p>初始化列表中的顺序并不决定初始化的真实顺序！<strong>成员变量的初始化顺序严格由它们在类定义（Class Definition）中声明的顺序决定</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>A</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>int</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>int</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>4</div></div><div><span>public:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>// 错误示范！看起来像先算 y，其实 x 会先初始化，次数 y 是垃圾值</span></div></div><div><div><div>6</div></div><div><span>    </span><span>A</span><span>(</span><span>int</span><span><span> </span><span>val</span><span>) : </span></span><span>y</span><span>(</span><span>val</span><span>), </span><span>x</span><span>(</span><span>y</span><span>) {}</span></div></div><div><div><div>7</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++ 对象的内存布局在编译器就确定了（<code>x</code> 在前，<code>y</code> 在后）。析构时必须按相反顺序销毁。为了保证构造和析构的对称性（LIFO），构造顺序必须固定，不能随你怎么写列表儿改变。</p></section></section>
<section><h2>列表初始化（List Initialization）<a href="#列表初始化list-initialization"><span>#</span></a></h2><p>它的形式是使用花括号 <code>{}</code>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>a</span><span>{</span><span>0</span><span>};</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v</span><span>{</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>统一初始化语义<a href="#统一初始化语义"><span>#</span></a></h3><p>在 C++11 之前，初始化的语法简直是精神分裂：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>5</span><span>;</span></div></div><div><div><div>2</div></div><div><span>int</span><span> </span><span>b</span><span>(</span><span>5</span><span>);</span></div></div><div><div><div>3</div></div><div><span>int</span><span> </span><span>arr</span><span>[] </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>};</span></div></div><div><div><div>4</div></div><div><span>struct</span><span> </span><span>Point</span><span> </span><span>p</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>C++11 引入 <code>{}</code> 旨在<strong>统一初始化</strong>（<em>Uniform Inialization</em>）：万物皆可 <code>{}</code> 初始化。</p></section><section><h3>底层机制：<code>std::initializer_list</code><a href="#底层机制stdinitializer_list"><span>#</span></a></h3><p>当你用<code>{1, 2, 3, 4}</code> 初始化一个容器（如 <code>std::vector</code>）时，发生了什么？</p><ol>
<li>编译器魔法：编译器会在静态存储区悄悄创建一个数组</li>
<li>包装：编译器构建一个 <code>std::initializer_list&lt;T&gt;</code> 对象。这其实是一个轻量级的 “视图” 或 “胖指针”，它只包含两个指针：一个指向数组开头，一个指向结尾（或长度）</li>
<li>传递：这个轻量级对象被传给 <code>std::vector</code> 的构造函数</li>
<li>消费：<code>vector</code> 遍历这个 list，把数据拷贝到自己的内存里</li>
</ol><p>这就是为什么你可以给 <code>std::vector</code> 赛任意数量的初始值。</p></section><section><h3>安全特性：防止窄化转换（Narrowing Conversion）<a href="#安全特性防止窄化转换narrowing-conversion"><span>#</span></a></h3><p>这是一个类型安全设计。初始化意味着 “建立一个合法的初始状态”。如果数据在初始化过程中丢失了精度，那么这个初始状态本身就是 “不诚实” 的。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>3.14</span><span>;</span><span> // C++98: 允许 a 变成 3（截断）</span></div></div><div><div><div>2</div></div><div><span>int</span><span> </span><span>b</span><span>{</span><span>3.14</span><span>};</span><span> // C++11: 编译器报错！禁止数据丢失</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>{}</code> 语法强制要求：如果源类型的值无法无损地放入目标类型，编译器必须报错。</p></section><section><h3>解决 “最令人头秃的解析”（Most Vexing Parse）<a href="#解决-最令人头秃的解析most-vexing-parse"><span>#</span></a></h3><p>C++ 的语法歧义：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 你的意图：创建一个对象 a，调用默认构造函数</span></div></div><div><div><div>2</div></div><div><span>A</span><span> </span><span>a</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>编译器的理解：声明了一个函数 <code>a()</code>，它不接受参数，返回类型是 <code>A</code>。</p><p>解决办法：使用 <code>{}</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A</span><span> </span><span>a</span><span>{};</span><span> // 明确无误，这就是创建对象</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为函数声明不可能用 <code>{}</code> 结尾。</p></section></section>
<section><h2>两者的 “撞车”<a href="#两者的-撞车"><span>#</span></a></h2><p>当一个类既有普通构造函数，又有接受 <code>std::initializer_list</code> 的构造函数时，<code>{}</code> 会极度优先匹配后者。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Magic</span><span> {</span></div></div><div><div><div>2</div></div><div><span>public:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Magic</span><span>(</span><span>int</span><span><span> </span><span>a</span><span>, </span></span><span>int</span><span><span> </span><span>b</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>// 构造函数 1：普通构造</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"Normal Constructor"</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>endl</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>Magic</span><span>(std::</span><span>initializer_list</span><span>&lt;</span><span>int</span><span><span>&gt; </span><span>list</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>// 构造函数 2：列表构造</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"List Constructor"</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>endl</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>};</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>Magic</span><span> </span><span>m1</span><span>(</span><span>10</span><span>, </span><span>20</span><span>);</span><span> // 调用构造函数 1</span></div></div><div><div><div>15</div></div><div><span>Magic</span><span> </span><span>m2</span><span>{</span><span>10</span><span>, </span><span>20</span><span>};</span><span> // 调用构造函数 2（因为 {10, 20} 被视为列表）</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>如何选择 <code>{}</code> 与 <code>()</code>？<a href="#如何选择--与-"><span>#</span></a></h2><section><h3><code>{}</code> 的写法<a href="#-的写法"><span>#</span></a></h3><section><h4>直接列表初始化（Direct List Initialization）<a href="#直接列表初始化direct-list-initialization"><span>#</span></a></h4><ul>
<li>特征：不带等号 <code>=</code></li>
<li>心智模型：我非常确定我要构造这个对象，请直接用这些参数造出来，不许做任何隐式类型转换。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 1. 普通变量</span></div></div><div><div><div>2</div></div><div><span>int</span><span> </span><span>x</span><span>{</span><span>0</span><span>};</span><span>                       // x 初始化为 0</span></div></div><div><div><div>3</div></div><div><span>double</span><span> </span><span>d</span><span>{</span><span>3.14</span><span>};</span><span>                 // d 初始化为 3.14</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 2. 类对象</span></div></div><div><div><div>6</div></div><div><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v</span><span>{</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>};</span><span>    // 容器包含 1, 2, 3</span></div></div><div><div><div>7</div></div><div><span>Person</span><span> </span><span>p</span><span>{</span><span>"Alice"</span><span>, </span><span>20</span><span>};</span><span>          // 调用 Person(string, int)</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// 3. 堆内存 new</span></div></div><div><div><div>10</div></div><div><span>int*</span><span> </span><span>ptr</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>int</span><span>{</span><span>5</span><span>};</span><span>          // 申请内存并初始化为 5</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>// 4. 临时对象（匿名对象）</span></div></div><div><div><div>13</div></div><div><span>func</span><span>(</span><span>Person</span><span>{</span><span>"Bob"</span><span>, </span><span>30</span><span>});</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>// 5. 基类与成员初始化</span></div></div><div><div><div>16</div></div><div><span>Derive</span><span>(</span><span>int</span><span> </span><span>x</span><span>, </span><span>int</span><span> </span><span>y</span><span>) : </span><span>Base</span><span>{</span><span>x</span><span>}, </span><span>m_val</span><span>{</span><span>y</span><span>} {}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h4>拷贝列表初始化（Copy List Initialization）<a href="#拷贝列表初始化copy-list-initialization"><span>#</span></a></h4><ul>
<li>特征：带有等号 <code>=</code></li>
<li>心智模型：先把右边的花括号转换成目标类型，然后再拷贝（或移动）给左边</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 1. 变量定义</span></div></div><div><div><div>2</div></div><div><span>int</span><span> </span><span>x</span><span> </span><span>=</span><span> {</span><span>0</span><span>};</span></div></div><div><div><div>3</div></div><div><span>std::</span><span>vector</span><span>&lt;int&gt;</span><span> </span><span>v</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 2. 函数传参（隐式构造）</span></div></div><div><div><div>6</div></div><div><span>void</span><span> </span><span>foo</span><span>(std::</span><span>vector</span><span>&lt;</span><span>int</span><span><span>&gt; </span><span>v</span><span>);</span></span></div></div><div><div><div>7</div></div><div><span>foo</span><span>({</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>});</span><span>                 // 编译器隐式地把 {1, 2, 3} 转换成了 vector</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>// 3. 函数返回值</span></div></div><div><div><div>10</div></div><div><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>bar</span><span>() {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>};</span><span>           // 隐式构造并返回</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>直接列表初始化和拷贝列表初始化的选择<a href="#直接列表初始化和拷贝列表初始化的选择"><span>#</span></a></h4><p>在绝大多数情况下，由于现代编译器的优化，拷贝列表初始化会被直接优化成为直接列表初始化，这被称为<strong>复制省略</strong>（<em>Copy Elision</em>）。</p><p>但是存在一种例外情况，那就是 <code>explicit</code> 关键字声明的构造函数。</p></section><section><h4><code>explicit</code> 关键字的影响<a href="#explicit-关键字的影响"><span>#</span></a></h4><p>如果一个类的构造函数被声明为 <code>explicit</code>（显式），那么第 2 种写法（带等号的会被禁止）。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Widget</span><span> {</span></div></div><div><div><div>2</div></div><div><span>public:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>explicit</span><span> </span><span>Widget</span><span>(</span><span>int</span><span><span> </span><span>n</span><span>) {}</span></span></div></div><div><div><div>4</div></div><div><span>};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>Widget</span><span> </span><span>w1</span><span>{</span><span>10</span><span>};</span><span>      // OK：直接初始化，显式调用</span></div></div><div><div><div>7</div></div><div><span>Widget</span><span> </span><span>w2</span><span> </span><span>=</span><span> {</span><span>10</span><span>};</span><span>   // Error：拷贝列表初始化要求 “隐式转换”，但被 explicit 阻止了</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以我们可以得出结论：<code>Type x{...}</code> 比 <code>Type x = { ... }</code> 更加通用，因为它不受 <code>explicit</code> 的限制，更推荐使用不写等号的写法。</p></section><section><h4>真的应该所有情况都用 <code>{}</code> 吗？<a href="#真的应该所有情况都用--吗"><span>#</span></a></h4><p>虽然 C++ Core Guidenlines 推荐默认使用 <code>{}</code>，但你必须警惕两个巨大的陷阱。这两个陷阱的根源在于编译器对 <code>std::initializer_list</code> 的极度偏爱。</p><section><h5>陷阱 1：<code>std::vector</code> 的构造<a href="#陷阱-1stdvector-的构造"><span>#</span></a></h5><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v1</span><span>(</span><span>10</span><span>, </span><span>5</span><span>);</span><span> // 用 ()</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v2</span><span>{</span><span>10</span><span>, </span><span>5</span><span>};</span><span> // 用 {}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>解析：</p><ul>
<li><code>v1</code>（圆括号）：调用 <code>vectore(size_type n, const T&amp; val)</code>，创建了 10 个元素，每个元素都是 5</li>
<li><code>v2</code>（花括号）：编译器看到 <code>{}</code>，优先查找是否接受 <code>std::initializer_list</code> 的构造函数，找到了，于是创建了 2 个元素，分别是 10 和 5</li>
</ul><p>当构造函数发生重载时，如果存在接受 <code>std::intitalizer_list</code> 的版本，编译器会贪婪地优先匹配它，哪怕需要进行一些类型提升。只有完全匹配不上了，才会退回去找普通的构造函数。</p><p>建议：</p><ul>
<li>原则上：默认用 <code>{}</code></li>
<li>例外：当在调用容器的大小/数量构造函数时，必须用 <code>()</code>，否则会被误解为 “元素内容”</li>
</ul></section><section><h5>陷阱 2：<code>auto</code> 类型推导的歧义<a href="#陷阱-2auto-类型推导的歧义"><span>#</span></a></h5><p>这一点在 C++11/14 和 C++17 中表现不同，非常令人困惑。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>auto</span><span> </span><span>x</span><span> </span><span>=</span><span> {</span><span>1</span><span>};</span></div></div><div><div><div>2</div></div><div><span>// x 的类型是什么？是 int 吗？</span></div></div><div><div><div>3</div></div><div><span>// NO! x 的类型是 std::initializer_list&lt;int&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你本意是想要一个 <code>int</code>，结果得到了一个轻量级列表对象，后面的代码可能会出问题。</p><p>修正写法（C++17 起）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>auto</span><span> </span><span>x</span><span>{</span><span>1</span><span>};</span><span>      // C++17 规定推导为 int</span></div></div><div><div><div>2</div></div><div><span>auto</span><span> </span><span>y</span><span> </span><span>=</span><span> {</span><span>1</span><span>};</span><span>   // 推导为 std::initializer_list&lt;int&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>建议：配合 <code>auto</code> 使用 <code>{}</code> 时要格外小心，最好明确类型，或者确保你真的想要 <code>std::initializer_list</code>。</p></section></section></section><section><h3><code>{}</code> 的特性：默认值初始化（Value Initialization）<a href="#-的特性默认值初始化value-initialization"><span>#</span></a></h3><p>这是 <code>{}</code> 最让我觉得 “舒服” 的地方。</p><p>如果你想把一个对象清零，或者初始化为 “空状态”：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 旧写法</span></div></div><div><div><div>2</div></div><div><span>int</span><span> </span><span>i</span><span>;</span><span>      // 危险！如果是局部变量，i 的值是随机的（垃圾值）</span></div></div><div><div><div>3</div></div><div><span>int*</span><span> </span><span>p</span><span>;</span><span>     // 危险！野指针</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>// 新写法（空花括号）</span></div></div><div><div><div>6</div></div><div><span>int</span><span> </span><span>i</span><span>{};</span><span>    // 安全！保证初始化为 0</span></div></div><div><div><div>7</div></div><div><span>int*</span><span> </span><span>p</span><span>{};</span><span>   // 安全！保证初始化为 nullptr</span></div></div><div><div><div>8</div></div><div><span>double</span><span> </span><span>d</span><span>{};</span><span> // 安全！保证初始化为 0.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>空的 <code>{}</code> 触发<strong>值初始化</strong>（<em>Value Initialization</em>）。对于内置类型（<code>int</code>、<code>float</code>、指针），它会执行<strong>零初始化</strong>（<em>Zero Initialization</em>）。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/constructor-in-cpp/</id>
      <title type="text">C++ 中的构造函数</title>
      <published>2026-01-03T13:05:00.000Z</published>
      <updated>2026-01-03T13:05:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/constructor-in-cpp/"/>
      <summary type="text">深入理解 C++ 构造函数的本质：从资源管理到性能优化。涵盖默认构造、拷贝构造、移动构造，以及 RAII、explicit、constexpr 等关键机制。</summary>
      <content type="html"><![CDATA[<p>对于 C++ 对象而言，我们认为：对象 = 内存 + 语义（不变量）。</p>
<ul>
<li>内存：仅仅是电子与硅晶体中状态未知的比特位。</li>
<li><strong>语义</strong>：这段内存代表什么含义（是 <code>int</code>、是 <code>char</code> 还是 <code>float</code>），以及它必须满足的条件（“不变量”，Invariant）。</li>
</ul>
<p><strong>构造函数</strong>（<em>Constructor</em>）的本质就是将 “原始、混沌” 的内存强制转换为 “持有特定语义的、合法的对象” 的原子操作过程。</p>
<section><h2>核心逻辑<a href="#核心逻辑"><span>#</span></a></h2><p>在 C 语言中，创建一个 <code>struct</code> 通常分为两步：</p><ol>
<li>分配内存（<code>malloc</code> 或栈上声明）</li>
<li>赋值（<code>init</code> 函数或手动赋值）</li>
</ol><p>问题在于：如果在第 1 步和第 2 步之间使用该对象，就会导致灾难（未定义行为）。或者，如果使用者忘记了第 2 步，系统就会处于 “非法状态”。</p><p>C++ 引入构造函数就是为了保证：</p><blockquote><p>如果一个对象存在，那么它一定是合法的。</p></blockquote><p>构造函数保证了<strong>初始化</strong>（<em>Initialization</em>）与<strong>定义</strong>（<em>Defination</em>）的不可分割性。</p></section>
<section><h2>构造函数的执行流<a href="#构造函数的执行流"><span>#</span></a></h2><p>当你写下 <code>T object(args);</code> 时，编译器实际执行了以下步骤：</p><ol>
<li><strong>分配内存</strong>：在栈或堆上找到一块足够容纳 <code>sizeof(T)</code> 的空间。此时，内存里的数据是随机的（Garbage）。</li>
<li><strong>执行初始化列表</strong>（Initialization List）：这是真正的初始化时刻。</li>
<li><strong>执行函数体</strong>（Function Body）：这实际上是后续的计算或赋值操作，而非初始化。</li>
</ol><section><h3>为什么首选初始化列表？<a href="#为什么首选初始化列表"><span>#</span></a></h3><p>因为 C++ 规定成员变量在进入构造函数体 <code>{}</code> 之前必须完成构建。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Class</span><span>() : </span><span>member</span><span>(</span><span>value</span><span>) {}</span><span> // 直接在内存位置上构造 member</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>使用初始化列表的成本仅为 1 次构造。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Class</span><span>() { </span><span>member</span><span> </span><span>=</span><span> </span><span>value</span><span>; }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>过程：</p><ol>
<li>调用 <code>member</code> 的默认构造函数（无参）。</li>
<li>调用 <code>member</code> 的赋值运算符 <code>operator=</code>。</li>
</ol><p>在这个过程中的成本为：1 次构造 + 1 次赋值（还可能设计旧内存释放和新内存申请）。</p><blockquote><p>初始化列表不仅是效率优化，对于 <code>const</code> 成员或 <code>reference</code>（引用）成员，它是<strong>唯一</strong>的初始化方式，因为它们创建后不可修改（不可赋值）。</p></blockquote></section></section>
<section><h2>构造函数的分类<a href="#构造函数的分类"><span>#</span></a></h2><p>根据对象资源管理的不同需求，构造函数演化出了四种主要形态。我们将用<strong>资源所有权</strong>的视角来区分它们。</p><section><h3>默认构造函数（Default Constructor）<a href="#默认构造函数default-constructor"><span>#</span></a></h3><ul>
<li>语义：无中生有</li>
<li>形式：<code>T()</code></li>
<li>视角：当对象被创建但外界未提供任何信息时，对象应处于什么状态？通常是 “空状态” 或 “零状态”</li>
<li>注意：如果类中包含原始指针，编译器生成的默认构造函数不会置空指针（由于 C 的遗留包袱），这会导致悬垂指针。因此现代 C++ 提倡显式定义或使用成员默认初始化（<code>int* p = nullptr;</code>）</li>
</ul></section><section><h3>参数化构造函数（Parameterized Constructor）<a href="#参数化构造函数parameterized-constructor"><span>#</span></a></h3><ul>
<li>语义：根据蓝图定制</li>
<li>形式：<code>T(args...)</code></li>
<li>视角：将外部数据约束映射到内部不变量。例如，创建 “圆” 对象，参数是半径。构造函数必须检查 <code>radius &gt; 0</code>，这就是维护 “不变量”</li>
</ul></section><section><h3>拷贝构造函数（Copy Constructor）<a href="#拷贝构造函数copy-constructor"><span>#</span></a></h3><ul>
<li>语义：复制（细胞分裂、克隆）</li>
<li>形式：<code>T(const T&amp; other)</code></li>
<li>视角：
<ul>
<li>如果对象时<strong>值语义</strong>（如整数、坐标），直接按位拷贝（Shallow Copy）</li>
<li>如果对象持有<strong>资源</strong>（如堆内存指针、文件句柄），必须进行<strong>深拷贝</strong>（<em>Deep Copy</em>）</li>
<li>本质矛盾：如果只复制指针，两个对象指向同一块内存，析构时会发生 “Double Free” 错误。因此拷贝构造函数必须重新分配资源。</li>
</ul>
</li>
</ul></section><section><h3>移动构造函数（Move Constructor）<a href="#移动构造函数move-constructor"><span>#</span></a></h3><p>移动构造函数是 C++11 提出的革命性进步。</p><ul>
<li>语义：所有权转移（器官移植）</li>
<li>形式：<code>T(T&amp;&amp; other)</code></li>
<li>视角：
<ul>
<li>在 C++98 中，如果要将一个临时对象（即将销毁）放入容器，比如先复制再销毁。这极度浪费性能（如复制一个巨大的 <code>std::vector</code>）</li>
<li>移动构造函数利用<strong>右值引用</strong>（<code>&amp;&amp;</code>），识别出 <code>other</code> 是一个即将消亡的对象。</li>
<li>它<strong>偷走</strong> <code>other</code> 的资源（指针指向新主，旧指针置空），而非复制数据</li>
<li>代价：极低（仅是指针赋值）</li>
</ul>
</li>
</ul></section></section>
<section><h2>关键机制与陷阱<a href="#关键机制与陷阱"><span>#</span></a></h2><section><h3><code>explicit</code> 关键字：拒绝隐式转换<a href="#explicit-关键字拒绝隐式转换"><span>#</span></a></h3><p>C++ 默认允许单参数构造函数进行隐式类型转换。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Buffer</span><span> { </span><span>Buffer</span><span>(</span><span>int</span><span> </span><span>size</span><span>) { ... } };</span></div></div><div><div><div>2</div></div><div><span>void</span><span> </span><span>func</span><span>(</span><span>Buffer</span><span><span> </span><span>b</span><span>);</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>func</span><span>(</span><span>42</span><span>)；</span><span> // 编译器偷偷执行了 Buffer(42)，可能并不是你想要的</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>从安全角度（Safety First）出发，隐式类型转换破坏了强类型系统。标记 <code>explicit</code> 禁止这种 “自作聪明” 的行为，强制显式调用。</p></section><section><h3>委托构造（Delegating Constructors）<a href="#委托构造delegating-constructors"><span>#</span></a></h3><p>允许一个构造函数调用同类的另一个构造函数。这是为了准许 <strong>DRY</strong>（<em>Don’t Repeat Yourself</em>）原则，防止初始化逻辑碎片化。</p></section><section><h3>构造与虚函数<a href="#构造与虚函数"><span>#</span></a></h3><p><strong>永远不要在构造函数中调用虚函数</strong>。</p><ul>
<li>原理：在基类构造期间，派生类的部分尚未初始化。为了安全，C++ 此时将对象视为基类类型。虚函数表（<code>vtalbe</code>）指针指向基类表，多态失效。</li>
</ul></section></section>
<section><h2>RAII 与构造函数<a href="#raii-与构造函数"><span>#</span></a></h2><p>将上述所有内容串联起来的概念就是 RAII（<em>Resource Acquisition Is Initialization</em>），这是 C++ 的灵魂。</p><ul>
<li><strong>资源获取即初始化</strong>：资源的生命周期严格绑定对象的生命周期</li>
<li><strong>构造函数</strong>：资源的获取点（锁住互斥量、打开文件、分配内存）</li>
<li><strong>析构函数</strong>：资源的释放点（解锁、关闭、释放）</li>
</ul><p>C++ 的构造函数不仅仅是用来 “赋值” 的函数，它是类型系统安全性的守门人，是资源管理自动化的起点。</p><p>掌握构造函数，不仅仅是记住语法，而是要时刻思考：</p><blockquote><p>这个对象诞生的一瞬间，我如何保证它拥有了所需的资源，且处于绝对合法的状态？</p></blockquote></section>
<section><h2>相关关键字<a href="#相关关键字"><span>#</span></a></h2><section><h3>控制编译器行为<a href="#控制编译器行为"><span>#</span></a></h3><p>C++ 编译器通常会 “自作聪明” 地为你生成默认构造、拷贝构造等。以下关键字则可以用于<strong>精确控制</strong>这种自动行为。</p><section><h4><code>= default</code><a href="#-default"><span>#</span></a></h4><ul>
<li>语义：出厂设置</li>
</ul><p>当你手写了一个参数化构造函数 <code>T(int a)</code> 后，编译器认为你是一个有主见的人，于是不再自动生成无参的默认构造函数 <code>T()</code>。如果此时你又想要那个 “空” 的默认构造函数，不需要再手写个空函数体 <code>{}</code>（这会导致它变成 “用户提供的”，从而失去某些 trivial/POD 特性），直接用 <code>= default</code> 让编译器恢复它的默认生成逻辑。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Example</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Example</span><span>(</span><span>int</span><span><span> </span><span>a</span><span>);</span></span><span>         // 自定义构造</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Example</span><span>() </span><span>=</span><span> </span><span>default</span><span>;</span><span>    // 强制找回默认构造，且比手写 {} 更高效</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><code>= delete</code>（C++11）<a href="#-deletec11"><span>#</span></a></h4><ul>
<li>语义：此路不通</li>
</ul><p>有些对象在语义上是<strong>独一无二</strong>的（例如：单例模式、硬件驱动句柄 <code>Mutex</code>、<code>FileStream</code>），它们<strong>绝不能</strong>被拷贝。</p><p>在 C++11 之前，我们通过把拷贝构造函数设为 <code>private</code> 来防止拷贝。C++11 之后，可以直接在<strong>语法层面</strong> “删除” 这个函数的存在。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Mutex</span><span> {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>// 任何尝试拷贝代码的操作，在编译期间就会报错</span></div></div><div><div><div>3</div></div><div><span>    </span><span>Mutex</span><span>(</span><span>const</span><span> </span><span>Mutex</span><span>&amp;</span><span>) </span><span>=</span><span> </span><span>delete</span><span>;</span></div></div><div><div><div>4</div></div><div><span>    </span><span>Mutex</span><span>&amp;</span><span> </span><span>operator</span><span>(</span><span>const</span><span> </span><span>Mutex</span><span>&amp;</span><span>) </span><span>=</span><span> </span><span>delete</span><span>;</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><code>using</code>（继承构造函数）<a href="#using继承构造函数"><span>#</span></a></h4><ul>
<li>语义：拿来主义</li>
</ul><p>派生类通常不会继承基类的构造函数。如果基类有 10 种构造方式，派生类想支持同样的 10 种，以前得手动写 10 个转发函数。</p><p><code>using</code> 关键字告诉编译器：把基类的构造函数直接 “引入” 到当前作用域。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Base</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>Base</span><span>(</span><span>int</span><span>); </span><span>Base</span><span>(std::</span><span>string</span><span>); </span><span>Base</span><span>(</span><span>float</span><span>);</span></div></div><div><div><div>3</div></div><div><span>};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>struct</span><span> </span><span>Derived</span><span>: Base {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>using</span><span> Base::</span><span>Base</span><span>;</span><span> // 一句话，拥有了上述三种构造方式</span></div></div><div><div><div>7</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>性能优化<a href="#性能优化"><span>#</span></a></h3><p>这部分关键字主要服务于嵌入式开发和高性能计算，通过向编译器提供更多信息来优化机器码。</p><section><h4><code>noexcept</code><a href="#noexcept"><span>#</span></a></h4><ul>
<li>语义：我保证不惹麻烦（不抛出异常）</li>
</ul><p>这是<strong>移动语义</strong>（Move Semantics）生效的关键。当 <code>std::vector</code> 扩容时，它需要把旧数据搬到新内存。如果你的移动构造函数没有标记 <code>noexcept</code>，<code>std::vector</code> 为了内存安全（怕搬到一半抛异常，导致旧数据没了，新数据也没好），会放弃移动，强行降级为拷贝。</p><p>这在大数据量或高性能要求场景下会带来极大的损耗。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>BigData</span><span> {</span></div></div><div><div><div>2</div></div><div><span>public:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>// 承诺：移动操作绝不会失败，编译器看到这个才会大胆优化</span></div></div><div><div><div>4</div></div><div><span>    </span><span>BigData</span><span>(</span><span>BigData</span><span>&amp;&amp;</span><span><span> </span><span>other</span><span>) </span></span><span>noexcept</span><span> { ... }</span></div></div><div><div><div>5</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><code>constexpr</code>（C++11/14）<a href="#constexprc1114"><span>#</span></a></h4><ul>
<li>语义：在编译时就已经准备好了</li>
</ul><p>如果一个对象的构造参数在编译时就是确定的常量，那么为什么要等到程序运行（Runtime）才去分配内存、赋值呢？</p><p><code>constexpr</code> 构造函数允许编译器在<strong>编译阶段</strong>就计算出对象的内存布局，并直接烧录在二进制文件的只读数据端（<code>.rodata</code>）或直接作为立即数嵌入指令中。</p><p>这对于<strong>嵌入式系统</strong>（节省运行时开销、Flash/RAM 布局）至关重要。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Point</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>int</span><span> </span><span>x</span><span>, </span><span>y</span><span>;</span></div></div><div><div><div>3</div></div><div><span>    </span><span>constexpr</span><span> </span><span>Point</span><span>(</span><span>int</span><span><span> </span><span>_x</span><span>, </span></span><span>int</span><span><span> </span><span>_y</span><span>) : </span></span><span>x</span><span>(</span><span>_x</span><span>), </span><span>y</span><span>(</span><span>_y</span><span>) {}</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>// 编译后，p 甚至可能不存在，直接被优化为立即数操作</span></div></div><div><div><div>7</div></div><div><span>constexpr</span><span> </span><span>Point</span><span> </span><span>p</span><span>(</span><span>10</span><span>, </span><span>20</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>逻辑控制与异常处理<a href="#逻辑控制与异常处理"><span>#</span></a></h3><section><h4><code>explicit</code><a href="#explicit"><span>#</span></a></h4><p>在前文已经讲到。同时，除了单参数构造函数，<strong>多参数构造函数</strong>（C++11 列表初始化）也需要注意。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>struct</span><span> </span><span>Vector3</span><span> {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>explicit</span><span> </span><span>Vector3</span><span>(</span><span>float</span><span><span> </span><span>x</span><span>, </span></span><span>float</span><span><span> </span><span>y</span><span>, </span></span><span>float</span><span><span> </span><span>z</span><span>);</span></span></div></div><div><div><div>3</div></div><div><span>};</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>func</span><span>(</span><span>Vector3</span><span><span> </span><span>v</span><span>);</span></span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>func</span><span>({</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>});</span><span> // 错误！因为 explicit 禁止了 {list} -&gt; Object 的隐式类型转换</span></div></div><div><div><div>8</div></div><div><span>func</span><span>(</span><span>Vector3</span><span>{</span><span>1.0</span><span>, </span><span>2.0</span><span>, </span><span>3.0</span><span>});</span><span> // 正确，显式调用</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4><code>try</code>（Function-try block）<a href="#tryfunction-try-block"><span>#</span></a></h4><ul>
<li>语义：在进入内部前就能捕获错误</li>
</ul><p>构造函数分两步：初始化列表 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 函数体。如果在<strong>初始化列表</strong>阶段（比如基类构造、成员对象构造）抛出了异常，普通的 <code>try-catch</code> 包裹函数体是<strong>抓不住</strong>的。必须把 <code>try</code> 写在函数体外，这就是<strong>函数 try 块</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ResourceManager</span><span>() </span><span>try</span><span> : </span><span>core_resource</span><span>(</span><span>new</span><span> </span><span>core</span><span>) {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>// ... 函数体</span></div></div><div><div><div>3</div></div><div><span>} </span><span>catch</span><span> (...) {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>// 能够捕获 core_resource 初始化时抛出的异常</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>// 注意：构造函数里的 catch 必定会再次抛出异常，因为对象构造函数失败了，必须通知外界</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/list-in-cpp/</id>
      <title type="text">C++ 中的 list</title>
      <published>2026-01-03T13:05:00.000Z</published>
      <updated>2026-10-05T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/list-in-cpp/"/>
      <summary type="text">为了搞清楚 std::list 在今天还值不值得用，我写了一套可复现的微基准，把 std::list 和 std::vector 放在同一台机器上逐项对比，并附上完整测试代码与参考资料。</summary>
      <content type="html"><![CDATA[<p>我经常被问到一个问题：都这个年代了，<code>std::list</code> 到底还有没有用？答案通常分成两派，一派说链表早就被缓存的现实淘汰了，另一派说头插和 splice 无可替代。我不想继续背结论，就在自己机器上写了一套微基准，把两个容器拉到一起正面打了一架。下面每个数字都是真跑出来的，完整代码和参考资料都附在文里。</p>
<section><h2>先把内存模型说清楚<a href="#先把内存模型说清楚"><span>#</span></a></h2><p>先看一张图，这是理解后面一切的前提：</p><div><div><div><div><span><p>next</p></span></div><div><span><p>next</p></span></div><div><span><p>next</p></span></div><div><span><p>prev</p></span></div><div><span><p>prev</p></span></div><div><span><p>prev</p></span></div><div><span><p>节点 1<br /><br />prev = null</p></span></div><div><span><p>节点 2</p></span></div><div><span><p>节点 3</p></span></div><div><span><p>节点 4<br /><br />next = null</p></span></div>
</div><div><div><span><p>next</p></span></div><div><span><p>next</p></span></div><div><span><p>next</p></span></div><div><span><p>prev</p></span></div><div><span><p>prev</p></span></div><div><span><p>prev</p></span></div><div><span><p>节点 1<br /><br />prev = null</p></span></div><div><span><p>节点 2</p></span></div><div><span><p>节点 3</p></span></div><div><span><p>节点 4<br /><br />next = null</p></span></div>
</div></div></div><p><code>std::list</code> 是双向链表。每个元素都独立分配在堆上，节点之间靠 <code>prev</code> 和 <code>next</code> 两个指针串起来。这带来两个直接后果：</p><ul>
<li><strong>内存不连续</strong>。第 <span><span>ii</span><span><span><span></span><span>i</span></span></span></span> 个元素和第 <span><span>i+1i+1</span><span><span><span></span><span>i</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 个元素在地址空间里可能隔得很远。</li>
<li><strong>没有随机访问</strong>。不能写 <code>l[5]</code>，因为没有任何办法从头部一步跳到第 5 个节点。想找它，只能从 <code>begin()</code> 开始一个一个 <code>++</code> 过去。</li>
</ul><p>打个比方。<code>std::vector</code> 像在一整排连续车位上停车，想找第 10 辆车，走过去就行；<code>std::list</code> 像把车停在城市里各个独立的停车场，每辆车上贴着两张便签，写着上一辆和下一辆在哪。想找第 10 辆，只能从第 1 辆开始，沿着便签一辆一辆跳过去。</p><p>这个”跳”的动作，就是后面所有性能故事的主角。</p></section>
<section><h2>初始化<a href="#初始化"><span>#</span></a></h2><p>用法上和 <code>std::vector</code> 几乎长得一样，没有学习成本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;list&gt;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>std::</span><span>list</span><span>&lt;int&gt;</span><span> </span><span>l1</span><span>;</span><span>                        // 空链表</span></div></div><div><div><div>4</div></div><div><span>std::</span><span>list</span><span>&lt;int&gt;</span><span> </span><span>l2</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>};</span><span>            // 列表初始化</span></div></div><div><div><div>5</div></div><div><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l3</span><span>(</span><span>l2</span><span>);</span><span>                    // 拷贝构造，O(N)</span></div></div><div><div><div>6</div></div><div><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l4</span><span>(</span><span>5</span><span>, </span><span>100</span><span>);</span><span>                // 5 个元素，全是 100</span></div></div><div><div><div>7</div></div><div><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l5</span><span>(</span><span>l2</span><span>.</span><span>begin</span><span>(), </span><span>l2</span><span>.</span><span>end</span><span>());</span><span>  // 从迭代器区间构造</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>基准是怎么测的<a href="#基准是怎么测的"><span>#</span></a></h2><p>一台机器、一个编译器、一套代码。环境如下：</p><ul>
<li>CPU：AMD Ryzen 7 PRO 6850U（Zen 3+，16 MB L3）</li>
<li>编译器：GCC 15.3.0，<code>-O2 -std=c++20</code>，<code>-Wall -Wextra</code> 无警告</li>
<li>系统：Linux</li>
<li>计时：<code>std::chrono::steady_clock</code> 的墙钟时间</li>
<li>内存：读取本进程的 <code>/proc/self/statm</code> 常驻集增量</li>
</ul><p>为了不让读者只看到结论，完整源码如下。它是一套独立的微基准，可以直接编译运行复现：</p><div><div><div><figure><figcaption><span>list_vs_vector_benchmark.cpp</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>//</span></div></div><div><div><div>2</div></div><div><span>// Microbenchmarks for the operations that decide whether std::list or</span></div></div><div><div><div>3</div></div><div><span>// std::vector is the right container. Build with:</span></div></div><div><div><div>4</div></div><div><span>//</span></div></div><div><div><div>5</div></div><div><span>//   g++ -O2 -std=c++20 -o list_vs_vector_benchmark</span></div></div><div><div><div>6</div></div><div><span>//       list_vs_vector_benchmark.cpp</span></div></div><div><div><div>7</div></div><div><span>//</span></div></div><div><div><div>8</div></div><div><span>// Every timing is wall-clock milliseconds and, except for the traversal</span></div></div><div><div><div>9</div></div><div><span>// benchmark (best of 5), comes from a single run. Memory is the</span></div></div><div><div><div>10</div></div><div><span>// resident-set delta of this process, read from /proc/self/statm (Linux).</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>#include</span><span> </span><span>&lt;algorithm&gt;</span></div></div><div><div><div>13</div></div><div><span>#include</span><span> </span><span>&lt;chrono&gt;</span></div></div><div><div><div>14</div></div><div><span>#include</span><span> </span><span>&lt;cstdint&gt;</span></div></div><div><div><div>15</div></div><div><span>#include</span><span> </span><span>&lt;fstream&gt;</span></div></div><div><div><div>16</div></div><div><span>#include</span><span> </span><span>&lt;iomanip&gt;</span></div></div><div><div><div>17</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>18</div></div><div><span>#include</span><span> </span><span>&lt;iterator&gt;</span></div></div><div><div><div>19</div></div><div><span>#include</span><span> </span><span>&lt;list&gt;</span></div></div><div><div><div>20</div></div><div><span>#include</span><span> </span><span>&lt;numeric&gt;</span></div></div><div><div><div>21</div></div><div><span>#include</span><span> </span><span>&lt;random&gt;</span></div></div><div><div><div>22</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>namespace</span><span> {</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>using</span><span> </span><span>Clock</span><span> </span><span>=</span><span> std::chrono::</span><span>steady_clock</span><span>;</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kTraverseN</span><span> </span><span>=</span><span> </span><span>10'000'000</span><span>;</span></div></div><div><div><div>29</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kMiddleN</span><span> </span><span>=</span><span> </span><span>200'000</span><span>;</span></div></div><div><div><div>30</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kMiddleInserts</span><span> </span><span>=</span><span> </span><span>1'000</span><span>;</span></div></div><div><div><div>31</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kFrontN</span><span> </span><span>=</span><span> </span><span>100'000</span><span>;</span></div></div><div><div><div>32</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kMoveN</span><span> </span><span>=</span><span> </span><span>1'000'000</span><span>;</span></div></div><div><div><div>33</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kSortN</span><span> </span><span>=</span><span> </span><span>1'000'000</span><span>;</span></div></div><div><div><div>34</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kRandomN</span><span> </span><span>=</span><span> </span><span>1'000'000</span><span>;</span></div></div><div><div><div>35</div></div><div><span>constexpr</span><span> </span><span>int</span><span> </span><span>kRandomInserts</span><span> </span><span>=</span><span> </span><span>2'000</span><span>;</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>// Accumulates loop results so the optimizer cannot delete the loops.</span></div></div><div><div><div>38</div></div><div><span>std::</span><span>int64_t</span><span> </span><span>g_sink</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>// Wall-clock milliseconds between two time points.</span></div></div><div><div><div>41</div></div><div><span>double</span><span> </span><span>ElapsedMs</span><span>(Clock::</span><span>time_point</span><span><span> </span><span>start</span><span>, Clock::</span></span><span>time_point</span><span><span> </span><span>end</span><span>) {</span></span></div></div><div><div><div>42</div></div><div><span>  </span><span>return</span><span><span> std::</span><span>chrono</span><span>::</span></span><span>duration</span><span>&lt;</span><span>double</span><span>, std::</span><span>milli</span><span>&gt;(</span><span>end</span><span> </span><span>-</span><span> </span><span>start</span><span>).</span><span>count</span><span>();</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>// Resident set size of this process, in KiB.</span></div></div><div><div><div>46</div></div><div><span>long</span><span> </span><span>ResidentKib</span><span>() {</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>std::</span><span>ifstream</span><span> </span><span>statm</span><span>(</span><span>"/proc/self/statm"</span><span>);</span></div></div><div><div><div>48</div></div><div><span>  </span><span>long</span><span> </span><span>total_pages</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>49</div></div><div><span>  </span><span>long</span><span> </span><span>resident_pages</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>50</div></div><div><span>  </span><span>statm</span><span> </span><span>&gt;&gt;</span><span> </span><span>total_pages</span><span> </span><span>&gt;&gt;</span><span> </span><span>resident_pages</span><span>;</span></div></div><div><div><div>51</div></div><div><span>  </span><span>constexpr</span><span> </span><span>long</span><span> </span><span>kPageKib</span><span> </span><span>=</span><span> </span><span>4</span><span>;</span><span>  // 4096-byte pages.</span></div></div><div><div><div>52</div></div><div><span>  </span><span>return</span><span> </span><span>resident_pages</span><span> </span><span>*</span><span> </span><span>kPageKib</span><span>;</span></div></div><div><div><div>53</div></div><div><span>}</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>// Sums a container `reps` times and returns the fastest run in milliseconds.</span></div></div><div><div><div>56</div></div><div><span>template</span><span> &lt;</span><span>typename</span><span> </span><span>Container</span><span>&gt;</span></div></div><div><div><div>57</div></div><div><span>double</span><span> </span><span>FastestSumMs</span><span>(</span><span>const</span><span> </span><span>Container</span><span>&amp;</span><span><span> </span><span>container</span><span>, </span></span><span>int</span><span><span> </span><span>reps</span><span>) {</span></span></div></div><div><div><div>58</div></div><div><span>  </span><span>double</span><span> </span><span>best</span><span> </span><span>=</span><span> </span><span><span>1</span><span>e</span><span>18</span></span><span>;</span></div></div><div><div><div>59</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>reps</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>60</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>61</div></div><div><span><span>    </span></span><span>std::</span><span>int64_t</span><span> </span><span>sum</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>62</div></div><div><span>    </span><span>for</span><span> (</span><span>const</span><span> </span><span>auto&amp;</span><span> </span><span>value</span><span> : </span><span>container</span><span>) </span><span>sum</span><span> </span><span>+=</span><span> </span><span>value</span><span>;</span></div></div><div><div><div>63</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>64</div></div><div><span>    </span><span>g_sink</span><span> </span><span>+=</span><span> </span><span>sum</span><span>;</span></div></div><div><div><div>65</div></div><div><span>    </span><span>best</span><span> </span><span>=</span><span> std::</span><span>min</span><span>(</span><span>best</span><span>, </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>));</span></div></div><div><div><div>66</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>67</div></div><div><span>  </span><span>return</span><span> </span><span>best</span><span>;</span></div></div><div><div><div>68</div></div><div><span>}</span></div></div><div><div><div>69</div></div><div>
</div></div><div><div><div>70</div></div><div><span>}</span><span>  // namespace</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>73</div></div><div><span><span>  </span></span><span>std::</span><span>mt19937_64</span><span> </span><span>rng</span><span>(</span><span>1234</span><span>);</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>data</span><span>(</span><span>kTraverseN</span><span>);</span></div></div><div><div><div>76</div></div><div><span>  </span><span>for</span><span> (</span><span>auto&amp;</span><span> </span><span>value</span><span> : </span><span>data</span><span>) </span><span>value</span><span> </span><span>=</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>rng</span><span>() </span><span>&amp;</span><span> </span><span><span>0x</span><span>FFFF</span></span><span>);</span></div></div><div><div><div>77</div></div><div><span>  </span><span>const</span><span> std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>data_list</span><span>(</span><span>data</span><span>.</span><span>begin</span><span>(), </span><span>data</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>78</div></div><div>
</div></div><div><div><div>79</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>values</span><span>(</span><span>kRandomN</span><span>);</span></div></div><div><div><div>80</div></div><div><span>  </span><span>for</span><span> (</span><span>auto&amp;</span><span> </span><span>value</span><span> : </span><span>values</span><span>) </span><span>value</span><span> </span><span>=</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>rng</span><span>() </span><span>&amp;</span><span> </span><span><span>0x</span><span>FFFF</span></span><span>);</span></div></div><div><div><div>81</div></div><div>
</div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>fixed</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>setprecision</span><span>(</span><span>2</span><span>);</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"=== A. traverse / sum "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kTraverseN</span></div></div><div><div><div>85</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" ints (best of 5) ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>86</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>FastestSumMs</span><span>(</span><span>data</span><span>, </span><span>5</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>87</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list:   "</span><span> </span><span>&lt;&lt;</span><span> </span><span>FastestSumMs</span><span>(</span><span>data_list</span><span>, </span><span>5</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>88</div></div><div>
</div></div><div><div><div>89</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>=== B. insert "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kMiddleInserts</span></div></div><div><div><div>90</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" at the middle, N = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kMiddleN</span><span> </span><span>&lt;&lt;</span><span> </span><span>" ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>91</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v</span><span>(</span><span>kMiddleN</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>93</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>94</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kMiddleInserts</span><span>; </span><span>++</span><span>i</span><span>)</span></div></div><div><div><div>95</div></div><div><span>      </span><span>v</span><span>.</span><span>insert</span><span>(</span><span>v</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>v</span><span>.</span><span>size</span><span>() </span><span>/</span><span> </span><span>2</span><span>, </span><span>i</span><span>);</span></div></div><div><div><div>96</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>97</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>98</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>99</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>100</div></div><div><span><span>    </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l</span><span>(</span><span>kMiddleN</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>101</div></div><div><span>    </span><span>auto</span><span> </span><span>mid</span><span> </span><span>=</span><span> std::</span><span>next</span><span>(</span><span>l</span><span>.</span><span>begin</span><span>(), </span><span>kMiddleN</span><span> </span><span>/</span><span> </span><span>2</span><span>);</span></div></div><div><div><div>102</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>103</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kMiddleInserts</span><span>; </span><span>++</span><span>i</span><span>) </span><span>l</span><span>.</span><span>insert</span><span>(</span><span>mid</span><span>, </span><span>i</span><span>);</span></div></div><div><div><div>104</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>105</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list:   "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>106</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>107</div></div><div>
</div></div><div><div><div>108</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>=== C. "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kFrontN</span><span> </span><span>&lt;&lt;</span><span> </span><span>" front insertions ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>109</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>110</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v</span><span>;</span></div></div><div><div><div>111</div></div><div><span>    </span><span>v</span><span>.</span><span>reserve</span><span>(</span><span>kFrontN</span><span>);</span></div></div><div><div><div>112</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>113</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kFrontN</span><span>; </span><span>++</span><span>i</span><span>) </span><span>v</span><span>.</span><span>insert</span><span>(</span><span>v</span><span>.</span><span>begin</span><span>(), </span><span>i</span><span>);</span></div></div><div><div><div>114</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>115</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector insert(begin): "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>116</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>117</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>118</div></div><div><span><span>    </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l</span><span>;</span></div></div><div><div><div>119</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>120</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kFrontN</span><span>; </span><span>++</span><span>i</span><span>) </span><span>l</span><span>.</span><span>push_front</span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>121</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>122</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list   push_front:    "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>123</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>124</div></div><div>
</div></div><div><div><div>125</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>=== D. move "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kMoveN</span></div></div><div><div><div>126</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" ints into another container (us) ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>127</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>128</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>a</span><span>(</span><span>1000</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>129</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>b</span><span>(</span><span>kMoveN</span><span>, </span><span>9</span><span>);</span></div></div><div><div><div>130</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>131</div></div><div><span>    </span><span>a</span><span>.</span><span>insert</span><span>(</span><span>a</span><span>.</span><span>begin</span><span>(), </span><span>b</span><span>.</span><span>begin</span><span>(), </span><span>b</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>132</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>133</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector insert(range): "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>*</span><span> </span><span>1000</span></div></div><div><div><div>134</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>" us</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>135</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>136</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>137</div></div><div><span><span>    </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>a</span><span>(</span><span>1000</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>138</div></div><div><span><span>    </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>b</span><span>(</span><span>kMoveN</span><span>, </span><span>9</span><span>);</span></div></div><div><div><div>139</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>140</div></div><div><span>    </span><span>a</span><span>.</span><span>splice</span><span>(</span><span>a</span><span>.</span><span>begin</span><span>(), </span><span>b</span><span>);</span></div></div><div><div><div>141</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>142</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list   splice:        "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>*</span><span> </span><span>1000</span></div></div><div><div><div>143</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>" us (b.size()="</span><span> </span><span>&lt;&lt;</span><span> </span><span>b</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>")</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>144</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>145</div></div><div>
</div></div><div><div><div>146</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>=== E. sort "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kSortN</span><span> </span><span>&lt;&lt;</span><span> </span><span>" random ints ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>147</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>148</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v</span><span>(</span><span>values</span><span>);</span></div></div><div><div><div>149</div></div><div><span><span>    </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l</span><span>(</span><span>values</span><span>.</span><span>begin</span><span>(), </span><span>values</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>150</div></div><div><span>    </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>151</div></div><div><span><span>    </span></span><span>std::</span><span>sort</span><span>(</span><span>v</span><span>.</span><span>begin</span><span>(), </span><span>v</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>152</div></div><div><span>    </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>153</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector std::sort: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>154</div></div><div><span>    </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>155</div></div><div><span>    </span><span>l</span><span>.</span><span>sort</span><span>();</span></div></div><div><div><div>156</div></div><div><span>    </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>157</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list   l.sort():  "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>158</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>159</div></div><div>
</div></div><div><div><div>160</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>=== F. resident memory for "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kTraverseN</span></div></div><div><div><div>161</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" ints (KiB) ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>162</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>163</div></div><div><span>    </span><span>const</span><span> </span><span>long</span><span> </span><span>base</span><span> </span><span>=</span><span> </span><span>ResidentKib</span><span>();</span></div></div><div><div><div>164</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>165</div></div><div><span><span>      </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>big</span><span>(</span><span>kTraverseN</span><span>);</span></div></div><div><div><div>166</div></div><div><span><span>      </span></span><span>std::</span><span>iota</span><span>(</span><span>big</span><span>.</span><span>begin</span><span>(), </span><span>big</span><span>.</span><span>end</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>167</div></div><div><span><span>      </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector: +"</span><span> </span><span>&lt;&lt;</span><span> </span><span>ResidentKib</span><span>() </span><span>-</span><span> </span><span>base</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>168</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>169</div></div><div><span><span>    </span></span><span>{</span></div></div><div><div><div>170</div></div><div><span><span>      </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>big</span><span>(</span><span>kTraverseN</span><span>);</span></div></div><div><div><div>171</div></div><div><span><span>      </span></span><span>std::</span><span>iota</span><span>(</span><span>big</span><span>.</span><span>begin</span><span>(), </span><span>big</span><span>.</span><span>end</span><span>(), </span><span>0</span><span>);</span></div></div><div><div><div>172</div></div><div><span><span>      </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list:   +"</span><span> </span><span>&lt;&lt;</span><span> </span><span>ResidentKib</span><span>() </span><span>-</span><span> </span><span>base</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>173</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>174</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>175</div></div><div>
</div></div><div><div><div>176</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>=== G. insert "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kRandomInserts</span></div></div><div><div><div>177</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" at random positions, N = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kRandomN</span><span> </span><span>&lt;&lt;</span><span> </span><span>" ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>178</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>179</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v</span><span>(</span><span>values</span><span>);</span></div></div><div><div><div>180</div></div><div><span><span>    </span></span><span>std::</span><span>uniform_int_distribution</span><span>&lt;</span><span>int</span><span>&gt; </span><span>dist</span><span>(</span><span>0</span><span>, </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>v</span><span>.</span><span>size</span><span>()) </span><span>-</span><span> </span><span>1</span><span>);</span></div></div><div><div><div>181</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>182</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kRandomInserts</span><span>; </span><span>++</span><span>i</span><span>) </span><span>v</span><span>.</span><span>insert</span><span>(</span><span>v</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>dist</span><span>(</span><span>rng</span><span>), </span><span>i</span><span>);</span></div></div><div><div><div>183</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>184</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>185</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>186</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>187</div></div><div><span><span>    </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l</span><span>(</span><span>values</span><span>.</span><span>begin</span><span>(), </span><span>values</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>188</div></div><div><span><span>    </span></span><span>std::</span><span>uniform_int_distribution</span><span>&lt;</span><span>int</span><span>&gt; </span><span>dist</span><span>(</span><span>0</span><span>, </span><span>kRandomN</span><span> </span><span>-</span><span> </span><span>1</span><span>);</span></div></div><div><div><div>189</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>190</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kRandomInserts</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>191</div></div><div><span>      </span><span>auto</span><span> </span><span>it</span><span> </span><span>=</span><span> std::</span><span>next</span><span>(</span><span>l</span><span>.</span><span>begin</span><span>(), </span><span>dist</span><span>(</span><span>rng</span><span>));</span></div></div><div><div><div>192</div></div><div><span>      </span><span>l</span><span>.</span><span>insert</span><span>(</span><span>it</span><span>, </span><span>i</span><span>);</span></div></div><div><div><div>193</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>194</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>195</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list:   "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ms</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>196</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>197</div></div><div>
</div></div><div><div><div>198</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>=== H. erase every even element, N = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kRandomN</span></div></div><div><div><div>199</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" ===</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>200</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>201</div></div><div><span><span>    </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>v</span><span>(</span><span>values</span><span>);</span></div></div><div><div><div>202</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>203</div></div><div><span>    </span><span>v</span><span>.</span><span>erase</span><span>(</span></div></div><div><div><div>204</div></div><div><span><span>        </span></span><span>std::</span><span>remove_if</span><span>(</span><span>v</span><span>.</span><span>begin</span><span>(), </span><span>v</span><span>.</span><span>end</span><span>(), [](</span><span>int</span><span><span> </span><span>x</span><span>) { </span></span><span>return</span><span> (</span><span>x</span><span> </span><span>&amp;</span><span> </span><span>1</span><span>) </span><span>==</span><span> </span><span>0</span><span>; }),</span></div></div><div><div><div>205</div></div><div><span>        </span><span>v</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>206</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>207</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector erase-remove: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>)</span></div></div><div><div><div>208</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>" ms (size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>v</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>")</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>209</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>210</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>211</div></div><div><span><span>    </span></span><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>l</span><span>(</span><span>values</span><span>.</span><span>begin</span><span>(), </span><span>values</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>212</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>213</div></div><div><span>    </span><span>l</span><span>.</span><span>remove_if</span><span>([](</span><span>int</span><span><span> </span><span>x</span><span>) { </span></span><span>return</span><span> (</span><span>x</span><span> </span><span>&amp;</span><span> </span><span>1</span><span>) </span><span>==</span><span> </span><span>0</span><span>; });</span></div></div><div><div><div>214</div></div><div><span>    </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span> Clock::</span><span>now</span><span>();</span></div></div><div><div><div>215</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"list   remove_if:    "</span><span> </span><span>&lt;&lt;</span><span> </span><span>ElapsedMs</span><span>(</span><span>start</span><span>, </span><span>end</span><span>)</span></div></div><div><div><div>216</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>" ms (size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>l</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>")</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>217</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>218</div></div><div>
</div></div><div><div><div>219</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>checksum: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>g_sink</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>220</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>221</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>代码遵循 <a href="https://google.github.io/styleguide/cppguide.html" target="_blank">Google C++ Style Guide</a>：2 空格缩进、<code>PascalCase</code> 函数名、<code>k</code> 前缀常量、<code>snake_case</code> 变量、80 列换行。基准里的累计和 <code>g_sink</code> 会在最后打印（<code>checksum: 3276322411190</code>），用来防止编译器把整个循环优化掉。</p></section>
<section><h2>结果<a href="#结果"><span>#</span></a></h2><p>下面是一次代表性运行的输出。整段程序跑了三遍，量级关系稳定；抖动最大的是 <code>list::sort</code> 和随机位置插入这两项，我会在对应小节注明。</p>

<table><thead><tr><th>场景</th><th><code>std::vector</code></th><th><code>std::list</code></th><th>差距</th></tr></thead><tbody><tr><td>A. 遍历求和 1000 万 <code>int</code>（best of 5）</td><td>2.53 ms</td><td>24.27 ms</td><td>vector 快 <strong>9.6 倍</strong></td></tr><tr><td>B. 中间插入 1000 次（N = 20 万，已持迭代器）</td><td>5.17 ms</td><td>0.03 ms</td><td>list 快约 <strong>170 倍</strong></td></tr><tr><td>C. 头部插入 10 万次</td><td>227.20 ms</td><td>2.87 ms</td><td>list 快约 <strong>79 倍</strong></td></tr><tr><td>D. 搬运 100 万个元素</td><td>2309.66 µs</td><td>5.39 µs</td><td>list 快约 <strong>429 倍</strong></td></tr><tr><td>E. 排序 100 万随机 <code>int</code></td><td>45.13 ms</td><td>318.73 ms</td><td>vector 快约 <strong>7.1 倍</strong></td></tr><tr><td>F. 常驻内存（1000 万 <code>int</code>）</td><td>+39,064 KiB</td><td>+294,340 KiB</td><td>list 多 <strong>7.5 倍</strong></td></tr><tr><td>G. 随机位置插入 2000 次（N = 100 万，每次定位）</td><td>79.77 ms</td><td>2348.11 ms</td><td>vector 快约 <strong>29 倍</strong></td></tr><tr><td>H. 删除全部偶数元素（N = 100 万）</td><td>2.86 ms</td><td>9.21 ms</td><td>vector 快约 <strong>3.2 倍</strong></td></tr></tbody></table><p>有意思的地方全在这些数字的裂缝里，我们一个个看。</p><section><h3>A. 遍历：连续内存的碾压<a href="#a-遍历连续内存的碾压"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>int64_t</span><span> </span><span>sum</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>2</div></div><div><span>for</span><span> (</span><span>const</span><span> </span><span>auto&amp;</span><span> </span><span>value</span><span> : </span><span>container</span><span>) </span><span>sum</span><span> </span><span>+=</span><span> </span><span>value</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>1000 万个 <code>int</code>，<code>vector</code> 2.53 ms，<code>list</code> 24.27 ms，<strong>9.6 倍</strong>的差距。</p><p>原因很朴素。现代 CPU 从内存取数据不是一次取 4 字节，而是按 64 字节的 cache line 整块搬。<code>vector</code> 的数据挨在一起，拿一个 <code>int</code> 顺带把后面 15 个也塞进了缓存，后续访问全是缓存命中。<code>list</code> 每访问一个节点，都要从一个陌生的地址拉回一整条 cache line，却只用掉其中的 4 字节数据加两个指针。9.6 倍这个数字，基本就是”有用字节占总搬运字节的比例”的倒数。</p></section><section><h3>B. 中间插入：真相取决于你要不要先找到那个位置<a href="#b-中间插入真相取决于你要不要先找到那个位置"><span>#</span></a></h3><p>这是全场最有教育意义的一组对比。</p><p><strong>情况一：已经手握迭代器，原地插入 1000 次。</strong> <code>vector</code> 5.17 ms，<code>list</code> 0.03 ms，<code>list</code> 快了约 <strong>170 倍</strong>。因为 <code>list</code> 每次插入是纯指针操作，<span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>；而 <code>vector</code> 每插一次，位置后面的所有元素都得搬一次家。</p><p><strong>情况二：每次都要先从头定位再插入（见 G 节）。</strong> 同样是在中间插 2000 个元素，<code>vector</code> 79.77 ms，<code>list</code> 冲到 <strong>2348.11 ms</strong>，反被 <code>vector</code> 快了约 <strong>29 倍</strong>。</p><p>同一个容器，同一个”在中间插入”的说法，结论直接反转。差别在于那一步定位：</p><ul>
<li><code>list</code> 定位要 <code>std::next(l.begin(), i)</code>，这是一次纯粹的指针追逐，每跳一步都可能是一次 cache miss，<span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span> 且常数巨大。</li>
<li><code>vector</code> 定位就是 <code>v.begin() + i</code>，一次加法，然后 <code>memmove</code> 连续搬数据，虽然也是 <span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>，但常数小得多。</li>
</ul><p>所以”list 中间插入快”这句话，条件是你要免费拿到那个迭代器。一旦定位成本算进来，<code>list</code> 经常连本带利亏回去。对小对象和连续内存密集的场景尤其如此，上面这组数字就是证据。</p></section><section><h3>C. 头插：list 这一次赢得很干脆<a href="#c-头插list-这一次赢得很干脆"><span>#</span></a></h3><p>10 万次头插，<code>vector</code> 用了 227.20 ms，<code>list</code> 只用 2.87 ms，差了约 <strong>79 倍</strong>。</p><p><code>vector</code> 根本没有 <code>push_front</code>，只能写 <code>v.insert(v.begin(), x)</code>。而每次往头部插一个元素，都要把后面所有元素整体往后挪一格。10 万次下来就是 <span><span>∑i=1100000i≈5×109\sum_{i=1}^{100000} i \approx 5 \times 10^9</span><span><span><span></span><span><span>∑</span><span><span><span><span><span><span></span><span><span><span>i</span><span>=</span><span>1</span></span></span></span><span><span></span><span><span><span>100000</span></span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span></span><span></span><span>i</span><span></span><span>≈</span><span></span></span><span><span></span><span>5</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>9</span></span></span></span></span></span></span></span></span></span></span> 次移动，即 <span><span>O(N2)O(N^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>N</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。</p><p><code>list</code> 的 <code>push_front</code> 只是分配一个节点、改两个指针，是货真价实的 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>，总共 <span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>。</p><p>顺带说一句，需要频繁在两端增删时，<code>std::deque</code> 通常是比 <code>list</code> 更聪明的选择（它有 <code>push_front</code>，且内存连续性更好），不过这是另一篇文章的题了。</p></section><section><h3>D. splice：<code>std::vector</code> 根本做不到的事<a href="#d-splicestdvector-根本做不到的事"><span>#</span></a></h3><p><code>splice</code> 是 <code>list</code> 的杀手锏。它把一个链表的一段，直接”剪”下来”粘”到另一个链表上，<strong>不拷贝任何数据，只改指针</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>a</span><span>(</span><span>1000</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>list</span><span>&lt;</span><span>int</span><span>&gt; </span><span>b</span><span>(</span><span>1'000'000</span><span>, </span><span>9</span><span>);</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// 把 b 的全部元素搬到 a 的头部，b 随后变空</span></div></div><div><div><div>5</div></div><div><span>a</span><span>.</span><span>splice</span><span>(</span><span>a</span><span>.</span><span>begin</span><span>(), </span><span>b</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>100 万个元素，<code>vector</code> 用区间插入要 2309.66 µs（要逐个拷贝并整体后移），<code>list</code> 的 <code>splice</code> 只用 <strong>5.39 µs</strong>，约 <strong>429 倍</strong>的差距。更重要的是，这个差距会随元素数量线性扩大：<code>splice</code> 的复杂度是 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>，和搬多少元素无关。</p><p><code>vector</code> 想完成同样的语义，只能拷贝加删除。这是量级上的差别，不是常数上的差别。如果某个设计里”把一段序列从一个容器挪到另一个容器”是核心操作，那这里基本就是 <code>list</code> 的专属地盘。</p><p><code>splice</code> 的几个重载都值得记住：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>l1</span><span>.</span><span>splice</span><span>(</span><span>pos</span><span>, </span><span>l2</span><span>);</span><span>               // 把 l2 全部搬到 pos 前</span></div></div><div><div><div>2</div></div><div><span>l1</span><span>.</span><span>splice</span><span>(</span><span>pos</span><span>, </span><span>l2</span><span>, </span><span>it</span><span>);</span><span>           // 只搬 l2 中 it 指向的那个元素</span></div></div><div><div><div>3</div></div><div><span>l1</span><span>.</span><span>splice</span><span>(</span><span>pos</span><span>, </span><span>l2</span><span>, </span><span>first</span><span>, </span><span>last</span><span>);</span><span>  // 搬 l2 中 [first, last) 这一段</span></div></div><div><div><div>4</div></div><div><span>l1</span><span>.</span><span>splice</span><span>(</span><span>pos</span><span>, </span><span>l1</span><span>, </span><span>it</span><span>);</span><span>           // 同一条链内部重排，把 it 搬到 pos 前</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>E. 排序：自带 <code>sort</code> 不代表更快<a href="#e-排序自带-sort-不代表更快"><span>#</span></a></h3><p>标准库的 <code>std::sort</code> 要求随机访问迭代器，<code>list</code> 用不了，所以它自带了一个 <code>l.sort()</code>（底层通常是自底向上的归并排序，稳定）。</p><p>100 万个随机 <code>int</code>：<code>vector</code> 的 <code>std::sort</code> 45.13 ms，<code>list::sort</code> 318.73 ms，<code>vector</code> 快了约 <strong>7 倍</strong>。这一项抖动比较大，我实测三次 <code>list::sort</code> 落在 318 到 376 ms 之间，<code>vector</code> 稳定在 44 到 45 ms，所以结论方向不变。</p><p>归并排序在链表上是 <span><span>O(Nlog⁡N)O(N \log N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span></span><span>lo<span>g</span></span><span></span><span>N</span><span>)</span></span></span></span> 次指针操作，理论上和数组版本同阶，但每一次指针跳转都是一次潜在的 cache miss。差距又回到了缓存。</p><div><div><div></div><div>别对 list 用 std::sort</div></div><div><p>如果你不小心写了 <code>std::sort(l.begin(), l.end())</code>，这不是运行时报错，而是直接<strong>编译失败</strong>。GCC 15 的报错长这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: no match for 'operator-'</span></div></div><div><div><div>2</div></div><div><span>(operand types are 'std::_List_iterator&lt;int&gt;' and 'std::_List_iterator&lt;int&gt;')</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>因为 <code>std::sort</code> 内部要算区间中点，需要 <code>__last - __first</code>，而双向迭代器不支持减法。这个报错其实是在提醒你：排序请用成员函数 <code>l.sort()</code>。</p></div></div></section><section><h3>F. 内存：每个元素都在为两个指针付房租<a href="#f-内存每个元素都在为两个指针付房租"><span>#</span></a></h3><p>1000 万个 <code>int</code>，<code>vector</code> 常驻内存增加 39,064 KiB（约 38 MiB，基本就是 <span><span>107×410^7 \times 4</span><span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span><span></span><span>×</span><span></span></span><span><span></span><span>4</span></span></span></span> 字节），<code>list</code> 增加了 <strong>294,340 KiB</strong>（约 287 MiB），约 <strong>7.5 倍</strong>。</p><p>算一下账：<code>list</code> 每个节点的载荷只有 4 字节的 <code>int</code>，但节点结构里还有两个 8 字节指针，加上分配器的对齐和元数据，实测平均每个元素约 30 字节。也就是说，<strong>数据本身只占了总内存的七分之一左右</strong>，其余全花在了”怎么找到下一个”这件事上。</p></section><section><h3>G. 随机位置插入：定位成本会反杀<a href="#g-随机位置插入定位成本会反杀"><span>#</span></a></h3><p>前面 B 节说 <code>list</code> 原地插入快 170 倍，这里补上定位成本。在 100 万元的容器里插 2000 个元素，每次都先从头走到随机位置：</p><ul>
<li><code>vector</code>：<code>v.begin() + dist(rng)</code> 是常数时间定位，79.77 ms。</li>
<li><code>list</code>：<code>std::next(l.begin(), dist(rng))</code> 平均要走 50 万步，总计约 <span><span>2×1092 \times 10^9</span><span><span><span></span><span>2</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>9</span></span></span></span></span></span></span></span></span></span></span> 次指针跳转，<strong>2348.11 ms</strong>。</li>
</ul><p><code>vector</code> 反超约 <strong>29 倍</strong>。这一项也抖得厉害，三次实测 <code>list</code> 在 2348 到 2658 ms 之间，<code>vector</code> 稳定在 78 到 80 ms。结论依然是：没有现成迭代器时，别指望 <code>list</code> 的中间插入。</p></section><section><h3>H. 删除：erase-remove 也赢了 <code>remove_if</code><a href="#h-删除erase-remove-也赢了-remove_if"><span>#</span></a></h3><p>删除 100 万个 <code>int</code> 里所有的偶数元素：<code>vector</code> 的 erase-remove 惯用法 2.86 ms，<code>list::remove_if</code> 9.21 ms，<code>vector</code> 快约 <strong>3.2 倍</strong>。</p><p><code>list::remove_if</code> 是 <span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span> 的，每删一个节点还伴随一次释放（回到分配器，可能触发锁或合并不连续的块）。而 <code>vector</code> 的 erase-remove 是一次连续扫描加一次 <code>memmove</code> 收尾，对内存友好太多。</p></section></section>
<section><h2>迭代器：这才是 list 真正难以替代的地方<a href="#迭代器这才是-list-真正难以替代的地方"><span>#</span></a></h2><p>前面全是性能，而 <code>list</code> 真正让它在某些设计里不可替代的，其实是迭代器的<strong>稳定性</strong>。</p><p>先说类型：<code>std::list</code> 提供的是<strong>双向迭代器</strong>（Bidirectional Iterator）。</p><ul>
<li>支持：<code>++it</code>、<code>--it</code>、<code>*it</code>。</li>
<li>不支持：<code>it + 5</code>、<code>it &lt; other_it</code>。不能跳，也不能比大小。</li>
</ul><p>然后是最关键的一条，容器操作对已有迭代器的影响：</p>

<table><thead><tr><th>操作</th><th><code>std::vector</code></th><th><code>std::list</code></th></tr></thead><tbody><tr><td>插入</td><td>可能扩容导致<strong>全部</strong>迭代器失效；中间插入使插入点之后的<strong>全部</strong>失效</td><td><strong>一个都不失效</strong></td></tr><tr><td>删除</td><td>删除点之后的全部失效</td><td><strong>只有</strong>指向被删元素的那个失效</td></tr></tbody></table><p>用一个例子把它钉死：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>list</span><span>&lt;int&gt;</span><span> </span><span>l</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>, </span><span>5</span><span>};</span></div></div><div><div><div>2</div></div><div><span>auto</span><span> </span><span>it</span><span> </span><span>=</span><span> std::</span><span>next</span><span>(</span><span>l</span><span>.</span><span>begin</span><span>(), </span><span>2</span><span>);</span><span>  // 指向 3</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>l</span><span>.</span><span>push_front</span><span>(</span><span>0</span><span>);</span></div></div><div><div><div>5</div></div><div><span>l</span><span>.</span><span>push_back</span><span>(</span><span>6</span><span>);</span></div></div><div><div><div>6</div></div><div><span>l</span><span>.</span><span>remove</span><span>(</span><span>4</span><span>);</span></div></div><div><div><div>7</div></div><div><span>// 以上操作之后 it 依然有效，依然指向 3</span></div></div><div><div><div>8</div></div><div><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>*</span><span>it</span><span> </span><span>&lt;&lt;</span><span> </span><span>'</span><span>\n</span><span>'</span><span>;</span><span>  // 输出 3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>std::vector</code> 做不到这一点。它在扩容时会重新分配整块内存并搬走所有元素，你手里的旧迭代器会指向一片已经被释放的内存，用它就是未定义行为。</p><p>这就引出 <code>list</code> 的一个经典用法：把迭代器当作稳定的”句柄”存起来，指回容器里的某个元素。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>using</span><span> </span><span>Bucket</span><span> </span><span>=</span><span> std::</span><span>list</span><span>&lt;std::</span><span>pair</span><span>&lt;</span><span>int</span><span>, std::</span><span>string</span><span>&gt;&gt;;</span></div></div><div><div><div>2</div></div><div><span>using</span><span> </span><span>Handle</span><span> </span><span>=</span><span> Bucket::</span><span>iterator</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>Bucket</span><span> </span><span>bucket</span><span>;</span></div></div><div><div><div>5</div></div><div><span>std::</span><span>unordered_map</span><span>&lt;int</span><span>, </span><span>Handle</span><span>&gt;</span><span> </span><span>index</span><span>;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>bucket</span><span>.</span><span>push_back</span><span>({</span><span>42</span><span>, </span><span>"answer"</span><span>});</span></div></div><div><div><div>8</div></div><div><span>auto</span><span> </span><span>it</span><span> </span><span>=</span><span> std::</span><span>prev</span><span>(</span><span>bucket</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>9</div></div><div><span>index</span><span>[</span><span>42</span><span>] </span><span>=</span><span> </span><span>it</span><span>;</span><span>  // 存下稳定句柄</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>// 之后无论往 bucket 里怎么插删，index[42] 指向的元素都还在</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你需要这种”外部索引加稳定句柄”的结构，<code>std::list</code> 几乎是标准库里的唯一选择。</p></section>
<section><h2>list 独有的工具箱<a href="#list-独有的工具箱"><span>#</span></a></h2><p>因为无法随机访问，标准库的通用算法（<code>std::sort</code>、<code>std::lower_bound</code> 等）对 <code>list</code> 大多无效。作为补偿，<code>list</code> 自带了一整套成员函数，都针对链表结构优化过：</p><ul>
<li><code>l.push_front(x)</code> / <code>l.pop_front()</code>：头部 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span> 增删（<code>vector</code> 没有）。</li>
<li><code>l.splice(...)</code>：<span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span> 搬家，见上文。</li>
<li><code>l.sort()</code>：<span><span>O(Nlog⁡N)O(N \log N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span></span><span>lo<span>g</span></span><span></span><span>N</span><span>)</span></span></span></span> 稳定排序。想按别的规则排就传比较器，例如 <code>l.sort([](int x, int y) { return x &gt; y; });</code></li>
<li><code>l.merge(other)</code>：合并两个<strong>已经有序</strong>的链表，稳定，且 <code>other</code> 会被搬空（同样是指针操作）。</li>
<li><code>l.remove(x)</code>：删除所有等于 <code>x</code> 的元素，<span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>。</li>
<li><code>l.remove_if(pred)</code>：删除所有满足条件的元素，<span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>。</li>
<li><code>l.unique()</code>：删除<strong>相邻</strong>的重复元素，<span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>。</li>
<li><code>l.reverse()</code>：原地逆置，<span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>，只是把所有 <code>next</code> 和 <code>prev</code> 指针互换一遍。</li>
</ul><p><code>unique</code> 需要特别留意”相邻”两个字：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>list</span><span>&lt;int&gt;</span><span> </span><span>l</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>3</span><span>, </span><span>1</span><span>, </span><span>1</span><span>};</span></div></div><div><div><div>2</div></div><div><span>l</span><span>.</span><span>unique</span><span>();</span><span>  // {1, 2, 3, 1}，只去掉了相邻的重复</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>想去掉全部重复，得先 <code>l.sort()</code> 让相同的元素聚到一起再 <code>unique()</code>。这也是为什么对链表来说，<code>sort + unique</code> 是一个固定搭配。</p></section>
<section><h2>那什么时候该用 list<a href="#那什么时候该用-list"><span>#</span></a></h2><p>综合上面所有数字，我的判断是：</p><ul>
<li><strong>95% 的情况用 <code>std::vector</code>。</strong> 遍历快 9.6 倍，排序快 7 倍，省 7.5 倍内存，删除也更快。这些年我写过的代码里，<code>list</code> 的比例一直在往下走。</li>
<li><strong>需要稳定迭代器 / 外部句柄时，用 <code>std::list</code>。</strong> 这是它最硬的理由，前面那套”map 存迭代器”的结构就是典型。</li>
<li><strong>核心操作是大段搬运（<code>splice</code>）时，用 <code>std::list</code>。</strong> <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span> 对 <span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>，这是量级的差距。</li>
<li><strong>只有头部频繁增删时，先想想 <code>std::deque</code>。</strong> 它也有 <code>push_front</code>，而且内存连续性比 <code>list</code> 好得多，<code>list</code> 在这个场景常常不是最优解。</li>
<li><strong>“元素很大、拷贝很贵”这个理由，现在已经基本失效了。</strong> C++11 之后有了移动语义，<code>vector</code> 搬元素往往只是搬一个指针。</li>
</ul><p>顺便说一句：<code>list</code> 的中间插入快，前提是你<strong>已经拿着迭代器</strong>。要是每次还得从头 <code>std::next</code> 过去定位，G 节那组 29 倍的翻车数据就是下场。</p></section>
<section><h2>补遗：更省内存的 <code>std::forward_list</code><a href="#补遗更省内存的-stdforward_list"><span>#</span></a></h2><p>C++11 引入了 <code>std::forward_list</code>，单向链表。</p><ul>
<li>只有一个 <code>next</code> 指针，没有 <code>prev</code>，所以每个节点比 <code>list</code> 少一个指针。</li>
<li>代价：只能向前遍历，是<strong>前向迭代器</strong>，连 <code>--it</code> 都不支持。</li>
<li>它没有 <code>size()</code>（想要长度得 <code>std::distance</code>，<span><span>O(N)O(N)</span><span><span><span></span><span>O</span><span>(</span><span>N</span><span>)</span></span></span></span>），增删也走 <code>insert_after</code> / <code>erase_after</code> 这种”后一个位置”的接口。</li>
<li>典型场景是极度在意内存的容器内部实现，比如 <code>std::unordered_map</code> 的桶，libstdc++ 就是用单向链表串起来的。</li>
</ul><p>如果 <code>list</code> 的两个指针你都用不上，<code>forward_list</code> 能把每个节点的开销再砍掉一个指针。省内存的路上，永远还有下一个台阶。</p></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>这组基准里最让我意外的一项，是 B 和 G 两个”中间插入”结论的反转。我原本以为 <code>list</code> 在中间插入上是无条件的赢家，结果一旦把定位成本算进去，<code>vector</code> 反杀了 29 倍。直觉和实测之间的这道裂缝，正是写基准的意义所在。</p><p>我猜未来这个天平还会继续往 <code>vector</code> 那一侧倾斜。因为缓存只会越来越重要，而链表的每一次指针跳转，都在和内存墙作对。<code>std::list</code> 不会消失，但它的生存空间会越来越集中在那几个 <code>vector</code> 真的做不到的角落：稳定的迭代器，和 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span> 的 <code>splice</code>。</p><p>想动手的话，可以试试把基准里元素类型从 <code>int</code> 换成 <code>std::string</code> 或者一个 64 字节的自定义结构体，看量级关系会不会变；再试试用 <code>std::deque</code> 替换 C 节的头插，看它和 <code>list</code> 到底谁更快。</p></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://google.github.io/styleguide/cppguide.html" target="_blank">Google C++ Style Guide</a>，本文所有代码遵循的风格。</li>
<li><a href="https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines" target="_blank">C++ Core Guidelines</a>，其中 SL.con.2：Prefer using STL <code>vector</code> by default unless you have a reason to use a different container。</li>
<li><a href="https://en.cppreference.com/w/cpp/container/list" target="_blank">cppreference: std::list</a>，接口、复杂度和迭代器失效规则。</li>
<li><a href="https://en.cppreference.com/w/cpp/container/vector" target="_blank">cppreference: std::vector</a>，容量增长与迭代器失效规则。</li>
<li><a href="https://www.stroustrup.com/bs_faq.html#list" target="_blank">Bjarne Stroustrup, “Are lists evil?”</a>，作者本人对 list 与 vector 之争的回应，结论是默认用连续存储。</li>
<li><a href="https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_list.h" target="_blank">libstdc++ <code>bits/stl_list.h</code></a>，节点布局与 <code>splice</code> 的实现。</li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/vector-in-cpp/</id>
      <title type="text">C++ 中的 vector</title>
      <published>2026-01-03T10:50:00.000Z</published>
      <updated>2026-10-05T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/vector-in-cpp/"/>
      <summary type="text">不再罗列 API，而是把 std::vector 的每条经验都写成能跑的程序量一遍：扩容系数、reserve 的真实收益、emplace_back 到底省了什么、迭代器失效如何复现、erase-remove 的陷阱、std::vector&lt;bool&gt; 的隐藏代价。代码全部遵循 Google C++ Style。</summary>
      <content type="html"><![CDATA[<p><code>std::vector</code> 是我在 C++ 里用得最多的容器，没有之一。但用了很多年之后，我发现自己对它的理解仍然停在两句口号上：<strong>连续内存</strong>、<strong>自动扩容</strong>。口号记住了，可是”扩容到底拷贝了多少个元素""<code>reserve</code> 究竟能省多少时间”这类问题，我从来没真正算过。</p>
<p>所以这篇笔记换了个写法：不再罗列 API，而是写一组能跑的小程序，把每条结论都量一遍。下面的数字都是实机跑出来的：GCC 15.3.0、libstdc++、C++20、x86-64 Linux，计时代码用 <code>-O2</code> 编译，抓 bug 的版本另开 sanitizer。代码为了能直接抄走使用，全部按 Google C++ Style 写。</p>
<section><h2>本质是三个指针，不是”数组”<a href="#本质是三个指针不是数组"><span>#</span></a></h2><p>libstdc++ 里 <code>std::vector&lt;T&gt;</code> 的全部状态就是三个指针。我翻了一下 <code>bits/stl_vector.h</code> 确认：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pointer</span><span> </span><span>_M_start</span><span>;</span><span>           // 首元素</span></div></div><div><div><div>2</div></div><div><span>pointer</span><span> </span><span>_M_finish</span><span>;</span><span>          // 尾后位置，size() 的上界</span></div></div><div><div><div>3</div></div><div><span>pointer</span><span> </span><span>_M_end_of_storage</span><span>;</span><span>  // 已分配内存的尾后位置，capacity() 的上界</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>于是 <code>size() == _M_finish - _M_start</code>，<code>capacity() == _M_end_of_storage - _M_start</code>。在 64 位平台上 <code>sizeof(std::vector&lt;int&gt;) == 24</code>，正好三个指针。</p><p>我觉得最好用的比喻是租仓库：<code>capacity</code> 是你租下的面积，<code>size</code> 是里面实际放了多少货。仓库可以是空的，但租金照付。这个区别后面会反复出现。</p>

<table><thead><tr><th>函数</th><th>改的是什么</th><th>备注</th></tr></thead><tbody><tr><td><code>size()</code></td><td>什么都不改</td><td>当前元素个数</td></tr><tr><td><code>capacity()</code></td><td>什么都不改</td><td>当前容量，永远 <span><span>⩾\geqslant</span><span><span><span></span><span>⩾</span></span></span></span> <code>size()</code></td></tr><tr><td><code>empty()</code></td><td>什么都不改</td><td>比 <code>size() == 0</code> 更达意</td></tr><tr><td><code>reserve(n)</code></td><td>只改 <code>capacity</code></td><td>只申请内存，不构造元素</td></tr><tr><td><code>resize(n)</code></td><td>改 <code>size</code></td><td>变大时构造新元素，变小时析构旧元素</td></tr><tr><td><code>shrink_to_fit()</code></td><td>尝试缩小 <code>capacity</code></td><td>非强制请求（C++11）</td></tr></tbody></table><p>“<code>resize</code> 改 <code>size</code>、<code>reserve</code> 改 <code>capacity</code>”这句话，光看定义很容易滑过去，跑一遍才印象深刻：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>values</span><span>;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>7</div></div><div><span>    </span><span>values</span><span>.</span><span>push_back</span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"after push_back: size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>10</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>capacity</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>  </span><span>values</span><span>.</span><span>erase</span><span>(</span><span>values</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>10</span><span>, </span><span>values</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"after erase:     size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>14</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>capacity</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>  </span><span>values</span><span>.</span><span>clear</span><span>();</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"after clear:     size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>18</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>capacity</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>  </span><span>values</span><span>.</span><span>shrink_to_fit</span><span>();</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"after shrink:    size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>22</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>capacity</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>23</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>after push_back: size=1000 capacity=1024</span></div></div><div><div><div>2</div></div><div><span>after erase:     size=10 capacity=1024</span></div></div><div><div><div>3</div></div><div><span>after clear:     size=0 capacity=1024</span></div></div><div><div><div>4</div></div><div><span>after shrink:    size=0 capacity=0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意后三行：<code>erase</code> 删掉了 990 个元素，<code>clear</code> 把 <code>size</code> 归零，但 <code>capacity</code> 一直是 1024，一块内存都没还。想还回去只能显式 <code>shrink_to_fit()</code>，而且它只是”请求”，标准不保证实现一定照做。</p></section>
<section><h2>扩容：libstdc++ 每次翻倍<a href="#扩容libstdc-每次翻倍"><span>#</span></a></h2><p>把 <code>capacity</code> 每次变化都打出来，规律立刻现形：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;int&gt;</span><span> </span><span>values</span><span>;</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>size_t</span><span> </span><span>last_capacity</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>3</div></div><div><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1025</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>values</span><span>.</span><span>push_back</span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>5</div></div><div><span>  </span><span>if</span><span> (</span><span>values</span><span>.</span><span>capacity</span><span>() </span><span>!=</span><span> </span><span>last_capacity</span><span>) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>last_capacity</span><span> </span><span>=</span><span> </span><span>values</span><span>.</span><span>capacity</span><span>();</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>" capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>last_capacity</span></div></div><div><div><div>8</div></div><div><span>              </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size=1 capacity=1</span></div></div><div><div><div>2</div></div><div><span>size=2 capacity=2</span></div></div><div><div><div>3</div></div><div><span>size=3 capacity=4</span></div></div><div><div><div>4</div></div><div><span>size=5 capacity=8</span></div></div><div><div><div>5</div></div><div><span>size=9 capacity=16</span></div></div><div><div><div>6</div></div><div><span>size=17 capacity=32</span></div></div><div><div><div>7</div></div><div><span>...</span></div></div><div><div><div>8</div></div><div><span>size=513 capacity=1024</span></div></div><div><div><div>9</div></div><div><span>size=1025 capacity=2048</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>1、2、4、8、16…… 每满一次就翻一倍。原因在 <code>_M_check_len</code>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>size_type</span><span> </span><span>__len</span><span> </span><span>=</span><span> </span><span>size</span><span>() </span><span>+</span><span> (std::</span><span>max</span><span>)(</span><span>size</span><span>(), </span><span>__n</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>push_back</code> 时 <code>__n == 1</code>，所以新容量是 <code>size() + size()</code>，也就是两倍。<strong>这是 libstdc++ 的实现选择，不是标准规定</strong>。我记得 MSVC 用的是 1.5 倍；1.5 倍的好处是更容易复用刚刚释放的内存块，翻倍的好处是元素搬移次数更少。这是纯实现层面的权衡，写代码时不该依赖具体系数：</p><div><div><div></div><div>不要依赖扩容系数</div></div><div><p>别写”反正它会翻倍，所以我 push 了 1000 个之后 capacity 一定是 1024”这种代码。换个标准库实现就不成立了。</p></div></div><p>翻倍的代价可以用等比数列算清楚：容量按 <span><span>1,2,4,…,2k1, 2, 4, \dots, 2^k</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span>k</span></span></span></span></span></span></span></span></span></span></span> 增长，元素从 0 涨到 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 的过程中，被搬移的元素总数不超过</p><span><span><span>N⋅rr−1N \cdot \frac{r}{r - 1}</span><span><span><span></span><span>N</span><span></span><span>⋅</span><span></span></span><span><span></span><span><span></span><span><span><span><span><span><span></span><span><span>r</span><span></span><span>−</span><span></span><span>1</span></span></span><span><span></span><span></span></span><span><span></span><span><span>r</span></span></span></span><span>​</span></span><span><span><span></span></span></span></span></span><span></span></span></span></span></span></span><p>其中 <span><span>rr</span><span><span><span></span><span>r</span></span></span></span> 是增长系数。<span><span>r=2r = 2</span><span><span><span></span><span>r</span><span></span><span>=</span><span></span></span><span><span></span><span>2</span></span></span></span> 时大约是 <span><span>2N2N</span><span><span><span></span><span>2</span><span>N</span></span></span></span>：<strong>每个元素平均被搬移两次</strong>。这个常数不大，代价可控，所以 <code>push_back</code> 的均摊复杂度才是 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>。</p></section>
<section><h2>reserve：我量出来是 3.2 倍<a href="#reserve我量出来是-32-倍"><span>#</span></a></h2><p>既然扩容就是要搬元素，那提前 <code>reserve</code> 掉扩容，收益有多大？我写了两个版本，各塞入 2000 万个 <code>int</code>（约 80 MB）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>double</span><span> </span><span>TimePushBack</span><span>(</span><span>bool</span><span><span> </span><span>reserve_first</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>values</span><span>;</span></div></div><div><div><div>3</div></div><div><span>  </span><span>if</span><span> (</span><span>reserve_first</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>values</span><span>.</span><span>reserve</span><span>(</span><span>kCount</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> </span><span>auto</span><span> </span><span>start</span><span> </span><span>=</span><span><span> std::</span><span>chrono</span><span>::</span><span>steady_clock</span><span>::</span></span><span>now</span><span>();</span></div></div><div><div><div>7</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kCount</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>values</span><span>.</span><span>push_back</span><span>(</span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>i</span><span>));</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>  </span><span>const</span><span> </span><span>auto</span><span> </span><span>end</span><span> </span><span>=</span><span><span> std::</span><span>chrono</span><span>::</span><span>steady_clock</span><span>::</span></span><span>now</span><span>();</span></div></div><div><div><div>11</div></div><div><span>  </span><span>const</span><span> std::chrono::</span><span>duration</span><span>&lt;</span><span>double</span><span>, std::</span><span>milli</span><span>&gt; </span><span>elapsed</span><span> </span><span>=</span><span> </span><span>end</span><span> </span><span>-</span><span> </span><span>start</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>return</span><span> </span><span>elapsed</span><span>.</span><span>count</span><span>();</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>跑两遍：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>no reserve: 124.928 ms      no reserve: 121.734 ms</span></div></div><div><div><div>2</div></div><div><span>reserve:    38.6949 ms      reserve:    38.1589 ms</span></div></div><div><div><div>3</div></div><div><span>speedup:    3.22855x        speedup:    3.19018x</span></div></div><div><div><div>4</div></div><div><span>reallocations for 20000000 push_backs: 26</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>3.2 倍</strong>，而且 20M 次 <code>push_back</code> 期间一共只发生了 26 次重新分配（<span><span>1,2,4,…,2251, 2, 4, \dots, 2^{25}</span><span><span><span></span><span>1</span><span>,</span><span></span><span>2</span><span>,</span><span></span><span>4</span><span>,</span><span></span><span>…</span><span></span><span>,</span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>25</span></span></span></span></span></span></span></span></span></span></span></span>，正好 26 个容量档位），代价是额外搬移了约 3350 万个 <code>int</code>，也就是一百多 MB 的内存流量。</p><p>所以”能预估大小时先 <code>reserve</code>”不是玄学，是一百多 MB 的差别。当然反过来也成立：</p><div><div><div></div><div>别乱 reserve</div></div><div><p>2000 万个元素你 reserve 两亿，那就是凭空占用八倍内存。<code>reserve</code> 的前提是真的能预估，而不是”多多益善”。</p></div></div></section>
<section><h2>push_back 和 emplace_back：省的到底是哪一次拷贝<a href="#push_back-和-emplace_back省的到底是哪一次拷贝"><span>#</span></a></h2><p>教科书式的说法是”<code>emplace_back</code> 原地构造，比 <code>push_back</code> 高效”。为了看清它到底省了什么，我给一个会数数的类型挂了两个计数器：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class</span><span> </span><span>Widget</span><span> {</span></div></div><div><div><div>2</div></div><div><span> </span><span>public:</span></div></div><div><div><div>3</div></div><div><span>  </span><span>explicit</span><span> </span><span>Widget</span><span>(std::</span><span>string</span><span><span> </span><span>name</span><span>) : </span></span><span>name_</span><span><span>(</span><span>std</span><span>::</span></span><span>move</span><span>(</span><span>name</span><span>)) {}</span></div></div><div><div><div>4</div></div><div><span>  </span><span>Widget</span><span>(</span><span>const</span><span> </span><span>Widget</span><span>&amp;</span><span><span> </span><span>other</span><span>) : </span></span><span>name_</span><span>(</span><span>other</span><span>.</span><span>name_</span><span>) { </span><span>++</span><span>copy_count_</span><span>; }</span></div></div><div><div><div>5</div></div><div><span>  </span><span>Widget</span><span>(</span><span>Widget</span><span>&amp;&amp;</span><span><span> </span><span>other</span><span>) </span></span><span>noexcept</span><span> : </span><span>name_</span><span><span>(</span><span>std</span><span>::</span></span><span>move</span><span>(</span><span>other</span><span>.</span><span>name_</span><span>)) {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>++</span><span>move_count_</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>  </span><span>Widget</span><span>&amp;</span><span> </span><span>operator</span><span>=(</span><span>const</span><span> </span><span>Widget</span><span>&amp;</span><span>) </span><span>=</span><span> </span><span>delete</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>Widget</span><span>&amp;</span><span> </span><span>operator</span><span>=(</span><span>Widget</span><span>&amp;&amp;</span><span>) </span><span>=</span><span> </span><span>delete</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>  </span><span>static</span><span> </span><span>int</span><span> </span><span>copy_count</span><span>() { </span><span>return</span><span> </span><span>copy_count_</span><span>; }</span></div></div><div><div><div>12</div></div><div><span>  </span><span>static</span><span> </span><span>int</span><span> </span><span>move_count</span><span>() { </span><span>return</span><span> </span><span>move_count_</span><span>; }</span></div></div><div><div><div>13</div></div><div><span>  </span><span>static</span><span> </span><span>void</span><span> </span><span>Reset</span><span>() {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>copy_count_</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>15</div></div><div><span>    </span><span>move_count_</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span> </span><span>private:</span></div></div><div><div><div>19</div></div><div><span>  </span><span>static</span><span> </span><span>int</span><span> </span><span>copy_count_</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>static</span><span> </span><span>int</span><span> </span><span>move_count_</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>name_</span><span>;</span></div></div><div><div><div>22</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>同样的两个元素，分别用两种方式放进去：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>push_back:    copies=1 moves=1</span></div></div><div><div><div>2</div></div><div><span>emplace_back: copies=1 moves=0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结论很清楚：<code>push_back</code> 传入临时对象时，多了一次<strong>移动构造</strong>；<code>emplace_back</code> 直接拿参数在 <code>vector</code> 的尾部原地构造，连那次移动都省了。两者的复制次数相同（那次复制来自传左值 <code>lvalue</code>，这点 <code>emplace_back</code> 救不了你：参数已经是现成对象时，怎么放进去都得拷一次）。</p><p>那么这次省下的移动值多少时间？我用一个带 1 KB 数组的类型，塞 100 万个元素，量了冷启动和预热后的版本：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>push_back cold: 513.6 ms      push_back warm: 122.5 ms</span></div></div><div><div><div>2</div></div><div><span>emplace   cold: 468.4 ms      emplace   warm: 134.7 ms</span></div></div><div><div><div>3</div></div><div><span>cold ratio: 1.096x            warm ratio: 1.18x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>老实说，这个结果让我有点意外：冷启动只有 1.1 倍，页面预热之后比例直接掉进噪音里（两次跑出来分别是 0.91 倍和 1.18 倍，<code>¯\_(ツ)_/¯</code>）。我的解释是：冷启动那次的时间主要花在新申请内存的<strong>首次触碰缺页</strong>上，那点移动开销被淹没了；而 <code>Heavy</code> 是平凡可拷贝类型，一次 1 KB 的移动就是一次 <code>memcpy</code>，现代移动语义早就把这种琐碎开销压得很低。</p><p>所以我的实践结论是：</p><ul>
<li>默认用 <code>emplace_back</code>，它逻辑上更准确（“在这里构造一个”而不是”构造一个再搬进来”），习惯也好。</li>
<li>但别指望它带来数量级提升。真正的差别是<strong>一次移动构造</strong>，对便宜移动的类型几乎可以忽略。</li>
<li>有个容易忽略的细节：<code>emplace_back</code> 走的是直接初始化，所以它会调用 <code>explicit</code> 构造函数。这是特性还是坑，取决于你想要的语义。</li>
<li>顺带一提，<code>std::vector&lt;T&gt;</code> 要能增长，<code>T</code> 必须是 MoveInsertable。把移动构造函数 <code>= delete</code> 掉之后，连 <code>reserve</code> 都编不过（我试了，错误信息一路指回 <code>Immovable::Immovable(Immovable&amp;&amp;)</code> 被删除）。</li>
</ul><p>（“预热”版本的改动只有一处：计时前先 <code>resize(kCount)</code> 把整块内存触碰一遍，再 <code>clear()</code> 掉，这样计时期间就不会再发生首次触碰缺页。）</p></section>
<section><h2>访问：operator[] 不做检查<a href="#访问operator-不做检查"><span>#</span></a></h2><p><code>operator[]</code> 和 <code>at()</code> 的区别只有一句话：<code>at()</code> 越界抛 <code>std::out_of_range</code>，<code>operator[]</code> 越界是未定义行为。日常读写用 <code>operator[]</code>，处理外部输入或者调试时用 <code>at()</code>。</p><p><code>data()</code> 返回底层数组的首指针，是和 C API 打交道的正经通道：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int</span><span> </span><span>CompareInts</span><span>(</span><span>const</span><span> </span><span>void*</span><span><span> </span><span>left</span><span>, </span></span><span>const</span><span> </span><span>void*</span><span><span> </span><span>right</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>  </span><span>const</span><span> </span><span>int</span><span> </span><span>left_value</span><span> </span><span>=</span><span> </span><span><span>*</span><span>static_cast</span><span>&lt;const</span></span><span> </span><span>int*&gt;</span><span>(</span><span>left</span><span>);</span></div></div><div><div><div>3</div></div><div><span>  </span><span>const</span><span> </span><span>int</span><span> </span><span>right_value</span><span> </span><span>=</span><span> </span><span><span>*</span><span>static_cast</span><span>&lt;const</span></span><span> </span><span>int*&gt;</span><span>(</span><span>right</span><span>);</span></div></div><div><div><div>4</div></div><div><span>  </span><span>return</span><span> </span><span>left_value</span><span> </span><span>-</span><span> </span><span>right_value</span><span>;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;</span><span>int</span><span>&gt; </span><span>values</span><span> </span><span>=</span><span> {</span><span>5</span><span>, </span><span>3</span><span>, </span><span>1</span><span>, </span><span>4</span><span>, </span><span>2</span><span>};</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>qsort</span><span>(</span><span>values</span><span>.</span><span>data</span><span>(), </span><span>values</span><span>.</span><span>size</span><span>(), </span><span>sizeof</span><span>(</span><span>int</span><span>), </span><span>CompareInts</span><span>);</span></div></div><div><div><div>10</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>value</span><span> : </span><span>values</span><span>) {</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>value</span><span> </span><span>&lt;&lt;</span><span> </span><span>" "</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 2 3 4 5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一个细节：<code>data()</code> 返回的指针在容器变化（尤其是扩容）后同样会失效，别缓存它。空 <code>vector</code> 上调用 <code>data()</code> 得到的指针不可解引用。</p></section>
<section><h2>迭代器失效：连续内存的代价<a href="#迭代器失效连续内存的代价"><span>#</span></a></h2><p>“连续内存”是所有优点的源头，也是所有 bug 的源头。只要发生扩容，旧内存被释放，指向它的<strong>迭代器、指针、引用</strong>全部悬空。这种 bug 最讨厌的地方是它经常”看起来能跑”，所以我用工具把它抓出来。</p><p>先看引用失效。下面这段代码很自然，但它读的是一个已经释放的地址：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;int&gt;</span><span> </span><span>values</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>};</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span><span>int</span><span>&amp;</span></span><span> </span><span>first</span><span> </span><span>=</span><span> </span><span>values</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>3</div></div><div><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>100</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>values</span><span>.</span><span>push_back</span><span>(</span><span>i</span><span>);</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>first</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 <code>-fsanitize=address</code> 编译，运行立刻报错：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>==25009==ERROR: AddressSanitizer: heap-use-after-free on address 0x6cc39e5e0010</span></div></div><div><div><div>2</div></div><div><span>READ of size 4 at 0x6cc39e5e0010 thread T0</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#0 0x586fe2251aa3 in main 04a_dangling.cpp:11</span></div></div><div><div><div>4</div></div><div><span>freed by thread T0 here:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#6 0x586fe2251997 in ...::_M_realloc_append&lt;int const&amp;&gt;(int const&amp;)</span></div></div><div><div><div>6</div></div><div><span>SUMMARY: AddressSanitizer: heap-use-after-free 04a_dangling.cpp:11 in main</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>堆已经被 <code>push_back</code> 触发的重新分配释放了，<code>first</code> 成了悬空引用。关掉 sanitizer 之后这段代码大概率还能打印出”正确”的值，然后在你改代码的那天突然崩。</p><p>再看迭代器。这是每个人第一次写 <code>vector</code> 删除都会踩的坑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;int&gt;</span><span> </span><span>values</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>};</span></div></div><div><div><div>2</div></div><div><span>for</span><span> (</span><span>auto</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>values</span><span>.</span><span>begin</span><span>(); </span><span>it</span><span> </span><span>!=</span><span> </span><span>values</span><span>.</span><span>end</span><span>(); </span><span>++</span><span>it</span><span>) {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>if</span><span> (</span><span>*</span><span>it</span><span> </span><span>%</span><span> </span><span>2</span><span> </span><span>==</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>values</span><span>.</span><span>erase</span><span>(</span><span>it</span><span>);</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 <code>-D_GLIBCXX_DEBUG</code> 编译（调试迭代器），运行直接终止：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Error: attempt to increment a singular iterator.</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>原因：<code>erase(it)</code> 之后，<code>it</code> 及其之后的迭代器全部失效，下一轮循环 <code>++it</code> 操作的是一个失效迭代器。正确写法是接住 <code>erase</code> 的返回值：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;int&gt;</span><span> </span><span>values</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>, </span><span>5</span><span>, </span><span>6</span><span>};</span></div></div><div><div><div>2</div></div><div><span>for</span><span> (</span><span>auto</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>values</span><span>.</span><span>begin</span><span>(); </span><span>it</span><span> </span><span>!=</span><span> </span><span>values</span><span>.</span><span>end</span><span>();) {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>if</span><span> (</span><span>*</span><span>it</span><span> </span><span>%</span><span> </span><span>2</span><span> </span><span>==</span><span> </span><span>0</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>it</span><span> </span><span>=</span><span> </span><span>values</span><span>.</span><span>erase</span><span>(</span><span>it</span><span>);</span><span>  // erase 返回下一个有效位置</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>++</span><span>it</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>1 3 5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>失效规则可以压缩成两条：</p><ul>
<li><strong>扩容</strong>：所有迭代器、指针、引用全部失效（内存整个换了一块）。</li>
<li><strong><code>insert</code> / <code>erase</code></strong>：操作位置及其之后的全部失效，之前的仍然有效。</li>
</ul><p>由此推出两条经验：</p><ol>
<li>需要长期持有指向元素的引用或指针时，先 <code>reserve</code> 到不会再扩容，或者干脆存<strong>下标</strong>，用的时候再 <code>values[i]</code> 取。</li>
<li>涉及 <code>insert</code> / <code>erase</code> 的循环，永远用返回值刷新迭代器。</li>
</ol><p>编译器可以在编译期帮你挡掉一部分这类问题：日常开发给 libstdc++ 加 <code>-D_GLIBCXX_DEBUG</code>（Clang 配 libstdc++ 时同样可用），提交前再跑一遍 <code>-fsanitize=address</code>。这类工具抓悬空引用是降维打击。</p></section>
<section><h2>erase-remove：为什么 remove 之后 size 还是 8<a href="#erase-remove为什么-remove-之后-size-还是-8"><span>#</span></a></h2><p>C++20 之前，从 <code>vector</code> 里删掉所有偶数要写 <code>erase(remove_if(...), end())</code>。这个组合看着别扭，是因为 <code>std::remove_if</code> <strong>根本不删除元素</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;int&gt;</span><span> </span><span>values</span><span> </span><span>=</span><span> {</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>, </span><span>4</span><span>, </span><span>5</span><span>, </span><span>6</span><span>, </span><span>7</span><span>, </span><span>8</span><span>};</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>auto</span><span> </span><span>new_end</span><span> </span><span>=</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>std::</span><span>remove_if</span><span>(</span><span>values</span><span>.</span><span>begin</span><span>(), </span><span>values</span><span>.</span><span>end</span><span>(),</span></div></div><div><div><div>4</div></div><div><span><span>                   </span></span><span>[](</span><span>int</span><span><span> </span><span>x</span><span>) { </span></span><span>return</span><span> </span><span>x</span><span> </span><span>%</span><span> </span><span>2</span><span> </span><span>==</span><span> </span><span>0</span><span>; });</span></div></div><div><div><div>5</div></div><div><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"after std::remove_if: size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>values</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>6</div></div><div><span>          </span><span>&lt;&lt;</span><span> </span><span>" logical size="</span><span> </span><span>&lt;&lt;</span><span> (</span><span>new_end</span><span> </span><span>-</span><span> </span><span>values</span><span>.</span><span>begin</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>after std::remove_if: size=8 logical size=4</span></div></div><div><div><div>2</div></div><div><span>1 3 5 7 5 6 7 8</span></div></div><div><div><div>3</div></div><div><span>after erase: size=4</span></div></div><div><div><div>4</div></div><div><span>1 3 5 7</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>remove_if</code> 只是把要保留的元素向前挪，返回新的逻辑尾位置，容器本身的大小没变。所以那串 <code>5 6 7 8</code> 是留下来的残渣，必须靠 <code>erase(new_end, values.end())</code> 才真正清掉。算法和容器各司其职，这是 STL “算法不改变容器大小” 原则的直接体现。</p><p>C++20 起，标准库直接提供了封装好的版本，不用再手写这个组合：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> std::</span><span>size_t</span><span> </span><span>removed</span><span> </span><span>=</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>std::</span><span>erase_if</span><span>(</span><span>values</span><span>, [](</span><span>int</span><span><span> </span><span>x</span><span>) { </span></span><span>return</span><span> </span><span>x</span><span> </span><span>%</span><span> </span><span>2</span><span> </span><span>==</span><span> </span><span>0</span><span>; });</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>removed=4 size=4</span></div></div><div><div><div>2</div></div><div><span>1 3 5 7</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>返回被删除的元素个数。按值删除用 <code>std::erase(values, value)</code>。这两个函数（<code>&lt;vector&gt;</code> 里就能用）是我个人认为 C++20 日常收益最高的几个特性之一。</p></section>
<section><h2>std::vector：一个真正的坑<a href="#stdvector一个真正的坑"><span>#</span></a></h2><p><code>std::vector&lt;bool&gt;</code> 是标准库里的历史遗留特化：为了省空间，它不存 <code>bool</code>，而是把每个元素压成 1 bit。省是省了，代价也实打实。</p><p><strong>省下八分之七的内存，代价是丢掉一个正常容器该有的行为。</strong> 8388608 个元素：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>vector&lt;bool&gt; payload: 1048576 bytes</span></div></div><div><div><div>2</div></div><div><span>vector&lt;char&gt; payload: 8388608 bytes</span></div></div><div><div><div>3</div></div><div><span>sizeof(std::vector&lt;bool&gt;::reference)=16</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>内存正好是 <code>std::vector&lt;char&gt;</code> 的八分之一。好处到此为止。因为单个 bit 没有地址，<code>operator[]</code> 不能返回 <code>bool&amp;</code>，只能返回一个 16 字节的<strong>代理对象</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;bool&gt;</span><span> </span><span>flags</span><span> </span><span>=</span><span> {</span><span>true</span><span>, </span><span>false</span><span>};</span></div></div><div><div><div>2</div></div><div><span><span>bool</span><span>&amp;</span></span><span> </span><span>ref</span><span> </span><span>=</span><span> </span><span>flags</span><span>[</span><span>0</span><span>];</span><span>  // 编不过</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>错误信息很直白：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>错误：无法将类型为 'bool&amp;' 的非 const 左值引用绑定到类型为 'bool' 的右值</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>代理对象还有个更阴险的地方。用 <code>auto</code> 接住它时，你拿到的不是一份 <code>bool</code> 快照，而是一个指向该 bit 的活引用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;bool&gt;</span><span> </span><span>flags</span><span> </span><span>=</span><span> {</span><span>true</span><span>, </span><span>false</span><span>};</span></div></div><div><div><div>2</div></div><div><span>auto</span><span> </span><span>bit</span><span> </span><span>=</span><span> </span><span>flags</span><span>[</span><span>0</span><span>];</span><span>  // 推导出的是代理类型，不是 bool</span></div></div><div><div><div>3</div></div><div><span>flags</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>false</span><span>;</span></div></div><div><div><div>4</div></div><div><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"flags[0]="</span><span> </span><span>&lt;&lt;</span><span> </span><span>flags</span><span>[</span><span>0</span><span>] </span><span>&lt;&lt;</span><span> </span><span>" bit="</span><span> </span><span>&lt;&lt;</span><span> </span><span>bit</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>flags[0]=0 bit=0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>bit</code> 跟着 <code>flags[0]</code> 一起变成了 0。它能用，但它的类型不是 <code>bool</code>，把它传给模板或者 <code>auto&amp;&amp;</code> 全展开的代码时，报错会非常难读。</p><p><strong>真正危险的是并发。</strong> 标准明确规定：同一个容器不同元素被并发修改是安全的，但这条保证<strong>明确把 <code>std::vector&lt;bool&gt;</code> 排除在外</strong>。原因是多个 bit 共享同一个机器字。用 ThreadSanitizer 跑两个线程分别翻转第 0 位和第 1 位：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>std::</span><span>vector</span><span>&lt;</span><span>bool</span><span>&gt; </span><span>bits</span><span>(</span><span>2</span><span>, </span><span>false</span><span>);</span></div></div><div><div><div>2</div></div><div><span>std::</span><span>thread</span><span> </span><span>first</span><span>([</span><span>&amp;</span><span><span>bits</span><span>] {</span></span></div></div><div><div><div>3</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000000</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>bits</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>!</span><span>bits</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>});</span></div></div><div><div><div>7</div></div><div><span>std::</span><span>thread</span><span> </span><span>second</span><span>([</span><span>&amp;</span><span><span>bits</span><span>] {</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000000</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>bits</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>!</span><span>bits</span><span>[</span><span>1</span><span>];</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>});</span></div></div><div><div><div>12</div></div><div><span>first</span><span>.</span><span>join</span><span>();</span></div></div><div><div><div>13</div></div><div><span>second</span><span>.</span><span>join</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>WARNING: ThreadSanitizer: data race</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>Read of size 8 at 0x720400000000 by thread T2:</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>#0 std::_Bit_reference::operator bool() const stl_bvector.h:106</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>Previous write of size 8 at 0x720400000000 by thread T1:</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>#0 std::_Bit_reference::operator=(bool) stl_bvector.h:113</span></div></div><div><div><div>6</div></div><div><span>SUMMARY: ThreadSanitizer: data race 05b_bool_race.cpp:14</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>看清楚读写大小：<strong>8 字节</strong>，也就是一个机器字。libstdc++ 的代理每次都要读改写整个机器字来翻转一个 bit，两个线程自然就撞上了。把类型换成 <code>std::vector&lt;char&gt;</code>（两个线程各写自己的字节），同样的测试静默通过。</p><p>所以要用的时候：</p><ul>
<li>只是存一堆开关，不缺那八分之一内存，用 <code>std::vector&lt;char&gt;</code>，行为和普通容器一致。</li>
<li>确实要位操作，用 <code>std::bitset</code>（大小固定）或者自己封装 <code>uint64_t</code> 数组（需要注意并发和边界）。</li>
<li><code>std::vector&lt;bool&gt;</code> 只在”元素极多、只读或单线程、并且真的在乎内存”时才值得。</li>
</ul></section>
<section><h2>什么时候别用它<a href="#什么时候别用它"><span>#</span></a></h2><p><code>vector</code> 的软肋是头部和中间插入，因为要搬动后面所有元素。20,000 次插入，只改插入位置：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>insert at front: 8.21697 ms      insert at front: 8.16348 ms</span></div></div><div><div><div>2</div></div><div><span>push_back:       0.04288 ms      push_back:       0.040175 ms</span></div></div><div><div><div>3</div></div><div><span>ratio:           191.627x        ratio:           203.198x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>约 200 倍</strong>，还只是在两万元素这个规模。这个量级没有悬念：需要在头部或中间频繁插入删除，就换 <code>std::deque</code> 或 <code>std::list</code>。</p><p>另一面是缓存。连续内存的真正优势在这里，我给 <code>vector</code> 和 <code>list</code> 各塞 1000 万个 <code>int</code>，只做一次求和：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>vector: 5.84785 ms      vector: 6.32172 ms</span></div></div><div><div><div>2</div></div><div><span>list:   25.3419 ms      list:   26.7437 ms</span></div></div><div><div><div>3</div></div><div><span>ratio:  4.33354x        ratio:  4.23046x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>4.3 倍</strong>。<code>list</code> 的每个节点都散在堆上，遍历就是一路 cache miss；<code>vector</code> 是顺序读取，CPU 预取器全程满载。这也解释了一个常见的反直觉现象：即使是在中间插入，对于小对象（比如 <code>int</code>），<code>vector</code> 往往仍然比 <code>list</code> 快。链表的 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span> 是指针层面的，但指针本身要跳内存。</p><p>所以默认选择永远先问自己：<strong>真的需要中间插入吗？</strong> 大多数”需要”其实是”我没想清楚”。真需要时，再考虑 <code>deque</code>、<code>list</code> 或者干脆换数据结构。</p></section>
<section><h2>一份可以直接抄的清单<a href="#一份可以直接抄的清单"><span>#</span></a></h2><ul>
<li>能预估大小时先 <code>reserve</code>，我实测尾部插入快 3.2 倍。</li>
<li>默认用 <code>emplace_back</code>，但别指望它比 <code>push_back</code> 快很多，差别只有一次移动构造。</li>
<li>头部或中间频繁插入，换 <code>std::deque</code> 或 <code>std::list</code>。</li>
<li>长期持有元素引用或指针时，先 <code>reserve</code> 或者改存下标。</li>
<li>涉及 <code>insert</code> / <code>erase</code> 的循环，用返回值刷新迭代器。</li>
<li>删除满足条件的元素，优先用 C++20 的 <code>std::erase_if</code>。</li>
<li>慎用 <code>std::vector&lt;bool&gt;</code>。并发场景下直接换 <code>std::vector&lt;char&gt;</code>。</li>
<li>开发期开 <code>-D_GLIBCXX_DEBUG</code>，提交前跑一遍 <code>-fsanitize=address</code>。</li>
</ul></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>如果这篇笔记只能留一句话，我会留这句：<strong><code>vector</code> 的几乎所有性能问题，都能翻译成”这次操作拷贝了多少字节”。</strong> 扩容是拷贝，中间插入是拷贝，<code>reserve</code> 是省掉拷贝，<code>emplace_back</code> 是省掉一次构造。想清楚拷贝量，剩下的都是推论。</p><p>顺着这个思路，你还可以接着往下量：换 MSVC 验证一下扩容系数是不是 1.5，把 <code>std::vector&lt;bool&gt;</code> 的并发程序换成 <code>std::vector&lt;char&gt;</code> 再看 TSan，或者把自己项目里最热的那个 <code>vector</code> 的 <code>reserve</code> 去掉跑一遍基准。这次实验用的每个程序都不超过四十行，文中列出的代码加上几行计时脚手架就能复现，花不了十分钟。真正让我对 <code>vector</code> 放心的，不是这篇文章，是那十几个程序。</p><p>参考：</p><ul>
<li><a href="https://en.cppreference.com/w/cpp/container/vector" target="_blank">cppreference: std::vector</a></li>
<li><a href="https://en.cppreference.com/w/cpp/container/vector_bool" target="_blank">cppreference: std::vector&lt;bool&gt;</a></li>
<li>libstdc++ 源码：<code>bits/stl_vector.h</code>、<code>bits/stl_bvector.h</code></li>
<li>《Effective STL》，第 14 条（<code>reserve</code>）与第 17、18 条（<code>erase-remove</code>）</li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/string-in-cpp/</id>
      <title type="text">C++ 中的 string 详解</title>
      <published>2026-01-03T08:28:00.000Z</published>
      <updated>2026-10-16T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/string-in-cpp/"/>
      <summary type="text">把 std::string 从头讲到尾：构造与赋值、元素访问、容量、修改、查找、比较、数值转换、string_view、编码，再到 SSO、移动语义和迭代器失效。全部示例在 GCC 15.3.0 (libstdc++) 和 Clang 21.1.8 (libc++) 上用 -std=c++23 编译运行。</summary>
      <content type="html"><![CDATA[<p>这是《C++ 日拱一卒》的第 1 篇，目标很具体：把 <code>std::string</code> 一次学完整。读完之后，cppreference 上 <code>std::basic_string</code> 页面里的每个成员函数你都应该看得懂，知道它的坑在哪，也知道它在内存里长什么样。</p>
<p>读法：每一节先说这组函数解决什么问题，再给一个能直接编译的程序和它的真实输出，最后总结要记住的规则。所有程序都用 <code>g++ 15.3.0 -std=c++23 -Wall -Wextra -O2</code> 编译运行 (libstdc++)，同时也用 Clang 21.1.8 加 libc++ 跑了一遍，两边结果不同的地方我会指出来。代码按 Google C++ Style 排版，用 <code>clang-format --style=Google</code> 检查过。需要特定标准的函数会标注 (C++17)、(C++20)、(C++23)。</p>
<section><h2>1. string 是什么<a href="#1-string-是什么"><span>#</span></a></h2><p><code>std::string</code> 不是一个独立的类，而是类模板 <code>std::basic_string</code> 的别名：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>template</span><span> &lt;</span><span>class</span><span> </span><span>CharT</span><span>, </span><span>class</span><span> </span><span>Traits</span><span> </span><span>=</span><span> std::</span><span>char_traits</span><span>&lt;</span><span>CharT</span><span>&gt;,</span></div></div><div><div><div>2</div></div><div><span>          </span><span>class</span><span> </span><span>Allocator</span><span> </span><span>=</span><span> std::</span><span>allocator</span><span>&lt;</span><span>CharT</span><span>&gt;&gt;</span></div></div><div><div><div>3</div></div><div><span>class</span><span> </span><span>basic_string</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个模板参数分别决定字符是什么类型、字符怎么比较和拷贝 (<code>Traits</code>)、内存从哪里来 (<code>Allocator</code>)。标准库给常用的字符类型各起了一个别名：</p>

<table><thead><tr><th>别名</th><th>字符类型</th><th>说明</th></tr></thead><tbody><tr><td><code>std::string</code></td><td><code>char</code></td><td>最常用，本文的主角</td></tr><tr><td><code>std::wstring</code></td><td><code>wchar_t</code></td><td>宽度由平台决定</td></tr><tr><td><code>std::u8string</code> (C++20)</td><td><code>char8_t</code></td><td>UTF-8 code unit</td></tr><tr><td><code>std::u16string</code></td><td><code>char16_t</code></td><td>UTF-16 code unit</td></tr><tr><td><code>std::u32string</code></td><td><code>char32_t</code></td><td>UTF-32 code unit</td></tr><tr><td><code>std::pmr::string</code> (C++17)</td><td><code>char</code></td><td>分配器换成 <code>polymorphic_allocator</code></td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;memory_resource&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;type_traits&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::</span><span>string</span><span>, std::basic_string&lt;char&gt;&gt;);</span></div></div><div><div><div>7</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::</span><span>wstring</span><span>, std::basic_string&lt;wchar_t&gt;&gt;);</span></div></div><div><div><div>8</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::</span><span>u8string</span><span>, std::basic_string&lt;char8_t&gt;&gt;);</span></div></div><div><div><div>9</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::</span><span>u16string</span><span>, std::basic_string&lt;char16_t&gt;&gt;);</span></div></div><div><div><div>10</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::</span><span>u32string</span><span>, std::basic_string&lt;char32_t&gt;&gt;);</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>static_assert</span><span>(</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>std::</span><span>is_same_v</span><span>&lt;</span><span>std::pmr::</span><span>string</span><span>,</span></div></div><div><div><div>14</div></div><div><span><span>                   </span></span><span>std::basic_string&lt;char, std::char_traits&lt;char&gt;,</span></div></div><div><div><div>15</div></div><div><span><span>                                     </span></span><span>std::pmr::polymorphic_allocator&lt;char&gt;&gt;&gt;);</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::string::</span><span>value_type</span><span>, char&gt;);</span></div></div><div><div><div>18</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::string::</span><span>size_type</span><span>, std::size_t&gt;);</span></div></div><div><div><div>19</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::string::</span><span>difference_type</span><span>, std::ptrdiff_t&gt;);</span></div></div><div><div><div>20</div></div><div><span>static_assert</span><span>(std::</span><span>is_same_v</span><span>&lt;</span><span>std::string::</span><span>traits_type</span><span>, std::char_traits&lt;char&gt;&gt;);</span></div></div><div><div><div>21</div></div><div><span>static_assert</span><span>(</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>std::</span><span>is_same_v</span><span>&lt;</span><span>std::string::</span><span>allocator_type</span><span>, std::allocator&lt;char&gt;&gt;);</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"sizeof(char)     = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sizeof</span><span>(</span><span>char</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"sizeof(wchar_t)  = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sizeof</span><span>(</span><span>wchar_t</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"sizeof(char8_t)  = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sizeof</span><span>(</span><span>char8_t</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"sizeof(char16_t) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sizeof</span><span>(</span><span>char16_t</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"sizeof(char32_t) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sizeof</span><span>(</span><span>char32_t</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"npos = "</span><span> </span><span>&lt;&lt;</span><span> std::string::</span><span>npos</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sizeof(char)     = 1</span></div></div><div><div><div>2</div></div><div><span>sizeof(wchar_t)  = 4</span></div></div><div><div><div>3</div></div><div><span>sizeof(char8_t)  = 1</span></div></div><div><div><div>4</div></div><div><span>sizeof(char16_t) = 2</span></div></div><div><div><div>5</div></div><div><span>sizeof(char32_t) = 4</span></div></div><div><div><div>6</div></div><div><span>npos = 18446744073709551615</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>static_assert</code> 通过，说明上面这些别名和成员类型的关系都成立。<code>wchar_t</code> 在 Linux 上是 4 字节（据我所知在 Windows 上是 2 字节，这是 <code>wstring</code> 不适合跨平台文本的原因之一，我没有在 Windows 上验证）。<code>size_type</code> 是无符号的 <code>size_t</code>，<code>npos</code> 是它的最大值，后面会反复遇到。</p><p>先建立一个心智模型，后面所有行为都能从它推出来：</p><ul>
<li>string 是一个<strong>对象</strong>，里面记着三样东西：指向字符的指针、当前长度 (size)、已分配的容量 (capacity)。</li>
<li>字符存放在对象<strong>自己拥有</strong>的一块<strong>连续</strong>内存里。拷贝 string 就是拷贝这块内存（值语义），对象销毁时内存随之释放。</li>
<li>缓冲区末尾始终有一个 <code>'\0'</code>，所以可以安全地交给 C 接口。但长度是单独记录的，不靠 <code>'\0'</code> 判断，所以 string 中间可以含 <code>'\0'</code>。</li>
<li>string 存的是 <code>char</code>，也就是<strong>字节</strong>，不是“字符”。它对编码一无所知，第 15 节专门讲这件事。</li>
</ul></section>
<section><h2>2. 构造与赋值<a href="#2-构造与赋值"><span>#</span></a></h2><p><code>std::string</code> 有十几种构造方式，按“从什么构造”分成这几类：</p>

<table><thead><tr><th>写法</th><th>含义</th></tr></thead><tbody><tr><td><code>std::string s;</code></td><td>空串</td></tr><tr><td><code>std::string s(n, ch);</code></td><td><code>n</code> 个 <code>ch</code></td></tr><tr><td><code>std::string s(str, pos);</code> <code>std::string s(str, pos, len);</code></td><td>从另一个 string 的 <code>pos</code> 开始，取 <code>len</code> 个（缺省取到末尾）</td></tr><tr><td><code>std::string s(ptr);</code></td><td>从以 <code>'\0'</code> 结尾的 C 字符串</td></tr><tr><td><code>std::string s(ptr, n);</code></td><td>从 <code>ptr</code> 开始取 <code>n</code> 个字符，不要求 <code>'\0'</code> 结尾</td></tr><tr><td><code>std::string s(first, last);</code></td><td>从迭代器区间</td></tr><tr><td><code>std::string s{'H', 'i'};</code></td><td>从 <code>initializer_list&lt;char&gt;</code></td></tr><tr><td><code>std::string s(other);</code> <code>std::string s(std::move(other));</code></td><td>拷贝 / 移动</td></tr><tr><td><code>std::string s(sv);</code></td><td>从 <code>string_view</code>（显式构造）</td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;utility&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>using</span><span> std::string_literals::</span><span>operator</span><span>""s;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>void</span><span> </span><span>Show</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>label</span><span>, </span></span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>label</span><span> </span><span>&lt;&lt;</span><span> </span><span>" size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>" ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s1</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s2</span><span>(</span><span>5</span><span>, </span><span>'A'</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s3</span><span>(</span><span>"Hello, World"</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s4</span><span>(</span><span>"Hello, World"</span><span>, </span><span>5</span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s5</span><span>(</span><span>s3</span><span>, </span><span>7</span><span>);</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s6</span><span>(</span><span>s3</span><span>, </span><span>7</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s7</span><span>(</span><span>s3</span><span>.</span><span>begin</span><span>(), </span><span>s3</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>5</span><span>);</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s8</span><span>{</span><span>'H'</span><span>, </span><span>'i'</span><span>};</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s9</span><span>(</span><span>s3</span><span>);</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s10</span><span><span>(</span><span>std</span><span>::</span></span><span>move</span><span>(</span><span>s9</span><span>));</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s11</span><span><span>(</span><span>std</span><span>::</span></span><span>string_view</span><span>(</span><span>"abcdef"</span><span>).</span><span>substr</span><span>(</span><span>2</span><span>, </span><span>3</span><span>));</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s1  default       "</span><span>, </span><span>s1</span><span>);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s2  (5, 'A')      "</span><span>, </span><span>s2</span><span>);</span></div></div><div><div><div>27</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s3  const char*   "</span><span>, </span><span>s3</span><span>);</span></div></div><div><div><div>28</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s4  (ptr, 5)      "</span><span>, </span><span>s4</span><span>);</span></div></div><div><div><div>29</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s5  (str, 7)      "</span><span>, </span><span>s5</span><span>);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s6  (str, 7, 3)   "</span><span>, </span><span>s6</span><span>);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s7  iterator pair "</span><span>, </span><span>s7</span><span>);</span></div></div><div><div><div>32</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s8  {'H', 'i'}    "</span><span>, </span><span>s8</span><span>);</span></div></div><div><div><div>33</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s10 moved-to      "</span><span>, </span><span>s10</span><span>);</span></div></div><div><div><div>34</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s9  moved-from    "</span><span>, </span><span>s9</span><span>);</span></div></div><div><div><div>35</div></div><div><span>  </span><span>Show</span><span>(</span><span>"s11 from view     "</span><span>, </span><span>s11</span><span>);</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>t1</span><span>(</span><span>3</span><span>, </span><span>'x'</span><span>);</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>t2</span><span>{</span><span>3</span><span>, </span><span>'x'</span><span>};</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"t1: size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>t1</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"t2: size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>t2</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>41</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", first char code="</span><span> </span><span>&lt;&lt;</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>t2</span><span>[</span><span>0</span><span>]) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>u1</span><span> </span><span>=</span><span> </span><span>"a</span><span>\0</span><span>b"</span><span>;</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>u2</span><span> </span><span>=</span><span> </span><span>"a</span><span>\0</span><span>b"</span><span>s</span><span>;</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"u1.size()="</span><span> </span><span>&lt;&lt;</span><span> </span><span>u1</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>", u2.size()="</span><span> </span><span>&lt;&lt;</span><span> </span><span>u2</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>46</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>s1  default        size=0 []</span></div></div><div><div><div>2</div></div><div><span>s2  (5, 'A')       size=5 [AAAAA]</span></div></div><div><div><div>3</div></div><div><span>s3  const char*    size=12 [Hello, World]</span></div></div><div><div><div>4</div></div><div><span>s4  (ptr, 5)       size=5 [Hello]</span></div></div><div><div><div>5</div></div><div><span>s5  (str, 7)       size=5 [World]</span></div></div><div><div><div>6</div></div><div><span>s6  (str, 7, 3)    size=3 [Wor]</span></div></div><div><div><div>7</div></div><div><span>s7  iterator pair  size=5 [Hello]</span></div></div><div><div><div>8</div></div><div><span>s8  {'H', 'i'}     size=2 [Hi]</span></div></div><div><div><div>9</div></div><div><span>s10 moved-to       size=12 [Hello, World]</span></div></div><div><div><div>10</div></div><div><span>s9  moved-from     size=0 []</span></div></div><div><div><div>11</div></div><div><span>s11 from view      size=3 [cde]</span></div></div><div><div><div>12</div></div><div><span>t1: size=3</span></div></div><div><div><div>13</div></div><div><span>t2: size=2, first char code=3</span></div></div><div><div><div>14</div></div><div><span>u1.size()=1, u2.size()=3</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这一节有三个坑，都写在程序的后半段里：</p><p><strong><code>{}</code> 和 <code>()</code> 不一样。</strong> <code>std::string t1(3, 'x')</code> 是三个 <code>'x'</code>，但 <code>std::string t2{3, 'x'}</code> 优先匹配 <code>initializer_list&lt;char&gt;</code>，<code>3</code> 被当成字符码，得到的是长度为 2 的串，第一个字符的编码是 3。输出里 <code>t2</code> 的 size 是 2，<code>t2[0]</code> 是 3，就是这么来的。</p><p><strong>字面量里的 <code>'\0'</code>。</strong> <code>std::string u1 = "a\0b"</code> 走的是 <code>const char*</code> 构造，到第一个 <code>'\0'</code> 就停了，size 是 1。要得到 3，用 <code>"a\0b"s</code>（C++14，<code>operator""s</code>，需要 <code>using std::string_literals::operator""s;</code>），或者 <code>std::string("a\0b", 3)</code>。</p><p><strong>不要用空指针构造。</strong> <code>std::string(nullptr)</code> 是未定义行为。C++23 把这个构造函数显式 <code>= delete</code> 了，所以 <code>std::string s = nullptr;</code> 在 <code>-std=c++23</code> 下直接编译失败：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: use of deleted function ‘std::basic_string&lt;...&gt;::basic_string(std::nullptr_t)’</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我同样的代码用 <code>-std=c++20</code> 编译，是能通过的（运行时才出事）。</p><p>被移动之后的 string（程序里的 <code>s9</code>）处于“有效但未指定”的状态。libstdc++ 和 libc++ 在这里都给了空串，但别依赖这一点，要复用就先给它重新赋值。</p><p>赋值有三个 <code>operator=</code>（接受 string、<code>const char*</code>、单个 <code>char</code>、<code>initializer_list</code>），外加 <code>assign()</code>。<code>assign()</code> 的重载和构造函数基本对应，区别只是作用在已有对象上：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>s</span><span> </span><span>=</span><span> </span><span>"text"</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"operator=(const char*): "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>s</span><span> </span><span>=</span><span> </span><span>'c'</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"operator=(char):        "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>s</span><span> </span><span>=</span><span> {</span><span>'a'</span><span>, </span><span>'b'</span><span>, </span><span>'c'</span><span>};</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"operator=(init list):   "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>s</span><span>.</span><span>assign</span><span>(</span><span>5</span><span>, </span><span>'z'</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"assign(5, 'z'):         "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>16</div></div><div><span>  </span><span>s</span><span>.</span><span>assign</span><span>(</span><span>"Hello, World"</span><span>, </span><span>5</span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"assign(ptr, 5):         "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>s</span><span>.</span><span>assign</span><span><span>(</span><span>std</span><span>::</span></span><span>string</span><span>(</span><span>"Hello, World"</span><span>), </span><span>7</span><span>, </span><span>5</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"assign(str, 7, 5):      "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>s</span><span>.</span><span>assign</span><span><span>(</span><span>std</span><span>::</span></span><span>string_view</span><span>(</span><span>"abcdef"</span><span>).</span><span>substr</span><span>(</span><span>1</span><span>, </span><span>3</span><span>));</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"assign(view):           "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>operator=(const char*): text</span></div></div><div><div><div>2</div></div><div><span>operator=(char):        c</span></div></div><div><div><div>3</div></div><div><span>operator=(init list):   abc</span></div></div><div><div><div>4</div></div><div><span>assign(5, 'z'):         zzzzz</span></div></div><div><div><div>5</div></div><div><span>assign(ptr, 5):         Hello</span></div></div><div><div><div>6</div></div><div><span>assign(str, 7, 5):      World</span></div></div><div><div><div>7</div></div><div><span>assign(view):           bcd</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>3. 访问元素<a href="#3-访问元素"><span>#</span></a></h2>

<table><thead><tr><th>函数</th><th>越界时</th><th>说明</th></tr></thead><tbody><tr><td><code>s[i]</code></td><td>未定义行为（<code>i == size()</code> 除外）</td><td>不检查，最快</td></tr><tr><td><code>s.at(i)</code></td><td>抛 <code>std::out_of_range</code></td><td>检查 <code>i &lt; size()</code></td></tr><tr><td><code>s.front()</code> / <code>s.back()</code></td><td>空串上未定义行为</td><td>首尾字符的引用</td></tr><tr><td><code>s.data()</code></td><td></td><td>指向首字符的指针，C++17 起有非 const 版本，可以写</td></tr><tr><td><code>s.c_str()</code></td><td></td><td>只读指针，和 <code>data()</code> 指向同一处，以 <code>'\0'</code> 结尾</td></tr><tr><td><code>std::string_view(s)</code></td><td></td><td>隐式转换为 <code>string_view</code></td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;stdexcept&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"Hello"</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"s[1]       = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>[</span><span>1</span><span>] </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"s.at(1)    = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>at</span><span>(</span><span>1</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"front/back = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>front</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"/"</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>back</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"s[size()]  = "</span><span> </span><span>&lt;&lt;</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>s</span><span>[</span><span>s</span><span>.</span><span>size</span><span>()]) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>[[maybe_unused]] </span><span>char</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>at</span><span>(</span><span>10</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>out_of_range</span><span>&amp;</span><span> </span><span>e</span><span>) {</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"at(10) threw: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>e</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>  </span><span>s</span><span>.</span><span>front</span><span>() </span><span>=</span><span> </span><span>'J'</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>s</span><span>[</span><span>s</span><span>.</span><span>size</span><span>() </span><span>-</span><span> </span><span>1</span><span>] </span><span>=</span><span> </span><span>'y'</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"after edits: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>  </span><span>char*</span><span> </span><span>p</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>data</span><span>();</span></div></div><div><div><div>25</div></div><div><span>  </span><span>p</span><span>[</span><span>1</span><span>] </span><span>=</span><span> </span><span>'E'</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"via data():  "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>c_str</span><span>();</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"c_str():     "</span><span> </span><span>&lt;&lt;</span><span> </span><span>c</span></div></div><div><div><div>30</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", c[size()] is NUL: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>c</span><span>[</span><span>s</span><span>.</span><span>size</span><span>()] </span><span>==</span><span> </span><span>'</span><span>\0</span><span>'</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>std::</span><span>string_view</span><span> </span><span>sv</span><span> </span><span>=</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"view:        "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sv</span><span> </span><span>&lt;&lt;</span><span> </span><span>" (size "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sv</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>")</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>s[1]       = e</span></div></div><div><div><div>2</div></div><div><span>s.at(1)    = e</span></div></div><div><div><div>3</div></div><div><span>front/back = H/o</span></div></div><div><div><div>4</div></div><div><span>s[size()]  = 0</span></div></div><div><div><div>5</div></div><div><span>at(10) threw: basic_string::at: __n (which is 10) &gt;= this-&gt;size() (which is 5)</span></div></div><div><div><div>6</div></div><div><span>after edits: Jelly</span></div></div><div><div><div>7</div></div><div><span>via data():  JElly</span></div></div><div><div><div>8</div></div><div><span>c_str():     JElly, c[size()] is NUL: 1</span></div></div><div><div><div>9</div></div><div><span>view:        JElly (size 5)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>几点说明：</p><ul>
<li><code>s[s.size()]</code> 是合法的，返回 <code>'\0'</code>（输出里的 <code>0</code>）。只读它没问题，往里写任何非 <code>'\0'</code> 的值是未定义行为。</li>
<li>C++11 起 <code>data()</code> 和 <code>c_str()</code> 都保证以 <code>'\0'</code> 结尾，两者等价（<code>data()</code> 另有可写的重载）。旧资料里“<code>data()</code> 不保证 <code>'\0'</code>”已经过时。</li>
<li><code>at()</code> 的异常信息是实现自己定的：libstdc++ 会写出下标和长度，libc++ 只写 <code>basic_string</code>。不要解析 <code>what()</code> 的内容。</li>
<li><code>c_str()</code> 和 <code>data()</code> 返回的指针，在 string 被修改或销毁之后就不能再用了，见第 13 节。</li>
</ul></section>
<section><h2>4. 迭代器与算法<a href="#4-迭代器与算法"><span>#</span></a></h2><p>string 提供 <code>begin()</code>/<code>end()</code>、<code>cbegin()</code>/<code>cend()</code>、<code>rbegin()</code>/<code>rend()</code>、<code>crbegin()</code>/<code>crend()</code>。迭代器是随机访问的，C++20 起还是 <code>contiguous_iterator</code>，也就是元素在内存里真的是一个挨一个排的。所以标准算法都能直接用：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;algorithm&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;cctype&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;iterator&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"Hello, World"</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>  </span><span>for</span><span> (</span><span>char</span><span> </span><span>c</span><span> : </span><span>s</span><span>) std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>c</span><span> </span><span>&lt;&lt;</span><span> </span><span>' '</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>  </span><span>for</span><span> (</span><span>auto</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>rbegin</span><span>(); </span><span>it</span><span> </span><span>!=</span><span> </span><span>s</span><span>.</span><span>rend</span><span>(); </span><span>++</span><span>it</span><span>) std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>*</span><span>it</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>  </span><span>for</span><span> (</span><span>char&amp;</span><span> </span><span>c</span><span> : </span><span>s</span><span>) {</span></div></div><div><div><div>17</div></div><div><span>    </span><span>c</span><span> </span><span>=</span><span> </span><span><span>static_cast</span><span>&lt;char&gt;</span></span><span><span>(</span><span>std</span><span>::</span></span><span>toupper</span><span>(</span><span><span>static_cast</span><span>&lt;unsigned</span></span><span> </span><span>char&gt;</span><span>(</span><span>c</span><span>)));</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"upper:   "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>sort</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>(), </span><span>s</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"sorted:  ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>std::</span><span>reverse</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>(), </span><span>s</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"reverse: ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>  </span><span>s</span><span>.</span><span>erase</span><span><span>(</span><span>std</span><span>::</span></span><span>remove</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>(), </span><span>s</span><span>.</span><span>end</span><span>(), </span><span>' '</span><span>), </span><span>s</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"no space:["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>  </span><span>auto</span><span> </span><span>n</span><span> </span><span>=</span><span> std::</span><span>count</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>(), </span><span>s</span><span>.</span><span>end</span><span>(), </span><span>'L'</span><span>);</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"count L: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>n</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span>  </span><span>static_assert</span><span>(std::</span><span>contiguous_iterator</span><span>&lt;</span><span>std::string::</span><span>iterator</span><span>&gt;</span><span>);</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"contiguous: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span><span>&amp;</span><span>*</span></span><span>(</span><span>s</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>1</span><span>) </span><span>==</span><span> </span><span><span>&amp;</span><span>*</span></span><span>s</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>1</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>35</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>H e l l o ,   W o r l d</span></div></div><div><div><div>2</div></div><div><span>dlroW ,olleH</span></div></div><div><div><div>3</div></div><div><span>upper:   HELLO, WORLD</span></div></div><div><div><div>4</div></div><div><span>sorted:  [ ,DEHLLLOORW]</span></div></div><div><div><div>5</div></div><div><span>reverse: [WROOLLLHED, ]</span></div></div><div><div><div>6</div></div><div><span>no space:[WROOLLLHED,]</span></div></div><div><div><div>7</div></div><div><span>count L: 3</span></div></div><div><div><div>8</div></div><div><span>contiguous: 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>范围 <code>for</code> 是遍历的首选。需要改字符时写 <code>char&amp;</code>。</li>
<li><code>std::toupper</code> 的参数是 <code>int</code>，必须是能用 <code>unsigned char</code> 表示的值（或 <code>EOF</code>）。<code>char</code> 在多数平台上有符号，直接传负数是未定义行为，所以要先 <code>static_cast&lt;unsigned char&gt;</code>。</li>
<li>删除所有等于某个值的字符，经典写法是 erase-remove：<code>s.erase(std::remove(s.begin(), s.end(), ' '), s.end())</code>。C++20 起可以直接 <code>std::erase(s, ' ')</code>，见第 6 节。</li>
<li>对 string 排序、反转都是按<strong>字节</strong>操作，遇到 UTF-8 多字节字符会破坏数据，见第 15 节。</li>
</ul></section>
<section><h2>5. 容量<a href="#5-容量"><span>#</span></a></h2>

<table><thead><tr><th>函数</th><th>作用</th></tr></thead><tbody><tr><td><code>size()</code> / <code>length()</code></td><td>字符个数，完全等价</td></tr><tr><td><code>empty()</code></td><td>是否为空，比 <code>size() == 0</code> 更直白</td></tr><tr><td><code>max_size()</code></td><td>理论上限，没有实际意义</td></tr><tr><td><code>capacity()</code></td><td>不重新分配内存就能容纳的字符数，始终 <code>&gt;= size()</code></td></tr><tr><td><code>reserve(n)</code></td><td>保证 <code>capacity() &gt;= n</code>，只会增长</td></tr><tr><td><code>shrink_to_fit()</code></td><td>请求释放多余内存，不保证成功</td></tr><tr><td><code>resize(n)</code> / <code>resize(n, ch)</code></td><td>改变 size，变长时用 <code>'\0'</code> 或 <code>ch</code> 填充</td></tr><tr><td><code>resize_and_overwrite(n, op)</code> (C++23)</td><td>让你直接往缓冲区里写，最后再确定 size</td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;cstdio&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>void</span><span> </span><span>Show</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>what</span><span>, </span></span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>what</span><span> </span><span>&lt;&lt;</span><span> </span><span>": size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>" capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>capacity</span><span>()</span></div></div><div><div><div>7</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"hello"</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>Show</span><span>(</span><span>"initial       "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"empty()="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>empty</span><span>()</span></div></div><div><div><div>14</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" length()==size(): "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>s</span><span>.</span><span>length</span><span>() </span><span>==</span><span> </span><span>s</span><span>.</span><span>size</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"max_size()="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>max_size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>  </span><span>s</span><span>.</span><span>reserve</span><span>(</span><span>100</span><span>);</span></div></div><div><div><div>18</div></div><div><span>  </span><span>Show</span><span>(</span><span>"reserve(100)  "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>19</div></div><div><span>  </span><span>s</span><span>.</span><span>reserve</span><span>(</span><span>10</span><span>);</span></div></div><div><div><div>20</div></div><div><span>  </span><span>Show</span><span>(</span><span>"reserve(10)   "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>s</span><span>.</span><span>shrink_to_fit</span><span>();</span></div></div><div><div><div>22</div></div><div><span>  </span><span>Show</span><span>(</span><span>"shrink_to_fit "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>  </span><span>s</span><span>.</span><span>resize</span><span>(</span><span>8</span><span>, </span><span>'!'</span><span>);</span></div></div><div><div><div>25</div></div><div><span>  </span><span>Show</span><span>(</span><span>"resize(8,'!') "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"  ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>27</div></div><div><span>  </span><span>s</span><span>.</span><span>resize</span><span>(</span><span>3</span><span>);</span></div></div><div><div><div>28</div></div><div><span>  </span><span>Show</span><span>(</span><span>"resize(3)     "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"  ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>30</div></div><div><span>  </span><span>s</span><span>.</span><span>resize</span><span>(</span><span>5</span><span>);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>Show</span><span>(</span><span>"resize(5)     "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"  size()="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>", s[3]="</span><span> </span><span>&lt;&lt;</span><span> </span><span><span>static_cast</span><span>&lt;int&gt;</span></span><span>(</span><span>s</span><span>[</span><span>3</span><span>])</span></div></div><div><div><div>33</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>empty</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"empty.size() - 1 = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>empty</span><span>.</span><span>size</span><span>() </span><span>-</span><span> </span><span>1</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>t</span><span>;</span></div></div><div><div><div>39</div></div><div><span>  </span><span>t</span><span>.</span><span>resize_and_overwrite</span><span>(</span><span>32</span><span>, [](</span><span>char*</span><span><span> </span><span>buf</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>40</div></div><div><span>    </span><span>return</span><span> </span><span><span>static_cast</span><span>&lt;</span></span><span>std::</span><span>size_t&gt;</span><span><span>(</span><span>std</span><span>::</span></span><span>snprintf</span><span>(</span><span>buf</span><span>, </span><span>n</span><span>, </span><span>"</span><span>%d</span><span>-</span><span>%s</span><span>"</span><span>, </span><span>42</span><span>, </span><span>"ok"</span><span>));</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>42</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"resize_and_overwrite: ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>t</span><span> </span><span>&lt;&lt;</span><span> </span><span>"] size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>t</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>43</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>initial       : size=5 capacity=15</span></div></div><div><div><div>2</div></div><div><span>empty()=0 length()==size(): 1</span></div></div><div><div><div>3</div></div><div><span>max_size()=9223372036854775806</span></div></div><div><div><div>4</div></div><div><span>reserve(100)  : size=5 capacity=100</span></div></div><div><div><div>5</div></div><div><span>reserve(10)   : size=5 capacity=100</span></div></div><div><div><div>6</div></div><div><span>shrink_to_fit : size=5 capacity=15</span></div></div><div><div><div>7</div></div><div><span>resize(8,'!') : size=8 capacity=15</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>[hello!!!]</span></div></div><div><div><div>9</div></div><div><span>resize(3)     : size=3 capacity=15</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>[hel]</span></div></div><div><div><div>11</div></div><div><span>resize(5)     : size=5 capacity=15</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>size()=5, s[3]=0</span></div></div><div><div><div>13</div></div><div><span>empty.size() - 1 = 18446744073709551615</span></div></div><div><div><div>14</div></div><div><span>resize_and_overwrite: [42-ok] size=5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>reserve(10)</code> 没有让 capacity 从 100 降下来。C++20 起标准规定 <code>reserve</code> 只增不减（之前是“非约束的缩小请求”），想缩小请用 <code>shrink_to_fit()</code>。这里它把 capacity 降回了 15，也就是回到对象内部的小缓冲区（第 12 节）。</li>
<li>同一个程序在 libc++ 上，初始 capacity 是 22，<code>reserve(100)</code> 之后是 103，<code>max_size()</code> 也不同。capacity 的具体数值是实现细节，只能依赖 <code>capacity() &gt;= size()</code> 和 <code>reserve</code> 之后 <code>capacity() &gt;= n</code>。</li>
<li><code>resize(5)</code> 把 <code>s</code> 从 3 变回 5 时，新增的两个字符是 <code>'\0'</code>，所以 <code>s[3]</code> 是 0，不是原来的 <code>'l'</code>。</li>
<li><code>empty.size() - 1</code> 是 18446744073709551615，因为 <code>size_type</code> 无符号。<code>for (size_t i = 0; i &lt; s.size() - 1; ++i)</code> 在空串上会循环到天荒地老。写成 <code>i + 1 &lt; s.size()</code>。</li>
<li><code>resize_and_overwrite</code> (C++23) 用来对接 C 风格的“写缓冲区”接口（<code>snprintf</code>、<code>read</code> 之类）：先 <code>resize</code> 再覆盖会多做一次无意义的填零，它省掉了这一步。回调拿到缓冲区指针和你请求的大小，返回实际写入的字符数。</li>
</ul></section>
<section><h2>6. 修改<a href="#6-修改"><span>#</span></a></h2><p>修改函数很多，但绝大多数遵循同一套参数约定：<strong>位置用下标 <code>pos</code>，长度用 <code>len</code>，来源可以是 string、C 字符串、字符加次数、迭代器区间</strong>。</p>

<table><thead><tr><th>函数</th><th>常用形式</th><th>说明</th></tr></thead><tbody><tr><td><code>append</code></td><td><code>(str)</code> <code>(str, subpos, sublen)</code> <code>(ptr, n)</code> <code>(n, ch)</code></td><td>追加到末尾</td></tr><tr><td><code>operator+=</code></td><td><code>(str)</code> <code>(ptr)</code> <code>(ch)</code> <code>(init_list)</code></td><td>追加，最常用</td></tr><tr><td><code>push_back</code> / <code>pop_back</code></td><td><code>(ch)</code> / <code>()</code></td><td>追加 / 删除一个字符</td></tr><tr><td><code>insert</code></td><td><code>(pos, str)</code> <code>(pos, n, ch)</code> <code>(it, ch)</code> <code>(pos, str, subpos, sublen)</code></td><td>在 <code>pos</code> 之前插入</td></tr><tr><td><code>erase</code></td><td><code>(pos, len)</code> <code>(it)</code> <code>(first, last)</code></td><td><code>len</code> 缺省为到末尾</td></tr><tr><td><code>replace</code></td><td><code>(pos, len, str)</code> <code>(first, last, str)</code> <code>(pos, len, n, ch)</code></td><td>用新内容替换一段，长度不必相等</td></tr><tr><td><code>clear</code></td><td><code>()</code></td><td>size 变 0，capacity 通常不变</td></tr><tr><td><code>copy</code></td><td><code>(dest, count, pos)</code></td><td>拷到字符数组，<strong>不写 <code>'\0'</code></strong>，返回拷贝的个数</td></tr><tr><td><code>swap</code></td><td><code>(other)</code></td><td>交换内容，不拷贝字符</td></tr><tr><td><code>std::erase</code> / <code>std::erase_if</code> (C++20)</td><td><code>(s, value)</code> / <code>(s, pred)</code></td><td>删除所有匹配的字符，返回删除个数</td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>void</span><span> </span><span>Show</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>op</span><span>, </span></span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>op</span><span> </span><span>&lt;&lt;</span><span> </span><span>" -&gt; ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"Hello"</span><span>;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>  </span><span>s</span><span>.</span><span>insert</span><span>(</span><span>0</span><span>, </span><span>"&gt;&gt; "</span><span>);</span></div></div><div><div><div>12</div></div><div><span>  </span><span>Show</span><span>(</span><span>"insert(0, </span><span>\"</span><span>&gt;&gt; </span><span>\"</span><span>)        "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>s</span><span>.</span><span>insert</span><span>(</span><span>s</span><span>.</span><span>size</span><span>(), </span><span>3</span><span>, </span><span>'!'</span><span>);</span></div></div><div><div><div>14</div></div><div><span>  </span><span>Show</span><span>(</span><span>"insert(size, 3, '!')    "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>15</div></div><div><span>  </span><span>s</span><span>.</span><span>insert</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>3</span><span>, </span><span>'_'</span><span>);</span></div></div><div><div><div>16</div></div><div><span>  </span><span>Show</span><span>(</span><span>"insert(iterator, '_')   "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>s</span><span>.</span><span>insert</span><span>(</span><span>3</span><span>, </span><span>"abcdef"</span><span>, </span><span>1</span><span>, </span><span>2</span><span>);</span></div></div><div><div><div>18</div></div><div><span>  </span><span>Show</span><span>(</span><span>"insert(3, </span><span>\"</span><span>abcdef</span><span>\"</span><span>, 1, 2)"</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>  </span><span>s</span><span>.</span><span>erase</span><span>(</span><span>3</span><span>, </span><span>2</span><span>);</span></div></div><div><div><div>21</div></div><div><span>  </span><span>Show</span><span>(</span><span>"erase(3, 2)             "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>22</div></div><div><span>  </span><span>s</span><span>.</span><span>erase</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>3</span><span>);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>Show</span><span>(</span><span>"erase(iterator)         "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>24</div></div><div><span>  </span><span>s</span><span>.</span><span>erase</span><span>(</span><span>s</span><span>.</span><span>size</span><span>() </span><span>-</span><span> </span><span>3</span><span>);</span></div></div><div><div><div>25</div></div><div><span>  </span><span>Show</span><span>(</span><span>"erase(size - 3) to end  "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>s</span><span>.</span><span>erase</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>(), </span><span>s</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>3</span><span>);</span></div></div><div><div><div>27</div></div><div><span>  </span><span>Show</span><span>(</span><span>"erase(first, last)      "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>  </span><span>s</span><span>.</span><span>push_back</span><span>(</span><span>'!'</span><span>);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>Show</span><span>(</span><span>"push_back('!')          "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>s</span><span>.</span><span>pop_back</span><span>();</span></div></div><div><div><div>32</div></div><div><span>  </span><span>Show</span><span>(</span><span>"pop_back()              "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>  </span><span>s</span><span>.</span><span>append</span><span>(</span><span>", World"</span><span>);</span></div></div><div><div><div>35</div></div><div><span>  </span><span>s</span><span>.</span><span>append</span><span>(</span><span>2</span><span>, </span><span>'.'</span><span>);</span></div></div><div><div><div>36</div></div><div><span>  </span><span>s</span><span>.</span><span>append</span><span>(</span><span>"xyz"</span><span>, </span><span>1</span><span>);</span></div></div><div><div><div>37</div></div><div><span>  </span><span>s</span><span>.</span><span>append</span><span><span>(</span><span>std</span><span>::</span></span><span>string</span><span>(</span><span>"0123456789"</span><span>), </span><span>2</span><span>, </span><span>3</span><span>);</span></div></div><div><div><div>38</div></div><div><span>  </span><span>Show</span><span>(</span><span>"append x4               "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>39</div></div><div><span>  </span><span>s</span><span> </span><span>+=</span><span> </span><span>'?'</span><span>;</span></div></div><div><div><div>40</div></div><div><span>  </span><span>s</span><span> </span><span>+=</span><span> </span><span>" ok"</span><span>;</span></div></div><div><div><div>41</div></div><div><span>  </span><span>Show</span><span>(</span><span>"+= char, +=  C string   "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>"Hello, World"</span><span>;</span></div></div><div><div><div>44</div></div><div><span>  </span><span>r</span><span>.</span><span>replace</span><span>(</span><span>7</span><span>, </span><span>5</span><span>, </span><span>"C++"</span><span>);</span></div></div><div><div><div>45</div></div><div><span>  </span><span>Show</span><span>(</span><span>"replace(7, 5, </span><span>\"</span><span>C++</span><span>\"</span><span>)    "</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>46</div></div><div><span>  </span><span>r</span><span>.</span><span>replace</span><span>(</span><span>r</span><span>.</span><span>begin</span><span>(), </span><span>r</span><span>.</span><span>begin</span><span>() </span><span>+</span><span> </span><span>5</span><span>, </span><span>"Bye"</span><span>);</span></div></div><div><div><div>47</div></div><div><span>  </span><span>Show</span><span>(</span><span>"replace(first,last,str) "</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>48</div></div><div><span>  </span><span>r</span><span>.</span><span>replace</span><span>(</span><span>0</span><span>, </span><span>3</span><span>, </span><span>2</span><span>, </span><span>'#'</span><span>);</span></div></div><div><div><div>49</div></div><div><span>  </span><span>Show</span><span>(</span><span>"replace(0, 3, 2, '#')   "</span><span>, </span><span>r</span><span>);</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>  </span><span>char</span><span> </span><span>buf</span><span>[</span><span>8</span><span>] </span><span>=</span><span> {};</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>n</span><span> </span><span>=</span><span> </span><span>r</span><span>.</span><span>copy</span><span>(</span><span>buf</span><span>, </span><span>3</span><span>, </span><span>4</span><span>);</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"copy(buf, 3, 4) -&gt; "</span><span> </span><span>&lt;&lt;</span><span> </span><span>n</span><span> </span><span>&lt;&lt;</span><span> </span><span>" chars: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>buf</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>"left"</span><span>;</span></div></div><div><div><div>56</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>"right"</span><span>;</span></div></div><div><div><div>57</div></div><div><span>  </span><span>a</span><span>.</span><span>swap</span><span>(</span><span>b</span><span>);</span></div></div><div><div><div>58</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"swap -&gt; a="</span><span> </span><span>&lt;&lt;</span><span> </span><span>a</span><span> </span><span>&lt;&lt;</span><span> </span><span>", b="</span><span> </span><span>&lt;&lt;</span><span> </span><span>b</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>"banana"</span><span>;</span></div></div><div><div><div>61</div></div><div><span>  </span><span>auto</span><span> </span><span>removed</span><span> </span><span>=</span><span> std::</span><span>erase</span><span>(</span><span>c</span><span>, </span><span>'a'</span><span>);</span></div></div><div><div><div>62</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"std::erase(c, 'a') removed "</span><span> </span><span>&lt;&lt;</span><span> </span><span>removed</span><span> </span><span>&lt;&lt;</span><span> </span><span>" -&gt; "</span><span> </span><span>&lt;&lt;</span><span> </span><span>c</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>d</span><span> </span><span>=</span><span> </span><span>"a1b2c3"</span><span>;</span></div></div><div><div><div>64</div></div><div><span><span>  </span></span><span>std::</span><span>erase_if</span><span>(</span><span>d</span><span>, [](</span><span>char</span><span><span> </span><span>ch</span><span>) { </span></span><span>return</span><span> </span><span>ch</span><span> </span><span>&gt;=</span><span> </span><span>'0'</span><span> </span><span>&amp;&amp;</span><span> </span><span>ch</span><span> </span><span>&lt;=</span><span> </span><span>'9'</span><span>; });</span></div></div><div><div><div>65</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"std::erase_if digits -&gt; "</span><span> </span><span>&lt;&lt;</span><span> </span><span>d</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>66</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>insert(0, "&gt;&gt; ")         -&gt; [&gt;&gt; Hello]</span></div></div><div><div><div>2</div></div><div><span>insert(size, 3, '!')     -&gt; [&gt;&gt; Hello!!!]</span></div></div><div><div><div>3</div></div><div><span>insert(iterator, '_')    -&gt; [&gt;&gt; _Hello!!!]</span></div></div><div><div><div>4</div></div><div><span>insert(3, "abcdef", 1, 2) -&gt; [&gt;&gt; bc_Hello!!!]</span></div></div><div><div><div>5</div></div><div><span>erase(3, 2)              -&gt; [&gt;&gt; _Hello!!!]</span></div></div><div><div><div>6</div></div><div><span>erase(iterator)          -&gt; [&gt;&gt; Hello!!!]</span></div></div><div><div><div>7</div></div><div><span>erase(size - 3) to end   -&gt; [&gt;&gt; Hello]</span></div></div><div><div><div>8</div></div><div><span>erase(first, last)       -&gt; [Hello]</span></div></div><div><div><div>9</div></div><div><span>push_back('!')           -&gt; [Hello!]</span></div></div><div><div><div>10</div></div><div><span>pop_back()               -&gt; [Hello]</span></div></div><div><div><div>11</div></div><div><span>append x4                -&gt; [Hello, World..x234]</span></div></div><div><div><div>12</div></div><div><span>+= char, +=  C string    -&gt; [Hello, World..x234? ok]</span></div></div><div><div><div>13</div></div><div><span>replace(7, 5, "C++")     -&gt; [Hello, C++]</span></div></div><div><div><div>14</div></div><div><span>replace(first,last,str)  -&gt; [Bye, C++]</span></div></div><div><div><div>15</div></div><div><span>replace(0, 3, 2, '#')    -&gt; [##, C++]</span></div></div><div><div><div>16</div></div><div><span>copy(buf, 3, 4) -&gt; 3 chars: C++</span></div></div><div><div><div>17</div></div><div><span>swap -&gt; a=right, b=left</span></div></div><div><div><div>18</div></div><div><span>std::erase(c, 'a') removed 3 -&gt; bnn</span></div></div><div><div><div>19</div></div><div><span>std::erase_if digits -&gt; abc</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>pos &gt; size()</code> 时 <code>insert</code>、<code>erase</code>、<code>replace</code>、<code>copy</code> 都会抛 <code>std::out_of_range</code>；<code>pos == size()</code> 是合法的（在末尾插入）。</li>
<li><code>erase(it)</code> 和 <code>erase(first, last)</code> 返回指向被删位置之后的迭代器，在循环里删元素要用返回值。</li>
<li><code>copy</code> 的目标缓冲区要自己保证够大，也要自己补 <code>'\0'</code>。上面的例子里 <code>buf</code> 事先清零，所以能直接当 C 字符串打印。</li>
<li>C++23 还新增了 <code>assign_range</code>、<code>append_range</code>、<code>insert_range</code>、<code>replace_with_range</code>，参数是任意字符区间。</li>
</ul></section>
<section><h2>7. 查找与子串<a href="#7-查找与子串"><span>#</span></a></h2><p>查找函数一共六个，加上三个判断函数。区别只有两点：<strong>找什么</strong>（子串，还是“一组字符里的任意一个”）和<strong>往哪个方向找</strong>：</p>

<table><thead><tr><th>函数</th><th>找什么</th><th>方向</th></tr></thead><tbody><tr><td><code>find(x, pos = 0)</code></td><td>子串 / 单个字符，第一次出现</td><td>从 <code>pos</code> 往后</td></tr><tr><td><code>rfind(x, pos = npos)</code></td><td>子串 / 单个字符，最后一次出现</td><td>匹配<strong>起点</strong>不超过 <code>pos</code></td></tr><tr><td><code>find_first_of(set, pos = 0)</code></td><td><code>set</code> 中<strong>任意一个</strong>字符</td><td>往后</td></tr><tr><td><code>find_last_of(set, pos = npos)</code></td><td><code>set</code> 中任意一个字符</td><td>往前</td></tr><tr><td><code>find_first_not_of(set, pos = 0)</code></td><td><strong>不在</strong> <code>set</code> 中的字符</td><td>往后</td></tr><tr><td><code>find_last_not_of(set, pos = npos)</code></td><td>不在 <code>set</code> 中的字符</td><td>往前</td></tr></tbody></table><p>找不到一律返回 <code>std::string::npos</code>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>void</span><span> </span><span>Show</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>call</span><span>, std::</span></span><span>size_t</span><span><span> </span><span>pos</span><span>) {</span></span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>call</span><span> </span><span>&lt;&lt;</span><span> </span><span>" = "</span><span>;</span></div></div><div><div><div>6</div></div><div><span>  </span><span>if</span><span> (</span><span>pos</span><span> </span><span>==</span><span> std::string::</span><span>npos</span><span>) {</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"npos</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>pos</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"archive.tar.gz"</span><span>;</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find(</span><span>\"</span><span>.</span><span>\"</span><span>)           "</span><span>, </span><span>s</span><span>.</span><span>find</span><span>(</span><span>"."</span><span>));</span></div></div><div><div><div>17</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find(</span><span>\"</span><span>.</span><span>\"</span><span>, 8)        "</span><span>, </span><span>s</span><span>.</span><span>find</span><span>(</span><span>"."</span><span>, </span><span>8</span><span>));</span></div></div><div><div><div>18</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find(</span><span>\"</span><span>zip</span><span>\"</span><span>)         "</span><span>, </span><span>s</span><span>.</span><span>find</span><span>(</span><span>"zip"</span><span>));</span></div></div><div><div><div>19</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find('t', 3)        "</span><span>, </span><span>s</span><span>.</span><span>find</span><span>(</span><span>'t'</span><span>, </span><span>3</span><span>));</span></div></div><div><div><div>20</div></div><div><span>  </span><span>Show</span><span>(</span><span>"rfind(</span><span>\"</span><span>.</span><span>\"</span><span>)          "</span><span>, </span><span>s</span><span>.</span><span>rfind</span><span>(</span><span>"."</span><span>));</span></div></div><div><div><div>21</div></div><div><span>  </span><span>Show</span><span>(</span><span>"rfind(</span><span>\"</span><span>.</span><span>\"</span><span>, 7)       "</span><span>, </span><span>s</span><span>.</span><span>rfind</span><span>(</span><span>"."</span><span>, </span><span>7</span><span>));</span></div></div><div><div><div>22</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find_first_of(</span><span>\"</span><span>aeiou</span><span>\"</span><span>)    "</span><span>, </span><span>s</span><span>.</span><span>find_first_of</span><span>(</span><span>"aeiou"</span><span>));</span></div></div><div><div><div>23</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find_last_of(</span><span>\"</span><span>aeiou</span><span>\"</span><span>)     "</span><span>, </span><span>s</span><span>.</span><span>find_last_of</span><span>(</span><span>"aeiou"</span><span>));</span></div></div><div><div><div>24</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find_first_not_of(</span><span>\"</span><span>arch</span><span>\"</span><span>) "</span><span>, </span><span>s</span><span>.</span><span>find_first_not_of</span><span>(</span><span>"arch"</span><span>));</span></div></div><div><div><div>25</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find_last_not_of(</span><span>\"</span><span>gz</span><span>\"</span><span>)    "</span><span>, </span><span>s</span><span>.</span><span>find_last_not_of</span><span>(</span><span>"gz"</span><span>));</span></div></div><div><div><div>26</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find(</span><span>\"\"</span><span>, 5)         "</span><span>, </span><span>s</span><span>.</span><span>find</span><span>(</span><span>""</span><span>, </span><span>5</span><span>));</span></div></div><div><div><div>27</div></div><div><span>  </span><span>Show</span><span>(</span><span>"find(</span><span>\"\"</span><span>, 99)        "</span><span>, </span><span>s</span><span>.</span><span>find</span><span>(</span><span>""</span><span>, </span><span>99</span><span>));</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>dot</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>rfind</span><span>(</span><span>'.'</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stem="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>dot</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" ext="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>dot</span><span> </span><span>+</span><span> </span><span>1</span><span>)</span></div></div><div><div><div>31</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"substr(8)="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>8</span><span>) </span><span>&lt;&lt;</span><span> </span><span>", substr(14)=["</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>14</span><span>)</span></div></div><div><div><div>33</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>34</div></div><div>
</div></div><div><div><div>35</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>boolalpha</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"starts_with(</span><span>\"</span><span>arch</span><span>\"</span><span>) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>starts_with</span><span>(</span><span>"arch"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"ends_with(</span><span>\"</span><span>.gz</span><span>\"</span><span>)    = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>ends_with</span><span>(</span><span>".gz"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"starts_with('a')     = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>starts_with</span><span>(</span><span>'a'</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"contains(</span><span>\"</span><span>tar</span><span>\"</span><span>)     = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>contains</span><span>(</span><span>"tar"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>find(".")            = 7</span></div></div><div><div><div>2</div></div><div><span>find(".", 8)         = 11</span></div></div><div><div><div>3</div></div><div><span>find("zip")          = npos</span></div></div><div><div><div>4</div></div><div><span>find('t', 3)         = 8</span></div></div><div><div><div>5</div></div><div><span>rfind(".")           = 11</span></div></div><div><div><div>6</div></div><div><span>rfind(".", 7)        = 7</span></div></div><div><div><div>7</div></div><div><span>find_first_of("aeiou")     = 0</span></div></div><div><div><div>8</div></div><div><span>find_last_of("aeiou")      = 9</span></div></div><div><div><div>9</div></div><div><span>find_first_not_of("arch")  = 4</span></div></div><div><div><div>10</div></div><div><span>find_last_not_of("gz")     = 11</span></div></div><div><div><div>11</div></div><div><span>find("", 5)          = 5</span></div></div><div><div><div>12</div></div><div><span>find("", 99)         = npos</span></div></div><div><div><div>13</div></div><div><span>stem=archive.tar ext=gz</span></div></div><div><div><div>14</div></div><div><span>substr(8)=tar.gz, substr(14)=[]</span></div></div><div><div><div>15</div></div><div><span>starts_with("arch") = true</span></div></div><div><div><div>16</div></div><div><span>ends_with(".gz")    = true</span></div></div><div><div><div>17</div></div><div><span>starts_with('a')     = true</span></div></div><div><div><div>18</div></div><div><span>contains("tar")     = true</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>find_first_of("aeiou")</code> 找的是“第一个元音字母”，不是找子串 <code>"aeiou"</code>。这组函数的参数是字符集合，很多人（包括我）一开始都会看错。</li>
<li><code>rfind(".", 7)</code> 的 7 是匹配起点的上限：下标 7 处的 <code>.</code> 刚好能匹配，所以返回 7。</li>
<li>空串总是能在 <code>pos &lt;= size()</code> 的位置找到，所以 <code>find("", 5)</code> 返回 5，<code>find("", 99)</code> 是 <code>npos</code>。</li>
<li>返回值是 <code>size_type</code>。别把它存进 <code>int</code>，也别和 <code>-1</code> 比较，永远写 <code>!= std::string::npos</code>。</li>
<li><code>substr(pos, len)</code> 返回的是拷贝。<code>pos &gt; size()</code> 抛 <code>out_of_range</code>，<code>pos == size()</code> 返回空串，<code>len</code> 太大会被截断到末尾，不会报错。</li>
<li><code>starts_with</code> 和 <code>ends_with</code> 是 C++20。<code>contains</code> 是 <strong>C++23</strong>，我用 <code>-std=c++20</code> 编译 <code>s.contains("name")</code>，GCC 报 <code>error: ‘std::string’ ... has no member named ‘contains’</code>，换成 <code>-std=c++23</code> 才通过。</li>
</ul></section>
<section><h2>8. 比较<a href="#8-比较"><span>#</span></a></h2><p><code>==</code>、<code>!=</code>、<code>&lt;</code>、<code>&lt;=</code>、<code>&gt;</code>、<code>&gt;=</code> 都能直接用，两边可以是 string 或 <code>const char*</code>。C++20 起它们由 <code>operator==</code> 和 <code>operator&lt;=&gt;</code> 合成。它们都做<strong>字典序</strong>比较：从头开始逐字节比，第一个不同的字节决定大小，其中一个是另一个的前缀时，短的更小。</p><p>另有成员函数 <code>compare()</code>，可以比较子串：<code>s.compare(pos, len, other)</code>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;compare&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>"apple"</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>"banana"</span><span>;</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>z</span><span> </span><span>=</span><span> </span><span>"Zebra"</span><span>;</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>boolalpha</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"a == b : "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span> </span><span>==</span><span> </span><span>b</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"a &lt;  b : "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span> </span><span>&lt;</span><span> </span><span>b</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"z &lt;  a : "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>z</span><span> </span><span>&lt;</span><span> </span><span>a</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"  (uppercase sorts first)</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"a == </span><span>\"</span><span>apple</span><span>\"</span><span> : "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span> </span><span>==</span><span> </span><span>"apple"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"a.compare(b) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>a</span><span>.</span><span>compare</span><span>(</span><span>b</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"b.compare(a) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>b</span><span>.</span><span>compare</span><span>(</span><span>a</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"a.compare(a) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>a</span><span>.</span><span>compare</span><span>(</span><span>a</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\"</span><span>app</span><span>\"</span><span> vs a  = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>string</span><span>(</span><span>"app"</span><span>).</span><span>compare</span><span>(</span><span>a</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>path</span><span> </span><span>=</span><span> </span><span>"/usr/local/bin"</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"path.compare(0, 4, </span><span>\"</span><span>/usr</span><span>\"</span><span>) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>path</span><span>.</span><span>compare</span><span>(</span><span>0</span><span>, </span><span>4</span><span>, </span><span>"/usr"</span><span>)</span></div></div><div><div><div>22</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>  </span><span>auto</span><span> </span><span>order</span><span> </span><span>=</span><span> </span><span>a</span><span> </span><span>&lt;=&gt;</span><span> </span><span>b</span><span>;</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"a &lt;=&gt; b is less: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>order</span><span> </span><span>&lt;</span><span> </span><span>0</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>high</span><span> </span><span>=</span><span> </span><span>"</span><span>\xe9</span><span>"</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>low</span><span> </span><span>=</span><span> </span><span>"a"</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\"\\</span><span>xe9</span><span>\"</span><span> &gt; </span><span>\"</span><span>a</span><span>\"</span><span>: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>high</span><span> </span><span>&gt;</span><span> </span><span>low</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>30</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>a == b : false</span></div></div><div><div><div>2</div></div><div><span>a &lt;  b : true</span></div></div><div><div><div>3</div></div><div><span>z &lt;  a : true  (uppercase sorts first)</span></div></div><div><div><div>4</div></div><div><span>a == "apple" : true</span></div></div><div><div><div>5</div></div><div><span>a.compare(b) = -1</span></div></div><div><div><div>6</div></div><div><span>b.compare(a) = 1</span></div></div><div><div><div>7</div></div><div><span>a.compare(a) = 0</span></div></div><div><div><div>8</div></div><div><span>"app" vs a  = -2</span></div></div><div><div><div>9</div></div><div><span>path.compare(0, 4, "/usr") = 0</span></div></div><div><div><div>10</div></div><div><span>a &lt;=&gt; b is less: true</span></div></div><div><div><div>11</div></div><div><span>"\xe9" &gt; "a": true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>大写字母的编码比小写字母小，所以 <code>"Zebra" &lt; "apple"</code>。标准库没有不区分大小写的比较，需要的话自己写比较器。</li>
<li><code>compare()</code> 只保证返回负数、零、正数。libstdc++ 对 <code>"app"</code> 和 <code>"apple"</code> 返回 -2（长度之差），libc++ 返回 -1。<strong>只能判断符号，不要和具体的 -1/1 比较。</strong></li>
<li>字符按 <code>unsigned char</code> 比较（这是 <code>char_traits&lt;char&gt;::compare</code> 规定的）。所以字节 <code>0xE9</code> 比 <code>'a'</code> 大，尽管在 <code>char</code> 有符号的平台上 <code>static_cast&lt;char&gt;(0xE9)</code> 是负数。这保证了 UTF-8 字符串的字节序比较和码点顺序一致。</li>
</ul></section>
<section><h2>9. 拼接、输入输出与格式化<a href="#9-拼接输入输出与格式化"><span>#</span></a></h2><section><h3>拼接<a href="#拼接"><span>#</span></a></h3><p><code>operator+</code> 要求<strong>至少一边是 <code>std::string</code></strong>。两个 <code>const char*</code> 相加是指针运算，不是拼接：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>error: invalid operands of types ‘const char [2]’ and ‘const char [2]’ to binary ‘operator+’</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>解决办法是让其中一个变成 string：<code>std::string("x") + "y"</code>，或者 <code>"x"s + "y"</code>。</p></section><section><h3>输入输出<a href="#输入输出"><span>#</span></a></h3><ul>
<li><code>std::cin &gt;&gt; s</code>(<code>operator&gt;&gt;</code>) 跳过前导空白，读到下一个空白为止。</li>
<li><code>std::getline(in, s)</code> 读一整行，丢弃行尾的 <code>'\n'</code>，不存进 <code>s</code>。第三个参数可以换分隔符。</li>
<li><code>std::format</code> (C++20) 支持对齐和填充：<code>{:&gt;8}</code> 右对齐，<code>{:&lt;8}</code> 左对齐，<code>{:^8}</code> 居中，<code>{:*^9}</code> 用 <code>*</code> 填充居中。</li>
<li>需要拼很多片段时，也可以用 <code>std::ostringstream</code>，最后 <code>.str()</code>。</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;format&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;sstream&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>using</span><span> std::string_literals::</span><span>operator</span><span>""s;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>"foo"</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>"bar"</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span> </span><span>+</span><span> </span><span>b</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span> </span><span>+</span><span> </span><span>"-"</span><span> </span><span>+</span><span> </span><span>b</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span> </span><span>+</span><span> </span><span>'!'</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> (</span><span>"x"</span><span>s</span><span> </span><span>+</span><span> </span><span>"y"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>// auto bad = "x" + "y";  // error: two const char* cannot be added</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>istringstream</span><span> </span><span>in</span><span>(</span><span>"  hello   world 42</span><span>\n</span><span>second line here</span><span>\n</span><span>last,field,x"</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>word</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>in</span><span> </span><span>&gt;&gt;</span><span> </span><span>word</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"operator&gt;&gt; -&gt; ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>word</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>in</span><span> </span><span>&gt;&gt;</span><span> </span><span>word</span><span>;</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"operator&gt;&gt; -&gt; ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>word</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>std::</span><span>getline</span><span>(</span><span>in</span><span>, </span><span>word</span><span>);</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"getline rest of line -&gt; ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>word</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>std::</span><span>getline</span><span>(</span><span>in</span><span>, </span><span>word</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"getline next line -&gt; ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>word</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>std::</span><span>getline</span><span>(</span><span>in</span><span>, </span><span>word</span><span>, </span><span>','</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"getline delim ',' -&gt; ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>word</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>30</div></div><div>
</div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>format</span><span>(</span><span>"[{:&gt;8}] [{:&lt;8}] [{:^8}] [{:*^9}]</span><span>\n</span><span>"</span><span>, </span><span>a</span><span>, </span><span>a</span><span>, </span><span>a</span><span>, </span><span>a</span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>format</span><span>(</span><span>"{} has {} chars</span><span>\n</span><span>"</span><span>, </span><span>b</span><span>, </span><span>b</span><span>.</span><span>size</span><span>());</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>std::</span><span>ostringstream</span><span> </span><span>out</span><span>;</span></div></div><div><div><div>35</div></div><div><span>  </span><span>out</span><span> </span><span>&lt;&lt;</span><span> </span><span>"x="</span><span> </span><span>&lt;&lt;</span><span> </span><span>1</span><span> </span><span>&lt;&lt;</span><span> </span><span>", y="</span><span> </span><span>&lt;&lt;</span><span> </span><span>2.5</span><span>;</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>out</span><span>.</span><span>str</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>37</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>foobar</span></div></div><div><div><div>2</div></div><div><span>foo-bar</span></div></div><div><div><div>3</div></div><div><span>foo!</span></div></div><div><div><div>4</div></div><div><span>xy</span></div></div><div><div><div>5</div></div><div><span>operator&gt;&gt; -&gt; [hello]</span></div></div><div><div><div>6</div></div><div><span>operator&gt;&gt; -&gt; [world]</span></div></div><div><div><div>7</div></div><div><span>getline rest of line -&gt; [ 42]</span></div></div><div><div><div>8</div></div><div><span>getline next line -&gt; [second line here]</span></div></div><div><div><div>9</div></div><div><span>getline delim ',' -&gt; [last]</span></div></div><div><div><div>10</div></div><div><span>[     foo] [foo     ] [  foo   ] [***foo***]</span></div></div><div><div><div>11</div></div><div><span>bar has 3 chars</span></div></div><div><div><div>12</div></div><div><span>x=1, y=2.5</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>getline</code> 那几行值得对照输入看一遍：两次 <code>&gt;&gt;</code> 读走了 <code>hello</code> 和 <code>world</code>，第一次 <code>getline</code> 读的是这一行剩下的 <code> 42</code>（前导空格保留），第二次才是下一整行，第三次用逗号做分隔符，只读到 <code>last</code>。</p></section></section>
<section><h2>10. 与数字互转<a href="#10-与数字互转"><span>#</span></a></h2><p>数字转字符串有三种选择，字符串转数字有两种：</p>

<table><thead><tr><th>方向</th><th>函数</th><th>特点</th></tr></thead><tbody><tr><td>数字 → string</td><td><code>std::to_string</code> (C++11)</td><td>等价于 <code>printf</code> 的 <code>%d</code>、<code>%f</code> 等，浮点数固定 6 位小数</td></tr><tr><td>数字 → string</td><td><code>std::format("{}", x)</code> (C++20)</td><td>浮点数输出最短的、能还原原值的表示</td></tr><tr><td>数字 → 字符数组</td><td><code>std::to_chars</code> (C++17，<code>&lt;charconv&gt;</code>)</td><td>不分配内存，不抛异常，可指定进制</td></tr><tr><td>string → 数字</td><td><code>std::stoi</code> <code>stol</code> <code>stoll</code> <code>stoul</code> <code>stoull</code> <code>stof</code> <code>stod</code> <code>stold</code></td><td>抛异常，跳过前导空白，遇到非法字符就停</td></tr><tr><td>字符数组 → 数字</td><td><code>std::from_chars</code> (C++17)</td><td>不抛异常，不跳空白，返回 <code>ec</code> 和停止位置</td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;charconv&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;climits&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;format&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;stdexcept&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>7</div></div><div><span>#include</span><span> </span><span>&lt;system_error&gt;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>to_string</span><span>(</span><span>42</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>to_string</span><span>(</span><span>-</span><span><span>7</span><span>L</span></span><span>) </span><span>&lt;&lt;</span><span> </span><span>" "</span></div></div><div><div><div>11</div></div><div><span>            </span><span>&lt;&lt;</span><span> std::</span><span>to_string</span><span>(</span><span>3.14</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>to_string</span><span>(</span><span>0.1</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" "</span></div></div><div><div><div>12</div></div><div><span>            </span><span>&lt;&lt;</span><span> std::</span><span>to_string</span><span>(</span><span><span>1</span><span>e</span></span><span>-</span><span>7</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>format</span><span>(</span><span>"{} {} {}"</span><span>, </span><span>3.14</span><span>, </span><span>0.1</span><span>, </span><span><span>1</span><span>e</span></span><span>-</span><span>7</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>used</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stoi(</span><span>\"</span><span>  42abc</span><span>\"</span><span>)  = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>stoi</span><span>(</span><span>"  42abc"</span><span>, </span><span>&amp;</span><span>used</span><span>)</span></div></div><div><div><div>17</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", used "</span><span> </span><span>&lt;&lt;</span><span> </span><span>used</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stoi(</span><span>\"</span><span>ff</span><span>\"</span><span>, 16)   = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>stoi</span><span>(</span><span>"ff"</span><span>, </span><span>nullptr</span><span>, </span><span>16</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stoi(</span><span>\"</span><span>0x1A</span><span>\"</span><span>, 0)  = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>stoi</span><span>(</span><span>"0x1A"</span><span>, </span><span>nullptr</span><span>, </span><span>0</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stoi(</span><span>\"</span><span>052</span><span>\"</span><span>, 0)   = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>stoi</span><span>(</span><span>"052"</span><span>, </span><span>nullptr</span><span>, </span><span>0</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stod(</span><span>\"</span><span>1e3x</span><span>\"</span><span>)     = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>stod</span><span>(</span><span>"1e3x"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stoul(</span><span>\"</span><span>-1</span><span>\"</span><span>)      = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>stoul</span><span>(</span><span>"-1"</span><span>)</span></div></div><div><div><div>23</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" (ULONG_MAX="</span><span> </span><span>&lt;&lt;</span><span> </span><span>ULONG_MAX</span><span> </span><span>&lt;&lt;</span><span> </span><span>")</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>std::</span><span>stoi</span><span>(</span><span>"abc"</span><span>);</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>invalid_argument</span><span>&amp;</span><span> </span><span>e</span><span>) {</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stoi(</span><span>\"</span><span>abc</span><span>\"</span><span>)      -&gt; invalid_argument</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>std::</span><span>stoi</span><span>(</span><span>"3000000000"</span><span>);</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>out_of_range</span><span>&amp;</span><span> </span><span>e</span><span>) {</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"stoi(</span><span>\"</span><span>3000000000</span><span>\"</span><span>) -&gt; out_of_range</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>text</span><span> </span><span>=</span><span> </span><span>"42abc"</span><span>;</span></div></div><div><div><div>37</div></div><div><span>  </span><span>int</span><span> </span><span>value</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>38</div></div><div><span>  </span><span>auto</span><span> [</span><span>ptr</span><span>, </span><span>ec</span><span>] </span><span>=</span></div></div><div><div><div>39</div></div><div><span><span>      </span></span><span>std::</span><span>from_chars</span><span>(</span><span>text</span><span>.</span><span>data</span><span>(), </span><span>text</span><span>.</span><span>data</span><span>() </span><span>+</span><span> </span><span>text</span><span>.</span><span>size</span><span>(), </span><span>value</span><span>);</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"from_chars(</span><span>\"</span><span>42abc</span><span>\"</span><span>): value="</span><span> </span><span>&lt;&lt;</span><span> </span><span>value</span></div></div><div><div><div>41</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", ec ok="</span><span> </span><span>&lt;&lt;</span><span> (</span><span>ec</span><span> </span><span>==</span><span> std::</span><span>errc</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>", rest="</span><span> </span><span>&lt;&lt;</span><span> </span><span>ptr</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>42</div></div><div>
</div></div><div><div><div>43</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>bad</span><span> </span><span>=</span><span> </span><span>"abc"</span><span>;</span></div></div><div><div><div>44</div></div><div><span>  </span><span>auto</span><span> [</span><span>ptr2</span><span>, </span><span>ec2</span><span>] </span><span>=</span></div></div><div><div><div>45</div></div><div><span><span>      </span></span><span>std::</span><span>from_chars</span><span>(</span><span>bad</span><span>.</span><span>data</span><span>(), </span><span>bad</span><span>.</span><span>data</span><span>() </span><span>+</span><span> </span><span>bad</span><span>.</span><span>size</span><span>(), </span><span>value</span><span>);</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"from_chars(</span><span>\"</span><span>abc</span><span>\"</span><span>): invalid_argument="</span></div></div><div><div><div>47</div></div><div><span>            </span><span>&lt;&lt;</span><span> (</span><span>ec2</span><span> </span><span>==</span><span> std::errc::</span><span>invalid_argument</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>spaced</span><span> </span><span>=</span><span> </span><span>" 7"</span><span>;</span></div></div><div><div><div>50</div></div><div><span>  </span><span>auto</span><span> [</span><span>ptr3</span><span>, </span><span>ec3</span><span>] </span><span>=</span></div></div><div><div><div>51</div></div><div><span><span>      </span></span><span>std::</span><span>from_chars</span><span>(</span><span>spaced</span><span>.</span><span>data</span><span>(), </span><span>spaced</span><span>.</span><span>data</span><span>() </span><span>+</span><span> </span><span>spaced</span><span>.</span><span>size</span><span>(), </span><span>value</span><span>);</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"from_chars(</span><span>\"</span><span> 7</span><span>\"</span><span>): invalid_argument="</span></div></div><div><div><div>53</div></div><div><span>            </span><span>&lt;&lt;</span><span> (</span><span>ec3</span><span> </span><span>==</span><span> std::errc::</span><span>invalid_argument</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>  </span><span>char</span><span> </span><span>buf</span><span>[</span><span>32</span><span>];</span></div></div><div><div><div>56</div></div><div><span>  </span><span>auto</span><span> [</span><span>end</span><span>, </span><span>ec4</span><span>] </span><span>=</span><span> std::</span><span>to_chars</span><span>(</span><span>buf</span><span>, </span><span>buf</span><span> </span><span>+</span><span> </span><span>sizeof</span><span>(</span><span>buf</span><span>), </span><span>255</span><span>, </span><span>16</span><span>);</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"to_chars(255, 16) = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>string</span><span>(</span><span>buf</span><span>, </span><span>end</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>58</div></div><div><span>  </span><span>auto</span><span> [</span><span>end2</span><span>, </span><span>ec5</span><span>] </span><span>=</span><span> std::</span><span>to_chars</span><span>(</span><span>buf</span><span>, </span><span>buf</span><span> </span><span>+</span><span> </span><span>sizeof</span><span>(</span><span>buf</span><span>), </span><span>0.1</span><span>);</span></div></div><div><div><div>59</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"to_chars(0.1)     = "</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>string</span><span>(</span><span>buf</span><span>, </span><span>end2</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>60</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>42 -7 3.140000 0.100000 0.000000</span></div></div><div><div><div>2</div></div><div><span>3.14 0.1 1e-07</span></div></div><div><div><div>3</div></div><div><span>stoi("  42abc")  = 42, used 4</span></div></div><div><div><div>4</div></div><div><span>stoi("ff", 16)   = 255</span></div></div><div><div><div>5</div></div><div><span>stoi("0x1A", 0)  = 26</span></div></div><div><div><div>6</div></div><div><span>stoi("052", 0)   = 42</span></div></div><div><div><div>7</div></div><div><span>stod("1e3x")     = 1000</span></div></div><div><div><div>8</div></div><div><span>stoul("-1")      = 18446744073709551615 (ULONG_MAX=18446744073709551615)</span></div></div><div><div><div>9</div></div><div><span>stoi("abc")      -&gt; invalid_argument</span></div></div><div><div><div>10</div></div><div><span>stoi("3000000000") -&gt; out_of_range</span></div></div><div><div><div>11</div></div><div><span>from_chars("42abc"): value=42, ec ok=1, rest=abc</span></div></div><div><div><div>12</div></div><div><span>from_chars("abc"): invalid_argument=1</span></div></div><div><div><div>13</div></div><div><span>from_chars(" 7"): invalid_argument=1</span></div></div><div><div><div>14</div></div><div><span>to_chars(255, 16) = ff</span></div></div><div><div><div>15</div></div><div><span>to_chars(0.1)     = 0.1</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><ul>
<li><code>to_string(0.1)</code> 是 <code>"0.100000"</code>，<code>to_string(1e-7)</code> 是 <code>"0.000000"</code>，精度全丢了。要完整输出用 <code>std::format</code> 或 <code>to_chars</code>。</li>
<li><code>stoi("  42abc")</code> 返回 42，<strong>并不会因为尾部的 <code>abc</code> 抛异常</strong>，只能通过第二个参数（<code>used</code>，停在下标 4）发现还有剩余。只有开头就没有数字才抛 <code>invalid_argument</code>，数值超出范围抛 <code>out_of_range</code>。</li>
<li>第三个参数是进制，<code>0</code> 表示按前缀自动判断（<code>0x</code> 十六进制、<code>0</code> 开头八进制）。</li>
<li><code>stoul("-1")</code> 不会失败，而是回绕成 <code>ULONG_MAX</code>。这是 <code>strtoul</code> 的行为，<code>stoul</code> 原样继承。</li>
<li><code>from_chars</code> 不跳空白，所以 <code>" 7"</code> 直接是 <code>invalid_argument</code>。要校验整串是否都是数字，检查返回的 <code>ptr == text.data() + text.size()</code> 和 <code>ec == std::errc()</code>。</li>
<li>选择：解析用户输入、不希望异常影响流程时用 <code>from_chars</code>；一次性脚本、能接受异常时 <code>stoi</code> 更省事。</li>
</ul></section>
<section><h2>11. string 与 string_view<a href="#11-string-与-string_view"><span>#</span></a></h2><p><code>std::string_view</code> (C++17) 是“指针加长度”的只读视图，不拥有字符，拷贝和 <code>substr</code> 都不分配内存。它是 string 的搭档：<strong>函数参数只读 string 时，用 <code>string_view</code></strong>。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;cstdio&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;functional&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;unordered_map&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>struct</span><span> </span><span>StringHash</span><span> {</span></div></div><div><div><div>9</div></div><div><span>  </span><span>using</span><span> </span><span>is_transparent</span><span> </span><span>=</span><span> </span><span>void</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>operator</span><span>()(std::</span><span>string_view</span><span><span> </span><span>sv</span><span>) </span></span><span>const</span><span> {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>return</span><span> std::</span><span>hash</span><span>&lt;</span><span>std::</span><span>string_view</span><span>&gt;</span><span>{}(</span><span>sv</span><span>);</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>};</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>void</span><span> </span><span>Greet</span><span>(std::</span><span>string_view</span><span><span> </span><span>name</span><span>) {</span></span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"hello, "</span><span> </span><span>&lt;&lt;</span><span> </span><span>name</span><span> </span><span>&lt;&lt;</span><span> </span><span>" ("</span><span> </span><span>&lt;&lt;</span><span> </span><span>name</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>")</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"world"</span><span>;</span></div></div><div><div><div>21</div></div><div><span>  </span><span>Greet</span><span>(</span><span>s</span><span>);</span></div></div><div><div><div>22</div></div><div><span>  </span><span>Greet</span><span>(</span><span>"literal"</span><span>);</span></div></div><div><div><div>23</div></div><div><span>  </span><span>Greet</span><span><span>(</span><span>std</span><span>::</span></span><span>string_view</span><span>(</span><span>s</span><span>).</span><span>substr</span><span>(</span><span>1</span><span>, </span><span>3</span><span>));</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>std::</span><span>string_view</span><span> </span><span>sv</span><span> </span><span>=</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>26</div></div><div><span><span>  </span></span><span>// std::string t = sv;     // error: the conversion is explicit</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>t</span><span>(</span><span>sv</span><span>);</span></div></div><div><div><div>28</div></div><div><span>  </span><span>t</span><span> </span><span>+=</span><span> </span><span>sv</span><span>;</span></div></div><div><div><div>29</div></div><div><span>  </span><span>t</span><span>.</span><span>append</span><span>(</span><span>sv</span><span>.</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>2</span><span>));</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>t</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>std::</span><span>string_view</span><span> </span><span>word</span><span> </span><span>=</span><span> std::</span><span>string_view</span><span>(</span><span>"hello world"</span><span>).</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>5</span><span>);</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span><span>"size=</span><span>%zu</span><span>, printf(</span><span>\"</span><span>%%</span><span>s</span><span>\"</span><span>) prints: </span><span>%s</span><span>\n</span><span>"</span><span>, </span><span>word</span><span>.</span><span>size</span><span>(),</span></div></div><div><div><div>34</div></div><div><span>              </span><span>word</span><span>.</span><span>data</span><span>());</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>std::</span><span>unordered_map</span><span>&lt;std::</span><span>string</span><span>, </span><span>int</span><span>, </span><span>StringHash</span><span>, std::</span><span>equal_to</span><span>&lt;&gt;&gt; </span><span>counts</span><span>;</span></div></div><div><div><div>37</div></div><div><span>  </span><span>counts</span><span>[</span><span>"apple"</span><span>] </span><span>=</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>std::</span><span>string_view</span><span> </span><span>key</span><span> </span><span>=</span><span> </span><span>"apple"</span><span>;</span></div></div><div><div><div>39</div></div><div><span>  </span><span>auto</span><span> </span><span>it</span><span> </span><span>=</span><span> </span><span>counts</span><span>.</span><span>find</span><span>(</span><span>key</span><span>);</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"lookup by view: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>it</span><span> </span><span>!=</span><span> </span><span>counts</span><span>.</span><span>end</span><span>() </span><span>?</span><span> </span><span>it</span><span>-&gt;</span><span>second</span><span> </span><span>:</span><span> </span><span>-</span><span>1</span><span>)</span></div></div><div><div><div>41</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>hello, world (5)</span></div></div><div><div><div>2</div></div><div><span>hello, literal (7)</span></div></div><div><div><div>3</div></div><div><span>hello, orl (3)</span></div></div><div><div><div>4</div></div><div><span>worldworldwo</span></div></div><div><div><div>5</div></div><div><span>size=5, printf("%s") prints: hello world</span></div></div><div><div><div>6</div></div><div><span>lookup by view: 1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>string 到 <code>string_view</code> 是<strong>隐式</strong>转换，反过来是<strong>显式</strong>的 (<code>std::string t(sv)</code>)，因为要分配内存拷贝。<code>std::string t = sv;</code> 不能编译。</li>
<li><code>string + string_view</code> 在 C++23 下也不能编译（GCC 报 <code>no match for ‘operator+’</code>），要用 <code>+=</code> 或 <code>append</code>，上面的 <code>t += sv</code> 和 <code>t.append(...)</code> 就是这么用的。</li>
<li><code>string_view</code> <strong>不保证以 <code>'\0'</code> 结尾</strong>。程序里 <code>word</code> 是 <code>"hello world"</code> 的前 5 个字符，但 <code>word.data()</code> 交给 <code>printf("%s")</code> 会一直打印到字面量的 <code>'\0'</code>，输出的是整个 <code>hello world</code>。把它传给 C 接口前，必须先构造成 string。</li>
<li><code>std::unordered_map&lt;std::string, ...&gt;</code> 默认查找必须构造一个 string 作为 key。C++20 起可以配上透明哈希和 <code>std::equal_to&lt;&gt;</code>，直接用 <code>string_view</code> 查找，避免临时 string，程序最后几行就是这个写法。</li>
</ul><p><code>string_view</code> 的代价是所有权：谁来保证它指向的内存还活着，是调用者的事。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>std::</span><span>string</span><span> </span><span>MakeName</span><span>() { </span><span>return</span><span> std::</span><span>string</span><span>(</span><span>30</span><span>, </span><span>'a'</span><span>) </span><span>+</span><span> </span><span>".txt"</span><span>; }</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>string_view</span><span> </span><span>sv</span><span> </span><span>=</span><span> </span><span>MakeName</span><span>();</span><span>  // BAD: the temporary dies at the ';'.</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>sv</span><span>.</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>5</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 <code>-g -fsanitize=address</code> 编译运行，输出开头是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>==719322==ERROR: AddressSanitizer: heap-use-after-free on address 0x6d03f1de0020 at pc 0x70a3f328835f bp 0x7ffc744fcee0 sp 0x7ffc744fc6b0</span></div></div><div><div><div>2</div></div><div><span>READ of size 5 at 0x6d03f1de0020 thread T0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>MakeName()</code> 返回的是一个 34 字节的临时 string，超过 SSO 的容量，字符在堆上；临时对象在分号处销毁，<code>sv</code> 就指向了已释放的内存。如果名字短到能放进 SSO（比如 <code>"a.txt"</code>），字符在临时对象内部，同样是悬空，ASan 会报 <code>stack-use-after-scope</code> 而不是 <code>heap-use-after-free</code>。</p><p>规则很简单：<code>string_view</code> 当函数参数随便用；要把它存进成员、放进容器或者当返回值，先问一句“谁是 owner”。</p></section>
<section><h2>12. 内部实现：SSO、扩容、拷贝与移动<a href="#12-内部实现sso扩容拷贝与移动"><span>#</span></a></h2><section><h3>小字符串优化<a href="#小字符串优化"><span>#</span></a></h3><p>每次创建字符串都 <code>malloc</code> 一次太贵，而绝大多数字符串都很短。所以实现把一小块缓冲区直接放进 string 对象里：字符串短的时候，字符就存在对象内部（口袋），长到放不下才去堆上申请（仓库），对象里只留一个指针（取货单）。这就是 SSO(Small String Optimization)。</p><p>读 libstdc++ 的 <code>bits/basic_string.h</code>，<code>sizeof(std::string)</code> 的 32 字节是：8 字节指针、8 字节 size，加一个 16 字节的 union，里面要么是 15 个字符加 <code>'\0'</code> 的本地缓冲区，要么是堆缓冲区的 capacity。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;cstdint&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>bool</span><span> </span><span>IsInline</span><span>(</span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>  </span><span>auto</span><span> </span><span>begin</span><span> </span><span>=</span><span> </span><span><span>reinterpret_cast</span><span>&lt;</span></span><span>std::</span><span>uintptr_t&gt;</span><span>(</span><span>&amp;</span><span>s</span><span>);</span></div></div><div><div><div>7</div></div><div><span>  </span><span>auto</span><span> </span><span>data</span><span> </span><span>=</span><span> </span><span><span>reinterpret_cast</span><span>&lt;</span></span><span>std::</span><span>uintptr_t&gt;</span><span>(</span><span>s</span><span>.</span><span>data</span><span>());</span></div></div><div><div><div>8</div></div><div><span>  </span><span>return</span><span> </span><span>data</span><span> </span><span>&gt;=</span><span> </span><span>begin</span><span> </span><span>&amp;&amp;</span><span> </span><span>data</span><span> </span><span>&lt;</span><span> </span><span>begin</span><span> </span><span>+</span><span> </span><span>sizeof</span><span>(</span><span>s</span><span>);</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"sizeof(std::string) = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>sizeof</span><span>(std::</span><span>string</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"empty: capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>capacity</span><span>() </span><span>&lt;&lt;</span><span> </span><span>" inline="</span><span> </span><span>&lt;&lt;</span><span> </span><span>IsInline</span><span>(</span><span>s</span><span>)</span></div></div><div><div><div>16</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>70</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>std::</span><span>size_t</span><span> </span><span>old_capacity</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>capacity</span><span>();</span></div></div><div><div><div>20</div></div><div><span>    </span><span>s</span><span>.</span><span>push_back</span><span>(</span><span>'x'</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>if</span><span> (</span><span>s</span><span>.</span><span>capacity</span><span>() </span><span>!=</span><span> </span><span>old_capacity</span><span>) {</span></div></div><div><div><div>22</div></div><div><span><span>      </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"size="</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>": capacity "</span><span> </span><span>&lt;&lt;</span><span> </span><span>old_capacity</span></div></div><div><div><div>23</div></div><div><span>                </span><span>&lt;&lt;</span><span> </span><span>" -&gt; "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>capacity</span><span>() </span><span>&lt;&lt;</span><span> </span><span>", inline="</span><span> </span><span>&lt;&lt;</span><span> </span><span>IsInline</span><span>(</span><span>s</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>exact</span><span>(</span><span>20</span><span>, </span><span>'a'</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"string(20, 'a'): capacity="</span><span> </span><span>&lt;&lt;</span><span> </span><span>exact</span><span>.</span><span>capacity</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sizeof(std::string) = 32</span></div></div><div><div><div>2</div></div><div><span>empty: capacity=15 inline=1</span></div></div><div><div><div>3</div></div><div><span>size=16: capacity 15 -&gt; 30, inline=0</span></div></div><div><div><div>4</div></div><div><span>size=31: capacity 30 -&gt; 60, inline=0</span></div></div><div><div><div>5</div></div><div><span>size=61: capacity 60 -&gt; 120, inline=0</span></div></div><div><div><div>6</div></div><div><span>string(20, 'a'): capacity=20</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个程序在 Clang 21.1.8 加 libc++ 上的结果：</p>

<table><thead><tr><th>项目</th><th>libstdc++ (GCC 15.3.0)</th><th>libc++ (Clang 21.1.8)</th></tr></thead><tbody><tr><td><code>sizeof(std::string)</code></td><td>32</td><td>24</td></tr><tr><td>空串的 capacity</td><td>15</td><td>22</td></tr><tr><td>增长路径</td><td>15 → 30 → 60 → 120</td><td>22 → 47 → 95</td></tr><tr><td><code>std::string(20, 'a')</code> 的 capacity</td><td>20（堆上）</td><td>22（还在对象内部）</td></tr></tbody></table><p>结论：SSO 的容量取决于标准库实现，15 和 22 都不是标准规定的。扩容都近似翻倍，这保证了 <code>push_back</code> 的均摊复杂度是 <span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>。</p></section><section><h3>拷贝与移动<a href="#拷贝与移动"><span>#</span></a></h3><p>SSO 有一个反直觉的后果：<strong>移动一个短字符串其实是拷贝</strong>，因为数据就在对象里，没有指针可偷。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;type_traits&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;utility&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>static_assert</span><span>(std::</span><span>is_nothrow_move_constructible_v</span><span>&lt;</span><span>std::</span><span>string</span><span>&gt;</span><span>);</span></div></div><div><div><div>8</div></div><div><span>static_assert</span><span>(std::</span><span>is_nothrow_move_assignable_v</span><span>&lt;</span><span>std::</span><span>string</span><span>&gt;</span><span>);</span></div></div><div><div><div>9</div></div><div><span>static_assert</span><span>(std::</span><span>is_nothrow_swappable_v</span><span>&lt;</span><span>std::</span><span>string</span><span>&gt;</span><span>);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> std::</span><span>boolalpha</span><span>;</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>small</span><span> </span><span>=</span><span> </span><span>"hello"</span><span>;</span></div></div><div><div><div>15</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>big</span><span>(</span><span>40</span><span>, </span><span>'x'</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>small_copy</span><span> </span><span>=</span><span> </span><span>small</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>big_copy</span><span> </span><span>=</span><span> </span><span>big</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"copy, small: shares buffer = "</span></div></div><div><div><div>19</div></div><div><span>            </span><span>&lt;&lt;</span><span> (</span><span>small</span><span>.</span><span>data</span><span>() </span><span>==</span><span> </span><span>small_copy</span><span>.</span><span>data</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"copy, big:   shares buffer = "</span></div></div><div><div><div>21</div></div><div><span>            </span><span>&lt;&lt;</span><span> (</span><span>big</span><span>.</span><span>data</span><span>() </span><span>==</span><span> </span><span>big_copy</span><span>.</span><span>data</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>"hello"</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>b</span><span>(</span><span>40</span><span>, </span><span>'x'</span><span>);</span></div></div><div><div><div>25</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>a_ptr</span><span> </span><span>=</span><span> </span><span>a</span><span>.</span><span>data</span><span>();</span></div></div><div><div><div>26</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>b_ptr</span><span> </span><span>=</span><span> </span><span>b</span><span>.</span><span>data</span><span>();</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>a_moved</span><span> </span><span>=</span><span> std::</span><span>move</span><span>(</span><span>a</span><span>);</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>b_moved</span><span> </span><span>=</span><span> std::</span><span>move</span><span>(</span><span>b</span><span>);</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"move, small: steals buffer = "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a_moved</span><span>.</span><span>data</span><span>() </span><span>==</span><span> </span><span>a_ptr</span><span>)</span></div></div><div><div><div>30</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", moved-from size = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>a</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"move, big:   steals buffer = "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>b_moved</span><span>.</span><span>data</span><span>() </span><span>==</span><span> </span><span>b_ptr</span><span>)</span></div></div><div><div><div>32</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", moved-from size = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>b</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>string</span><span>&gt; </span><span>v</span><span>;</span></div></div><div><div><div>35</div></div><div><span>  </span><span>v</span><span>.</span><span>push_back</span><span><span>(</span><span>std</span><span>::</span></span><span>string</span><span>(</span><span>40</span><span>, </span><span>'y'</span><span>));</span></div></div><div><div><div>36</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>before</span><span> </span><span>=</span><span> </span><span>v</span><span>[</span><span>0</span><span>].</span><span>data</span><span>();</span></div></div><div><div><div>37</div></div><div><span>  </span><span>v</span><span>.</span><span>reserve</span><span>(</span><span>1000</span><span>);</span><span>  // reallocates the vector's storage</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"vector regrowth keeps the string's buffer = "</span></div></div><div><div><div>39</div></div><div><span>            </span><span>&lt;&lt;</span><span> (</span><span>v</span><span>[</span><span>0</span><span>].</span><span>data</span><span>() </span><span>==</span><span> </span><span>before</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>copy, small: shares buffer = false</span></div></div><div><div><div>2</div></div><div><span>copy, big:   shares buffer = false</span></div></div><div><div><div>3</div></div><div><span>move, small: steals buffer = false, moved-from size = 0</span></div></div><div><div><div>4</div></div><div><span>move, big:   steals buffer = true, moved-from size = 0</span></div></div><div><div><div>5</div></div><div><span>vector regrowth keeps the string's buffer = true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>拷贝：不管长短，都有独立的缓冲区。长串是 <code>malloc</code> 加 <code>memcpy</code>，短串是拷对象内部的字节。</li>
<li>移动大串：直接偷走指针，<span><span>O(1)O(1)</span><span><span><span></span><span>O</span><span>(</span><span>1</span><span>)</span></span></span></span>。</li>
<li>移动小串：只能拷。所以“需要所有权就按值传参再 <code>std::move</code>”这条建议仍然成立，但对短串它不是白送的。</li>
<li>移动构造、移动赋值和 <code>swap</code> 都是 <code>noexcept</code>（程序开头的 <code>static_assert</code> 验证了）。所以 <code>std::vector&lt;std::string&gt;</code> 重新分配存储时会移动元素而不是拷贝：程序最后几行里，<code>reserve</code> 之后元素的缓冲区地址没变。</li>
</ul></section><section><h3>COW：旧 ABI 里的 8 字节 string<a href="#cow旧-abi-里的-8-字节-string"><span>#</span></a></h3><p>GCC 5.1 之前，libstdc++ 的 string 是引用计数的 COW (Copy-On-Write) 实现。C++11 的迭代器和引用失效规则不再允许它，GCC 5.1 因此引入了新实现（也就是上面的 32 字节加 SSO），旧实现通过 <code>_GLIBCXX_USE_CXX11_ABI=0</code> 保留，详见 <a href="https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html" target="_blank">libstdc++ 手册的 Dual ABI 一节</a>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>a</span><span>(</span><span>40</span><span>, </span><span>'x'</span><span>);</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>b</span><span> </span><span>=</span><span> </span><span>a</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"ABI="</span><span> </span><span>&lt;&lt;</span><span> </span><span>_GLIBCXX_USE_CXX11_ABI</span><span> </span><span>&lt;&lt;</span><span> </span><span>", sizeof="</span><span> </span><span>&lt;&lt;</span><span> </span><span>sizeof</span><span>(</span><span>a</span><span>)</span></div></div><div><div><div>8</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", copy shares buffer: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span>.</span><span>c_str</span><span>() </span><span>==</span><span> </span><span>b</span><span>.</span><span>c_str</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>9</div></div><div><span>  </span><span>b</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>'y'</span><span>;</span><span>  // The non-const operator[] detaches b.</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"after write, shares buffer: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>a</span><span>.</span><span>c_str</span><span>() </span><span>==</span><span> </span><span>b</span><span>.</span><span>c_str</span><span>())</span></div></div><div><div><div>11</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", a[0]="</span><span> </span><span>&lt;&lt;</span><span> </span><span>a</span><span>[</span><span>0</span><span>] </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>分别用 <code>-D_GLIBCXX_USE_CXX11_ABI=1</code> 和 <code>=0</code> 编译，输出是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ABI=1, sizeof=32, copy shares buffer: 0</span></div></div><div><div><div>2</div></div><div><span>after write, shares buffer: 0, a[0]=x</span></div></div><div><div><div>3</div></div><div><span>ABI=0, sizeof=8, copy shares buffer: 1</span></div></div><div><div><div>4</div></div><div><span>after write, shares buffer: 0, a[0]=x</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>旧 ABI 下对象只有一个 8 字节的指针；拷贝只是共享缓冲区，第一次写才真正复制。我最早写这个实验时拿到的是“不共享”，因为我对非 const 的 <code>b</code> 调用了 <code>data()</code>，COW 实现里非 const 访问会先把缓冲区分离出来。今天默认用的是新 ABI，只有链接旧的二进制或库时才会撞上旧的，链接报错里出现 <code>__cxx11</code> 就是两个 ABI 混用了。</p></section></section>
<section><h2>13. 迭代器、指针和引用什么时候失效<a href="#13-迭代器指针和引用什么时候失效"><span>#</span></a></h2><p>string 的字符存在一块可能被重新分配的缓冲区里，所以任何指向它的迭代器、指针（包括 <code>c_str()</code> 和 <code>data()</code> 的结果）和引用，都可能在你修改 string 之后变成野指针。标准规定，它们<strong>只会</strong>被这些操作作废：</p><ul>
<li>调用任何以<strong>非 const <code>std::string&amp;</code></strong> 为参数的标准库函数，例如 <code>std::getline</code>、<code>std::swap</code>、<code>operator&gt;&gt;</code>。</li>
<li>调用任何<strong>非 const 成员函数</strong>，<strong>除了</strong> <code>operator[]</code>、<code>at</code>、<code>data</code>、<code>front</code>、<code>back</code>、<code>begin</code>、<code>rbegin</code>、<code>end</code>、<code>rend</code>。</li>
</ul><p>也就是说 <code>push_back</code>、<code>append</code>、<code>insert</code>、<code>erase</code>、<code>resize</code>、<code>reserve</code>、<code>shrink_to_fit</code>、<code>clear</code>、<code>assign</code>、<code>operator=</code> 都可能让旧指针失效，哪怕你觉得“反正容量够”。所有 const 成员函数（<code>size</code>、<code>find</code>、<code>substr</code>、<code>c_str</code>……）都不会。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span>(</span><span>40</span><span>, </span><span>'a'</span><span>);</span></div></div><div><div><div>6</div></div><div><span>  </span><span>char&amp;</span><span> </span><span>first</span><span> </span><span>=</span><span> </span><span>s</span><span>[</span><span>0</span><span>];</span></div></div><div><div><div>7</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>p</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>c_str</span><span>();</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>  </span><span>s</span><span>.</span><span>append</span><span>(</span><span>1000</span><span>, </span><span>'b'</span><span>);</span><span>  // exceeds capacity: the buffer is reallocated</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"same buffer: "</span><span> </span><span>&lt;&lt;</span><span> (</span><span>p</span><span> </span><span>==</span><span> </span><span>s</span><span>.</span><span>c_str</span><span>()) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>12</div></div><div><span>  </span><span>first</span><span> </span><span>=</span><span> </span><span>'z'</span><span>;</span><span>  // BAD: first refers to the freed buffer</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>[</span><span>0</span><span>] </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 ASan 运行：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>==719362==ERROR: AddressSanitizer: heap-use-after-free on address 0x771dee1e0010 at pc 0x5b7c60ad67c7 bp 0x7ffe70b04bb0 sp 0x7ffe70b04ba8</span></div></div><div><div><div>2</div></div><div><span>WRITE of size 1 at 0x771dee1e0010 thread T0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不加 ASan 直接运行，程序也不会崩溃：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>same buffer: 0</span></div></div><div><div><div>2</div></div><div><span>a</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它悄悄写进了已释放的内存，后面读到的还是 <code>a</code>，没有任何症状。这类 bug 只有在内存被别人重用时才爆发。需要稳定引用字符串里的某个位置时，保存下标，不要保存指针、引用或迭代器。</p></section>
<section><h2>14. 异常<a href="#14-异常"><span>#</span></a></h2>

<table><thead><tr><th>情况</th><th>结果</th></tr></thead><tbody><tr><td><code>at(pos)</code>，<code>pos &gt;= size()</code></td><td><code>std::out_of_range</code></td></tr><tr><td><code>operator[]</code>，<code>pos &gt; size()</code></td><td>未定义行为，不抛异常</td></tr><tr><td><code>substr</code> / <code>insert</code> / <code>erase</code> / <code>replace</code> / <code>copy</code>，<code>pos &gt; size()</code></td><td><code>std::out_of_range</code></td></tr><tr><td><code>find</code> 系列，<code>pos</code> 过大</td><td>不抛，返回 <code>npos</code></td></tr><tr><td>长度超过 <code>max_size()</code></td><td><code>std::length_error</code></td></tr><tr><td>内存分配失败</td><td><code>std::bad_alloc</code></td></tr></tbody></table><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;new&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;stdexcept&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>template</span><span> &lt;</span><span>typename</span><span> </span><span>F</span><span>&gt;</span></div></div><div><div><div>7</div></div><div><span>void</span><span> </span><span>Try</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>what</span><span>, </span></span><span>F</span><span><span> </span><span>f</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>f</span><span>();</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>what</span><span> </span><span>&lt;&lt;</span><span> </span><span>": no exception</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>out_of_range</span><span>&amp;</span><span> </span><span>e</span><span>) {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>what</span><span> </span><span>&lt;&lt;</span><span> </span><span>": out_of_range: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>e</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>length_error</span><span>&amp;</span><span> </span><span>e</span><span>) {</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>what</span><span> </span><span>&lt;&lt;</span><span> </span><span>": length_error: "</span><span> </span><span>&lt;&lt;</span><span> </span><span>e</span><span>.</span><span>what</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (</span><span>const</span><span> std::</span><span>bad_alloc</span><span>&amp;</span><span> </span><span>e</span><span>) {</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>what</span><span> </span><span>&lt;&lt;</span><span> </span><span>": bad_alloc</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"hello"</span><span>;</span></div></div><div><div><div>22</div></div><div><span>  </span><span>Try</span><span>(</span><span>"at(5)            "</span><span>, [</span><span>&amp;</span><span>] { [[maybe_unused]] </span><span>char</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>at</span><span>(</span><span>5</span><span>); });</span></div></div><div><div><div>23</div></div><div><span>  </span><span>Try</span><span>(</span><span>"s[5]             "</span><span>, [</span><span>&amp;</span><span>] { [[maybe_unused]] </span><span>char</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>s</span><span>[</span><span>5</span><span>]; });</span></div></div><div><div><div>24</div></div><div><span>  </span><span>Try</span><span>(</span><span>"substr(5)        "</span><span>, [</span><span>&amp;</span><span>] { [[maybe_unused]] </span><span>auto</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>5</span><span>); });</span></div></div><div><div><div>25</div></div><div><span>  </span><span>Try</span><span>(</span><span>"substr(6)        "</span><span>, [</span><span>&amp;</span><span>] { [[maybe_unused]] </span><span>auto</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>6</span><span>); });</span></div></div><div><div><div>26</div></div><div><span>  </span><span>Try</span><span>(</span><span>"substr(1, 999)   "</span><span>, [</span><span>&amp;</span><span>] { [[maybe_unused]] </span><span>auto</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>1</span><span>, </span><span>999</span><span>); });</span></div></div><div><div><div>27</div></div><div><span>  </span><span>Try</span><span>(</span><span>"insert(9, </span><span>\"</span><span>x</span><span>\"</span><span>)   "</span><span>, [</span><span>&amp;</span><span>] {</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>29</div></div><div><span>    </span><span>t</span><span>.</span><span>insert</span><span>(</span><span>9</span><span>, </span><span>"x"</span><span>);</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>31</div></div><div><span>  </span><span>Try</span><span>(</span><span>"erase(9)         "</span><span>, [</span><span>&amp;</span><span>] {</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>33</div></div><div><span>    </span><span>t</span><span>.</span><span>erase</span><span>(</span><span>9</span><span>);</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>35</div></div><div><span>  </span><span>Try</span><span>(</span><span>"find(</span><span>\"</span><span>l</span><span>\"</span><span>, 99)    "</span><span>,</span></div></div><div><div><div>36</div></div><div><span><span>      </span></span><span>[</span><span>&amp;</span><span>] { [[maybe_unused]] </span><span>auto</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>find</span><span>(</span><span>"l"</span><span>, </span><span>99</span><span>); });</span></div></div><div><div><div>37</div></div><div><span>  </span><span>Try</span><span>(</span><span>"copy(buf, 3, 9)  "</span><span>, [</span><span>&amp;</span><span>] {</span></div></div><div><div><div>38</div></div><div><span>    </span><span>char</span><span> </span><span>buf</span><span>[</span><span>8</span><span>];</span></div></div><div><div><div>39</div></div><div><span>    </span><span>s</span><span>.</span><span>copy</span><span>(</span><span>buf</span><span>, </span><span>3</span><span>, </span><span>9</span><span>);</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>41</div></div><div><span>  </span><span>Try</span><span>(</span><span>"reserve(max + 1) "</span><span>, [</span><span>&amp;</span><span>] {</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>t</span><span>;</span></div></div><div><div><div>43</div></div><div><span>    </span><span>t</span><span>.</span><span>reserve</span><span>(</span><span>t</span><span>.</span><span>max_size</span><span>() </span><span>+</span><span> </span><span>1</span><span>);</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>at(5)            : out_of_range: basic_string::at: __n (which is 5) &gt;= this-&gt;size() (which is 5)</span></div></div><div><div><div>2</div></div><div><span>s[5]             : no exception</span></div></div><div><div><div>3</div></div><div><span>substr(5)        : no exception</span></div></div><div><div><div>4</div></div><div><span>substr(6)        : out_of_range: basic_string::substr: __pos (which is 6) &gt; this-&gt;size() (which is 5)</span></div></div><div><div><div>5</div></div><div><span>substr(1, 999)   : no exception</span></div></div><div><div><div>6</div></div><div><span>insert(9, "x")   : out_of_range: basic_string::replace: __pos (which is 9) &gt; this-&gt;size() (which is 5)</span></div></div><div><div><div>7</div></div><div><span>erase(9)         : out_of_range: basic_string::erase: __pos (which is 9) &gt; this-&gt;size() (which is 5)</span></div></div><div><div><div>8</div></div><div><span>find("l", 99)    : no exception</span></div></div><div><div><div>9</div></div><div><span>copy(buf, 3, 9)  : out_of_range: basic_string::copy: __pos (which is 9) &gt; this-&gt;size() (which is 5)</span></div></div><div><div><div>10</div></div><div><span>reserve(max + 1) : length_error: basic_string::_M_create</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意 <code>substr(5)</code> 在长度为 5 的串上不抛异常（返回空串），<code>substr(6)</code> 才抛。<code>what()</code> 的文本在 libc++ 上只有 <code>basic_string</code>，再次说明不要依赖它。移动构造、移动赋值和 <code>swap</code> 不会抛异常，见第 12 节。</p></section>
<section><h2>15. 编码：size() 数的是字节<a href="#15-编码size-数的是字节"><span>#</span></a></h2><p><code>std::string</code> 只管 <code>char</code>，也就是字节。源文件是 UTF-8 的话，<code>"你好"</code> 就是 6 个字节，<code>size()</code> 返回 6。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;algorithm&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;cctype&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;cstdio&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>std::</span><span>size_t</span><span> </span><span>CountCodePoints</span><span>(std::</span><span>string_view</span><span><span> </span><span>utf8</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>n</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>10</div></div><div><span>  </span><span>for</span><span> (</span><span>char</span><span> </span><span>c</span><span> : </span><span>utf8</span><span>) {</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> ((</span><span><span>static_cast</span><span>&lt;unsigned</span></span><span> </span><span>char&gt;</span><span>(</span><span>c</span><span>) </span><span>&amp;</span><span> </span><span><span>0x</span><span>C0</span></span><span>) </span><span>!=</span><span> </span><span><span>0x</span><span>80</span></span><span>) </span><span>++</span><span>n</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>  </span><span>return</span><span> </span><span>n</span><span>;</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>void</span><span> </span><span>Hex</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>label</span><span>, std::</span></span><span>string_view</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>label</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>for</span><span> (</span><span>char</span><span> </span><span>c</span><span> : </span><span>s</span><span>) std::</span><span>printf</span><span>(</span><span>" </span><span>%02x</span><span>"</span><span>, </span><span><span>static_cast</span><span>&lt;unsigned</span></span><span> </span><span>char&gt;</span><span>(</span><span>c</span><span>));</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span> </span><span>=</span><span> </span><span>"你好"</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"size() = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>25</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", code points = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>CountCodePoints</span><span>(</span><span>s</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>26</div></div><div><span>  </span><span>Hex</span><span>(</span><span>"bytes:   "</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>std::</span><span>u8string</span><span> </span><span>u8</span><span> </span><span>=</span><span> </span><span>u8"你好"</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>std::</span><span>u32string</span><span> </span><span>u32</span><span> </span><span>=</span><span> </span><span>U"你好"</span><span>;</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"u8string size  = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>u8</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"u32string size = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>u32</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>32</div></div><div>
</div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>mixed</span><span> </span><span>=</span><span> </span><span>"a你b"</span><span>;</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"mixed size = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>mixed</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>35</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>", code points = "</span><span> </span><span>&lt;&lt;</span><span> </span><span>CountCodePoints</span><span>(</span><span>mixed</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span><span>  </span></span><span>std::</span><span>reverse</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>(), </span><span>s</span><span>.</span><span>end</span><span>());</span></div></div><div><div><div>38</div></div><div><span>  </span><span>Hex</span><span>(</span><span>"reversed:"</span><span>, </span><span>s</span><span>);</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>e</span><span> </span><span>=</span><span> </span><span>"</span><span>\xc3\xa9</span><span>"</span><span>;</span><span>  // U+00E9, e with acute accent</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>std::</span><span>transform</span><span>(</span><span>e</span><span>.</span><span>begin</span><span>(), </span><span>e</span><span>.</span><span>end</span><span>(), </span><span>e</span><span>.</span><span>begin</span><span>(), [](</span><span>unsigned</span><span> </span><span>char</span><span><span> </span><span>c</span><span>) {</span></span></div></div><div><div><div>42</div></div><div><span>    </span><span>return</span><span> </span><span><span>static_cast</span><span>&lt;char&gt;</span></span><span><span>(</span><span>std</span><span>::</span></span><span>toupper</span><span>(</span><span>c</span><span>));</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>44</div></div><div><span>  </span><span>Hex</span><span>(</span><span>"toupper on UTF-8 e-acute:"</span><span>, </span><span>e</span><span>);</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size() = 6, code points = 2</span></div></div><div><div><div>2</div></div><div><span>bytes:    e4 bd a0 e5 a5 bd</span></div></div><div><div><div>3</div></div><div><span>u8string size  = 6</span></div></div><div><div><div>4</div></div><div><span>u32string size = 2</span></div></div><div><div><div>5</div></div><div><span>mixed size = 5, code points = 3</span></div></div><div><div><div>6</div></div><div><span>reversed: bd a5 e5 a0 bd e4</span></div></div><div><div><div>7</div></div><div><span>toupper on UTF-8 e-acute: c3 a9</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>std::u8string</code> 的 size 也是 6，它的单位是 UTF-8 的 code unit（字节），不是字符。<code>std::u32string</code> 的 size 是 2，因为 UTF-32 里一个 code unit 就是一个码点。</li>
<li>数码点个数：UTF-8 的后续字节形如 <code>10xxxxxx</code>，所以数“不是后续字节”的字节即可，程序里的 <code>CountCodePoints</code> 就是这么做的。显示宽度又是另一回事（中文通常占两列），标准库不管。</li>
<li>用 <code>std::reverse</code> 反转 <code>"你好"</code>，得到的字节是 <code>bd a5 e5 a0 bd e4</code>，已经不是合法的 UTF-8 了。下标、<code>substr</code>、排序、<code>erase</code> 都是按字节操作的，对多字节字符的串要当心切在字符中间。</li>
<li><code>toupper</code>、<code>tolower</code> 只能处理 ASCII。输入 <code>é</code> 的 UTF-8 字节 <code>c3 a9</code>，输出不变。</li>
<li><code>u8"..."</code> 字面量在 C++20 里是 <code>char8_t</code> 数组，不能直接初始化 <code>std::string</code>：<code>std::string s = u8"hello";</code> 会报 <code>conversion from ‘const char8_t [6]’ to non-scalar type ‘std::string’ requested</code>。</li>
<li>需要真正的 Unicode 处理（规范化、大小写折叠、按字素切分），应该用 ICU 之类的专门的库，<code>std::string</code> 本身不提供。</li>
</ul></section>
<section><h2>16. 标准库没有的：trim、split、join、replace_all<a href="#16-标准库没有的trimsplitjoinreplace_all"><span>#</span></a></h2><p><code>std::string</code> 没有这几个常用函数。自己写时有两个原则：<strong>参数和返回值尽量用 <code>string_view</code></strong>，避免不必要的拷贝；字符串操作前 <code>unsigned char</code> 转换不能忘。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;algorithm&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;cctype&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>std::</span><span>string_view</span><span> </span><span>Trim</span><span>(std::</span><span>string_view</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>spaces</span><span> </span><span>=</span><span> </span><span>" </span><span>\t\r\n\f\v</span><span>"</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>first</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>find_first_not_of</span><span>(</span><span>spaces</span><span>);</span></div></div><div><div><div>11</div></div><div><span>  </span><span>if</span><span> (</span><span>first</span><span> </span><span>==</span><span> std::string_view::</span><span>npos</span><span>) </span><span>return</span><span> {};</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>last</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>find_last_not_of</span><span>(</span><span>spaces</span><span>);</span></div></div><div><div><div>13</div></div><div><span>  </span><span>return</span><span> </span><span>s</span><span>.</span><span>substr</span><span>(</span><span>first</span><span>, </span><span>last</span><span> </span><span>-</span><span> </span><span>first</span><span> </span><span>+</span><span> </span><span>1</span><span>);</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>std::</span><span>vector</span><span>&lt;std::</span><span>string_view</span><span>&gt; </span><span>Split</span><span>(std::</span><span>string_view</span><span><span> </span><span>s</span><span>, </span></span><span>char</span><span><span> </span><span>delim</span><span>) {</span></span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>string_view</span><span>&gt; </span><span>parts</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>start</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>19</div></div><div><span>  </span><span>while</span><span> (</span><span>true</span><span>) {</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>std::</span><span>size_t</span><span> </span><span>end</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>find</span><span>(</span><span>delim</span><span>, </span><span>start</span><span>);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>if</span><span> (</span><span>end</span><span> </span><span>==</span><span> std::string_view::</span><span>npos</span><span>) {</span></div></div><div><div><div>22</div></div><div><span>      </span><span>parts</span><span>.</span><span>push_back</span><span>(</span><span>s</span><span>.</span><span>substr</span><span>(</span><span>start</span><span>));</span></div></div><div><div><div>23</div></div><div><span>      </span><span>return</span><span> </span><span>parts</span><span>;</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>    </span><span>parts</span><span>.</span><span>push_back</span><span>(</span><span>s</span><span>.</span><span>substr</span><span>(</span><span>start</span><span>, </span><span>end</span><span> </span><span>-</span><span> </span><span>start</span><span>));</span></div></div><div><div><div>26</div></div><div><span>    </span><span>start</span><span> </span><span>=</span><span> </span><span>end</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>27</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>std::</span><span>string</span><span> </span><span>Join</span><span>(</span><span>const</span><span> std::</span><span>vector</span><span>&lt;std::</span><span>string_view</span><span>&gt;</span><span>&amp;</span><span><span> </span><span>parts</span><span>,</span></span></div></div><div><div><div>31</div></div><div><span><span>                 </span></span><span>std::</span><span>string_view</span><span><span> </span><span>sep</span><span>) {</span></span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>out</span><span>;</span></div></div><div><div><div>33</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>parts</span><span>.</span><span>size</span><span>(); </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>34</div></div><div><span>    </span><span>if</span><span> (</span><span>i</span><span> </span><span>&gt;</span><span> </span><span>0</span><span>) </span><span>out</span><span> </span><span>+=</span><span> </span><span>sep</span><span>;</span></div></div><div><div><div>35</div></div><div><span>    </span><span>out</span><span> </span><span>+=</span><span> </span><span>parts</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span>  </span><span>return</span><span> </span><span>out</span><span>;</span></div></div><div><div><div>38</div></div><div><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>std::</span><span>string</span><span> </span><span>ReplaceAll</span><span>(std::</span><span>string</span><span><span> </span><span>s</span><span>, std::</span></span><span>string_view</span><span><span> </span><span>from</span><span>,</span></span></div></div><div><div><div>41</div></div><div><span><span>                       </span></span><span>std::</span><span>string_view</span><span><span> </span><span>to</span><span>) {</span></span></div></div><div><div><div>42</div></div><div><span>  </span><span>if</span><span> (</span><span>from</span><span>.</span><span>empty</span><span>()) </span><span>return</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>pos</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>44</div></div><div><span>  </span><span>while</span><span> ((</span><span>pos</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>find</span><span>(</span><span>from</span><span>, </span><span>pos</span><span>)) </span><span>!=</span><span> std::string::</span><span>npos</span><span>) {</span></div></div><div><div><div>45</div></div><div><span>    </span><span>s</span><span>.</span><span>replace</span><span>(</span><span>pos</span><span>, </span><span>from</span><span>.</span><span>size</span><span>(), </span><span>to</span><span>);</span></div></div><div><div><div>46</div></div><div><span>    </span><span>pos</span><span> </span><span>+=</span><span> </span><span>to</span><span>.</span><span>size</span><span>();</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>48</div></div><div><span>  </span><span>return</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>49</div></div><div><span>}</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>std::</span><span>string</span><span> </span><span>ToLowerAscii</span><span>(std::</span><span>string</span><span><span> </span><span>s</span><span>) {</span></span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>std::</span><span>transform</span><span>(</span><span>s</span><span>.</span><span>begin</span><span>(), </span><span>s</span><span>.</span><span>end</span><span>(), </span><span>s</span><span>.</span><span>begin</span><span>(), [](</span><span>unsigned</span><span> </span><span>char</span><span><span> </span><span>c</span><span>) {</span></span></div></div><div><div><div>53</div></div><div><span>    </span><span>return</span><span> </span><span><span>static_cast</span><span>&lt;char&gt;</span></span><span><span>(</span><span>std</span><span>::</span></span><span>tolower</span><span>(</span><span>c</span><span>));</span></div></div><div><div><div>54</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>55</div></div><div><span>  </span><span>return</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>56</div></div><div><span>}</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>59</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"["</span><span> </span><span>&lt;&lt;</span><span> </span><span>Trim</span><span>(</span><span>"  </span><span>\t</span><span> hello world </span><span>\n</span><span>"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>60</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"["</span><span> </span><span>&lt;&lt;</span><span> </span><span>Trim</span><span>(</span><span>"   "</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>61</div></div><div>
</div></div><div><div><div>62</div></div><div><span>  </span><span>auto</span><span> </span><span>parts</span><span> </span><span>=</span><span> </span><span>Split</span><span>(</span><span>"a,b,,c"</span><span>, </span><span>','</span><span>);</span></div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"split into "</span><span> </span><span>&lt;&lt;</span><span> </span><span>parts</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>":"</span><span>;</span></div></div><div><div><div>64</div></div><div><span>  </span><span>for</span><span> (</span><span>auto</span><span> </span><span>p</span><span> : </span><span>parts</span><span>) std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>" ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>p</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]"</span><span>;</span></div></div><div><div><div>65</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>66</div></div><div>
</div></div><div><div><div>67</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>Join</span><span>(</span><span>parts</span><span>, </span><span>"|"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>ReplaceAll</span><span>(</span><span>"a.b.c"</span><span>, </span><span>"."</span><span>, </span><span>"::"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>ReplaceAll</span><span>(</span><span>"aaaa"</span><span>, </span><span>"aa"</span><span>, </span><span>"a"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>ToLowerAscii</span><span>(</span><span>"Hello, WORLD"</span><span>) </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>71</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[hello world]</span></div></div><div><div><div>2</div></div><div><span>[]</span></div></div><div><div><div>3</div></div><div><span>split into 4: [a] [b] [] [c]</span></div></div><div><div><div>4</div></div><div><span>a|b||c</span></div></div><div><div><div>5</div></div><div><span>a::b::c</span></div></div><div><div><div>6</div></div><div><span>aa</span></div></div><div><div><div>7</div></div><div><span>hello, world</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>Trim</code> 和 <code>Split</code> 返回的是原字符串的视图，所以结果的生命周期不能超过原字符串。</li>
<li><code>Split</code> 保留了空字段（<code>a,b,,c</code> 得到 4 段，第 3 段是空的），这是对的，不要在这里悄悄丢数据。</li>
<li><code>ReplaceAll</code> 里每次替换后 <code>pos += to.size()</code>，否则当 <code>to</code> 包含 <code>from</code> 时会死循环。<code>"aaaa"</code> 替换 <code>"aa"</code> 为 <code>"a"</code> 得到 <code>"aa"</code>，而不是 <code>"a"</code>，因为替换结果不会被再次扫描。</li>
</ul></section>
<section><h2>17. 性能与传参<a href="#17-性能与传参"><span>#</span></a></h2><p>先看几个数字。这个程序重载了 <code>operator new</code> 来数分配次数：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;chrono&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;cstdio&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;cstdlib&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;new&gt;</span></div></div><div><div><div>5</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>6</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>namespace</span><span> {</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>std::</span><span>size_t</span><span> </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>template</span><span> &lt;</span><span>typename</span><span> </span><span>F</span><span>&gt;</span></div></div><div><div><div>13</div></div><div><span>double</span><span> </span><span>Ms</span><span>(</span><span>F</span><span><span> </span><span>f</span><span>) {</span></span></div></div><div><div><div>14</div></div><div><span>  </span><span>auto</span><span> </span><span>t0</span><span> </span><span>=</span><span><span> std::</span><span>chrono</span><span>::</span><span>steady_clock</span><span>::</span></span><span>now</span><span>();</span></div></div><div><div><div>15</div></div><div><span>  </span><span>f</span><span>();</span></div></div><div><div><div>16</div></div><div><span><span>  </span></span><span>std::chrono::</span><span>duration</span><span>&lt;</span><span>double</span><span>, std::</span><span>milli</span><span>&gt; </span><span>dt</span><span> </span><span>=</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>std::</span><span>chrono</span><span>::</span><span>steady_clock</span><span>::</span><span>now</span><span>() </span><span>-</span><span> </span><span>t0</span><span>;</span></div></div><div><div><div>18</div></div><div><span>  </span><span>return</span><span> </span><span>dt</span><span>.</span><span>count</span><span>();</span></div></div><div><div><div>19</div></div><div><span>}</span></div></div><div><div><div>20</div></div><div>
</div></div><div><div><div>21</div></div><div><span>void</span><span> </span><span>Sink</span><span>(</span><span>const</span><span> </span><span>char*</span><span><span> </span><span>p</span><span>) { </span></span><span>asm</span><span> </span><span>volatile</span><span>(</span><span>""</span><span> : : </span><span>"</span><span>r</span><span>"</span><span>(</span><span>p</span><span>) : </span><span>"</span><span>memory</span><span>"</span><span>); }</span></div></div><div><div><div>22</div></div><div>
</div></div><div><div><div>23</div></div><div><span><span>[[</span><span>gnu</span><span>::noipa]] std::</span></span><span>size_t</span><span> </span><span>LenRef</span><span>(</span><span>const</span><span> std::</span><span>string</span><span>&amp;</span><span><span> </span><span>s</span><span>) { </span></span><span>return</span><span> </span><span>s</span><span>.</span><span>size</span><span>(); }</span></div></div><div><div><div>24</div></div><div><span><span>[[</span><span>gnu</span><span>::noipa]] std::</span></span><span>size_t</span><span> </span><span>LenView</span><span>(std::</span><span>string_view</span><span><span> </span><span>s</span><span>) { </span></span><span>return</span><span> </span><span>s</span><span>.</span><span>size</span><span>(); }</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span>}</span><span>  // namespace</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>void*</span><span> </span><span>operator</span><span> new(std::</span><span>size_t</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>29</div></div><div><span>  </span><span>++</span><span>g_allocs</span><span>;</span></div></div><div><div><div>30</div></div><div><span>  </span><span>if</span><span> (</span><span>void*</span><span> </span><span>p</span><span> </span><span>=</span><span> std::</span><span>malloc</span><span>(</span><span>n</span><span>)) </span><span>return</span><span> </span><span>p</span><span>;</span></div></div><div><div><div>31</div></div><div><span>  </span><span>throw</span><span> std::</span><span>bad_alloc</span><span>();</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>void</span><span> </span><span>operator</span><span> delete(</span><span>void*</span><span><span> </span><span>p</span><span>) </span></span><span>noexcept</span><span> { std::</span><span>free</span><span>(</span><span>p</span><span>); }</span></div></div><div><div><div>35</div></div><div><span>void</span><span> </span><span>operator</span><span> delete(</span><span>void*</span><span><span> </span><span>p</span><span>, std::</span></span><span>size_t</span><span>) </span><span>noexcept</span><span> { std::</span><span>free</span><span>(</span><span>p</span><span>); }</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>38</div></div><div><span>  </span><span>constexpr</span><span> </span><span>int</span><span> </span><span>kN</span><span> </span><span>=</span><span> </span><span>100000</span><span>;</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>a</span><span>;</span></div></div><div><div><div>41</div></div><div><span>  </span><span>double</span><span> </span><span>t_plus</span><span> </span><span>=</span><span> </span><span>Ms</span><span>([</span><span>&amp;</span><span>] {</span></div></div><div><div><div>42</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kN</span><span>; </span><span>++</span><span>i</span><span>) </span><span>a</span><span> </span><span>=</span><span> </span><span>a</span><span> </span><span>+</span><span> </span><span>"x"</span><span>;</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>b</span><span>;</span></div></div><div><div><div>45</div></div><div><span>  </span><span>double</span><span> </span><span>t_append</span><span> </span><span>=</span><span> </span><span>Ms</span><span>([</span><span>&amp;</span><span>] {</span></div></div><div><div><div>46</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kN</span><span>; </span><span>++</span><span>i</span><span>) </span><span>b</span><span> </span><span>+=</span><span> </span><span>"x"</span><span>;</span></div></div><div><div><div>47</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>48</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span><span>"</span><span>%d</span><span> times, s = s + </span><span>\"</span><span>x</span><span>\"</span><span>: </span><span>%8.2f</span><span> ms</span><span>\n</span><span>"</span><span>, </span><span>kN</span><span>, </span><span>t_plus</span><span>);</span></div></div><div><div><div>49</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span><span>"</span><span>%d</span><span> times, s += </span><span>\"</span><span>x</span><span>\"</span><span>:    </span><span>%8.2f</span><span> ms (</span><span>%.0f</span><span>x faster)</span><span>\n</span><span>"</span><span>, </span><span>kN</span><span>,</span></div></div><div><div><div>50</div></div><div><span>              </span><span>t_append</span><span>, </span><span>t_plus</span><span> </span><span>/</span><span> </span><span>t_append</span><span>);</span></div></div><div><div><div>51</div></div><div>
</div></div><div><div><div>52</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>x</span><span>(</span><span>30</span><span>, </span><span>'x'</span><span>);</span></div></div><div><div><div>53</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>y</span><span>(</span><span>30</span><span>, </span><span>'y'</span><span>);</span></div></div><div><div><div>54</div></div><div><span>  </span><span>const</span><span> std::</span><span>string</span><span> </span><span>z</span><span>(</span><span>30</span><span>, </span><span>'z'</span><span>);</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>r1</span><span> </span><span>=</span><span> </span><span>x</span><span> </span><span>+</span><span> </span><span>","</span><span> </span><span>+</span><span> </span><span>y</span><span> </span><span>+</span><span> </span><span>","</span><span> </span><span>+</span><span> </span><span>z</span><span>;</span></div></div><div><div><div>58</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>chain</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>r2</span><span>;</span></div></div><div><div><div>62</div></div><div><span>  </span><span>r2</span><span>.</span><span>reserve</span><span>(</span><span>x</span><span>.</span><span>size</span><span>() </span><span>+</span><span> </span><span>y</span><span>.</span><span>size</span><span>() </span><span>+</span><span> </span><span>z</span><span>.</span><span>size</span><span>() </span><span>+</span><span> </span><span>2</span><span>);</span></div></div><div><div><div>63</div></div><div><span>  </span><span>r2</span><span> </span><span>+=</span><span> </span><span>x</span><span>;</span></div></div><div><div><div>64</div></div><div><span>  </span><span>r2</span><span> </span><span>+=</span><span> </span><span>','</span><span>;</span></div></div><div><div><div>65</div></div><div><span>  </span><span>r2</span><span> </span><span>+=</span><span> </span><span>y</span><span>;</span></div></div><div><div><div>66</div></div><div><span>  </span><span>r2</span><span> </span><span>+=</span><span> </span><span>','</span><span>;</span></div></div><div><div><div>67</div></div><div><span>  </span><span>r2</span><span> </span><span>+=</span><span> </span><span>z</span><span>;</span></div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>reserved</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span><span>"x + </span><span>\"</span><span>,</span><span>\"</span><span> + y + </span><span>\"</span><span>,</span><span>\"</span><span> + z: </span><span>%zu</span><span> allocs; reserve + append: </span><span>%zu</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>70</div></div><div><span>              </span><span>chain</span><span>, </span><span>reserved</span><span>);</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>  </span><span>constexpr</span><span> </span><span>int</span><span> </span><span>kPushes</span><span> </span><span>=</span><span> </span><span>10'000'000</span><span>;</span></div></div><div><div><div>73</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>74</div></div><div><span>  </span><span>double</span><span> </span><span>t_plain</span><span> </span><span>=</span><span> </span><span>Ms</span><span>([] {</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>76</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kPushes</span><span>; </span><span>++</span><span>i</span><span>) </span><span>s</span><span>.</span><span>push_back</span><span>(</span><span>'x'</span><span>);</span></div></div><div><div><div>77</div></div><div><span>    </span><span>Sink</span><span>(</span><span>s</span><span>.</span><span>data</span><span>());</span></div></div><div><div><div>78</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>79</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>plain_allocs</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>80</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>81</div></div><div><span>  </span><span>double</span><span> </span><span>t_reserve</span><span> </span><span>=</span><span> </span><span>Ms</span><span>([] {</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>83</div></div><div><span>    </span><span>s</span><span>.</span><span>reserve</span><span>(</span><span>kPushes</span><span>);</span></div></div><div><div><div>84</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>kPushes</span><span>; </span><span>++</span><span>i</span><span>) </span><span>s</span><span>.</span><span>push_back</span><span>(</span><span>'x'</span><span>);</span></div></div><div><div><div>85</div></div><div><span>    </span><span>Sink</span><span>(</span><span>s</span><span>.</span><span>data</span><span>());</span></div></div><div><div><div>86</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>87</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>reserve_allocs</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>88</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span></div></div><div><div><div>89</div></div><div><span>      </span><span>"</span><span>%d</span><span> push_back: </span><span>%.2f</span><span> ms / </span><span>%zu</span><span> allocs; with reserve: </span><span>%.2f</span><span> ms / "</span></div></div><div><div><div>90</div></div><div><span>      </span><span>"</span><span>%zu</span><span> allocs</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>91</div></div><div><span>      </span><span>kPushes</span><span>, </span><span>t_plain</span><span>, </span><span>plain_allocs</span><span>, </span><span>t_reserve</span><span>, </span><span>reserve_allocs</span><span>);</span></div></div><div><div><div>92</div></div><div>
</div></div><div><div><div>93</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>lit</span><span> </span><span>=</span><span> </span><span>"this literal is definitely longer than fifteen bytes"</span><span>;</span></div></div><div><div><div>94</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>sum</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>95</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>96</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000</span><span>; </span><span>++</span><span>i</span><span>) </span><span>sum</span><span> </span><span>+=</span><span> </span><span>LenRef</span><span>(</span><span>lit</span><span>);</span></div></div><div><div><div>97</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>ref_allocs</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>98</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>99</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000</span><span>; </span><span>++</span><span>i</span><span>) </span><span>sum</span><span> </span><span>+=</span><span> </span><span>LenView</span><span>(</span><span>lit</span><span>);</span></div></div><div><div><div>100</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>view_allocs</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>101</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span></div></div><div><div><div>102</div></div><div><span>      </span><span>"1000 calls with a 52-byte literal: const std::string&amp; -&gt; </span><span>%zu</span><span> "</span></div></div><div><div><div>103</div></div><div><span>      </span><span>"allocs, std::string_view -&gt; </span><span>%zu</span><span> allocs</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>104</div></div><div><span>      </span><span>ref_allocs</span><span>, </span><span>view_allocs</span><span>);</span></div></div><div><div><div>105</div></div><div>
</div></div><div><div><div>106</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>big</span><span>(</span><span>1000</span><span>, </span><span>'a'</span><span>);</span></div></div><div><div><div>107</div></div><div><span><span>  </span></span><span>std::</span><span>string_view</span><span> </span><span>big_view</span><span> </span><span>=</span><span> </span><span>big</span><span>;</span></div></div><div><div><div>108</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>109</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000</span><span>; </span><span>++</span><span>i</span><span>) </span><span>Sink</span><span>(</span><span>big</span><span>.</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>500</span><span>).</span><span>data</span><span>());</span></div></div><div><div><div>110</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>substr_allocs</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>111</div></div><div><span>  </span><span>g_allocs</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>112</div></div><div><span>  </span><span>for</span><span> (</span><span>int</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>1000</span><span>; </span><span>++</span><span>i</span><span>) </span><span>Sink</span><span>(</span><span>big_view</span><span>.</span><span>substr</span><span>(</span><span>0</span><span>, </span><span>500</span><span>).</span><span>data</span><span>());</span></div></div><div><div><div>113</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>view_substr_allocs</span><span> </span><span>=</span><span> </span><span>g_allocs</span><span>;</span></div></div><div><div><div>114</div></div><div><span><span>  </span></span><span>std::</span><span>printf</span><span>(</span></div></div><div><div><div>115</div></div><div><span>      </span><span>"1000 substr(0, 500): string -&gt; </span><span>%zu</span><span> allocs, string_view -&gt; </span><span>%zu</span><span> "</span></div></div><div><div><div>116</div></div><div><span>      </span><span>"allocs</span><span>\n</span><span>"</span><span>,</span></div></div><div><div><div>117</div></div><div><span>      </span><span>substr_allocs</span><span>, </span><span>view_substr_allocs</span><span>);</span></div></div><div><div><div>118</div></div><div><span>  </span><span>return</span><span> (</span><span>r1</span><span> </span><span>!=</span><span> </span><span>r2</span><span>) </span><span>||</span><span> </span><span>sum</span><span> </span><span>==</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>119</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>100000 times, s = s + "x":   115.03 ms</span></div></div><div><div><div>2</div></div><div><span>100000 times, s += "x":        0.19 ms (620x faster)</span></div></div><div><div><div>3</div></div><div><span>x + "," + y + "," + z: 3 allocs; reserve + append: 1</span></div></div><div><div><div>4</div></div><div><span>10000000 push_back: 27.97 ms / 20 allocs; with reserve: 17.64 ms / 1 allocs</span></div></div><div><div><div>5</div></div><div><span>1000 calls with a 52-byte literal: const std::string&amp; -&gt; 1000 allocs, std::string_view -&gt; 0 allocs</span></div></div><div><div><div>6</div></div><div><span>1000 substr(0, 500): string -&gt; 1000 allocs, string_view -&gt; 0 allocs</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>（计时每次略有波动，分配次数是确定的。同一程序在 libc++ 上：<code>+=</code> 比 <code>s = s + "x"</code> 快 142 倍，<code>push_back</code> 的分配次数是 19 而不是 20。）</p><ul>
<li><strong><code>s = s + "x"</code> 每次都构造一个新 string</strong>，把旧内容整个拷一遍，10 万次循环是 <span><span>O(n2)O(n^2)</span><span><span><span></span><span>O</span><span>(</span><span><span>n</span><span><span><span><span><span><span></span><span><span>2</span></span></span></span></span></span></span></span><span>)</span></span></span></span>。<code>s += "x"</code> 只在尾部追加。这里差了 601 倍。</li>
<li><strong><code>reserve</code></strong> 把 1000 万次 <code>push_back</code> 的分配从 20 次降到 1 次（从 15 开始翻倍，<span><span>15×220≈1.6×10715 \times 2^{20} \approx 1.6 \times 10^7</span><span><span><span></span><span>15</span><span></span><span>×</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span><span></span><span>≈</span><span></span></span><span><span></span><span>1.6</span><span></span><span>×</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span></span></span></span> 才够），耗时从 26 ms 降到 17 ms。这是单次测量，波动不小，我猜省下的时间主要是每次扩容时的拷贝，没有单独测。</li>
<li><strong><code>x + "," + y + "," + z</code></strong> 要分配 3 次，先 <code>reserve</code> 再 <code>+=</code> 只要 1 次。单次拼接无所谓，循环里值得改。</li>
<li><strong><code>const std::string&amp;</code> 接收长字面量</strong>，每次调用都要先构造一个临时 string：1000 次调用 1000 次分配。<code>string_view</code> 是 0。<code>substr</code> 同理，string 版拷贝，view 版只改指针和长度。</li>
</ul><p>这个实验我第一次跑时，<code>const std::string&amp;</code> 那一项也是 0 次分配，我差点就信了。原因是 <code>-O2</code> 把整个调用内联并常量传播掉了，string 压根没被构造，所以程序里给两个函数加了 <code>[[gnu::noipa]]</code>（GCC 专有，Clang 会警告并忽略它，但 Clang 在这里本来就没有优化掉）。0 看着很美，但那不是 string 的功劳。</p><p>函数参数的选择，可以按这张表判断：</p>

<table><thead><tr><th>函数要对字符串做什么</th><th>参数类型</th></tr></thead><tbody><tr><td>只读，不保存</td><td><code>std::string_view</code></td></tr><tr><td>只读，且要传给需要 <code>'\0'</code> 结尾的 C 接口</td><td><code>const std::string&amp;</code>（或 <code>const char*</code>）</td></tr><tr><td>修改调用者的字符串</td><td><code>std::string&amp;</code></td></tr><tr><td>要保存下来（存进成员、容器）</td><td>按值传 <code>std::string</code>，内部 <code>std::move</code></td></tr><tr><td>返回新字符串</td><td>按值返回，编译器会优化掉拷贝</td></tr></tbody></table></section>
<section><h2>18. 常见陷阱清单<a href="#18-常见陷阱清单"><span>#</span></a></h2><ul>
<li><code>"hello" + 1</code> 不是拼接，是指针算术。GCC 对它一声不吭，Clang 在 <code>-Wall</code> 下会警告 <code>adding 'int' to a string does not append to the string</code>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>// "hello" + 1 is pointer arithmetic on a const char[6], not concatenation.</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>t</span><span> </span><span>=</span><span> </span><span>"hello"</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\"</span><span>hello</span><span>\"</span><span> + 1 -&gt; ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>t</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>// The argument aliases the object itself; the library handles that.</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>a</span><span> </span><span>=</span><span> </span><span>"abc"</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>a</span><span>.</span><span>append</span><span>(</span><span>a</span><span>);</span></div></div><div><div><div>12</div></div><div><span>  </span><span>a</span><span>.</span><span>insert</span><span>(</span><span>0</span><span>, </span><span>a</span><span>);</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"append and insert of itself -&gt; "</span><span> </span><span>&lt;&lt;</span><span> </span><span>a</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"hello" + 1 -&gt; [ello]</span></div></div><div><div><div>2</div></div><div><span>append and insert of itself -&gt; abcabcabcabc</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>临时 string 的 <code>c_str()</code> 在分号处就悬空了：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>first</span><span> </span><span>=</span><span> </span><span>"a"</span><span>;</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>second</span><span> </span><span>=</span><span> </span><span>"b"</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>const</span><span> </span><span>char*</span><span> </span><span>p</span><span> </span><span>=</span><span> (</span><span>first</span><span> </span><span>+</span><span> </span><span>second</span><span>).</span><span>c_str</span><span>();</span><span>  // BAD: temporary dies at ';'</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>p</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>==719494==ERROR: AddressSanitizer: stack-use-after-scope on address 0x76378bc00130 at pc 0x7a378e28604f bp 0x7fffa44df900 sp 0x7fffa44df0d0</span></div></div><div><div><div>2</div></div><div><span>READ of size 3 at 0x76378bc00130 thread T0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>其余陷阱前面都讲过：
<ul>
<li><code>std::string t{3, 'x'}</code> 是两个字符，不是三个（第 2 节）。</li>
<li><code>"a\0b"</code> 在 <code>const char*</code> 构造下只有 1 个字符（第 2 节）。</li>
<li><code>s.size() - 1</code> 在空串上回绕（第 5 节）。</li>
<li><code>find</code> 的返回值要和 <code>npos</code> 比较，不能存进 <code>int</code>（第 7 节）。</li>
<li><code>compare()</code> 只有符号有意义（第 8 节）。</li>
<li><code>stoi</code> 不会因为尾部的垃圾字符抛异常，<code>stoul("-1")</code> 不会失败（第 10 节）。</li>
<li><code>string_view</code> 不拥有内存，也不保证 <code>'\0'</code> 结尾（第 11 节）。</li>
<li>修改 string 后，旧的指针、引用、迭代器全都不能用（第 13 节）。</li>
<li><code>toupper</code> 要先转 <code>unsigned char</code>；UTF-8 字符串按字节操作会破坏数据（第 4、15 节）。</li>
</ul>
</li>
</ul></section>
<section><h2>19. 速查表<a href="#19-速查表"><span>#</span></a></h2>

<table><thead><tr><th>类别</th><th>成员</th><th>版本</th><th>备注</th></tr></thead><tbody><tr><td>构造/赋值</td><td>构造函数、<code>operator=</code>、<code>assign</code></td><td></td><td><code>assign_range</code> (C++23)</td></tr><tr><td></td><td><code>get_allocator</code></td><td></td><td></td></tr><tr><td>元素访问</td><td><code>at</code>、<code>operator[]</code>、<code>front</code>、<code>back</code></td><td></td><td><code>at</code> 检查越界</td></tr><tr><td></td><td><code>data</code>、<code>c_str</code></td><td></td><td>都以 <code>'\0'</code> 结尾</td></tr><tr><td></td><td><code>operator string_view</code></td><td>C++17</td><td>隐式转换</td></tr><tr><td>迭代器</td><td><code>begin</code> <code>end</code> <code>rbegin</code> <code>rend</code> 及 <code>c</code> 版本</td><td>C++11</td><td>C++20 起为 contiguous</td></tr><tr><td>容量</td><td><code>empty</code>、<code>size</code>、<code>length</code>、<code>max_size</code></td><td></td><td></td></tr><tr><td></td><td><code>capacity</code>、<code>reserve</code>、<code>shrink_to_fit</code></td><td>C++11</td><td><code>reserve</code> C++20 起不缩小</td></tr><tr><td>修改</td><td><code>clear</code>、<code>insert</code>、<code>erase</code></td><td></td><td><code>insert_range</code> (C++23)</td></tr><tr><td></td><td><code>push_back</code>、<code>pop_back</code></td><td></td><td></td></tr><tr><td></td><td><code>append</code>、<code>operator+=</code></td><td></td><td><code>append_range</code> (C++23)</td></tr><tr><td></td><td><code>replace</code>、<code>copy</code>、<code>swap</code></td><td></td><td><code>replace_with_range</code> (C++23)</td></tr><tr><td></td><td><code>resize</code>、<code>resize_and_overwrite</code></td><td>C++23</td><td>后者写缓冲区</td></tr><tr><td>查找</td><td><code>find</code>、<code>rfind</code></td><td></td><td>返回 <code>npos</code></td></tr><tr><td></td><td><code>find_first_of</code>、<code>find_last_of</code></td><td></td><td>参数是字符集合</td></tr><tr><td></td><td><code>find_first_not_of</code>、<code>find_last_not_of</code></td><td></td><td></td></tr><tr><td>操作</td><td><code>compare</code>、<code>substr</code></td><td></td><td><code>substr</code> 返回拷贝</td></tr><tr><td></td><td><code>starts_with</code>、<code>ends_with</code></td><td>C++20</td><td></td></tr><tr><td></td><td><code>contains</code></td><td>C++23</td><td></td></tr><tr><td>非成员</td><td><code>operator+</code>、比较运算符、<code>operator&lt;=&gt;</code></td><td>C++20</td><td><code>&lt;=&gt;</code> 合成比较</td></tr><tr><td></td><td><code>operator&lt;&lt;</code>、<code>operator&gt;&gt;</code>、<code>getline</code></td><td></td><td></td></tr><tr><td></td><td><code>std::erase</code>、<code>std::erase_if</code></td><td>C++20</td><td>返回删除个数</td></tr><tr><td></td><td><code>stoi</code> 系列、<code>to_string</code></td><td>C++11</td><td></td></tr><tr><td></td><td><code>operator""s</code></td><td>C++14</td><td><code>"..."s</code></td></tr><tr><td></td><td><code>std::hash&lt;std::string&gt;</code></td><td>C++11</td><td></td></tr></tbody></table></section>
<section><h2>参考与练习<a href="#参考与练习"><span>#</span></a></h2><ul>
<li><a href="https://cppreference.com/cpp/string/basic_string" target="_blank">cppreference: std::basic_string</a>，成员函数全表和每个函数的重载细节。</li>
<li><a href="https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html" target="_blank">libstdc++ 手册：Dual ABI</a>，第 12 节 COW 的来龙去脉。</li>
</ul><p>练习，答案折叠在每题下面，先自己想一想再点开：</p><ol>
<li>把第 13 节程序里的 <code>first = 'z';</code> 改成 <code>s[0] = 'z';</code>，程序就合法了。为什么？</li>
</ol>
参考答案<p><code>first</code> 是在 <code>append</code> 之前取的引用，指向旧缓冲区。<code>append(1000, 'b')</code> 超出了 capacity，缓冲区被重新分配，旧缓冲区被释放，<code>first</code> 就悬空了。<code>s[0]</code> 每次调用都从<strong>当前</strong>缓冲区里取字符，所以不受影响。规则是：不要跨过修改操作保存引用，每次用的时候重新取，或者保存下标。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span>(</span><span>40</span><span>, </span><span>'a'</span><span>);</span></div></div><div><div><div>6</div></div><div><span>  </span><span>s</span><span>.</span><span>append</span><span>(</span><span>1000</span><span>, </span><span>'b'</span><span>);</span><span>  // may reallocate; any old reference is now invalid</span></div></div><div><div><div>7</div></div><div><span>  </span><span>s</span><span>[</span><span>0</span><span>] </span><span>=</span><span> </span><span>'z'</span><span>;</span><span>           // OK: operator[] looks at the current buffer</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span>[</span><span>0</span><span>] </span><span>&lt;&lt;</span><span> </span><span>s</span><span>.</span><span>size</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>9</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 <code>-g -fsanitize=address</code> 编译运行，ASan 没有报告，输出是首字符 <code>z</code> 加 size 1040：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>z1040</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li><code>Split</code> 现在只能按单个字符切。改成按子串切，并写出会让朴素实现死循环的输入。</li>
</ol>
参考答案<p>按子串切时，每段之后要跳过整个分隔符：<code>start = end + delim.size()</code>。朴素实现会死循环的输入是<strong>空分隔符</strong>：<code>find("", start)</code> 返回 <code>start</code> 本身，<code>delim.size()</code> 是 0，<code>start</code> 永远不前进。所以必须先判断 <code>delim.empty()</code>，这里选择把整个串当作一段返回。</p><p>另外两点：分隔符从左到右<strong>不重叠</strong>地匹配，<code>"aaa"</code> 按 <code>"aa"</code> 切得到 <code>[] [a]</code>；结尾紧跟分隔符时保留末尾的空段，和第 16 节的行为一致。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;string_view&gt;</span></div></div><div><div><div>4</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>std::</span><span>vector</span><span>&lt;std::</span><span>string_view</span><span>&gt; </span><span>SplitBy</span><span>(std::</span><span>string_view</span><span><span> </span><span>s</span><span>,</span></span></div></div><div><div><div>7</div></div><div><span><span>                                      </span></span><span>std::</span><span>string_view</span><span><span> </span><span>delim</span><span>) {</span></span></div></div><div><div><div>8</div></div><div><span>  </span><span>if</span><span> (</span><span>delim</span><span>.</span><span>empty</span><span>()) </span><span>return</span><span> {</span><span>s</span><span>};</span><span>  // nothing to split on</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>string_view</span><span>&gt; </span><span>parts</span><span>;</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>start</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>11</div></div><div><span>  </span><span>while</span><span> (</span><span>true</span><span>) {</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>std::</span><span>size_t</span><span> </span><span>end</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>find</span><span>(</span><span>delim</span><span>, </span><span>start</span><span>);</span></div></div><div><div><div>13</div></div><div><span>    </span><span>if</span><span> (</span><span>end</span><span> </span><span>==</span><span> std::string_view::</span><span>npos</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>      </span><span>parts</span><span>.</span><span>push_back</span><span>(</span><span>s</span><span>.</span><span>substr</span><span>(</span><span>start</span><span>));</span></div></div><div><div><div>15</div></div><div><span>      </span><span>return</span><span> </span><span>parts</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>    </span><span>parts</span><span>.</span><span>push_back</span><span>(</span><span>s</span><span>.</span><span>substr</span><span>(</span><span>start</span><span>, </span><span>end</span><span> </span><span>-</span><span> </span><span>start</span><span>));</span></div></div><div><div><div>18</div></div><div><span>    </span><span>start</span><span> </span><span>=</span><span> </span><span>end</span><span> </span><span>+</span><span> </span><span>delim</span><span>.</span><span>size</span><span>();</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>// BAD: without the empty-delimiter check, find("", start) returns start and</span></div></div><div><div><div>23</div></div><div><span>// start never advances. The cap makes the hang visible instead of real.</span></div></div><div><div><div>24</div></div><div><span>int</span><span> </span><span>NaiveIterations</span><span>(std::</span><span>string_view</span><span><span> </span><span>s</span><span>, std::</span></span><span>string_view</span><span><span> </span><span>delim</span><span>, </span></span><span>int</span><span><span> </span><span>cap</span><span>) {</span></span></div></div><div><div><div>25</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>start</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>26</div></div><div><span>  </span><span>int</span><span> </span><span>n</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>27</div></div><div><span>  </span><span>while</span><span> (</span><span>n</span><span> </span><span>&lt;</span><span> </span><span>cap</span><span>) {</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>std::</span><span>size_t</span><span> </span><span>end</span><span> </span><span>=</span><span> </span><span>s</span><span>.</span><span>find</span><span>(</span><span>delim</span><span>, </span><span>start</span><span>);</span></div></div><div><div><div>29</div></div><div><span>    </span><span>if</span><span> (</span><span>end</span><span> </span><span>==</span><span> std::string_view::</span><span>npos</span><span>) </span><span>return</span><span> </span><span>n</span><span>;</span></div></div><div><div><div>30</div></div><div><span>    </span><span>start</span><span> </span><span>=</span><span> </span><span>end</span><span> </span><span>+</span><span> </span><span>delim</span><span>.</span><span>size</span><span>();</span></div></div><div><div><div>31</div></div><div><span>    </span><span>++</span><span>n</span><span>;</span></div></div><div><div><div>32</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>33</div></div><div><span>  </span><span>return</span><span> </span><span>n</span><span>;</span></div></div><div><div><div>34</div></div><div><span>}</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span>void</span><span> </span><span>Show</span><span>(std::</span><span>string_view</span><span><span> </span><span>s</span><span>, std::</span></span><span>string_view</span><span><span> </span><span>delim</span><span>) {</span></span></div></div><div><div><div>37</div></div><div><span>  </span><span>auto</span><span> </span><span>parts</span><span> </span><span>=</span><span> </span><span>SplitBy</span><span>(</span><span>s</span><span>, </span><span>delim</span><span>);</span></div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\"</span><span>"</span><span> </span><span>&lt;&lt;</span><span> </span><span>s</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\"</span><span> by </span><span>\"</span><span>"</span><span> </span><span>&lt;&lt;</span><span> </span><span>delim</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\"</span><span> -&gt; "</span><span> </span><span>&lt;&lt;</span><span> </span><span>parts</span><span>.</span><span>size</span><span>()</span></div></div><div><div><div>39</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>":"</span><span>;</span></div></div><div><div><div>40</div></div><div><span>  </span><span>for</span><span> (</span><span>auto</span><span> </span><span>p</span><span> : </span><span>parts</span><span>) std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>" ["</span><span> </span><span>&lt;&lt;</span><span> </span><span>p</span><span> </span><span>&lt;&lt;</span><span> </span><span>"]"</span><span>;</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>42</div></div><div><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>45</div></div><div><span>  </span><span>Show</span><span>(</span><span>"a--b--c"</span><span>, </span><span>"--"</span><span>);</span></div></div><div><div><div>46</div></div><div><span>  </span><span>Show</span><span>(</span><span>"a--b--"</span><span>, </span><span>"--"</span><span>);</span></div></div><div><div><div>47</div></div><div><span>  </span><span>Show</span><span>(</span><span>"aaa"</span><span>, </span><span>"aa"</span><span>);</span></div></div><div><div><div>48</div></div><div><span>  </span><span>Show</span><span>(</span><span>"aaaa"</span><span>, </span><span>"aa"</span><span>);</span></div></div><div><div><div>49</div></div><div><span>  </span><span>Show</span><span>(</span><span>"abc"</span><span>, </span><span>""</span><span>);</span></div></div><div><div><div>50</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"naive loop with empty delimiter: still running after "</span></div></div><div><div><div>51</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>NaiveIterations</span><span>(</span><span>"abc"</span><span>, </span><span>""</span><span>, </span><span>1000000</span><span>) </span><span>&lt;&lt;</span><span> </span><span>" iterations</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>52</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"a--b--c" by "--" -&gt; 3: [a] [b] [c]</span></div></div><div><div><div>2</div></div><div><span>"a--b--" by "--" -&gt; 3: [a] [b] []</span></div></div><div><div><div>3</div></div><div><span>"aaa" by "aa" -&gt; 2: [] [a]</span></div></div><div><div><div>4</div></div><div><span>"aaaa" by "aa" -&gt; 3: [] [] []</span></div></div><div><div><div>5</div></div><div><span>"abc" by "" -&gt; 1: [abc]</span></div></div><div><div><div>6</div></div><div><span>naive loop with empty delimiter: still running after 1000000 iterations</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>最后一行是故意写的 BAD 版本：给循环加了一个上限，否则它就真的不会停。</p><ol>
<li>libc++ 的增长路径是 22 → 47 → 95 → …，用它算一算 1000 万次 <code>push_back</code> 为什么是 19 次分配，再对照 libstdc++ 的 20 次。</li>
</ol>
参考答案<p>libstdc++ 每次容量翻倍，从 15 开始：<span><span>15×2k≥10715 \times 2^{k} \ge 10^7</span><span><span><span></span><span>15</span><span></span><span>×</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>k</span></span></span></span></span></span></span></span></span><span></span><span>≥</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span></span></span></span> 的最小 <span><span>kk</span><span><span><span></span><span>k</span></span></span></span> 是 20（<span><span>15×219=786432015 \times 2^{19} = 7864320</span><span><span><span></span><span>15</span><span></span><span>×</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>19</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>7864320</span></span></span></span> 不够，<span><span>15×220=1572864015 \times 2^{20} = 15728640</span><span><span><span></span><span>15</span><span></span><span>×</span><span></span></span><span><span></span><span><span>2</span><span><span><span><span><span><span></span><span><span><span>20</span></span></span></span></span></span></span></span></span><span></span><span>=</span><span></span></span><span><span></span><span>15728640</span></span></span></span> 够），所以 20 次。</p><p>libc++ 的数列 22、47、95 不是纯翻倍。我拟合出的规律是：新容量 = 把 <span><span>2c+12c + 1</span><span><span><span></span><span>2</span><span>c</span><span></span><span>+</span><span></span></span><span><span></span><span>1</span></span></span></span> 向上取整到 16 的倍数，再减 1（<span><span>22→45→48→4722 \to 45 \to 48 \to 47</span><span><span><span></span><span>22</span><span></span><span>→</span><span></span></span><span><span></span><span>45</span><span></span><span>→</span><span></span></span><span><span></span><span>48</span><span></span><span>→</span><span></span></span><span><span></span><span>47</span></span></span></span>，<span><span>47→95→96→9547 \to 95 \to 96 \to 95</span><span><span><span></span><span>47</span><span></span><span>→</span><span></span></span><span><span></span><span>95</span><span></span><span>→</span><span></span></span><span><span></span><span>96</span><span></span><span>→</span><span></span></span><span><span></span><span>95</span></span></span></span>）。这个规律是我从数字里反推的，没有读 libc++ 的源码确认，但它预测的增长次数和最终容量都和实际观察完全一致。从 22 开始需要 19 次才到 <span><span>≥107\ge 10^7</span><span><span><span></span><span>≥</span><span></span></span><span><span></span><span>1</span><span><span>0</span><span><span><span><span><span><span></span><span><span>7</span></span></span></span></span></span></span></span></span></span></span>，所以是 19 次。起点高（22 对 15）加上每次略多于翻倍，省了一次。</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;iostream&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string&gt;</span></div></div><div><div><div>3</div></div><div><span>#include</span><span> </span><span>&lt;vector&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>std::</span><span>vector</span><span>&lt;std::</span><span>size_t</span><span>&gt; </span><span>Observe</span><span>(std::</span><span>size_t</span><span><span> </span><span>n</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>size_t</span><span>&gt; </span><span>caps</span><span>;</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>std::</span><span>string</span><span> </span><span>s</span><span>;</span></div></div><div><div><div>8</div></div><div><span>  </span><span>caps</span><span>.</span><span>push_back</span><span>(</span><span>s</span><span>.</span><span>capacity</span><span>());</span></div></div><div><div><div>9</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>n</span><span>; </span><span>++</span><span>i</span><span>) {</span></div></div><div><div><div>10</div></div><div><span>    </span><span>s</span><span>.</span><span>push_back</span><span>(</span><span>'x'</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>if</span><span> (</span><span>s</span><span>.</span><span>capacity</span><span>() </span><span>!=</span><span> </span><span>caps</span><span>.</span><span>back</span><span>()) </span><span>caps</span><span>.</span><span>push_back</span><span>(</span><span>s</span><span>.</span><span>capacity</span><span>());</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>  </span><span>return</span><span> </span><span>caps</span><span>;</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>17</div></div><div><span>  </span><span>constexpr</span><span> std::</span><span>size_t</span><span> </span><span>kN</span><span> </span><span>=</span><span> </span><span>10'000'000</span><span>;</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>std::</span><span>vector</span><span>&lt;std::</span><span>size_t</span><span>&gt; </span><span>caps</span><span> </span><span>=</span><span> </span><span>Observe</span><span>(</span><span>kN</span><span>);</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"observed:"</span><span>;</span></div></div><div><div><div>20</div></div><div><span>  </span><span>for</span><span> (std::</span><span>size_t</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>0</span><span>; </span><span>i</span><span> </span><span>&lt;</span><span> </span><span>5</span><span>; </span><span>++</span><span>i</span><span>) std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>" "</span><span> </span><span>&lt;&lt;</span><span> </span><span>caps</span><span>[</span><span>i</span><span>];</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>" ... "</span><span> </span><span>&lt;&lt;</span><span> </span><span>caps</span><span>.</span><span>back</span><span>() </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"growths to reach "</span><span> </span><span>&lt;&lt;</span><span> </span><span>kN</span><span> </span><span>&lt;&lt;</span><span> </span><span>": "</span><span> </span><span>&lt;&lt;</span><span> </span><span>caps</span><span>.</span><span>size</span><span>() </span><span>-</span><span> </span><span>1</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>std::</span><span>size_t</span><span> </span><span>cap</span><span> </span><span>=</span><span> </span><span>caps</span><span>.</span><span>front</span><span>();</span></div></div><div><div><div>25</div></div><div><span>  </span><span>int</span><span> </span><span>n</span><span> </span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>26</div></div><div><span>  </span><span>while</span><span> (</span><span>cap</span><span> </span><span>&lt;</span><span> </span><span>kN</span><span>) {</span></div></div><div><div><div>27</div></div><div><span>    </span><span>cap</span><span> </span><span>=</span><span> (</span><span>2</span><span> </span><span>*</span><span> </span><span>cap</span><span> </span><span>+</span><span> </span><span>1</span><span> </span><span>+</span><span> </span><span>15</span><span>) </span><span>/</span><span> </span><span>16</span><span> </span><span>*</span><span> </span><span>16</span><span> </span><span>-</span><span> </span><span>1</span><span>;</span><span>  // round the block up to 16 bytes</span></div></div><div><div><div>28</div></div><div><span>    </span><span>++</span><span>n</span><span>;</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>std::</span><span>cout</span><span> </span><span>&lt;&lt;</span><span> </span><span>"model (2 * cap + 1 rounded up to 16, minus 1): "</span><span> </span><span>&lt;&lt;</span><span> </span><span>n</span></div></div><div><div><div>31</div></div><div><span>            </span><span>&lt;&lt;</span><span> </span><span>" growths, final capacity "</span><span> </span><span>&lt;&lt;</span><span> </span><span>cap</span><span> </span><span>&lt;&lt;</span><span> </span><span>"</span><span>\n</span><span>"</span><span>;</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>在 libc++ 上的输出（<code>observed</code> 是真实观察，<code>model</code> 是上面的规律算出来的）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>observed: 22 47 95 191 383 ... 12582911</span></div></div><div><div><div>2</div></div><div><span>growths to reach 10000000: 19</span></div></div><div><div><div>3</div></div><div><span>model (2 * cap + 1 rounded up to 16, minus 1): 19 growths, final capacity 12582911</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同一个程序在 libstdc++ 上，<code>observed</code> 两行是真实的，<code>model</code> 那行不适用（它是 libc++ 的规律）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>observed: 15 30 60 120 240 ... 15728640</span></div></div><div><div><div>2</div></div><div><span>growths to reach 10000000: 20</span></div></div><div><div><div>3</div></div><div><span>model (2 * cap + 1 rounded up to 16, minus 1): 20 growths, final capacity 16777215</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/windows-scoop-package-manager-guide/</id>
      <title type="text">Windows 包管理器 Scoop：从底层原理、命令行到提交自己的软件包</title>
      <published>2025-12-05T13:00:00.000Z</published>
      <updated>2026-09-07T00:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/windows-scoop-package-manager-guide/"/>
      <summary type="text">从 Scoop 的目录、Shim、Bucket 和 Manifest 入手，完整讲解安装维护、仓库选择、清单编写、自动更新与提交 PR，并拆解两个我实际提交的软件包。</summary>
      <content type="html"><![CDATA[<p>我第一次写 Scoop 时，只记下了安装、添加 Bucket、更新和清理这几件事。它能用，但更像一张贴在显示器边上的命令速查表。后来我真的给 Scoop 提交了两个软件包，才发现 Scoop 最有意思的地方并不是少点几次 “下一步”，而是它把一个 Windows 软件的安装过程压缩成了一份可以阅读、审查和自动更新的 JSON 文件。</p>
<p>这次我想把盖子掀开。我们先看 <code>scoop install</code> 到底做了什么，再把日常命令走一遍，最后从零写一个 Manifest，并用我提交的 <a href="https://github.com/ScoopInstaller/Main/blob/master/bucket/mfgtools.json" target="_blank"><code>mfgtools</code></a> 和 <a href="https://github.com/Calinou/scoop-games/blob/master/bucket/axolotl-launcher.json" target="_blank"><code>axolotl-launcher</code></a> 两个真实案例，看看一个软件怎样进入公共 Bucket。</p>
<div><div><div></div><div>本文的边界</div></div><div><p>Scoop 可以管理大量便携软件，也可以静默处理部分安装程序，但它并不能把所有 Windows 软件都变成绿色软件。Manifest 中如果调用了厂商安装器或自定义脚本，软件仍可能写注册表、安装服务或修改系统目录。先读 Manifest，再决定是否安装。</p></div></div>
<section><h2>Scoop 到底是什么<a href="#scoop-到底是什么"><span>#</span></a></h2><p>Scoop 的核心其实很朴素：一组 PowerShell 脚本、若干 Git 仓库，以及描述软件安装方式的 JSON Manifest。它不像传统安装器那样努力接管整个系统，而是尽量把文件、版本、入口和持久数据收拢到自己的目录中。</p><p>我更愿意把它想成一个小型仓库管理员：</p><ul>
<li>Bucket 是货架目录，告诉它有哪些软件。</li>
<li>Manifest 是每件货物的装箱单，写明版本、下载地址、校验值和摆放方法。</li>
<li><code>apps</code> 保存按版本分开的程序文件。</li>
<li><code>shims</code> 提供稳定的命令入口。</li>
<li><code>persist</code> 保存更新时不能丢的数据。</li>
<li><code>cache</code> 保存已经下载的安装包。</li>
</ul><p>默认的用户级目录大致如下：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>%USERPROFILE%\scoop\</span></div></div><div><div><div>2</div></div><div><span>├── apps\</span></div></div><div><div><div>3</div></div><div><span>│   └── git\</span></div></div><div><div><div>4</div></div><div><span>│       ├── 2.51.0.windows.1\</span></div></div><div><div><div>5</div></div><div><span>│       └── current -&gt; 2.51.0.windows.1</span></div></div><div><div><div>6</div></div><div><span>├── buckets\</span></div></div><div><div><div>7</div></div><div><span>│   ├── main\</span></div></div><div><div><div>8</div></div><div><span>│   └── extras\</span></div></div><div><div><div>9</div></div><div><span>├── cache\</span></div></div><div><div><div>10</div></div><div><span>├── persist\</span></div></div><div><div><div>11</div></div><div><span>└── shims\</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这里最关键的是 <code>版本目录 -&gt; current -&gt; shim</code> 这条链。</p><p>假设我执行：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop install git</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Scoop 会完成这样一条流水线：</p><div><div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>在 Bucket 中找到 git.json</p></span></div><div><span><p>解析版本与系统架构</p></span></div><div><span><p>下载文件到 cache</p></span></div><div><span><p>计算并核对哈希</p></span></div><div><span><p>解压到 apps/git/版本号</p></span></div><div><span><p>执行 Manifest 安装步骤</p></span></div><div><span><p>创建 current 目录联接</p></span></div><div><span><p>在 shims 创建命令入口</p></span></div><div><span><p>连接 persist 数据并创建快捷方式</p></span></div>
</div><div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span></span></div><div><span><p>在 Bucket 中找到 git.json</p></span></div><div><span><p>解析版本与系统架构</p></span></div><div><span><p>下载文件到 cache</p></span></div><div><span><p>计算并核对哈希</p></span></div><div><span><p>解压到 apps/git/版本号</p></span></div><div><span><p>执行 Manifest 安装步骤</p></span></div><div><span><p>创建 current 目录联接</p></span></div><div><span><p>在 shims 创建命令入口</p></span></div><div><span><p>连接 persist 数据并创建快捷方式</p></span></div>
</div></div></div><p>这不是概念上的猜测。<a href="https://github.com/ScoopInstaller/Scoop/blob/master/lib/install.ps1" target="_blank">Scoop 的安装实现</a>就是按这个顺序调用下载、解压、安装脚本、<code>link_current</code>、<code>create_shims</code>、快捷方式和持久化逻辑。</p><section><h3><code>current</code> 为什么重要<a href="#current-为什么重要"><span>#</span></a></h3><p>每个版本各住一个目录，<code>current</code> 则是指向当前版本的目录联接。更新时，Scoop 可以先把新版本放进新目录，再把 <code>current</code> 改指过去。旧版本不会立刻消失，因此我们还能切换版本或在确认无误后清理它。</p><p>这有点像铁路道岔：程序版本是不同轨道，<code>current</code> 只负责把入口切到当前轨道。切换版本不需要搬动整列火车。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 查看一个软件当前实际目录</span></div></div><div><div><div>2</div></div><div><span>scoop prefix git</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 重新建立 current、Shim 和快捷方式</span></div></div><div><div><div>5</div></div><div><span>scoop reset git</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>Shim 为什么不等于把所有目录塞进 PATH<a href="#shim-为什么不等于把所有目录塞进-path"><span>#</span></a></h3><p>传统做法常常给每个软件追加一条 <code>PATH</code>。安装 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 个软件，环境变量里就可能多出 <span><span>NN</span><span><span><span></span><span>N</span></span></span></span> 个目录。Scoop 通常只需要把一个固定的 <code>shims</code> 目录加入 <code>PATH</code>，再由里面的入口转发到 <code>apps\&lt;app&gt;\current</code> 下的真实程序。</p><p>Manifest 中的 <code>bin</code> 决定创建哪些 Shim：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"bin"</span><span>: </span><span>"uuu.exe"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>它也能给程序换一个更适合敲命令的别名：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"bin"</span><span>: [</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>3</div></div><div><span>        </span><span>"Axolotl Launcher.exe"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>"axolotl-launcher"</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>6</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以升级后真实路径变了，命令名通常不变。<code>scoop which &lt;command&gt;</code> 可以沿着 Shim 找到它来自哪个软件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop which git</span></div></div><div><div><div>2</div></div><div><span><span>scoop which axolotl</span><span>-</span><span>launcher</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>Persist 怎样让配置跨版本存活<a href="#persist-怎样让配置跨版本存活"><span>#</span></a></h3><p>如果配置文件跟程序一起留在版本目录，清理旧版本时配置也会被删掉。Manifest 的 <code>persist</code> 会把这些文件或目录转移到 <code>persist\&lt;app&gt;</code>，再用目录联接或硬链接接回程序目录。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"persist"</span><span>: </span><span>"data"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>于是程序仍然以为自己在读 <code>apps\&lt;app&gt;\current\data</code>，实际数据却住在版本目录之外。普通卸载默认保留这些数据，彻底删除时才加 <code>--purge</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop uninstall &lt;app&gt;</span></div></div><div><div><div>2</div></div><div><span><span>scoop uninstall &lt;app&gt; </span><span>--</span><span>purge</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>Bucket 为什么本质上是供应链边界<a href="#bucket-为什么本质上是供应链边界"><span>#</span></a></h3><p>一个 Bucket 就是一个包含 JSON Manifest 的 Git 仓库。<code>scoop bucket add</code> 会把它克隆到本地，<code>scoop update</code> 再通过 Git 拉取变化。</p><p>这也意味着添加第三方 Bucket 不是一个纯粹的搜索操作，而是在增加新的软件供应源。Manifest 可以包含 <code>pre_install</code>、<code>installer</code> 和 <code>post_install</code> PowerShell 脚本。哈希能证明下载内容与 Manifest 期望的一致，却不能证明发布者或 Bucket 本身值得信任。密码学很认真，但它不会替我做价值判断，多少有点遗憾。</p><p>安装前我会看一眼 Manifest：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop info extras</span><span>/</span><span>vscode</span></span></div></div><div><div><div>2</div></div><div><span><span>scoop cat extras</span><span>/</span><span>vscode</span></span></div></div><div><div><div>3</div></div><div><span><span>scoop home extras</span><span>/</span><span>vscode</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果同名软件存在于多个 Bucket，我会显式写成 <code>bucket/app</code>，而不是让搜索顺序替我决定：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop install main</span><span>/</span><span>aria2</span></span></div></div><div><div><div>2</div></div><div><span><span>scoop install games</span><span>/</span><span>axolotl</span><span>-</span><span>launcher</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>安装 Scoop<a href="#安装-scoop"><span>#</span></a></h2><p><a href="https://github.com/ScoopInstaller/Scoop/wiki/Quick-Start" target="_blank">Scoop 当前要求 PowerShell 5.1 或更高版本</a>。先检查版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$PSVersionTable</span><span>.PSVersion</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>普通用户安装不需要管理员 PowerShell：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>Set-ExecutionPolicy</span><span><span> </span><span>-</span><span>ExecutionPolicy RemoteSigned </span><span>-</span><span>Scope CurrentUser</span></span></div></div><div><div><div>2</div></div><div><span>irm get.scoop.sh | iex</span></div></div><div><div><div>3</div></div><div><span>scoop help</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>irm ... | iex</code> 很方便，也确实是在下载远程脚本后立即执行。介意这一点时，可以先保存并检查脚本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>irm get.scoop.sh </span><span>-</span><span>OutFile install.ps1</span></span></div></div><div><div><div>2</div></div><div><span>Get-Content</span><span> .\install.ps1</span></div></div><div><div><div>3</div></div><div><span>.\install.ps1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>在安装前修改目录<a href="#在安装前修改目录"><span>#</span></a></h3><p>默认用户目录是 <code>%USERPROFILE%\scoop</code>，全局目录是 <code>%ProgramData%\scoop</code>。如果我想把用户级软件放到 <code>D:\Scoop</code>，会在安装前设置 <code>SCOOP</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>Environment</span><span>]::SetEnvironmentVariable(</span><span>"SCOOP"</span><span>, </span><span>"D:\Scoop"</span><span>, </span><span>"User"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>$env:SCOOP</span><span><span> </span><span>=</span><span> </span></span><span>"D:\Scoop"</span></div></div><div><div><div>3</div></div><div><span>irm get.scoop.sh | iex</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>全局目录由 <code>SCOOP_GLOBAL</code> 控制。写入 Machine 级环境变量和安装全局软件都需要管理员权限：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>[</span><span>Environment</span><span>]::SetEnvironmentVariable(</span><span>"SCOOP_GLOBAL"</span><span>, </span><span>"D:\ScoopGlobal"</span><span>, </span><span>"Machine"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我会使用纯英文、无空格的短路径。这不是为了某种神秘仪式，而是为了避开历史安装脚本对空格、编码和引号处理不一致的问题。</p></section><section><h3>已安装后的迁移<a href="#已安装后的迁移"><span>#</span></a></h3><p>已经安装后再搬目录，比安装前指定路径更容易遗漏状态。基本步骤是：</p><ol>
<li>关闭正在运行的 Scoop 软件。</li>
<li>把整个 Scoop 根目录移动到新位置。</li>
<li>更新 <code>SCOOP</code> 和 <code>PATH</code> 中的 <code>shims</code> 路径。</li>
<li>打开新终端，重建全部链接。</li>
</ol><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop reset </span><span>*</span></span></div></div><div><div><div>2</div></div><div><span>scoop checkup</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>我实际使用的命令行工作流<a href="#我实际使用的命令行工作流"><span>#</span></a></h2><p>Scoop 的帮助风格更像 Git，而不是传统 PowerShell Cmdlet：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop help</span></div></div><div><div><div>2</div></div><div><span>scoop help install</span></div></div><div><div><div>3</div></div><div><span>scoop help bucket</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我把常用命令分成发现、安装、维护和迁移四组。记住工作流比背完整命令表更有用。</p><section><h3>发现软件<a href="#发现软件"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 搜索名称和命令</span></div></div><div><div><div>2</div></div><div><span>scoop search ripgrep</span></div></div><div><div><div>3</div></div><div><span>scoop search rg</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 查看来源、版本、依赖等信息</span></div></div><div><div><div>6</div></div><div><span><span>scoop info main</span><span>/</span><span>ripgrep</span></span></div></div><div><div><div>7</div></div><div><span><span>scoop depends main</span><span>/</span><span>ripgrep</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># 阅读安装清单和打开项目主页</span></div></div><div><div><div>10</div></div><div><span><span>scoop cat main</span><span>/</span><span>ripgrep</span></span></div></div><div><div><div>11</div></div><div><span><span>scoop home main</span><span>/</span><span>ripgrep</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>搜索不到时，先到 <a href="https://scoop.sh/" target="_blank">Scoop Directory</a> 搜索社区 Bucket。仍然没有，再考虑自己写 Manifest，不要随手下载一个来历不明的 <code>setup.exe</code> 然后给它管理员权限。</p></section><section><h3>安装、卸载与全局安装<a href="#安装卸载与全局安装"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 安装一个或多个软件</span></div></div><div><div><div>2</div></div><div><span>scoop install git aria2</span></div></div><div><div><div>3</div></div><div><span><span>scoop install extras</span><span>/</span><span>vscode</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 指定架构</span></div></div><div><div><div>6</div></div><div><span><span>scoop install main</span><span>/</span><span>7zip </span><span>--</span><span>arch 64bit</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 卸载，但保留 persist 数据</span></div></div><div><div><div>9</div></div><div><span>scoop uninstall vscode</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># 连同 persist 数据彻底删除</span></div></div><div><div><div>12</div></div><div><span><span>scoop uninstall vscode </span><span>--</span><span>purge</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>只有确实需要所有用户共享时，我才使用全局安装：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 在管理员 PowerShell 中执行</span></div></div><div><div><div>2</div></div><div><span><span>scoop install git </span><span>--</span><span>global</span></span></div></div><div><div><div>3</div></div><div><span><span>scoop update git </span><span>--</span><span>global</span></span></div></div><div><div><div>4</div></div><div><span><span>scoop uninstall git </span><span>--</span><span>global</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>--skip-hash-check</code> 会跳过完整性校验。除非我正在调试自己刚写的 Manifest，否则不会使用它，更不会把它当作下载失败的通用修复。</p></section><section><h3>更新、锁定、回退与清理<a href="#更新锁定回退与清理"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 更新 Scoop 本体和本地 Bucket</span></div></div><div><div><div>2</div></div><div><span>scoop update</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 查看哪些软件可以更新</span></div></div><div><div><div>5</div></div><div><span>scoop status</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 更新一个软件或全部软件</span></div></div><div><div><div>8</div></div><div><span>scoop update git</span></div></div><div><div><div>9</div></div><div><span><span>scoop update </span><span>*</span></span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># 暂停和恢复某个软件的更新</span></div></div><div><div><div>12</div></div><div><span>scoop hold git</span></div></div><div><div><div>13</div></div><div><span>scoop unhold git</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果 Manifest 带有 <code>autoupdate</code>，Scoop 还能根据当前清单生成指定旧版本的 Manifest：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop install gh</span><span>@2.7.0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这不是对任意软件、任意历史版本都有效。下载地址模式变过、旧文件被上游删除、旧哈希无法取得时，生成仍会失败。</p><p>版本切换或 Shim 冲突时使用 <code>reset</code>：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop reset python</span></div></div><div><div><div>2</div></div><div><span>scoop reset python</span><span>@3.12.10</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>确认新版本工作正常后再清理旧目录和下载缓存：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop cleanup </span><span>*</span></span></div></div><div><div><div>2</div></div><div><span>scoop cache show</span></div></div><div><div><div>3</div></div><div><span><span>scoop cache rm </span><span>*</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>cleanup</code> 删除旧的安装版本，<code>cache rm</code> 删除下载包，两者不是同一件事。磁盘空间凭空消失时，我通常两个地方都看。</p></section><section><h3>导出和恢复环境<a href="#导出和恢复环境"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop export &gt; scoopfile.json</span></div></div><div><div><div>2</div></div><div><span>scoop import scoopfile.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>当前的 <code>export</code> 会记录应用和 Bucket，也可以按命令帮助提供的选项导出配置。我仍然会单独备份重要的 <code>persist</code> 数据，因为软件列表不是数据备份，正如购物清单不是冰箱。</p></section><section><h3>Aria2 要不要装<a href="#aria2-要不要装"><span>#</span></a></h3><p>Scoop 检测到 Aria2 后会使用它进行多连接下载：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop install aria2</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我不再默认推荐把连接数一口气调到 <code>16</code>。单文件限速、代理、CDN 策略和网络质量不同，多连接不一定更快，反而可能触发限流。遇到异常时先关闭它做对照：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop config aria2</span><span>-</span><span>enabled false</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>确认确实受益，再按自己的网络配置 <code>aria2-split</code> 和 <code>aria2-max-connection-per-server</code>。</p></section></section>
<section><h2>推荐添加哪些 Bucket<a href="#推荐添加哪些-bucket"><span>#</span></a></h2><p>Scoop 内置的 <a href="https://github.com/ScoopInstaller/Scoop/blob/master/buckets.json" target="_blank">Known Bucket 列表</a> 当前包含 10 个名字。先查看，再按需添加：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop bucket known</span></div></div><div><div><div>2</div></div><div><span>scoop bucket list</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我通常从下面几个开始：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop bucket add extras</span></div></div><div><div><div>2</div></div><div><span>scoop bucket add versions</span></div></div><div><div><div>3</div></div><div><span>scoop bucket add java</span></div></div><div><div><div>4</div></div><div><span><span>scoop bucket add nerd</span><span>-</span><span>fonts</span></span></div></div><div><div><div>5</div></div><div><span>scoop bucket add games</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><a href="https://github.com/ScoopInstaller/Main" target="_blank"><code>main</code></a>：默认启用，主要收录知名、稳定、非 GUI 的开发工具。</li>
<li><a href="https://github.com/ScoopInstaller/Extras" target="_blank"><code>extras</code></a>：浏览器、编辑器和桌面 GUI 软件最常用的来源。</li>
<li><a href="https://github.com/ScoopInstaller/Versions" target="_blank"><code>versions</code></a>：旧版、测试版和并行版本。只有确实需要多版本时再添加。</li>
<li><a href="https://github.com/ScoopInstaller/Java" target="_blank"><code>java</code></a>：不同厂商和版本的 JDK/JRE。</li>
<li><a href="https://github.com/matthewjberger/scoop-nerd-fonts" target="_blank"><code>nerd-fonts</code></a>：Nerd Fonts 字体，安装字体通常需要管理员权限。</li>
<li><a href="https://github.com/Calinou/scoop-games" target="_blank"><code>games</code></a>：开源或免费游戏、启动器和相关工具。</li>
</ul><p>另外四个更偏场景化：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop bucket add nirsoft</span></div></div><div><div><div>2</div></div><div><span>scoop bucket add sysinternals</span></div></div><div><div><div>3</div></div><div><span>scoop bucket add php</span></div></div><div><div><div>4</div></div><div><span>scoop bucket add nonportable</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>nirsoft</code> 和 <code>sysinternals</code>：对应两套 Windows 系统工具集合。</li>
<li><code>php</code>：需要切换多个 PHP 版本时有用。</li>
<li><code>nonportable</code>：收录必须运行传统安装器的软件。它天然更可能修改系统状态，我不会无脑添加。</li>
</ul><p>第三方 Bucket 需要同时给名字和 Git 地址：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop bucket add my</span><span>-</span><span>bucket https:</span><span>//</span></span><span>github.com</span><span><span>/</span><span>&lt;user&gt;</span><span>/</span><span>my</span><span>-</span><span>bucket</span></span></div></div><div><div><div>2</div></div><div><span><span>scoop bucket rm my</span><span>-</span><span>bucket</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Bucket 越多，搜索结果和信任面越大。我的策略很简单：需要哪个就加哪个，不把 <code>bucket known</code> 当成集邮册。</p></section>
<section><h2>官方仓库没有软件时，先写一个 Manifest<a href="#官方仓库没有软件时先写一个-manifest"><span>#</span></a></h2><p>Manifest 是 Scoop 的真正接口。<a href="https://github.com/ScoopInstaller/Scoop/wiki/App-Manifests" target="_blank">官方定义</a>很直接：它是一份描述如何安装程序的 JSON 文件。</p><p>最小可用清单通常只需要版本、下载地址、哈希和命令入口，再补齐公共 Bucket 要求的描述、主页与许可证：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"version"</span><span>: </span><span>"1.0.0"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"description"</span><span>: </span><span>"A small command-line tool that does one useful thing"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"homepage"</span><span>: </span><span>"https://github.com/example/tool"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"license"</span><span>: </span><span>"MIT"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"architecture"</span><span>: {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>"url"</span><span>: </span><span>"https://github.com/example/tool/releases/download/v1.0.0/tool-windows-x64.zip"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>"hash"</span><span>: </span><span>"&lt;sha256&gt;"</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"bin"</span><span>: </span><span>"tool.exe"</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保存为 <code>tool.json</code> 后，可以直接安装本地文件：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop install .\tool.json</span></div></div><div><div><div>2</div></div><div><span>scoop which tool</span></div></div><div><div><div>3</div></div><div><span>scoop uninstall tool</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>也可以让 Scoop 根据下载 URL 生成一个骨架，再手工补齐：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop create https:</span><span>//</span></span><span>github.com</span><span><span>/</span><span>example</span><span>/</span><span>tool</span><span>/</span><span>releases</span><span>/</span><span>download</span><span>/</span><span>v1.</span></span><span>0.0</span><span><span>/</span><span>tool</span><span>-</span><span>windows</span><span>-</span><span>x64.zip</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h3>先研究发布物，不要先写 JSON<a href="#先研究发布物不要先写-json"><span>#</span></a></h3><p>我写 Manifest 前会回答这几个问题：</p><ol>
<li>上游有没有稳定、可预测、无需登录的直链？</li>
<li>发布物是单个 EXE、ZIP、7z、MSI，还是必须运行的安装器？</li>
<li>压缩包解开后，真正的程序在哪一层目录？</li>
<li>软件把配置、存档和缓存写到哪里？哪些应该 <code>persist</code>？</li>
<li>版本能否从 GitHub Releases、网页、JSON API 或文件名稳定提取？</li>
<li>下个版本发布时，URL 能否只替换 <code>$version</code> 就得到？</li>
</ol><p>这里最常见的错误是围着当前版本硬编码，最后得到一个只能安装一次的 JSON。好的 Manifest 描述的不是一个文件，而是一族版本。</p></section><section><h3>哈希是安装契约的一部分<a href="#哈希是安装契约的一部分"><span>#</span></a></h3><p>Scoop 默认使用 SHA256。PowerShell 可以直接计算：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>Invoke-WebRequest</span><span> </span><span>$url</span><span><span> </span><span>-</span><span>OutFile .\download.zip</span></span></div></div><div><div><div>2</div></div><div><span>(</span><span>Get-FileHash</span><span><span> .\download.zip </span><span>-</span><span>Algorithm SHA256).Hash.ToLower()</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>SHA256 是 256 bit，通常写成 64 个十六进制字符。只要上游悄悄替换了同一 URL 下的文件，哈希检查就会失败。这既可能是供应链风险，也可能只是上游重新打包，但两种情况都值得停下来调查，而不是条件反射地跳过校验。</p></section><section><h3>常用字段怎样协作<a href="#常用字段怎样协作"><span>#</span></a></h3><ul>
<li><code>architecture</code>：分别描述 <code>64bit</code>、<code>32bit</code> 和 <code>arm64</code> 发布物。</li>
<li><code>url</code> / <code>hash</code>：下载地址与对应校验值，多个 URL 就要有同样数量的哈希。</li>
<li><code>extract_dir</code> / <code>extract_to</code>：控制从压缩包中取哪一层、放到哪里。</li>
<li><code>bin</code>：创建命令行 Shim，也可以定义别名和固定参数。</li>
<li><code>shortcuts</code>：创建开始菜单快捷方式，适合 GUI 软件。</li>
<li><code>persist</code>：把配置或用户数据移出版本目录。</li>
<li><code>depends</code>：必须安装的运行时依赖。</li>
<li><code>suggest</code>：可选增强，不强制安装。</li>
<li><code>pre_install</code> / <code>post_install</code>：安装前后的 PowerShell 逻辑，能不用就不用。</li>
<li><code>installer</code> / <code>uninstaller</code>：必须运行传统安装器时使用。</li>
<li><code>checkver</code>：找出上游最新版本。</li>
<li><code>autoupdate</code>：用最新版本号重建 URL 等字段。</li>
</ul><p>公共 Bucket 偏爱简单、可预测、便携的安装。十几行自定义 PowerShell 也许能把任何东西按进 Scoop，但维护者还要在六个月后理解它。能用字段表达，就不要写脚本。</p></section><section><h3>让软件自动跟随新版本<a href="#让软件自动跟随新版本"><span>#</span></a></h3><p>GitHub Releases 是最省事的一种情况：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"checkver"</span><span>: {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"github"</span><span>: </span><span>"https://github.com/example/tool"</span></div></div><div><div><div>3</div></div><div><span>},</span></div></div><div><div><div>4</div></div><div><span>"autoupdate"</span><span>: {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"architecture"</span><span>: {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>"url"</span><span>: </span><span>"https://github.com/example/tool/releases/download/v$version/tool-$version-windows-x64.zip"</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>checkver</code> 负责回答 “最新版本是什么”，<code>autoupdate</code> 负责回答 “知道版本号后，下载地址长什么样”。Scoop 的自动更新工具会替换版本、生成新 URL、下载新文件并计算哈希。</p><p>在 Bucket 仓库根目录可以这样检查：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>$checkver</span><span><span> </span><span>=</span><span> </span></span><span>"</span><span>$(</span><span>scoop prefix scoop</span><span>)</span><span>\bin\checkver.ps1"</span></div></div><div><div><div>2</div></div><div><span>&amp; </span><span>$checkver</span><span> tool .\bucket</span></div></div><div><div><div>3</div></div><div><span>&amp; </span><span>$checkver</span><span><span> tool .\bucket </span><span>-</span><span>Update</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>然后安装生成后的本地 Manifest，不能只看 JSON 长得顺眼：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>scoop install .\bucket\tool.json</span></div></div><div><div><div>2</div></div><div><span>scoop which tool</span></div></div><div><div><div>3</div></div><div><span>scoop uninstall tool</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>GUI 软件还要真的启动、创建配置、升级一次、卸载一次，再检查快捷方式和 <code>persist</code>。安装成功只证明安装流程没有立刻摔倒，标准不算太高。</p></section></section>
<section><h2>案例一：我提交的 mfgtools<a href="#案例一我提交的-mfgtools"><span>#</span></a></h2><p><a href="https://github.com/ScoopInstaller/Main/blob/master/bucket/mfgtools.json" target="_blank"><code>mfgtools.json</code></a> 是最干净的一类 CLI Manifest。NXP 的项目名叫 mfgtools，实际命令行程序叫 <code>uuu</code>。截至本文更新时，Manifest 安装 <code>1.5.243</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"version"</span><span>: </span><span>"1.5.243"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"description"</span><span>: </span><span>"Freescale/NXP I.MX Chip image deploy tools."</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>"homepage"</span><span>: </span><span>"https://github.com/nxp-imx/mfgtools"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"license"</span><span>: </span><span>"BSD-3-Clause"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"architecture"</span><span>: {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>8</div></div><div><span>            </span><span>"url"</span><span>: </span><span>"https://github.com/nxp-imx/mfgtools/releases/download/uuu_1.5.243/uuu.exe"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>            </span><span>"hash"</span><span>: </span><span>"f6b76a6246befabeadfebdc1cbfe58f35939596caf7b78717ceab599b0c85027"</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>12</div></div><div><span>    </span><span>"bin"</span><span>: </span><span>"uuu.exe"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>    </span><span>"checkver"</span><span>: {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>"github"</span><span>: </span><span>"https://github.com/nxp-imx/mfgtools"</span><span>,</span></div></div><div><div><div>15</div></div><div><span>        </span><span>"regex"</span><span>: </span><span>"uuu_([</span><span>\\</span><span>d.]+)"</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>17</div></div><div><span>    </span><span>"autoupdate"</span><span>: {</span></div></div><div><div><div>18</div></div><div><span>        </span><span>"architecture"</span><span>: {</span></div></div><div><div><div>19</div></div><div><span>            </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>20</div></div><div><span>                </span><span>"url"</span><span>: </span><span>"https://github.com/nxp-imx/mfgtools/releases/download/uuu_$version/uuu.exe"</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我在 <a href="https://github.com/ScoopInstaller/Main/pull/7614" target="_blank">PR #7614</a> 提交了它。这个包的安装路径只有几个动作：下载 1 个 <code>uuu.exe</code>，验证 1 个 SHA256，创建 1 个 <code>uuu</code> Shim。没有压缩包，没有快捷方式，没有持久数据，也没有安装脚本。</p><p>稍微特殊的是上游 Tag 为 <code>uuu_1.5.243</code>，而 Scoop 版本只需要 <code>1.5.243</code>。因此 <code>checkver.regex</code> 从 Tag 中捕获数字部分，<code>autoupdate.url</code> 再把 <code>uuu_</code> 前缀拼回去。</p><p>这份清单后来已经由 GitHub Actions 从我提交的 <code>1.5.233</code> 自动更新到 <code>1.5.243</code>。从手工提交到机器人接管后续版本，这才是 <code>checkver + autoupdate</code> 真正完成闭环的地方。</p><p>它适合进入 <code>main</code>，因为它是非 GUI 开发工具、提供稳定版本直链，而且安装过程足够标准。<a href="https://github.com/ScoopInstaller/Scoop/wiki/Criteria-for-including-apps-in-the-main-bucket" target="_blank"><code>main</code> 的收录标准</a>还会考虑知名度、稳定版、完整版本和维护复杂度。写好 Manifest 只是技术门槛，选对 Bucket 是产品门槛。</p></section>
<section><h2>案例二：我提交的 axolotl-launcher<a href="#案例二我提交的-axolotl-launcher"><span>#</span></a></h2><p><a href="https://github.com/Calinou/scoop-games/blob/master/bucket/axolotl-launcher.json" target="_blank"><code>axolotl-launcher.json</code></a> 是另一端。它是 Minecraft Java Edition 的 GUI 启动器，所以我把它提交到 <code>games</code>，对应 <a href="https://github.com/Calinou/scoop-games/pull/1795" target="_blank">PR #1795</a>。</p><p>下载部分并不复杂：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"architecture"</span><span>: {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>        </span><span>"url"</span><span>: </span><span>"https://github.com/Mystic-Stars/Axolotl/releases/download/v1.9.5/Axolotl_Launcher_1.9.5_x64_portable.zip"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>        </span><span>"hash"</span><span>: </span><span>"f83b553a3b7857927e4a190ba7dd548ce33532b482a06d1fda8519901bed65fa"</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>6</div></div><div><span>},</span></div></div><div><div><div>7</div></div><div><span>"extract_dir"</span><span>: </span><span>"Axolotl"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>但一个 GUI 软件还要处理入口、快捷方式、运行时建议和用户数据：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"suggest"</span><span>: {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"Microsoft Edge WebView2"</span><span>: </span><span>"extras/webview2"</span></div></div><div><div><div>3</div></div><div><span>},</span></div></div><div><div><div>4</div></div><div><span>"bin"</span><span>: [</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>6</div></div><div><span>        </span><span>"Axolotl Launcher.exe"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>        </span><span>"axolotl-launcher"</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>9</div></div><div><span>],</span></div></div><div><div><div>10</div></div><div><span>"shortcuts"</span><span>: [</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>12</div></div><div><span>        </span><span>"Axolotl Launcher.exe"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>"Axolotl Launcher"</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>15</div></div><div><span>],</span></div></div><div><div><div>16</div></div><div><span>"persist"</span><span>: [</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>[</span></div></div><div><div><div>18</div></div><div><span>        </span><span>".Axolotl"</span><span>,</span></div></div><div><div><div>19</div></div><div><span>        </span><span>"red.ghs.axolotl"</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>21</div></div><div><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>这里同时产生 2 个用户入口：终端里的 <code>axolotl-launcher</code> 和开始菜单里的 <code>Axolotl Launcher</code>。WebView2 是可选依赖，所以用 <code>suggest</code>，不是强制所有用户再安装一份 <code>depends</code>。</p><p>真正麻烦的是旧数据迁移。Axolotl 原本把数据放在 <code>%APPDATA%\red.ghs.axolotl</code>，而 Scoop 需要把它纳入 <code>persist</code>。Manifest 的 <code>pre_install</code> 做了几件防守性工作：</p><ol>
<li>只有目标持久化目录不存在、旧数据存在时才迁移。</li>
<li>如果 Axolotl 仍在运行，立即中止，避免复制变化中的数据。</li>
<li>先复制到 <code>.migration</code> 暂存目录，成功后再移动到最终位置。</li>
<li>失败时清理暂存目录并重新抛出错误。</li>
<li>最后删除压缩包自带的空 <code>.Axolotl</code>，让 <code>persist</code> 接管这个位置。</li>
</ol><p>这比 <code>mfgtools</code> 多得多，但复杂度来自一个真实约束：已经使用官方安装方式的用户不能因为改用 Scoop 就丢掉数据。这里不能用一句 <code>Copy-Item</code> 糊过去。数据迁移像心脏手术，“大部分复制成功” 不是一种可以接受的状态。</p><p>它同样使用 GitHub Releases 做 <code>checkver</code>，并用 <code>$version</code> 构造后续下载地址：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>"checkver"</span><span>: {</span></div></div><div><div><div>2</div></div><div><span>    </span><span>"github"</span><span>: </span><span>"https://github.com/Mystic-Stars/Axolotl"</span></div></div><div><div><div>3</div></div><div><span>},</span></div></div><div><div><div>4</div></div><div><span>"autoupdate"</span><span>: {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>"architecture"</span><span>: {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>"64bit"</span><span>: {</span></div></div><div><div><div>7</div></div><div><span>            </span><span>"url"</span><span>: </span><span>"https://github.com/Mystic-Stars/Axolotl/releases/download/v$version/Axolotl_Launcher_$version_x64_portable.zip"</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个案例放在一起，Scoop 的设计就很清楚了：简单软件保持极简，复杂软件只为真实状态增加逻辑。不是每个 Manifest 都需要 <code>pre_install</code>，正如不是每个纸箱都需要起重机。</p></section>
<section><h2>把 Manifest 提交到公共 Bucket<a href="#把-manifest-提交到公共-bucket"><span>#</span></a></h2><p>根据 Scoop 当前的<a href="https://github.com/ScoopInstaller/.github/blob/main/.github/CONTRIBUTING.md" target="_blank">贡献规范</a>，我会按下面的顺序提交。</p><section><h3>1. 选对仓库并先提 Issue<a href="#1-选对仓库并先提-issue"><span>#</span></a></h3><p>先根据软件类型选择 <code>main</code>、<code>extras</code>、<code>games</code> 或其他专用 Bucket，搜索是否已有同名软件和相关 Issue。贡献规范要求新软件先创建 Issue，说明实现方式和加入理由，等待维护者确认后再写 PR。</p><p><code>main</code> 的要求最严格。一个 GUI 软件即使 Manifest 完美，也不应该硬塞进 <code>main</code>。我的两个例子正好展示了这个分流：<code>mfgtools</code> 去 <code>main</code>，Axolotl Launcher 去 <code>games</code>。</p></section><section><h3>2. Fork、建分支、添加 JSON<a href="#2-fork建分支添加-json"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>git clone https:</span><span>//</span></span><span>github.com</span><span><span>/</span><span>&lt;your</span><span>-</span><span>name&gt;</span><span>/</span><span>Main.git</span></span></div></div><div><div><div>2</div></div><div><span>Set-Location</span><span> Main</span></div></div><div><div><div>3</div></div><div><span>git </span><span>switch</span><span><span> </span><span>-</span><span>c </span></span><span>add-tool</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># 把清单放进 bucket 目录</span></div></div><div><div><div>6</div></div><div><span>git add .\bucket\tool.json</span></div></div><div><div><div>7</div></div><div><span><span>git commit </span><span>-</span><span>m </span></span><span>"tool: Add version 1.0.0"</span></div></div><div><div><div>8</div></div><div><span><span>git push </span><span>-</span><span>u origin </span></span><span>add-tool</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>字段顺序也有约定，大致从元数据、依赖、架构和安装步骤，走到入口、持久化、版本检测与自动更新。直接参考 <a href="https://github.com/ScoopInstaller/BucketTemplate/blob/master/bucket/app-name.json.template" target="_blank">BucketTemplate</a>，比发明自己的排列方式省下 review 往返。</p><p>公共 Bucket 还要求：</p><ul>
<li>使用 4 个空格缩进。</li>
<li><code>license</code> 尽量使用有效的 SPDX Identifier。</li>
<li>优先提供便携配置和 <code>persist</code>。</li>
<li>纯 CLI 软件不创建 <code>shortcuts</code>。</li>
<li>不接受命令行参数的纯 GUI 软件通常不需要 <code>bin</code>。</li>
<li>数组只有一个元素时尽量改成字符串。</li>
<li>只有 32 bit 下载时可以省略 <code>architecture</code>，其他情况应明确架构。</li>
</ul></section><section><h3>3. 做一次完整生命周期测试<a href="#3-做一次完整生命周期测试"><span>#</span></a></h3><p>我至少会验证：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span># JSON 能否解析</span></div></div><div><div><div>2</div></div><div><span>Get-Content</span><span><span> .\bucket\tool.json </span><span>-</span><span>Raw | </span></span><span>ConvertFrom-Json</span><span> | </span><span>Out-Null</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 本地清单能否完成安装和运行</span></div></div><div><div><div>5</div></div><div><span>scoop install .\bucket\tool.json</span></div></div><div><div><div>6</div></div><div><span>scoop which tool</span></div></div><div><div><div>7</div></div><div><span><span>tool </span><span>--</span><span>version</span></span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span># checkver 能否找到同一版本，autoupdate 能否生成清单</span></div></div><div><div><div>10</div></div><div><span>$checkver</span><span><span> </span><span>=</span><span> </span></span><span>"</span><span>$(</span><span>scoop prefix scoop</span><span>)</span><span>\bin\checkver.ps1"</span></div></div><div><div><div>11</div></div><div><span>&amp; </span><span>$checkver</span><span> tool .\bucket</span></div></div><div><div><div>12</div></div><div><span>&amp; </span><span>$checkver</span><span><span> tool .\bucket </span><span>-</span><span>Update</span></span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span># 卸载后入口是否消失</span></div></div><div><div><div>15</div></div><div><span>scoop uninstall tool</span></div></div><div><div><div>16</div></div><div><span>Get-Command</span><span><span> tool </span><span>-</span><span>ErrorAction SilentlyContinue</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>GUI 软件不能用 <code>--version</code> 就算了。我会实际启动它，检查快捷方式，写入一份配置，执行升级，再确认配置仍在。然后普通卸载一次、<code>--purge</code> 卸载一次，分别观察持久数据是否保留和删除。</p></section><section><h3>4. 发 PR，让自动验证器工作<a href="#4-发-pr让自动验证器工作"><span>#</span></a></h3><p>新 Manifest 的 PR 标题格式是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;app name&gt;: Add version &lt;version&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>提交 PR 后，再评论：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/verify</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这会启动自动 Manifest verifier。CI 通过不代表 review 一定结束，维护者仍可能要求简化脚本、修正许可证、调整 Bucket、补充 <code>persist</code>，或证明 URL 足够稳定。我会把这些反馈理解成未来维护成本的预演，而不是 JSON 格式考试。</p></section><section><h3>5. 合并之后，自动更新才刚开始值班<a href="#5-合并之后自动更新才刚开始值班"><span>#</span></a></h3><p>只要 <code>checkver</code> 和 <code>autoupdate</code> 正确，Bucket 的自动化就能发现上游新版本、替换 URL、重新计算哈希并提交更新。我的两个 Manifest 都已经进入这条流水线。</p><p>这也是我现在判断一个 Manifest 是否完成的标准：不是 “今天能装”，而是 “下个版本发布后，我大概率不用半夜爬起来改 3 个数字”。</p></section></section>
<section><h2>继续往里走<a href="#继续往里走"><span>#</span></a></h2><p>Scoop 把 Windows 软件安装拆成了几个普通、透明的部件：Git 提供版本化的软件目录，JSON 描述安装契约，SHA256 检查下载内容，目录联接切换版本，Shim 稳定命令入口，<code>persist</code> 隔离用户数据。每一层都不神秘，组合起来却把大量一次性的鼠标操作变成了可审查、可复现的文本。</p><p>如果你准备写第一个 Manifest，我建议按这个顺序继续读：</p><ul>
<li><a href="https://github.com/ScoopInstaller/Scoop/wiki/App-Manifests" target="_blank">Scoop App Manifests 字段参考</a></li>
<li><a href="https://github.com/ScoopInstaller/Scoop/wiki/Creating-an-app-manifest" target="_blank">Creating an App Manifest</a></li>
<li><a href="https://github.com/ScoopInstaller/Scoop/wiki/App-Manifest-Autoupdate" target="_blank">App Manifest Autoupdate</a></li>
<li><a href="https://github.com/ScoopInstaller/Scoop/wiki/Persistent-data" target="_blank">Persistent data</a></li>
<li><a href="https://github.com/ScoopInstaller/.github/blob/main/.github/CONTRIBUTING.md" target="_blank">Scoop 贡献规范</a></li>
<li><a href="https://github.com/ScoopInstaller/BucketTemplate" target="_blank">BucketTemplate</a></li>
</ul><p>挑一个你真的在用、上游发布方式稳定、安装流程足够简单的软件开始。先让它在自己的电脑上可靠地安装、更新、卸载，再把那份 JSON 交给整个社区。最坏的情况，你更理解了一个软件如何在 Windows 上生活；最好的情况，下一位用户只需要输入 <code>scoop install</code>。这笔交易相当不错。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/english-translation-techniques/</id>
      <title type="text">英语翻译技巧</title>
      <published>2025-12-02T01:35:00.000Z</published>
      <updated>2025-12-02T01:35:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/english-translation-techniques/"/>
      <summary type="text">本文深入解析了中英翻译中的核心思维差异（如动态与静态、意合与形合），介绍了英语句法的平衡学原则（尾重原则），并提供了一套实用的“翻译三步法”（拆解、降级、润色），旨在帮助备考 CET-6 或雅思的同学克服中式英语，提升学术写作与翻译水平。</summary>
      <content type="html"><![CDATA[<p>在备考 CET-6 或雅思写作时，许多同学面临的最大障碍不是词汇量，而是<strong>思维模式（Mindset）</strong>。中英文在底层逻辑上存在巨大的鸿沟：中文重意合、动态、叙事；英文重形合、静态、结构。</p>
<p>本文将拆解中英翻译的<strong>核心差异</strong>，并提供一套可操作的<strong>翻译三步法</strong>。</p>
<section><h2>一、 核心思维差异（Core Divergences）<a href="#一-核心思维差异core-divergences"><span>#</span></a></h2><section><h3>1. 动态 vs. 静态（Nominalization）<a href="#1-动态-vs-静态nominalization"><span>#</span></a></h3><blockquote><p><strong>法则：</strong> 汉语多用<strong>动词</strong>（Verb-heavy）；学术英语多用<strong>名词</strong>（Noun-heavy）。</p></blockquote><p>在翻译时，要学会<strong>名词化（Nominalization）</strong>：将中文的动作或性质抽象为英语的名词短语。这能显著提升句子的“信息密度”。</p><ul>
<li><strong>原文</strong>：这就解释了为什么我们需要从不同角度看问题。</li>
<li><strong>❌ Chinglish:</strong> This explains why we need to <strong>see</strong> the problem from different angles. (动词堆砌)</li>
<li><strong>✅ Academic:</strong> This is an <strong>illustration</strong> of the <strong>need</strong> for a different <strong>perspective</strong> on the issue. (名词主导)</li>
</ul></section><section><h3>2. 主动 vs. 被动（Objectivity）<a href="#2-主动-vs-被动objectivity"><span>#</span></a></h3><blockquote><p><strong>法则：</strong> 汉语习惯“人”做主语；英语习惯“物”做主语，强调客观事实。</p></blockquote><ul>
<li><strong>原文:</strong> 人们普遍认为，阅读能扩大词汇量。</li>
<li><strong>❌ Chinglish:</strong> People generally believe that reading can increase vocabulary. (语法正确，但太口语)</li>
<li><strong>✅ Academic:</strong> It is widely <strong>acknowledged</strong> that vocabulary can be <strong>expanded</strong> through reading. (被动结构，客观严谨)</li>
</ul></section><section><h3>3. 意合 vs. 形合（Explicit Logic）<a href="#3-意合-vs-形合explicit-logic"><span>#</span></a></h3><blockquote><p><strong>法则：</strong> 汉语逻辑往往隐含在语序中（隐性）；英语逻辑必须通过<strong>连接词</strong>显性表达（显性）。</p></blockquote><p>中文常用“流水句”，英语则必须用逻辑词像胶水一样把句子粘起来。</p>

<table><thead><tr><th><strong>逻辑关系</strong></th><th><strong>中文结构 (隐性)</strong></th><th><strong>英文连接词 (显性)</strong></th></tr></thead><tbody><tr><td><strong>因果</strong></td><td>天气冷，电池坏了。</td><td>The battery failed <strong>due to</strong> the low temperature.</td></tr><tr><td><strong>转折</strong></td><td>代码跑通了，效率很低。</td><td>The code runs successfully; <strong>however</strong>, its efficiency is low.</td></tr><tr><td><strong>递进</strong></td><td>他修好了bug，优化了算法。</td><td>He fixed the bug; <strong>furthermore</strong>, he optimized the algorithm.</td></tr></tbody></table><hr /></section></section>
<section><h2>二、 句子的平衡学（Syntactic Balance）<a href="#二-句子的平衡学syntactic-balance"><span>#</span></a></h2><section><h3>1. 尾重原则（End-Weight）<a href="#1-尾重原则end-weight"><span>#</span></a></h3><p>英语句法中，句子是有“物理重量”的。</p><ul>
<li><strong>轻元素：</strong> 代词、系动词 (is/are)。</li>
<li><strong>重元素：</strong> 长从句、复杂修饰语。</li>
</ul><p>为了避免“头重脚轻”，英语倾向于<strong>快速给出主谓骨架</strong>，把复杂的修饰扔到后面。</p><ul>
<li><strong>原文:</strong> 掌握一门外语对大学生来说是至关重要的。</li>
<li><strong>❌ Chinglish:</strong> <em>To master a foreign language</em> is crucial for college students. (主语太长)</li>
<li><strong>✅ Academic:</strong> <strong>It is</strong> crucial for college students <strong>to master a foreign language proficiently</strong>. (形式主语 It 占位)</li>
</ul><p><strong>尾重原则</strong>出现是因为<strong>人类的短期记忆是有限的</strong>。如果主语（Subject）太长，听者/读者要在大脑里面一直“悬着”这个主语，等很久才能等到谓语（Verb），这会造成认知负担（Cognitive Load）。</p></section><section><h3>2. 如何选择主句（Subject Selection）<a href="#2-如何选择主句subject-selection"><span>#</span></a></h3><p>英语句子只能有一个核心（Main Clause），其他信息必须降级。</p><ul>
<li><strong>结果（观点） &gt; 原因/方式：</strong> 英语看重 Result（新信息）。
<ul>
<li><em>原文:</em> 他每天努力学习，终于考上了北大。</li>
<li><em>译文:</em> He was admitted to Peking University (<strong>主句</strong>) by studying hard every day.</li>
</ul>
</li>
<li><strong>实意动作 &gt; 状态描述：</strong>
<ul>
<li><em>原文:</em> 他是个热心人，经常帮助邻居。</li>
<li><em>译文:</em> Being a warm-hearted man (<strong>降级为分词</strong>), he frequently helps his neighbors (<strong>主句</strong>).</li>
</ul>
</li>
</ul><hr /></section></section>
<section><h2>三、 翻译三步法<a href="#三-翻译三步法"><span>#</span></a></h2><section><h3>Step 1: 拆解与定位 (Analysis)<a href="#step-1-拆解与定位-analysis"><span>#</span></a></h3><p>不要一上来就翻。先找到中文里“谁”做了“什么”。如果句子过长（比如一段话只有一个句号的场景），需要根据句子表达的含义对句子进行划分层次，对每个层次分别进行翻译，再使用逻辑词联系上下文。</p><ul>
<li><strong>注意：</strong> 中文常省略主语，翻译时必须补全（如补 <code>It</code> 或 <code>There be</code>）。</li>
</ul></section><section><h3>Step 2: 转换与降级 (Syntactic Demotion)<a href="#step-2-转换与降级-syntactic-demotion"><span>#</span></a></h3><p>确定主句后，对其余成分进行<strong>语法降级</strong>：</p><ol>
<li><strong>V-ing / V-ed (分词):</strong> 表伴随、原因。 (<em>Standing at the window…</em>)</li>
<li><strong>Preposition (介词):</strong> 学术神器 <code>With</code>, <code>By</code>, <code>Due to</code>, <code>In terms of</code>. (<em>With the population increasing…</em>)</li>
<li><strong>Clause (从句):</strong> <code>Which</code>, <code>That</code>, <code>Although</code>.</li>
</ol></section><section><h3>Step 3: 润色与精准 (Polishing)<a href="#step-3-润色与精准-polishing"><span>#</span></a></h3><ul>
<li><strong>拒绝绝对化 (Hedging):</strong> 除非是定理，否则少用 <code>must/will</code>，多用 <code>may</code>, <code>likely</code>, <code>tend to</code>。体现学术严谨性。</li>
<li><strong>词汇升级 (Lexical Precision):</strong>
<ul>
<li><code>Change</code> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <code>Modify / Alter</code></li>
<li><code>Think</code> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <code>Argue / Contend</code></li>
<li><code>Make</code> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <code>Generate / Fabricate</code></li>
</ul>
</li>
</ul><hr /></section></section>
<section><h2>四、 总结<a href="#四-总结"><span>#</span></a></h2><p>翻译的本质不是 Words to Words，而是 <strong>Meaning to Meaning</strong>。</p><ol>
<li><strong>多用名词</strong>代替动词。</li>
<li><strong>多用被动</strong>代替主动。</li>
<li><strong>多用连接词</strong>显化逻辑。</li>
<li><strong>长短结合</strong>，利用 <code>It</code> 句型平衡结构。</li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/understanding-logic-of-concession/</id>
      <title type="text">深入理解逻辑与英语中的 “让步” (Concession)</title>
      <published>2025-12-02T01:30:00.000Z</published>
      <updated>2025-12-02T01:30:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/understanding-logic-of-concession/"/>
      <summary type="text">为什么我们在英语学习中常听到“让步状语从句”？“让步”的逻辑内核究竟是什么？本文通过“弹簧效应”的比喻，解析让步逻辑的本质——“欲扬先抑”。同时探讨了为何英语强调逻辑连接词，以及在沟通、辩论和工程思维中如何利用“让步”来增强论证力度和体现严谨性。</summary>
      <content type="html"><![CDATA[<section><h2>什么是 “让步” 的逻辑内核？<a href="#什么是-让步-的逻辑内核"><span>#</span></a></h2><p>在正常的线性逻辑中，我们通常认为：</p><ul>
<li>原因 A <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 导致结果 B</li>
<li>例如：因为下雨了（A），所以我没出门（B）。<span><span>←\leftarrow</span><span><span><span></span><span>←</span></span></span></span> 顺理成章。</li>
</ul><p>但是，世界是复杂的。有时候，原因 A 发生了，它<strong>预期</strong>的结果 B 缺没有发生，<strong>反而</strong>发生了相反的结果 C。</p><ul>
<li>事实 A（本来应该阻碍 C）<span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 依然发生了结果 C。</li>
<li>例如：虽然下雨了（A），但我还是出门了（C）。</li>
</ul><p>这就是 “让步” 的<strong>本质</strong>：</p><div><div><div></div><div>Tip</div></div><div><p>承认一个不利于你结论的事实（这就是 “让”，退一步），然后转折，通过强调你的结论依旧成立，来反衬结论的强大（这就是 “步”，进两步）。</p></div></div><p>想象一个弹簧：你把它<strong>压下去（让步）</strong>，是为了让它<strong>弹得更高（主句）</strong>。</p></section>
<section><h2>为什么英语中如此强调 “让步”？<a href="#为什么英语中如此强调-让步"><span>#</span></a></h2><p>在英语学习中经常看到 “<strong>让步状语从句</strong>”（Adverbial Clause of Concession），是因为英语非常注重<strong>逻辑连接词</strong>（Connectives）。</p><p>在中文里，我们往往靠语境来表达：“下雨了，我还是去了。”（没有关联词，你也懂）。但在英语里，必须用<strong>逻辑连接词</strong>把这个逻辑关系标出来。</p><section><h3>常见的表达 “让步” 的逻辑连接词<a href="#常见的表达-让步-的逻辑连接词"><span>#</span></a></h3><p>英语里的 <em>Although</em>, <em>Though</em>, <em>Even if</em>, <em>Despite</em>, <em>In spite of</em> 都在大声喊：“注意！下面我要说一个跟前面预期相反的结论了！”</p></section><section><h3>经典的 “虽然……但是……” 陷阱<a href="#经典的-虽然但是-陷阱"><span>#</span></a></h3><p>这是中国学生最容易错的地方。</p><p>中文逻辑： 因为有冲突，所以既要说“虽然”，又要说“但是”来强调冲突。例如：虽然他很穷，但是他很开心。 (双重标记)。</p><p>而在英文逻辑中： 只要你用了 “虽然（Although）”，逻辑关系就已经确立了，再加“但是（But）”就是废话（Redundant）。例如：Although he is poor, he is happy. (正确)，而 Although he is poor, but he is happy. (错误)</p></section></section>
<section><h2>为什么我们需要 “让步”？<a href="#为什么我们需要-让步"><span>#</span></a></h2><p>你可能会问：“为什么要这么麻烦？直接说结论不就行了吗？”。比如，为什么要说 “虽然我很忙，但我会帮你”？直接说 “我会帮你” 不行吗？运用“让步”逻辑，通常有三个高级目的：</p><section><h3>增强论证的力度（反衬法）<a href="#增强论证的力度反衬法"><span>#</span></a></h3><p>普通说法： “这个电机性能很好。”（平铺直叙）让步说法： “虽然这个电机体积只有拳头大，但它能输出巨大的扭矩。”</p><p>通过承认“体积小”这个通常的劣势，反衬出“扭矩大”这个技术指标的强悍。这就是工程思维中的 Trade-off（权衡）后的亮点。</p></section><section><h3>展现思维的严谨性（防守反击）<a href="#展现思维的严谨性防守反击"><span>#</span></a></h3><p>在与他人进行辩论时，如果你只说自己对，别人会觉得你偏激。而如果采用让步策略： “我承认（Concede）你的观点 X 在某些情况下是有道理的（让一步），但是，在当前的核心场景下，我的方案 Y 更优。”</p><p>以此达到的效果效果是你先封堵了对方的嘴，表明你考虑过反对意见，显得你客观、全面。</p></section><section><h3>礼貌与情商（缓冲地带）<a href="#礼貌与情商缓冲地带"><span>#</span></a></h3><p>直接拒绝： “你是错的。”（太生硬），而如果采用让步拒绝： “尽管你的出发点是好的，但这个方案目前不可行。”，以此达到的效果： 先肯定对方的一部分（给面子），再否定核心内容。</p></section></section>
<section><h2>总结<a href="#总结"><span>#</span></a></h2><p>回到最开始的问题，“让步”到底是什么意思？</p><ul>
<li>字面上： 先退一步，承认障碍或对立面的存在。</li>
<li>逻辑上： 即使有 <span><span>XX</span><span><span><span></span><span>X</span></span></span></span> 这个干扰因素，结论 <span><span>YY</span><span><span><span></span><span>Y</span></span></span></span> 依然成立。</li>
<li>功能上： 它是一种**“欲扬先抑”**的思维工具。</li>
</ul><p>简而言之：让步，就是为了让你的结论看起来更不容易、更坚定、或者更客观，而故意先承认一些相反事实的逻辑手段。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/cs2-optimization-guide/</id>
      <title type="text">CS2 游戏设置优化</title>
      <published>2025-11-25T15:00:00.000Z</published>
      <updated>2025-11-25T15:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/cs2-optimization-guide/"/>
      <summary type="text">告别默认设置！本文提供从画面优化、鼠标灵敏度到枪械配装的全方位 CS2 入门指引，助你提升 FPS 与游戏体验。</summary>
      <content type="html"><![CDATA[<p>CS2（Counter-Strike 2）是 Valve 推出的经典第一人称射击游戏，继承了 CS 的核心玩法并全面升级了 Source 2 引擎。</p>
<p>游戏的默认设置通常并不适合竞技，我们需要做出一些更改。本文设置的核心逻辑是：<strong>流畅度（FPS） &gt; 画面清晰度（能看清敌人） &gt; 画面特效</strong>。</p>
<section><h2>启动项设置（Launch Options）<a href="#启动项设置launch-options"><span>#</span></a></h2><p>在 Steam 库中右键 CS2 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 属性 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 通用 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 启动选项，输入以下代码：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>-novid</span><span> </span><span>-nojoy</span><span> </span><span>-consold</span><span> </span><span>-high</span><span> </span><span>+fps_max</span><span> </span><span>0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>novid</code>：跳过开场动画，启动更快。</li>
<li><code>nojoy</code>：禁用手柄支持，释放少量系统资源。</li>
<li><code>-console</code>：开启控制台。</li>
<li><code>-high</code>：将 CS2 进程设置为高优先级，<strong>理论上</strong>能提升性能稳定性。</li>
<li><code>+fps_max 0</code>：解除游戏内帧率上限，让显卡全力运行。</li>
</ul></section>
<section><h2>视频设置（Video Settings）<a href="#视频设置video-settings"><span>#</span></a></h2><p>按 Esc <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 设置 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 视频 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 高级视频</p><ul>
<li><strong>增强角色对比度</strong>（Boost Player Contrast）：【启用】（必开！这能让你在暗处更容易看清敌人，非常重要）</li>
<li><strong>多重采样抗锯齿</strong>（MSAA）：【4X MSAA】（建议值。如电脑配置较低，可降至 2X。不建议关闭，否则物体边缘锯齿严重，难以看清远处的敌人）</li>
<li><strong>全局阴影效果</strong>（Global Shadow Quality）：【高】或【中】（尽量要开低，因为这决定了能否看到墙角的敌人影子）</li>
<li><strong>模型/贴图细节</strong>（Shader Detail）：【低】（为了提高 FPS，且画面更干净，减少干扰）</li>
<li><strong>着色器细节</strong>（Shader Detail）：【低】（CS2 的水面和燃烧瓶）</li>
<li><strong>粒子细节</strong>（Praticle Detail）：【低】（同上）</li>
<li><strong>环境光遮蔽</strong>（Ambient Occlusion）：【禁用】（一种增加画面深度的特效，但会显著降低 FPS，对竞技无益）</li>
<li><strong>高动态范围</strong>（HDR）：【性能】（Performance）</li>
<li><strong>NVIDIA Reflex 低延迟</strong>：【启用 + 增强】（减少鼠标延迟，手感更跟手）</li>
</ul><p>常规视频设置：</p><ul>
<li>显示模式：【全屏】（必须全屏，窗口化会有输入延迟）</li>
<li>刷新率：选你显示器支持的最高数值（例如 144Hz，240Hz，这在 FPS 游戏中至关重要）</li>
</ul></section>
<section><h2>鼠标与感度（Mouse &amp; Sensitivity）<a href="#鼠标与感度mouse--sensitivity"><span>#</span></a></h2><p><strong>核心原则：千万不要用过高的灵敏度</strong>。低灵敏度能提供更强的稳定性和精准度。</p><ul>
<li>鼠标 DPI（硬件）：建议设置为 400 或 800（大多数职业选手的选择）</li>
<li>游戏内灵敏度
<ul>
<li>如果你是 800 DPI，建议灵敏度在 0.8 - 1.2 之间。</li>
<li>如果你是 1600 DPI，建议灵敏度在 0.4 - 0.6 之间。</li>
<li>原则：你需要能够用一次从鼠标垫最左滑到最右的操作，在游戏中转身 180 度多一点点即可。太快了你根本瞄不准头。</li>
</ul>
</li>
<li>变焦灵敏度（狙击镜）：1.00（默认即可）。</li>
</ul><p><strong>特别提醒</strong>：请务必在 Windows 系统鼠标设置中，关闭“提高指针精确度”（Mouse Acceleration）。这对形成肌肉记忆非常有害。</p></section>
<section><h2>游戏设置（Game Settings）<a href="#游戏设置game-settings"><span>#</span></a></h2><p>主要调整 HUD（界面）和雷达，让你对战局更加清晰。</p><ul>
<li><strong>最大匹配延迟</strong>：100（防止匹配到太远的服务器，太卡没法玩）。</li>
<li><strong>启用开发者控制台（~）</strong>：【是】（以后你会用到很多指令）。</li>
</ul><p>雷达/平板（Radar）：非常重要，默认雷达太小了</p><ul>
<li><strong>雷达地图定中玩家</strong>：【是】</li>
<li><strong>雷达旋转</strong>：【是】</li>
<li><strong>雷达缩放</strong>（Radar Scale）：0.40 - 0.50（建议调小一点，这样你能看到大半张地图的信息，防止队友报点你看不到）。</li>
<li><strong>HUD 比例</strong>：可以稍微调小（0.9），让视野更开阔。</li>
</ul></section>
<section><h2>音频设置（Audio）<a href="#音频设置audio"><span>#</span></a></h2><ul>
<li><strong>主音量</strong>：适中。</li>
<li><strong>音频均衡器</strong>：【清晰】（Crisp）（通常能让脚步声更明显）。</li>
<li><strong>L/R 隔离度</strong>：50% - 70% 之间。</li>
<li><strong>音乐</strong>：
<ul>
<li><strong>主菜单音量</strong>：随意。</li>
<li><strong>MVP 音量</strong>：建议调低，太吵。</li>
<li><strong>炸弹/人质音量</strong>（Ten Second Warning）：【10% - 15%】（这个必须开！当音乐响起时，说明炸弹还有 10 秒爆炸，此时如果没有拆弹器就赶紧跑保抢，不要去拆了）。</li>
<li>其他音乐：全部设为 0（避免干扰听脚步）。</li>
</ul>
</li>
</ul></section>
<section><h2>准星推荐（Crosshair）<a href="#准星推荐crosshair"><span>#</span></a></h2><p>新手不建议用动态准星（走路回扩散的那种），建议用<strong>静态准星</strong>。</p><ul>
<li><strong>风格</strong>：经典（静态）</li>
<li><strong>颜色</strong>：绿色、青色或黄色（在地图中对比度高，容易看清）。</li>
<li><strong>中心点</strong>：不建议开</li>
</ul><p>推荐代码：按 Esc <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 设置 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 游戏 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 准星 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 分享或导入，粘贴下面的代码：</p><ul>
<li>ZywOo (世界顶级选手同款，小且精准): <code>CSGO-Qzpx5-BRLwA-xEljr-coqJp-hzrLq</code></li>
<li>简单易用的十字 (适合新手): <code>CSGO-7ANjy-XxRae-25YoE-7Bfnp-QQSFE</code></li>
</ul></section>
<section><h2>装备（Loadout）<a href="#装备loadout"><span>#</span></a></h2><p>CS2 采用 5-5-5 装备选择系统，你需要为每个栏位预设好枪械。</p><section><h3>T 方配置（恐怖分子）<a href="#t-方配置恐怖分子"><span>#</span></a></h3><p>T 的枪械的特点是：<strong>便宜</strong>、<strong>威力大</strong>（AK 能一枪爆头）、**适合进攻。</p><p><strong>手枪</strong>（Pistos）：</p><ul>
<li><strong>Glock-18</strong>（格洛克，默认）：自带的。跑打精准度高，适合手枪局一群人冲锋。</li>
<li><strong>P250</strong>：只要 $300，没钱时买一把，近距离大头很痛。</li>
<li><strong>Tec-9</strong>：T 方跑打之王。一定要带！它的射速快、弹匣大、移动射击极其精准。非常适合新手冲锋破点。</li>
<li><strong>Desert Eagle</strong>（沙鹰）：$700 的“手炮”。虽然难用，但是还是带上吧，虽然也用不到。</li>
<li><strong>Dual Berettas</strong> (双持贝瑞塔或双枪): 备选，近距离火力压制强。</li>
</ul><p><strong>中级武器</strong>（Mid-Tier）：</p><ul>
<li><strong>MAC-10</strong> (吹风机): 必带。T方经济局神器，跑得飞快，贴脸无敌，而且很便宜。</li>
<li><strong>MP7</strong>: 稍微贵一点，但比 MAC-10 更稳，适合稍微远一点的距离。</li>
<li><strong>XM1014</strong> (连喷): 全自动散弹枪，在特定地图（如办公室、炼狱小镇）用来无脑推土。</li>
</ul><p><strong>步枪</strong>（Rifles）：</p><ul>
<li><strong>AK-47</strong>: 核心中的核心。CS2 的灵魂，一枪爆头必杀。必须练好。</li>
<li><strong>Galil AR</strong>（加利尔/咖喱）: 穷人救星。当你钱不够买 AK 时，$1800 的咖喱是性价比最高的选择，弹匣大（35发），容错率高。</li>
<li><strong>SG 553</strong>: 带瞄准镜的步枪。虽然射速慢，但架点稳，打远距离比 AK 容易。</li>
<li><strong>SSG 08</strong>（鸟狙）: 便宜的狙击枪，移动速度快。</li>
<li><strong>AWP</strong>（大狙）: 必须带，哪怕你不玩，也要帮队友买。</li>
</ul></section><section><h3>CT 方配置（反恐精英）<a href="#ct-方配置反恐精英"><span>#</span></a></h3><p>CT 的枪械特点是：<strong>精准</strong>、<strong>消音</strong>、<strong>适合架点防守</strong>，但普遍较贵，且主战步枪不能一枪爆头（有头盔时）。</p><p><strong>手枪</strong>（Pistols）：</p><ul>
<li><strong>USP-S</strong>（默认）: 推荐选 USP 而不是 P2000。USP 有消音器，且第一发非常准，适合远距离点名。</li>
<li><strong>P250</strong>: 经济局混起专用。</li>
<li><strong>Five-SeveN</strong>（57/FN57）: CT方近战神枪。一定要带（替换掉 CZ75）。它的穿甲能力极强，弹匣大，当 T rush 过来时，这把手枪经常能刷屏。</li>
<li><strong>Desert Eagle</strong>: 同上。</li>
</ul><p><strong>中级武器</strong>（Mid-Tier）：</p><ul>
<li><strong>MP9</strong>（小蜜蜂）: CT方最强冲锋枪。射速极快，是反制 T Rush 的神器。跑着打、跳着打都很准。</li>
<li><strong>MAG-7</strong>（警喷）: 只有 5 发子弹，但它是唯一一把跳起来打依然精准的喷子，非常适合守门口。</li>
</ul><p><strong>步枪</strong>（Rifles）：</p><ul>
<li><strong>M4A1-S</strong> (消音M4): 新手强烈推荐，<strong>后坐力极小</strong>，非常好压枪。</li>
<li><strong>FAMAS</strong> (法玛斯): CT 的经济步枪。当你钱不够买 M4 时只能买它，虽然手感一般，但还得带。</li>
<li><strong>AUG</strong>: 带镜子的 M4。当你守长距离点位（如 Dust2 A大）时，开镜很稳。</li>
<li><strong>AWP</strong>: 防守方的威慑力核心。</li>
<li><strong>SSG 08</strong>: 鸟狙。</li>
</ul></section><section><h3>其他<a href="#其他"><span>#</span></a></h3><ul>
<li><strong>PP-Bizon</strong>（野牛）：非常推荐，跑打真的很爽，建议多玩。只要第一局什么都不起（不买甲，不买道具），哪怕手枪局输了，后面也能把把起全甲 + 野牛。</li>
<li><strong>M249</strong>：价格昂贵（$5200），性能笨重，实战性价比极低。不建议新手购买，有这钱不如给大哥发把大狙，再给自己买把野牛冲前面给大哥拿信息。</li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/xiaomi-debloat-guide/</id>
      <title type="text">小米手机 (HyperOS/MIUI) 深度优化指南：移除 AI 组件与去广告</title>
      <published>2025-11-25T14:00:00.000Z</published>
      <updated>2025-11-25T14:00:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/xiaomi-debloat-guide/"/>
      <summary type="text">通过 ADB 移除小米手机中冗余的 AI 组件、广告服务及臃肿软件，并调整系统设置以提升流畅度和续航。</summary>
      <content type="html"><![CDATA[<p>随着 HyperOS 和 MIUI 的更新，系统集成了越来越多的 AI 功能和后台服务。虽然部分功能很实用，但对于追求极致流畅、隐私保护或老机型用户来说，这些组件可能意味着额外的耗电和卡顿。</p>
<p>本文将介绍如何通过 ADB (Android Debug Bridge) 安全地移除这些组件，并进行系统级优化。</p>
<section><h2>通过 ADB 卸载 AI 及冗余组件<a href="#通过-adb-卸载-ai-及冗余组件"><span>#</span></a></h2><p>此操作<strong>不需要 ROOT 权限</strong>，但需要一台电脑（Windows/Mac/Linux 均可）。</p><section><h3>1. 准备工作<a href="#1-准备工作"><span>#</span></a></h3><ol>
<li>
<p><strong>开启开发者模式</strong>：</p>
<ul>
<li>进入<strong>设置</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>我的设备</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>全部参数</strong>。</li>
<li>连续点击“OS 版本号”约 5-7 次，直到提示“您已处于开发者模式”。</li>
</ul>
</li>
<li>
<p><strong>开启 USB 调试</strong>：</p>
<ul>
<li>进入<strong>设置</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>更多设置</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>开发者选项</strong>。</li>
<li>开启**“USB 调试”**。</li>
<li>开启 <strong>“USB 调试（安全设置）”</strong>（重要：如果不开启此项，部分系统应用无法被卸载，提示 <code>Permission denied</code>）。</li>
</ul>
</li>
<li>
<p><strong>连接电脑</strong>：</p>
<ul>
<li>使用数据线连接手机与电脑。</li>
<li>在电脑上下载并解压 <a href="https://developer.android.com/studio/releases/platform-tools" target="_blank">SDK Platform Tools</a> (ADB 工具包)。</li>
</ul>
</li>
</ol></section><section><h3>2. 执行卸载命令<a href="#2-执行卸载命令"><span>#</span></a></h3><p>在电脑终端（CMD 或 Powershell）进入 ADB 工具目录，输入以下命令确认连接：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>adb</span><span> </span><span>devices</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果手机弹出授权窗口，请点击“允许”。看到设备序列号后，即可使用以下格式卸载应用：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>adb</span><span> </span><span>shell</span><span> </span><span>pm</span><span> </span><span>uninstall</span><span> </span><span>--user</span><span> </span><span>0</span><span> </span><span>&lt;包名&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p><strong>注意</strong>：<code>--user 0</code> 表示仅针对当前用户卸载，不会破坏系统分区。如果出现问题，可以通过恢复出厂设置完全还原。</p></blockquote></section><section><h3>3. 建议卸载的清单<a href="#3-建议卸载的清单"><span>#</span></a></h3><p>以下是小米系统中常见的 AI 相关及“臃肿”组件的包名。请根据你的需求<strong>选择性</strong>执行：</p><section><h4>🤖 核心 AI 与语音组件<a href="#-核心-ai-与语音组件"><span>#</span></a></h4><p>如果你不使用小爱同学，这些服务在后台占用大量资源。</p>

<table><thead><tr><th>组件名称</th><th>包名</th><th>说明</th></tr></thead><tbody><tr><td><strong>小爱同学</strong></td><td><code>com.miui.voiceassist</code></td><td>语音助手主程序</td></tr><tr><td><strong>小米 AI 引擎</strong></td><td><code>com.xiaomi.aiasst.service</code></td><td>场景识别、AI 调度</td></tr><tr><td><strong>AI 视觉/传送门</strong></td><td><code>com.miui.contentextension</code></td><td>屏幕识别、选词搜索</td></tr><tr><td><strong>小爱建议/负一屏</strong></td><td><code>com.miui.personalassistant</code></td><td>桌面左侧的负一屏</td></tr><tr><td><strong>小爱翻译</strong></td><td><code>com.xiaomi.scanner</code></td><td>包含扫一扫和翻译功能</td></tr></tbody></table></section><section><h4>📢 广告与分析组件 (推荐删除)<a href="#-广告与分析组件-推荐删除"><span>#</span></a></h4><p>这些组件主要用于推送广告和收集数据，删除后系统更清爽。</p>

<table><thead><tr><th>组件名称</th><th>包名</th><th>说明</th></tr></thead><tbody><tr><td><strong>系统广告服务 (MSA)</strong></td><td><code>com.miui.systemAdSolution</code></td><td>MIUI System Ads，广告核心</td></tr><tr><td><strong>小米分析</strong></td><td><code>com.miui.analytics</code></td><td>数据采集与分析</td></tr><tr><td><strong>用户反馈</strong></td><td><code>com.miui.miservice</code></td><td>反馈服务</td></tr><tr><td><strong>智能服务</strong></td><td><code>com.miui.hybrid</code></td><td>混合推送服务</td></tr><tr><td><strong>快应用服务框架</strong></td><td><code>com.miui.hybrid.accessory</code></td><td>经常误触打开快应用</td></tr></tbody></table></section><section><h4>📦 其他常见臃肿软件<a href="#-其他常见臃肿软件"><span>#</span></a></h4>

<table><thead><tr><th>组件名称</th><th>包名</th><th>说明</th></tr></thead><tbody><tr><td><strong>小米浏览器</strong></td><td><code>com.android.browser</code></td><td>建议使用 Chrome 或 Edge 替代</td></tr><tr><td><strong>小米视频</strong></td><td><code>com.miui.video</code></td><td>包含大量在线内容</td></tr><tr><td><strong>小米音乐</strong></td><td><code>com.miui.player</code></td><td>QQ 音乐简化版，可以试一试</td></tr><tr><td><strong>小米钱包</strong></td><td><code>com.mipay.wallet</code></td><td>如果不用 NFC 刷卡可删</td></tr></tbody></table></section></section><section><h3>4. 如何恢复误删的应用？<a href="#4-如何恢复误删的应用"><span>#</span></a></h3><p>如果你不小心删除了重要组件，可以通过以下命令恢复（无需重置手机）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>adb</span><span> </span><span>shell</span><span> </span><span>cmd</span><span> </span><span>package</span><span> </span><span>install-existing</span><span> </span><span>&lt;包名&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>例如恢复小爱同学：<code>adb shell cmd package install-existing com.miui.voiceassist</code></p></section><section><h3>5. 推荐工具：UAD<a href="#5-推荐工具uad"><span>#</span></a></h3><p>如果你觉得敲命令太麻烦，可以使用开源图形化工具 <strong><a href="https://github.com/0x192/universal-android-debloater" target="_blank">UAD (Universal Android Debloater)</a></strong>。</p><p>UAD 是一个跨平台的图形界面软件，能自动识别小米手机的臃肿软件，并标注“安全删除”、“谨慎删除”，非常适合新手。</p><blockquote><p><strong>⚠️ 警告</strong>：绝对不要卸载 <code>com.miui.home</code>（系统桌面）或 <code>com.android.systemui</code>，否则手机会黑屏或无法进入系统。</p></blockquote><hr /></section></section>
<section><h2>系统设置优化<a href="#系统设置优化"><span>#</span></a></h2><p>除了卸载软件，正确的设置也能显著提升体验。</p><section><h3>1. 关闭内存扩展 (Memory Extension)<a href="#1-关闭内存扩展-memory-extension"><span>#</span></a></h3><ul>
<li><strong>路径</strong>：<strong>设置</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>更多设置</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>内存扩展</strong></li>
<li><strong>建议</strong>：<strong>关闭</strong></li>
<li><strong>原因</strong>：这个功能使用闪存（Flash Storage）模拟内存（SWAP）。手机闪存的随机读写速度远低于物理内存（RAM）。开启后不仅不会让手机变快，反而会增加 I/O 负担，导致系统在切换应用时掉帧，并增加发热和耗电。对于 8GB 及以上内存的机型，此功能完全是副作用。</li>
</ul></section><section><h3>2. 关闭系统广告开关<a href="#2-关闭系统广告开关"><span>#</span></a></h3><p>虽然卸载了 MSA，但部分开关仍建议手动关闭以防漏网之鱼。</p><ul>
<li><strong>系统工具广告</strong>：<strong>设置</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>账号</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>关于小米账号</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>系统广告</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 关闭。</li>
<li><strong>应用内推荐</strong>：分别进入<strong>天气、日历、下载管理、应用商店、文件管理</strong>的设置中，找到并关闭“内容推荐”、“个性化服务”或“资讯”。</li>
</ul></section><section><h3>3. 调整动画速度<a href="#3-调整动画速度"><span>#</span></a></h3><ul>
<li><strong>路径</strong>：<strong>开发者选项</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 向下滚动找到<strong>绘图</strong>栏目。</li>
<li><strong>设置</strong>：
<ul>
<li>窗口动画缩放 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>0.5x</strong></li>
<li>过渡动画缩放 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>0.5x</strong></li>
<li>Animator 时长缩放 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>0.5x</strong></li>
</ul>
</li>
<li><strong>效果</strong>：这不会改变手机的实际处理速度，但会缩短 UI 动画的持续时间，让手机在体感上响应更加迅速干脆。</li>
</ul><hr /><p>*免责声明：本文提供的操作涉及系统层面的修改。虽然通过 <code>pm uninstall --user 0</code> 卸载应用通常是安全的且可逆的，但操作不当仍可能导致系统不稳定。</p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/overleaf-community-windows/</id>
      <title type="text">在 Windows 上使用 Docker、Git Bash 和 overleaf-toolkit 部署 Overleaf 社区版</title>
      <published>2025-10-18T14:30:00.000Z</published>
      <updated>2025-10-18T14:30:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/overleaf-community-windows/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<section><h2>0. 前言<a href="#0-前言"><span>#</span></a></h2><p><a href="https://www.overleaf.com/" target="_blank">Overleaf</a> 是一个广受欢迎的在线 LaTeX 编辑器，它极大地简化了 LaTeX 的使用门槛。虽然官方提供了在线服务，但是处于数据隐私、网络稳定或自定义需求，我们常常希望搭建一个私有的 Overleaf 实例。Overleaf 官方提供了基于 Docker 的 <code>toolkit</code> 工具来简化社区版的部署流程。</p><p>然而，官方的 <code>toolkit</code> 主要面向 Linux/macOS 环境，在 Windows 上通过 Git Bash 执行时会遇到一个棘手的路径问题。本教程将详细记录如何在 Windows 11/10 环境下，借助 Docker Desktop 和 Git Bash，成功部署 Overleaf 社区版，并提供关键的脚本修复方案。</p></section>
<section><h2>1. 环境准备<a href="#1-环境准备"><span>#</span></a></h2><p>在开始之前，请确保你的系统已安装并正确配置了以下软件：</p><ul>
<li>Windows 10/11：需开启 Hyper-V 或 WSL 2 功能</li>
<li><a href="https://www.docker.com/products/docker-desktop/" target="_blank">Docker Desktop for Windows</a>：建议使用 WSL 2 后端，性能更佳。安装后请确保 Docker 服务已正常启动。</li>
<li><a href="https://git-scm.com/downloads/win" target="_blank">Git for Windows</a>: 安装时请务必勾选 <code>Git Bash Here</code> 选项，我们将使用它来执行 shell 脚本。</li>
</ul><p><strong>笔者环境参考</strong>：</p><ul>
<li>OS: Windows 11 Professional 24H2</li>
<li>Docker: 28.5.1</li>
<li>Git: 2.51.0.windows.2</li>
</ul></section>
<section><h2>2. 部署步骤<a href="#2-部署步骤"><span>#</span></a></h2><section><h3>第一步：克隆 Overleaf Toolkit 仓库<a href="#第一步克隆-overleaf-toolkit-仓库"><span>#</span></a></h3><p>首先，打开 Git Bash，选择一个你喜欢的工作目录（例如 <code>D:\</code>），然后执行以下命令克隆官方的 <code>toolkit</code> 仓库：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 进入 D:\</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>/d</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 克隆官方仓库</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>https://github.com/overleaf/toolkit.git</span><span> </span><span>./overleaf-toolkit</span></div></div><div><div><div>6</div></div><div><span>cd</span><span> </span><span>./overleaf-toolkit</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>第二步：修复 Windows 路径兼容性问题<a href="#第二步修复-windows-路径兼容性问题"><span>#</span></a></h3><p>这是整个教程最核心的一步。官方的 <code>docker-compose</code> 脚本在生成容器挂载路径时，没有正确处理 Git Bash 在 Windows 上的路径格式，导致 Docker 无法找到数据卷。</p><p>我已就此问题向官方提交了 <a href="https://github.com/overleaf/toolkit/issues/379" target="_blank">Issue #379</a>，并提交了修复方案 <a href="https://github.com/overleaf/toolkit/pull/380" target="_blank">PR #380</a>。</p><p>鉴于此 PR 还并未被合并到主分支，需要用户自行进行修改。</p><p>解决方案如下：</p><p>使用你喜欢的代码编辑器（如 VS Code 或者记事本）打开 <code>bin/docker-compose</code> 文件。</p><p>找到 <code>set_base_vars()</code> 函数，其中有两次对 <code>OVERLEAF_IN_CONTAINER_DATA_PATH</code> 变量的赋值。我们需要在路径的开头额外添加一个斜杠 <code>/</code>。</p><p>修改前：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>OVERLEAF_IN_CONTAINER_DATA_PATH</span><span>=</span><span>/var/lib/overleaf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>修改后：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>OVERLEAF_IN_CONTAINER_DATA_PATH</span><span>=</span><span>//var/lib/overleaf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意：此函数有<strong>两处</strong> <code>OVERLEAF_IN_CONTAINER_DATA_PATH=/var/lib/overleaf</code>，请务必全部修改。这个修改的本质是帮助 Git Bash/MSYS2 正确地将 POSIX 风格的路径 <code>/var/lib/overleaf</code> 转换为 Docker 能理解的 Windows 路径。</p></section><section><h3>第三步：初始化并启动 Overleaf 服务<a href="#第三步初始化并启动-overleaf-服务"><span>#</span></a></h3><p>修改脚本后，我们回到 Git Bash 终端，继续在 <code>overleaf-toolkit</code> 目录下执行命令。</p><section><h4>初始化 Overleaf 配置<a href="#初始化-overleaf-配置"><span>#</span></a></h4><p>执行 <code>init</code> 命令，该命令会生成初始化配置文件，例如 <code>config/overleaf.rc</code>、<code>config/variables.env</code>、<code>config/version</code> 等。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>./bin/init</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>启动 Overleaf 容器服务<a href="#启动-overleaf-容器服务"><span>#</span></a></h4><p>初始化完成后，执行 <code>up</code> 命令来启动所有服务。</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>./bin/up</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>提示：首次执行时，由于需要下载多个 Docker 镜像，此过程可能会持续较长时间，具体取决于你的网络速度。请耐心等待，确保所有镜像都成功拉取。</p><p>执行成功后，你将看到大量的服务日志输出。</p></section></section><section><h3>第四步：首次访问与管理员配置<a href="#第四步首次访问与管理员配置"><span>#</span></a></h3><p>服务启动后，打开你的浏览器，访问 <code>http://127.0.0.1</code>。</p><p>首次访问时，你会被引导至管理员账户创建页面。请根据提示设置你的第一个管理员用户的邮箱和密码。创建成功后，你就可以使用这个账户登录并开始使用你的私有 Overleaf 实例了！</p></section></section>
<section><h2>3. 日常管理<a href="#3-日常管理"><span>#</span></a></h2><p>在日常使用中，你可能会需要启动或停止 Overleaf 服务。</p><ul>
<li>停止服务：在 <code>overleaf-toolkit</code> 目录下打开 Git Bash，执行：</li>
</ul><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>./bin/stop</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>重新启动服务：同样，在 <code>overleaf-toolkit</code> 目录下执行：</li>
</ul><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>./bin/up</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>总结<a href="#总结"><span>#</span></a></h2><p>通过对 <code>bin/docker-compose</code> 脚本进行一个小小的修改，我们成功解决了 <code>overleaf-toolkit</code> 在 Windows + Git Bash 环境下的路径兼容性问题，顺利地部署了 Overleaf 社区版。自托管 Overleaf 不仅保障了数据安全，还提供了更自由的定制空间。</p><p>希望这篇教程能帮助到同样遇到此问题的同学。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/notes-of-effective-cmake/</id>
      <title type="text">Notes of Effective CMake</title>
      <published>2025-08-08T15:17:00.000Z</published>
      <updated>2025-08-08T15:17:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/notes-of-effective-cmake/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<section><h2>1. The Philosophy of Modern CMake<a href="#1-the-philosophy-of-modern-cmake"><span>#</span></a></h2><section><h3>Why “Effective CMake”?<a href="#why-effective-cmake"><span>#</span></a></h3><p>Just like with C++, the way you write CMake code significantly impacts your project’s maintainability, ease of use for others, and scalability. Adopting modern practices is key.</p></section><section><h3>CMake is Code<a href="#cmake-is-code"><span>#</span></a></h3><p>Treat your <code>CMakeLists.txt</code> files with the same care as your source code. Apply principles like Don’t Repeat Yourself (DRY), keep it clean, and write comments where necessary.</p></section></section>
<section><h2>2. The CMake Language: A Quick Tour<a href="#2-the-cmake-language-a-quick-tour"><span>#</span></a></h2><section><h3>Organization<a href="#organization"><span>#</span></a></h3><p>CMake code can be organized in three ways:</p><ul>
<li><strong>Directories (<code>CMakeLists.txt</code>)</strong>: The entry point for a project or sub-project. <code>add_subdirectory()</code> adds another directory (which must contain a <code>CMakeLists.txt</code>) to the build.</li>
<li><strong>Scripts (<code>&lt;name&gt;.cmake</code>)</strong>: Executed with <code>cmake -P &lt;script&gt;.cmake</code>. They are useful for automation but cannot define build targets like executables or libraries. In other words, <em>not all commands are supported</em>.</li>
<li><strong>Modules (<code>&lt;name&gt;.cmake</code>)</strong>: Reusable code included in your projects via <code>include()</code>. They are located in the <code>CMAKE_MODULE_PATH</code>.</li>
</ul></section><section><h3>Commands<a href="#commands"><span>#</span></a></h3><p>CMake commands are <strong>case-insensitive</strong>, but their arguments (including variable names) are <strong>case-sensitive</strong>.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># command_name(ARGUMENT1 ARGUMENT2 ...)</span></div></div><div><div><div>2</div></div><div><span>project</span><span>(MyProject VERSION 1.0)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div>Caution</div></div><div><p><strong>Command invovations are not expressions</strong>, so you cannnot put a command invocation directly as an argument of another command or inside an if condition. They are not possible.</p></div></div></section><section><h3>Variables<a href="#variables"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Set a variable</span></div></div><div><div><div>2</div></div><div><span>set</span><span>(MY_VARIABLE </span><span>"Hello"</span><span>)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># Reference a variable (dereference)</span></div></div><div><div><div>5</div></div><div><span>message</span><span>(STATUS </span><span>"My variable is: ${MY_VARIABLE}"</span><span>)</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># Unset a variable</span></div></div><div><div><div>8</div></div><div><span>unset</span><span>(MY_VARIABLE)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><strong>IMPORTANT</strong></div></div><div><ul>
<li>In CMake, everything is a string. Lists are just strings separated by semicolons <code>;</code> (e.g., <code>"item1;item2;item3"</code>).</li>
<li><strong>An unset or undefined variable expands to an empty string</strong>. This can be a common source of bugs! Use <code>if(DEFINED VAR_NAME)</code> to check if a variable is set.</li>
<li>CMake variables are not environment variables (unlink <code>Makefile</code>).</li>
</ul></div></div><div><div><div></div><div><strong>WARNING</strong></div></div><div><p>Avoid custom variables in the arguments of project commands.</p><p>In modern CMake, what we need is targets and properties. See them later.</p></div></div></section><section><h3>Comments<a href="#comments"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># This is a single-line comment.</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>#[=[</span></div></div><div><div><div>4</div></div><div><span>This is a multi-line comment.</span></div></div><div><div><div>5</div></div><div><span>It can contain other symbols and even # characters.</span></div></div><div><div><div>6</div></div><div><span>#]=]</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>Generator Expressions: The <code>$&lt;...&gt;</code> Syntax<a href="#generator-expressions-the--syntax"><span>#</span></a></h3><p>Generator expressions, often called “genex,” are a powerful CMake feature that uses the <code>$&lt;...&gt;</code> syntax. They are <strong>not</strong> evaluated when CMake first reads your <code>CMakeLists.txt</code>. Instead, they are written into the native build files (like Makefiles or Visual Studio projects) and are evaluated <strong>during the build process</strong>.</p><p>This delayed evaluation is crucial because it allows you to create build configurations that are aware of things that are only known at build time, such as the specific build type (<code>Debug</code>, <code>Release</code>), the compiler being used, or the language of a source file.</p><div><div><div></div><div><strong>IMPORTANT</strong></div></div><div><p>Think of generator expressions as placeholders that the final build tool (like Make, Ninja, or MSBuild) will fill in with the correct value at the right time. This is much more flexible than using <code>if()</code> statements in CMake, which are only evaluated once when you run <code>cmake</code>.</p></div></div><section><h4>Common Use Cases and Examples<a href="#common-use-cases-and-examples"><span>#</span></a></h4><ol>
<li><strong>Conditional Compilation Definitions (<code>$&lt;CONFIG:...&gt;</code>)</strong></li>
</ol><p>This is the most common use case. You want to define a preprocessor macro differently for <code>Debug</code> and <code>Release</code> builds.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># In Debug mode, VERBOSITY will be 2. In all other modes (e.g., Release), it will be 0.</span></div></div><div><div><div>2</div></div><div><span>target_compile_definitions</span><span>(my_app PRIVATE</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"VERBOSITY=$&lt;IF:$&lt;CONFIG:Debug&gt;,2,0&gt;"</span></div></div><div><div><div>4</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>The <code>$&lt;IF:condition,true_value,false_value&gt;</code> expression is evaluated at build time. If the configuration is <code>Debug</code>, it resolves to <code>VERBOSITY=2</code>; otherwise, it becomes <code>VERBOSITY=0</code>.</p><ol>
<li><strong>Conditional Include Directories (<code>$&lt;BUILD_INTERFACE:...&gt;</code> and <code>$&lt;INSTALL_INTERFACE:...&gt;</code>)</strong></li>
</ol><p>A library often has different include paths when being built inside a project versus when it’s installed on a system.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>target_include_directories</span><span>(my_lib PUBLIC</span></div></div><div><div><div>2</div></div><div><span>    </span><span># When my_lib is built as part of this project, use the source directory.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>$&lt;BUILD_INTERFACE:</span><span>${CMAKE_CURRENT_SOURCE_DIR}</span><span>/include&gt;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span># When an external project uses an installed version of my_lib, the path is just 'include'.</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>$&lt;INSTALL_INTERFACE:include&gt;</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>This ensures your library is usable both during development and after deployment without any changes.</p><ol>
<li><strong>Compiler-Specific Flags (<code>$&lt;CXX_COMPILER_ID:...&gt;</code>)</strong></li>
</ol><p>You can pass specific flags depending on the compiler being used (e.g., GCC, Clang, MSVC).</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Enable strong warnings, but use the correct flag for each compiler.</span></div></div><div><div><div>2</div></div><div><span>set</span><span>(GCC_CLANG_WARNINGS </span><span>"-Wall -Wextra -Wpedantic"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>set</span><span>(MSVC_WARNINGS </span><span>"/W4"</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>target_compile_options</span><span>(my_app PRIVATE</span></div></div><div><div><div>6</div></div><div><span>    </span><span>"$&lt;IF:$&lt;CXX_COMPILER_ID:MSVC&gt;,${MSVC_WARNINGS},${GCC_CLANG_WARNINGS}&gt;"</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li><strong>Language-Specific Standards (<code>$&lt;COMPILE_LANGUAGE:...&gt;</code>)</strong></li>
</ol><p>If your target mixes C and C++ code, you can set standards for each language.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>target_compile_features</span><span>(my_app PRIVATE</span></div></div><div><div><div>2</div></div><div><span>    </span><span># Set C++ standard to 17 for all C++ files.</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>$&lt;COMPILE_LANGUAGE:CXX&gt;:cxx_std_17</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>    </span><span># Set C standard to 11 for all C files.</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>$&lt;COMPILE_LANGUAGE:C&gt;:c_std_11</span></div></div><div><div><div>7</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Generator expressions are a cornerstone of modern, robust, and portable CMake scripts. Mastering them allows you to write cleaner and more powerful <code>CMakeLists.txt</code> files.</p></section></section><section><h3>Custom Commands: <code>function()</code> vs. <code>macro()</code><a href="#custom-commands-function-vs-macro"><span>#</span></a></h3><p>You can create your own commands to reduce code duplication.</p><ul>
<li><strong><code>function()</code></strong>: Creates a new variable scope. To pass results back to the caller, you must use <code>set(... PARENT_SCOPE)</code>.</li>
<li><strong><code>macro()</code></strong>: Does not create a new scope. It performs simple text replacement, much like a C preprocessor macro.</li>
</ul><div><div><div></div><div><strong>TIP</strong></div></div><div><p><strong>Rule of Thumb:</strong></p><ul>
<li>Use <code>function()</code> by default to avoid polluting the caller’s scope with side effects.</li>
<li>Use <code>macro()</code> only when you need to wrap a command that has an output parameter or when you explicitly want side effects in the caller’s scope.</li>
</ul></div></div></section></section>
<section><h2>3. The Core of Modern CMake: Targets and Properties<a href="#3-the-core-of-modern-cmake-targets-and-properties"><span>#</span></a></h2><p>Modern CMake revolves around <strong>targets</strong> and their <strong>properties</strong>. A target can be an executable, a library, or a custom target.</p><div><div><div></div><div><strong>WARNING</strong></div></div><div><p>Avoid directory-level commands like <code>include_directories()</code>, <code>link_libraries()</code>, and <code>add_compile_options()</code>. They use global state and make dependencies hard to reason about. Always prefer the <code>target_*</code> equivalents.</p></div></div><div><div><div></div><div><strong>WARNING</strong></div></div><div><p>Don’t use <code>file(GLOB)</code> in your <code>CMakeLists.txt</code> anymore.</p></div></div><section><h3>Thinking in Targets<a href="#thinking-in-targets"><span>#</span></a></h3><p>Imagine targets as objects in an OOP language.</p><ul>
<li><strong>Constructor</strong>: <code>add_executable()</code>, <code>add_library()</code></li>
<li><strong>Member Functions</strong>: <code>target_sources()</code>, <code>target_include_directories()</code>, <code>target_link_libraries()</code>, etc.</li>
<li><strong>Member Variables</strong>: Properties like <code>VERSION</code>, <code>SOURCES</code>, <code>INTERFACE_INCLUDE_DIRECTORIES</code>.</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Create a library target</span></div></div><div><div><div>2</div></div><div><span>add_library</span><span>(my_lib STATIC my_lib.cpp my_lib.h)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># Add properties to the target</span></div></div><div><div><div>5</div></div><div><span>target_include_directories</span><span>(my_lib PUBLIC include)</span></div></div><div><div><div>6</div></div><div><span>target_compile_features</span><span>(my_lib PUBLIC cxx_std_17)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>Build Specifications vs. Usage Requirements<a href="#build-specifications-vs-usage-requirements"><span>#</span></a></h3><ul>
<li><code>Non-INTERFACE_</code> properties define the build specification of a terget</li>
<li><code>INTERFACE_</code> properties define the usage requirements of a target.</li>
</ul><p>This is the most critical concept in modern CMake. When you link a library, the consumer needs to know its include directories, compile definitions, etc.</p><ul>
<li><strong><code>PRIVATE</code></strong>: The property is only for building this target. It is not passed on to consumers (<code>Non-INTERFACE_</code>).</li>
<li><strong><code>INTERFACE</code></strong>: The property is <em>only</em> for consumers of this target. The target itself doesn’t use it for its own build. This is perfect for header-only libraries (<code>INTERFACE_</code>).</li>
<li><strong><code>PUBLIC</code></strong>: The property is for both the target’s build (<code>PRIVATE</code>) and for its consumers (<code>INTERFACE</code>).</li>
</ul><section><h4>Example<a href="#example"><span>#</span></a></h4><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># A logging library that uses spdlog internally</span></div></div><div><div><div>2</div></div><div><span>add_library</span><span>(my_logger my_logger.cpp)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># my_logger needs the spdlog headers to compile.</span></div></div><div><div><div>5</div></div><div><span># Anyone who uses my_logger does NOT need spdlog headers directly.</span></div></div><div><div><div>6</div></div><div><span>target_include_directories</span><span>(my_logger</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>PRIVATE</span></div></div><div><div><div>8</div></div><div><span>        </span><span>${spdlog_SOURCE_DIR}</span><span>/include</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># Anyone who uses my_logger needs my_logger's own headers.</span></div></div><div><div><div>12</div></div><div><span>target_include_directories</span><span>(my_logger</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>PUBLIC</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>$&lt;BUILD_INTERFACE:</span><span>${CMAKE_CURRENT_SOURCE_DIR}</span><span>/include&gt;</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>$&lt;INSTALL_INTERFACE:include&gt; </span><span># Different path after installation</span></div></div><div><div><div>16</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section><section><h3>Header-Only Libraries<a href="#header-only-libraries"><span>#</span></a></h3><p>For libraries that are headers-only, create an <code>INTERFACE</code> library. It has no sources and only defines usage requirements.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>add_library</span><span>(my_header_lib INTERFACE)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>target_include_directories</span><span>(my_header_lib INTERFACE</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>$&lt;BUILD_INTERFACE:</span><span>${CMAKE_CURRENT_SOURCE_DIR}</span><span>/include&gt;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>$&lt;INSTALL_INTERFACE:include&gt;</span></div></div><div><div><div>6</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><strong>WARNING</strong></div></div><div><p>The library interface may change during installation. Use the <code>BUILD_INTERFACE</code> and <code>INSTALL_INTERFACE</code> generator expressions as filters</p></div></div></section></section>
<section><h2>4. Working with Dependencies<a href="#4-working-with-dependencies"><span>#</span></a></h2><section><h3>Finding Packages with <code>find_package()</code><a href="#finding-packages-with-find_package"><span>#</span></a></h3><p>This is the standard way to find and use external libraries that have been installed on the system or are part of the project.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Find the Boost library, version 1.71 or newer.</span></div></div><div><div><div>2</div></div><div><span># It is REQUIRED; CMake will fail if it's not found.</span></div></div><div><div><div>3</div></div><div><span>find_package</span><span>(Boost 1.71 REQUIRED COMPONENTS </span><span>system</span><span> thread)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># If found, use the imported target provided by Boost</span></div></div><div><div><div>6</div></div><div><span>if</span><span>(Boost_FOUND)</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>target_link_libraries</span><span>(my_app PRIVATE Boost::</span><span>system</span><span> Boost::thread)</span></div></div><div><div><div>8</div></div><div><span>endif</span><span>()</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><strong>Note</strong></div></div><div><p>Regardless of the mode/package used, a <code>&lt;PackageName&gt;_FOUND</code> variable will be set to indicate whether the package was found.</p></div></div><div><div><div></div><div><strong>IMPORTANT</strong></div></div><div><p>Always use the official, namespaced, imported targets (e.g., <code>Boost::system</code>, <code>Qt5::Core</code>, <code>GTest::GTest</code>). Never use the old-style <code>_LIBRARIES</code> and <code>_INCLUDE_DIRS</code> variables. Imported targets handle all dependency properties for you automatically.</p></div></div><div><div><div></div><div><strong>IMPORTANT</strong></div></div><div><p>Use a Find module for third party libraries that are not built with CMake that don’t support clients to use CMake. Also, report this as a bug to their authors.</p></div></div><p>If you need to write a find module for a third-party library, report this as a bug to the authors. Because most people use CMake, it’s a problem that don’t support it.</p></section><section><h3>Fetching Dependencies at Configure Time with <code>FetchContent</code><a href="#fetching-dependencies-at-configure-time-with-fetchcontent"><span>#</span></a></h3><p>For dependencies that you want to download and build alongside your project, <code>FetchContent</code> is the modern, preferred approach. It’s great for ensuring all developers use the exact same version of a dependency.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include</span><span>(FetchContent)</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span># Declare the dependency</span></div></div><div><div><div>4</div></div><div><span>FetchContent_Declare(</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>googletest</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>GIT_REPOSITORY https://github.com/google/googletest.git</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>GIT_TAG        release-1.12.1</span></div></div><div><div><div>8</div></div><div><span>)</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span># Make it available (downloads and adds it as a sub-project)</span></div></div><div><div><div>11</div></div><div><span>FetchContent_MakeAvailable(googletest)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span># Now you can link to it just like any other target in your project</span></div></div><div><div><div>14</div></div><div><span>target_link_libraries</span><span>(my_tests PRIVATE GTest::GTest GTest::Main)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><strong>Note</strong></div></div><div><p><code>FetchContent</code> is generally preferred over Git submodules because it gives the parent project more control and is easier to manage.</p></div></div></section><section><h3>Exporting Your Project as a Package<a href="#exporting-your-project-as-a-package"><span>#</span></a></h3><p>When you want other projects to use your library with <code>find_package()</code>, you need to generate package configuration files.</p><ol>
<li><strong>Install Targets</strong>: Use <code>install(TARGETS ...)</code> to specify where your library files (<code>.a</code>, <code>.so</code>, <code>.dll</code>) and headers should be installed. Use the <code>EXPORT</code> keyword to associate them with a target export set.</li>
<li><strong>Install Export Set</strong>: Use <code>install(EXPORT ...)</code> to create a <code>&lt;name&gt;Targets.cmake</code> file. This file contains the definitions of your imported targets (e.g., <code>MyLib::MyLib</code>).</li>
<li><strong>Create Version and Config Files</strong>: Use <code>CMakePackageConfigHelpers</code> to generate a version file (<code>&lt;name&gt;ConfigVersion.cmake</code>) and write a config file (<code>&lt;name&gt;Config.cmake</code>). The config file is the entry point that <code>find_package</code> looks for.</li>
</ol><p>This process tells other projects how to use your library by defining an imported target like <code>YourProject::YourLib</code>.</p><p>For example:</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># standard steps to build the library</span></div></div><div><div><div>2</div></div><div><span>find_package</span><span>(Bar 2.0 REQUIRED)</span></div></div><div><div><div>3</div></div><div><span>add_library</span><span>(Foo ...)</span></div></div><div><div><div>4</div></div><div><span>target_link_libraries</span><span>(Foo PRIVATE Bar::Bar)</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span># 1. Install targets</span></div></div><div><div><div>7</div></div><div><span>install</span><span>(TARGETS Foo EXPORT FooTargets</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>LIBRARY DESTINATION lib</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>ARCHIVE DESTINATION lib</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>RUNTIME DESTINATION bin</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>INCLUDES DESTINATION include)</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span># 2. Install Export Set</span></div></div><div><div><div>14</div></div><div><span>install</span><span>(EXPORT FooTargets</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>FILE</span><span> FooTargets.cmake</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>NAMESPACE Foo::</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>DESTINATION lib/cmake/Foo)</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span># 3. Create Version file</span></div></div><div><div><div>20</div></div><div><span>include</span><span>(CMakePackageConfigHelpers)</span></div></div><div><div><div>21</div></div><div><span>write_basic_package_version_file(</span><span>"FooConfigVersion.cmake"</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>VERSION </span><span>${Foo_VERSION}</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>COMPATIBILITY SameMajorVersion</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>)</span></div></div><div><div><div>25</div></div><div><span>install</span><span>(FILES </span><span>"FooConfig.cmake"</span><span> </span><span>"FooConfigVersion.cmake"</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>DESTINATION lib/cmake/Foo)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>Create and write config files <code>FooConfig.cmake</code>:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>include</span><span>(CMakeFindDependencyMacro)</span></div></div><div><div><div>2</div></div><div><span>find_dependency(Bar 2.0)</span></div></div><div><div><div>3</div></div><div><span>include</span><span>(</span><span>"$${CMAKE_CURRENT_LIST_DIR}/FooTargets.cmake"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>5. Testing with CTest<a href="#5-testing-with-ctest"><span>#</span></a></h2><p>CTest is CMake’s testing framework. It acts as a test driver, running test executables and reporting results.</p><section><h3>Basic Setup<a href="#basic-setup"><span>#</span></a></h3><ol>
<li><strong>Enable Testing</strong>: Add <code>enable_testing()</code> to your root <code>CMakeLists.txt</code>. This command should be called only once in the top-level project.</li>
<li><strong>Add Tests</strong>: Use <code>add_test()</code> to define a test case. A test is typically an executable that returns 0 for success and non-zero for failure.</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># In your tests/CMakeLists.txt</span></div></div><div><div><div>2</div></div><div><span>add_executable</span><span>(run_all_tests tests.cpp)</span></div></div><div><div><div>3</div></div><div><span>target_link_libraries</span><span>(run_all_tests PRIVATE my_lib GTest::GTest GTest::Main)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># Define a test named "MyLib.UnitTests" that runs the "run_all_tests" executable.</span></div></div><div><div><div>6</div></div><div><span>add_test</span><span>(</span><span>NAME</span><span> MyLib.UnitTests </span><span>COMMAND</span><span> run_all_tests)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><strong>TIP</strong></div></div><div><p>It’s a good practice to adopt a naming convention for your tests, like <code>Project.Component.TestType</code>. This makes filtering much easier.</p></div></div><section><h4>Running Tests<a href="#running-tests"><span>#</span></a></h4><p>You can then run all tests from your build directory:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># Run all tests, with 4 parallel jobs and verbose output on failure</span></div></div><div><div><div>2</div></div><div><span>ctest</span><span> </span><span>-j4</span><span> </span><span>--output-on-failure</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>Advanced: Filtering Tests<a href="#advanced-filtering-tests"><span>#</span></a></h4><p>You can run a subset of your tests using a regular expression with the <code>-R</code> flag.</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># Run only the tests whose names start with "MyLib."</span></div></div><div><div><div>2</div></div><div><span>ctest</span><span> </span><span>-R</span><span> </span><span>"^MyLib</span><span>\\</span><span>."</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>Advanced: Testing for Compile Failure<a href="#advanced-testing-for-compile-failure"><span>#</span></a></h4><p>Sometimes, you want to ensure that certain code <em>fails</em> to compile (e.g., when testing <code>static_assert</code>). You can create a test for this.</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Create a library that is expected to fail compilation.</span></div></div><div><div><div>2</div></div><div><span># EXCLUDE_FROM_ALL prevents it from being built during a normal build.</span></div></div><div><div><div>3</div></div><div><span>add_library</span><span>(foo_fail STATIC EXCLUDE_FROM_ALL foo_fail.cpp)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span># Add a test that tries to build this specific target.</span></div></div><div><div><div>6</div></div><div><span># The test "passes" if the build command fails.</span></div></div><div><div><div>7</div></div><div><span>add_test</span><span>(</span><span>NAME</span><span> Foo.CompileFail</span></div></div><div><div><div>8</div></div><div><span>    </span><span>COMMAND</span><span> </span><span>${CMAKE_COMMAND}</span><span> --build </span><span>${CMAKE_BINARY_DIR}</span><span> --</span><span>target</span><span> foo_fail</span></div></div><div><div><div>9</div></div><div><span>)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># We can even check for a specific error message in the build output.</span></div></div><div><div><div>12</div></div><div><span># The test passes if the build fails AND the output contains this regex.</span></div></div><div><div><div>13</div></div><div><span>set_tests_properties</span><span>(Foo.CompileFail PROPERTIES</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>PASS_REGULAR_EXPRESSION </span><span>"static assert message"</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>WILL_FAIL </span><span>TRUE</span><span> </span><span># Informs CTest that a non-zero return code is expected (success)</span></div></div><div><div><div>16</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h4>Advanced: Driving CTest with a Script<a href="#advanced-driving-ctest-with-a-script"><span>#</span></a></h4><p>For complex testing scenarios, especially in CI/CD environments, you can use a CMake script to drive CTest.</p><p><strong><code>build_and_test.cmake</code></strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># This script automates the configure, build, test, and submit steps.</span></div></div><div><div><div>2</div></div><div><span>set</span><span>(CTEST_SOURCE_DIRECTORY </span><span>"/path/to/source"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>set</span><span>(CTEST_BINARY_DIRECTORY </span><span>"/path/to/build"</span><span>)</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>set</span><span>(CTEST_CMAKE_GENERATOR </span><span>"Ninja"</span><span>) </span><span># Specify the generator</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>ctest_start</span><span>(</span><span>"Continuous"</span><span>) </span><span># Start a new CTest run</span></div></div><div><div><div>8</div></div><div><span>ctest_configure</span><span>()         </span><span># Run cmake</span></div></div><div><div><div>9</div></div><div><span>ctest_build</span><span>()             </span><span># Run cmake --build</span></div></div><div><div><div>10</div></div><div><span>ctest_test</span><span>()              </span><span># Run ctest</span></div></div><div><div><div>11</div></div><div><span>ctest_submit</span><span>()            </span><span># Submit results to a dashboard like CDash</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>You would run this script from the command line:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ctest</span><span> </span><span>-S</span><span> </span><span>build_and_test.cmake</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>This keeps CI-specific logic out of your main <code>CMakeLists.txt</code>.</p></section></section></section>
<section><h2>6. Cross-Compiling with Toolchain Files<a href="#6-cross-compiling-with-toolchain-files"><span>#</span></a></h2><p>Cross-compiling is the process of building code on one machine (the <em>host</em>) that is intended to run on a different machine (the <em>target</em>), which may have a different architecture (e.g., building for ARM on an x86-64 host).</p><p>CMake handles this through <strong>Toolchain Files</strong>. A toolchain file tells CMake how to find the correct compilers, linkers, and libraries for the target system.</p><section><h3>The Role of the Toolchain File<a href="#the-role-of-the-toolchain-file"><span>#</span></a></h3><p>You specify the toolchain file when you first configure your project with <code>cmake</code>.</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># Configure the project using the my-arm-toolchain.cmake file</span></div></div><div><div><div>2</div></div><div><span>cmake</span><span> </span><span>-S</span><span> </span><span>.</span><span> </span><span>-B</span><span> </span><span>build</span><span> </span><span>-DCMAKE_TOOLCHAIN_FILE=/path/to/my-arm-toolchain.cmake</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><strong>WARNING</strong></div></div><div><p>A toolchain file is for describing the <strong>environment</strong>, not for project logic. Avoid setting project options or variables in it. Its only job is to set up the compilers and search paths.</p></div></div><section><h4>Example Toolchain File<a href="#example-toolchain-file"><span>#</span></a></h4><p>Here is an example of a toolchain file for cross-compiling to Windows from a Linux host using MinGW-w64.</p><p><strong><code>mingw-toolchain.cmake</code></strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># The name of the target operating system</span></div></div><div><div><div>2</div></div><div><span>set</span><span>(</span><span>CMAKE_SYSTEM_NAME</span><span> Windows)</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># Specify the cross-compilers</span></div></div><div><div><div>5</div></div><div><span>set</span><span>(CMAKE_C_COMPILER   x86_64-w64-mingw32-gcc)</span></div></div><div><div><div>6</div></div><div><span>set</span><span>(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># Where to find the target environment's headers and libraries</span></div></div><div><div><div>9</div></div><div><span>set</span><span>(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># Adjust the search behavior for programs, libraries, and includes</span></div></div><div><div><div>12</div></div><div><span>set</span><span>(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)</span></div></div><div><div><div>13</div></div><div><span>set</span><span>(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)</span></div></div><div><div><div>14</div></div><div><span>set</span><span>(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span># Set an emulator to run target executables on the host system</span></div></div><div><div><div>17</div></div><div><span>set</span><span>(CMAKE_CROSSCOMPILING_EMULATOR </span><span>"wine64"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h4>Running Cross-Compiled Tests<a href="#running-cross-compiled-tests"><span>#</span></a></h4><p>You can’t directly run a target executable on the host machine. The <code>CMAKE_CROSSCOMPILING_EMULATOR</code> variable (set in the toolchain file) tells CTest how to run the tests. It specifies a program (like <code>wine</code> for Windows apps on Linux, or <code>qemu-arm</code> for ARM binaries) that can emulate the target environment.</p><p>When you run <code>ctest</code>, it will automatically prefix test commands with the emulator:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># CTest will effectively run:</span></div></div><div><div><div>2</div></div><div><span>wine64</span><span> </span><span>my_test_executable.exe</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>This allows you to run your unit tests on the host machine even when cross-compiling.</p></section></section></section>
<section><h2>7. Packaging with CPack<a href="#7-packaging-with-cpack"><span>#</span></a></h2><p>CPack is CMake’s packaging tool. It can create installers (NSIS, WiX), archives (<code>.zip</code>, <code>.tar.gz</code>), and Linux packages (<code>.deb</code>, <code>.rpm</code>).</p><ol>
<li><strong>Include CPack</strong>: Add <code>include(CPack)</code> to your root <code>CMakeLists.txt</code>.</li>
<li><strong>Set CPack Variables</strong>: Configure package metadata by setting <code>CPACK_*</code> variables.</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>set</span><span>(CPACK_PACKAGE_NAME </span><span>"MyAwesomeApp"</span><span>)</span></div></div><div><div><div>2</div></div><div><span>set</span><span>(CPACK_PACKAGE_VERSION </span><span>"1.0.0"</span><span>)</span></div></div><div><div><div>3</div></div><div><span>set</span><span>(CPACK_GENERATOR </span><span>"ZIP;TGZ"</span><span>) </span><span># Create a .zip and a .tar.gz</span></div></div><div><div><div>4</div></div><div><span># ... other variables</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Run CPack from your build directory to create the packages:</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cpack</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>8. Static Analysis Integration<a href="#8-static-analysis-integration"><span>#</span></a></h2><p>Integrating static analysis into the build process is a powerful way to enforce code quality and catch bugs early.</p><section><h3>The Philosophy of Handling Warnings<a href="#the-philosophy-of-handling-warnings"><span>#</span></a></h3><p>A common piece of advice is to “treat warnings as errors,” typically by passing the <code>-Werror</code> flag to the compiler. However, this approach can be counterproductive and painful.</p><p><strong>Why <code>-Werror</code> Causes Pain:</strong></p><ul>
<li><strong>Blocks Progress</strong>: You cannot enable <code>-Werror</code> until you have fixed every single existing warning in the codebase.</li>
<li><strong>Hinders Upgrades</strong>: You cannot upgrade your compiler or increase the warning level (e.g., from <code>-Wall</code> to <code>-Wextra</code>) until you have fixed all the new warings that appear. This creates a significant barrier to modernization.</li>
<li><strong>Makes Deprecation Difficult</strong>: You cannot mark your own internal APIs as <code>[[deprecated]]</code> as long as they are still in use, because this would generate a warning and fail the build. But once they are no longer used, you might as well just remove them.</li>
</ul></section><section><h3>A Better Approach: Treat <em>New</em> Warnings as Errors<a href="#a-better-approach-treat-new-warnings-as-errors"><span>#</span></a></h3><p>A more practical and agile strategy is to manage warnings in development cycles:</p><ol>
<li><strong>Allow New Warnings Temporarily</strong>: At the beginning of a development cycle (e.g., a new sprint), allow new warnings to be introduced. This is the time to upgrade compilers, update dependencies, or enable more aggressive warning flags.</li>
<li><strong>Burn Down Warnings</strong>: During the cycle, the team’s goal is to fix all existing warnings and drive the count back down to zero.</li>
<li><strong>Repeat</strong>: This iterative process allows for continuous improvement without bringing development to a halt.</li>
</ol></section><section><h3>Pull Out All the Stops: Powerful Analysis Tools<a href="#pull-out-all-the-stops-powerful-analysis-tools"><span>#</span></a></h3><p>Modern C++ has a rich ecosystem of static analysis tools that go far beyond compiler warnings. You should integrate them into your build.</p><ul>
<li><strong>clang-tidy</strong>: A Clang-based linter framework to diagnose and fix typical programming errors, style violations, and interface misuse.
<strong>cpplint</strong>: An automated checker to ensure code adheres to Google’s C++ style guide.</li>
<li><strong>include-what-you-use (IWYU)</strong>: A tool for analyzing <code>#include</code>s to ensure you include exactly what you use, which can improve compile times and code clarity.</li>
<li><strong>clazy</strong>: A Clang plugin that finds Qt-specific antipatterns and performance issues.</li>
</ul></section><section><h3>Modern CMake Integration via Target Properties<a href="#modern-cmake-integration-via-target-properties"><span>#</span></a></h3><p>Modern CMake provides dedicated target properties to run these tools alongside the compiler.</p><ul>
<li><code>&lt;LANG&gt;_CLANG_TIDY</code></li>
<li><code>&lt;LANG&gt;_CPPLINT</code></li>
<li><code>&lt;LANG&gt;_INCLUDE_WHAT_YOU_USE</code></li>
<li><code>LINK_WHAT_YOU_USE</code> (Checks for unused library links)</li>
</ul><p>Where <code>&lt;LANG&gt;</code> is <code>C</code> or <code>CXX</code>. These properties are initialized by their corresponding <code>CMAKE_...</code> variables.</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># Example of enabling clang-tidy and include-what-you-use</span></div></div><div><div><div>2</div></div><div><span>set_target_properties</span><span>(my_app PROPERTIES</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>CXX_CLANG_TIDY </span><span>"clang-tidy;-checks=-*,readability-*,modernize-*"</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>CXX_INCLUDE_WHAT_YOU_USE </span><span>"include-what-you-use;-Xiwyu;--mapping_file=/iwyu.imp"</span></div></div><div><div><div>5</div></div><div><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div></div><div><strong>IMPORTANT</strong></div></div><div><p>The major advantage of this approach is that diagnostics from these tools are seamlessly integrated into your build output and appear directly in your IDE, just like regular compiler errors and warnings.</p></div></div></section><section><h3>Best Practice: Analyzing Header Files<a href="#best-practice-analyzing-header-files"><span>#</span></a></h3><p>A common pitfall is that header files without an associated source file (.cpp) will not be analyzed.</p><div><div><div></div><div><strong>TIP</strong></div></div><div><p>For each header file, ensure there is an associated source file that <code>#include</code> it, preferably as the very first line. This source file can even be empty otherwise.</p></div></div><p>You can use a simple script to create these missing source files:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env bash</span></div></div><div><div><div>2</div></div><div><span># Create an empty .cpp file for every .h file that doesn't have one.</span></div></div><div><div><div>3</div></div><div><span>for</span><span> </span><span>fn</span><span> </span><span>in</span><span> </span><span>`</span><span>comm</span><span> </span><span>-23</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>&lt;(</span><span>ls</span><span> </span><span>*</span><span>.h </span><span>|</span><span> </span><span>cut</span><span> </span><span>-d</span><span> '.' </span><span>-f</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>sort</span><span>) </span><span>\</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>&lt;(</span><span>ls</span><span> </span><span>*</span><span>.c </span><span>*</span><span>.cpp </span><span>|</span><span> </span><span>cut</span><span> </span><span>-d</span><span> '.' </span><span>-f</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>sort</span><span>)`</span></div></div><div><div><div>6</div></div><div><span>do</span></div></div><div><div><div>7</div></div><div><span>    </span><span>echo</span><span> </span><span>"#include </span><span>\"</span><span>$fn</span><span>.h</span><span>\"</span><span>"</span><span> &gt;&gt; </span><span>$fn</span><span>.cpp</span></div></div><div><div><div>8</div></div><div><span>done</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Enabling Analysis from Outside the Project</p><p>To keep your <code>CMakeLists.txt</code> clean, you can enable and configure these tools from the command line during the configure step. This is ideal for CI/CD pipelines.</p></section></section>
<section><h2>Reference<a href="#reference"><span>#</span></a></h2><ul>
<li><a href="https://www.youtube.com/watch?v=bsXLMQ6WgIk" target="_blank">Effective CMake: a random selectino of best practices</a></li>
<li><a href="https://cmake.org/documentation/" target="_blank">CMake Documentation and Community</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/visual-studio-project-management/</id>
      <title type="text">Visual Studio C++ 工程实践指南</title>
      <published>2025-07-29T17:50:00.000Z</published>
      <updated>2025-07-29T17:50:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/visual-studio-project-management/"/>
      <summary type="text">详解 Visual Studio 解决方案与项目的组织结构、过滤器与路径配置技巧、常用宏的使用，以及静态库与动态库的引用方法，并介绍了 CMake 的集成支持，帮助开发者构建清晰规范的 C++。</summary>
      <content type="html"><![CDATA[<section><h2>解决方案（Solution）与项目（Project）<a href="#解决方案solution与项目project"><span>#</span></a></h2><p>一个<strong>解决方案</strong>是用于组织和管理一个或多个相关联<strong>项目</strong>的容器。这套结构旨在将一个复杂的实际问题，分解成多个功能独立的<strong>模块</strong>（<strong>项目</strong>）来开发和管理。</p><ul>
<li><strong>解决方案</strong>（<code>.sln</code> 文件）：这是最高层级的组织单位，它定义了所包含的项目以及它们之间的依赖关系和构建配置。当你打开一个 <code>.sln</code> 文件时，Visual Studio 会加载整个工作区。</li>
<li><strong>项目</strong>（<code>.vcxproj</code>、<code>csproj</code> 等文件）：每个项目对应一个独立的模块，最终会生成一个可执行文件（<code>.exe</code>）、一个静态库（<code>.lib</code>）或一个动态链接库（<code>.dll</code>）。</li>
</ul><p>需要注意的地方：</p><ul>
<li><strong>单一启动项目</strong>：在一个解决方案中，通常只有一个项目被设置为<strong>启动项目</strong>。<strong>启动项目</strong>是点击 “开始调试”（<kbd>F5</kbd>）时，Visual Studio 将会编译和运行的项目。可以在<strong>解决方案资源管理器</strong>中右键点击不同的项目，选择 “<strong>设为启动项目</strong>”。</li>
<li>“<strong>将解决方案和项目放在同一目录中</strong>”：
<ul>
<li>勾选：解决方案文件（<code>.sln</code>）和项目文件（例如 <code>.vcxproj</code>）会存放在同一个文件夹中。这对于只有一个或少数几个项目的简单解决方案来说，目录结构更扁平、更清晰。</li>
<li>不勾选（默认）：Visual Studio 会创建一个顶层文件夹来存放解决方案文件（<code>.sln</code>），然后在该文件夹下为每一个项目再创建一个独立的子文件夹。当解决方案包含多个项目时，这种结构更加有条理，是<strong>更推荐的做法</strong>。</li>
</ul>
</li>
</ul></section>
<section><h2>过滤器（Filter）<a href="#过滤器filter"><span>#</span></a></h2><p><strong>过滤器</strong>（<em>Filter</em>）是 Visual Studio 解决方案资源管理器中的一个<strong>虚拟文件夹结构</strong>。它的主要作用是帮助开发者组织和分类源文件、头文件等，使项目看起来更整洁，所有的代码文件（源文件和头文件）都应该加入过滤器中。开发者可以自由地创建、重命名或删除过滤器，这<strong>不会</strong>影响磁盘上文件的实际位置。</p><p>而对于编译器（例如 MSVC、GCC 等）在编译代码时，只关心真实的目录结构，而非<strong>过滤器</strong>中的配置。所以当编译器报错 “无法打开源文件” 或 “找不到头文件” 之类的错误时，问题几乎总是出在<strong>项目属性</strong>的路径配置上，而不是解决方案资源管理器的过滤器结构。</p><section><h3>如何正确配置包含目录？<a href="#如何正确配置包含目录"><span>#</span></a></h3><p>当项目需要引入外部头文件（例如来自另一个项目货第三方库）时，必须明确告知编译器去哪里查找这些文件。</p><ol>
<li>在<strong>解决方案资源管理器</strong>中右键点击项目，选择<strong>属性</strong>。</li>
<li>确保顶部的<strong>配置</strong>和<strong>平台</strong>设置正确。</li>
<li>导航到 <strong>C/C++</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>常规</strong>。</li>
<li>在<strong>附加包含目录</strong>字段中，添加所需头文件所在的<strong>真实目录路径</strong>。</li>
</ol><p>在 Visual Studio 中选择的目录会被设置为<strong>绝对路径</strong>，当<strong>解决方案</strong>发生移动，或者发送给合作者时，就会导致错误。这可以通过 Visual Studio 中提供的<strong>宏</strong>来解决。</p></section></section>
<section><h2>宏（Macro）<a href="#宏macro"><span>#</span></a></h2><p>Visual Studio 提供了一系列预定义的<strong>宏</strong>（<em>Macro</em>），这些宏会在构建时被动态地解析为具体的路径。</p><p>常用宏变量：</p>

<table><thead><tr><th>宏</th><th>描述</th></tr></thead><tbody><tr><td><code>$(SolutionDir)</code></td><td>解决方案文件（<code>.sln</code>）所在的目录</td></tr><tr><td><code>$(ProjectDir)</code></td><td>当前项目文件（<code>.vcxproj</code>）所在的目录</td></tr><tr><td><code>$(OutDir)</code></td><td>最终输出文件（如 <code>.exe</code>、<code>.dll</code>）的存放目录</td></tr><tr><td><code>$(IntDir)</code></td><td>编译过程中产生的中间文件（如 <code>.obj</code>）的存放目录</td></tr><tr><td><code>$(Configuration)</code></td><td>当前的构建配置名称，如 <code>Debug</code> 或 <code>Release</code></td></tr><tr><td><code>$(Platform)</code></td><td>当前的构建平台名称，如 <code>x64</code> 或 <code>Win32</code></td></tr></tbody></table><section><h3>示例<a href="#示例"><span>#</span></a></h3><p>假设解决方案目录结构如下：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>MySolution/</span></div></div><div><div><div>2</div></div><div><span>├──</span><span> </span><span>MySolution.sln</span></div></div><div><div><div>3</div></div><div><span>├──</span><span> </span><span>ProjectA/</span></div></div><div><div><div>4</div></div><div><span>│</span><span>   </span><span>└──</span><span> </span><span>ProjectA.vcxproj</span></div></div><div><div><div>5</div></div><div><span>└──</span><span> </span><span>Libraries/</span></div></div><div><div><div>6</div></div><div><span>    </span><span>└──</span><span> </span><span>MyLib/</span></div></div><div><div><div>7</div></div><div><span>        </span><span>└──</span><span> </span><span>include/</span></div></div><div><div><div>8</div></div><div><span>            </span><span>└──</span><span> </span><span>my_header.h</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果 <code>ProjectA</code> 需要包含 <code>my_header.h</code>，则可以在 <code>ProjectA</code> 的 “附加包含目录” 中设置为：<code>$(SolutionDir)\Libraries\MyLib\include</code>。</p><p>这样一来，即使将整个 <code>MySolution</code> 文件夹移动到其他位置，这个相对路径依然有效。</p></section></section>
<section><h2>库的使用<a href="#库的使用"><span>#</span></a></h2><section><h3>静态库（Static Library，<code>.lib</code>）<a href="#静态库static-librarylib"><span>#</span></a></h3><p>静态库是在<strong>链接阶段</strong>被并入到目标程序中的。最终生成的可执行文件会包含静态库的所有代码，因此文件体积较大，但运行时不依赖外部文件。</p><section><h4>创建<a href="#创建"><span>#</span></a></h4><p>在解决方案中右键点击 “解决方案” <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 添加 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 新建项目 <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> 静态库</p></section><section><h4>使用<a href="#使用"><span>#</span></a></h4><section><h5>引用<a href="#引用"><span>#</span></a></h5><ul>
<li>在需要使用此库的项目上，右键点击<strong>引用</strong></li>
<li>选择<strong>添加引用</strong>，然后在弹出的对话框中勾选所需的静态库项目</li>
</ul><p>这是推荐的方式，原因如下：</p><ul>
<li>它会自动设置<strong>链接器</strong>的输入，确保链接器能够找到库文件</li>
<li>它会自动创建<strong>项目依赖项</strong>，保证在编译主项目之前，静态库项目总是被优先编译</li>
<li>它还会自动将静态库的头文件目录添加到主项目的<strong>附加包含目录</strong>中，让开发者能够直接 <code>#include</code></li>
</ul></section><section><h5>手动配置（不推荐）<a href="#手动配置不推荐"><span>#</span></a></h5><p>如果引用的库是一个已经编译好、非解决方案内的 <code>.lib</code> 文件，则需要手动配置：</p><ol>
<li><strong>附加库目录</strong>：在项目属性的<strong>链接器</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>常规</strong>中，添加 <code>.lib</code> 文件所在的目录。</li>
<li><strong>附加依赖项</strong>：在项目属性的<strong>链接器</strong> <span><span>→\rightarrow</span><span><span><span></span><span>→</span></span></span></span> <strong>输入</strong>中，添加 <code>.lib</code> 文件的具体名称（例如 <code>libtest.lib</code>）</li>
</ol></section></section></section><section><h3>动态链接库（Dynamic Link Library，<code>.dll</code>）<a href="#动态链接库dynamic-link-librarydll"><span>#</span></a></h3><p>动态库是程序<strong>运行时</strong>由操作系统加载到内存中的。多个程序可以共享同一个 DLL，从而节省磁盘空间和内存。项目生成时会同时产生一个 <code>.dll</code> 文件（运行时需要）和一个 <code>.lib</code> 文件（链接时需要）。</p><p>注意事项：</p><ul>
<li>导出与导入：从 DLL 中暴露函数或类给其他项目使用，需要使用 <code>__declspec(dllexport)</code> 关键字。而在使用该 DLL 的项目中，则需要使用 <code>__declspc(dllimport)</code> 来声明这些函数或类。</li>
<li>运行时环境：当运行依赖于 DLL 的程序时，操作系统必须能够找到这个 <code>.dll</code> 文件。通常由以下几种方式：
<ol>
<li>将 <code>.dll</code> 文件放置在与可执行文件（<code>.exe</code>）相同的目录下</li>
<li>将 <code>.dll</code> 文件所在的目录添加到系统的 <code>PATH</code> 环境变量中。</li>
<li>在 Visual Studio 中进行调试时，它通常会自动处理好路径问题。</li>
</ol>
</li>
</ul></section></section>
<section><h2>使用 CMake 自动生成解决方案<a href="#使用-cmake-自动生成解决方案"><span>#</span></a></h2><p>对于跨平台项目或大型复杂项目，直接使用 Visual Studio 的项目文件进行管理可能会变得非常繁琐。CMake 是一个强大的、开源的构建系统生成工具，它可以根据简单的配置文件（<code>CMakeLists.txt</code>）自动生成适用与多种平台和 IDE 的原生构建环境，包括 Visual Studio 的 <code>.sln</code> 和 <code>.vcxproj</code> 文件。</p><section><h3>手动配置<a href="#手动配置"><span>#</span></a></h3><ol>
<li>为项目的根目录以及各个子模块（库、可执行文件）编写 <code>CMakeLists.txt</code> 文件。</li>
<li>创建一个独立的构建目录（通常命名为 <code>build</code>），以保持源码树的干净。</li>
<li>在 <code>build</code> 目录中运行 CMake，指令通常为 <code>cmake ..</code>，它会查找上一级目录的 <code>CMakeLists.txt</code> 并生成构建文件。</li>
<li>最后，用 Visual Studio 打开在 <code>build</code> 中生成的 <code>.sln</code> 文件。</li>
</ol></section><section><h3>原生支持<a href="#原生支持"><span>#</span></a></h3><p>从 Visual Studio 2017 开始，Visual Studio 提供了对 CMake 的原生集成支持。</p><p>可以直接用 Visual Studio 打开一个包含 <code>CMakeLists.txt</code> 的文件节哀，而无需手动运行 <code>cmake</code> 命令和打开生成的 <code>.sln</code> 文件。Visual Studio 会自动在后台完成这一切，并提供完整的智能提示、构建和调试支持。</p><p>这对于使用 CMake 的项目来说，是目前最便捷的开发方式。</p></section></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ol>
<li><a href="https://www.bilibili.com/video/BV1S541137P2?spm_id_from=333.788.videopod.episodes&amp;vd_source=aece08350e40b390e044f67307ffd650" target="_blank">工程课代表——Visual Studio 工程实践开发</a></li>
</ol></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/vmware-keyboard-lag-fix/</id>
      <title type="text">VMware Workstation 17.x 中的虚拟机按键反应迟钝解决办法</title>
      <published>2025-05-23T14:34:00.000Z</published>
      <updated>2025-05-23T14:34:00.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/vmware-keyboard-lag-fix/"/>
      <summary type="text">本文介绍了如何解决 VMware Workstation 17.x 中虚拟机（如 Ubuntu）按键反应迟钝、跟手性差的问题。通过在 .vmx 配置文件中添加 keyboard.vusb.enable = &quot;TRUE&quot; 参数，可以有效改善输入延迟，无需降低 CPU 核心数或关闭 3D 图形加速。</summary>
      <content type="html"><![CDATA[<section><h2>问题描述<a href="#问题描述"><span>#</span></a></h2><p>在 VMware Workstation 17.x 中的虚拟机按键反映迟钝，具体而言即是屏幕显示的字符速度跟不上手打字的速度，你能很明显得感觉到迟钝。</p><p>特别是长按方向键选择时，长按一两秒，光标会移动好几秒，跟手性非常差，非常难受。</p><p>笔者这里所说的“<strong>跟手性</strong>”是指：当用户开始输入时，屏幕就开始刷新字符，刷新字符的速度与用户打字的速度不能差太多，并且当用户停止输入时，屏幕能立即停止刷新字符。</p></section>
<section><h2>笔者环境<a href="#笔者环境"><span>#</span></a></h2><ul>
<li>VMware Workstatio 17 Pro：17.6.3 build-24583834</li>
<li>虚拟机：Ubuntu 16.04</li>
<li>操作系统：Windows 11 专业版 26100.4061</li>
</ul></section>
<section><h2>解决方法<a href="#解决方法"><span>#</span></a></h2><p>请按照以下步骤操作，修改虚拟机的配置文件：</p><ol>
<li><strong>关闭虚拟机</strong>：确保虚拟机处于完全关闭状态（Power Off），而非挂起（Suspend）。</li>
<li><strong>找到配置文件</strong>：进入虚拟机所在的文件夹，找到后缀为 <code>.vmx</code> 的配置文件（通常与虚拟机同名）。</li>
<li><strong>编辑文件</strong>：右键点击该文件，选择使用记事本（Notepad）或 VS Code 打开。</li>
<li><strong>添加配置</strong>：在文件的末尾添加以下代码：</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>keyboard.vusb.enable</span><span><span> </span><span>=</span><span> </span></span><span>"TRUE"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li><strong>保存并重启</strong>：保存文件后，重新启动虚拟机即可生效。</li>
</ol><blockquote><p><strong>原理说明</strong>：该配置强制启用了虚拟 USB 键盘设备，绕过了某些导致延迟的输入仿真层。</p></blockquote></section>
<section><h2>测试环境<a href="#测试环境"><span>#</span></a></h2><ul>
<li>Ubuntu 16.04</li>
<li>Ubuntu 18.04</li>
<li>Ubuntu 22.04</li>
<li>ElementaryOS 7 Horus</li>
</ul><p>在以上虚拟机中，笔者对此方法进行了测试，都取得了很好的效果。</p></section>
<section><h2>FAQ<a href="#faq"><span>#</span></a></h2><section><h3>是否需要降低核心数<a href="#是否需要降低核心数"><span>#</span></a></h3><p>网上有很多其他的教程说可以降低 CPU 的核心数，具体原理是减少虚拟机等待的时间。</p><p>笔者实测，降低 CPU 核心数确实能带来一定速度的提升，但是效果很微弱。</p><p>但是当我在 <code>*.vmx</code> 文件中添加了 <code>keyboard.vusb.enable = "TRUE"</code> 后，我将虚拟机的核心数提高到了 16 个，在虚拟机中打字时的跟手性也非常好，只是整个虚拟机有时候会有点卡顿，完全在可接受的范围内。</p><p>不过一般来说，核心数只需要设置为 1-4 即可，不然可能会拖慢宿主机的运行。</p><p>终上所述，核心数并无太大影响，读者自己有数就行。</p></section><section><h3>是否需要关闭 3D 图形加速<a href="#是否需要关闭-3d-图形加速"><span>#</span></a></h3><p>网上还有很多的教程说需要关闭 3D 图形加速，但是有很多软件的运行是需要这个功能的比如 Gazebo，不然就会变得非常卡。</p><p>关闭 3D 图形加速后，确实能够比较明显的感觉到在虚拟机中打字时的跟手性有改善，但是也还没有到那种可以忍受的地步（至少对我而言）。</p><p>在 <code>*.vmx</code> 文件中添加了 <code>keyboard.vusb.enable = "TRUE"</code> 后，我开启了 3D 图形加速，笔者实测打字的跟手性依旧非常好，同上的效果，只是虚拟机本身有一点卡顿，但完全在可接受的范围内。</p><p>综上所述，无需关闭 3D 图形加速。不过开启了 3D 加速确实会影响虚拟机的运行，如果你的工作不需要大量 3D 渲染的话，笔者还是建议关闭 3D 图形加速功能。</p></section></section>
<section><h2>参考资料<a href="#参考资料"><span>#</span></a></h2><ul>
<li><a href="https://community.broadcom.com/vmware-cloud-foundation/discussion/ws-1761-keyboard-lag-with-ubuntu-guest" target="_blank">WS 17.6.1 keyboard lag with Ubuntu guest</a></li>
<li><a href="https://www.reddit.com/r/vmware/comments/15kx569/experiencing_keyboard_laginput_delay_on_any_linux/" target="_blank">Experiencing keyboard lag/input delay on any Linux Distribution</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://bfmhno3.github.io/posts/learning-fgets-and-strcspn/</id>
      <title type="text">学习 fgets 函数和 strcspn 函数</title>
      <published>2023-08-20T15:44:31.000Z</published>
      <updated>2023-08-20T15:44:31.000Z</updated>
      <author><name>bfmhno3</name></author>
      <link rel="alternate" href="https://bfmhno3.github.io/posts/learning-fgets-and-strcspn/"/>
      <summary type="text">本文记录了在 C 语言刷题过程中遇到的字符串输入问题。详细介绍了 fgets 函数和 strcspn 函数的用法，用于解决 scanf 无法读取带空格字符串以及换行符处理的问题，并补充了 scanf(&quot;%[^\n]&quot;) 这一更简洁的替代方案。</summary>
      <content type="html"><![CDATA[<section><h2>0. 为什么要专门发布一篇文章来说明这两个函数？<a href="#0-为什么要专门发布一篇文章来说明这两个函数"><span>#</span></a></h2><p>原因是今晚上在做牛客网的题目的时候，遇到了这么一个<a href="https://www.nowcoder.com/questionTerminal/cc57022cb4194697ac30bcb566aeb47b?f=discussion&amp;toCommentId=16721626" target="_blank">问题</a>。</p><p>虽然这道题对大多数人来说都算是比较简单的题目，但是对于我这么一个小白还是比较难写的。最开始我是这么写的代码：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>void</span><span> </span><span>reverse</span><span>(</span><span>char*</span><span><span> </span><span>arr</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>char*</span><span> left </span><span>=</span><span> arr;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>char*</span><span> right </span><span>=</span><span> </span><span>&amp;</span><span>arr</span><span>[</span><span>strlen</span><span>(arr) </span><span>-</span><span> </span><span>1</span><span>];</span></div></div><div><div><div>6</div></div><div><span>    </span><span>while</span><span>(left </span><span>&lt;</span><span> right) {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>char</span><span> mid </span><span>=</span><span> </span><span>*</span><span>left;</span></div></div><div><div><div>8</div></div><div><span>        </span><span>*</span><span>left </span><span>=</span><span> </span><span>*</span><span>right;</span></div></div><div><div><div>9</div></div><div><span>        </span><span>*</span><span>right </span><span>=</span><span> mid;</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>left</span><span>++</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>right</span><span>--</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>    </span><span>for</span><span>(</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>strlen</span><span>(arr); i</span><span>++</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>printf</span><span>(</span><span>"</span><span>%c</span><span>"</span><span>, </span><span>arr</span><span>[i]);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>char</span><span> </span><span>arr</span><span>[</span><span>10000</span><span>];</span><span>//之所以创建这么大一个数组是因为题目中的示例10非常长</span></div></div><div><div><div>20</div></div><div><span>    </span><span>scanf</span><span>(</span><span>"</span><span>%s</span><span>"</span><span>, arr);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>reverse</span><span>(arr);</span></div></div><div><div><div>22</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>23</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>但是这样写代码连题目中的示例 1：输入：<code>I am a student</code>；输出：<code>tneduts a ma I</code> 都没有通过，最终只输出了一个 <code>I</code>。</p><p>这是为什么呢？我思索了半天才明白是我的这一行代码 <code>scanf("%s", arr);</code> 有问题，就是因为 <code>scanf("%s", arr)</code> 会读取一个字符串，但会在遇到空格、制表符或换行符时停止。换句话说，<strong>它只会读取输入字符串中的第一个单词，然后停止</strong>。</p><p>例如，如果你输入 “Hello World”，<code>scanf("%s", str)</code> 会将 “Hello” 作为一个字符串读取，而 “World” 则会留在输入流中等待后续读取。</p><p>所以示例输入的 <code>I am a student</code> 只有 <code>I</code> 进入了 <code>arr[10000]</code> 中，剩下的字符串仍然留在输入流中等待下一次读取。所以最终自然而然也就只会输出 <code>I</code>。</p><p>知道了问题的原因自然就要想办法解决问题，但是说实话，对于小编这种很笨的学生来说，几乎是不可能的。所以，我就去求助了 ChatGPT。ChatGPT 给出的解决办法是使用 <code>fgets</code> 函数，但是我真就第一次听这个函数，也完全不懂这个函数该如何使用，于是我就准备写下这篇文章来记录我的欠缺。</p><p>那么下面就开始正题：</p></section>
<section><h2>1. <code>fgets</code>函数<a href="#1-fgets函数"><span>#</span></a></h2><section><h3>1.1 函数介绍<a href="#11-函数介绍"><span>#</span></a></h3><p><code>fgets</code> 是 C 语言标准库 <code>stdio</code> 中的一个函数，用于从文件流（通常是标准输入流 <code>stdin</code> 或其他文件流）中读取一行字符串。它的主要作用是读取指定长度的字符，并且<strong>会保留换行符（如果存在的话）</strong>。</p><p>函数原型：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>char</span><span> </span><span>*</span><span>fgets</span><span>(</span><span>char</span><span> </span><span>*</span><span><span>str</span><span>,</span><span> </span></span><span>int</span><span><span> </span><span>n</span><span>,</span><span> FILE </span></span><span>*</span><span><span>stream</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>1.2 参数说明<a href="#12-参数说明"><span>#</span></a></h3><ul>
<li><code>str</code>：一个指向字符数组的指针，用于存储读取的字符串。</li>
<li><code>n</code>：要读取的最大字符数（包括字符串结尾的空字符 \0），通常为字符数组的长度。</li>
<li><code>stream</code>：文件流指针，通常是 <code>stdin</code>（标准输入流）或其他文件指针。</li>
<li><code>fgets</code> 会读取指定长度的字符，直到遇到换行符（<strong>包括换行符在内</strong>），或者读取了 <code>n-1</code> 个字符（包括字符串结尾的空字符 <code>\0</code> ），或者到达文件末尾为止。它会将读取的字符串存储到 <code>str</code> 指向的字符数组中，并在字符串的末尾添加一个空字符 <code>\0</code>，以表示字符串的结束。</li>
</ul></section><section><h3>1.3 注意事项<a href="#13-注意事项"><span>#</span></a></h3><ul>
<li><code>fgets</code> 会保留字符串中的换行符，所以如果不需要换行符，可以使用字符串处理函数（如 <code>strcspn</code>）将其替换为空字符 <code>\0</code>。</li>
<li>如果输入的字符串的长度超过了指定的 <code>n-1</code>，<code>fgets</code> 只会读取 <code>n-1</code> 个字符，其余字符会留在输入流中，可能影响后续的输入。</li>
<li><code>fgets</code> 在成功读取字符串时返回 <code>str</code>，在到达文件末尾或发生错误时返回 <code>NULL</code>。</li>
</ul></section><section><h3>1.4 使用示例<a href="#14-使用示例"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string.h&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>char</span><span> </span><span>str</span><span>[</span><span>100</span><span>];</span><span> // 最多读取 99 个字符（包括结尾的空字符）</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>    </span><span>printf</span><span>(</span><span>"请输入一个字符串："</span><span>);</span></div></div><div><div><div>8</div></div><div><span>    </span><span>fgets</span><span>(str, </span><span>sizeof</span><span>(str), stdin);</span><span> // 从标准输入读取一行字符串</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>str</span><span>[</span><span>strcspn</span><span>(str, </span><span>"</span><span>\n</span><span>"</span><span>)] </span><span>=</span><span> </span><span>'</span><span>\0</span><span>'</span><span>;</span><span> // 去掉换行符（如果有）</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span>printf</span><span>(</span><span>"你输入的字符串是：</span><span>%s</span><span>\n</span><span>"</span><span>, str);</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>15</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>1.5 题目代码修改<a href="#15-题目代码修改"><span>#</span></a></h3><p>所以我原本写的代码中的 <code>scanf("%s", arr)</code> 应该改成 <code>fgets(arr, sizeof(arr), stdin)</code>。</p></section><section><h3>是否这么写代码我就能通过测试了呢？<a href="#是否这么写代码我就能通过测试了呢"><span>#</span></a></h3><p>当然不是，事实上代码最终的输出结果是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tneduts a ma I</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>所以最后的输出还是错误了，多输出了一个换行符 <code>\n</code>。</p><p>事实上这里有很多种方法可以去除原来字符串末尾的换行符，但我最终选择了使用 <code>strcspn</code> 函数来处理,所以下面我会介绍 <code>strcspn</code> 函数。</p></section></section>
<section><h2>2. <code>strcspn</code>函数<a href="#2-strcspn函数"><span>#</span></a></h2><section><h3>2.1 函数介绍<a href="#21-函数介绍"><span>#</span></a></h3><p><code>strcspn</code> 是 C 语言标准库 <code>string</code> 中的一个函数，用于查找一个字符串中第一次出现指定字符集合中的任何字符的位置。<strong>它返回的是第一个匹配字符的索引位置</strong>。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>size_t</span><span> </span><span>strcspn</span><span>(</span><span>const</span><span> </span><span>char</span><span> </span><span>*</span><span><span>str1</span><span>,</span><span> </span></span><span>const</span><span> </span><span>char</span><span> </span><span>*</span><span><span>str2</span><span>);</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2.2 参数说明<a href="#22-参数说明"><span>#</span></a></h3><ul>
<li><code>str1</code>：要查找的字符串。</li>
<li><code>str2</code>：一个包含字符集合的字符串，<code>strcspn</code> 会查找这个字符集合中的字符在 <code>str1</code> 中的第一次出现。</li>
<li>strcspn 会从 <code>str1</code> 的起始位置开始查找，直到遇到 <code>str2</code> 中的任何字符，或者到达字符串末尾。<strong>它返回第一个匹配字符的索引位置，如果未找到匹配字符，则返回 <code>str1</code> 的长度</strong>。</li>
</ul></section><section><h3>2.3 注意事项<a href="#23-注意事项"><span>#</span></a></h3><ul>
<li><code>strcspn</code> 返回的是 <code>size_t</code> 类型，它是无符号整数类型。如果未找到匹配字符，返回值会等于字符串的长度。因此，在使用返回值时，应该<strong>注意数据类型和范围</strong>。</li>
</ul></section><section><h3>2.4 使用示例<a href="#24-使用示例"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>2</div></div><div><span>#include</span><span> </span><span>&lt;string.h&gt;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>int</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>5</div></div><div><span>    </span><span>char</span><span> str</span><span>[]</span><span> </span><span>=</span><span> </span><span>"Hello, World!"</span><span>;</span></div></div><div><div><div>6</div></div><div><span>    </span><span>char</span><span> chars</span><span>[]</span><span> </span><span>=</span><span> </span><span>" ,"</span><span>;</span><span> // 字符集合包含空格和逗号</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>    </span><span>size_t</span><span> index </span><span>=</span><span> </span><span>strcspn</span><span>(str, chars);</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span> (index </span><span>&lt;</span><span> </span><span>strlen</span><span>(str)) {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>printf</span><span>(</span><span>"第一个匹配字符在位置：</span><span>%zu</span><span>\n</span><span>"</span><span>, index);</span></div></div><div><div><div>12</div></div><div><span>        </span><span>printf</span><span>(</span><span>"匹配字符是：</span><span>%c</span><span>\n</span><span>"</span><span>, </span><span>str</span><span>[index]);</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>} </span><span>else</span><span> {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>printf</span><span>(</span><span>"未找到匹配字符</span><span>\n</span><span>"</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section></section>
<section><h2>3. 最终代码<a href="#3-最终代码"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#include</span><span> </span><span>&lt;stdio.h&gt;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>void</span><span> </span><span>reverse</span><span>(</span><span>char*</span><span><span> </span><span>arr</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>    </span><span>char*</span><span> left </span><span>=</span><span> arr;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>char*</span><span> right </span><span>=</span><span> </span><span>&amp;</span><span>arr</span><span>[</span><span>strlen</span><span>(arr) </span><span>-</span><span> </span><span>1</span><span>];</span></div></div><div><div><div>6</div></div><div><span>    </span><span>while</span><span>(left </span><span>&lt;</span><span> right) {</span></div></div><div><div><div>7</div></div><div><span>        </span><span>char</span><span> mid </span><span>=</span><span> </span><span>*</span><span>left;</span></div></div><div><div><div>8</div></div><div><span>        </span><span>*</span><span>left </span><span>=</span><span> </span><span>*</span><span>right;</span></div></div><div><div><div>9</div></div><div><span>        </span><span>*</span><span>right </span><span>=</span><span> mid;</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>left</span><span>++</span><span>;</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>right</span><span>--</span><span>;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>    </span><span>for</span><span>(</span><span>int</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span>&lt;</span><span> </span><span>strlen</span><span>(arr); i</span><span>++</span><span>) {</span></div></div><div><div><div>14</div></div><div><span>        </span><span>printf</span><span>(</span><span>"</span><span>%c</span><span>"</span><span>, </span><span>arr</span><span>[i]);</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>int</span><span> </span><span>main</span><span>(</span><span>void</span><span>) {</span></div></div><div><div><div>19</div></div><div><span>    </span><span>char</span><span> </span><span>arr</span><span>[</span><span>100000</span><span>];</span></div></div><div><div><div>20</div></div><div><span>    </span><span>fgets</span><span>(arr, </span><span>sizeof</span><span>(arr), stdin);</span></div></div><div><div><div>21</div></div><div><span>    </span><span>arr</span><span>[</span><span>strcspn</span><span>(arr, </span><span>"</span><span>\n</span><span>"</span><span>)] </span><span>=</span><span> </span><span>'</span><span>\0</span><span>'</span><span>;</span></div></div><div><div><div>22</div></div><div><span>    </span><span>reverse</span><span>(arr);</span></div></div><div><div><div>23</div></div><div><span>    </span><span>return</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>24</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>4. 题后反思<a href="#4-题后反思"><span>#</span></a></h2><p>最终的代码运行成功后，我总有一种大材小用的感觉，我就思考能不能用 <code>scanf</code> 函数来实现呢？经过与 ChatGPT 的一番讨论后，我最终找到了另一种更为简单的代码：那就是使用 <code>%[^\n]</code> 来格式化输入。</p></section>
<section><h2>5. <code>%[^\n]</code><a href="#5-n"><span>#</span></a></h2><section><h3>5.1 介绍与解释<a href="#51-介绍与解释"><span>#</span></a></h3><p><code>%[^\n]</code> 是 C 语言中用于格式化输入的格式字符串之一。它用于 <code>scanf</code> 函数中，用于读取一行字符串，直到遇到换行符为止。</p><p>让我解释一下 <code>%[^\n]</code> 的含义：</p><ul>
<li><code>%[</code>：这部分表示开始读取一个自定义字符集。在方括号内放置你想要读取的字符的集合。</li>
<li><code>^</code>：在方括号内的开头，<code>^</code> 表示反转字符集。即，<code>%[^\n]</code> 表示读取除换行符外的所有字符。</li>
<li><code>\n</code>：这是一个转义序列，表示换行符。</li>
</ul><p>所以，<code>%[^\n]</code> 的意思是，<strong>读取除换行符之外的所有字符，直到遇到换行符为止</strong>，并且<strong>不会读取换行符</strong>！！！</p></section></section>
<section><h2>5.2 再次修改代码<a href="#52-再次修改代码"><span>#</span></a></h2><p>所以，我们最终可以将代码从 <code>fgets(arr, sizeof(arr), stdin); arr[strcspn(arr, "\n")];</code> 修改成 <code>scanf("%[^\n]");</code>。</p></section>
<section><h2>6. 感悟<a href="#6-感悟"><span>#</span></a></h2><p>通过这到题目，我深刻的感受到了什么叫做“条条大路通罗马”。作为小白的我，还有很多的知识去学习、去探索、去使用。</p></section>]]></content>
    </entry>
</feed>
